feat(loyalty): restructure program CRUD by interface
Some checks failed
Some checks failed
Move program CRUD from store to merchant/admin interfaces. Store becomes view-only for program config while merchant gets full CRUD and admin gets override capabilities. Merchant portal: - New API endpoints (GET/POST/PATCH/DELETE /program) - New settings page with create/edit/delete form - Overview page now has Create/Edit Program buttons - Settings menu item added to sidebar Admin portal: - New CRUD endpoints (create for merchant, update, delete) - New activate/deactivate program endpoints - Programs list has edit and toggle buttons per row - Merchant detail has create/delete/toggle program actions Store portal: - Removed POST/PATCH /program endpoints (now read-only) - Removed settings page route and template - Terminal, cards, stats, enroll unchanged Tests: 112 passed (58 new) covering merchant API, admin CRUD, store endpoint removal, and program service unit tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -105,10 +105,27 @@
|
||||
|
||||
<!-- Program Configuration -->
|
||||
<div x-show="program" class="px-4 py-3 mb-8 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('cog', 'inline w-5 h-5 mr-2')"></span>
|
||||
Program Configuration
|
||||
</h3>
|
||||
<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>
|
||||
@@ -142,11 +159,39 @@
|
||||
|
||||
<!-- 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">
|
||||
<div class="flex items-center">
|
||||
<span x-html="$icon('exclamation-triangle', 'w-5 h-5 text-yellow-500 mr-3')"></span>
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-yellow-800 dark:text-yellow-200">No Loyalty Program</h3>
|
||||
<p class="text-sm text-yellow-700 dark:text-yellow-300">This merchant has not set up a loyalty program yet. Stores can set up the program from their dashboard.</p>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<span x-html="$icon('exclamation-triangle', 'w-5 h-5 text-yellow-500 mr-3')"></span>
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-yellow-800 dark:text-yellow-200">No Loyalty Program</h3>
|
||||
<p class="text-sm text-yellow-700 dark:text-yellow-300">This merchant has not set up a loyalty program yet.</p>
|
||||
</div>
|
||||
</div>
|
||||
<button @click="createProgram()"
|
||||
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('plus', 'w-4 h-4 mr-2')"></span>
|
||||
Create Program
|
||||
</button>
|
||||
</div>
|
||||
</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 the loyalty program and all associated data. 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()"
|
||||
class="px-4 py-2 text-sm font-medium text-white bg-red-600 rounded-lg hover:bg-red-700">
|
||||
Delete Program
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -217,14 +217,24 @@
|
||||
<span x-html="$icon('eye', 'w-5 h-5')"></span>
|
||||
</a>
|
||||
|
||||
<!-- Settings Button -->
|
||||
<!-- Edit Button -->
|
||||
<a
|
||||
:href="'/admin/loyalty/merchants/' + program.merchant_id + '/settings'"
|
||||
:href="'/admin/loyalty/merchants/' + program.merchant_id"
|
||||
class="flex items-center justify-center p-2 text-purple-600 rounded-lg hover:bg-purple-50 dark:text-purple-400 dark:hover:bg-gray-700 focus:outline-none transition-colors"
|
||||
title="Merchant loyalty settings"
|
||||
title="Edit program"
|
||||
>
|
||||
<span x-html="$icon('cog', 'w-5 h-5')"></span>
|
||||
<span x-html="$icon('pencil', 'w-5 h-5')"></span>
|
||||
</a>
|
||||
|
||||
<!-- Activate/Deactivate Toggle -->
|
||||
<button
|
||||
@click="toggleProgramActive(program)"
|
||||
class="flex items-center justify-center p-2 rounded-lg focus:outline-none transition-colors"
|
||||
:class="program.is_active ? 'text-orange-600 hover:bg-orange-50 dark:text-orange-400 dark:hover:bg-gray-700' : 'text-green-600 hover:bg-green-50 dark:text-green-400 dark:hover:bg-gray-700'"
|
||||
:title="program.is_active ? 'Deactivate program' : 'Activate program'"
|
||||
>
|
||||
<span x-html="$icon(program.is_active ? 'pause' : 'play', 'w-5 h-5')"></span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -7,9 +7,18 @@
|
||||
<div x-data="merchantLoyaltyOverview()">
|
||||
|
||||
<!-- Page Header -->
|
||||
<div class="mb-8 mt-6">
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-gray-100">Loyalty Overview</h2>
|
||||
<p class="mt-1 text-gray-500 dark:text-gray-400">Loyalty program statistics across all your stores.</p>
|
||||
<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>
|
||||
<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 -->
|
||||
@@ -18,8 +27,13 @@
|
||||
<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. Contact the platform administrator or set it up from your store dashboard.
|
||||
Your loyalty program hasn't been set up yet. Create one to start rewarding your customers.
|
||||
</p>
|
||||
<a href="/merchants/loyalty/settings"
|
||||
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>
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{# app/modules/loyalty/templates/loyalty/store/settings.html #}
|
||||
{% extends "store/base.html" %}
|
||||
{# 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 %}storeLoyaltySettings(){% endblock %}
|
||||
{% block alpine_data %}merchantLoyaltySettings(){% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% call page_header_flex(title='Loyalty Program Settings', subtitle='Configure your loyalty program') %}{% endcall %}
|
||||
@@ -24,7 +24,7 @@
|
||||
<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" {# noqa: FE-008 #}
|
||||
<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>
|
||||
@@ -142,17 +142,48 @@
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex items-center justify-end gap-4">
|
||||
<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...' : 'Save Settings'"></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='store/js/loyalty-settings.js') }}"></script>
|
||||
<script defer src="{{ url_for('loyalty_static', path='merchant/js/loyalty-settings.js') }}"></script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user