feat(loyalty): align program view, edit, and analytics pages across all frontends
Some checks failed
Some checks failed
Standardize naming (Program for view/edit, Analytics for stats), create shared read-only program-view partial, fix admin edit field population bug (14 missing fields), add store Program menu item, and rename merchant Overview→Program, Settings→Analytics. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -111,58 +111,8 @@
|
||||
</div>
|
||||
|
||||
<!-- Program Configuration -->
|
||||
<div x-show="program" class="px-4 py-3 mb-8 bg-white rounded-lg shadow-md dark:bg-gray-800">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200">
|
||||
<span x-html="$icon('cog', 'inline w-5 h-5 mr-2')"></span>
|
||||
Program Configuration
|
||||
</h3>
|
||||
<div class="flex items-center gap-2">
|
||||
<button @click="toggleActive()"
|
||||
class="flex items-center px-3 py-1.5 text-sm font-medium rounded-lg border transition-colors"
|
||||
:class="program?.is_active
|
||||
? 'text-orange-600 border-orange-300 hover:bg-orange-50 dark:text-orange-400 dark:border-orange-700 dark:hover:bg-orange-900/20'
|
||||
: 'text-green-600 border-green-300 hover:bg-green-50 dark:text-green-400 dark:border-green-700 dark:hover:bg-green-900/20'">
|
||||
<span x-html="$icon(program?.is_active ? 'pause' : 'play', 'w-4 h-4 mr-1')"></span>
|
||||
<span x-text="program?.is_active ? 'Deactivate' : 'Activate'"></span>
|
||||
</button>
|
||||
<button @click="confirmDeleteProgram()"
|
||||
class="flex items-center px-3 py-1.5 text-sm font-medium text-red-600 border border-red-300 rounded-lg hover:bg-red-50 dark:text-red-400 dark:border-red-700 dark:hover:bg-red-900/20">
|
||||
<span x-html="$icon('trash', 'w-4 h-4 mr-1')"></span>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase mb-2">Program Name</p>
|
||||
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="program?.display_name || program?.card_name || '-'">-</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase mb-2">Points Per Euro</p>
|
||||
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="program?.points_per_euro || 1">-</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase mb-2">Welcome Bonus</p>
|
||||
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="program?.welcome_bonus_points ? program.welcome_bonus_points + ' points' : 'None'">-</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase mb-2">Minimum Redemption</p>
|
||||
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="program?.minimum_redemption_points ? program.minimum_redemption_points + ' points' : 'None'">-</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase mb-2">Points Expiration</p>
|
||||
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="program?.points_expiration_days ? program.points_expiration_days + ' days of inactivity' : 'Never'">-</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase mb-2">Status</p>
|
||||
<span class="inline-flex items-center px-2 py-1 text-xs font-semibold leading-tight rounded-full"
|
||||
:class="program?.is_active ? 'text-green-700 bg-green-100 dark:bg-green-700 dark:text-green-100' : 'text-gray-700 bg-gray-100 dark:text-gray-100 dark:bg-gray-700'">
|
||||
<span x-text="program?.is_active ? 'Active' : 'Inactive'"></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% set show_edit_button = false %}
|
||||
{% include "loyalty/shared/program-view.html" %}
|
||||
|
||||
<!-- No Program Notice -->
|
||||
<div x-show="!program" class="px-4 py-3 mb-8 bg-yellow-50 border border-yellow-200 rounded-lg dark:bg-yellow-900/20 dark:border-yellow-800">
|
||||
|
||||
@@ -1,24 +1,17 @@
|
||||
{# app/modules/loyalty/templates/loyalty/merchant/overview.html #}
|
||||
{# app/modules/loyalty/templates/loyalty/merchant/analytics.html #}
|
||||
{% extends "merchant/base.html" %}
|
||||
|
||||
{% block title %}Loyalty Overview{% endblock %}
|
||||
{% block title %}Loyalty Analytics{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div x-data="merchantLoyaltyOverview()">
|
||||
<div x-data="merchantLoyaltyAnalytics()">
|
||||
|
||||
<!-- Page Header -->
|
||||
<div class="mb-8 mt-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-gray-100">Loyalty Overview</h2>
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-gray-100">Loyalty Analytics</h2>
|
||||
<p class="mt-1 text-gray-500 dark:text-gray-400">Loyalty program statistics across all your stores.</p>
|
||||
</div>
|
||||
<template x-if="stats.program_id">
|
||||
<a href="/merchants/loyalty/settings"
|
||||
class="flex items-center px-4 py-2 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700">
|
||||
<span x-html="$icon('cog', 'w-4 h-4 mr-2')"></span>
|
||||
Edit Program
|
||||
</a>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- No Program State -->
|
||||
@@ -27,9 +20,9 @@
|
||||
<span x-html="$icon('gift', 'w-12 h-12 mx-auto text-gray-400')"></span>
|
||||
<h3 class="mt-4 text-lg font-medium text-gray-900 dark:text-white">No Loyalty Program</h3>
|
||||
<p class="mt-2 text-gray-500 dark:text-gray-400">
|
||||
Your loyalty program hasn't been set up yet. Create one to start rewarding your customers.
|
||||
Set up a loyalty program to see analytics here.
|
||||
</p>
|
||||
<a href="/merchants/loyalty/settings"
|
||||
<a href="/merchants/loyalty/program/edit"
|
||||
class="inline-flex items-center mt-4 px-6 py-2 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700">
|
||||
<span x-html="$icon('plus', 'w-4 h-4 mr-2')"></span>
|
||||
Create Program
|
||||
@@ -120,10 +113,16 @@
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script>
|
||||
function merchantLoyaltyOverview() {
|
||||
function merchantLoyaltyAnalytics() {
|
||||
return {
|
||||
...data(),
|
||||
currentPage: 'loyalty-analytics',
|
||||
loading: false,
|
||||
stats: {{ loyalty_stats | tojson }},
|
||||
|
||||
init() {
|
||||
this.loadMenuConfig();
|
||||
},
|
||||
};
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,42 @@
|
||||
{# app/modules/loyalty/templates/loyalty/merchant/program-edit.html #}
|
||||
{% extends "merchant/base.html" %}
|
||||
{% from 'shared/macros/headers.html' import page_header_flex %}
|
||||
{% from 'shared/macros/alerts.html' import loading_state, error_state %}
|
||||
{% from 'shared/macros/modals.html' import confirm_modal %}
|
||||
|
||||
{% block title %}Loyalty Settings{% endblock %}
|
||||
|
||||
{% block alpine_data %}merchantLoyaltySettings(){% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% call page_header_flex(title='Loyalty Program Settings', subtitle='Configure your loyalty program') %}{% endcall %}
|
||||
|
||||
{{ loading_state('Loading settings...') }}
|
||||
{{ error_state('Error loading settings') }}
|
||||
|
||||
<div x-show="!loading">
|
||||
<form @submit.prevent="saveSettings">
|
||||
{% set show_delete = true %}
|
||||
{% set show_status = true %}
|
||||
{% set cancel_url = '/merchants/loyalty/program' %}
|
||||
{% include "loyalty/shared/program-form.html" %}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Delete Confirmation Modal -->
|
||||
{{ confirm_modal(
|
||||
'deleteProgramModal',
|
||||
'Delete Loyalty Program',
|
||||
'This will permanently delete your loyalty program and all associated data (cards, transactions, rewards). This action cannot be undone.',
|
||||
'deleteProgram()',
|
||||
'showDeleteModal',
|
||||
'Delete Program',
|
||||
'Cancel',
|
||||
'danger'
|
||||
) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script defer src="{{ url_for('loyalty_static', path='shared/js/loyalty-program-form.js') }}"></script>
|
||||
<script defer src="{{ url_for('loyalty_static', path='merchant/js/loyalty-settings.js') }}"></script>
|
||||
{% endblock %}
|
||||
67
app/modules/loyalty/templates/loyalty/merchant/program.html
Normal file
67
app/modules/loyalty/templates/loyalty/merchant/program.html
Normal file
@@ -0,0 +1,67 @@
|
||||
{# app/modules/loyalty/templates/loyalty/merchant/program.html #}
|
||||
{% extends "merchant/base.html" %}
|
||||
|
||||
{% block title %}Loyalty Program{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div x-data="merchantLoyaltyProgram()">
|
||||
|
||||
<!-- Page Header -->
|
||||
<div class="mb-8 mt-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-gray-100">Loyalty Program</h2>
|
||||
<p class="mt-1 text-gray-500 dark:text-gray-400">Your loyalty program configuration.</p>
|
||||
</div>
|
||||
<template x-if="stats.program_id">
|
||||
<a href="/merchants/loyalty/program/edit"
|
||||
class="flex items-center px-4 py-2 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700">
|
||||
<span x-html="$icon('pencil', 'w-4 h-4 mr-2')"></span>
|
||||
Edit Program
|
||||
</a>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- No Program State -->
|
||||
<template x-if="!stats.program_id && !loading">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-12 text-center">
|
||||
<span x-html="$icon('gift', 'w-12 h-12 mx-auto text-gray-400')"></span>
|
||||
<h3 class="mt-4 text-lg font-medium text-gray-900 dark:text-white">No Loyalty Program</h3>
|
||||
<p class="mt-2 text-gray-500 dark:text-gray-400">
|
||||
Your loyalty program hasn't been set up yet. Create one to start rewarding your customers.
|
||||
</p>
|
||||
<a href="/merchants/loyalty/program/edit"
|
||||
class="inline-flex items-center mt-4 px-6 py-2 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700">
|
||||
<span x-html="$icon('plus', 'w-4 h-4 mr-2')"></span>
|
||||
Create Program
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Program View -->
|
||||
<template x-if="stats.program_id || loading">
|
||||
<div>
|
||||
{% set edit_url = '/merchants/loyalty/program/edit' %}
|
||||
{% set show_edit_button = false %}
|
||||
{% include "loyalty/shared/program-view.html" %}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script>
|
||||
function merchantLoyaltyProgram() {
|
||||
return {
|
||||
...data(),
|
||||
currentPage: 'loyalty-program',
|
||||
loading: false,
|
||||
stats: {{ loyalty_stats | tojson }},
|
||||
program: {{ (loyalty_stats.program if loyalty_stats.program else 'null') | tojson }},
|
||||
|
||||
init() {
|
||||
this.loadMenuConfig();
|
||||
},
|
||||
};
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,189 +0,0 @@
|
||||
{# app/modules/loyalty/templates/loyalty/merchant/settings.html #}
|
||||
{% extends "merchant/base.html" %}
|
||||
{% from 'shared/macros/headers.html' import page_header_flex %}
|
||||
{% from 'shared/macros/alerts.html' import loading_state, error_state %}
|
||||
|
||||
{% block title %}Loyalty Settings{% endblock %}
|
||||
|
||||
{% block alpine_data %}merchantLoyaltySettings(){% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% call page_header_flex(title='Loyalty Program Settings', subtitle='Configure your loyalty program') %}{% endcall %}
|
||||
|
||||
{{ loading_state('Loading settings...') }}
|
||||
{{ error_state('Error loading settings') }}
|
||||
|
||||
<div x-show="!loading">
|
||||
<form @submit.prevent="saveSettings">
|
||||
<!-- Points Configuration -->
|
||||
<div class="px-4 py-5 mb-6 bg-white rounded-lg shadow-md dark:bg-gray-800">
|
||||
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
|
||||
<span x-html="$icon('currency-dollar', 'inline w-5 h-5 mr-2')"></span>
|
||||
Points Configuration
|
||||
</h3>
|
||||
<div class="grid gap-6 md:grid-cols-2">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Points per EUR spent</label>
|
||||
<input type="number" min="1" max="100" x-model.number="settings.points_per_euro"
|
||||
class="w-full px-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none dark:bg-gray-700 dark:text-gray-300">
|
||||
<p class="mt-1 text-xs text-gray-500">1 EUR = <span x-text="settings.points_per_euro || 1"></span> point(s)</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Welcome Bonus Points</label>
|
||||
<input type="number" min="0" x-model.number="settings.welcome_bonus_points"
|
||||
class="w-full px-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none dark:bg-gray-700 dark:text-gray-300">
|
||||
<p class="mt-1 text-xs text-gray-500">Bonus points awarded on enrollment</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Minimum Redemption Points</label>
|
||||
<input type="number" min="1" x-model.number="settings.minimum_redemption_points"
|
||||
class="w-full px-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none dark:bg-gray-700 dark:text-gray-300">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Points Expiration (days)</label>
|
||||
<input type="number" min="0" x-model.number="settings.points_expiration_days"
|
||||
placeholder="0 = never expire"
|
||||
class="w-full px-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none dark:bg-gray-700 dark:text-gray-300">
|
||||
<p class="mt-1 text-xs text-gray-500">Days of inactivity before points expire (0 = never)</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rewards Configuration -->
|
||||
<div class="px-4 py-5 mb-6 bg-white rounded-lg shadow-md dark:bg-gray-800">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200">
|
||||
<span x-html="$icon('gift', 'inline w-5 h-5 mr-2')"></span>
|
||||
Redemption Rewards
|
||||
</h3>
|
||||
<button type="button" @click="addReward()"
|
||||
class="flex items-center px-3 py-1 text-sm text-purple-600 hover:text-purple-700">
|
||||
<span x-html="$icon('plus', 'w-4 h-4 mr-1')"></span>
|
||||
Add Reward
|
||||
</button>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<template x-if="settings.points_rewards.length === 0">
|
||||
<p class="text-gray-500 dark:text-gray-400 text-sm">No rewards configured. Add a reward to allow customers to redeem points.</p>
|
||||
</template>
|
||||
<template x-for="(reward, index) in settings.points_rewards" :key="index">
|
||||
<div class="flex items-start gap-4 p-4 border border-gray-200 dark:border-gray-700 rounded-lg">
|
||||
<div class="flex-1 grid gap-4 md:grid-cols-3">
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 mb-1">Reward Name</label>
|
||||
<input type="text" x-model="reward.name" placeholder="e.g., EUR5 off"
|
||||
class="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg dark:bg-gray-700 dark:text-gray-300">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 mb-1">Points Required</label>
|
||||
<input type="number" min="1" x-model.number="reward.points_required"
|
||||
class="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg dark:bg-gray-700 dark:text-gray-300">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 mb-1">Description</label>
|
||||
<input type="text" x-model="reward.description" placeholder="Optional description"
|
||||
class="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg dark:bg-gray-700 dark:text-gray-300">
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" @click="removeReward(index)"
|
||||
class="text-red-500 hover:text-red-700 p-2">
|
||||
<span x-html="$icon('trash', 'w-5 h-5')"></span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Branding -->
|
||||
<div class="px-4 py-5 mb-6 bg-white rounded-lg shadow-md dark:bg-gray-800">
|
||||
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
|
||||
<span x-html="$icon('paint-brush', 'inline w-5 h-5 mr-2')"></span>
|
||||
Branding
|
||||
</h3>
|
||||
<div class="grid gap-6 md:grid-cols-2">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Card Name</label>
|
||||
<input type="text" x-model="settings.card_name" placeholder="e.g., VIP Rewards"
|
||||
class="w-full px-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none dark:bg-gray-700 dark:text-gray-300">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Primary Color</label>
|
||||
<div class="flex items-center gap-3">
|
||||
<input type="color" x-model="settings.card_color"
|
||||
class="w-12 h-10 rounded cursor-pointer">
|
||||
<input type="text" x-model="settings.card_color" pattern="^#[0-9A-Fa-f]{6}$"
|
||||
class="flex-1 px-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg dark:bg-gray-700 dark:text-gray-300">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status -->
|
||||
<div class="px-4 py-5 mb-6 bg-white rounded-lg shadow-md dark:bg-gray-800">
|
||||
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
|
||||
<span x-html="$icon('power', 'inline w-5 h-5 mr-2')"></span>
|
||||
Program Status
|
||||
</h3>
|
||||
<label class="flex items-center justify-between p-4 border border-gray-200 dark:border-gray-600 rounded-lg">
|
||||
<div>
|
||||
<p class="font-medium text-gray-700 dark:text-gray-300">Program Active</p>
|
||||
<p class="text-sm text-gray-500">When disabled, customers cannot earn or redeem points</p>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<input type="checkbox" x-model="settings.is_active" class="sr-only peer">
|
||||
<div @click="settings.is_active = !settings.is_active"
|
||||
class="w-11 h-6 bg-gray-200 rounded-full cursor-pointer peer-checked:bg-purple-600 dark:bg-gray-700"
|
||||
:class="settings.is_active ? 'bg-purple-600' : ''">
|
||||
<div class="absolute top-[2px] left-[2px] bg-white w-5 h-5 rounded-full transition-transform"
|
||||
:class="settings.is_active ? 'translate-x-5' : ''"></div>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<template x-if="!isNewProgram">
|
||||
<button type="button" @click="confirmDelete()"
|
||||
class="flex items-center px-4 py-2 text-sm font-medium text-red-600 border border-red-300 rounded-lg hover:bg-red-50 dark:text-red-400 dark:border-red-700 dark:hover:bg-red-900/20">
|
||||
<span x-html="$icon('trash', 'w-4 h-4 mr-2')"></span>
|
||||
Delete Program
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
<button type="submit" :disabled="saving"
|
||||
class="flex items-center px-6 py-2 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700 disabled:opacity-50">
|
||||
<span x-show="saving" x-html="$icon('spinner', 'w-4 h-4 mr-2 animate-spin')"></span>
|
||||
<span x-text="saving ? 'Saving...' : (isNewProgram ? 'Create Program' : 'Save Settings')"></span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Delete Confirmation Modal -->
|
||||
<div x-show="showDeleteModal" x-cloak
|
||||
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl p-6 max-w-md w-full mx-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">Delete Loyalty Program</h3>
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-4">
|
||||
This will permanently delete your loyalty program and all associated data (cards, transactions, rewards).
|
||||
This action cannot be undone.
|
||||
</p>
|
||||
<div class="flex justify-end gap-3">
|
||||
<button @click="showDeleteModal = false"
|
||||
class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||
Cancel
|
||||
</button>
|
||||
<button @click="deleteProgram()" :disabled="deleting"
|
||||
class="px-4 py-2 text-sm font-medium text-white bg-red-600 rounded-lg hover:bg-red-700 disabled:opacity-50">
|
||||
<span x-text="deleting ? 'Deleting...' : 'Delete Program'"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script defer src="{{ url_for('loyalty_static', path='merchant/js/loyalty-settings.js') }}"></script>
|
||||
{% endblock %}
|
||||
228
app/modules/loyalty/templates/loyalty/shared/program-view.html
Normal file
228
app/modules/loyalty/templates/loyalty/shared/program-view.html
Normal file
@@ -0,0 +1,228 @@
|
||||
{# app/modules/loyalty/templates/loyalty/shared/program-view.html #}
|
||||
{#
|
||||
Read-only program configuration view partial.
|
||||
Include with:
|
||||
{% include "loyalty/shared/program-view.html" %}
|
||||
|
||||
Expected Jinja2 variables (set before include):
|
||||
- edit_url (str) — href for "Edit Program" button
|
||||
- show_edit_button (bool, default true) — whether to show the edit button
|
||||
|
||||
Expected Alpine.js state on the parent component:
|
||||
- program.* — full program object (from API or stats.program)
|
||||
#}
|
||||
|
||||
<div x-show="program" class="px-4 py-3 mb-8 bg-white rounded-lg shadow-md dark:bg-gray-800">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200">
|
||||
<span x-html="$icon('cog', 'inline w-5 h-5 mr-2')"></span>
|
||||
Program Configuration
|
||||
</h3>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium capitalize"
|
||||
:class="{
|
||||
'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300': program?.loyalty_type === 'points',
|
||||
'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300': program?.loyalty_type === 'stamps',
|
||||
'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300': program?.loyalty_type === 'hybrid'
|
||||
}"
|
||||
x-text="program?.loyalty_type || 'unknown'"></span>
|
||||
<span class="inline-flex items-center px-2 py-1 text-xs font-semibold leading-tight rounded-full"
|
||||
:class="program?.is_active ? 'text-green-700 bg-green-100 dark:bg-green-700 dark:text-green-100' : 'text-gray-700 bg-gray-100 dark:text-gray-100 dark:bg-gray-700'">
|
||||
<span x-text="program?.is_active ? 'Active' : 'Inactive'"></span>
|
||||
</span>
|
||||
{% if show_edit_button is not defined or show_edit_button %}
|
||||
<a href="{{ edit_url }}"
|
||||
class="flex items-center px-3 py-1.5 text-sm font-medium text-purple-600 border border-purple-300 rounded-lg hover:bg-purple-50 dark:text-purple-400 dark:border-purple-700 dark:hover:bg-purple-900/20">
|
||||
<span x-html="$icon('pencil', 'w-4 h-4 mr-1')"></span>
|
||||
Edit
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Program Info -->
|
||||
<div class="grid gap-6 md:grid-cols-2 lg:grid-cols-3 mb-6">
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase mb-2">Program Name</p>
|
||||
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="program?.display_name || '-'">-</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase mb-2">Card Name</p>
|
||||
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="program?.card_name || '-'">-</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stamps Config -->
|
||||
<template x-if="program?.loyalty_type === 'stamps' || program?.loyalty_type === 'hybrid'">
|
||||
<div class="mb-6">
|
||||
<h4 class="text-sm font-semibold text-gray-600 dark:text-gray-400 uppercase mb-3 flex items-center">
|
||||
<span x-html="$icon('star', 'inline w-4 h-4 mr-1')"></span>
|
||||
Stamps Configuration
|
||||
</h4>
|
||||
<div class="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mb-1">Stamps Target</p>
|
||||
<p class="text-sm font-medium text-gray-700 dark:text-gray-300" x-text="program?.stamps_target || '-'">-</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mb-1">Reward Description</p>
|
||||
<p class="text-sm font-medium text-gray-700 dark:text-gray-300" x-text="program?.stamps_reward_description || '-'">-</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mb-1">Reward Value</p>
|
||||
<p class="text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
x-text="program?.stamps_reward_value_cents ? '€' + (program.stamps_reward_value_cents / 100).toFixed(2) : '-'">-</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Points Config -->
|
||||
<template x-if="program?.loyalty_type === 'points' || program?.loyalty_type === 'hybrid'">
|
||||
<div class="mb-6">
|
||||
<h4 class="text-sm font-semibold text-gray-600 dark:text-gray-400 uppercase mb-3 flex items-center">
|
||||
<span x-html="$icon('currency-dollar', 'inline w-4 h-4 mr-1')"></span>
|
||||
Points Configuration
|
||||
</h4>
|
||||
<div class="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mb-1">Points per EUR</p>
|
||||
<p class="text-sm font-medium text-gray-700 dark:text-gray-300" x-text="program?.points_per_euro || 1">-</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mb-1">Welcome Bonus</p>
|
||||
<p class="text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
x-text="program?.welcome_bonus_points ? program.welcome_bonus_points + ' points' : 'None'">-</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mb-1">Minimum Redemption</p>
|
||||
<p class="text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
x-text="program?.minimum_redemption_points ? program.minimum_redemption_points + ' points' : 'None'">-</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mb-1">Minimum Purchase</p>
|
||||
<p class="text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
x-text="program?.minimum_purchase_cents ? '€' + (program.minimum_purchase_cents / 100).toFixed(2) : 'None'">-</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mb-1">Points Expiration</p>
|
||||
<p class="text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
x-text="program?.points_expiration_days ? program.points_expiration_days + ' days of inactivity' : 'Never'">-</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Redemption Rewards -->
|
||||
<template x-if="(program?.loyalty_type === 'points' || program?.loyalty_type === 'hybrid') && program?.points_rewards?.length > 0">
|
||||
<div class="mb-6">
|
||||
<h4 class="text-sm font-semibold text-gray-600 dark:text-gray-400 uppercase mb-3 flex items-center">
|
||||
<span x-html="$icon('gift', 'inline w-4 h-4 mr-1')"></span>
|
||||
Redemption Rewards
|
||||
</h4>
|
||||
<div class="overflow-hidden border border-gray-200 dark:border-gray-700 rounded-lg">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-gray-50 dark:bg-gray-700">
|
||||
<tr>
|
||||
<th class="px-4 py-2 text-left text-xs font-semibold text-gray-600 dark:text-gray-300 uppercase">Reward</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-semibold text-gray-600 dark:text-gray-300 uppercase">Points Required</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-semibold text-gray-600 dark:text-gray-300 uppercase">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<template x-for="reward in program.points_rewards" :key="reward.name">
|
||||
<tr>
|
||||
<td class="px-4 py-2 font-medium text-gray-700 dark:text-gray-300" x-text="reward.name">-</td>
|
||||
<td class="px-4 py-2 text-gray-600 dark:text-gray-400" x-text="reward.points_required">-</td>
|
||||
<td class="px-4 py-2 text-gray-600 dark:text-gray-400" x-text="reward.description || '-'">-</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Anti-Fraud -->
|
||||
<div class="mb-6">
|
||||
<h4 class="text-sm font-semibold text-gray-600 dark:text-gray-400 uppercase mb-3 flex items-center">
|
||||
<span x-html="$icon('shield-check', 'inline w-4 h-4 mr-1')"></span>
|
||||
Anti-Fraud
|
||||
</h4>
|
||||
<div class="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mb-1">Cooldown</p>
|
||||
<p class="text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
x-text="program?.cooldown_minutes ? program.cooldown_minutes + ' minutes' : 'None'">-</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mb-1">Max Daily Stamps</p>
|
||||
<p class="text-sm font-medium text-gray-700 dark:text-gray-300" x-text="program?.max_daily_stamps || '-'">-</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mb-1">Staff PIN Required</p>
|
||||
<span class="inline-flex items-center px-2 py-1 text-xs font-semibold leading-tight rounded-full"
|
||||
:class="program?.require_staff_pin ? 'text-green-700 bg-green-100 dark:bg-green-700 dark:text-green-100' : 'text-gray-700 bg-gray-100 dark:bg-gray-700 dark:text-gray-100'">
|
||||
<span x-text="program?.require_staff_pin ? 'Yes' : 'No'"></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Branding -->
|
||||
<div class="mb-6">
|
||||
<h4 class="text-sm font-semibold text-gray-600 dark:text-gray-400 uppercase mb-3 flex items-center">
|
||||
<span x-html="$icon('paint-brush', 'inline w-4 h-4 mr-1')"></span>
|
||||
Branding
|
||||
</h4>
|
||||
<div class="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mb-1">Primary Color</p>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-6 h-6 rounded border border-gray-300 dark:border-gray-600"
|
||||
:style="'background-color: ' + (program?.card_color || '#6B21A8')"></div>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300" x-text="program?.card_color || '#6B21A8'"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mb-1">Secondary Color</p>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-6 h-6 rounded border border-gray-300 dark:border-gray-600"
|
||||
:style="'background-color: ' + (program?.card_secondary_color || '#FFFFFF')"></div>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300" x-text="program?.card_secondary_color || '#FFFFFF'"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mb-1">Logo URL</p>
|
||||
<p class="text-sm text-gray-700 dark:text-gray-300 truncate" x-text="program?.logo_url || '-'">-</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mb-1">Hero Image URL</p>
|
||||
<p class="text-sm text-gray-700 dark:text-gray-300 truncate" x-text="program?.hero_image_url || '-'">-</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Terms -->
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-gray-600 dark:text-gray-400 uppercase mb-3 flex items-center">
|
||||
<span x-html="$icon('document-text', 'inline w-4 h-4 mr-1')"></span>
|
||||
Terms & Privacy
|
||||
</h4>
|
||||
<div class="grid gap-6 md:grid-cols-2">
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mb-1">Terms & Conditions</p>
|
||||
<p class="text-sm text-gray-700 dark:text-gray-300 whitespace-pre-line" x-text="program?.terms_text || '-'">-</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mb-1">Privacy Policy URL</p>
|
||||
<template x-if="program?.privacy_url">
|
||||
<a :href="program.privacy_url" target="_blank" class="text-sm text-purple-600 hover:text-purple-700 dark:text-purple-400" x-text="program.privacy_url"></a>
|
||||
</template>
|
||||
<template x-if="!program?.privacy_url">
|
||||
<p class="text-sm text-gray-700 dark:text-gray-300">-</p>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,21 +1,21 @@
|
||||
{# app/modules/loyalty/templates/loyalty/store/stats.html #}
|
||||
{# app/modules/loyalty/templates/loyalty/store/analytics.html #}
|
||||
{% extends "store/base.html" %}
|
||||
{% from 'shared/macros/headers.html' import page_header_flex, refresh_button %}
|
||||
{% from 'shared/macros/alerts.html' import loading_state, error_state %}
|
||||
|
||||
{% block title %}Loyalty Stats{% endblock %}
|
||||
{% block title %}Loyalty Analytics{% endblock %}
|
||||
|
||||
{% block alpine_data %}storeLoyaltyStats(){% endblock %}
|
||||
{% block alpine_data %}storeLoyaltyAnalytics(){% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% call page_header_flex(title='Loyalty Statistics', subtitle='Track your loyalty program performance') %}
|
||||
{% call page_header_flex(title='Loyalty Analytics', subtitle='Track your loyalty program performance') %}
|
||||
<div class="flex items-center gap-3">
|
||||
{{ refresh_button(loading_var='loading', onclick='loadStats()', variant='secondary') }}
|
||||
</div>
|
||||
{% endcall %}
|
||||
|
||||
{{ loading_state('Loading statistics...') }}
|
||||
{{ error_state('Error loading statistics') }}
|
||||
{{ loading_state('Loading analytics...') }}
|
||||
{{ error_state('Error loading analytics') }}
|
||||
|
||||
<!-- No Program Setup Notice -->
|
||||
<div x-show="!loading && !program" class="mb-6 px-4 py-5 bg-yellow-50 border border-yellow-200 rounded-lg dark:bg-yellow-900/20 dark:border-yellow-800">
|
||||
@@ -25,7 +25,7 @@
|
||||
<h3 class="text-sm font-semibold text-yellow-800 dark:text-yellow-200">Loyalty Program Not Set Up</h3>
|
||||
<p class="mt-1 text-sm text-yellow-700 dark:text-yellow-300">Your merchant doesn't have a loyalty program configured yet.</p>
|
||||
{% if user.role == 'merchant_owner' %}
|
||||
<a href="/store/{{ store_code }}/loyalty/settings"
|
||||
<a href="/store/{{ store_code }}/loyalty/program/edit"
|
||||
class="mt-3 inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-yellow-600 rounded-lg hover:bg-yellow-700">
|
||||
<span x-html="$icon('cog', 'w-4 h-4 mr-2')"></span>
|
||||
Set Up Loyalty Program
|
||||
@@ -139,10 +139,10 @@
|
||||
<span x-html="$icon('users', 'w-4 h-4 mr-2')"></span>
|
||||
View Members
|
||||
</a>
|
||||
<a href="/store/{{ store_code }}/loyalty/settings"
|
||||
<a href="/store/{{ store_code }}/loyalty/program"
|
||||
class="flex items-center px-4 py-2 text-sm font-medium text-gray-600 bg-gray-50 rounded-lg hover:bg-gray-100 dark:bg-gray-700 dark:text-gray-400">
|
||||
<span x-html="$icon('cog', 'w-4 h-4 mr-2')"></span>
|
||||
Settings
|
||||
Program
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -150,5 +150,5 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script defer src="{{ url_for('loyalty_static', path='store/js/loyalty-stats.js') }}"></script>
|
||||
<script defer src="{{ url_for('loyalty_static', path='store/js/loyalty-analytics.js') }}"></script>
|
||||
{% endblock %}
|
||||
@@ -34,7 +34,7 @@
|
||||
<h3 class="text-sm font-semibold text-yellow-800 dark:text-yellow-200">Loyalty Program Not Set Up</h3>
|
||||
<p class="mt-1 text-sm text-yellow-700 dark:text-yellow-300">Your merchant doesn't have a loyalty program configured yet.</p>
|
||||
{% if user.role == 'merchant_owner' %}
|
||||
<a href="/store/{{ store_code }}/loyalty/settings"
|
||||
<a href="/store/{{ store_code }}/loyalty/program/edit"
|
||||
class="mt-3 inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-yellow-600 rounded-lg hover:bg-yellow-700">
|
||||
<span x-html="$icon('cog', 'w-4 h-4 mr-2')"></span>
|
||||
Set Up Loyalty Program
|
||||
|
||||
100
app/modules/loyalty/templates/loyalty/store/program.html
Normal file
100
app/modules/loyalty/templates/loyalty/store/program.html
Normal file
@@ -0,0 +1,100 @@
|
||||
{# app/modules/loyalty/templates/loyalty/store/program.html #}
|
||||
{% extends "store/base.html" %}
|
||||
{% from 'shared/macros/headers.html' import page_header_flex %}
|
||||
{% from 'shared/macros/alerts.html' import loading_state, error_state %}
|
||||
|
||||
{% block title %}Loyalty Program{% endblock %}
|
||||
|
||||
{% block alpine_data %}storeLoyaltyProgram(){% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% call page_header_flex(title='Loyalty Program', subtitle='Your loyalty program configuration') %}
|
||||
<div class="flex items-center gap-3">
|
||||
{% if user.role == 'merchant_owner' %}
|
||||
<a href="/store/{{ store_code }}/loyalty/program/edit"
|
||||
class="flex items-center px-4 py-2 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700"
|
||||
x-show="program">
|
||||
<span x-html="$icon('pencil', 'w-4 h-4 mr-2')"></span>
|
||||
Edit Program
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endcall %}
|
||||
|
||||
{{ loading_state('Loading program...') }}
|
||||
{{ error_state('Error loading program') }}
|
||||
|
||||
<!-- No Program State -->
|
||||
<div x-show="!loading && !program" class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-12 text-center">
|
||||
<span x-html="$icon('gift', 'w-12 h-12 mx-auto text-gray-400')"></span>
|
||||
<h3 class="mt-4 text-lg font-medium text-gray-900 dark:text-white">No Loyalty Program</h3>
|
||||
<p class="mt-2 text-gray-500 dark:text-gray-400">
|
||||
Your merchant doesn't have a loyalty program configured yet.
|
||||
</p>
|
||||
{% if user.role == 'merchant_owner' %}
|
||||
<a href="/store/{{ store_code }}/loyalty/program/edit"
|
||||
class="inline-flex items-center mt-4 px-6 py-2 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700">
|
||||
<span x-html="$icon('plus', 'w-4 h-4 mr-2')"></span>
|
||||
Create Program
|
||||
</a>
|
||||
{% else %}
|
||||
<p class="mt-4 text-sm text-gray-500 dark:text-gray-400">Contact your administrator to set up a loyalty program.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Program View -->
|
||||
<div x-show="!loading && program">
|
||||
{% set edit_url = '/store/' ~ store_code ~ '/loyalty/program/edit' %}
|
||||
{% if user.role == 'merchant_owner' %}
|
||||
{% set show_edit_button = true %}
|
||||
{% else %}
|
||||
{% set show_edit_button = false %}
|
||||
{% endif %}
|
||||
{% include "loyalty/shared/program-view.html" %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script>
|
||||
function storeLoyaltyProgram() {
|
||||
return {
|
||||
...data(),
|
||||
currentPage: 'loyalty-program',
|
||||
|
||||
program: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
|
||||
async init() {
|
||||
if (window._storeLoyaltyProgramInitialized) return;
|
||||
window._storeLoyaltyProgramInitialized = true;
|
||||
|
||||
const parentInit = data().init;
|
||||
if (parentInit) {
|
||||
await parentInit.call(this);
|
||||
}
|
||||
|
||||
await this.loadProgram();
|
||||
},
|
||||
|
||||
async loadProgram() {
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
|
||||
try {
|
||||
const response = await apiClient.get('/store/loyalty/program');
|
||||
if (response) {
|
||||
this.program = response;
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.status !== 404) {
|
||||
this.error = error.message || 'Failed to load program';
|
||||
}
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -2,6 +2,7 @@
|
||||
{% extends "store/base.html" %}
|
||||
{% from 'shared/macros/headers.html' import page_header_flex %}
|
||||
{% from 'shared/macros/alerts.html' import loading_state, error_state %}
|
||||
{% from 'shared/macros/modals.html' import confirm_modal %}
|
||||
|
||||
{% block title %}Loyalty Settings{% endblock %}
|
||||
|
||||
@@ -9,12 +10,12 @@
|
||||
|
||||
{% block content %}
|
||||
<!-- Page Header -->
|
||||
{% call page_header_flex(title='Loyalty Settings', subtitle='Configure your loyalty program') %}
|
||||
{% call page_header_flex(title='Loyalty Program Settings', subtitle='Configure your loyalty program') %}
|
||||
<div class="flex items-center gap-3">
|
||||
<a href="/store/{{ store_code }}/loyalty/terminal"
|
||||
<a href="/store/{{ store_code }}/loyalty/program"
|
||||
class="flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 dark:hover:bg-gray-700">
|
||||
<span x-html="$icon('terminal', 'w-4 h-4 mr-2')"></span>
|
||||
Terminal
|
||||
<span x-html="$icon('arrow-left', 'w-4 h-4 mr-2')"></span>
|
||||
Back to Program
|
||||
</a>
|
||||
</div>
|
||||
{% endcall %}
|
||||
@@ -35,212 +36,29 @@
|
||||
</div>
|
||||
|
||||
<!-- Settings Form -->
|
||||
<div x-show="!loading && isOwner" class="space-y-6">
|
||||
|
||||
<!-- Program Type (create only) -->
|
||||
<div x-show="!program" class="bg-white rounded-lg shadow-md dark:bg-gray-800">
|
||||
<div class="px-4 py-3 border-b border-gray-200 dark:border-gray-700">
|
||||
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200">
|
||||
<span x-html="$icon('layers', 'inline w-5 h-5 mr-2')"></span>
|
||||
Program Type
|
||||
</h3>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<div class="grid gap-4 md:grid-cols-3">
|
||||
<label class="relative flex items-start p-4 border-2 rounded-lg cursor-pointer transition-colors"
|
||||
:class="form.loyalty_type === 'stamps' ? 'border-purple-500 bg-purple-50 dark:bg-purple-900/20' : 'border-gray-200 dark:border-gray-700 hover:border-gray-300'">
|
||||
<input type="radio" x-model="form.loyalty_type" value="stamps" class="sr-only">
|
||||
<div>
|
||||
<span x-html="$icon('stamp', 'w-8 h-8 text-purple-500 mb-2')"></span>
|
||||
<p class="font-semibold text-gray-700 dark:text-gray-200">Stamps</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">Collect stamps for rewards</p>
|
||||
</div>
|
||||
</label>
|
||||
<label class="relative flex items-start p-4 border-2 rounded-lg cursor-pointer transition-colors"
|
||||
:class="form.loyalty_type === 'points' ? 'border-purple-500 bg-purple-50 dark:bg-purple-900/20' : 'border-gray-200 dark:border-gray-700 hover:border-gray-300'">
|
||||
<input type="radio" x-model="form.loyalty_type" value="points" class="sr-only">
|
||||
<div>
|
||||
<span x-html="$icon('coins', 'w-8 h-8 text-purple-500 mb-2')"></span>
|
||||
<p class="font-semibold text-gray-700 dark:text-gray-200">Points</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">Earn points per purchase</p>
|
||||
</div>
|
||||
</label>
|
||||
<label class="relative flex items-start p-4 border-2 rounded-lg cursor-pointer transition-colors"
|
||||
:class="form.loyalty_type === 'hybrid' ? 'border-purple-500 bg-purple-50 dark:bg-purple-900/20' : 'border-gray-200 dark:border-gray-700 hover:border-gray-300'">
|
||||
<input type="radio" x-model="form.loyalty_type" value="hybrid" class="sr-only">
|
||||
<div>
|
||||
<span x-html="$icon('layers', 'w-8 h-8 text-purple-500 mb-2')"></span>
|
||||
<p class="font-semibold text-gray-700 dark:text-gray-200">Hybrid</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">Both stamps and points</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stamps Configuration -->
|
||||
<div x-show="form.loyalty_type === 'stamps' || form.loyalty_type === 'hybrid'"
|
||||
class="bg-white rounded-lg shadow-md dark:bg-gray-800">
|
||||
<div class="px-4 py-3 border-b border-gray-200 dark:border-gray-700">
|
||||
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200">Stamps Configuration</h3>
|
||||
</div>
|
||||
<div class="p-4 grid gap-4 md:grid-cols-2">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Stamps for Reward</label>
|
||||
<input type="number" x-model.number="form.stamps_target" min="1" max="50"
|
||||
class="w-full px-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none dark:bg-gray-700 dark:text-gray-300">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Reward Description</label>
|
||||
<input type="text" x-model="form.stamps_reward_description" maxlength="255"
|
||||
placeholder="e.g., Free coffee"
|
||||
class="w-full px-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none dark:bg-gray-700 dark:text-gray-300">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Reward Value (cents)</label>
|
||||
<input type="number" x-model.number="form.stamps_reward_value_cents" min="0"
|
||||
placeholder="e.g., 500 for 5 EUR"
|
||||
class="w-full px-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none dark:bg-gray-700 dark:text-gray-300">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Points Configuration -->
|
||||
<div x-show="form.loyalty_type === 'points' || form.loyalty_type === 'hybrid'"
|
||||
class="bg-white rounded-lg shadow-md dark:bg-gray-800">
|
||||
<div class="px-4 py-3 border-b border-gray-200 dark:border-gray-700">
|
||||
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200">Points Configuration</h3>
|
||||
</div>
|
||||
<div class="p-4 grid gap-4 md:grid-cols-2">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Points per EUR</label>
|
||||
<input type="number" x-model.number="form.points_per_euro" min="1" max="100"
|
||||
class="w-full px-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none dark:bg-gray-700 dark:text-gray-300">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Welcome Bonus Points</label>
|
||||
<input type="number" x-model.number="form.welcome_bonus_points" min="0"
|
||||
class="w-full px-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none dark:bg-gray-700 dark:text-gray-300">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Min Redemption Points</label>
|
||||
<input type="number" x-model.number="form.minimum_redemption_points" min="1"
|
||||
class="w-full px-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none dark:bg-gray-700 dark:text-gray-300">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Min Purchase (cents)</label>
|
||||
<input type="number" x-model.number="form.minimum_purchase_cents" min="0"
|
||||
class="w-full px-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none dark:bg-gray-700 dark:text-gray-300">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Points Expiry (days)</label>
|
||||
<input type="number" x-model.number="form.points_expiration_days" min="30"
|
||||
placeholder="Leave empty for no expiry"
|
||||
class="w-full px-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none dark:bg-gray-700 dark:text-gray-300">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rewards List -->
|
||||
<div class="px-4 pb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Rewards</label>
|
||||
<div class="space-y-2">
|
||||
<template x-for="(reward, index) in form.points_rewards" :key="index">
|
||||
<div class="flex items-center gap-2">
|
||||
<input type="text" x-model="reward.name" placeholder="Reward name"
|
||||
class="flex-1 px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg dark:bg-gray-700 dark:text-gray-300">
|
||||
<input type="number" x-model.number="reward.points_required" placeholder="Points" min="1"
|
||||
class="w-24 px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg dark:bg-gray-700 dark:text-gray-300">
|
||||
<button @click="form.points_rewards.splice(index, 1)" type="button"
|
||||
class="p-2 text-red-500 hover:text-red-700">
|
||||
<span x-html="$icon('x', 'w-4 h-4')"></span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<button @click="addReward()" type="button"
|
||||
class="mt-2 text-sm text-purple-600 hover:text-purple-800 dark:text-purple-400">
|
||||
+ Add Reward
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Anti-Fraud -->
|
||||
<div class="bg-white rounded-lg shadow-md dark:bg-gray-800">
|
||||
<div class="px-4 py-3 border-b border-gray-200 dark:border-gray-700">
|
||||
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200">Anti-Fraud Settings</h3>
|
||||
</div>
|
||||
<div class="p-4 grid gap-4 md:grid-cols-3">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Cooldown (minutes)</label>
|
||||
<input type="number" x-model.number="form.cooldown_minutes" min="0" max="1440"
|
||||
class="w-full px-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none dark:bg-gray-700 dark:text-gray-300">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Max Daily Stamps</label>
|
||||
<input type="number" x-model.number="form.max_daily_stamps" min="1" max="50"
|
||||
class="w-full px-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none dark:bg-gray-700 dark:text-gray-300">
|
||||
</div>
|
||||
<div class="flex items-end">
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" x-model="form.require_staff_pin"
|
||||
class="w-4 h-4 text-purple-600 border-gray-300 rounded focus:ring-purple-500">
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">Require Staff PIN</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Branding -->
|
||||
<div class="bg-white rounded-lg shadow-md dark:bg-gray-800">
|
||||
<div class="px-4 py-3 border-b border-gray-200 dark:border-gray-700">
|
||||
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200">Branding</h3>
|
||||
</div>
|
||||
<div class="p-4 grid gap-4 md:grid-cols-2">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Card Name</label>
|
||||
<input type="text" x-model="form.card_name" maxlength="100"
|
||||
placeholder="e.g., My Rewards Card"
|
||||
class="w-full px-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none dark:bg-gray-700 dark:text-gray-300">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Card Color</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<input type="color" x-model="form.card_color"
|
||||
class="w-10 h-10 rounded border border-gray-300 dark:border-gray-600 cursor-pointer">
|
||||
<input type="text" x-model="form.card_color" maxlength="7" placeholder="#4F46E5"
|
||||
class="flex-1 px-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg dark:bg-gray-700 dark:text-gray-300">
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Logo URL</label>
|
||||
<input type="url" x-model="form.logo_url" maxlength="500"
|
||||
placeholder="https://..."
|
||||
class="w-full px-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none dark:bg-gray-700 dark:text-gray-300">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Terms & Conditions</label>
|
||||
<textarea x-model="form.terms_text" rows="3"
|
||||
class="w-full px-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none dark:bg-gray-700 dark:text-gray-300"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex justify-end gap-3">
|
||||
<a href="/store/{{ store_code }}/loyalty/terminal"
|
||||
class="px-6 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600">
|
||||
Cancel
|
||||
</a>
|
||||
<button @click="saveProgram()"
|
||||
:disabled="saving"
|
||||
class="px-6 py-2 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700 disabled:opacity-50">
|
||||
<span x-show="saving" x-html="$icon('spinner', 'w-4 h-4 mr-2 inline animate-spin')"></span>
|
||||
<span x-text="saving ? 'Saving...' : (program ? 'Save Changes' : 'Create Program')"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div x-show="!loading && isOwner">
|
||||
<form @submit.prevent="saveSettings">
|
||||
{% set show_delete = true %}
|
||||
{% set show_status = true %}
|
||||
{% set cancel_url = '/store/' ~ store_code ~ '/loyalty/program' %}
|
||||
{% include "loyalty/shared/program-form.html" %}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Delete Confirmation Modal -->
|
||||
{{ confirm_modal(
|
||||
'deleteProgramModal',
|
||||
'Delete Loyalty Program',
|
||||
'This will permanently delete the loyalty program and all associated data (cards, transactions, rewards). This action cannot be undone.',
|
||||
'deleteProgram()',
|
||||
'showDeleteModal',
|
||||
'Delete Program',
|
||||
'Cancel',
|
||||
'danger'
|
||||
) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script defer src="{{ url_for('loyalty_static', path='shared/js/loyalty-program-form.js') }}"></script>
|
||||
<script defer src="{{ url_for('loyalty_static', path='store/js/loyalty-settings.js') }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -17,10 +17,10 @@
|
||||
<span x-html="$icon('users', 'w-4 h-4 mr-2')"></span>
|
||||
Members
|
||||
</a>
|
||||
<a href="/store/{{ store_code }}/loyalty/stats"
|
||||
<a href="/store/{{ store_code }}/loyalty/analytics"
|
||||
class="flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 dark:hover:bg-gray-700">
|
||||
<span x-html="$icon('chart-bar', 'w-4 h-4 mr-2')"></span>
|
||||
Stats
|
||||
Analytics
|
||||
</a>
|
||||
</div>
|
||||
{% endcall %}
|
||||
@@ -37,7 +37,7 @@
|
||||
<h3 class="text-sm font-semibold text-yellow-800 dark:text-yellow-200">Loyalty Program Not Set Up</h3>
|
||||
<p class="mt-1 text-sm text-yellow-700 dark:text-yellow-300">Your merchant doesn't have a loyalty program configured yet.</p>
|
||||
{% if user.role == 'merchant_owner' %}
|
||||
<a href="/store/{{ store_code }}/loyalty/settings"
|
||||
<a href="/store/{{ store_code }}/loyalty/program/edit"
|
||||
class="mt-3 inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-yellow-600 rounded-lg hover:bg-yellow-700">
|
||||
<span x-html="$icon('cog', 'w-4 h-4 mr-2')"></span>
|
||||
Set Up Loyalty Program
|
||||
|
||||
Reference in New Issue
Block a user