Files
orion/app/modules/loyalty/templates/loyalty/storefront/dashboard.html
Samir Boulahtit 24219e4d9a a11y(loyalty): Phase 4.2 — accessibility audit fixes
Fix 15 accessibility issues across loyalty templates:

Modals (4 fixes):
- storefront/dashboard.html: barcode modal — add role="dialog",
  aria-modal, aria-labelledby, @keydown.escape
- storefront/enroll.html: terms modal — add role="dialog",
  aria-modal, aria-labelledby, aria-label on close button
- store/enroll.html: success modal — add role="dialog",
  aria-modal, aria-labelledby, @keydown.escape
- store/terminal.html: PIN entry — add aria-live="polite" on
  digit display with role="status" for screen reader announcements

Icon-only buttons (10 fixes):
- shared/pins-list.html: edit, delete, unlock — add aria-label
- admin/programs.html: view, edit, delete, activate/deactivate —
  add aria-label (dynamic for toggle state)
- store/terminal.html: clear customer, backspace — add aria-label

All buttons also get explicit type="button" where missing.

342 tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 20:14:03 +02:00

237 lines
14 KiB
HTML

{# app/modules/loyalty/templates/loyalty/storefront/dashboard.html #}
{% extends "storefront/base.html" %}
{% block title %}{{ _('loyalty.storefront.dashboard.my_loyalty') }} - {{ store.name }}{% endblock %}
{% block i18n_modules %}['loyalty']{% endblock %}
{% block alpine_data %}customerLoyaltyDashboard(){% endblock %}
{% block content %}
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Page Header -->
<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>
{{ _('loyalty.storefront.dashboard.back_to_account') }}
</a>
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">{{ _('loyalty.storefront.dashboard.my_loyalty') }}</h1>
</div>
<!-- Loading State -->
<div x-show="loading" class="flex justify-center py-12">
<span x-html="$icon('spinner', 'w-8 h-8 animate-spin text-primary')" style="color: var(--color-primary)"></span>
</div>
<!-- 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">{{ _('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>
{{ _('loyalty.storefront.dashboard.join_now') }}
</a>
</div>
<!-- Loyalty Card Content -->
<div x-show="!loading && card">
<!-- Loyalty Card Display -->
<div class="mb-8 rounded-2xl overflow-hidden shadow-lg"
:style="'background: linear-gradient(135deg, ' + (program?.card_color || '#4F46E5') + ' 0%, ' + (program?.card_secondary_color || program?.card_color || '#4F46E5') + 'cc 100%)'">
<div class="p-6 text-white">
<div class="flex justify-between items-start mb-6">
<div>
<p class="text-sm opacity-80" x-text="program?.display_name || 'Loyalty Program'"></p>
<p class="text-lg font-semibold" x-text="card?.customer_name"></p>
</div>
<template x-if="program?.logo_url">
<img :src="program.logo_url" alt="Logo" class="h-10 w-auto">
</template>
</div>
<div class="text-center py-4">
<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">{{ _('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>
{{ _('loyalty.storefront.dashboard.show_card') }}
</button>
</div>
</div>
</div>
<!-- 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">{{ _('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">{{ _('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">{{ _('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">{{ _('loyalty.storefront.dashboard.no_rewards_yet') }}</p>
</div>
</template>
<template x-for="reward in rewards" :key="reward.id">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow border border-gray-200 dark:border-gray-700 p-4">
<div class="flex items-center justify-between mb-3">
<span x-html="$icon('gift', 'w-6 h-6')" :style="'color: ' + (program?.card_color || 'var(--color-primary)')"></span>
<span class="text-sm font-semibold" :style="'color: ' + (program?.card_color || 'var(--color-primary)')"
x-text="reward.points_required + ' pts'"></span>
</div>
<h3 class="font-semibold text-gray-900 dark:text-white" x-text="reward.name"></h3>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1" x-text="reward.description || ''"></p>
<div class="mt-3">
<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>
<span>{{ _('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"
x-text="'{{ _('loyalty.storefront.dashboard.x_more_to_go') }}'.replace('{count}', reward.points_required - (card?.points_balance || 0))">
</span>
</template>
</div>
</div>
</template>
</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>
{{ _('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">{{ _('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)">
{{ _('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">
{{ _('loyalty.storefront.dashboard.no_transactions') }}
</div>
</template>
<template x-if="transactions.length > 0">
<div class="divide-y divide-gray-200 dark:divide-gray-700">
<template x-for="tx in transactions.slice(0, 5)" :key="tx.id">
<div class="flex items-center justify-between p-4">
<div class="flex items-center">
<div class="w-10 h-10 rounded-full flex items-center justify-center"
:class="tx.points_delta > 0 ? 'bg-green-100 dark:bg-green-900/30' : 'bg-orange-100 dark:bg-orange-900/30'">
<span x-html="$icon(tx.points_delta > 0 ? 'plus' : 'gift', 'w-5 h-5')"
:class="tx.points_delta > 0 ? 'text-green-600' : 'text-orange-600'"></span>
</div>
<div class="ml-4">
<p class="font-medium text-gray-900 dark:text-white"
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>
<p class="font-semibold"
:class="tx.points_delta > 0 ? 'text-green-600' : 'text-orange-600'"
x-text="(tx.points_delta > 0 ? '+' : '') + formatNumber(tx.points_delta)"></p>
</div>
</template>
</div>
</template>
</div>
</div>
<!-- Locations -->
<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>
{{ _('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">
<template x-for="loc in locations" :key="loc.id">
<li class="flex items-center text-gray-700 dark:text-gray-300">
<span x-html="$icon('check', 'w-4 h-4 mr-2 text-green-500')"></span>
<span x-text="loc.name"></span>
</li>
</template>
</ul>
</div>
</div>
</div>
</div>
<!-- Barcode Modal -->
<div x-show="showBarcode" x-cloak
role="dialog" aria-modal="true" aria-labelledby="barcode-modal-title"
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"
@click.self="showBarcode = false"
@keydown.escape.window="showBarcode = false">
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-xl max-w-sm w-full p-6 text-center">
<h3 id="barcode-modal-title" 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">
<div class="h-20 flex items-center justify-center border-2 border-gray-200 rounded">
<span class="font-mono text-2xl tracking-wider" x-text="card?.card_number"></span>
</div>
<p class="text-xs text-gray-500 mt-2" x-text="card?.card_number"></p>
</div>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-6">
{{ _('loyalty.storefront.dashboard.show_to_staff') }}
</p>
<!-- Wallet Buttons -->
<div class="space-y-2 mb-4">
<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('phone', 'w-5 h-5 mr-2')"></span>
{{ _('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('phone', 'w-5 h-5 mr-2')"></span>
{{ _('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">
{{ _('loyalty.enrollment.close') }}
</button>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script defer src="{{ url_for('loyalty_static', path='storefront/js/loyalty-dashboard.js') }}"></script>
{% endblock %}