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:
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>
|
||||
Reference in New Issue
Block a user