Complete implementation of loyalty module Phase 2 features: Database & Models: - Add company_id to LoyaltyProgram for chain-wide loyalty - Add company_id to LoyaltyCard for multi-location support - Add CompanyLoyaltySettings model for admin-controlled settings - Add points expiration, welcome bonus, and minimum redemption fields - Add POINTS_EXPIRED, WELCOME_BONUS transaction types Services: - Update program_service for company-based queries - Update card_service with enrollment and welcome bonus - Update points_service with void_points for returns - Update stamp_service for company context - Update pin_service for company-wide operations API Endpoints: - Admin: Program listing with stats, company detail views - Vendor: Terminal operations, card management, settings - Storefront: Customer card/transactions, self-enrollment UI Templates: - Admin: Programs dashboard, company detail, settings - Vendor: Terminal, cards list, card detail, settings, stats, enrollment - Storefront: Dashboard, history, enrollment, success pages Background Tasks: - Point expiration task (daily, based on inactivity) - Wallet sync task (hourly) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
159 lines
9.7 KiB
HTML
159 lines
9.7 KiB
HTML
{# app/modules/loyalty/templates/loyalty/vendor/settings.html #}
|
|
{% extends "vendor/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 %}vendorLoyaltySettings(){% 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-end gap-4">
|
|
<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...' : 'Save Settings'"></span>
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_scripts %}
|
|
<script src="{{ url_for('loyalty_static', path='vendor/js/loyalty-settings.js') }}"></script>
|
|
{% endblock %}
|