feat(loyalty): add full i18n support for all loyalty module pages
Replace hardcoded English strings across all 22 templates, 10 JS files, and 4 locale files (en/fr/de/lb) with ~300 translation keys per language. Uses server-side _() for Jinja2 templates and I18n.t() for JS toast messages and dynamic Alpine.js expressions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,12 +4,14 @@
|
||||
{% from 'shared/macros/alerts.html' import loading_state, error_state %}
|
||||
{% from 'shared/macros/inputs.html' import search_autocomplete, selected_item_display %}
|
||||
|
||||
{% block title %}Loyalty Analytics{% endblock %}
|
||||
{% block title %}{{ _('loyalty.admin.analytics.title') }}{% endblock %}
|
||||
|
||||
{% block i18n_modules %}['loyalty']{% endblock %}
|
||||
|
||||
{% block alpine_data %}adminLoyaltyAnalytics(){% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% call page_header_flex(title='Loyalty Analytics', subtitle='Platform-wide loyalty program statistics') %}
|
||||
{% call page_header_flex(title=_('loyalty.admin.analytics.title'), subtitle=_('loyalty.admin.analytics.subtitle')) %}
|
||||
<div class="flex items-center gap-3">
|
||||
{{ refresh_button(loading_var='loading', onclick='loadStats()', variant='secondary') }}
|
||||
</div>
|
||||
@@ -17,7 +19,7 @@
|
||||
|
||||
<!-- Merchant Filter -->
|
||||
<div class="mb-6 p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-1">Filter by Merchant</label>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-1">{{ _('loyalty.admin.analytics.filter_by_merchant') }}</label>
|
||||
<div x-show="!selectedMerchant">
|
||||
{{ search_autocomplete(
|
||||
search_var='merchantSearch',
|
||||
@@ -28,20 +30,20 @@
|
||||
select_action='selectMerchant(item)',
|
||||
display_field='merchant_name',
|
||||
secondary_field='loyalty_type',
|
||||
placeholder='Search merchants by name...',
|
||||
placeholder=_('loyalty.admin.analytics.search_merchants_placeholder'),
|
||||
) }}
|
||||
</div>
|
||||
{{ selected_item_display(
|
||||
selected_var='selectedMerchant',
|
||||
display_field='merchant_name',
|
||||
clear_action='clearMerchantFilter()',
|
||||
label='Showing stats for:'
|
||||
label=_('loyalty.admin.analytics.showing_stats_for')
|
||||
) }}
|
||||
</div>
|
||||
|
||||
{{ loading_state('Loading analytics...') }}
|
||||
{{ loading_state(_('loyalty.admin.analytics.loading')) }}
|
||||
|
||||
{{ error_state('Error loading analytics') }}
|
||||
{{ error_state(_('loyalty.admin.analytics.error_loading')) }}
|
||||
|
||||
<!-- Analytics Dashboard -->
|
||||
<div x-show="!loading">
|
||||
@@ -54,42 +56,42 @@
|
||||
<div class="mb-6 px-4 py-5 bg-white rounded-lg shadow-md dark:bg-gray-800" x-show="walletStatus">
|
||||
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
|
||||
<span x-html="$icon('device-phone-mobile', 'w-5 h-5 inline mr-1')"></span>
|
||||
Wallet Integration Status
|
||||
{{ _('loyalty.admin.analytics.wallet_status') }}
|
||||
</h3>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- Google Wallet -->
|
||||
<div class="p-4 border rounded-lg dark:border-gray-700" x-show="walletStatus?.google_wallet">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h4 class="font-medium text-gray-700 dark:text-gray-300">Google Wallet</h4>
|
||||
<h4 class="font-medium text-gray-700 dark:text-gray-300">{{ _('loyalty.admin.analytics.google_wallet') }}</h4>
|
||||
<template x-if="walletStatus?.google_wallet?.credentials_valid">
|
||||
<span class="px-2 py-1 text-xs font-medium text-green-700 bg-green-100 rounded-full dark:text-green-300 dark:bg-green-900/30">Connected</span>
|
||||
<span class="px-2 py-1 text-xs font-medium text-green-700 bg-green-100 rounded-full dark:text-green-300 dark:bg-green-900/30">{{ _('loyalty.admin.analytics.connected') }}</span>
|
||||
</template>
|
||||
<template x-if="walletStatus?.google_wallet?.configured && !walletStatus?.google_wallet?.credentials_valid">
|
||||
<span class="px-2 py-1 text-xs font-medium text-red-700 bg-red-100 rounded-full dark:text-red-300 dark:bg-red-900/30">Error</span>
|
||||
<span class="px-2 py-1 text-xs font-medium text-red-700 bg-red-100 rounded-full dark:text-red-300 dark:bg-red-900/30">{{ _('loyalty.admin.analytics.error') }}</span>
|
||||
</template>
|
||||
<template x-if="!walletStatus?.google_wallet?.configured">
|
||||
<span class="px-2 py-1 text-xs font-medium text-gray-500 bg-gray-100 rounded-full dark:text-gray-400 dark:bg-gray-700">Not Configured</span>
|
||||
<span class="px-2 py-1 text-xs font-medium text-gray-500 bg-gray-100 rounded-full dark:text-gray-400 dark:bg-gray-700">{{ _('loyalty.admin.analytics.not_configured') }}</span>
|
||||
</template>
|
||||
</div>
|
||||
<template x-if="walletStatus?.google_wallet?.configured">
|
||||
<div class="space-y-2 text-sm">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-500 dark:text-gray-400">Issuer ID</span>
|
||||
<span class="text-gray-500 dark:text-gray-400">{{ _('loyalty.admin.analytics.issuer_id') }}</span>
|
||||
<span class="text-gray-700 dark:text-gray-300" x-text="walletStatus.google_wallet.issuer_id"></span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-500 dark:text-gray-400">Project</span>
|
||||
<span class="text-gray-500 dark:text-gray-400">{{ _('loyalty.admin.analytics.project') }}</span>
|
||||
<span class="text-gray-700 dark:text-gray-300" x-text="walletStatus.google_wallet.project_id || '-'"></span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-500 dark:text-gray-400">Wallet Objects</span>
|
||||
<span class="text-gray-500 dark:text-gray-400">{{ _('loyalty.admin.analytics.wallet_objects') }}</span>
|
||||
<span class="font-medium text-gray-700 dark:text-gray-300" x-text="walletStatus.google_wallet.total_objects || 0"></span>
|
||||
</div>
|
||||
<!-- Class statuses -->
|
||||
<template x-if="walletStatus.google_wallet.classes?.length > 0">
|
||||
<div class="mt-2 pt-2 border-t dark:border-gray-700">
|
||||
<p class="text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">Loyalty Classes</p>
|
||||
<p class="text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">{{ _('loyalty.admin.analytics.loyalty_classes') }}</p>
|
||||
<template x-for="cls in walletStatus.google_wallet.classes" :key="cls.class_id">
|
||||
<div class="flex justify-between text-xs py-1">
|
||||
<span class="text-gray-600 dark:text-gray-400" x-text="cls.program_name"></span>
|
||||
@@ -115,29 +117,29 @@
|
||||
<!-- Apple Wallet -->
|
||||
<div class="p-4 border rounded-lg dark:border-gray-700" x-show="walletStatus?.apple_wallet">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h4 class="font-medium text-gray-700 dark:text-gray-300">Apple Wallet</h4>
|
||||
<h4 class="font-medium text-gray-700 dark:text-gray-300">{{ _('loyalty.admin.analytics.apple_wallet') }}</h4>
|
||||
<template x-if="walletStatus?.apple_wallet?.credentials_valid">
|
||||
<span class="px-2 py-1 text-xs font-medium text-green-700 bg-green-100 rounded-full dark:text-green-300 dark:bg-green-900/30">Connected</span>
|
||||
<span class="px-2 py-1 text-xs font-medium text-green-700 bg-green-100 rounded-full dark:text-green-300 dark:bg-green-900/30">{{ _('loyalty.admin.analytics.connected') }}</span>
|
||||
</template>
|
||||
<template x-if="walletStatus?.apple_wallet?.configured && !walletStatus?.apple_wallet?.credentials_valid">
|
||||
<span class="px-2 py-1 text-xs font-medium text-red-700 bg-red-100 rounded-full dark:text-red-300 dark:bg-red-900/30">Error</span>
|
||||
<span class="px-2 py-1 text-xs font-medium text-red-700 bg-red-100 rounded-full dark:text-red-300 dark:bg-red-900/30">{{ _('loyalty.admin.analytics.error') }}</span>
|
||||
</template>
|
||||
<template x-if="!walletStatus?.apple_wallet?.configured">
|
||||
<span class="px-2 py-1 text-xs font-medium text-gray-500 bg-gray-100 rounded-full dark:text-gray-400 dark:bg-gray-700">Not Configured</span>
|
||||
<span class="px-2 py-1 text-xs font-medium text-gray-500 bg-gray-100 rounded-full dark:text-gray-400 dark:bg-gray-700">{{ _('loyalty.admin.analytics.not_configured') }}</span>
|
||||
</template>
|
||||
</div>
|
||||
<template x-if="walletStatus?.apple_wallet?.configured">
|
||||
<div class="space-y-2 text-sm">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-500 dark:text-gray-400">Pass Type ID</span>
|
||||
<span class="text-gray-500 dark:text-gray-400">{{ _('loyalty.admin.analytics.pass_type_id') }}</span>
|
||||
<span class="text-gray-700 dark:text-gray-300" x-text="walletStatus.apple_wallet.pass_type_id"></span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-500 dark:text-gray-400">Team ID</span>
|
||||
<span class="text-gray-500 dark:text-gray-400">{{ _('loyalty.admin.analytics.team_id') }}</span>
|
||||
<span class="text-gray-700 dark:text-gray-300" x-text="walletStatus.apple_wallet.team_id"></span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-500 dark:text-gray-400">Active Passes</span>
|
||||
<span class="text-gray-500 dark:text-gray-400">{{ _('loyalty.admin.analytics.active_passes') }}</span>
|
||||
<span class="font-medium text-gray-700 dark:text-gray-300" x-text="walletStatus.apple_wallet.total_passes || 0"></span>
|
||||
</div>
|
||||
<template x-if="walletStatus.apple_wallet.errors?.length > 0">
|
||||
@@ -155,17 +157,17 @@
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="px-4 py-5 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">Quick Actions</h3>
|
||||
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">{{ _('loyalty.admin.analytics.quick_actions') }}</h3>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<a href="/admin/loyalty/programs"
|
||||
class="inline-flex items-center px-4 py-2 text-sm font-medium text-purple-600 bg-purple-100 rounded-lg hover:bg-purple-200 dark:text-purple-300 dark:bg-purple-900/30 dark:hover:bg-purple-900/50">
|
||||
<span x-html="$icon('gift', 'w-4 h-4 mr-2')"></span>
|
||||
View All Programs
|
||||
{{ _('loyalty.admin.analytics.view_all_programs') }}
|
||||
</a>
|
||||
<a href="/admin/merchants"
|
||||
class="inline-flex items-center px-4 py-2 text-sm font-medium text-blue-600 bg-blue-100 rounded-lg hover:bg-blue-200 dark:text-blue-300 dark:bg-blue-900/30 dark:hover:bg-blue-900/50">
|
||||
<span x-html="$icon('building-office', 'w-4 h-4 mr-2')"></span>
|
||||
Manage Merchants
|
||||
{{ _('loyalty.admin.analytics.manage_merchants') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,44 +5,46 @@
|
||||
{% from 'shared/macros/tables.html' import table_wrapper, table_header %}
|
||||
{% from 'shared/macros/modals.html' import confirm_modal %}
|
||||
|
||||
{% block title %}Merchant Loyalty Details{% endblock %}
|
||||
{% block title %}{{ _('loyalty.admin.merchant_detail.title') }}{% endblock %}
|
||||
|
||||
{% block i18n_modules %}['loyalty']{% endblock %}
|
||||
|
||||
{% block alpine_data %}adminLoyaltyMerchantDetail(){% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% call detail_page_header("merchant?.name || 'Merchant Loyalty'", '/admin/loyalty/programs', subtitle_show='merchant') %}
|
||||
<span x-text="program ? 'Loyalty Program Active' : 'No Loyalty Program'"></span>
|
||||
<span x-text="program ? $t('loyalty.admin.merchant_detail.program_active') : $t('loyalty.admin.merchant_detail.no_program_subtitle')"></span>
|
||||
{% endcall %}
|
||||
|
||||
{{ loading_state('Loading merchant loyalty details...') }}
|
||||
{{ loading_state(_('loyalty.admin.merchant_detail.loading')) }}
|
||||
|
||||
{{ error_state('Error loading merchant loyalty') }}
|
||||
{{ error_state(_('loyalty.admin.merchant_detail.error_loading')) }}
|
||||
|
||||
<!-- Merchant Details -->
|
||||
<div x-show="!loading && merchant">
|
||||
<!-- Quick Actions Card -->
|
||||
<div class="px-4 py-3 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">
|
||||
Quick Actions
|
||||
{{ _('loyalty.admin.merchant_detail.quick_actions') }}
|
||||
</h3>
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<a x-show="program"
|
||||
:href="`/admin/loyalty/merchants/${merchantId}/program`"
|
||||
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple">
|
||||
<span x-html="$icon('pencil', 'w-4 h-4 mr-2')"></span>
|
||||
Edit Program
|
||||
{{ _('loyalty.admin.merchant_detail.edit_program') }}
|
||||
</a>
|
||||
<a
|
||||
:href="`/admin/loyalty/merchants/${merchantId}/settings`"
|
||||
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-gray-700 transition-colors duration-150 bg-gray-100 border border-gray-300 rounded-lg hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-600 dark:hover:bg-gray-600">
|
||||
<span x-html="$icon('shield-check', 'w-4 h-4 mr-2')"></span>
|
||||
Admin Policy
|
||||
{{ _('loyalty.admin.merchant_detail.admin_policy') }}
|
||||
</a>
|
||||
<a
|
||||
:href="`/admin/merchants/${merchant?.id}`"
|
||||
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-gray-700 transition-colors duration-150 bg-gray-100 border border-gray-300 rounded-lg hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-600 dark:hover:bg-gray-600">
|
||||
<span x-html="$icon('building-office', 'w-4 h-4 mr-2')"></span>
|
||||
View Merchant
|
||||
{{ _('loyalty.admin.merchant_detail.view_merchant') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -56,7 +58,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
Total Members
|
||||
{{ _('loyalty.admin.merchant_detail.total_members') }}
|
||||
</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="formatNumber(stats.total_cards)">
|
||||
0
|
||||
@@ -71,7 +73,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
Active (30d)
|
||||
{{ _('loyalty.admin.merchant_detail.active_30d') }}
|
||||
</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="formatNumber(stats.active_cards)">
|
||||
0
|
||||
@@ -86,7 +88,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
Points Issued (30d)
|
||||
{{ _('loyalty.admin.merchant_detail.points_issued_30d') }}
|
||||
</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="formatNumber(stats.points_issued_30d)">
|
||||
0
|
||||
@@ -101,7 +103,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
Points Redeemed (30d)
|
||||
{{ _('loyalty.admin.merchant_detail.points_redeemed_30d') }}
|
||||
</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="formatNumber(stats.points_redeemed_30d)">
|
||||
0
|
||||
@@ -120,14 +122,14 @@
|
||||
<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>
|
||||
<h3 class="text-sm font-semibold text-yellow-800 dark:text-yellow-200">{{ _('loyalty.admin.merchant_detail.no_program') }}</h3>
|
||||
<p class="text-sm text-yellow-700 dark:text-yellow-300">{{ _('loyalty.admin.merchant_detail.no_program_desc') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<a :href="`/admin/loyalty/merchants/${merchantId}/program`"
|
||||
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
|
||||
{{ _('loyalty.admin.merchant_detail.create_program') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -135,12 +137,12 @@
|
||||
<!-- Delete Confirmation Modal -->
|
||||
{{ confirm_modal(
|
||||
'deleteProgramModal',
|
||||
'Delete Loyalty Program',
|
||||
'This will permanently delete the loyalty program and all associated data. This action cannot be undone.',
|
||||
_('loyalty.admin.merchant_detail.delete_title'),
|
||||
_('loyalty.admin.merchant_detail.delete_message'),
|
||||
'deleteProgram()',
|
||||
'showDeleteModal',
|
||||
'Delete Program',
|
||||
'Cancel',
|
||||
_('loyalty.admin.merchant_detail.delete_confirm'),
|
||||
_('loyalty.common.cancel'),
|
||||
'danger'
|
||||
) }}
|
||||
|
||||
@@ -148,10 +150,10 @@
|
||||
<div x-show="locations.length > 0" 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('map-pin', 'inline w-5 h-5 mr-2')"></span>
|
||||
Location Breakdown (<span x-text="locations.length"></span>)
|
||||
{{ _('loyalty.admin.merchant_detail.location_breakdown') }} (<span x-text="locations.length"></span>)
|
||||
</h3>
|
||||
{% call table_wrapper() %}
|
||||
{{ table_header(['Location', 'Enrolled', 'Points Earned', 'Points Redeemed', 'Transactions (30d)']) }}
|
||||
{{ table_header([_('loyalty.admin.merchant_detail.table_location'), _('loyalty.admin.merchant_detail.table_enrolled'), _('loyalty.admin.merchant_detail.table_points_earned'), _('loyalty.admin.merchant_detail.table_points_redeemed'), _('loyalty.admin.merchant_detail.table_transactions_30d')]) }}
|
||||
<tbody class="bg-white divide-y dark:divide-gray-700 dark:bg-gray-800">
|
||||
<template x-for="location in locations" :key="location.store_id">
|
||||
<tr class="text-gray-700 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||
@@ -171,7 +173,7 @@
|
||||
</template>
|
||||
<!-- Totals Row -->
|
||||
<tr class="text-gray-900 dark:text-gray-100 font-semibold bg-gray-50 dark:bg-gray-700">
|
||||
<td class="px-4 py-3 text-sm">TOTAL</td>
|
||||
<td class="px-4 py-3 text-sm">{{ _('loyalty.common.total') }}</td>
|
||||
<td class="px-4 py-3 text-sm" x-text="formatNumber(stats.total_cards)">0</td>
|
||||
<td class="px-4 py-3 text-sm" x-text="formatNumber(stats.total_points_issued)">0</td>
|
||||
<td class="px-4 py-3 text-sm" x-text="formatNumber(stats.total_points_redeemed)">0</td>
|
||||
@@ -185,11 +187,11 @@
|
||||
<div 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('shield-check', 'inline w-5 h-5 mr-2')"></span>
|
||||
Admin Policy Settings
|
||||
{{ _('loyalty.admin.merchant_detail.admin_policy_settings') }}
|
||||
</h3>
|
||||
<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">Staff PIN Policy</p>
|
||||
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase mb-2">{{ _('loyalty.admin.merchant_detail.staff_pin_policy') }}</p>
|
||||
<span class="inline-flex items-center px-2 py-1 text-xs font-semibold leading-tight rounded-full"
|
||||
:class="{
|
||||
'text-green-700 bg-green-100 dark:bg-green-700 dark:text-green-100': settings?.staff_pin_policy === 'required',
|
||||
@@ -200,17 +202,17 @@
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase mb-2">Self Enrollment</p>
|
||||
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase mb-2">{{ _('loyalty.admin.merchant_detail.self_enrollment') }}</p>
|
||||
<span class="inline-flex items-center px-2 py-1 text-xs font-semibold leading-tight rounded-full"
|
||||
:class="settings?.allow_self_enrollment ? '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="settings?.allow_self_enrollment ? 'Allowed' : 'Disabled'"></span>
|
||||
<span x-text="settings?.allow_self_enrollment ? $t('loyalty.admin.merchant_detail.allowed') : $t('loyalty.admin.merchant_detail.disabled')"></span>
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase mb-2">Cross-Location Redemption</p>
|
||||
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase mb-2">{{ _('loyalty.admin.merchant_detail.cross_location_redemption') }}</p>
|
||||
<span class="inline-flex items-center px-2 py-1 text-xs font-semibold leading-tight rounded-full"
|
||||
:class="settings?.allow_cross_location_redemption !== false ? '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="settings?.allow_cross_location_redemption !== false ? 'Allowed' : 'Disabled'"></span>
|
||||
<span x-text="settings?.allow_cross_location_redemption !== false ? $t('loyalty.admin.merchant_detail.allowed') : $t('loyalty.admin.merchant_detail.disabled')"></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -219,7 +221,7 @@
|
||||
:href="`/admin/loyalty/merchants/${merchantId}/settings`"
|
||||
class="text-sm text-purple-600 hover:text-purple-700 dark:text-purple-400">
|
||||
<span x-html="$icon('cog', 'inline w-4 h-4 mr-1')"></span>
|
||||
Modify admin policy
|
||||
{{ _('loyalty.admin.merchant_detail.modify_policy') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,18 +4,20 @@
|
||||
{% from 'shared/macros/headers.html' import detail_page_header %}
|
||||
{% from 'shared/macros/forms.html' import form_section, form_actions %}
|
||||
|
||||
{% block title %}Merchant Loyalty Settings{% endblock %}
|
||||
{% block title %}{{ _('loyalty.admin.merchant_settings.title') }}{% endblock %}
|
||||
|
||||
{% block i18n_modules %}['loyalty']{% endblock %}
|
||||
|
||||
{% block alpine_data %}adminLoyaltyMerchantSettings(){% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% call detail_page_header("'Admin Policy: ' + (merchant?.name || '')", '/admin/loyalty/merchants/' ~ merchant_id, subtitle_show='merchant') %}
|
||||
Admin-controlled settings for this merchant's loyalty program
|
||||
{{ _('loyalty.admin.merchant_settings.admin_controlled') }}
|
||||
{% endcall %}
|
||||
|
||||
{{ loading_state('Loading settings...') }}
|
||||
{{ loading_state(_('loyalty.admin.merchant_settings.loading')) }}
|
||||
|
||||
{{ error_state('Error loading settings') }}
|
||||
{{ error_state(_('loyalty.admin.merchant_settings.error_loading')) }}
|
||||
|
||||
<!-- Settings Form -->
|
||||
<div x-show="!loading">
|
||||
@@ -24,10 +26,10 @@
|
||||
<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('key', 'inline w-5 h-5 mr-2')"></span>
|
||||
Staff PIN Policy
|
||||
{{ _('loyalty.admin.merchant_settings.staff_pin_policy') }}
|
||||
</h3>
|
||||
<p class="mb-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
Control whether staff members at this merchant's locations must enter a PIN to process loyalty transactions.
|
||||
{{ _('loyalty.admin.merchant_settings.staff_pin_description') }}
|
||||
</p>
|
||||
|
||||
<div class="space-y-4">
|
||||
@@ -37,8 +39,8 @@
|
||||
x-model="settings.staff_pin_policy"
|
||||
class="mt-1 text-purple-600 form-radio">
|
||||
<div class="ml-3">
|
||||
<p class="font-medium text-gray-700 dark:text-gray-300">Required</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">Staff must enter their PIN for every transaction. Recommended for security.</p>
|
||||
<p class="font-medium text-gray-700 dark:text-gray-300">{{ _('loyalty.admin.merchant_settings.required') }}</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">{{ _('loyalty.admin.merchant_settings.required_desc') }}</p>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
@@ -48,8 +50,8 @@
|
||||
x-model="settings.staff_pin_policy"
|
||||
class="mt-1 text-purple-600 form-radio">
|
||||
<div class="ml-3">
|
||||
<p class="font-medium text-gray-700 dark:text-gray-300">Optional</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">Stores can choose whether to require PINs at their locations.</p>
|
||||
<p class="font-medium text-gray-700 dark:text-gray-300">{{ _('loyalty.admin.merchant_settings.optional') }}</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">{{ _('loyalty.admin.merchant_settings.optional_desc') }}</p>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
@@ -59,8 +61,8 @@
|
||||
x-model="settings.staff_pin_policy"
|
||||
class="mt-1 text-purple-600 form-radio">
|
||||
<div class="ml-3">
|
||||
<p class="font-medium text-gray-700 dark:text-gray-300">Disabled</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">Staff PINs are not used. Any staff member can process transactions.</p>
|
||||
<p class="font-medium text-gray-700 dark:text-gray-300">{{ _('loyalty.admin.merchant_settings.pin_disabled') }}</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">{{ _('loyalty.admin.merchant_settings.pin_disabled_desc') }}</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
@@ -68,26 +70,26 @@
|
||||
<!-- PIN Lockout Settings -->
|
||||
<div x-show="settings.staff_pin_policy !== 'disabled'" class="mt-6 pt-6 border-t border-gray-200 dark:border-gray-700">
|
||||
<h4 class="mb-4 text-md font-medium text-gray-700 dark:text-gray-300">
|
||||
PIN Lockout Settings
|
||||
{{ _('loyalty.admin.merchant_settings.pin_lockout_settings') }}
|
||||
</h4>
|
||||
<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">
|
||||
Max Failed Attempts
|
||||
{{ _('loyalty.admin.merchant_settings.max_failed_attempts') }}
|
||||
</label>
|
||||
<input type="number" min="3" max="10" {# noqa: FE-008 #}
|
||||
x-model.number="settings.staff_pin_lockout_attempts"
|
||||
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 dark:text-gray-400">Number of wrong attempts before lockout (3-10)</p>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">{{ _('loyalty.admin.merchant_settings.max_failed_attempts_help') }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Lockout Duration (minutes)
|
||||
{{ _('loyalty.admin.merchant_settings.lockout_duration') }}
|
||||
</label>
|
||||
<input type="number" min="5" max="120"
|
||||
x-model.number="settings.staff_pin_lockout_minutes"
|
||||
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 dark:text-gray-400">How long to lock out after failed attempts (5-120 minutes)</p>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">{{ _('loyalty.admin.merchant_settings.lockout_duration_help') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -97,14 +99,14 @@
|
||||
<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('user-plus', 'inline w-5 h-5 mr-2')"></span>
|
||||
Enrollment Settings
|
||||
{{ _('loyalty.admin.merchant_settings.enrollment_settings') }}
|
||||
</h3>
|
||||
|
||||
<div class="space-y-4">
|
||||
<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">Allow Self-Service Enrollment</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">Customers can sign up via QR code without staff assistance</p>
|
||||
<p class="font-medium text-gray-700 dark:text-gray-300">{{ _('loyalty.admin.merchant_settings.allow_self_enrollment') }}</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">{{ _('loyalty.admin.merchant_settings.self_enrollment_desc') }}</p>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<input type="checkbox" x-model="settings.allow_self_enrollment"
|
||||
@@ -122,14 +124,14 @@
|
||||
<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('arrows-right-left', 'inline w-5 h-5 mr-2')"></span>
|
||||
Transaction Settings
|
||||
{{ _('loyalty.admin.merchant_settings.transaction_settings') }}
|
||||
</h3>
|
||||
|
||||
<div class="space-y-4">
|
||||
<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">Allow Cross-Location Redemption</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">Customers can redeem points at any merchant location</p>
|
||||
<p class="font-medium text-gray-700 dark:text-gray-300">{{ _('loyalty.admin.merchant_settings.allow_cross_location') }}</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">{{ _('loyalty.admin.merchant_settings.cross_location_desc') }}</p>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<input type="checkbox" x-model="settings.allow_cross_location_redemption"
|
||||
@@ -143,8 +145,8 @@
|
||||
|
||||
<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">Allow Void Transactions</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">Staff can void points/stamps for returns</p>
|
||||
<p class="font-medium text-gray-700 dark:text-gray-300">{{ _('loyalty.admin.merchant_settings.allow_void') }}</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">{{ _('loyalty.admin.merchant_settings.void_desc') }}</p>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<input type="checkbox" x-model="settings.allow_void_transactions"
|
||||
@@ -162,13 +164,13 @@
|
||||
<div class="flex items-center justify-end gap-4">
|
||||
<a :href="backUrl"
|
||||
class="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">
|
||||
Cancel
|
||||
{{ _('loyalty.common.cancel') }}
|
||||
</a>
|
||||
<button type="submit"
|
||||
:disabled="saving"
|
||||
class="flex items-center px-4 py-2 text-sm font-medium text-white bg-purple-600 border border-transparent rounded-lg hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple 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 ? $t('loyalty.common.saving') : $t('loyalty.admin.merchant_settings.save_settings')"></span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -4,17 +4,19 @@
|
||||
{% from 'shared/macros/alerts.html' import loading_state, error_state %}
|
||||
{% from 'shared/macros/modals.html' import confirm_modal %}
|
||||
|
||||
{% block title %}Program Configuration{% endblock %}
|
||||
{% block title %}{{ _('loyalty.admin.program_edit.title') }}{% endblock %}
|
||||
|
||||
{% block i18n_modules %}['loyalty']{% endblock %}
|
||||
|
||||
{% block alpine_data %}adminLoyaltyProgramEdit(){% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% call detail_page_header("isNewProgram ? 'Create Program: ' + (merchant?.name || '') : 'Edit Program: ' + (merchant?.name || '')", '/admin/loyalty/merchants/' ~ merchant_id, subtitle_show='merchant') %}
|
||||
<span x-text="isNewProgram ? 'Create a loyalty program for this merchant' : 'Edit program configuration'"></span>
|
||||
<span x-text="isNewProgram ? $t('loyalty.admin.program_edit.create_subtitle') : $t('loyalty.admin.program_edit.edit_subtitle')"></span>
|
||||
{% endcall %}
|
||||
|
||||
{{ loading_state('Loading program configuration...') }}
|
||||
{{ error_state('Error loading program configuration') }}
|
||||
{{ loading_state(_('loyalty.admin.program_edit.loading')) }}
|
||||
{{ error_state(_('loyalty.admin.program_edit.error_loading')) }}
|
||||
|
||||
<div x-show="!loading">
|
||||
<form @submit.prevent="saveSettings">
|
||||
@@ -28,12 +30,12 @@
|
||||
<!-- 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.',
|
||||
_('loyalty.admin.program_edit.delete_title'),
|
||||
_('loyalty.admin.program_edit.delete_message'),
|
||||
'deleteProgram()',
|
||||
'showDeleteModal',
|
||||
'Delete Program',
|
||||
'Cancel',
|
||||
_('loyalty.admin.program_edit.delete_confirm'),
|
||||
_('loyalty.common.cancel'),
|
||||
'danger'
|
||||
) }}
|
||||
{% endblock %}
|
||||
|
||||
@@ -6,16 +6,18 @@
|
||||
{% from 'shared/macros/tables.html' import table_wrapper, table_header %}
|
||||
{% from 'shared/macros/modals.html' import modal, confirm_modal_dynamic %}
|
||||
|
||||
{% block title %}Loyalty Programs{% endblock %}
|
||||
{% block title %}{{ _('loyalty.admin.programs.title') }}{% endblock %}
|
||||
|
||||
{% block i18n_modules %}['loyalty']{% endblock %}
|
||||
|
||||
{% block alpine_data %}adminLoyaltyPrograms(){% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{{ page_header('Loyalty Programs', action_label='Create Program', action_onclick="showCreateModal = true") }}
|
||||
{{ page_header(_('loyalty.admin.programs.title'), action_label=_('loyalty.admin.programs.create_program'), action_onclick="showCreateModal = true") }}
|
||||
|
||||
{{ loading_state('Loading loyalty programs...') }}
|
||||
{{ loading_state(_('loyalty.admin.programs.loading')) }}
|
||||
|
||||
{{ error_state('Error loading loyalty programs') }}
|
||||
{{ error_state(_('loyalty.admin.programs.error_loading')) }}
|
||||
|
||||
<!-- Stats Cards -->
|
||||
<div x-show="!loading" class="grid gap-6 mb-8 md:grid-cols-2 xl:grid-cols-4">
|
||||
@@ -26,7 +28,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
Total Programs
|
||||
{{ _('loyalty.admin.programs.total_programs') }}
|
||||
</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="stats.total_programs || 0">
|
||||
0
|
||||
@@ -41,7 +43,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
Active
|
||||
{{ _('loyalty.admin.programs.active') }}
|
||||
</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="stats.active_programs || 0">
|
||||
0
|
||||
@@ -56,7 +58,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
Total Members
|
||||
{{ _('loyalty.admin.programs.total_members') }}
|
||||
</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="formatNumber(stats.total_cards) || 0">
|
||||
0
|
||||
@@ -71,7 +73,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
Transactions (30d)
|
||||
{{ _('loyalty.admin.programs.transactions_30d') }}
|
||||
</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="formatNumber(stats.transactions_30d) || 0">
|
||||
0
|
||||
@@ -93,7 +95,7 @@
|
||||
type="text"
|
||||
x-model="filters.search"
|
||||
@input="debouncedSearch()"
|
||||
placeholder="Search by merchant name..."
|
||||
placeholder="{{ _('loyalty.admin.programs.search_placeholder') }}"
|
||||
class="w-full pl-10 pr-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>
|
||||
@@ -107,19 +109,19 @@
|
||||
@change="pagination.page = 1; loadPrograms()"
|
||||
class="px-4 py-2 text-sm text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none"
|
||||
>
|
||||
<option value="">All Status</option>
|
||||
<option value="true">Active</option>
|
||||
<option value="false">Inactive</option>
|
||||
<option value="">{{ _('loyalty.admin.programs.all_status') }}</option>
|
||||
<option value="true">{{ _('loyalty.common.active') }}</option>
|
||||
<option value="false">{{ _('loyalty.common.inactive') }}</option>
|
||||
</select>
|
||||
|
||||
<!-- Refresh Button -->
|
||||
<button
|
||||
@click="loadPrograms(); loadStats()"
|
||||
class="flex items-center 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 focus:outline-none transition-colors"
|
||||
title="Refresh"
|
||||
title="{{ _('loyalty.common.refresh') }}"
|
||||
>
|
||||
<span x-html="$icon('refresh', 'w-4 h-4 mr-2')"></span>
|
||||
Refresh
|
||||
{{ _('loyalty.common.refresh') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -128,7 +130,7 @@
|
||||
<!-- Programs Table -->
|
||||
<div x-show="!loading">
|
||||
{% call table_wrapper() %}
|
||||
{{ table_header(['Merchant', 'Program Type', 'Members', 'Points Issued', 'Status', 'Created', 'Actions']) }}
|
||||
{{ table_header([_('loyalty.admin.programs.table_merchant'), _('loyalty.admin.programs.table_program_type'), _('loyalty.admin.programs.table_members'), _('loyalty.admin.programs.table_points_issued'), _('loyalty.admin.programs.table_status'), _('loyalty.admin.programs.table_created'), _('loyalty.admin.programs.table_actions')]) }}
|
||||
<tbody class="bg-white divide-y dark:divide-gray-700 dark:bg-gray-800">
|
||||
<!-- Empty State -->
|
||||
<template x-if="programs.length === 0">
|
||||
@@ -136,8 +138,8 @@
|
||||
<td colspan="7" class="px-4 py-8 text-center text-gray-600 dark:text-gray-400">
|
||||
<div class="flex flex-col items-center">
|
||||
<span x-html="$icon('gift', 'w-12 h-12 mb-2 text-gray-300')"></span>
|
||||
<p class="font-medium">No loyalty programs found</p>
|
||||
<p class="text-xs mt-1" x-text="filters.search || filters.is_active ? 'Try adjusting your search or filters' : 'No merchants have set up loyalty programs yet'"></p>
|
||||
<p class="font-medium">{{ _('loyalty.admin.programs.no_programs') }}</p>
|
||||
<p class="text-xs mt-1" x-text="filters.search || filters.is_active ? $t('loyalty.admin.programs.adjust_filters') : $t('loyalty.admin.programs.no_merchants_yet')"></p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -175,23 +177,23 @@
|
||||
<span x-text="program.loyalty_type?.charAt(0).toUpperCase() + program.loyalty_type?.slice(1) || 'Unknown'"></span>
|
||||
</span>
|
||||
<p class="text-xs text-gray-500 mt-1" x-show="program.is_points_enabled">
|
||||
<span x-text="program.points_per_euro"></span> pt/EUR
|
||||
<span x-text="program.points_per_euro"></span> <span x-text="$t('loyalty.admin.programs.pt_per_eur')"></span>
|
||||
</p>
|
||||
</td>
|
||||
|
||||
<!-- Members -->
|
||||
<td class="px-4 py-3 text-sm">
|
||||
<span class="font-semibold" x-text="formatNumber(program.total_cards) || 0"></span>
|
||||
<span class="text-xs text-gray-500" x-show="program.active_cards">
|
||||
(<span x-text="formatNumber(program.active_cards)"></span> active)
|
||||
<span class="text-xs text-gray-500" x-show="program.active_cards"
|
||||
x-text="$t('loyalty.admin.programs.x_active', {count: program.active_cards})">
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<!-- Points Issued -->
|
||||
<td class="px-4 py-3 text-sm">
|
||||
<span x-text="formatNumber(program.total_points_issued) || 0"></span>
|
||||
<p class="text-xs text-gray-500" x-show="program.total_points_redeemed">
|
||||
<span x-text="formatNumber(program.total_points_redeemed)"></span> redeemed
|
||||
<p class="text-xs text-gray-500" x-show="program.total_points_redeemed"
|
||||
x-text="$t('loyalty.admin.programs.x_redeemed', {count: formatNumber(program.total_points_redeemed)})">
|
||||
</p>
|
||||
</td>
|
||||
|
||||
@@ -199,7 +201,7 @@
|
||||
<td class="px-4 py-3 text-xs">
|
||||
<span class="inline-flex items-center px-2 py-1 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 x-text="program.is_active ? $t('loyalty.common.active') : $t('loyalty.common.inactive')"></span>
|
||||
</span>
|
||||
</td>
|
||||
|
||||
@@ -213,7 +215,7 @@
|
||||
<a
|
||||
:href="'/admin/loyalty/merchants/' + program.merchant_id"
|
||||
class="flex items-center justify-center p-2 text-blue-600 rounded-lg hover:bg-blue-50 dark:text-blue-400 dark:hover:bg-gray-700 focus:outline-none transition-colors"
|
||||
title="View merchant loyalty details"
|
||||
title="{{ _('loyalty.common.view') }}"
|
||||
>
|
||||
<span x-html="$icon('eye', 'w-5 h-5')"></span>
|
||||
</a>
|
||||
@@ -222,7 +224,7 @@
|
||||
<a
|
||||
:href="'/admin/loyalty/merchants/' + program.merchant_id + '/program'"
|
||||
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="Edit program configuration"
|
||||
title="{{ _('loyalty.common.edit') }}"
|
||||
>
|
||||
<span x-html="$icon('edit', 'w-5 h-5')"></span>
|
||||
</a>
|
||||
@@ -231,7 +233,7 @@
|
||||
<button
|
||||
@click="confirmDeleteProgram(program)"
|
||||
class="flex items-center justify-center p-2 text-red-600 rounded-lg hover:bg-red-50 dark:text-red-400 dark:hover:bg-gray-700 focus:outline-none transition-colors"
|
||||
title="Delete program"
|
||||
title="{{ _('loyalty.common.delete') }}"
|
||||
>
|
||||
<span x-html="$icon('delete', 'w-5 h-5')"></span>
|
||||
</button>
|
||||
@@ -258,24 +260,24 @@
|
||||
<!-- Delete Confirmation Modal -->
|
||||
{{ confirm_modal_dynamic(
|
||||
'deleteProgramModal',
|
||||
'Delete Loyalty Program',
|
||||
"'Delete the loyalty program for \"' + (deletingProgram?.merchant_name || '') + '\"? This will permanently remove all associated data (cards, transactions, rewards). This cannot be undone.'",
|
||||
_('loyalty.admin.programs.delete_title'),
|
||||
"$t('loyalty.admin.programs.delete_message', {name: deletingProgram?.merchant_name || ''})",
|
||||
'deleteProgram()',
|
||||
'showDeleteModal',
|
||||
'Delete Program',
|
||||
'Cancel',
|
||||
_('loyalty.admin.programs.delete_confirm'),
|
||||
_('loyalty.common.cancel'),
|
||||
'danger'
|
||||
) }}
|
||||
|
||||
<!-- Create Program Modal -->
|
||||
{% call modal('createProgramModal', 'Create Loyalty Program', 'showCreateModal', show_footer=false) %}
|
||||
{% call modal('createProgramModal', _('loyalty.admin.programs.create_title'), 'showCreateModal', show_footer=false) %}
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||
Select a merchant to create a loyalty program for.
|
||||
{{ _('loyalty.admin.programs.create_description') }}
|
||||
</p>
|
||||
|
||||
<!-- Merchant Search -->
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Search Merchant</label>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">{{ _('loyalty.admin.programs.search_merchant') }}</label>
|
||||
<div class="relative">
|
||||
<span class="absolute inset-y-0 left-0 flex items-center pl-3">
|
||||
<span x-html="$icon('search', 'w-4 h-4 text-gray-400')"></span>
|
||||
@@ -283,7 +285,7 @@
|
||||
<input type="text"
|
||||
x-model="merchantSearch"
|
||||
@input="searchMerchants()"
|
||||
placeholder="Type merchant name..."
|
||||
placeholder="{{ _('loyalty.admin.programs.type_merchant_name') }}"
|
||||
class="w-full pl-10 pr-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>
|
||||
@@ -304,7 +306,7 @@
|
||||
</div>
|
||||
|
||||
<div x-show="merchantSearch && merchantResults.length === 0 && !searchingMerchants" class="mb-4 text-sm text-gray-500 text-center py-4">
|
||||
No merchants found
|
||||
{{ _('loyalty.admin.programs.no_merchants_found') }}
|
||||
</div>
|
||||
|
||||
<!-- Existing program warning -->
|
||||
@@ -313,11 +315,11 @@
|
||||
<div class="flex items-start">
|
||||
<span x-html="$icon('exclamation-triangle', 'w-5 h-5 text-yellow-500 mr-3 mt-0.5 flex-shrink-0')"></span>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-yellow-800 dark:text-yellow-200">This merchant already has a loyalty program.</p>
|
||||
<p class="text-sm font-medium text-yellow-800 dark:text-yellow-200">{{ _('loyalty.admin.programs.existing_program_warning') }}</p>
|
||||
<a :href="'/admin/loyalty/merchants/' + selectedMerchant.id + '/program'"
|
||||
class="inline-flex items-center mt-1 text-sm text-purple-600 hover:text-purple-700 dark:text-purple-400">
|
||||
<span x-html="$icon('eye', 'w-4 h-4 mr-1')"></span>
|
||||
View / Edit existing program
|
||||
{{ _('loyalty.admin.programs.view_edit_existing') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -327,12 +329,12 @@
|
||||
<div class="flex justify-end gap-3">
|
||||
<button @click="showCreateModal = false; merchantSearch = ''; merchantResults = []; selectedMerchant = null"
|
||||
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
|
||||
{{ _('loyalty.common.cancel') }}
|
||||
</button>
|
||||
<button @click="goToCreateProgram()"
|
||||
:disabled="!selectedMerchant || existingProgramForMerchant(selectedMerchant?.id)"
|
||||
class="px-4 py-2 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700 disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
Continue
|
||||
{{ _('loyalty.common.continue') }}
|
||||
</button>
|
||||
</div>
|
||||
{% endcall %}
|
||||
|
||||
@@ -3,32 +3,34 @@
|
||||
{% from 'shared/macros/headers.html' import page_header_flex, refresh_button %}
|
||||
{% from 'shared/macros/alerts.html' import loading_state, error_state %}
|
||||
|
||||
{% block title %}Loyalty Analytics{% endblock %}
|
||||
{% block title %}{{ _('loyalty.merchant.analytics.title') }}{% endblock %}
|
||||
|
||||
{% block i18n_modules %}['loyalty']{% endblock %}
|
||||
|
||||
{% block alpine_data %}merchantLoyaltyAnalytics(){% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% call page_header_flex(title='Loyalty Analytics', subtitle='Loyalty program statistics across all your stores') %}
|
||||
{% call page_header_flex(title=_('loyalty.merchant.analytics.title'), subtitle=_('loyalty.merchant.analytics.subtitle')) %}
|
||||
<div class="flex items-center gap-3">
|
||||
{{ refresh_button(loading_var='loading', onclick='loadStats()', variant='secondary') }}
|
||||
</div>
|
||||
{% endcall %}
|
||||
|
||||
{{ loading_state('Loading analytics...') }}
|
||||
{{ loading_state(_('loyalty.merchant.analytics.loading')) }}
|
||||
|
||||
{{ error_state('Error loading analytics') }}
|
||||
{{ error_state(_('loyalty.merchant.analytics.error_loading')) }}
|
||||
|
||||
<!-- No Program State -->
|
||||
<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('gift', '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">No Loyalty Program</h3>
|
||||
<p class="mt-1 text-sm text-yellow-700 dark:text-yellow-300">Set up a loyalty program to see analytics here.</p>
|
||||
<h3 class="text-sm font-semibold text-yellow-800 dark:text-yellow-200">{{ _('loyalty.merchant.analytics.no_program') }}</h3>
|
||||
<p class="mt-1 text-sm text-yellow-700 dark:text-yellow-300">{{ _('loyalty.merchant.analytics.no_program_desc') }}</p>
|
||||
<a href="/merchants/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('plus', 'w-4 h-4 mr-2')"></span>
|
||||
Create Program
|
||||
{{ _('loyalty.merchant.analytics.create_program') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -43,17 +45,17 @@
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="px-4 py-5 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">Quick Actions</h3>
|
||||
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">{{ _('loyalty.merchant.analytics.quick_actions') }}</h3>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<a href="/merchants/loyalty/program"
|
||||
class="inline-flex items-center px-4 py-2 text-sm font-medium text-purple-600 bg-purple-100 rounded-lg hover:bg-purple-200 dark:text-purple-300 dark:bg-purple-900/30 dark:hover:bg-purple-900/50">
|
||||
<span x-html="$icon('eye', 'w-4 h-4 mr-2')"></span>
|
||||
View Program
|
||||
{{ _('loyalty.merchant.analytics.view_program') }}
|
||||
</a>
|
||||
<a href="/merchants/loyalty/program/edit"
|
||||
class="inline-flex items-center px-4 py-2 text-sm font-medium text-blue-600 bg-blue-100 rounded-lg hover:bg-blue-200 dark:text-blue-300 dark:bg-blue-900/30 dark:hover:bg-blue-900/50">
|
||||
<span x-html="$icon('pencil', 'w-4 h-4 mr-2')"></span>
|
||||
Edit Program
|
||||
{{ _('loyalty.merchant.analytics.edit_program') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,15 +4,17 @@
|
||||
{% from 'shared/macros/alerts.html' import loading_state, error_state %}
|
||||
{% from 'shared/macros/modals.html' import confirm_modal %}
|
||||
|
||||
{% block title %}Loyalty Settings{% endblock %}
|
||||
{% block title %}{{ _('loyalty.merchant.program_edit.title') }}{% endblock %}
|
||||
|
||||
{% block i18n_modules %}['loyalty']{% endblock %}
|
||||
|
||||
{% block alpine_data %}merchantLoyaltySettings(){% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% call page_header_flex(title='Loyalty Program Settings', subtitle='Configure your loyalty program') %}{% endcall %}
|
||||
{% call page_header_flex(title=_('loyalty.merchant.program_edit.page_title'), subtitle=_('loyalty.merchant.program_edit.subtitle')) %}{% endcall %}
|
||||
|
||||
{{ loading_state('Loading settings...') }}
|
||||
{{ error_state('Error loading settings') }}
|
||||
{{ loading_state(_('loyalty.merchant.program_edit.loading')) }}
|
||||
{{ error_state(_('loyalty.merchant.program_edit.error_loading')) }}
|
||||
|
||||
<div x-show="!loading">
|
||||
<form @submit.prevent="saveSettings">
|
||||
@@ -26,12 +28,12 @@
|
||||
<!-- 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.',
|
||||
_('loyalty.merchant.program_edit.delete_title'),
|
||||
_('loyalty.merchant.program_edit.delete_message'),
|
||||
'deleteProgram()',
|
||||
'showDeleteModal',
|
||||
'Delete Program',
|
||||
'Cancel',
|
||||
_('loyalty.common.delete'),
|
||||
_('loyalty.common.cancel'),
|
||||
'danger'
|
||||
) }}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
{# app/modules/loyalty/templates/loyalty/merchant/program.html #}
|
||||
{% extends "merchant/base.html" %}
|
||||
|
||||
{% block title %}Loyalty Program{% endblock %}
|
||||
{% block title %}{{ _('loyalty.merchant.program.title') }}{% endblock %}
|
||||
|
||||
{% block i18n_modules %}['loyalty']{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div x-data="merchantLoyaltyProgram()">
|
||||
@@ -9,14 +11,14 @@
|
||||
<!-- 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>
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-gray-100">{{ _('loyalty.merchant.program.title') }}</h2>
|
||||
<p class="mt-1 text-gray-500 dark:text-gray-400">{{ _('loyalty.merchant.program.subtitle') }}</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
|
||||
{{ _('loyalty.merchant.program.edit_program') }}
|
||||
</a>
|
||||
</template>
|
||||
</div>
|
||||
@@ -25,14 +27,14 @@
|
||||
<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>
|
||||
<h3 class="mt-4 text-lg font-medium text-gray-900 dark:text-white">{{ _('loyalty.merchant.program.no_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.
|
||||
{{ _('loyalty.merchant.program.no_program_desc') }}
|
||||
</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
|
||||
{{ _('loyalty.merchant.program.create_program') }}
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -15,11 +15,9 @@
|
||||
<span x-html="$icon('gift', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">Total Programs</p>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">{{ _('loyalty.shared.analytics.total_programs') }}</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="formatNumber(stats.total_programs)">0</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
<span x-text="stats.active_programs"></span> active
|
||||
</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400" x-text="$t('loyalty.shared.analytics.x_active', {count: stats.active_programs})"></p>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
@@ -29,11 +27,9 @@
|
||||
<span x-html="$icon('users', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">Total Members</p>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">{{ _('loyalty.shared.analytics.total_members') }}</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="formatNumber(stats.total_cards)">0</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
<span x-text="formatNumber(stats.active_cards)"></span> active
|
||||
</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400" x-text="$t('loyalty.shared.analytics.x_active', {count: formatNumber(stats.active_cards)})"></p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -45,7 +41,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
{% if show_programs_card %}Total Members{% else %}Active Members{% endif %}
|
||||
{% if show_programs_card %}{{ _('loyalty.shared.analytics.total_members') }}{% else %}{{ _('loyalty.shared.analytics.active_members') }}{% endif %}
|
||||
</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200"
|
||||
x-text="formatNumber({% if show_programs_card %}stats.total_cards{% else %}stats.active_cards{% endif %})">0</p>
|
||||
@@ -58,7 +54,7 @@
|
||||
<span x-html="$icon('trending-up', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">Points Issued (30d)</p>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">{{ _('loyalty.shared.analytics.points_issued_30d') }}</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="formatNumber(stats.points_issued_30d)">0</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -69,7 +65,7 @@
|
||||
<span x-html="$icon('chart-bar', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">Transactions (30d)</p>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">{{ _('loyalty.shared.analytics.transactions_30d') }}</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="formatNumber(stats.transactions_30d)">0</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -81,13 +77,13 @@
|
||||
<div class="px-4 py-5 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 Overview
|
||||
{{ _('loyalty.shared.analytics.points_overview') }}
|
||||
</h3>
|
||||
<div class="space-y-4">
|
||||
<!-- Progress bar: Issued vs Redeemed (30d) -->
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">Points Issued vs Redeemed (30d)</span>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">{{ _('loyalty.shared.analytics.points_issued_vs_redeemed') }}</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-4 dark:bg-gray-700">
|
||||
<div class="h-4 rounded-full flex">
|
||||
@@ -96,16 +92,16 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
<span><span class="inline-block w-3 h-3 bg-green-500 rounded-full mr-1"></span>Issued: <span x-text="formatNumber(stats.points_issued_30d)"></span></span>
|
||||
<span><span class="inline-block w-3 h-3 bg-orange-500 rounded-full mr-1"></span>Redeemed: <span x-text="formatNumber(stats.points_redeemed_30d)"></span></span>
|
||||
<span><span class="inline-block w-3 h-3 bg-green-500 rounded-full mr-1"></span>{{ _('loyalty.shared.analytics.issued') }} <span x-text="formatNumber(stats.points_issued_30d)"></span></span>
|
||||
<span><span class="inline-block w-3 h-3 bg-orange-500 rounded-full mr-1"></span>{{ _('loyalty.shared.analytics.redeemed') }} <span x-text="formatNumber(stats.points_redeemed_30d)"></span></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">Redemption Rate</span>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">{{ _('loyalty.shared.analytics.redemption_rate') }}</span>
|
||||
<span class="font-semibold text-gray-700 dark:text-gray-200" x-text="redemptionRate + '%'">0%</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">Outstanding Balance</span>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">{{ _('loyalty.shared.analytics.outstanding_balance') }}</span>
|
||||
<span class="font-semibold text-purple-600 dark:text-purple-400" x-text="formatNumber(stats.total_points_balance || 0)">0</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -115,25 +111,25 @@
|
||||
<div class="px-4 py-5 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('users', 'inline w-5 h-5 mr-2')"></span>
|
||||
Member Activity
|
||||
{{ _('loyalty.shared.analytics.member_activity') }}
|
||||
</h3>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">Active Members (30d)</span>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">{{ _('loyalty.shared.analytics.active_members_30d') }}</span>
|
||||
<span class="font-semibold text-gray-700 dark:text-gray-200" x-text="formatNumber(stats.active_cards)">0</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">New This Month</span>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">{{ _('loyalty.shared.analytics.new_this_month') }}</span>
|
||||
<span class="font-semibold text-gray-700 dark:text-gray-200" x-text="formatNumber(stats.new_this_month || 0)">0</span>
|
||||
</div>
|
||||
{% if show_merchants_metric %}
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">Merchants with Programs</span>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">{{ _('loyalty.shared.analytics.merchants_with_programs') }}</span>
|
||||
<span class="font-semibold text-gray-700 dark:text-gray-200" x-text="formatNumber(stats.merchants_with_programs || 0)">0</span>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">Avg Points Per Member</span>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">{{ _('loyalty.shared.analytics.avg_points_per_member') }}</span>
|
||||
<span class="font-semibold text-gray-700 dark:text-gray-200" x-text="formatNumber(stats.avg_points_per_member || 0)">0</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -144,24 +140,24 @@
|
||||
<!-- All-Time Statistics -->
|
||||
<div class="mb-8 bg-white dark:bg-gray-800 rounded-lg shadow-md overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200">All-Time Statistics</h3>
|
||||
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200">{{ _('loyalty.shared.analytics.all_time_statistics') }}</h3>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<div>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">Total Points Issued</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">{{ _('loyalty.shared.analytics.total_points_issued') }}</p>
|
||||
<p class="text-xl font-bold text-gray-900 dark:text-white" x-text="formatNumber(stats.total_points_issued || 0)">0</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">Total Points Redeemed</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">{{ _('loyalty.shared.analytics.total_points_redeemed') }}</p>
|
||||
<p class="text-xl font-bold text-gray-900 dark:text-white" x-text="formatNumber(stats.total_points_redeemed || 0)">0</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">Points Redeemed (30d)</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">{{ _('loyalty.shared.analytics.points_redeemed_30d') }}</p>
|
||||
<p class="text-xl font-bold text-gray-900 dark:text-white" x-text="formatNumber(stats.points_redeemed_30d || 0)">0</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">Outstanding Liability</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">{{ _('loyalty.shared.analytics.outstanding_liability') }}</p>
|
||||
<p class="text-xl font-bold text-gray-900 dark:text-white"
|
||||
x-text="'€' + ((stats.estimated_liability_cents || 0) / 100).toFixed(2)">0</p>
|
||||
</div>
|
||||
@@ -173,17 +169,17 @@
|
||||
{% if show_locations %}
|
||||
<div x-show="locations && locations.length > 0" class="mb-8 bg-white dark:bg-gray-800 rounded-lg shadow-md overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200">Location Breakdown</h3>
|
||||
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200">{{ _('loyalty.shared.analytics.location_breakdown') }}</h3>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full whitespace-nowrap">
|
||||
<thead>
|
||||
<tr class="text-xs font-semibold tracking-wide text-left text-gray-500 uppercase border-b dark:border-gray-700 bg-gray-50 dark:text-gray-400 dark:bg-gray-800">
|
||||
<th class="px-4 py-3">Store</th>
|
||||
<th class="px-4 py-3">Enrolled</th>
|
||||
<th class="px-4 py-3">Points Earned</th>
|
||||
<th class="px-4 py-3">Points Redeemed</th>
|
||||
<th class="px-4 py-3">Transactions (30d)</th>
|
||||
<th class="px-4 py-3">{{ _('loyalty.shared.analytics.store') }}</th>
|
||||
<th class="px-4 py-3">{{ _('loyalty.shared.analytics.enrolled') }}</th>
|
||||
<th class="px-4 py-3">{{ _('loyalty.shared.analytics.points_earned') }}</th>
|
||||
<th class="px-4 py-3">{{ _('loyalty.shared.analytics.points_redeemed') }}</th>
|
||||
<th class="px-4 py-3">{{ _('loyalty.shared.analytics.transactions_30d') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y dark:divide-gray-700 dark:bg-gray-800">
|
||||
|
||||
@@ -23,31 +23,31 @@
|
||||
<div x-show="isNewProgram" 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('squares-2x2', 'inline w-5 h-5 mr-2')"></span>
|
||||
Program Type
|
||||
{{ _('loyalty.shared.program_form.program_type') }}
|
||||
</h3>
|
||||
<div class="grid gap-4 md:grid-cols-3">
|
||||
<label class="flex items-start p-4 border rounded-lg cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||
:class="settings.loyalty_type === 'points' ? 'border-purple-500 bg-purple-50 dark:bg-purple-900/20' : 'border-gray-200 dark:border-gray-600'">
|
||||
<input type="radio" name="loyalty_type" value="points" x-model="settings.loyalty_type" class="mt-1 text-purple-600 form-radio">
|
||||
<div class="ml-3">
|
||||
<p class="font-medium text-gray-700 dark:text-gray-300">Points</p>
|
||||
<p class="text-sm text-gray-500">Earn points per EUR spent</p>
|
||||
<p class="font-medium text-gray-700 dark:text-gray-300">{{ _('loyalty.loyalty.program.type.points') }}</p>
|
||||
<p class="text-sm text-gray-500">{{ _('loyalty.shared.program_form.points_type_desc') }}</p>
|
||||
</div>
|
||||
</label>
|
||||
<label class="flex items-start p-4 border rounded-lg cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||
:class="settings.loyalty_type === 'stamps' ? 'border-purple-500 bg-purple-50 dark:bg-purple-900/20' : 'border-gray-200 dark:border-gray-600'">
|
||||
<input type="radio" name="loyalty_type" value="stamps" x-model="settings.loyalty_type" class="mt-1 text-purple-600 form-radio">
|
||||
<div class="ml-3">
|
||||
<p class="font-medium text-gray-700 dark:text-gray-300">Stamps</p>
|
||||
<p class="text-sm text-gray-500">Collect N stamps, get reward</p>
|
||||
<p class="font-medium text-gray-700 dark:text-gray-300">{{ _('loyalty.loyalty.program.type.stamps') }}</p>
|
||||
<p class="text-sm text-gray-500">{{ _('loyalty.shared.program_form.stamps_type_desc') }}</p>
|
||||
</div>
|
||||
</label>
|
||||
<label class="flex items-start p-4 border rounded-lg cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||
:class="settings.loyalty_type === 'hybrid' ? 'border-purple-500 bg-purple-50 dark:bg-purple-900/20' : 'border-gray-200 dark:border-gray-600'">
|
||||
<input type="radio" name="loyalty_type" value="hybrid" x-model="settings.loyalty_type" class="mt-1 text-purple-600 form-radio">
|
||||
<div class="ml-3">
|
||||
<p class="font-medium text-gray-700 dark:text-gray-300">Hybrid</p>
|
||||
<p class="text-sm text-gray-500">Both stamps and points</p>
|
||||
<p class="font-medium text-gray-700 dark:text-gray-300">{{ _('loyalty.loyalty.program.type.hybrid') }}</p>
|
||||
<p class="text-sm text-gray-500">{{ _('loyalty.shared.program_form.hybrid_type_desc') }}</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
@@ -57,22 +57,22 @@
|
||||
<div x-show="settings.loyalty_type === 'stamps' || settings.loyalty_type === 'hybrid'" 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('star', 'inline w-5 h-5 mr-2')"></span>
|
||||
Stamps Configuration
|
||||
{{ _('loyalty.shared.program_form.stamps_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">Stamps Target</label>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">{{ _('loyalty.shared.program_form.stamps_target') }}</label>
|
||||
<input type="number" min="2" max="50" x-model.number="settings.stamps_target"
|
||||
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">Number of stamps needed for reward</p>
|
||||
<p class="mt-1 text-xs text-gray-500">{{ _('loyalty.shared.program_form.stamps_target_help') }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Reward Description</label>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">{{ _('loyalty.shared.program_form.reward_description') }}</label>
|
||||
<input type="text" x-model="settings.stamps_reward_description" placeholder="e.g., Free coffee" maxlength="255"
|
||||
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">Reward Value (cents)</label>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">{{ _('loyalty.shared.program_form.reward_value_cents') }}</label>
|
||||
<input type="number" min="0" x-model.number="settings.stamps_reward_value_cents"
|
||||
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">
|
||||
@@ -84,38 +84,38 @@
|
||||
<div x-show="settings.loyalty_type === 'points' || settings.loyalty_type === 'hybrid'" 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
|
||||
{{ _('loyalty.shared.program_form.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>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">{{ _('loyalty.shared.program_form.points_per_eur') }}</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>
|
||||
<p class="mt-1 text-xs text-gray-500" x-text="$t('loyalty.shared.program_form.eur_equals_points', {points: settings.points_per_euro || 1})"></p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Welcome Bonus Points</label>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">{{ _('loyalty.shared.program_form.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>
|
||||
<p class="mt-1 text-xs text-gray-500">{{ _('loyalty.shared.program_form.welcome_bonus_help') }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Minimum Redemption Points</label>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">{{ _('loyalty.shared.program_form.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">Minimum Purchase (cents)</label>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">{{ _('loyalty.shared.program_form.minimum_purchase_cents') }}</label>
|
||||
<input type="number" min="0" x-model.number="settings.minimum_purchase_cents"
|
||||
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">Minimum purchase amount to earn points (0 = no minimum)</p>
|
||||
<p class="mt-1 text-xs text-gray-500">{{ _('loyalty.shared.program_form.minimum_purchase_help') }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Points Expiration (days)</label>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">{{ _('loyalty.shared.program_form.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>
|
||||
<p class="mt-1 text-xs text-gray-500">{{ _('loyalty.shared.program_form.points_expiration_help') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -125,33 +125,33 @@
|
||||
<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
|
||||
{{ _('loyalty.shared.program_form.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
|
||||
{{ _('loyalty.shared.program_form.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>
|
||||
<p class="text-gray-500 dark:text-gray-400 text-sm">{{ _('loyalty.shared.program_form.no_rewards_configured') }}</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>
|
||||
<label class="block text-xs text-gray-500 mb-1">{{ _('loyalty.shared.program_form.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>
|
||||
<label class="block text-xs text-gray-500 mb-1">{{ _('loyalty.shared.program_form.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>
|
||||
<label class="block text-xs text-gray-500 mb-1">{{ _('loyalty.shared.program_form.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>
|
||||
@@ -169,26 +169,26 @@
|
||||
<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('shield-check', 'inline w-5 h-5 mr-2')"></span>
|
||||
Anti-Fraud Settings
|
||||
{{ _('loyalty.shared.program_form.anti_fraud_settings') }}
|
||||
</h3>
|
||||
<div class="grid gap-6 md:grid-cols-3">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Cooldown (minutes)</label>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">{{ _('loyalty.shared.program_form.cooldown_minutes') }}</label>
|
||||
<input type="number" min="0" max="1440" x-model.number="settings.cooldown_minutes"
|
||||
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">Time between stamps from the same card</p>
|
||||
<p class="mt-1 text-xs text-gray-500">{{ _('loyalty.shared.program_form.cooldown_help') }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Max Daily Stamps</label>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">{{ _('loyalty.shared.program_form.max_daily_stamps') }}</label>
|
||||
<input type="number" min="1" max="50" x-model.number="settings.max_daily_stamps"
|
||||
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">Maximum stamps per card per day</p>
|
||||
<p class="mt-1 text-xs text-gray-500">{{ _('loyalty.shared.program_form.max_daily_stamps_help') }}</p>
|
||||
</div>
|
||||
<div class="flex items-end">
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" x-model="settings.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>
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">{{ _('loyalty.shared.program_form.require_staff_pin') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -198,16 +198,16 @@
|
||||
<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
|
||||
{{ _('loyalty.shared.program_form.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>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">{{ _('loyalty.shared.program_form.card_name') }}</label>
|
||||
<input type="text" x-model="settings.card_name" placeholder="e.g., VIP Rewards" maxlength="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-2">Primary Color</label>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">{{ _('loyalty.shared.program_form.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">
|
||||
@@ -216,7 +216,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Secondary Color</label>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">{{ _('loyalty.shared.program_form.secondary_color') }}</label>
|
||||
<div class="flex items-center gap-3">
|
||||
<input type="color" x-model="settings.card_secondary_color"
|
||||
class="w-12 h-10 rounded cursor-pointer">
|
||||
@@ -226,13 +226,13 @@
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Logo URL <span class="text-red-500">*</span></label>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">{{ _('loyalty.shared.program_form.logo_url') }} <span class="text-red-500">*</span></label>
|
||||
<input type="url" x-model="settings.logo_url" maxlength="500" placeholder="https://..." required
|
||||
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 dark:text-gray-400">Required for Google Wallet integration. Must be a publicly accessible image URL (PNG or JPG).</p>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">{{ _('loyalty.shared.program_form.logo_url_help') }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Hero Image URL</label>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">{{ _('loyalty.shared.program_form.hero_image_url') }}</label>
|
||||
<input type="url" x-model="settings.hero_image_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>
|
||||
@@ -243,16 +243,16 @@
|
||||
<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('document-text', 'inline w-5 h-5 mr-2')"></span>
|
||||
Terms & Privacy
|
||||
{{ _('loyalty.shared.program_form.terms_privacy') }}
|
||||
</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">Terms & Conditions</label>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">{{ _('loyalty.shared.program_form.terms_conditions') }}</label>
|
||||
<textarea x-model="settings.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>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Privacy Policy URL</label>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">{{ _('loyalty.shared.program_form.privacy_policy_url') }}</label>
|
||||
<input type="url" x-model="settings.privacy_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>
|
||||
@@ -264,12 +264,12 @@
|
||||
<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
|
||||
{{ _('loyalty.shared.program_form.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</p>
|
||||
<p class="font-medium text-gray-700 dark:text-gray-300">{{ _('loyalty.shared.program_form.program_active') }}</p>
|
||||
<p class="text-sm text-gray-500">{{ _('loyalty.shared.program_form.program_active_help') }}</p>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<input type="checkbox" x-model="settings.is_active" class="sr-only peer">
|
||||
@@ -292,7 +292,7 @@
|
||||
<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
|
||||
{{ _('loyalty.shared.program_form.delete_program') }}
|
||||
</button>
|
||||
</template>
|
||||
{% endif %}
|
||||
@@ -300,12 +300,12 @@
|
||||
<div class="flex items-center gap-3">
|
||||
<a href="{{ cancel_url }}"
|
||||
class="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">
|
||||
Cancel
|
||||
{{ _('loyalty.common.cancel') }}
|
||||
</a>
|
||||
<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 Changes')"></span>
|
||||
<span x-text="saving ? $t('loyalty.common.saving') : (isNewProgram ? $t('loyalty.shared.program_form.create_program') : $t('loyalty.shared.program_form.save_changes'))"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<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
|
||||
{{ _('loyalty.shared.program_view.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"
|
||||
@@ -28,13 +28,13 @@
|
||||
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 x-text="program?.is_active ? $t('loyalty.common.active') : $t('loyalty.common.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
|
||||
{{ _('loyalty.common.edit') }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -43,11 +43,11 @@
|
||||
<!-- 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-xs font-semibold text-gray-600 dark:text-gray-400 uppercase mb-2">{{ _('loyalty.shared.program_view.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-xs font-semibold text-gray-600 dark:text-gray-400 uppercase mb-2">{{ _('loyalty.shared.program_view.card_name') }}</p>
|
||||
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="program?.card_name || '-'">-</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -57,19 +57,19 @@
|
||||
<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
|
||||
{{ _('loyalty.shared.program_view.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-xs text-gray-500 dark:text-gray-400 mb-1">{{ _('loyalty.shared.program_view.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-xs text-gray-500 dark:text-gray-400 mb-1">{{ _('loyalty.shared.program_view.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-xs text-gray-500 dark:text-gray-400 mb-1">{{ _('loyalty.shared.program_view.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>
|
||||
@@ -82,32 +82,32 @@
|
||||
<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
|
||||
{{ _('loyalty.shared.program_view.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-xs text-gray-500 dark:text-gray-400 mb-1">{{ _('loyalty.shared.program_view.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-xs text-gray-500 dark:text-gray-400 mb-1">{{ _('loyalty.shared.program_view.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>
|
||||
x-text="program?.welcome_bonus_points ? $t('loyalty.shared.program_view.x_points', {count: program.welcome_bonus_points}) : $t('loyalty.common.none')">-</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mb-1">Minimum Redemption</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mb-1">{{ _('loyalty.shared.program_view.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>
|
||||
x-text="program?.minimum_redemption_points ? $t('loyalty.shared.program_view.x_points', {count: program.minimum_redemption_points}) : $t('loyalty.common.none')">-</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mb-1">Minimum Purchase</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mb-1">{{ _('loyalty.shared.program_view.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>
|
||||
x-text="program?.minimum_purchase_cents ? '€' + (program.minimum_purchase_cents / 100).toFixed(2) : $t('loyalty.common.none')">-</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mb-1">Points Expiration</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mb-1">{{ _('loyalty.shared.program_view.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>
|
||||
x-text="program?.points_expiration_days ? $t('loyalty.shared.program_view.x_days_inactivity', {days: program.points_expiration_days}) : $t('loyalty.common.never')">-</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -118,15 +118,15 @@
|
||||
<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
|
||||
{{ _('loyalty.shared.program_view.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>
|
||||
<th class="px-4 py-2 text-left text-xs font-semibold text-gray-600 dark:text-gray-300 uppercase">{{ _('loyalty.shared.program_view.reward') }}</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-semibold text-gray-600 dark:text-gray-300 uppercase">{{ _('loyalty.shared.program_view.points_required') }}</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-semibold text-gray-600 dark:text-gray-300 uppercase">{{ _('loyalty.shared.program_view.description') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
@@ -147,23 +147,23 @@
|
||||
<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
|
||||
{{ _('loyalty.shared.program_view.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-xs text-gray-500 dark:text-gray-400 mb-1">{{ _('loyalty.shared.program_view.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>
|
||||
x-text="program?.cooldown_minutes ? $t('loyalty.shared.program_view.x_minutes', {count: program.cooldown_minutes}) : $t('loyalty.common.none')">-</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mb-1">Max Daily Stamps</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mb-1">{{ _('loyalty.shared.program_view.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>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mb-1">{{ _('loyalty.shared.program_view.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 x-text="program?.require_staff_pin ? $t('loyalty.common.yes') : $t('loyalty.common.no')"></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -173,11 +173,11 @@
|
||||
<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
|
||||
{{ _('loyalty.shared.program_view.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>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mb-1">{{ _('loyalty.shared.program_view.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>
|
||||
@@ -185,7 +185,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mb-1">Secondary Color</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mb-1">{{ _('loyalty.shared.program_view.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>
|
||||
@@ -193,11 +193,11 @@
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mb-1">Logo URL</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mb-1">{{ _('loyalty.shared.program_view.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-xs text-gray-500 dark:text-gray-400 mb-1">{{ _('loyalty.shared.program_view.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>
|
||||
@@ -207,15 +207,15 @@
|
||||
<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
|
||||
{{ _('loyalty.shared.program_view.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-xs text-gray-500 dark:text-gray-400 mb-1">{{ _('loyalty.shared.program_view.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>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mb-1">{{ _('loyalty.shared.program_view.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>
|
||||
|
||||
@@ -3,35 +3,37 @@
|
||||
{% from 'shared/macros/headers.html' import page_header_flex, refresh_button %}
|
||||
{% from 'shared/macros/alerts.html' import loading_state, error_state %}
|
||||
|
||||
{% block title %}Loyalty Analytics{% endblock %}
|
||||
{% block title %}{{ _('loyalty.store.analytics.title') }}{% endblock %}
|
||||
|
||||
{% block i18n_modules %}['loyalty']{% endblock %}
|
||||
|
||||
{% block alpine_data %}storeLoyaltyAnalytics(){% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% call page_header_flex(title='Loyalty Analytics', subtitle='Track your loyalty program performance') %}
|
||||
{% call page_header_flex(title=_('loyalty.store.analytics.title'), subtitle=_('loyalty.store.analytics.subtitle')) %}
|
||||
<div class="flex items-center gap-3">
|
||||
{{ refresh_button(loading_var='loading', onclick='loadStats()', variant='secondary') }}
|
||||
</div>
|
||||
{% endcall %}
|
||||
|
||||
{{ loading_state('Loading analytics...') }}
|
||||
{{ error_state('Error loading analytics') }}
|
||||
{{ loading_state(_('loyalty.store.analytics.loading')) }}
|
||||
{{ error_state(_('loyalty.store.analytics.error_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>
|
||||
<h3 class="text-sm font-semibold text-yellow-800 dark:text-yellow-200">{{ _('loyalty.common.program_not_setup') }}</h3>
|
||||
<p class="mt-1 text-sm text-yellow-700 dark:text-yellow-300">{{ _('loyalty.common.program_not_setup_desc') }}</p>
|
||||
{% if user.role == 'merchant_owner' %}
|
||||
<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
|
||||
{{ _('loyalty.common.setup_program') }}
|
||||
</a>
|
||||
{% else %}
|
||||
<p class="mt-2 text-sm text-yellow-700 dark:text-yellow-300">Contact your administrator to complete the setup.</p>
|
||||
<p class="mt-2 text-sm text-yellow-700 dark:text-yellow-300">{{ _('loyalty.common.contact_admin_setup') }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -46,22 +48,22 @@
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="px-4 py-5 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">Quick Actions</h3>
|
||||
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">{{ _('loyalty.store.analytics.quick_actions') }}</h3>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<a href="/store/{{ store_code }}/loyalty/terminal"
|
||||
class="inline-flex items-center px-4 py-2 text-sm font-medium text-purple-600 bg-purple-100 rounded-lg hover:bg-purple-200 dark:text-purple-300 dark:bg-purple-900/30 dark:hover:bg-purple-900/50">
|
||||
<span x-html="$icon('device-tablet', 'w-4 h-4 mr-2')"></span>
|
||||
Open Terminal
|
||||
{{ _('loyalty.store.analytics.open_terminal') }}
|
||||
</a>
|
||||
<a href="/store/{{ store_code }}/loyalty/cards"
|
||||
class="inline-flex items-center px-4 py-2 text-sm font-medium text-blue-600 bg-blue-100 rounded-lg hover:bg-blue-200 dark:text-blue-300 dark:bg-blue-900/30 dark:hover:bg-blue-900/50">
|
||||
<span x-html="$icon('users', 'w-4 h-4 mr-2')"></span>
|
||||
View Members
|
||||
{{ _('loyalty.store.analytics.view_members') }}
|
||||
</a>
|
||||
<a href="/store/{{ store_code }}/loyalty/program"
|
||||
class="inline-flex items-center px-4 py-2 text-sm font-medium text-gray-600 bg-gray-100 rounded-lg hover:bg-gray-200 dark:text-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600">
|
||||
<span x-html="$icon('eye', 'w-4 h-4 mr-2')"></span>
|
||||
View Program
|
||||
{{ _('loyalty.store.analytics.view_program') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,17 +4,19 @@
|
||||
{% from 'shared/macros/alerts.html' import loading_state, error_state %}
|
||||
{% from 'shared/macros/tables.html' import table_wrapper, table_header %}
|
||||
|
||||
{% block title %}Member Details{% endblock %}
|
||||
{% block title %}{{ _('loyalty.store.card_detail.title') }}{% endblock %}
|
||||
|
||||
{% block i18n_modules %}['loyalty']{% endblock %}
|
||||
|
||||
{% block alpine_data %}storeLoyaltyCardDetail(){% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% call detail_page_header("card?.customer_name || 'Member Details'", '/store/' + store_code + '/loyalty/cards', subtitle_show='card') %}
|
||||
Card: <span x-text="card?.card_number"></span>
|
||||
{% call detail_page_header("card?.customer_name || '" + _('loyalty.store.card_detail.title') + "'", '/store/' + store_code + '/loyalty/cards', subtitle_show='card') %}
|
||||
{{ _('loyalty.store.card_detail.card_label') }}: <span x-text="card?.card_number"></span>
|
||||
{% endcall %}
|
||||
|
||||
{{ loading_state('Loading member details...') }}
|
||||
{{ error_state('Error loading member') }}
|
||||
{{ loading_state(_('loyalty.store.card_detail.loading')) }}
|
||||
{{ error_state(_('loyalty.store.card_detail.error_loading')) }}
|
||||
|
||||
<div x-show="!loading && card">
|
||||
<!-- Quick Stats -->
|
||||
@@ -24,7 +26,7 @@
|
||||
<span x-html="$icon('currency-dollar', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">Points Balance</p>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">{{ _('loyalty.store.card_detail.points_balance') }}</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="formatNumber(card?.points_balance)">0</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -33,7 +35,7 @@
|
||||
<span x-html="$icon('trending-up', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">Total Earned</p>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">{{ _('loyalty.store.card_detail.total_earned') }}</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="formatNumber(card?.total_points_earned)">0</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -42,7 +44,7 @@
|
||||
<span x-html="$icon('gift', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">Total Redeemed</p>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">{{ _('loyalty.store.card_detail.total_redeemed') }}</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="formatNumber(card?.total_points_redeemed)">0</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -51,7 +53,7 @@
|
||||
<span x-html="$icon('calendar', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">Member Since</p>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">{{ _('loyalty.store.card_detail.member_since') }}</p>
|
||||
<p class="text-sm font-semibold text-gray-700 dark:text-gray-200" x-text="formatDate(card?.created_at)">-</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -62,23 +64,23 @@
|
||||
<div class="px-4 py-5 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('user', 'inline w-5 h-5 mr-2')"></span>
|
||||
Customer Information
|
||||
{{ _('loyalty.store.card_detail.customer_information') }}
|
||||
</h3>
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Name</p>
|
||||
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">{{ _('loyalty.store.card_detail.name') }}</p>
|
||||
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="card?.customer_name || '-'">-</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Email</p>
|
||||
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">{{ _('loyalty.store.card_detail.email') }}</p>
|
||||
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="card?.customer_email || '-'">-</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Phone</p>
|
||||
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">{{ _('loyalty.store.card_detail.phone') }}</p>
|
||||
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="card?.customer_phone || '-'">-</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Birthday</p>
|
||||
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">{{ _('loyalty.store.card_detail.birthday') }}</p>
|
||||
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="card?.customer_birthday || '-'">-</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -88,25 +90,25 @@
|
||||
<div class="px-4 py-5 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('credit-card', 'inline w-5 h-5 mr-2')"></span>
|
||||
Card Details
|
||||
{{ _('loyalty.store.card_detail.card_details') }}
|
||||
</h3>
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Card Number</p>
|
||||
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">{{ _('loyalty.store.card_detail.card_number') }}</p>
|
||||
<p class="text-sm font-mono text-gray-700 dark:text-gray-300" x-text="card?.card_number">-</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Status</p>
|
||||
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">{{ _('loyalty.store.card_detail.status') }}</p>
|
||||
<span class="px-2 py-1 text-xs font-semibold rounded-full"
|
||||
:class="card?.is_active ? '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'"
|
||||
x-text="card?.is_active ? 'Active' : 'Inactive'"></span>
|
||||
x-text="card?.is_active ? $t('loyalty.common.active') : $t('loyalty.common.inactive')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Last Activity</p>
|
||||
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">{{ _('loyalty.store.card_detail.last_activity') }}</p>
|
||||
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="formatDate(card?.last_activity_at) || 'Never'">-</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Enrolled At</p>
|
||||
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">{{ _('loyalty.store.card_detail.enrolled_at') }}</p>
|
||||
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="card?.enrolled_at_store_name || 'Unknown'">-</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -117,15 +119,15 @@
|
||||
<div class="px-4 py-5 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('clock', 'inline w-5 h-5 mr-2')"></span>
|
||||
Transaction History
|
||||
{{ _('loyalty.store.card_detail.transaction_history') }}
|
||||
</h3>
|
||||
{% call table_wrapper() %}
|
||||
{{ table_header(['Date', 'Type', 'Points', 'Location', 'Notes']) }}
|
||||
{{ table_header([_('loyalty.store.card_detail.col_date'), _('loyalty.store.card_detail.col_type'), _('loyalty.store.card_detail.col_points'), _('loyalty.store.card_detail.col_location'), _('loyalty.store.card_detail.col_notes')]) }}
|
||||
<tbody class="bg-white divide-y dark:divide-gray-700 dark:bg-gray-800">
|
||||
<template x-if="transactions.length === 0">
|
||||
<tr>
|
||||
<td colspan="5" class="px-4 py-6 text-center text-gray-500 dark:text-gray-400">
|
||||
No transactions yet
|
||||
{{ _('loyalty.store.card_detail.no_transactions') }}
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
@@ -5,42 +5,44 @@
|
||||
{% from 'shared/macros/alerts.html' import loading_state, error_state %}
|
||||
{% from 'shared/macros/tables.html' import table_wrapper, table_header %}
|
||||
|
||||
{% block title %}Loyalty Members{% endblock %}
|
||||
{% block title %}{{ _('loyalty.store.cards.title') }}{% endblock %}
|
||||
|
||||
{% block i18n_modules %}['loyalty']{% endblock %}
|
||||
|
||||
{% block alpine_data %}storeLoyaltyCards(){% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Page Header -->
|
||||
{% call page_header_flex(title='Loyalty Members', subtitle='View and manage your loyalty program members') %}
|
||||
{% call page_header_flex(title=_('loyalty.store.cards.title'), subtitle=_('loyalty.store.cards.subtitle')) %}
|
||||
<div class="flex items-center gap-3">
|
||||
{{ refresh_button(loading_var='loading', onclick='loadCards()', variant='secondary') }}
|
||||
<a href="/store/{{ store_code }}/loyalty/enroll"
|
||||
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('user-plus', 'w-4 h-4 mr-2')"></span>
|
||||
Enroll New
|
||||
{{ _('loyalty.store.cards.enroll_new') }}
|
||||
</a>
|
||||
</div>
|
||||
{% endcall %}
|
||||
|
||||
{{ loading_state('Loading members...') }}
|
||||
{{ loading_state(_('loyalty.store.cards.loading')) }}
|
||||
|
||||
{{ error_state('Error loading members') }}
|
||||
{{ error_state(_('loyalty.store.cards.error_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>
|
||||
<h3 class="text-sm font-semibold text-yellow-800 dark:text-yellow-200">{{ _('loyalty.common.program_not_setup') }}</h3>
|
||||
<p class="mt-1 text-sm text-yellow-700 dark:text-yellow-300">{{ _('loyalty.common.program_not_setup_desc') }}</p>
|
||||
{% if user.role == 'merchant_owner' %}
|
||||
<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
|
||||
{{ _('loyalty.common.setup_program') }}
|
||||
</a>
|
||||
{% else %}
|
||||
<p class="mt-2 text-sm text-yellow-700 dark:text-yellow-300">Contact your administrator to complete the setup.</p>
|
||||
<p class="mt-2 text-sm text-yellow-700 dark:text-yellow-300">{{ _('loyalty.common.contact_admin_setup') }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -53,7 +55,7 @@
|
||||
<span x-html="$icon('users', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">Total Members</p>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">{{ _('loyalty.store.cards.total_members') }}</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="formatNumber(stats.total_cards)">0</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -62,7 +64,7 @@
|
||||
<span x-html="$icon('check-circle', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">Active (30d)</p>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">{{ _('loyalty.store.cards.active_30d') }}</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="formatNumber(stats.active_cards)">0</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -71,7 +73,7 @@
|
||||
<span x-html="$icon('user-plus', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">New This Month</p>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">{{ _('loyalty.store.cards.new_this_month') }}</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="formatNumber(stats.new_this_month)">0</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -80,7 +82,7 @@
|
||||
<span x-html="$icon('currency-dollar', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">Total Points Balance</p>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">{{ _('loyalty.store.cards.total_points_balance') }}</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="formatNumber(stats.total_points_balance)">0</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -97,15 +99,15 @@
|
||||
<input type="text"
|
||||
x-model="filters.search"
|
||||
@input="debouncedSearch()"
|
||||
placeholder="Search by name, email, phone, or card..."
|
||||
placeholder="{{ _('loyalty.store.cards.search_placeholder') }}"
|
||||
class="w-full pl-10 pr-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>
|
||||
<select x-model="filters.status" @change="applyFilter()"
|
||||
class="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">
|
||||
<option value="">All Status</option>
|
||||
<option value="active">Active</option>
|
||||
<option value="inactive">Inactive</option>
|
||||
<option value="">{{ _('loyalty.store.cards.all_status') }}</option>
|
||||
<option value="active">{{ _('loyalty.common.active') }}</option>
|
||||
<option value="inactive">{{ _('loyalty.common.inactive') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -113,15 +115,15 @@
|
||||
<!-- Cards Table -->
|
||||
<div x-show="!loading && program">
|
||||
{% call table_wrapper() %}
|
||||
{{ table_header(['Member', 'Card Number', 'Points Balance', 'Last Activity', 'Status', 'Actions']) }}
|
||||
{{ table_header([_('loyalty.store.cards.col_member'), _('loyalty.store.cards.col_card_number'), _('loyalty.store.cards.col_points_balance'), _('loyalty.store.cards.col_last_activity'), _('loyalty.store.cards.col_status'), _('loyalty.store.cards.col_actions')]) }}
|
||||
<tbody class="bg-white divide-y dark:divide-gray-700 dark:bg-gray-800">
|
||||
<template x-if="cards.length === 0">
|
||||
<tr>
|
||||
<td colspan="6" class="px-4 py-8 text-center text-gray-600 dark:text-gray-400">
|
||||
<div class="flex flex-col items-center">
|
||||
<span x-html="$icon('users', 'w-12 h-12 mb-2 text-gray-300')"></span>
|
||||
<p class="font-medium">No members found</p>
|
||||
<p class="text-xs mt-1" x-text="filters.search ? 'Try adjusting your search' : 'Enroll your first customer to get started'"></p>
|
||||
<p class="font-medium" x-text="$t('loyalty.store.cards.no_members_found')"></p>
|
||||
<p class="text-xs mt-1" x-text="filters.search ? $t('loyalty.store.cards.try_adjusting_search') : $t('loyalty.store.cards.enroll_first_customer')"></p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -148,12 +150,12 @@
|
||||
<td class="px-4 py-3 text-xs">
|
||||
<span class="px-2 py-1 font-semibold leading-tight rounded-full"
|
||||
:class="card.is_active ? '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'"
|
||||
x-text="card.is_active ? 'Active' : 'Inactive'"></span>
|
||||
x-text="card.is_active ? $t('loyalty.common.active') : $t('loyalty.common.inactive')"></span>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<a :href="'/store/{{ store_code }}/loyalty/cards/' + card.id"
|
||||
class="text-purple-600 hover:text-purple-700 dark:text-purple-400">
|
||||
View
|
||||
class="text-purple-600 hover:text-purple-700 dark:text-purple-400"
|
||||
x-text="$t('loyalty.common.view')">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -3,17 +3,19 @@
|
||||
{% from 'shared/macros/headers.html' import detail_page_header %}
|
||||
{% from 'shared/macros/alerts.html' import loading_state, error_state %}
|
||||
|
||||
{% block title %}Enroll Customer{% endblock %}
|
||||
{% block title %}{{ _('loyalty.store.enroll.title') }}{% endblock %}
|
||||
|
||||
{% block i18n_modules %}['loyalty']{% endblock %}
|
||||
|
||||
{% block alpine_data %}storeLoyaltyEnroll(){% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% call detail_page_header("'Enroll New Customer'", '/store/' + store_code + '/loyalty/terminal') %}
|
||||
Add a new member to your loyalty program
|
||||
{% call detail_page_header("'" + _('loyalty.store.enroll.page_title') + "'", '/store/' + store_code + '/loyalty/terminal') %}
|
||||
{{ _('loyalty.store.enroll.subtitle') }}
|
||||
{% endcall %}
|
||||
|
||||
{{ loading_state('Loading...') }}
|
||||
{{ error_state('Error loading enrollment form') }}
|
||||
{{ loading_state(_('loyalty.common.loading')) }}
|
||||
{{ error_state(_('loyalty.store.enroll.error_loading')) }}
|
||||
|
||||
<div x-show="!loading" class="max-w-2xl">
|
||||
<form @submit.prevent="enrollCustomer">
|
||||
@@ -21,20 +23,20 @@
|
||||
<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('user', 'inline w-5 h-5 mr-2')"></span>
|
||||
Customer Information
|
||||
{{ _('loyalty.store.enroll.customer_information') }}
|
||||
</h3>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
First Name <span class="text-red-500">*</span>
|
||||
{{ _('loyalty.store.enroll.first_name') }} <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input type="text" x-model="form.first_name" required
|
||||
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">Last Name</label>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">{{ _('loyalty.store.enroll.last_name') }}</label>
|
||||
<input type="text" x-model="form.last_name"
|
||||
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>
|
||||
@@ -42,23 +44,23 @@
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Email <span class="text-red-500">*</span>
|
||||
{{ _('loyalty.store.enroll.email') }} <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input type="email" x-model="form.email" required
|
||||
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">Phone</label>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">{{ _('loyalty.store.enroll.phone') }}</label>
|
||||
<input type="tel" x-model="form.phone"
|
||||
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">Birthday</label>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">{{ _('loyalty.store.enroll.birthday') }}</label>
|
||||
<input type="date" x-model="form.birthday"
|
||||
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">For birthday rewards (optional)</p>
|
||||
<p class="mt-1 text-xs text-gray-500">{{ _('loyalty.store.enroll.birthday_help') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -67,19 +69,19 @@
|
||||
<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('bell', 'inline w-5 h-5 mr-2')"></span>
|
||||
Communication Preferences
|
||||
{{ _('loyalty.store.enroll.communication_preferences') }}
|
||||
</h3>
|
||||
|
||||
<div class="space-y-3">
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" x-model="form.marketing_email"
|
||||
class="w-4 h-4 text-purple-600 border-gray-300 rounded focus:ring-purple-500">
|
||||
<span class="ml-2 text-sm text-gray-700 dark:text-gray-300">Send promotional emails</span>
|
||||
<span class="ml-2 text-sm text-gray-700 dark:text-gray-300">{{ _('loyalty.store.enroll.send_emails') }}</span>
|
||||
</label>
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" x-model="form.marketing_sms"
|
||||
class="w-4 h-4 text-purple-600 border-gray-300 rounded focus:ring-purple-500">
|
||||
<span class="ml-2 text-sm text-gray-700 dark:text-gray-300">Send promotional SMS</span>
|
||||
<span class="ml-2 text-sm text-gray-700 dark:text-gray-300">{{ _('loyalty.store.enroll.send_sms') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -89,9 +91,9 @@
|
||||
<div class="flex items-center">
|
||||
<span x-html="$icon('gift', 'w-5 h-5 text-green-500 mr-3')"></span>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-green-800 dark:text-green-200">Welcome Bonus</p>
|
||||
<p class="text-sm font-medium text-green-800 dark:text-green-200">{{ _('loyalty.store.enroll.welcome_bonus') }}</p>
|
||||
<p class="text-sm text-green-700 dark:text-green-300">
|
||||
Customer will receive <span class="font-bold" x-text="program?.welcome_bonus_points"></span> bonus points!
|
||||
{{ _('loyalty.store.enroll.welcome_bonus_desc') }} <span class="font-bold" x-text="program?.welcome_bonus_points"></span> {{ _('loyalty.store.enroll.bonus_points') }}!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -101,12 +103,12 @@
|
||||
<div class="flex items-center gap-4">
|
||||
<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
|
||||
{{ _('loyalty.common.cancel') }}
|
||||
</a>
|
||||
<button type="submit" :disabled="enrolling || !form.first_name || !form.email"
|
||||
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="enrolling" x-html="$icon('spinner', 'w-4 h-4 mr-2 animate-spin')"></span>
|
||||
<span x-text="enrolling ? 'Enrolling...' : 'Enroll Customer'"></span>
|
||||
<span x-text="enrolling ? $t('loyalty.store.enroll.enrolling') : $t('loyalty.store.enroll.enroll_customer')"></span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -118,21 +120,21 @@
|
||||
<div class="w-16 h-16 mx-auto mb-4 rounded-full bg-green-100 flex items-center justify-center">
|
||||
<span x-html="$icon('check', 'w-8 h-8 text-green-500')"></span>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200 mb-2">Customer Enrolled!</h3>
|
||||
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200 mb-2">{{ _('loyalty.store.enroll.customer_enrolled') }}</h3>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400 mb-4">
|
||||
Card Number: <span class="font-mono font-semibold" x-text="enrolledCard?.card_number"></span>
|
||||
{{ _('loyalty.store.enroll.card_number_label') }}: <span class="font-mono font-semibold" x-text="enrolledCard?.card_number"></span>
|
||||
</p>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-300 mb-6">
|
||||
Starting Balance: <span class="font-bold text-purple-600" x-text="enrolledCard?.points_balance"></span> points
|
||||
{{ _('loyalty.store.enroll.starting_balance') }}: <span class="font-bold text-purple-600" x-text="enrolledCard?.points_balance"></span> {{ _('loyalty.store.enroll.points') }}
|
||||
</p>
|
||||
<div class="flex gap-3 justify-center">
|
||||
<a href="/store/{{ store_code }}/loyalty/terminal"
|
||||
class="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300">
|
||||
Back to Terminal
|
||||
{{ _('loyalty.store.enroll.back_to_terminal') }}
|
||||
</a>
|
||||
<button @click="enrolledCard = null; resetForm()"
|
||||
class="px-4 py-2 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700">
|
||||
Enroll Another
|
||||
{{ _('loyalty.store.enroll.enroll_another') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,42 +3,44 @@
|
||||
{% 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 title %}{{ _('loyalty.store.program.title') }}{% endblock %}
|
||||
|
||||
{% block i18n_modules %}['loyalty']{% endblock %}
|
||||
|
||||
{% block alpine_data %}storeLoyaltyProgram(){% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% call page_header_flex(title='Loyalty Program', subtitle='Your loyalty program configuration') %}
|
||||
{% call page_header_flex(title=_('loyalty.store.program.title'), subtitle=_('loyalty.store.program.subtitle')) %}
|
||||
<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
|
||||
{{ _('loyalty.store.program.edit_program') }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endcall %}
|
||||
|
||||
{{ loading_state('Loading program...') }}
|
||||
{{ error_state('Error loading program') }}
|
||||
{{ loading_state(_('loyalty.store.program.loading')) }}
|
||||
{{ error_state(_('loyalty.store.program.error_loading')) }}
|
||||
|
||||
<!-- 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>
|
||||
<h3 class="mt-4 text-lg font-medium text-gray-900 dark:text-white">{{ _('loyalty.store.program.no_program') }}</h3>
|
||||
<p class="mt-2 text-gray-500 dark:text-gray-400">
|
||||
Your merchant doesn't have a loyalty program configured yet.
|
||||
{{ _('loyalty.common.program_not_setup_desc') }}
|
||||
</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
|
||||
{{ _('loyalty.store.program.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>
|
||||
<p class="mt-4 text-sm text-gray-500 dark:text-gray-400">{{ _('loyalty.store.program.contact_admin') }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -4,33 +4,35 @@
|
||||
{% from 'shared/macros/alerts.html' import loading_state, error_state %}
|
||||
{% from 'shared/macros/modals.html' import confirm_modal %}
|
||||
|
||||
{% block title %}Loyalty Settings{% endblock %}
|
||||
{% block title %}{{ _('loyalty.store.settings.title') }}{% endblock %}
|
||||
|
||||
{% block i18n_modules %}['loyalty']{% endblock %}
|
||||
|
||||
{% block alpine_data %}loyaltySettings(){% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Page Header -->
|
||||
{% call page_header_flex(title='Loyalty Program Settings', subtitle='Configure your loyalty program') %}
|
||||
{% call page_header_flex(title=_('loyalty.store.settings.page_title'), subtitle=_('loyalty.store.settings.subtitle')) %}
|
||||
<div class="flex items-center gap-3">
|
||||
<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('arrow-left', 'w-4 h-4 mr-2')"></span>
|
||||
Back to Program
|
||||
{{ _('loyalty.store.settings.back_to_program') }}
|
||||
</a>
|
||||
</div>
|
||||
{% endcall %}
|
||||
|
||||
{{ loading_state('Loading settings...') }}
|
||||
{{ loading_state(_('loyalty.store.settings.loading')) }}
|
||||
|
||||
{{ error_state('Error loading settings') }}
|
||||
{{ error_state(_('loyalty.store.settings.error_loading')) }}
|
||||
|
||||
<!-- 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>
|
||||
<h3 class="text-sm font-semibold text-yellow-800 dark:text-yellow-200">{{ _('loyalty.store.settings.access_restricted') }}</h3>
|
||||
<p class="mt-1 text-sm text-yellow-700 dark:text-yellow-300">{{ _('loyalty.store.settings.access_restricted_desc') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -48,12 +50,12 @@
|
||||
<!-- 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.',
|
||||
_('loyalty.store.settings.delete_program_title'),
|
||||
_('loyalty.store.settings.delete_program_desc'),
|
||||
'deleteProgram()',
|
||||
'showDeleteModal',
|
||||
'Delete Program',
|
||||
'Cancel',
|
||||
_('loyalty.store.settings.delete_program_confirm'),
|
||||
_('loyalty.common.cancel'),
|
||||
'danger'
|
||||
) }}
|
||||
{% endblock %}
|
||||
|
||||
@@ -4,46 +4,48 @@
|
||||
{% from 'shared/macros/alerts.html' import loading_state, error_state %}
|
||||
{% from 'shared/macros/modals.html' import modal_simple %}
|
||||
|
||||
{% block title %}Loyalty Terminal{% endblock %}
|
||||
{% block title %}{{ _('loyalty.store.terminal.title') }}{% endblock %}
|
||||
|
||||
{% block i18n_modules %}['loyalty']{% endblock %}
|
||||
|
||||
{% block alpine_data %}storeLoyaltyTerminal(){% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Page Header -->
|
||||
{% call page_header_flex(title='Loyalty Terminal', subtitle='Process loyalty transactions') %}
|
||||
{% call page_header_flex(title=_('loyalty.store.terminal.title'), subtitle=_('loyalty.store.terminal.subtitle')) %}
|
||||
<div class="flex items-center gap-3">
|
||||
<a href="/store/{{ store_code }}/loyalty/cards"
|
||||
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('users', 'w-4 h-4 mr-2')"></span>
|
||||
Members
|
||||
{{ _('loyalty.store.terminal.members') }}
|
||||
</a>
|
||||
<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>
|
||||
Analytics
|
||||
{{ _('loyalty.store.terminal.analytics') }}
|
||||
</a>
|
||||
</div>
|
||||
{% endcall %}
|
||||
|
||||
{{ loading_state('Loading loyalty terminal...') }}
|
||||
{{ loading_state(_('loyalty.store.terminal.loading')) }}
|
||||
|
||||
{{ error_state('Error loading terminal') }}
|
||||
{{ error_state(_('loyalty.store.terminal.error_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>
|
||||
<h3 class="text-sm font-semibold text-yellow-800 dark:text-yellow-200">{{ _('loyalty.common.program_not_setup') }}</h3>
|
||||
<p class="mt-1 text-sm text-yellow-700 dark:text-yellow-300">{{ _('loyalty.common.program_not_setup_desc') }}</p>
|
||||
{% if user.role == 'merchant_owner' %}
|
||||
<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
|
||||
{{ _('loyalty.common.setup_program') }}
|
||||
</a>
|
||||
{% else %}
|
||||
<p class="mt-2 text-sm text-yellow-700 dark:text-yellow-300">Contact your administrator to complete the setup.</p>
|
||||
<p class="mt-2 text-sm text-yellow-700 dark:text-yellow-300">{{ _('loyalty.common.contact_admin_setup') }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -57,7 +59,7 @@
|
||||
<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('search', 'inline w-5 h-5 mr-2')"></span>
|
||||
Find Customer
|
||||
{{ _('loyalty.store.terminal.find_customer') }}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
@@ -70,7 +72,7 @@
|
||||
type="text"
|
||||
x-model="searchQuery"
|
||||
@keyup.enter="lookupCustomer()"
|
||||
placeholder="Email, phone, or card number..."
|
||||
placeholder="{{ _('loyalty.store.terminal.search_placeholder') }}"
|
||||
class="w-full pl-10 pr-4 py-3 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>
|
||||
@@ -80,7 +82,7 @@
|
||||
class="w-full flex items-center justify-center px-4 py-3 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700 focus:outline-none disabled:opacity-50"
|
||||
>
|
||||
<span x-show="lookingUp" x-html="$icon('spinner', 'w-5 h-5 mr-2 animate-spin')"></span>
|
||||
<span x-text="lookingUp ? 'Looking up...' : 'Look Up Customer'"></span>
|
||||
<span x-text="lookingUp ? $t('loyalty.store.terminal.looking_up') : $t('loyalty.store.terminal.look_up_customer')"></span>
|
||||
</button>
|
||||
|
||||
<!-- Divider -->
|
||||
@@ -89,7 +91,7 @@
|
||||
<div class="w-full border-t border-gray-200 dark:border-gray-700"></div>
|
||||
</div>
|
||||
<div class="relative flex justify-center text-sm">
|
||||
<span class="px-3 bg-white text-gray-500 dark:bg-gray-800 dark:text-gray-400">or</span>
|
||||
<span class="px-3 bg-white text-gray-500 dark:bg-gray-800 dark:text-gray-400">{{ _('loyalty.common.or') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -97,7 +99,7 @@
|
||||
<a href="/store/{{ store_code }}/loyalty/enroll"
|
||||
class="w-full flex items-center justify-center px-4 py-3 text-sm font-medium text-purple-600 bg-purple-50 border border-purple-200 rounded-lg hover:bg-purple-100 dark:bg-purple-900/20 dark:text-purple-400 dark:border-purple-800">
|
||||
<span x-html="$icon('user-plus', 'w-5 h-5 mr-2')"></span>
|
||||
Enroll New Customer
|
||||
{{ _('loyalty.store.terminal.enroll_new_customer') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -107,7 +109,7 @@
|
||||
<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('user', 'inline w-5 h-5 mr-2')"></span>
|
||||
Customer Found
|
||||
{{ _('loyalty.store.terminal.customer_found') }}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
@@ -122,7 +124,7 @@
|
||||
<div class="ml-4 flex-1">
|
||||
<p class="font-semibold text-gray-700 dark:text-gray-200" x-text="selectedCard?.customer_name || 'Unknown'"></p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400" x-text="selectedCard?.customer_email"></p>
|
||||
<p class="text-xs text-gray-400 dark:text-gray-500" x-text="'Card: ' + selectedCard?.card_number"></p>
|
||||
<p class="text-xs text-gray-400 dark:text-gray-500" x-text="$t('loyalty.store.terminal.card_label') + ': ' + selectedCard?.card_number"></p>
|
||||
</div>
|
||||
<button @click="clearCustomer()" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
|
||||
<span x-html="$icon('x', 'w-5 h-5')"></span>
|
||||
@@ -135,7 +137,7 @@
|
||||
<!-- 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-sm font-medium text-gray-600 dark:text-gray-400" x-text="$t('loyalty.store.terminal.points_balance')"></p>
|
||||
<p class="text-3xl font-bold"
|
||||
:style="'color: ' + (program?.card_color || '#4F46E5')"
|
||||
x-text="formatNumber(selectedCard?.points_balance || 0)"></p>
|
||||
@@ -144,12 +146,12 @@
|
||||
<!-- 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-sm font-medium text-gray-600 dark:text-gray-400" x-text="$t('loyalty.store.terminal.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>
|
||||
x-text="selectedCard?.stamps_until_reward > 0 ? $t('loyalty.store.terminal.more_for_reward', {count: selectedCard.stamps_until_reward}) : $t('loyalty.store.terminal.ready_to_redeem')"></p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -162,18 +164,18 @@
|
||||
<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
|
||||
{{ _('loyalty.store.terminal.add_stamp') }}
|
||||
</h4>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mb-3">
|
||||
Current: <span class="font-semibold" x-text="(selectedCard?.stamp_count || 0) + '/' + (program?.stamps_target || 10)"></span>
|
||||
{{ _('loyalty.store.terminal.current') }}: <span class="font-semibold" x-text="(selectedCard?.stamp_count || 0) + '/' + (program?.stamps_target || 10)"></span>
|
||||
</p>
|
||||
<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
|
||||
{{ _('loyalty.store.terminal.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>
|
||||
<p class="text-xs text-red-500 mt-2" x-text="$t('loyalty.store.terminal.cooldown_active')"></p>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
@@ -182,14 +184,14 @@
|
||||
<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
|
||||
{{ _('loyalty.store.terminal.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>
|
||||
x-text="selectedCard?.can_redeem_stamps ? $t('loyalty.store.terminal.reward_label') + ': ' + (selectedCard?.stamp_reward_description || program?.stamps_reward_description || $t('loyalty.store.terminal.free_item')) : $t('loyalty.store.terminal.not_enough_stamps')"></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
|
||||
{{ _('loyalty.store.terminal.redeem_stamps') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
@@ -199,10 +201,10 @@
|
||||
<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
|
||||
{{ _('loyalty.store.terminal.earn_points') }}
|
||||
</h4>
|
||||
<div class="mb-3">
|
||||
<label class="block text-xs text-gray-500 dark:text-gray-400 mb-1">Purchase Amount</label>
|
||||
<label class="block text-xs text-gray-500 dark:text-gray-400 mb-1">{{ _('loyalty.store.terminal.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 #}
|
||||
@@ -211,12 +213,12 @@
|
||||
</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>
|
||||
{{ _('loyalty.store.terminal.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
|
||||
{{ _('loyalty.store.terminal.award_points') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
@@ -225,13 +227,13 @@
|
||||
<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
|
||||
{{ _('loyalty.store.terminal.redeem_reward') }}
|
||||
</h4>
|
||||
<div class="mb-3">
|
||||
<label class="block text-xs text-gray-500 dark:text-gray-400 mb-1">Select Reward</label>
|
||||
<label class="block text-xs text-gray-500 dark:text-gray-400 mb-1">{{ _('loyalty.store.terminal.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>
|
||||
<option value="">{{ _('loyalty.store.terminal.select_reward_placeholder') }}</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>
|
||||
@@ -240,13 +242,13 @@
|
||||
</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>
|
||||
{{ _('loyalty.store.terminal.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
|
||||
{{ _('loyalty.store.terminal.redeem_reward') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
@@ -258,7 +260,7 @@
|
||||
<div x-show="!selectedCard" class="bg-white rounded-lg shadow-md dark:bg-gray-800">
|
||||
<div class="p-8 text-center">
|
||||
<span x-html="$icon('user-circle', 'w-16 h-16 mx-auto text-gray-300 dark:text-gray-600')"></span>
|
||||
<p class="mt-4 text-gray-500 dark:text-gray-400">Search for a customer to process a transaction</p>
|
||||
<p class="mt-4 text-gray-500 dark:text-gray-400">{{ _('loyalty.store.terminal.search_empty_state') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -268,25 +270,25 @@
|
||||
<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('clock', 'inline w-5 h-5 mr-2')"></span>
|
||||
Recent Transactions at This Location
|
||||
{{ _('loyalty.store.terminal.recent_transactions') }}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full">
|
||||
<thead>
|
||||
<tr class="text-xs font-semibold tracking-wide text-left text-gray-500 uppercase border-b dark:border-gray-700 bg-gray-50 dark:text-gray-400 dark:bg-gray-700">
|
||||
<th class="px-4 py-3">Time</th>
|
||||
<th class="px-4 py-3">Customer</th>
|
||||
<th class="px-4 py-3">Type</th>
|
||||
<th class="px-4 py-3 text-right">Points</th>
|
||||
<th class="px-4 py-3">Notes</th>
|
||||
<th class="px-4 py-3">{{ _('loyalty.store.terminal.col_time') }}</th>
|
||||
<th class="px-4 py-3">{{ _('loyalty.store.terminal.col_customer') }}</th>
|
||||
<th class="px-4 py-3">{{ _('loyalty.store.terminal.col_type') }}</th>
|
||||
<th class="px-4 py-3 text-right">{{ _('loyalty.store.terminal.col_points') }}</th>
|
||||
<th class="px-4 py-3">{{ _('loyalty.store.terminal.col_notes') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y dark:divide-gray-700 dark:bg-gray-800">
|
||||
<template x-if="recentTransactions.length === 0">
|
||||
<tr>
|
||||
<td colspan="5" class="px-4 py-6 text-center text-gray-500 dark:text-gray-400">
|
||||
No recent transactions
|
||||
{{ _('loyalty.store.terminal.no_recent_transactions') }}
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
@@ -312,10 +314,10 @@
|
||||
</div>
|
||||
|
||||
<!-- Staff PIN Modal -->
|
||||
{% call modal_simple(id='pinModal', title='Enter Staff PIN', show_var='showPinEntry') %}
|
||||
{% call modal_simple(id='pinModal', title=_('loyalty.store.terminal.enter_staff_pin'), show_var='showPinEntry') %}
|
||||
<div class="p-6">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||
Enter your staff PIN to authorize this transaction.
|
||||
{{ _('loyalty.store.terminal.pin_authorize_text') }}
|
||||
</p>
|
||||
<div class="flex justify-center mb-4">
|
||||
<div class="flex gap-2">
|
||||
@@ -335,7 +337,7 @@
|
||||
</template>
|
||||
<button @click="pinDigits = ''"
|
||||
class="h-14 text-sm font-medium rounded-lg bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600">
|
||||
Clear
|
||||
{{ _('loyalty.store.terminal.clear') }}
|
||||
</button>
|
||||
<button @click="addPinDigit(0)"
|
||||
class="h-14 text-xl font-semibold rounded-lg bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600">
|
||||
@@ -349,13 +351,13 @@
|
||||
<div class="mt-4 flex justify-end gap-3">
|
||||
<button @click="cancelPinEntry()"
|
||||
class="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">
|
||||
Cancel
|
||||
{{ _('loyalty.common.cancel') }}
|
||||
</button>
|
||||
<button @click="submitTransaction()"
|
||||
:disabled="pinDigits.length !== 4 || processing"
|
||||
class="px-4 py-2 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700 disabled:opacity-50">
|
||||
<span x-show="processing" x-html="$icon('spinner', 'w-4 h-4 mr-2 inline animate-spin')"></span>
|
||||
<span x-text="processing ? 'Processing...' : 'Confirm'"></span>
|
||||
<span x-text="processing ? $t('loyalty.store.terminal.processing') : $t('loyalty.store.terminal.confirm')"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
{# app/modules/loyalty/templates/loyalty/storefront/dashboard.html #}
|
||||
{% extends "storefront/base.html" %}
|
||||
|
||||
{% block title %}My Loyalty - {{ store.name }}{% endblock %}
|
||||
{% block title %}{{ _('loyalty.storefront.dashboard.my_loyalty') }} - {{ store.name }}{% endblock %}
|
||||
|
||||
{% block i18n_modules %}['loyalty']{% endblock %}
|
||||
|
||||
{% block alpine_data %}customerLoyaltyDashboard(){% endblock %}
|
||||
|
||||
@@ -11,9 +13,9 @@
|
||||
<div class="mb-8">
|
||||
<a href="{{ base_url }}account/dashboard" class="inline-flex items-center text-sm text-gray-600 dark:text-gray-400 hover:text-primary mb-4">
|
||||
<span x-html="$icon('arrow-left', 'w-4 h-4 mr-2')"></span>
|
||||
Back to Account
|
||||
{{ _('loyalty.storefront.dashboard.back_to_account') }}
|
||||
</a>
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">My Loyalty</h1>
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">{{ _('loyalty.storefront.dashboard.my_loyalty') }}</h1>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
@@ -24,13 +26,13 @@
|
||||
<!-- No Card State -->
|
||||
<div x-show="!loading && !card" class="text-center py-12">
|
||||
<span x-html="$icon('gift', 'w-16 h-16 mx-auto text-gray-300 dark:text-gray-600')"></span>
|
||||
<h2 class="mt-4 text-xl font-semibold text-gray-900 dark:text-white">Join Our Rewards Program!</h2>
|
||||
<p class="mt-2 text-gray-600 dark:text-gray-400">Earn points on every purchase and redeem for rewards.</p>
|
||||
<h2 class="mt-4 text-xl font-semibold text-gray-900 dark:text-white">{{ _('loyalty.storefront.dashboard.join_title') }}</h2>
|
||||
<p class="mt-2 text-gray-600 dark:text-gray-400">{{ _('loyalty.storefront.dashboard.join_subtitle') }}</p>
|
||||
<a href="{{ base_url }}loyalty/join"
|
||||
class="mt-6 inline-flex items-center px-6 py-3 text-sm font-medium text-white rounded-lg"
|
||||
style="background-color: var(--color-primary)">
|
||||
<span x-html="$icon('plus', 'w-5 h-5 mr-2')"></span>
|
||||
Join Now
|
||||
{{ _('loyalty.storefront.dashboard.join_now') }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -51,19 +53,19 @@
|
||||
</div>
|
||||
|
||||
<div class="text-center py-4">
|
||||
<p class="text-sm opacity-80">Points Balance</p>
|
||||
<p class="text-sm opacity-80">{{ _('loyalty.storefront.dashboard.points_balance') }}</p>
|
||||
<p class="text-5xl font-bold" x-text="formatNumber(card?.points_balance || 0)"></p>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-end mt-6">
|
||||
<div>
|
||||
<p class="text-xs opacity-70">Card Number</p>
|
||||
<p class="text-xs opacity-70">{{ _('loyalty.storefront.dashboard.card_number') }}</p>
|
||||
<p class="font-mono" x-text="card?.card_number"></p>
|
||||
</div>
|
||||
<button @click="showBarcode = true"
|
||||
class="px-4 py-2 bg-white/20 hover:bg-white/30 rounded-lg text-sm font-medium transition-colors">
|
||||
<span x-html="$icon('qr-code', 'w-5 h-5 inline mr-1')"></span>
|
||||
Show Card
|
||||
{{ _('loyalty.storefront.dashboard.show_card') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -72,22 +74,22 @@
|
||||
<!-- Quick Stats -->
|
||||
<div class="grid grid-cols-2 gap-4 mb-8">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-4 border border-gray-200 dark:border-gray-700">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">Total Earned</p>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">{{ _('loyalty.storefront.dashboard.total_earned') }}</p>
|
||||
<p class="text-2xl font-bold text-gray-900 dark:text-white" x-text="formatNumber(card?.total_points_earned || 0)"></p>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-4 border border-gray-200 dark:border-gray-700">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">Total Redeemed</p>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">{{ _('loyalty.storefront.dashboard.total_redeemed') }}</p>
|
||||
<p class="text-2xl font-bold text-gray-900 dark:text-white" x-text="formatNumber(card?.total_points_redeemed || 0)"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Available Rewards -->
|
||||
<div class="mb-8">
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">Available Rewards</h2>
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">{{ _('loyalty.storefront.dashboard.available_rewards') }}</h2>
|
||||
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<template x-if="rewards.length === 0">
|
||||
<div class="col-span-full text-center py-8 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
|
||||
<p class="text-gray-500 dark:text-gray-400">No rewards available yet</p>
|
||||
<p class="text-gray-500 dark:text-gray-400">{{ _('loyalty.storefront.dashboard.no_rewards_yet') }}</p>
|
||||
</div>
|
||||
</template>
|
||||
<template x-for="reward in rewards" :key="reward.id">
|
||||
@@ -103,12 +105,12 @@
|
||||
<template x-if="(card?.points_balance || 0) >= reward.points_required">
|
||||
<span class="inline-flex items-center text-sm font-medium text-green-600">
|
||||
<span x-html="$icon('check-circle', 'w-4 h-4 mr-1')"></span>
|
||||
Ready to redeem
|
||||
<span x-text="$t('loyalty.storefront.dashboard.ready_to_redeem')"></span>
|
||||
</span>
|
||||
</template>
|
||||
<template x-if="(card?.points_balance || 0) < reward.points_required">
|
||||
<span class="text-sm text-gray-500">
|
||||
<span x-text="reward.points_required - (card?.points_balance || 0)"></span> more to go
|
||||
<span class="text-sm text-gray-500"
|
||||
x-text="$t('loyalty.storefront.dashboard.x_more_to_go', {count: reward.points_required - (card?.points_balance || 0)})">
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
@@ -117,23 +119,23 @@
|
||||
</div>
|
||||
<p class="mt-4 text-sm text-gray-500 dark:text-gray-400">
|
||||
<span x-html="$icon('information-circle', 'w-4 h-4 inline mr-1')"></span>
|
||||
Show your card to staff to redeem rewards in-store.
|
||||
{{ _('loyalty.storefront.dashboard.redeem_hint') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Recent Activity -->
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">Recent Activity</h2>
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">{{ _('loyalty.storefront.dashboard.recent_activity') }}</h2>
|
||||
<a href="{{ base_url }}account/loyalty/history"
|
||||
class="text-sm font-medium hover:underline" style="color: var(--color-primary)">
|
||||
View All
|
||||
{{ _('loyalty.storefront.dashboard.view_all') }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow border border-gray-200 dark:border-gray-700 overflow-hidden">
|
||||
<template x-if="transactions.length === 0">
|
||||
<div class="p-8 text-center text-gray-500 dark:text-gray-400">
|
||||
No transactions yet. Make a purchase to start earning points!
|
||||
{{ _('loyalty.storefront.dashboard.no_transactions') }}
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="transactions.length > 0">
|
||||
@@ -148,7 +150,7 @@
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="font-medium text-gray-900 dark:text-white"
|
||||
x-text="tx.points_delta > 0 ? 'Points Earned' : 'Reward Redeemed'"></p>
|
||||
x-text="$t('loyalty.transactions.' + tx.transaction_type)"></p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400" x-text="formatDate(tx.transaction_at)"></p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -166,7 +168,7 @@
|
||||
<div class="mt-8" x-show="locations.length > 0">
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">
|
||||
<span x-html="$icon('map-pin', 'w-5 h-5 inline mr-2')"></span>
|
||||
Earn & Redeem Locations
|
||||
{{ _('loyalty.storefront.dashboard.earn_redeem_locations') }}
|
||||
</h2>
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow border border-gray-200 dark:border-gray-700 p-4">
|
||||
<ul class="space-y-2">
|
||||
@@ -187,7 +189,7 @@
|
||||
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"
|
||||
@click.self="showBarcode = false">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-xl max-w-sm w-full p-6 text-center">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Your Loyalty Card</h3>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">{{ _('loyalty.storefront.dashboard.your_loyalty_card') }}</h3>
|
||||
|
||||
<!-- Barcode Placeholder -->
|
||||
<div class="bg-white p-4 rounded-lg mb-4">
|
||||
@@ -198,7 +200,7 @@
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-6">
|
||||
Show this to staff when making a purchase or redeeming rewards.
|
||||
{{ _('loyalty.storefront.dashboard.show_to_staff') }}
|
||||
</p>
|
||||
|
||||
<!-- Wallet Buttons -->
|
||||
@@ -207,21 +209,21 @@
|
||||
<a :href="walletUrls.apple_wallet_url" target="_blank" rel="noopener"
|
||||
class="w-full flex items-center justify-center px-4 py-3 bg-black text-white rounded-lg hover:bg-gray-800 transition-colors">
|
||||
<span x-html="$icon('device-mobile', 'w-5 h-5 mr-2')"></span>
|
||||
Add to Apple Wallet
|
||||
{{ _('loyalty.loyalty.wallet.apple') }}
|
||||
</a>
|
||||
</template>
|
||||
<template x-if="walletUrls.google_wallet_url">
|
||||
<a :href="walletUrls.google_wallet_url" target="_blank" rel="noopener"
|
||||
class="w-full flex items-center justify-center px-4 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
|
||||
<span x-html="$icon('device-mobile', 'w-5 h-5 mr-2')"></span>
|
||||
Add to Google Wallet
|
||||
{{ _('loyalty.loyalty.wallet.google') }}
|
||||
</a>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<button @click="showBarcode = false"
|
||||
class="w-full px-4 py-2 text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200">
|
||||
Close
|
||||
{{ _('loyalty.enrollment.close') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{# app/modules/loyalty/templates/loyalty/storefront/enroll-success.html #}
|
||||
{% extends "storefront/base.html" %}
|
||||
|
||||
{% block title %}Welcome to Rewards! - {{ store.name }}{% endblock %}
|
||||
{% block title %}{{ _('loyalty.enrollment.success.title') }} - {{ store.name }}{% endblock %}
|
||||
|
||||
{% block alpine_data %}customerLoyaltyEnrollSuccess(){% endblock %}
|
||||
|
||||
@@ -16,32 +16,32 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white mb-2">Welcome!</h1>
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-8">You're now a member of our rewards program.</p>
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white mb-2">{{ _('loyalty.enrollment.success.title') }}</h1>
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-8">{{ _('loyalty.enrollment.success.message') }}</p>
|
||||
|
||||
<!-- Card Number Display -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6 mb-8">
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400 mb-2">Your Card Number</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400 mb-2">{{ _('loyalty.enrollment.success.card_number') }}</p>
|
||||
<p class="text-2xl font-mono font-bold text-gray-900 dark:text-white">{{ enrolled_card_number or 'Loading...' }}</p>
|
||||
|
||||
<div x-show="walletUrls.apple_wallet_url || walletUrls.google_wallet_url"
|
||||
class="mt-6 pt-6 border-t border-gray-200 dark:border-gray-700">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||
Save your card to your phone for easy access:
|
||||
{{ _('loyalty.enrollment.success.wallet_prompt') }}
|
||||
</p>
|
||||
<div class="space-y-2">
|
||||
<template x-if="walletUrls.apple_wallet_url">
|
||||
<a :href="walletUrls.apple_wallet_url" target="_blank" rel="noopener"
|
||||
class="w-full flex items-center justify-center px-4 py-3 bg-black text-white rounded-lg hover:bg-gray-800 transition-colors">
|
||||
<span x-html="$icon('device-mobile', 'w-5 h-5 mr-2')"></span>
|
||||
Add to Apple Wallet
|
||||
{{ _('loyalty.loyalty.wallet.apple') }}
|
||||
</a>
|
||||
</template>
|
||||
<template x-if="walletUrls.google_wallet_url">
|
||||
<a :href="walletUrls.google_wallet_url" target="_blank" rel="noopener"
|
||||
class="w-full flex items-center justify-center px-4 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
|
||||
<span x-html="$icon('device-mobile', 'w-5 h-5 mr-2')"></span>
|
||||
Add to Google Wallet
|
||||
{{ _('loyalty.loyalty.wallet.google') }}
|
||||
</a>
|
||||
</template>
|
||||
</div>
|
||||
@@ -50,19 +50,19 @@
|
||||
|
||||
<!-- Next Steps -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 text-left mb-8">
|
||||
<h2 class="font-semibold text-gray-900 dark:text-white mb-4">What's Next?</h2>
|
||||
<h2 class="font-semibold text-gray-900 dark:text-white mb-4">{{ _('loyalty.enrollment.success.next_steps_title') }}</h2>
|
||||
<ul class="space-y-3 text-sm text-gray-600 dark:text-gray-400">
|
||||
<li class="flex items-start">
|
||||
<span x-html="$icon('check-circle', 'w-5 h-5 mr-2 text-green-500 flex-shrink-0')"></span>
|
||||
<span>Show your card number when making purchases to earn points</span>
|
||||
<span>{{ _('loyalty.enrollment.success.step_earn') }}</span>
|
||||
</li>
|
||||
<li class="flex items-start">
|
||||
<span x-html="$icon('check-circle', 'w-5 h-5 mr-2 text-green-500 flex-shrink-0')"></span>
|
||||
<span>Check your balance online or in the app</span>
|
||||
<span>{{ _('loyalty.enrollment.success.step_balance') }}</span>
|
||||
</li>
|
||||
<li class="flex items-start">
|
||||
<span x-html="$icon('check-circle', 'w-5 h-5 mr-2 text-green-500 flex-shrink-0')"></span>
|
||||
<span>Redeem points for rewards at any of our locations</span>
|
||||
<span>{{ _('loyalty.enrollment.success.step_redeem') }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -72,11 +72,11 @@
|
||||
<a href="{{ base_url }}account/loyalty"
|
||||
class="block w-full py-3 px-4 text-white font-semibold rounded-lg transition-colors text-center"
|
||||
style="background-color: var(--color-primary)">
|
||||
View My Loyalty Dashboard
|
||||
{{ _('loyalty.enrollment.success.view_dashboard') }}
|
||||
</a>
|
||||
<a href="{{ base_url }}"
|
||||
class="block w-full py-3 px-4 text-gray-700 dark:text-gray-300 font-medium rounded-lg border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors text-center">
|
||||
Continue Shopping
|
||||
{{ _('loyalty.enrollment.success.continue_shopping') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -89,6 +89,7 @@ function customerLoyaltyEnrollSuccess() {
|
||||
return {
|
||||
...storefrontLayoutData(),
|
||||
walletUrls: { google_wallet_url: null, apple_wallet_url: null },
|
||||
|
||||
init() {
|
||||
// Read wallet URLs saved during enrollment (no auth needed)
|
||||
try {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
{# app/modules/loyalty/templates/loyalty/storefront/enroll.html #}
|
||||
{% extends "storefront/base.html" %}
|
||||
|
||||
{% block title %}Join Loyalty Program - {{ store.name }}{% endblock %}
|
||||
{% block title %}{{ _('loyalty.enrollment.title') }} - {{ store.name }}{% endblock %}
|
||||
|
||||
{% block i18n_modules %}['loyalty']{% endblock %}
|
||||
|
||||
{% block alpine_data %}customerLoyaltyEnroll(){% endblock %}
|
||||
|
||||
@@ -13,8 +15,8 @@
|
||||
{% if store.logo_url %}
|
||||
<img src="{{ store.logo_url }}" alt="{{ store.name }}" class="h-16 w-auto mx-auto mb-4">
|
||||
{% endif %}
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">Join Our Rewards Program!</h1>
|
||||
<p class="mt-2 text-gray-600 dark:text-gray-400" x-text="'Earn ' + (program?.points_per_euro || 1) + ' point for every EUR you spend'"></p>
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">{{ _('loyalty.enrollment.title') }}</h1>
|
||||
<p class="mt-2 text-gray-600 dark:text-gray-400" x-text="I18n.t('loyalty.enrollment.subtitle', {points: program?.points_per_euro || 1})"></p>
|
||||
</div>
|
||||
|
||||
<!-- Loading -->
|
||||
@@ -25,8 +27,8 @@
|
||||
<!-- No Program Available -->
|
||||
<div x-show="!loading && !program" class="bg-white dark:bg-gray-800 rounded-lg shadow p-8 text-center">
|
||||
<span x-html="$icon('exclamation-circle', 'w-12 h-12 mx-auto text-yellow-500')"></span>
|
||||
<h2 class="mt-4 text-xl font-semibold text-gray-900 dark:text-white">Program Not Available</h2>
|
||||
<p class="mt-2 text-gray-600 dark:text-gray-400">This store doesn't have a loyalty program set up yet.</p>
|
||||
<h2 class="mt-4 text-xl font-semibold text-gray-900 dark:text-white">{{ _('loyalty.enrollment.not_available_title') }}</h2>
|
||||
<p class="mt-2 text-gray-600 dark:text-gray-400">{{ _('loyalty.enrollment.not_available_message') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Enrollment Form -->
|
||||
@@ -36,13 +38,13 @@
|
||||
class="p-4 text-center text-white"
|
||||
:style="'background-color: ' + (program?.card_color || 'var(--color-primary)')">
|
||||
<span x-html="$icon('gift', 'w-6 h-6 inline mr-2')"></span>
|
||||
<span class="font-semibold">Get <span x-text="program?.welcome_bonus_points"></span> bonus points when you join!</span>
|
||||
<span class="font-semibold" x-text="I18n.t('loyalty.enrollment.welcome_bonus', {points: program?.welcome_bonus_points})"></span>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="submitEnrollment" class="p-6 space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Email <span class="text-red-500">*</span>
|
||||
{{ _('loyalty.enrollment.form.email') }} <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input type="email" x-model="form.email" required
|
||||
class="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent dark:bg-gray-700 dark:text-white"
|
||||
@@ -53,7 +55,7 @@
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
First Name <span class="text-red-500">*</span>
|
||||
{{ _('loyalty.enrollment.form.first_name') }} <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input type="text" x-model="form.first_name" required
|
||||
class="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent dark:bg-gray-700 dark:text-white"
|
||||
@@ -61,7 +63,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Last Name
|
||||
{{ _('loyalty.enrollment.form.last_name') }}
|
||||
</label>
|
||||
<input type="text" x-model="form.last_name"
|
||||
class="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent dark:bg-gray-700 dark:text-white"
|
||||
@@ -71,7 +73,7 @@
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Phone (optional)
|
||||
{{ _('loyalty.enrollment.form.phone') }}
|
||||
</label>
|
||||
<input type="tel" x-model="form.phone"
|
||||
class="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent dark:bg-gray-700 dark:text-white"
|
||||
@@ -80,11 +82,11 @@
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Birthday (optional)
|
||||
{{ _('loyalty.enrollment.form.birthday') }}
|
||||
</label>
|
||||
<input type="date" x-model="form.birthday"
|
||||
class="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent dark:bg-gray-700 dark:text-white">
|
||||
<p class="mt-1 text-xs text-gray-500">For special birthday rewards</p>
|
||||
<p class="mt-1 text-xs text-gray-500">{{ _('loyalty.enrollment.form.birthday_hint') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3 pt-2">
|
||||
@@ -93,12 +95,12 @@
|
||||
class="mt-1 w-4 h-4 rounded border-gray-300 text-primary focus:ring-primary"
|
||||
style="color: var(--color-primary)">
|
||||
<span class="ml-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
I agree to the
|
||||
{{ _('loyalty.enrollment.form.terms_agree') }}
|
||||
<template x-if="program?.terms_text">
|
||||
<a href="#" @click.prevent="showTerms = true" class="underline" style="color: var(--color-primary)">Terms & Conditions</a>
|
||||
<a href="#" @click.prevent="showTerms = true" class="underline" style="color: var(--color-primary)">{{ _('loyalty.enrollment.form.terms') }}</a>
|
||||
</template>
|
||||
<template x-if="!program?.terms_text">
|
||||
<span class="underline" style="color: var(--color-primary)">Terms & Conditions</span>
|
||||
<span class="underline" style="color: var(--color-primary)">{{ _('loyalty.enrollment.form.terms') }}</span>
|
||||
</template>
|
||||
</span>
|
||||
</label>
|
||||
@@ -107,7 +109,7 @@
|
||||
class="mt-1 w-4 h-4 rounded border-gray-300 text-primary focus:ring-primary"
|
||||
style="color: var(--color-primary)">
|
||||
<span class="ml-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
Send me news and special offers
|
||||
{{ _('loyalty.enrollment.form.marketing_consent') }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
@@ -117,13 +119,13 @@
|
||||
class="w-full py-3 px-4 text-white font-semibold rounded-lg transition-colors disabled:opacity-50"
|
||||
:style="'background-color: ' + (program?.card_color || 'var(--color-primary)')">
|
||||
<span x-show="enrolling" x-html="$icon('spinner', 'w-5 h-5 inline animate-spin mr-2')"></span>
|
||||
<span x-text="enrolling ? 'Joining...' : 'Join & Get ' + (program?.welcome_bonus_points || 0) + ' Points'"></span>
|
||||
<span x-text="enrolling ? I18n.t('loyalty.enrollment.form.joining') : I18n.t('loyalty.enrollment.form.join_button', {points: program?.welcome_bonus_points || 0})"></span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="px-6 pb-6 text-center">
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
Already a member? Your points are linked to your email.
|
||||
{{ _('loyalty.enrollment.already_member') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -145,7 +147,7 @@
|
||||
@keydown.escape.window="showTerms = false">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-lg w-full max-h-[80vh] flex flex-col">
|
||||
<div class="flex items-center justify-between p-4 border-b dark:border-gray-700">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Terms & Conditions</h3>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">{{ _('loyalty.enrollment.form.terms') }}</h3>
|
||||
<button @click="showTerms = false" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
|
||||
<span x-html="$icon('x-mark', 'w-5 h-5')"></span>
|
||||
</button>
|
||||
@@ -153,14 +155,14 @@
|
||||
<div class="p-4 overflow-y-auto text-sm text-gray-700 dark:text-gray-300 whitespace-pre-line" x-text="program?.terms_text"></div>
|
||||
<template x-if="program?.privacy_url">
|
||||
<div class="px-4 pb-2">
|
||||
<a :href="program.privacy_url" target="_blank" class="text-sm underline" style="color: var(--color-primary)">Privacy Policy</a>
|
||||
<a :href="program.privacy_url" target="_blank" class="text-sm underline" style="color: var(--color-primary)">{{ _('loyalty.enrollment.privacy_policy') }}</a>
|
||||
</div>
|
||||
</template>
|
||||
<div class="p-4 border-t dark:border-gray-700">
|
||||
<button @click="showTerms = false"
|
||||
class="w-full py-2 px-4 text-white font-medium rounded-lg"
|
||||
:style="'background-color: ' + (program?.card_color || 'var(--color-primary)')">
|
||||
Close
|
||||
{{ _('loyalty.enrollment.close') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
{# app/modules/loyalty/templates/loyalty/storefront/history.html #}
|
||||
{% extends "storefront/base.html" %}
|
||||
|
||||
{% block title %}Loyalty History - {{ store.name }}{% endblock %}
|
||||
{% block title %}{{ _('loyalty.storefront.history.title') }} - {{ store.name }}{% endblock %}
|
||||
|
||||
{% block i18n_modules %}['loyalty']{% endblock %}
|
||||
|
||||
{% block alpine_data %}customerLoyaltyHistory(){% endblock %}
|
||||
|
||||
@@ -11,10 +13,10 @@
|
||||
<div class="mb-8">
|
||||
<a href="{{ base_url }}account/loyalty" class="inline-flex items-center text-sm text-gray-600 dark:text-gray-400 hover:text-primary mb-4">
|
||||
<span x-html="$icon('arrow-left', 'w-4 h-4 mr-2')"></span>
|
||||
Back to Loyalty
|
||||
{{ _('loyalty.storefront.history.back_to_loyalty') }}
|
||||
</a>
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">Transaction History</h1>
|
||||
<p class="mt-2 text-gray-600 dark:text-gray-400">View all your loyalty point transactions</p>
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">{{ _('loyalty.storefront.history.title') }}</h1>
|
||||
<p class="mt-2 text-gray-600 dark:text-gray-400">{{ _('loyalty.storefront.history.subtitle') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
@@ -26,15 +28,15 @@
|
||||
<div x-show="!loading && card" class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-8 border border-gray-200 dark:border-gray-700">
|
||||
<div class="grid grid-cols-3 gap-4 text-center">
|
||||
<div>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">Current Balance</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">{{ _('loyalty.storefront.history.current_balance') }}</p>
|
||||
<p class="text-2xl font-bold" style="color: var(--color-primary)" x-text="formatNumber(card?.points_balance || 0)"></p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">Total Earned</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">{{ _('loyalty.storefront.history.total_earned') }}</p>
|
||||
<p class="text-2xl font-bold text-green-600" x-text="formatNumber(card?.total_points_earned || 0)"></p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">Total Redeemed</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">{{ _('loyalty.storefront.history.total_redeemed') }}</p>
|
||||
<p class="text-2xl font-bold text-orange-600" x-text="formatNumber(card?.total_points_redeemed || 0)"></p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -45,7 +47,7 @@
|
||||
<template x-if="transactions.length === 0">
|
||||
<div class="p-12 text-center">
|
||||
<span x-html="$icon('receipt-refund', 'w-12 h-12 mx-auto text-gray-300 dark:text-gray-600')"></span>
|
||||
<p class="mt-4 text-gray-500 dark:text-gray-400">No transactions yet</p>
|
||||
<p class="mt-4 text-gray-500 dark:text-gray-400">{{ _('loyalty.storefront.history.no_transactions') }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -61,7 +63,7 @@
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="font-medium text-gray-900 dark:text-white"
|
||||
x-text="getTransactionLabel(tx)"></p>
|
||||
x-text="$t('loyalty.transactions.' + tx.transaction_type)"></p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
<span x-text="formatDateTime(tx.transaction_at)"></span>
|
||||
<span x-show="tx.store_name" class="ml-2">
|
||||
@@ -76,7 +78,7 @@
|
||||
:class="tx.points_delta > 0 ? 'text-green-600' : 'text-orange-600'"
|
||||
x-text="(tx.points_delta > 0 ? '+' : '') + formatNumber(tx.points_delta)"></p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
Balance: <span x-text="formatNumber(tx.balance_after)"></span>
|
||||
{{ _('loyalty.storefront.history.balance') }} <span x-text="formatNumber(tx.balance_after)"></span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -88,14 +90,14 @@
|
||||
<div x-show="pagination.pages > 1" class="px-4 py-3 border-t border-gray-200 dark:border-gray-700 flex items-center justify-between">
|
||||
<button @click="previousPage()" :disabled="pagination.page <= 1"
|
||||
class="px-3 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50">
|
||||
Previous
|
||||
{{ _('loyalty.storefront.history.previous') }}
|
||||
</button>
|
||||
<span class="text-sm text-gray-500 dark:text-gray-400">
|
||||
Page <span x-text="pagination.page"></span> of <span x-text="pagination.pages"></span>
|
||||
<span class="text-sm text-gray-500 dark:text-gray-400"
|
||||
x-text="$t('loyalty.storefront.history.page_x_of_y', {page: pagination.page, pages: pagination.pages})">
|
||||
</span>
|
||||
<button @click="nextPage()" :disabled="pagination.page >= pagination.pages"
|
||||
class="px-3 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50">
|
||||
Next
|
||||
{{ _('loyalty.storefront.history.next') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user