feat: add SQL query tool, platform debug, loyalty settings, and multi-module improvements
Some checks failed
Some checks failed
- Add admin SQL query tool with saved queries, schema explorer presets, and collapsible category sections (dev_tools module) - Add platform debug tool for admin diagnostics - Add loyalty settings page with owner-only access control - Fix loyalty settings owner check (use currentUser instead of window.__userData) - Replace HTTPException with AuthorizationException in loyalty routes - Expand loyalty module with PIN service, Apple Wallet, program management - Improve store login with platform detection and multi-platform support - Update billing feature gates and subscription services - Add store platform sync improvements and remove is_primary column - Add unit tests for loyalty (PIN, points, stamps, program services) - Update i18n translations across dev_tools locales Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -26,8 +26,28 @@
|
||||
|
||||
{{ error_state('Error loading members') }}
|
||||
|
||||
<!-- 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">
|
||||
<div class="flex items-start">
|
||||
<span x-html="$icon('exclamation-triangle', 'w-6 h-6 text-yellow-500 mr-3 flex-shrink-0')"></span>
|
||||
<div>
|
||||
<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"
|
||||
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
|
||||
</a>
|
||||
{% else %}
|
||||
<p class="mt-2 text-sm text-yellow-700 dark:text-yellow-300">Contact your administrator to complete the setup.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats Cards -->
|
||||
<div x-show="!loading" class="grid gap-6 mb-8 md:grid-cols-4">
|
||||
<div x-show="!loading && program" class="grid gap-6 mb-8 md:grid-cols-4">
|
||||
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<div class="p-3 mr-4 text-purple-500 bg-purple-100 rounded-full dark:text-purple-100 dark:bg-purple-500">
|
||||
<span x-html="$icon('users', 'w-5 h-5')"></span>
|
||||
@@ -67,7 +87,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Search and Filters -->
|
||||
<div x-show="!loading" class="mb-6 p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<div x-show="!loading && program" class="mb-6 p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<div class="flex flex-wrap items-center gap-4">
|
||||
<div class="flex-1 min-w-[200px]">
|
||||
<div class="relative">
|
||||
@@ -91,7 +111,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Cards Table -->
|
||||
<div x-show="!loading">
|
||||
<div x-show="!loading && program">
|
||||
{% call table_wrapper() %}
|
||||
{{ table_header(['Member', 'Card Number', 'Points Balance', 'Last Activity', 'Status', 'Actions']) }}
|
||||
<tbody class="bg-white divide-y dark:divide-gray-700 dark:bg-gray-800">
|
||||
|
||||
246
app/modules/loyalty/templates/loyalty/store/settings.html
Normal file
246
app/modules/loyalty/templates/loyalty/store/settings.html
Normal file
@@ -0,0 +1,246 @@
|
||||
{# app/modules/loyalty/templates/loyalty/store/settings.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 Settings{% endblock %}
|
||||
|
||||
{% block alpine_data %}loyaltySettings(){% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Page Header -->
|
||||
{% call page_header_flex(title='Loyalty Settings', subtitle='Configure your loyalty program') %}
|
||||
<div class="flex items-center gap-3">
|
||||
<a href="/store/{{ store_code }}/loyalty/terminal"
|
||||
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
|
||||
</a>
|
||||
</div>
|
||||
{% endcall %}
|
||||
|
||||
{{ loading_state('Loading settings...') }}
|
||||
|
||||
{{ error_state('Error loading settings') }}
|
||||
|
||||
<!-- Access Denied (non-owner) -->
|
||||
<div x-show="!loading && !isOwner" 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">
|
||||
<div class="flex items-start">
|
||||
<span x-html="$icon('shield', 'w-6 h-6 text-yellow-500 mr-3 flex-shrink-0')"></span>
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-yellow-800 dark:text-yellow-200">Access Restricted</h3>
|
||||
<p class="mt-1 text-sm text-yellow-700 dark:text-yellow-300">Only the merchant owner can manage loyalty program settings.</p>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script defer src="{{ url_for('loyalty_static', path='store/js/loyalty-settings.js') }}"></script>
|
||||
{% endblock %}
|
||||
@@ -17,7 +17,27 @@
|
||||
{{ loading_state('Loading statistics...') }}
|
||||
{{ error_state('Error loading statistics') }}
|
||||
|
||||
<div x-show="!loading">
|
||||
<!-- 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">
|
||||
<div class="flex items-start">
|
||||
<span x-html="$icon('exclamation-triangle', 'w-6 h-6 text-yellow-500 mr-3 flex-shrink-0')"></span>
|
||||
<div>
|
||||
<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"
|
||||
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
|
||||
</a>
|
||||
{% else %}
|
||||
<p class="mt-2 text-sm text-yellow-700 dark:text-yellow-300">Contact your administrator to complete the setup.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div x-show="!loading && program">
|
||||
<!-- Summary Stats -->
|
||||
<div class="grid gap-6 mb-8 md:grid-cols-2 xl:grid-cols-4">
|
||||
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
|
||||
@@ -36,11 +36,15 @@
|
||||
<div>
|
||||
<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"
|
||||
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
|
||||
</a>
|
||||
{% else %}
|
||||
<p class="mt-2 text-sm text-yellow-700 dark:text-yellow-300">Contact your administrator to complete the setup.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -125,70 +129,127 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Points Balance -->
|
||||
<!-- Balance Area -->
|
||||
<div class="mb-6 p-4 rounded-lg text-center"
|
||||
:style="'background-color: ' + (program?.card_color || '#4F46E5') + '10'">
|
||||
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Points Balance</p>
|
||||
<p class="text-3xl font-bold"
|
||||
:style="'color: ' + (program?.card_color || '#4F46E5')"
|
||||
x-text="formatNumber(selectedCard?.points_balance || 0)"></p>
|
||||
<!-- Points balance (for points and hybrid) -->
|
||||
<template x-if="program?.is_points_enabled">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Points Balance</p>
|
||||
<p class="text-3xl font-bold"
|
||||
:style="'color: ' + (program?.card_color || '#4F46E5')"
|
||||
x-text="formatNumber(selectedCard?.points_balance || 0)"></p>
|
||||
</div>
|
||||
</template>
|
||||
<!-- Stamps progress (for stamps and hybrid) -->
|
||||
<template x-if="program?.is_stamps_enabled">
|
||||
<div :class="program?.is_points_enabled ? 'mt-3 pt-3 border-t border-gray-200 dark:border-gray-700' : ''">
|
||||
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Stamps</p>
|
||||
<p class="text-3xl font-bold"
|
||||
:style="'color: ' + (program?.card_color || '#4F46E5')"
|
||||
x-text="(selectedCard?.stamp_count || 0) + ' / ' + (program?.stamps_target || 10)"></p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1"
|
||||
x-text="selectedCard?.stamps_until_reward > 0 ? (selectedCard.stamps_until_reward + ' more for reward') : 'Ready to redeem!'"></p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<!-- Action Panels -->
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<!-- Earn Points -->
|
||||
<div class="p-4 border border-gray-200 dark:border-gray-700 rounded-lg">
|
||||
<h4 class="font-medium text-gray-700 dark:text-gray-300 mb-3">
|
||||
<span x-html="$icon('plus-circle', 'inline w-4 h-4 mr-1 text-green-500')"></span>
|
||||
Earn Points
|
||||
</h4>
|
||||
<div class="mb-3">
|
||||
<label class="block text-xs text-gray-500 dark:text-gray-400 mb-1">Purchase Amount</label>
|
||||
<div class="relative">
|
||||
<span class="absolute inset-y-0 left-0 flex items-center pl-3 text-gray-500">EUR</span>
|
||||
<input type="number" step="0.01" min="0" {# noqa: FE-008 #}
|
||||
x-model.number="earnAmount"
|
||||
class="w-full pl-12 pr-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:border-green-400 focus:outline-none dark:bg-gray-700 dark:text-gray-300">
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mb-3">
|
||||
Points to award: <span class="font-semibold text-green-600" x-text="Math.floor((earnAmount || 0) * (program?.points_per_euro || 1))"></span>
|
||||
</p>
|
||||
<button @click="showPinModal('earn')"
|
||||
:disabled="!earnAmount || earnAmount <= 0"
|
||||
class="w-full px-4 py-2 text-sm font-medium text-white bg-green-600 rounded-lg hover:bg-green-700 disabled:opacity-50">
|
||||
Award Points
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Redeem Points -->
|
||||
<div class="p-4 border border-gray-200 dark:border-gray-700 rounded-lg">
|
||||
<h4 class="font-medium text-gray-700 dark:text-gray-300 mb-3">
|
||||
<span x-html="$icon('gift', 'inline w-4 h-4 mr-1 text-orange-500')"></span>
|
||||
Redeem Reward
|
||||
</h4>
|
||||
<div class="mb-3">
|
||||
<label class="block text-xs text-gray-500 dark:text-gray-400 mb-1">Select Reward</label>
|
||||
<select x-model="selectedReward"
|
||||
class="w-full px-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:border-orange-400 focus:outline-none dark:bg-gray-700 dark:text-gray-300">
|
||||
<option value="">Select reward...</option>
|
||||
<template x-for="reward in availableRewards" :key="reward.id">
|
||||
<option :value="reward.id" :disabled="(selectedCard?.points_balance || 0) < reward.points_required"
|
||||
x-text="reward.name + ' (' + reward.points_required + ' pts)'"></option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
<template x-if="selectedReward">
|
||||
<!-- Stamp Panels (for stamps and hybrid) -->
|
||||
<template x-if="program?.is_stamps_enabled">
|
||||
<div class="p-4 border border-gray-200 dark:border-gray-700 rounded-lg">
|
||||
<h4 class="font-medium text-gray-700 dark:text-gray-300 mb-3">
|
||||
<span x-html="$icon('plus-circle', 'inline w-4 h-4 mr-1 text-green-500')"></span>
|
||||
Add Stamp
|
||||
</h4>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mb-3">
|
||||
Points after: <span class="font-semibold text-orange-600" x-text="formatNumber((selectedCard?.points_balance || 0) - (getSelectedRewardPoints() || 0))"></span>
|
||||
Current: <span class="font-semibold" x-text="(selectedCard?.stamp_count || 0) + '/' + (program?.stamps_target || 10)"></span>
|
||||
</p>
|
||||
</template>
|
||||
<button @click="showPinModal('redeem')"
|
||||
:disabled="!selectedReward"
|
||||
class="w-full px-4 py-2 text-sm font-medium text-white bg-orange-600 rounded-lg hover:bg-orange-700 disabled:opacity-50">
|
||||
Redeem Reward
|
||||
</button>
|
||||
</div>
|
||||
<button @click="showPinModal('stamp')"
|
||||
:disabled="!selectedCard?.can_stamp"
|
||||
class="w-full px-4 py-2 text-sm font-medium text-white bg-green-600 rounded-lg hover:bg-green-700 disabled:opacity-50">
|
||||
Add Stamp
|
||||
</button>
|
||||
<template x-if="!selectedCard?.can_stamp && selectedCard?.cooldown_ends_at">
|
||||
<p class="text-xs text-red-500 mt-2">Cooldown active</p>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template x-if="program?.is_stamps_enabled">
|
||||
<div class="p-4 border border-gray-200 dark:border-gray-700 rounded-lg">
|
||||
<h4 class="font-medium text-gray-700 dark:text-gray-300 mb-3">
|
||||
<span x-html="$icon('gift', 'inline w-4 h-4 mr-1 text-orange-500')"></span>
|
||||
Redeem Stamps
|
||||
</h4>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mb-3"
|
||||
x-text="selectedCard?.can_redeem_stamps ? 'Reward: ' + (selectedCard?.stamp_reward_description || program?.stamps_reward_description || 'Free item') : 'Not enough stamps yet'"></p>
|
||||
<button @click="showPinModal('redeemStamps')"
|
||||
:disabled="!selectedCard?.can_redeem_stamps"
|
||||
class="w-full px-4 py-2 text-sm font-medium text-white bg-orange-600 rounded-lg hover:bg-orange-700 disabled:opacity-50">
|
||||
Redeem Stamps
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Point Panels (for points and hybrid) -->
|
||||
<template x-if="program?.is_points_enabled">
|
||||
<div class="p-4 border border-gray-200 dark:border-gray-700 rounded-lg">
|
||||
<h4 class="font-medium text-gray-700 dark:text-gray-300 mb-3">
|
||||
<span x-html="$icon('plus-circle', 'inline w-4 h-4 mr-1 text-green-500')"></span>
|
||||
Earn Points
|
||||
</h4>
|
||||
<div class="mb-3">
|
||||
<label class="block text-xs text-gray-500 dark:text-gray-400 mb-1">Purchase Amount</label>
|
||||
<div class="relative">
|
||||
<span class="absolute inset-y-0 left-0 flex items-center pl-3 text-gray-500">EUR</span>
|
||||
<input type="number" step="0.01" min="0" {# noqa: FE-008 #}
|
||||
x-model.number="earnAmount"
|
||||
class="w-full pl-12 pr-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:border-green-400 focus:outline-none dark:bg-gray-700 dark:text-gray-300">
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mb-3">
|
||||
Points to award: <span class="font-semibold text-green-600" x-text="Math.floor((earnAmount || 0) * (program?.points_per_euro || 1))"></span>
|
||||
</p>
|
||||
<button @click="showPinModal('earn')"
|
||||
:disabled="!earnAmount || earnAmount <= 0"
|
||||
class="w-full px-4 py-2 text-sm font-medium text-white bg-green-600 rounded-lg hover:bg-green-700 disabled:opacity-50">
|
||||
Award Points
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template x-if="program?.is_points_enabled">
|
||||
<div class="p-4 border border-gray-200 dark:border-gray-700 rounded-lg">
|
||||
<h4 class="font-medium text-gray-700 dark:text-gray-300 mb-3">
|
||||
<span x-html="$icon('gift', 'inline w-4 h-4 mr-1 text-orange-500')"></span>
|
||||
Redeem Reward
|
||||
</h4>
|
||||
<div class="mb-3">
|
||||
<label class="block text-xs text-gray-500 dark:text-gray-400 mb-1">Select Reward</label>
|
||||
<select x-model="selectedReward"
|
||||
class="w-full px-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:border-orange-400 focus:outline-none dark:bg-gray-700 dark:text-gray-300">
|
||||
<option value="">Select reward...</option>
|
||||
<template x-for="reward in availableRewards" :key="reward.id">
|
||||
<option :value="reward.id" :disabled="(selectedCard?.points_balance || 0) < reward.points_required"
|
||||
x-text="reward.name + ' (' + reward.points_required + ' pts)'"></option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
<template x-if="selectedReward">
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mb-3">
|
||||
Points after: <span class="font-semibold text-orange-600" x-text="formatNumber((selectedCard?.points_balance || 0) - (getSelectedRewardPoints() || 0))"></span>
|
||||
</p>
|
||||
</template>
|
||||
<button @click="showPinModal('redeem')"
|
||||
:disabled="!selectedReward"
|
||||
class="w-full px-4 py-2 text-sm font-medium text-white bg-orange-600 rounded-lg hover:bg-orange-700 disabled:opacity-50">
|
||||
Redeem Reward
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user