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:
2026-03-13 19:53:17 +01:00
parent 826ef2ddd2
commit 694a1cd1a5
37 changed files with 2916 additions and 563 deletions

View File

@@ -96,5 +96,589 @@
"title": "Treueprogramm erstellen",
"description": "Erstellen Sie Ihr erstes Stempel- oder Punkteprogramm"
}
},
"enrollment": {
"title": "Treten Sie unserem Prämienprogramm bei!",
"subtitle": "Verdienen Sie {points} Punkt pro ausgegebenem EUR",
"not_available_title": "Programm nicht verfügbar",
"not_available_message": "Dieser Shop hat noch kein Treueprogramm eingerichtet.",
"welcome_bonus": "Erhalten Sie {points} Bonuspunkte bei der Anmeldung!",
"already_member": "Bereits Mitglied? Ihre Punkte sind mit Ihrer E-Mail verknüpft.",
"form": {
"email": "E-Mail",
"first_name": "Vorname",
"last_name": "Nachname",
"phone": "Telefon (optional)",
"birthday": "Geburtstag (optional)",
"birthday_hint": "Für besondere Geburtstagsbelohnungen",
"terms_agree": "Ich stimme den",
"terms": "Allgemeinen Geschäftsbedingungen",
"marketing_consent": "Neuigkeiten und Sonderangebote senden",
"joining": "Anmeldung läuft...",
"join_button": "Beitreten & {points} Punkte erhalten"
},
"privacy_policy": "Datenschutzrichtlinie",
"close": "Schließen",
"success": {
"title": "Willkommen!",
"message": "Sie sind jetzt Mitglied unseres Prämienprogramms.",
"card_number": "Ihre Kartennummer",
"wallet_prompt": "Speichern Sie Ihre Karte auf Ihrem Handy für einfachen Zugriff:",
"next_steps_title": "Wie geht es weiter?",
"step_earn": "Zeigen Sie Ihre Kartennummer beim Einkauf, um Punkte zu sammeln",
"step_balance": "Prüfen Sie Ihren Kontostand online oder in der App",
"step_redeem": "Lösen Sie Punkte gegen Prämien an allen unseren Standorten ein",
"view_dashboard": "Mein Treue-Dashboard anzeigen",
"continue_shopping": "Weiter einkaufen"
},
"errors": {
"load_failed": "Programminformationen konnten nicht geladen werden",
"email_exists": "Diese E-Mail ist bereits in unserem Treueprogramm registriert.",
"failed": "Anmeldung fehlgeschlagen. Bitte versuchen Sie es erneut."
}
},
"common": {
"active": "Aktiv",
"inactive": "Inaktiv",
"cancel": "Abbrechen",
"save": "Speichern",
"delete": "Löschen",
"confirm": "Bestätigen",
"refresh": "Aktualisieren",
"loading": "Laden...",
"saving": "Speichern...",
"view": "Anzeigen",
"edit": "Bearbeiten",
"yes": "Ja",
"no": "Nein",
"none": "Keine",
"never": "Nie",
"total": "GESAMT",
"continue": "Weiter",
"back": "Zurück",
"points": "Punkte",
"minutes": "Minuten",
"or": "oder",
"at": "bei"
},
"transactions": {
"card_created": "Angemeldet",
"welcome_bonus": "Willkommensbonus",
"stamp_earned": "Stempel erhalten",
"stamp_redeemed": "Stempel eingelöst",
"stamp_voided": "Stempel storniert",
"stamp_adjustment": "Stempel angepasst",
"points_earned": "Punkte verdient",
"points_redeemed": "Punkte eingelöst",
"points_voided": "Punkte storniert",
"points_adjustment": "Punkte angepasst",
"points_expired": "Punkte verfallen",
"card_deactivated": "Deaktiviert",
"reward_redeemed": "Prämie eingelöst"
},
"shared": {
"analytics": {
"total_programs": "Programme insgesamt",
"total_members": "Mitglieder insgesamt",
"active_members": "Aktive Mitglieder",
"points_issued_30d": "Punkte vergeben (30T)",
"transactions_30d": "Transaktionen (30T)",
"x_active": "{count} aktiv",
"points_overview": "Punkteübersicht",
"points_issued_vs_redeemed": "Punkte vergeben vs eingelöst (30T)",
"issued": "Vergeben:",
"redeemed": "Eingelöst:",
"redemption_rate": "Einlösungsrate",
"outstanding_balance": "Ausstehender Saldo",
"member_activity": "Mitgliederaktivität",
"active_members_30d": "Aktive Mitglieder (30T)",
"new_this_month": "Neu diesen Monat",
"merchants_with_programs": "Händler mit Programmen",
"avg_points_per_member": "Durchschn. Punkte pro Mitglied",
"all_time_statistics": "Gesamtstatistiken",
"total_points_issued": "Punkte insgesamt vergeben",
"total_points_redeemed": "Punkte insgesamt eingelöst",
"points_redeemed_30d": "Punkte eingelöst (30T)",
"outstanding_liability": "Ausstehende Verbindlichkeit",
"location_breakdown": "Aufschlüsselung nach Standort",
"store": "Geschäft",
"enrolled": "Angemeldet",
"points_earned": "Punkte verdient",
"points_redeemed": "Punkte eingelöst"
},
"program_view": {
"program_configuration": "Programmkonfiguration",
"program_name": "Programmname",
"card_name": "Kartenname",
"stamps_configuration": "Stempelkonfiguration",
"stamps_target": "Stempelziel",
"reward_description": "Prämienbeschreibung",
"reward_value": "Prämienwert",
"points_configuration": "Punktekonfiguration",
"points_per_eur": "Punkte pro EUR",
"welcome_bonus": "Willkommensbonus",
"x_points": "{count} Punkte",
"minimum_redemption": "Mindesteinlösung",
"minimum_purchase": "Mindesteinkauf",
"points_expiration": "Punkteverfall",
"x_days_inactivity": "{days} Tage Inaktivität",
"redemption_rewards": "Einlösungsprämien",
"reward": "Prämie",
"points_required": "Benötigte Punkte",
"description": "Beschreibung",
"anti_fraud": "Betrugsschutz",
"cooldown": "Wartezeit",
"x_minutes": "{count} Minuten",
"max_daily_stamps": "Max. Stempel pro Tag",
"staff_pin_required": "Mitarbeiter-PIN erforderlich",
"branding": "Branding",
"primary_color": "Primärfarbe",
"secondary_color": "Sekundärfarbe",
"logo_url": "Logo-URL",
"hero_image_url": "Hintergrundbild-URL",
"terms_privacy": "AGB & Datenschutz",
"terms_conditions": "Allgemeine Geschäftsbedingungen",
"privacy_policy_url": "Datenschutzrichtlinien-URL"
},
"program_form": {
"program_type": "Programmtyp",
"points_type_desc": "Punkte pro ausgegebenem EUR verdienen",
"stamps_type_desc": "N Stempel sammeln, Prämie erhalten",
"hybrid_type_desc": "Stempel und Punkte kombiniert",
"stamps_configuration": "Stempelkonfiguration",
"stamps_target": "Stempelziel",
"stamps_target_help": "Anzahl der Stempel für die Prämie",
"reward_description": "Prämienbeschreibung",
"reward_value_cents": "Prämienwert (Cent)",
"points_configuration": "Punktekonfiguration",
"points_per_eur": "Punkte pro ausgegebenem EUR",
"eur_equals_points": "1 EUR = {points} Punkt(e)",
"welcome_bonus_points": "Willkommensbonuspunkte",
"welcome_bonus_help": "Bonuspunkte bei der Anmeldung",
"minimum_redemption_points": "Mindesteinlösungspunkte",
"minimum_purchase_cents": "Mindesteinkauf (Cent)",
"minimum_purchase_help": "Mindesteinkaufsbetrag zum Punktesammeln (0 = kein Minimum)",
"points_expiration_days": "Punkteverfall (Tage)",
"points_expiration_help": "Tage der Inaktivität bis zum Punkteverfall (0 = nie)",
"redemption_rewards": "Einlösungsprämien",
"add_reward": "Prämie hinzufügen",
"no_rewards_configured": "Keine Prämien konfiguriert. Fügen Sie eine Prämie hinzu, damit Kunden Punkte einlösen können.",
"reward_name": "Prämienname",
"points_required": "Benötigte Punkte",
"description": "Beschreibung",
"anti_fraud_settings": "Betrugsschutz-Einstellungen",
"cooldown_minutes": "Wartezeit (Minuten)",
"cooldown_help": "Zeit zwischen Stempeln derselben Karte",
"max_daily_stamps": "Max. Stempel pro Tag",
"max_daily_stamps_help": "Maximale Stempel pro Karte pro Tag",
"require_staff_pin": "Mitarbeiter-PIN verlangen",
"branding": "Branding",
"card_name": "Kartenname",
"primary_color": "Primärfarbe",
"secondary_color": "Sekundärfarbe",
"logo_url": "Logo-URL",
"logo_url_help": "Erforderlich für Google Wallet-Integration. Muss eine öffentlich zugängliche Bild-URL sein (PNG oder JPG).",
"hero_image_url": "Hintergrundbild-URL",
"terms_privacy": "AGB & Datenschutz",
"terms_conditions": "Allgemeine Geschäftsbedingungen",
"privacy_policy_url": "Datenschutzrichtlinien-URL",
"program_status": "Programmstatus",
"program_active": "Programm aktiv",
"program_active_help": "Deaktiviert können Kunden weder sammeln noch einlösen",
"delete_program": "Programm löschen",
"create_program": "Programm erstellen",
"save_changes": "Änderungen speichern"
}
},
"admin": {
"programs": {
"title": "Treueprogramme",
"create_program": "Programm erstellen",
"loading": "Treueprogramme werden geladen...",
"error_loading": "Fehler beim Laden der Treueprogramme",
"total_programs": "Programme insgesamt",
"active": "Aktiv",
"total_members": "Mitglieder insgesamt",
"transactions_30d": "Transaktionen (30T)",
"search_placeholder": "Nach Händlername suchen...",
"all_status": "Alle Status",
"table_merchant": "Händler",
"table_program_type": "Programmtyp",
"table_members": "Mitglieder",
"table_points_issued": "Punkte vergeben",
"table_status": "Status",
"table_created": "Erstellt",
"table_actions": "Aktionen",
"no_programs": "Keine Treueprogramme gefunden",
"adjust_filters": "Versuchen Sie, Ihre Suche oder Filter anzupassen",
"no_merchants_yet": "Noch keine Händler haben Treueprogramme erstellt",
"x_active": "({count} aktiv)",
"x_redeemed": "{count} eingelöst",
"pt_per_eur": "Pkt/EUR",
"delete_title": "Treueprogramm löschen",
"delete_message": "Treueprogramm für \"{name}\" löschen? Alle zugehörigen Daten (Karten, Transaktionen, Prämien) werden dauerhaft gelöscht. Dies kann nicht rückgängig gemacht werden.",
"delete_confirm": "Programm löschen",
"create_title": "Treueprogramm erstellen",
"create_description": "Wählen Sie einen Händler, um ein Treueprogramm zu erstellen.",
"search_merchant": "Händler suchen",
"type_merchant_name": "Händlername eingeben...",
"no_merchants_found": "Keine Händler gefunden",
"existing_program_warning": "Dieser Händler hat bereits ein Treueprogramm.",
"view_edit_existing": "Bestehendes Programm anzeigen / bearbeiten"
},
"merchant_detail": {
"title": "Händler-Treuedetails",
"loading": "Treuedetails werden geladen...",
"error_loading": "Fehler beim Laden der Händlertreue",
"program_active": "Treueprogramm aktiv",
"no_program_subtitle": "Kein Treueprogramm",
"quick_actions": "Schnellaktionen",
"edit_program": "Programm bearbeiten",
"admin_policy": "Admin-Richtlinie",
"view_merchant": "Händler anzeigen",
"total_members": "Mitglieder insgesamt",
"active_30d": "Aktiv (30T)",
"points_issued_30d": "Punkte vergeben (30T)",
"points_redeemed_30d": "Punkte eingelöst (30T)",
"no_program": "Kein Treueprogramm",
"no_program_desc": "Dieser Händler hat noch kein Treueprogramm eingerichtet.",
"create_program": "Programm erstellen",
"delete_title": "Treueprogramm löschen",
"delete_message": "Das Treueprogramm und alle zugehörigen Daten werden dauerhaft gelöscht. Dies kann nicht rückgängig gemacht werden.",
"delete_confirm": "Programm löschen",
"location_breakdown": "Aufschlüsselung nach Standort",
"table_location": "Standort",
"table_enrolled": "Angemeldet",
"table_points_earned": "Punkte verdient",
"table_points_redeemed": "Punkte eingelöst",
"table_transactions_30d": "Transaktionen (30T)",
"admin_policy_settings": "Admin-Richtlinieneinstellungen",
"staff_pin_policy": "Mitarbeiter-PIN-Richtlinie",
"self_enrollment": "Selbstanmeldung",
"cross_location_redemption": "Standortübergreifende Einlösung",
"allowed": "Erlaubt",
"disabled": "Deaktiviert",
"modify_policy": "Admin-Richtlinie ändern"
},
"merchant_settings": {
"title": "Händler-Treueeinstellungen",
"loading": "Einstellungen werden geladen...",
"error_loading": "Fehler beim Laden der Einstellungen",
"admin_controlled": "Admin-kontrollierte Einstellungen für das Treueprogramm dieses Händlers",
"staff_pin_policy": "Mitarbeiter-PIN-Richtlinie",
"staff_pin_description": "Legen Sie fest, ob Mitarbeiter einen PIN eingeben müssen, um Treuetransaktionen zu verarbeiten.",
"required": "Erforderlich",
"required_desc": "Mitarbeiter müssen ihren PIN bei jeder Transaktion eingeben. Empfohlen für die Sicherheit.",
"optional": "Optional",
"optional_desc": "Geschäfte können wählen, ob PINs erforderlich sind.",
"pin_disabled": "Deaktiviert",
"pin_disabled_desc": "Mitarbeiter-PINs werden nicht verwendet. Jeder Mitarbeiter kann Transaktionen verarbeiten.",
"pin_lockout_settings": "PIN-Sperreinstellungen",
"max_failed_attempts": "Max. Fehlversuche",
"max_failed_attempts_help": "Anzahl falscher Versuche vor Sperrung (3-10)",
"lockout_duration": "Sperrdauer (Minuten)",
"lockout_duration_help": "Sperrdauer nach Fehlversuchen (5-120 Minuten)",
"enrollment_settings": "Anmeldeeinstellungen",
"allow_self_enrollment": "Selbstanmeldung erlauben",
"self_enrollment_desc": "Kunden können sich per QR-Code ohne Personal anmelden",
"transaction_settings": "Transaktionseinstellungen",
"allow_cross_location": "Standortübergreifende Einlösung erlauben",
"cross_location_desc": "Kunden können Punkte an allen Standorten des Händlers einlösen",
"allow_void": "Stornierungen erlauben",
"void_desc": "Mitarbeiter können Punkte/Stempel bei Rückgaben stornieren",
"save_settings": "Einstellungen speichern"
},
"analytics": {
"title": "Treue-Analytik",
"subtitle": "Plattformweite Treueprogramm-Statistiken",
"loading": "Analytik wird geladen...",
"error_loading": "Fehler beim Laden der Analytik",
"filter_by_merchant": "Nach Händler filtern",
"search_merchants_placeholder": "Händler nach Name suchen...",
"showing_stats_for": "Statistiken für:",
"wallet_status": "Wallet-Integrationsstatus",
"google_wallet": "Google Wallet",
"apple_wallet": "Apple Wallet",
"connected": "Verbunden",
"error": "Fehler",
"not_configured": "Nicht konfiguriert",
"issuer_id": "Aussteller-ID",
"project": "Projekt",
"wallet_objects": "Wallet-Objekte",
"loyalty_classes": "Treueklassen",
"pass_type_id": "Pass-Typ-ID",
"team_id": "Team-ID",
"active_passes": "Aktive Pässe",
"quick_actions": "Schnellaktionen",
"view_all_programs": "Alle Programme anzeigen",
"manage_merchants": "Händler verwalten"
},
"program_edit": {
"title": "Programmkonfiguration",
"loading": "Konfiguration wird geladen...",
"error_loading": "Fehler beim Laden der Programmkonfiguration",
"create_subtitle": "Treueprogramm für diesen Händler erstellen",
"edit_subtitle": "Programmkonfiguration bearbeiten",
"delete_title": "Treueprogramm löschen",
"delete_message": "Das Treueprogramm und alle zugehörigen Daten (Karten, Transaktionen, Prämien) werden dauerhaft gelöscht. Dies kann nicht rückgängig gemacht werden.",
"delete_confirm": "Programm löschen"
}
},
"merchant": {
"program": {
"title": "Treueprogramm",
"subtitle": "Ihre Treueprogramm-Konfiguration.",
"edit_program": "Programm bearbeiten",
"no_program": "Kein Treueprogramm",
"no_program_desc": "Ihr Treueprogramm wurde noch nicht eingerichtet. Erstellen Sie eines, um Ihre Kunden zu belohnen.",
"create_program": "Programm erstellen"
},
"program_edit": {
"title": "Treue-Einstellungen",
"page_title": "Treueprogramm-Einstellungen",
"subtitle": "Konfigurieren Sie Ihr Treueprogramm",
"loading": "Einstellungen werden geladen...",
"error_loading": "Fehler beim Laden der Einstellungen",
"delete_title": "Treueprogramm löschen",
"delete_message": "Ihr Treueprogramm und alle zugehörigen Daten (Karten, Transaktionen, Prämien) werden dauerhaft gelöscht. Dies kann nicht rückgängig gemacht werden.",
"delete_confirm": "Programm löschen"
},
"analytics": {
"title": "Treue-Analytik",
"subtitle": "Treueprogramm-Statistiken für alle Ihre Geschäfte",
"loading": "Analytik wird geladen...",
"error_loading": "Fehler beim Laden der Analytik",
"no_program": "Kein Treueprogramm",
"no_program_desc": "Richten Sie ein Treueprogramm ein, um hier Analytik zu sehen.",
"create_program": "Programm erstellen",
"quick_actions": "Schnellaktionen",
"view_program": "Programm anzeigen",
"edit_program": "Programm bearbeiten"
}
},
"store": {
"terminal": {
"title": "Treue-Terminal",
"subtitle": "Treuetransaktionen verarbeiten",
"members": "Mitglieder",
"analytics": "Analytik",
"loading": "Treue-Terminal wird geladen...",
"error_loading": "Fehler beim Laden des Terminals",
"not_setup": "Treueprogramm nicht eingerichtet",
"not_setup_desc": "Ihr Händler hat noch kein Treueprogramm konfiguriert.",
"setup_program": "Treueprogramm einrichten",
"contact_admin": "Kontaktieren Sie Ihren Administrator, um die Einrichtung abzuschließen.",
"find_customer": "Kunde finden",
"search_placeholder": "E-Mail, Telefon oder Kartennummer...",
"looking_up": "Suche läuft...",
"look_up_customer": "Kunde suchen",
"enroll_new_customer": "Neuen Kunden anmelden",
"customer_found": "Kunde gefunden",
"points_balance": "Punktestand",
"stamps": "Stempel",
"x_more_for_reward": "Noch {count} für Prämie",
"ready_to_redeem": "Bereit zum Einlösen!",
"add_stamp": "Stempel hinzufügen",
"current": "Aktuell:",
"cooldown_active": "Wartezeit aktiv",
"redeem_stamps": "Stempel einlösen",
"not_enough_stamps": "Noch nicht genug Stempel",
"earn_points": "Punkte sammeln",
"purchase_amount": "Einkaufsbetrag",
"points_to_award": "Zu vergebende Punkte:",
"award_points": "Punkte vergeben",
"redeem_reward": "Prämie einlösen",
"select_reward": "Prämie auswählen",
"select_reward_placeholder": "Prämie auswählen...",
"points_after": "Punkte danach:",
"search_to_process": "Suchen Sie einen Kunden, um eine Transaktion zu verarbeiten",
"recent_transactions": "Letzte Transaktionen an diesem Standort",
"table_time": "Zeit",
"table_customer": "Kunde",
"table_type": "Typ",
"table_points": "Punkte",
"table_notes": "Notizen",
"no_recent_transactions": "Keine aktuellen Transaktionen",
"enter_staff_pin": "Mitarbeiter-PIN eingeben",
"pin_authorize": "Geben Sie Ihren Mitarbeiter-PIN ein, um diese Transaktion zu autorisieren.",
"clear": "Löschen",
"processing": "Verarbeitung...",
"customer_not_found": "Kunde nicht gefunden. Sie können ihn als neues Mitglied anmelden.",
"error_lookup": "Fehler bei der Kundensuche: {message}",
"transaction_failed": "Transaktion fehlgeschlagen: {message}",
"stamp_added": "Stempel hinzugefügt!",
"stamps_redeemed": "Stempel eingelöst! Prämie erhalten.",
"x_points_awarded": "{points} Punkte vergeben!",
"reward_redeemed": "Prämie eingelöst: {name}"
},
"cards": {
"title": "Treue-Mitglieder",
"subtitle": "Mitglieder Ihres Treueprogramms anzeigen und verwalten",
"enroll_new": "Anmelden",
"loading": "Mitglieder werden geladen...",
"error_loading": "Fehler beim Laden der Mitglieder",
"total_members": "Mitglieder insgesamt",
"active_30d": "Aktiv (30T)",
"new_this_month": "Neu diesen Monat",
"total_points_balance": "Gesamtpunktestand",
"search_placeholder": "Nach Name, E-Mail, Telefon oder Karte suchen...",
"all_status": "Alle Status",
"table_member": "Mitglied",
"table_card_number": "Kartennummer",
"table_points_balance": "Punktestand",
"table_last_activity": "Letzte Aktivität",
"table_status": "Status",
"table_actions": "Aktionen",
"no_members": "Keine Mitglieder gefunden",
"adjust_search": "Versuchen Sie, Ihre Suche anzupassen",
"enroll_first": "Melden Sie Ihren ersten Kunden an"
},
"card_detail": {
"title": "Mitgliederdetails",
"loading": "Mitgliederdetails werden geladen...",
"error_loading": "Fehler beim Laden des Mitglieds",
"points_balance": "Punktestand",
"total_earned": "Insgesamt verdient",
"total_redeemed": "Insgesamt eingelöst",
"member_since": "Mitglied seit",
"customer_information": "Kundeninformation",
"name": "Name",
"email": "E-Mail",
"phone": "Telefon",
"birthday": "Geburtstag",
"card_details": "Kartendetails",
"card_number": "Kartennummer",
"status": "Status",
"last_activity": "Letzte Aktivität",
"enrolled_at": "Angemeldet am",
"transaction_history": "Transaktionsverlauf",
"table_date": "Datum",
"table_type": "Typ",
"table_points": "Punkte",
"table_location": "Standort",
"table_notes": "Notizen",
"no_transactions": "Noch keine Transaktionen"
},
"enroll": {
"title": "Kunde anmelden",
"page_title": "Neuen Kunden anmelden",
"subtitle": "Neues Mitglied zu Ihrem Treueprogramm hinzufügen",
"loading": "Laden...",
"error_loading": "Fehler beim Laden des Anmeldeformulars",
"customer_information": "Kundeninformation",
"first_name": "Vorname",
"last_name": "Nachname",
"email": "E-Mail",
"phone": "Telefon",
"birthday": "Geburtstag",
"birthday_help": "Für Geburtstagsbelohnungen (optional)",
"communication_preferences": "Kommunikationseinstellungen",
"send_emails": "Werbe-E-Mails senden",
"send_sms": "Werbe-SMS senden",
"welcome_bonus": "Willkommensbonus",
"welcome_bonus_desc": "Kunde erhält {points} Bonuspunkte!",
"enroll_customer": "Kunde anmelden",
"enrolling": "Anmeldung...",
"customer_enrolled": "Kunde angemeldet!",
"starting_balance": "Anfangssaldo:",
"x_points": "{count} Punkte",
"back_to_terminal": "Zurück zum Terminal",
"enroll_another": "Weiteren anmelden",
"enrollment_failed": "Anmeldung fehlgeschlagen: {message}"
},
"analytics": {
"title": "Treue-Analytik",
"subtitle": "Verfolgen Sie die Leistung Ihres Treueprogramms",
"loading": "Analytik wird geladen...",
"error_loading": "Fehler beim Laden der Analytik",
"quick_actions": "Schnellaktionen",
"open_terminal": "Terminal öffnen",
"view_members": "Mitglieder anzeigen",
"view_program": "Programm anzeigen"
},
"program": {
"title": "Treueprogramm",
"subtitle": "Ihre Treueprogramm-Konfiguration",
"edit_program": "Programm bearbeiten",
"loading": "Programm wird geladen...",
"error_loading": "Fehler beim Laden des Programms",
"no_program": "Kein Treueprogramm",
"no_program_desc": "Ihr Händler hat noch kein Treueprogramm konfiguriert.",
"create_program": "Programm erstellen",
"contact_admin": "Kontaktieren Sie Ihren Administrator, um ein Treueprogramm einzurichten."
},
"settings": {
"title": "Treue-Einstellungen",
"page_title": "Treueprogramm-Einstellungen",
"subtitle": "Konfigurieren Sie Ihr Treueprogramm",
"back_to_program": "Zurück zum Programm",
"loading": "Einstellungen werden geladen...",
"error_loading": "Fehler beim Laden der Einstellungen",
"access_restricted": "Zugriff eingeschränkt",
"owner_only": "Nur der Geschäftsinhaber kann die Treueprogramm-Einstellungen verwalten.",
"delete_title": "Treueprogramm löschen",
"delete_message": "Das Treueprogramm und alle zugehörigen Daten (Karten, Transaktionen, Prämien) werden dauerhaft gelöscht. Dies kann nicht rückgängig gemacht werden.",
"delete_confirm": "Programm löschen",
"program_created": "Programm erfolgreich erstellt",
"program_updated": "Programm erfolgreich aktualisiert",
"program_deleted": "Treueprogramm gelöscht",
"save_failed": "Speichern fehlgeschlagen: {message}",
"delete_failed": "Löschen fehlgeschlagen: {message}"
}
},
"storefront": {
"dashboard": {
"back_to_account": "Zurück zum Konto",
"my_loyalty": "Meine Treue",
"join_title": "Treten Sie unserem Prämienprogramm bei!",
"join_subtitle": "Sammeln Sie Punkte bei jedem Einkauf und lösen Sie sie gegen Prämien ein.",
"join_now": "Jetzt beitreten",
"points_balance": "Punktestand",
"card_number": "Kartennummer",
"show_card": "Karte anzeigen",
"total_earned": "Insgesamt verdient",
"total_redeemed": "Insgesamt eingelöst",
"available_rewards": "Verfügbare Prämien",
"no_rewards_yet": "Noch keine Prämien verfügbar",
"ready_to_redeem": "Bereit zum Einlösen",
"x_more_to_go": "Noch {count}",
"redeem_hint": "Zeigen Sie Ihre Karte dem Personal, um Prämien im Geschäft einzulösen.",
"recent_activity": "Letzte Aktivität",
"view_all": "Alle anzeigen",
"no_transactions": "Noch keine Transaktionen. Machen Sie einen Einkauf, um Punkte zu sammeln!",
"earn_redeem_locations": "Sammel- & Einlöse-Standorte",
"your_loyalty_card": "Ihre Treuekarte",
"show_to_staff": "Zeigen Sie dies dem Personal beim Einkauf oder Einlösen von Prämien."
},
"history": {
"back_to_loyalty": "Zurück zur Treue",
"title": "Transaktionsverlauf",
"subtitle": "Alle Ihre Treuepunkttransaktionen anzeigen",
"current_balance": "Aktueller Saldo",
"total_earned": "Insgesamt verdient",
"total_redeemed": "Insgesamt eingelöst",
"no_transactions": "Noch keine Transaktionen",
"balance": "Saldo:",
"previous": "Zurück",
"next": "Weiter",
"page_x_of_y": "Seite {page} von {pages}"
}
},
"toasts": {
"program_activated": "Programm erfolgreich aktiviert",
"program_deactivated": "Programm erfolgreich deaktiviert",
"activate_failed": "Programm konnte nicht aktiviert werden: {message}",
"deactivate_failed": "Programm konnte nicht deaktiviert werden: {message}",
"program_deleted": "Programm erfolgreich gelöscht",
"delete_failed": "Programm konnte nicht gelöscht werden: {message}",
"program_created": "Programm erfolgreich erstellt",
"program_updated": "Programm erfolgreich aktualisiert",
"loyalty_program_created": "Treueprogramm erstellt",
"loyalty_program_deleted": "Treueprogramm gelöscht",
"settings_saved": "Einstellungen erfolgreich gespeichert",
"save_failed": "Speichern fehlgeschlagen: {message}",
"settings_save_failed": "Einstellungen konnten nicht gespeichert werden: {message}",
"create_failed": "Programm konnte nicht erstellt werden: {message}",
"logo_required": "Logo-URL ist für die Wallet-Integration erforderlich."
}
}

View File

@@ -97,5 +97,589 @@
"title": "Create a loyalty program",
"description": "Set up your first stamp or points program"
}
},
"enrollment": {
"title": "Join Our Rewards Program!",
"subtitle": "Earn {points} point for every EUR you spend",
"not_available_title": "Program Not Available",
"not_available_message": "This store doesn't have a loyalty program set up yet.",
"welcome_bonus": "Get {points} bonus points when you join!",
"already_member": "Already a member? Your points are linked to your email.",
"form": {
"email": "Email",
"first_name": "First Name",
"last_name": "Last Name",
"phone": "Phone (optional)",
"birthday": "Birthday (optional)",
"birthday_hint": "For special birthday rewards",
"terms_agree": "I agree to the",
"terms": "Terms & Conditions",
"marketing_consent": "Send me news and special offers",
"joining": "Joining...",
"join_button": "Join & Get {points} Points"
},
"privacy_policy": "Privacy Policy",
"close": "Close",
"success": {
"title": "Welcome!",
"message": "You're now a member of our rewards program.",
"card_number": "Your Card Number",
"wallet_prompt": "Save your card to your phone for easy access:",
"next_steps_title": "What's Next?",
"step_earn": "Show your card number when making purchases to earn points",
"step_balance": "Check your balance online or in the app",
"step_redeem": "Redeem points for rewards at any of our locations",
"view_dashboard": "View My Loyalty Dashboard",
"continue_shopping": "Continue Shopping"
},
"errors": {
"load_failed": "Failed to load program information",
"email_exists": "This email is already registered in our loyalty program.",
"failed": "Enrollment failed. Please try again."
}
},
"common": {
"active": "Active",
"inactive": "Inactive",
"cancel": "Cancel",
"save": "Save",
"delete": "Delete",
"confirm": "Confirm",
"refresh": "Refresh",
"loading": "Loading...",
"saving": "Saving...",
"view": "View",
"edit": "Edit",
"yes": "Yes",
"no": "No",
"none": "None",
"never": "Never",
"total": "TOTAL",
"continue": "Continue",
"back": "Back",
"points": "points",
"minutes": "minutes",
"or": "or",
"at": "at"
},
"transactions": {
"card_created": "Enrolled",
"welcome_bonus": "Welcome Bonus",
"stamp_earned": "Stamp Earned",
"stamp_redeemed": "Stamp Redeemed",
"stamp_voided": "Stamp Voided",
"stamp_adjustment": "Stamp Adjusted",
"points_earned": "Points Earned",
"points_redeemed": "Points Redeemed",
"points_voided": "Points Voided",
"points_adjustment": "Points Adjusted",
"points_expired": "Points Expired",
"card_deactivated": "Deactivated",
"reward_redeemed": "Reward Redeemed"
},
"shared": {
"analytics": {
"total_programs": "Total Programs",
"total_members": "Total Members",
"active_members": "Active Members",
"points_issued_30d": "Points Issued (30d)",
"transactions_30d": "Transactions (30d)",
"x_active": "{count} active",
"points_overview": "Points Overview",
"points_issued_vs_redeemed": "Points Issued vs Redeemed (30d)",
"issued": "Issued:",
"redeemed": "Redeemed:",
"redemption_rate": "Redemption Rate",
"outstanding_balance": "Outstanding Balance",
"member_activity": "Member Activity",
"active_members_30d": "Active Members (30d)",
"new_this_month": "New This Month",
"merchants_with_programs": "Merchants with Programs",
"avg_points_per_member": "Avg Points Per Member",
"all_time_statistics": "All-Time Statistics",
"total_points_issued": "Total Points Issued",
"total_points_redeemed": "Total Points Redeemed",
"points_redeemed_30d": "Points Redeemed (30d)",
"outstanding_liability": "Outstanding Liability",
"location_breakdown": "Location Breakdown",
"store": "Store",
"enrolled": "Enrolled",
"points_earned": "Points Earned",
"points_redeemed": "Points Redeemed"
},
"program_view": {
"program_configuration": "Program Configuration",
"program_name": "Program Name",
"card_name": "Card Name",
"stamps_configuration": "Stamps Configuration",
"stamps_target": "Stamps Target",
"reward_description": "Reward Description",
"reward_value": "Reward Value",
"points_configuration": "Points Configuration",
"points_per_eur": "Points per EUR",
"welcome_bonus": "Welcome Bonus",
"x_points": "{count} points",
"minimum_redemption": "Minimum Redemption",
"minimum_purchase": "Minimum Purchase",
"points_expiration": "Points Expiration",
"x_days_inactivity": "{days} days of inactivity",
"redemption_rewards": "Redemption Rewards",
"reward": "Reward",
"points_required": "Points Required",
"description": "Description",
"anti_fraud": "Anti-Fraud",
"cooldown": "Cooldown",
"x_minutes": "{count} minutes",
"max_daily_stamps": "Max Daily Stamps",
"staff_pin_required": "Staff PIN Required",
"branding": "Branding",
"primary_color": "Primary Color",
"secondary_color": "Secondary Color",
"logo_url": "Logo URL",
"hero_image_url": "Hero Image URL",
"terms_privacy": "Terms & Privacy",
"terms_conditions": "Terms & Conditions",
"privacy_policy_url": "Privacy Policy URL"
},
"program_form": {
"program_type": "Program Type",
"points_type_desc": "Earn points per EUR spent",
"stamps_type_desc": "Collect N stamps, get reward",
"hybrid_type_desc": "Both stamps and points",
"stamps_configuration": "Stamps Configuration",
"stamps_target": "Stamps Target",
"stamps_target_help": "Number of stamps needed for reward",
"reward_description": "Reward Description",
"reward_value_cents": "Reward Value (cents)",
"points_configuration": "Points Configuration",
"points_per_eur": "Points per EUR spent",
"eur_equals_points": "1 EUR = {points} point(s)",
"welcome_bonus_points": "Welcome Bonus Points",
"welcome_bonus_help": "Bonus points awarded on enrollment",
"minimum_redemption_points": "Minimum Redemption Points",
"minimum_purchase_cents": "Minimum Purchase (cents)",
"minimum_purchase_help": "Minimum purchase amount to earn points (0 = no minimum)",
"points_expiration_days": "Points Expiration (days)",
"points_expiration_help": "Days of inactivity before points expire (0 = never)",
"redemption_rewards": "Redemption Rewards",
"add_reward": "Add Reward",
"no_rewards_configured": "No rewards configured. Add a reward to allow customers to redeem points.",
"reward_name": "Reward Name",
"points_required": "Points Required",
"description": "Description",
"anti_fraud_settings": "Anti-Fraud Settings",
"cooldown_minutes": "Cooldown (minutes)",
"cooldown_help": "Time between stamps from the same card",
"max_daily_stamps": "Max Daily Stamps",
"max_daily_stamps_help": "Maximum stamps per card per day",
"require_staff_pin": "Require Staff PIN",
"branding": "Branding",
"card_name": "Card Name",
"primary_color": "Primary Color",
"secondary_color": "Secondary Color",
"logo_url": "Logo URL",
"logo_url_help": "Required for Google Wallet integration. Must be a publicly accessible image URL (PNG or JPG).",
"hero_image_url": "Hero Image URL",
"terms_privacy": "Terms & Privacy",
"terms_conditions": "Terms & Conditions",
"privacy_policy_url": "Privacy Policy URL",
"program_status": "Program Status",
"program_active": "Program Active",
"program_active_help": "When disabled, customers cannot earn or redeem",
"delete_program": "Delete Program",
"create_program": "Create Program",
"save_changes": "Save Changes"
}
},
"admin": {
"programs": {
"title": "Loyalty Programs",
"create_program": "Create Program",
"loading": "Loading loyalty programs...",
"error_loading": "Error loading loyalty programs",
"total_programs": "Total Programs",
"active": "Active",
"total_members": "Total Members",
"transactions_30d": "Transactions (30d)",
"search_placeholder": "Search by merchant name...",
"all_status": "All Status",
"table_merchant": "Merchant",
"table_program_type": "Program Type",
"table_members": "Members",
"table_points_issued": "Points Issued",
"table_status": "Status",
"table_created": "Created",
"table_actions": "Actions",
"no_programs": "No loyalty programs found",
"adjust_filters": "Try adjusting your search or filters",
"no_merchants_yet": "No merchants have set up loyalty programs yet",
"x_active": "({count} active)",
"x_redeemed": "{count} redeemed",
"pt_per_eur": "pt/EUR",
"delete_title": "Delete Loyalty Program",
"delete_message": "Delete the loyalty program for \"{name}\"? This will permanently remove all associated data (cards, transactions, rewards). This cannot be undone.",
"delete_confirm": "Delete Program",
"create_title": "Create Loyalty Program",
"create_description": "Select a merchant to create a loyalty program for.",
"search_merchant": "Search Merchant",
"type_merchant_name": "Type merchant name...",
"no_merchants_found": "No merchants found",
"existing_program_warning": "This merchant already has a loyalty program.",
"view_edit_existing": "View / Edit existing program"
},
"merchant_detail": {
"title": "Merchant Loyalty Details",
"loading": "Loading merchant loyalty details...",
"error_loading": "Error loading merchant loyalty",
"program_active": "Loyalty Program Active",
"no_program_subtitle": "No Loyalty Program",
"quick_actions": "Quick Actions",
"edit_program": "Edit Program",
"admin_policy": "Admin Policy",
"view_merchant": "View Merchant",
"total_members": "Total Members",
"active_30d": "Active (30d)",
"points_issued_30d": "Points Issued (30d)",
"points_redeemed_30d": "Points Redeemed (30d)",
"no_program": "No Loyalty Program",
"no_program_desc": "This merchant has not set up a loyalty program yet.",
"create_program": "Create Program",
"delete_title": "Delete Loyalty Program",
"delete_message": "This will permanently delete the loyalty program and all associated data. This action cannot be undone.",
"delete_confirm": "Delete Program",
"location_breakdown": "Location Breakdown",
"table_location": "Location",
"table_enrolled": "Enrolled",
"table_points_earned": "Points Earned",
"table_points_redeemed": "Points Redeemed",
"table_transactions_30d": "Transactions (30d)",
"admin_policy_settings": "Admin Policy Settings",
"staff_pin_policy": "Staff PIN Policy",
"self_enrollment": "Self Enrollment",
"cross_location_redemption": "Cross-Location Redemption",
"allowed": "Allowed",
"disabled": "Disabled",
"modify_policy": "Modify admin policy"
},
"merchant_settings": {
"title": "Merchant Loyalty Settings",
"loading": "Loading settings...",
"error_loading": "Error loading settings",
"admin_controlled": "Admin-controlled settings for this merchant's loyalty program",
"staff_pin_policy": "Staff PIN Policy",
"staff_pin_description": "Control whether staff members at this merchant's locations must enter a PIN to process loyalty transactions.",
"required": "Required",
"required_desc": "Staff must enter their PIN for every transaction. Recommended for security.",
"optional": "Optional",
"optional_desc": "Stores can choose whether to require PINs at their locations.",
"pin_disabled": "Disabled",
"pin_disabled_desc": "Staff PINs are not used. Any staff member can process transactions.",
"pin_lockout_settings": "PIN Lockout Settings",
"max_failed_attempts": "Max Failed Attempts",
"max_failed_attempts_help": "Number of wrong attempts before lockout (3-10)",
"lockout_duration": "Lockout Duration (minutes)",
"lockout_duration_help": "How long to lock out after failed attempts (5-120 minutes)",
"enrollment_settings": "Enrollment Settings",
"allow_self_enrollment": "Allow Self-Service Enrollment",
"self_enrollment_desc": "Customers can sign up via QR code without staff assistance",
"transaction_settings": "Transaction Settings",
"allow_cross_location": "Allow Cross-Location Redemption",
"cross_location_desc": "Customers can redeem points at any merchant location",
"allow_void": "Allow Void Transactions",
"void_desc": "Staff can void points/stamps for returns",
"save_settings": "Save Settings"
},
"analytics": {
"title": "Loyalty Analytics",
"subtitle": "Platform-wide loyalty program statistics",
"loading": "Loading analytics...",
"error_loading": "Error loading analytics",
"filter_by_merchant": "Filter by Merchant",
"search_merchants_placeholder": "Search merchants by name...",
"showing_stats_for": "Showing stats for:",
"wallet_status": "Wallet Integration Status",
"google_wallet": "Google Wallet",
"apple_wallet": "Apple Wallet",
"connected": "Connected",
"error": "Error",
"not_configured": "Not Configured",
"issuer_id": "Issuer ID",
"project": "Project",
"wallet_objects": "Wallet Objects",
"loyalty_classes": "Loyalty Classes",
"pass_type_id": "Pass Type ID",
"team_id": "Team ID",
"active_passes": "Active Passes",
"quick_actions": "Quick Actions",
"view_all_programs": "View All Programs",
"manage_merchants": "Manage Merchants"
},
"program_edit": {
"title": "Program Configuration",
"loading": "Loading program configuration...",
"error_loading": "Error loading program configuration",
"create_subtitle": "Create a loyalty program for this merchant",
"edit_subtitle": "Edit program configuration",
"delete_title": "Delete Loyalty Program",
"delete_message": "This will permanently delete the loyalty program and all associated data (cards, transactions, rewards). This action cannot be undone.",
"delete_confirm": "Delete Program"
}
},
"merchant": {
"program": {
"title": "Loyalty Program",
"subtitle": "Your loyalty program configuration.",
"edit_program": "Edit Program",
"no_program": "No Loyalty Program",
"no_program_desc": "Your loyalty program hasn't been set up yet. Create one to start rewarding your customers.",
"create_program": "Create Program"
},
"program_edit": {
"title": "Loyalty Settings",
"page_title": "Loyalty Program Settings",
"subtitle": "Configure your loyalty program",
"loading": "Loading settings...",
"error_loading": "Error loading settings",
"delete_title": "Delete Loyalty Program",
"delete_message": "This will permanently delete your loyalty program and all associated data (cards, transactions, rewards). This action cannot be undone.",
"delete_confirm": "Delete Program"
},
"analytics": {
"title": "Loyalty Analytics",
"subtitle": "Loyalty program statistics across all your stores",
"loading": "Loading analytics...",
"error_loading": "Error loading analytics",
"no_program": "No Loyalty Program",
"no_program_desc": "Set up a loyalty program to see analytics here.",
"create_program": "Create Program",
"quick_actions": "Quick Actions",
"view_program": "View Program",
"edit_program": "Edit Program"
}
},
"store": {
"terminal": {
"title": "Loyalty Terminal",
"subtitle": "Process loyalty transactions",
"members": "Members",
"analytics": "Analytics",
"loading": "Loading loyalty terminal...",
"error_loading": "Error loading terminal",
"not_setup": "Loyalty Program Not Set Up",
"not_setup_desc": "Your merchant doesn't have a loyalty program configured yet.",
"setup_program": "Set Up Loyalty Program",
"contact_admin": "Contact your administrator to complete the setup.",
"find_customer": "Find Customer",
"search_placeholder": "Email, phone, or card number...",
"looking_up": "Looking up...",
"look_up_customer": "Look Up Customer",
"enroll_new_customer": "Enroll New Customer",
"customer_found": "Customer Found",
"points_balance": "Points Balance",
"stamps": "Stamps",
"x_more_for_reward": "{count} more for reward",
"ready_to_redeem": "Ready to redeem!",
"add_stamp": "Add Stamp",
"current": "Current:",
"cooldown_active": "Cooldown active",
"redeem_stamps": "Redeem Stamps",
"not_enough_stamps": "Not enough stamps yet",
"earn_points": "Earn Points",
"purchase_amount": "Purchase Amount",
"points_to_award": "Points to award:",
"award_points": "Award Points",
"redeem_reward": "Redeem Reward",
"select_reward": "Select Reward",
"select_reward_placeholder": "Select reward...",
"points_after": "Points after:",
"search_to_process": "Search for a customer to process a transaction",
"recent_transactions": "Recent Transactions at This Location",
"table_time": "Time",
"table_customer": "Customer",
"table_type": "Type",
"table_points": "Points",
"table_notes": "Notes",
"no_recent_transactions": "No recent transactions",
"enter_staff_pin": "Enter Staff PIN",
"pin_authorize": "Enter your staff PIN to authorize this transaction.",
"clear": "Clear",
"processing": "Processing...",
"customer_not_found": "Customer not found. You can enroll them as a new member.",
"error_lookup": "Error looking up customer: {message}",
"transaction_failed": "Transaction failed: {message}",
"stamp_added": "Stamp added!",
"stamps_redeemed": "Stamps redeemed! Reward earned.",
"x_points_awarded": "{points} points awarded!",
"reward_redeemed": "Reward redeemed: {name}"
},
"cards": {
"title": "Loyalty Members",
"subtitle": "View and manage your loyalty program members",
"enroll_new": "Enroll New",
"loading": "Loading members...",
"error_loading": "Error loading members",
"total_members": "Total Members",
"active_30d": "Active (30d)",
"new_this_month": "New This Month",
"total_points_balance": "Total Points Balance",
"search_placeholder": "Search by name, email, phone, or card...",
"all_status": "All Status",
"table_member": "Member",
"table_card_number": "Card Number",
"table_points_balance": "Points Balance",
"table_last_activity": "Last Activity",
"table_status": "Status",
"table_actions": "Actions",
"no_members": "No members found",
"adjust_search": "Try adjusting your search",
"enroll_first": "Enroll your first customer to get started"
},
"card_detail": {
"title": "Member Details",
"loading": "Loading member details...",
"error_loading": "Error loading member",
"points_balance": "Points Balance",
"total_earned": "Total Earned",
"total_redeemed": "Total Redeemed",
"member_since": "Member Since",
"customer_information": "Customer Information",
"name": "Name",
"email": "Email",
"phone": "Phone",
"birthday": "Birthday",
"card_details": "Card Details",
"card_number": "Card Number",
"status": "Status",
"last_activity": "Last Activity",
"enrolled_at": "Enrolled At",
"transaction_history": "Transaction History",
"table_date": "Date",
"table_type": "Type",
"table_points": "Points",
"table_location": "Location",
"table_notes": "Notes",
"no_transactions": "No transactions yet"
},
"enroll": {
"title": "Enroll Customer",
"page_title": "Enroll New Customer",
"subtitle": "Add a new member to your loyalty program",
"loading": "Loading...",
"error_loading": "Error loading enrollment form",
"customer_information": "Customer Information",
"first_name": "First Name",
"last_name": "Last Name",
"email": "Email",
"phone": "Phone",
"birthday": "Birthday",
"birthday_help": "For birthday rewards (optional)",
"communication_preferences": "Communication Preferences",
"send_emails": "Send promotional emails",
"send_sms": "Send promotional SMS",
"welcome_bonus": "Welcome Bonus",
"welcome_bonus_desc": "Customer will receive {points} bonus points!",
"enroll_customer": "Enroll Customer",
"enrolling": "Enrolling...",
"customer_enrolled": "Customer Enrolled!",
"starting_balance": "Starting Balance:",
"x_points": "{count} points",
"back_to_terminal": "Back to Terminal",
"enroll_another": "Enroll Another",
"enrollment_failed": "Enrollment failed: {message}"
},
"analytics": {
"title": "Loyalty Analytics",
"subtitle": "Track your loyalty program performance",
"loading": "Loading analytics...",
"error_loading": "Error loading analytics",
"quick_actions": "Quick Actions",
"open_terminal": "Open Terminal",
"view_members": "View Members",
"view_program": "View Program"
},
"program": {
"title": "Loyalty Program",
"subtitle": "Your loyalty program configuration",
"edit_program": "Edit Program",
"loading": "Loading program...",
"error_loading": "Error loading program",
"no_program": "No Loyalty Program",
"no_program_desc": "Your merchant doesn't have a loyalty program configured yet.",
"create_program": "Create Program",
"contact_admin": "Contact your administrator to set up a loyalty program."
},
"settings": {
"title": "Loyalty Settings",
"page_title": "Loyalty Program Settings",
"subtitle": "Configure your loyalty program",
"back_to_program": "Back to Program",
"loading": "Loading settings...",
"error_loading": "Error loading settings",
"access_restricted": "Access Restricted",
"owner_only": "Only the merchant owner can manage loyalty program settings.",
"delete_title": "Delete Loyalty Program",
"delete_message": "This will permanently delete the loyalty program and all associated data (cards, transactions, rewards). This action cannot be undone.",
"delete_confirm": "Delete Program",
"program_created": "Program created successfully",
"program_updated": "Program updated successfully",
"program_deleted": "Loyalty program deleted",
"save_failed": "Failed to save: {message}",
"delete_failed": "Failed to delete: {message}"
}
},
"storefront": {
"dashboard": {
"back_to_account": "Back to Account",
"my_loyalty": "My Loyalty",
"join_title": "Join Our Rewards Program!",
"join_subtitle": "Earn points on every purchase and redeem for rewards.",
"join_now": "Join Now",
"points_balance": "Points Balance",
"card_number": "Card Number",
"show_card": "Show Card",
"total_earned": "Total Earned",
"total_redeemed": "Total Redeemed",
"available_rewards": "Available Rewards",
"no_rewards_yet": "No rewards available yet",
"ready_to_redeem": "Ready to redeem",
"x_more_to_go": "{count} more to go",
"redeem_hint": "Show your card to staff to redeem rewards in-store.",
"recent_activity": "Recent Activity",
"view_all": "View All",
"no_transactions": "No transactions yet. Make a purchase to start earning points!",
"earn_redeem_locations": "Earn & Redeem Locations",
"your_loyalty_card": "Your Loyalty Card",
"show_to_staff": "Show this to staff when making a purchase or redeeming rewards."
},
"history": {
"back_to_loyalty": "Back to Loyalty",
"title": "Transaction History",
"subtitle": "View all your loyalty point transactions",
"current_balance": "Current Balance",
"total_earned": "Total Earned",
"total_redeemed": "Total Redeemed",
"no_transactions": "No transactions yet",
"balance": "Balance:",
"previous": "Previous",
"next": "Next",
"page_x_of_y": "Page {page} of {pages}"
}
},
"toasts": {
"program_activated": "Program activated successfully",
"program_deactivated": "Program deactivated successfully",
"activate_failed": "Failed to activate program: {message}",
"deactivate_failed": "Failed to deactivate program: {message}",
"program_deleted": "Program deleted successfully",
"delete_failed": "Failed to delete program: {message}",
"program_created": "Program created successfully",
"program_updated": "Program updated successfully",
"loyalty_program_created": "Loyalty program created",
"loyalty_program_deleted": "Loyalty program deleted",
"settings_saved": "Settings saved successfully",
"save_failed": "Failed to save: {message}",
"settings_save_failed": "Failed to save settings: {message}",
"create_failed": "Failed to create program: {message}",
"logo_required": "Logo URL is required for wallet integration."
}
}

View File

@@ -97,5 +97,589 @@
"title": "Créer un programme de fidélité",
"description": "Créez votre premier programme de tampons ou de points"
}
},
"enrollment": {
"title": "Rejoignez notre programme de récompenses !",
"subtitle": "Gagnez {points} point pour chaque EUR dépensé",
"not_available_title": "Programme non disponible",
"not_available_message": "Ce magasin n'a pas encore de programme de fidélité.",
"welcome_bonus": "Obtenez {points} points bonus en vous inscrivant !",
"already_member": "Déjà membre ? Vos points sont liés à votre e-mail.",
"form": {
"email": "E-mail",
"first_name": "Prénom",
"last_name": "Nom",
"phone": "Téléphone (facultatif)",
"birthday": "Date de naissance (facultatif)",
"birthday_hint": "Pour des récompenses d'anniversaire spéciales",
"terms_agree": "J'accepte les",
"terms": "Conditions Générales",
"marketing_consent": "M'envoyer des nouvelles et offres spéciales",
"joining": "Inscription en cours...",
"join_button": "S'inscrire et obtenir {points} points"
},
"privacy_policy": "Politique de confidentialité",
"close": "Fermer",
"success": {
"title": "Bienvenue !",
"message": "Vous êtes maintenant membre de notre programme de récompenses.",
"card_number": "Votre numéro de carte",
"wallet_prompt": "Enregistrez votre carte sur votre téléphone pour un accès facile :",
"next_steps_title": "Et maintenant ?",
"step_earn": "Présentez votre numéro de carte lors de vos achats pour gagner des points",
"step_balance": "Consultez votre solde en ligne ou dans l'application",
"step_redeem": "Échangez vos points contre des récompenses dans tous nos points de vente",
"view_dashboard": "Voir mon tableau de bord fidélité",
"continue_shopping": "Continuer mes achats"
},
"errors": {
"load_failed": "Impossible de charger les informations du programme",
"email_exists": "Cet e-mail est déjà inscrit dans notre programme de fidélité.",
"failed": "L'inscription a échoué. Veuillez réessayer."
}
},
"common": {
"active": "Actif",
"inactive": "Inactif",
"cancel": "Annuler",
"save": "Enregistrer",
"delete": "Supprimer",
"confirm": "Confirmer",
"refresh": "Actualiser",
"loading": "Chargement...",
"saving": "Enregistrement...",
"view": "Voir",
"edit": "Modifier",
"yes": "Oui",
"no": "Non",
"none": "Aucun",
"never": "Jamais",
"total": "TOTAL",
"continue": "Continuer",
"back": "Retour",
"points": "points",
"minutes": "minutes",
"or": "ou",
"at": "à"
},
"transactions": {
"card_created": "Inscrit",
"welcome_bonus": "Bonus de bienvenue",
"stamp_earned": "Tampon obtenu",
"stamp_redeemed": "Tampon échangé",
"stamp_voided": "Tampon annulé",
"stamp_adjustment": "Tampon ajusté",
"points_earned": "Points gagnés",
"points_redeemed": "Points échangés",
"points_voided": "Points annulés",
"points_adjustment": "Points ajustés",
"points_expired": "Points expirés",
"card_deactivated": "Désactivé",
"reward_redeemed": "Récompense échangée"
},
"shared": {
"analytics": {
"total_programs": "Programmes au total",
"total_members": "Membres au total",
"active_members": "Membres actifs",
"points_issued_30d": "Points émis (30j)",
"transactions_30d": "Transactions (30j)",
"x_active": "{count} actifs",
"points_overview": "Aperçu des points",
"points_issued_vs_redeemed": "Points émis vs échangés (30j)",
"issued": "Émis :",
"redeemed": "Échangés :",
"redemption_rate": "Taux d'échange",
"outstanding_balance": "Solde restant",
"member_activity": "Activité des membres",
"active_members_30d": "Membres actifs (30j)",
"new_this_month": "Nouveaux ce mois",
"merchants_with_programs": "Commerçants avec programmes",
"avg_points_per_member": "Moy. points par membre",
"all_time_statistics": "Statistiques globales",
"total_points_issued": "Total points émis",
"total_points_redeemed": "Total points échangés",
"points_redeemed_30d": "Points échangés (30j)",
"outstanding_liability": "Passif en cours",
"location_breakdown": "Répartition par point de vente",
"store": "Magasin",
"enrolled": "Inscrits",
"points_earned": "Points gagnés",
"points_redeemed": "Points échangés"
},
"program_view": {
"program_configuration": "Configuration du programme",
"program_name": "Nom du programme",
"card_name": "Nom de la carte",
"stamps_configuration": "Configuration des tampons",
"stamps_target": "Objectif de tampons",
"reward_description": "Description de la récompense",
"reward_value": "Valeur de la récompense",
"points_configuration": "Configuration des points",
"points_per_eur": "Points par EUR",
"welcome_bonus": "Bonus de bienvenue",
"x_points": "{count} points",
"minimum_redemption": "Échange minimum",
"minimum_purchase": "Achat minimum",
"points_expiration": "Expiration des points",
"x_days_inactivity": "{days} jours d'inactivité",
"redemption_rewards": "Récompenses d'échange",
"reward": "Récompense",
"points_required": "Points requis",
"description": "Description",
"anti_fraud": "Anti-fraude",
"cooldown": "Temps d'attente",
"x_minutes": "{count} minutes",
"max_daily_stamps": "Tampons max par jour",
"staff_pin_required": "PIN personnel requis",
"branding": "Image de marque",
"primary_color": "Couleur principale",
"secondary_color": "Couleur secondaire",
"logo_url": "URL du logo",
"hero_image_url": "URL de l'image principale",
"terms_privacy": "Conditions & Confidentialité",
"terms_conditions": "Conditions Générales",
"privacy_policy_url": "URL politique de confidentialité"
},
"program_form": {
"program_type": "Type de programme",
"points_type_desc": "Gagner des points par EUR dépensé",
"stamps_type_desc": "Collecter N tampons, obtenir une récompense",
"hybrid_type_desc": "Tampons et points combinés",
"stamps_configuration": "Configuration des tampons",
"stamps_target": "Objectif de tampons",
"stamps_target_help": "Nombre de tampons nécessaires pour la récompense",
"reward_description": "Description de la récompense",
"reward_value_cents": "Valeur de la récompense (centimes)",
"points_configuration": "Configuration des points",
"points_per_eur": "Points par EUR dépensé",
"eur_equals_points": "1 EUR = {points} point(s)",
"welcome_bonus_points": "Points bonus de bienvenue",
"welcome_bonus_help": "Points bonus attribués à l'inscription",
"minimum_redemption_points": "Points minimum d'échange",
"minimum_purchase_cents": "Achat minimum (centimes)",
"minimum_purchase_help": "Montant minimum d'achat pour gagner des points (0 = pas de minimum)",
"points_expiration_days": "Expiration des points (jours)",
"points_expiration_help": "Jours d'inactivité avant expiration des points (0 = jamais)",
"redemption_rewards": "Récompenses d'échange",
"add_reward": "Ajouter une récompense",
"no_rewards_configured": "Aucune récompense configurée. Ajoutez une récompense pour permettre aux clients d'échanger des points.",
"reward_name": "Nom de la récompense",
"points_required": "Points requis",
"description": "Description",
"anti_fraud_settings": "Paramètres anti-fraude",
"cooldown_minutes": "Temps d'attente (minutes)",
"cooldown_help": "Temps entre les tampons d'une même carte",
"max_daily_stamps": "Tampons max par jour",
"max_daily_stamps_help": "Maximum de tampons par carte par jour",
"require_staff_pin": "Exiger le PIN du personnel",
"branding": "Image de marque",
"card_name": "Nom de la carte",
"primary_color": "Couleur principale",
"secondary_color": "Couleur secondaire",
"logo_url": "URL du logo",
"logo_url_help": "Requis pour l'intégration Google Wallet. Doit être une URL d'image publique (PNG ou JPG).",
"hero_image_url": "URL de l'image principale",
"terms_privacy": "Conditions & Confidentialité",
"terms_conditions": "Conditions Générales",
"privacy_policy_url": "URL politique de confidentialité",
"program_status": "Statut du programme",
"program_active": "Programme actif",
"program_active_help": "Désactivé, les clients ne peuvent ni gagner ni échanger",
"delete_program": "Supprimer le programme",
"create_program": "Créer le programme",
"save_changes": "Enregistrer les modifications"
}
},
"admin": {
"programs": {
"title": "Programmes de fidélité",
"create_program": "Créer un programme",
"loading": "Chargement des programmes de fidélité...",
"error_loading": "Erreur de chargement des programmes de fidélité",
"total_programs": "Programmes au total",
"active": "Actifs",
"total_members": "Membres au total",
"transactions_30d": "Transactions (30j)",
"search_placeholder": "Rechercher par nom de commerçant...",
"all_status": "Tous les statuts",
"table_merchant": "Commerçant",
"table_program_type": "Type de programme",
"table_members": "Membres",
"table_points_issued": "Points émis",
"table_status": "Statut",
"table_created": "Créé",
"table_actions": "Actions",
"no_programs": "Aucun programme de fidélité trouvé",
"adjust_filters": "Essayez d'ajuster votre recherche ou vos filtres",
"no_merchants_yet": "Aucun commerçant n'a encore créé de programme de fidélité",
"x_active": "({count} actifs)",
"x_redeemed": "{count} échangés",
"pt_per_eur": "pt/EUR",
"delete_title": "Supprimer le programme de fidélité",
"delete_message": "Supprimer le programme de fidélité de \"{name}\" ? Cela supprimera définitivement toutes les données associées (cartes, transactions, récompenses). Cette action est irréversible.",
"delete_confirm": "Supprimer le programme",
"create_title": "Créer un programme de fidélité",
"create_description": "Sélectionnez un commerçant pour créer un programme de fidélité.",
"search_merchant": "Rechercher un commerçant",
"type_merchant_name": "Tapez le nom du commerçant...",
"no_merchants_found": "Aucun commerçant trouvé",
"existing_program_warning": "Ce commerçant a déjà un programme de fidélité.",
"view_edit_existing": "Voir / Modifier le programme existant"
},
"merchant_detail": {
"title": "Détails fidélité du commerçant",
"loading": "Chargement des détails de fidélité...",
"error_loading": "Erreur de chargement de la fidélité du commerçant",
"program_active": "Programme de fidélité actif",
"no_program_subtitle": "Aucun programme de fidélité",
"quick_actions": "Actions rapides",
"edit_program": "Modifier le programme",
"admin_policy": "Politique admin",
"view_merchant": "Voir le commerçant",
"total_members": "Membres au total",
"active_30d": "Actifs (30j)",
"points_issued_30d": "Points émis (30j)",
"points_redeemed_30d": "Points échangés (30j)",
"no_program": "Aucun programme de fidélité",
"no_program_desc": "Ce commerçant n'a pas encore configuré de programme de fidélité.",
"create_program": "Créer un programme",
"delete_title": "Supprimer le programme de fidélité",
"delete_message": "Cela supprimera définitivement le programme de fidélité et toutes les données associées. Cette action est irréversible.",
"delete_confirm": "Supprimer le programme",
"location_breakdown": "Répartition par point de vente",
"table_location": "Point de vente",
"table_enrolled": "Inscrits",
"table_points_earned": "Points gagnés",
"table_points_redeemed": "Points échangés",
"table_transactions_30d": "Transactions (30j)",
"admin_policy_settings": "Paramètres de politique admin",
"staff_pin_policy": "Politique PIN du personnel",
"self_enrollment": "Auto-inscription",
"cross_location_redemption": "Échange inter-points de vente",
"allowed": "Autorisé",
"disabled": "Désactivé",
"modify_policy": "Modifier la politique admin"
},
"merchant_settings": {
"title": "Paramètres de fidélité du commerçant",
"loading": "Chargement des paramètres...",
"error_loading": "Erreur de chargement des paramètres",
"admin_controlled": "Paramètres contrôlés par l'admin pour le programme de fidélité de ce commerçant",
"staff_pin_policy": "Politique PIN du personnel",
"staff_pin_description": "Contrôlez si les membres du personnel doivent saisir un PIN pour traiter les transactions de fidélité.",
"required": "Obligatoire",
"required_desc": "Le personnel doit saisir son PIN pour chaque transaction. Recommandé pour la sécurité.",
"optional": "Facultatif",
"optional_desc": "Les magasins peuvent choisir d'exiger ou non les PINs.",
"pin_disabled": "Désactivé",
"pin_disabled_desc": "Les PINs du personnel ne sont pas utilisés. Tout membre du personnel peut traiter les transactions.",
"pin_lockout_settings": "Paramètres de verrouillage PIN",
"max_failed_attempts": "Tentatives échouées max",
"max_failed_attempts_help": "Nombre de tentatives erronées avant verrouillage (3-10)",
"lockout_duration": "Durée de verrouillage (minutes)",
"lockout_duration_help": "Durée du verrouillage après échecs (5-120 minutes)",
"enrollment_settings": "Paramètres d'inscription",
"allow_self_enrollment": "Autoriser l'auto-inscription",
"self_enrollment_desc": "Les clients peuvent s'inscrire via QR code sans aide du personnel",
"transaction_settings": "Paramètres de transaction",
"allow_cross_location": "Autoriser l'échange inter-points de vente",
"cross_location_desc": "Les clients peuvent échanger des points dans tous les points de vente du commerçant",
"allow_void": "Autoriser les annulations",
"void_desc": "Le personnel peut annuler des points/tampons pour les retours",
"save_settings": "Enregistrer les paramètres"
},
"analytics": {
"title": "Analytique fidélité",
"subtitle": "Statistiques de fidélité à l'échelle de la plateforme",
"loading": "Chargement des analytiques...",
"error_loading": "Erreur de chargement des analytiques",
"filter_by_merchant": "Filtrer par commerçant",
"search_merchants_placeholder": "Rechercher des commerçants par nom...",
"showing_stats_for": "Statistiques pour :",
"wallet_status": "Statut de l'intégration Wallet",
"google_wallet": "Google Wallet",
"apple_wallet": "Apple Wallet",
"connected": "Connecté",
"error": "Erreur",
"not_configured": "Non configuré",
"issuer_id": "ID émetteur",
"project": "Projet",
"wallet_objects": "Objets Wallet",
"loyalty_classes": "Classes de fidélité",
"pass_type_id": "ID type de pass",
"team_id": "ID équipe",
"active_passes": "Pass actifs",
"quick_actions": "Actions rapides",
"view_all_programs": "Voir tous les programmes",
"manage_merchants": "Gérer les commerçants"
},
"program_edit": {
"title": "Configuration du programme",
"loading": "Chargement de la configuration...",
"error_loading": "Erreur de chargement de la configuration du programme",
"create_subtitle": "Créer un programme de fidélité pour ce commerçant",
"edit_subtitle": "Modifier la configuration du programme",
"delete_title": "Supprimer le programme de fidélité",
"delete_message": "Cela supprimera définitivement le programme de fidélité et toutes les données associées (cartes, transactions, récompenses). Cette action est irréversible.",
"delete_confirm": "Supprimer le programme"
}
},
"merchant": {
"program": {
"title": "Programme de fidélité",
"subtitle": "Configuration de votre programme de fidélité.",
"edit_program": "Modifier le programme",
"no_program": "Aucun programme de fidélité",
"no_program_desc": "Votre programme de fidélité n'a pas encore été configuré. Créez-en un pour commencer à récompenser vos clients.",
"create_program": "Créer un programme"
},
"program_edit": {
"title": "Paramètres de fidélité",
"page_title": "Paramètres du programme de fidélité",
"subtitle": "Configurez votre programme de fidélité",
"loading": "Chargement des paramètres...",
"error_loading": "Erreur de chargement des paramètres",
"delete_title": "Supprimer le programme de fidélité",
"delete_message": "Cela supprimera définitivement votre programme de fidélité et toutes les données associées (cartes, transactions, récompenses). Cette action est irréversible.",
"delete_confirm": "Supprimer le programme"
},
"analytics": {
"title": "Analytique fidélité",
"subtitle": "Statistiques de fidélité pour tous vos magasins",
"loading": "Chargement des analytiques...",
"error_loading": "Erreur de chargement des analytiques",
"no_program": "Aucun programme de fidélité",
"no_program_desc": "Configurez un programme de fidélité pour voir les analytiques ici.",
"create_program": "Créer un programme",
"quick_actions": "Actions rapides",
"view_program": "Voir le programme",
"edit_program": "Modifier le programme"
}
},
"store": {
"terminal": {
"title": "Terminal de fidélité",
"subtitle": "Traiter les transactions de fidélité",
"members": "Membres",
"analytics": "Analytique",
"loading": "Chargement du terminal de fidélité...",
"error_loading": "Erreur de chargement du terminal",
"not_setup": "Programme de fidélité non configuré",
"not_setup_desc": "Votre commerçant n'a pas encore configuré de programme de fidélité.",
"setup_program": "Configurer le programme de fidélité",
"contact_admin": "Contactez votre administrateur pour terminer la configuration.",
"find_customer": "Trouver un client",
"search_placeholder": "E-mail, téléphone ou numéro de carte...",
"looking_up": "Recherche en cours...",
"look_up_customer": "Rechercher un client",
"enroll_new_customer": "Inscrire un nouveau client",
"customer_found": "Client trouvé",
"points_balance": "Solde de points",
"stamps": "Tampons",
"x_more_for_reward": "Encore {count} pour la récompense",
"ready_to_redeem": "Prêt à échanger !",
"add_stamp": "Ajouter un tampon",
"current": "Actuel :",
"cooldown_active": "Temps d'attente actif",
"redeem_stamps": "Échanger les tampons",
"not_enough_stamps": "Pas assez de tampons encore",
"earn_points": "Gagner des points",
"purchase_amount": "Montant de l'achat",
"points_to_award": "Points à attribuer :",
"award_points": "Attribuer des points",
"redeem_reward": "Échanger une récompense",
"select_reward": "Sélectionner une récompense",
"select_reward_placeholder": "Sélectionner une récompense...",
"points_after": "Points après :",
"search_to_process": "Recherchez un client pour traiter une transaction",
"recent_transactions": "Transactions récentes dans ce point de vente",
"table_time": "Heure",
"table_customer": "Client",
"table_type": "Type",
"table_points": "Points",
"table_notes": "Notes",
"no_recent_transactions": "Aucune transaction récente",
"enter_staff_pin": "Saisir le PIN du personnel",
"pin_authorize": "Saisissez votre PIN personnel pour autoriser cette transaction.",
"clear": "Effacer",
"processing": "Traitement...",
"customer_not_found": "Client introuvable. Vous pouvez l'inscrire comme nouveau membre.",
"error_lookup": "Erreur de recherche du client : {message}",
"transaction_failed": "Transaction échouée : {message}",
"stamp_added": "Tampon ajouté !",
"stamps_redeemed": "Tampons échangés ! Récompense obtenue.",
"x_points_awarded": "{points} points attribués !",
"reward_redeemed": "Récompense échangée : {name}"
},
"cards": {
"title": "Membres fidélité",
"subtitle": "Voir et gérer les membres de votre programme de fidélité",
"enroll_new": "Inscrire",
"loading": "Chargement des membres...",
"error_loading": "Erreur de chargement des membres",
"total_members": "Membres au total",
"active_30d": "Actifs (30j)",
"new_this_month": "Nouveaux ce mois",
"total_points_balance": "Solde total de points",
"search_placeholder": "Rechercher par nom, e-mail, téléphone ou carte...",
"all_status": "Tous les statuts",
"table_member": "Membre",
"table_card_number": "Numéro de carte",
"table_points_balance": "Solde de points",
"table_last_activity": "Dernière activité",
"table_status": "Statut",
"table_actions": "Actions",
"no_members": "Aucun membre trouvé",
"adjust_search": "Essayez d'ajuster votre recherche",
"enroll_first": "Inscrivez votre premier client pour commencer"
},
"card_detail": {
"title": "Détails du membre",
"loading": "Chargement des détails du membre...",
"error_loading": "Erreur de chargement du membre",
"points_balance": "Solde de points",
"total_earned": "Total gagné",
"total_redeemed": "Total échangé",
"member_since": "Membre depuis",
"customer_information": "Informations client",
"name": "Nom",
"email": "E-mail",
"phone": "Téléphone",
"birthday": "Date de naissance",
"card_details": "Détails de la carte",
"card_number": "Numéro de carte",
"status": "Statut",
"last_activity": "Dernière activité",
"enrolled_at": "Inscrit le",
"transaction_history": "Historique des transactions",
"table_date": "Date",
"table_type": "Type",
"table_points": "Points",
"table_location": "Point de vente",
"table_notes": "Notes",
"no_transactions": "Aucune transaction"
},
"enroll": {
"title": "Inscrire un client",
"page_title": "Inscrire un nouveau client",
"subtitle": "Ajouter un nouveau membre à votre programme de fidélité",
"loading": "Chargement...",
"error_loading": "Erreur de chargement du formulaire d'inscription",
"customer_information": "Informations client",
"first_name": "Prénom",
"last_name": "Nom",
"email": "E-mail",
"phone": "Téléphone",
"birthday": "Date de naissance",
"birthday_help": "Pour les récompenses d'anniversaire (facultatif)",
"communication_preferences": "Préférences de communication",
"send_emails": "Envoyer des e-mails promotionnels",
"send_sms": "Envoyer des SMS promotionnels",
"welcome_bonus": "Bonus de bienvenue",
"welcome_bonus_desc": "Le client recevra {points} points bonus !",
"enroll_customer": "Inscrire le client",
"enrolling": "Inscription...",
"customer_enrolled": "Client inscrit !",
"starting_balance": "Solde initial :",
"x_points": "{count} points",
"back_to_terminal": "Retour au terminal",
"enroll_another": "Inscrire un autre",
"enrollment_failed": "Inscription échouée : {message}"
},
"analytics": {
"title": "Analytique fidélité",
"subtitle": "Suivez les performances de votre programme de fidélité",
"loading": "Chargement des analytiques...",
"error_loading": "Erreur de chargement des analytiques",
"quick_actions": "Actions rapides",
"open_terminal": "Ouvrir le terminal",
"view_members": "Voir les membres",
"view_program": "Voir le programme"
},
"program": {
"title": "Programme de fidélité",
"subtitle": "Configuration de votre programme de fidélité",
"edit_program": "Modifier le programme",
"loading": "Chargement du programme...",
"error_loading": "Erreur de chargement du programme",
"no_program": "Aucun programme de fidélité",
"no_program_desc": "Votre commerçant n'a pas encore configuré de programme de fidélité.",
"create_program": "Créer un programme",
"contact_admin": "Contactez votre administrateur pour configurer un programme de fidélité."
},
"settings": {
"title": "Paramètres de fidélité",
"page_title": "Paramètres du programme de fidélité",
"subtitle": "Configurez votre programme de fidélité",
"back_to_program": "Retour au programme",
"loading": "Chargement des paramètres...",
"error_loading": "Erreur de chargement des paramètres",
"access_restricted": "Accès restreint",
"owner_only": "Seul le propriétaire du commerce peut gérer les paramètres du programme de fidélité.",
"delete_title": "Supprimer le programme de fidélité",
"delete_message": "Cela supprimera définitivement le programme de fidélité et toutes les données associées (cartes, transactions, récompenses). Cette action est irréversible.",
"delete_confirm": "Supprimer le programme",
"program_created": "Programme créé avec succès",
"program_updated": "Programme mis à jour avec succès",
"program_deleted": "Programme de fidélité supprimé",
"save_failed": "Échec de l'enregistrement : {message}",
"delete_failed": "Échec de la suppression : {message}"
}
},
"storefront": {
"dashboard": {
"back_to_account": "Retour au compte",
"my_loyalty": "Ma fidélité",
"join_title": "Rejoignez notre programme de récompenses !",
"join_subtitle": "Gagnez des points à chaque achat et échangez-les contre des récompenses.",
"join_now": "Rejoindre maintenant",
"points_balance": "Solde de points",
"card_number": "Numéro de carte",
"show_card": "Afficher la carte",
"total_earned": "Total gagné",
"total_redeemed": "Total échangé",
"available_rewards": "Récompenses disponibles",
"no_rewards_yet": "Aucune récompense disponible pour le moment",
"ready_to_redeem": "Prêt à échanger",
"x_more_to_go": "Encore {count}",
"redeem_hint": "Montrez votre carte au personnel pour échanger des récompenses en magasin.",
"recent_activity": "Activité récente",
"view_all": "Tout voir",
"no_transactions": "Aucune transaction. Faites un achat pour commencer à gagner des points !",
"earn_redeem_locations": "Points de vente partenaires",
"your_loyalty_card": "Votre carte de fidélité",
"show_to_staff": "Montrez ceci au personnel lors d'un achat ou d'un échange de récompense."
},
"history": {
"back_to_loyalty": "Retour à la fidélité",
"title": "Historique des transactions",
"subtitle": "Voir toutes vos transactions de points de fidélité",
"current_balance": "Solde actuel",
"total_earned": "Total gagné",
"total_redeemed": "Total échangé",
"no_transactions": "Aucune transaction",
"balance": "Solde :",
"previous": "Précédent",
"next": "Suivant",
"page_x_of_y": "Page {page} sur {pages}"
}
},
"toasts": {
"program_activated": "Programme activé avec succès",
"program_deactivated": "Programme désactivé avec succès",
"activate_failed": "Échec de l'activation du programme : {message}",
"deactivate_failed": "Échec de la désactivation du programme : {message}",
"program_deleted": "Programme supprimé avec succès",
"delete_failed": "Échec de la suppression du programme : {message}",
"program_created": "Programme créé avec succès",
"program_updated": "Programme mis à jour avec succès",
"loyalty_program_created": "Programme de fidélité créé",
"loyalty_program_deleted": "Programme de fidélité supprimé",
"settings_saved": "Paramètres enregistrés avec succès",
"save_failed": "Échec de l'enregistrement : {message}",
"settings_save_failed": "Échec de l'enregistrement des paramètres : {message}",
"create_failed": "Échec de la création du programme : {message}",
"logo_required": "L'URL du logo est requise pour l'intégration wallet."
}
}

View File

@@ -96,5 +96,589 @@
"title": "Treieprogramm erstellen",
"description": "Erstellt Äert éischt Stempel- oder Punkteprogramm"
}
},
"enrollment": {
"title": "Gitt Member vun eisem Belounungsprogramm!",
"subtitle": "Verdéngt {points} Punkt pro ausgegoenen EUR",
"not_available_title": "Programm net verfügbar",
"not_available_message": "Dëse Buttek huet nach kee Treieprogramm ageriicht.",
"welcome_bonus": "Kritt {points} Bonuspunkten wann Dir Iech umellt!",
"already_member": "Schonn Member? Är Punkten sinn mat Ärer E-Mail verlinkt.",
"form": {
"email": "E-Mail",
"first_name": "Virnumm",
"last_name": "Nonumm",
"phone": "Telefon (fakultativ)",
"birthday": "Gebuertsdag (fakultativ)",
"birthday_hint": "Fir speziell Gebuertsdagsbelounungen",
"terms_agree": "Ech akzeptéieren d'",
"terms": "Allgemeng Geschäftsbedingungen",
"marketing_consent": "Mir Neiegkeeten an Sonderoffere schécken",
"joining": "Umeldung leeft...",
"join_button": "Bäitrieden & {points} Punkten kréien"
},
"privacy_policy": "Dateschutzrichtlinn",
"close": "Zoumaachen",
"success": {
"title": "Wëllkomm!",
"message": "Dir sidd elo Member vun eisem Belounungsprogramm.",
"card_number": "Är Kaartnummer",
"wallet_prompt": "Späichert Är Kaart op Ärem Handy fir einfachen Zougang:",
"next_steps_title": "Wéi geet et weider?",
"step_earn": "Weist Är Kaartnummer beim Akaf fir Punkten ze sammelen",
"step_balance": "Préift Äre Kontostand online oder an der App",
"step_redeem": "Léist Punkten géint Belounungen an all eise Standuerter an",
"view_dashboard": "Mäin Treie-Dashboard kucken",
"continue_shopping": "Weider akafen"
},
"errors": {
"load_failed": "Programminformatiounen konnten net gelueden ginn",
"email_exists": "Dës E-Mail ass schonn an eisem Treieprogramm registréiert.",
"failed": "Umeldung feelgeschloen. Probéiert w.e.g. nach eng Kéier."
}
},
"common": {
"active": "Aktiv",
"inactive": "Inaktiv",
"cancel": "Ofbriechen",
"save": "Späicheren",
"delete": "Läschen",
"confirm": "Bestätegen",
"refresh": "Aktualiséieren",
"loading": "Lueden...",
"saving": "Späicheren...",
"view": "Kucken",
"edit": "Beaarbechten",
"yes": "Jo",
"no": "Neen",
"none": "Keen",
"never": "Ni",
"total": "TOTAL",
"continue": "Weider",
"back": "Zréck",
"points": "Punkten",
"minutes": "Minutten",
"or": "oder",
"at": "bei"
},
"transactions": {
"card_created": "Ageschriwwen",
"welcome_bonus": "Wëllkommensbonus",
"stamp_earned": "Stempel kritt",
"stamp_redeemed": "Stempel agelées",
"stamp_voided": "Stempel stornéiert",
"stamp_adjustment": "Stempel ugepasst",
"points_earned": "Punkten verdéngt",
"points_redeemed": "Punkten agelées",
"points_voided": "Punkten stornéiert",
"points_adjustment": "Punkten ugepasst",
"points_expired": "Punkten ofgelaf",
"card_deactivated": "Deaktivéiert",
"reward_redeemed": "Belounung agelées"
},
"shared": {
"analytics": {
"total_programs": "Programmer insgesamt",
"total_members": "Memberen insgesamt",
"active_members": "Aktiv Memberen",
"points_issued_30d": "Punkten vergi (30D)",
"transactions_30d": "Transaktiounen (30D)",
"x_active": "{count} aktiv",
"points_overview": "Punkteniwwersiicht",
"points_issued_vs_redeemed": "Punkten vergi vs agelées (30D)",
"issued": "Vergi:",
"redeemed": "Agelées:",
"redemption_rate": "Aléisungsquot",
"outstanding_balance": "Ausstehende Solde",
"member_activity": "Memberaktivitéit",
"active_members_30d": "Aktiv Memberen (30D)",
"new_this_month": "Nei dëse Mount",
"merchants_with_programs": "Händler mat Programmer",
"avg_points_per_member": "Durchschn. Punkten pro Member",
"all_time_statistics": "Gesamtstatistiken",
"total_points_issued": "Total Punkten vergi",
"total_points_redeemed": "Total Punkten agelées",
"points_redeemed_30d": "Punkten agelées (30D)",
"outstanding_liability": "Ausstehend Verbindlechkeet",
"location_breakdown": "Opschlësselung no Standuert",
"store": "Geschäft",
"enrolled": "Ageschriwwen",
"points_earned": "Punkten verdéngt",
"points_redeemed": "Punkten agelées"
},
"program_view": {
"program_configuration": "Programmkonfiguratioun",
"program_name": "Programmnumm",
"card_name": "Kaartnumm",
"stamps_configuration": "Stempelkonfiguratioun",
"stamps_target": "Stempelzil",
"reward_description": "Belounungsbeschreiwung",
"reward_value": "Belounungswäert",
"points_configuration": "Punktekonfiguratioun",
"points_per_eur": "Punkten pro EUR",
"welcome_bonus": "Wëllkommensbonus",
"x_points": "{count} Punkten",
"minimum_redemption": "Mindest-Aléisung",
"minimum_purchase": "Mindest-Akaf",
"points_expiration": "Punktenoflaaf",
"x_days_inactivity": "{days} Deeg Inaktivitéit",
"redemption_rewards": "Aléisungsbelounungen",
"reward": "Belounung",
"points_required": "Néideg Punkten",
"description": "Beschreiwung",
"anti_fraud": "Betrugsschutz",
"cooldown": "Waardezäit",
"x_minutes": "{count} Minutten",
"max_daily_stamps": "Max. Stempelen pro Dag",
"staff_pin_required": "Personal-PIN erfuerderlech",
"branding": "Branding",
"primary_color": "Haaptfaarf",
"secondary_color": "Secondärfaarf",
"logo_url": "Logo-URL",
"hero_image_url": "Hannergrondbild-URL",
"terms_privacy": "AGB & Dateschutz",
"terms_conditions": "Allgemeng Geschäftsbedingungen",
"privacy_policy_url": "Dateschutzrichtlinn-URL"
},
"program_form": {
"program_type": "Programmtyp",
"points_type_desc": "Punkten pro ausgegoenen EUR verdéngen",
"stamps_type_desc": "N Stempelen sammelen, Belounung kréien",
"hybrid_type_desc": "Stempelen a Punkten kombinéiert",
"stamps_configuration": "Stempelkonfiguratioun",
"stamps_target": "Stempelzil",
"stamps_target_help": "Unzuel vun de Stempelen fir d'Belounung",
"reward_description": "Belounungsbeschreiwung",
"reward_value_cents": "Belounungswäert (Cent)",
"points_configuration": "Punktekonfiguratioun",
"points_per_eur": "Punkten pro ausgegoenen EUR",
"eur_equals_points": "1 EUR = {points} Punkt(en)",
"welcome_bonus_points": "Wëllkommensbonuspunkten",
"welcome_bonus_help": "Bonuspunkten bei der Umeldung",
"minimum_redemption_points": "Mindest-Aléisungspunkten",
"minimum_purchase_cents": "Mindest-Akaf (Cent)",
"minimum_purchase_help": "Mindestakafsbetrag fir Punkten ze sammelen (0 = kee Minimum)",
"points_expiration_days": "Punktenoflaaf (Deeg)",
"points_expiration_help": "Deeg vun Inaktivitéit bis zum Punktenoflaaf (0 = ni)",
"redemption_rewards": "Aléisungsbelounungen",
"add_reward": "Belounung derbäisetzen",
"no_rewards_configured": "Keng Belounungen konfiguréiert. Setzt eng Belounung derbäi fir datt Clienten Punkten aléise kënnen.",
"reward_name": "Belounungsnumm",
"points_required": "Néideg Punkten",
"description": "Beschreiwung",
"anti_fraud_settings": "Betrugsschutz-Astellungen",
"cooldown_minutes": "Waardezäit (Minutten)",
"cooldown_help": "Zäit tëschent Stempelen vun der selwechter Kaart",
"max_daily_stamps": "Max. Stempelen pro Dag",
"max_daily_stamps_help": "Maximal Stempelen pro Kaart pro Dag",
"require_staff_pin": "Personal-PIN verlaangen",
"branding": "Branding",
"card_name": "Kaartnumm",
"primary_color": "Haaptfaarf",
"secondary_color": "Secondärfaarf",
"logo_url": "Logo-URL",
"logo_url_help": "Erfuerderlech fir Google Wallet-Integratioun. Muss eng ëffentlech zougänglech Bild-URL sinn (PNG oder JPG).",
"hero_image_url": "Hannergrondbild-URL",
"terms_privacy": "AGB & Dateschutz",
"terms_conditions": "Allgemeng Geschäftsbedingungen",
"privacy_policy_url": "Dateschutzrichtlinn-URL",
"program_status": "Programmstatus",
"program_active": "Programm aktiv",
"program_active_help": "Deaktivéiert kënne Clienten net sammelen oder aléisen",
"delete_program": "Programm läschen",
"create_program": "Programm erstellen",
"save_changes": "Ännerunge späicheren"
}
},
"admin": {
"programs": {
"title": "Treieprogrammer",
"create_program": "Programm erstellen",
"loading": "Treieprogrammer gi gelueden...",
"error_loading": "Feeler beim Luede vun den Treieprogrammer",
"total_programs": "Programmer insgesamt",
"active": "Aktiv",
"total_members": "Memberen insgesamt",
"transactions_30d": "Transaktiounen (30D)",
"search_placeholder": "No Händlernumm sichen...",
"all_status": "All Status",
"table_merchant": "Händler",
"table_program_type": "Programmtyp",
"table_members": "Memberen",
"table_points_issued": "Punkten vergi",
"table_status": "Status",
"table_created": "Erstellt",
"table_actions": "Aktiounen",
"no_programs": "Keng Treieprogrammer fonnt",
"adjust_filters": "Probéiert Är Sich oder Filter unzepassen",
"no_merchants_yet": "Nach kee Händler huet en Treieprogramm erstellt",
"x_active": "({count} aktiv)",
"x_redeemed": "{count} agelées",
"pt_per_eur": "Pkt/EUR",
"delete_title": "Treieprogramm läschen",
"delete_message": "Treieprogramm fir \"{name}\" läschen? All verbonnen Daten (Kaarten, Transaktiounen, Belounungen) ginn dauerhaft geläscht. Dëst kann net réckgängeg gemaach ginn.",
"delete_confirm": "Programm läschen",
"create_title": "Treieprogramm erstellen",
"create_description": "Wielt en Händler fir en Treieprogramm ze erstellen.",
"search_merchant": "Händler sichen",
"type_merchant_name": "Händlernumm aginn...",
"no_merchants_found": "Keen Händler fonnt",
"existing_program_warning": "Dësen Händler huet schonn en Treieprogramm.",
"view_edit_existing": "Bestehend Programm kucken / beaarbechten"
},
"merchant_detail": {
"title": "Händler-Treiedetailer",
"loading": "Treiedetailer gi gelueden...",
"error_loading": "Feeler beim Luede vun der Händlertreie",
"program_active": "Treieprogramm aktiv",
"no_program_subtitle": "Keen Treieprogramm",
"quick_actions": "Schnellaktiounen",
"edit_program": "Programm beaarbechten",
"admin_policy": "Admin-Richtlinn",
"view_merchant": "Händler kucken",
"total_members": "Memberen insgesamt",
"active_30d": "Aktiv (30D)",
"points_issued_30d": "Punkten vergi (30D)",
"points_redeemed_30d": "Punkten agelées (30D)",
"no_program": "Keen Treieprogramm",
"no_program_desc": "Dësen Händler huet nach keen Treieprogramm ageriicht.",
"create_program": "Programm erstellen",
"delete_title": "Treieprogramm läschen",
"delete_message": "D'Treieprogramm an all verbonnen Daten ginn dauerhaft geläscht. Dëst kann net réckgängeg gemaach ginn.",
"delete_confirm": "Programm läschen",
"location_breakdown": "Opschlësselung no Standuert",
"table_location": "Standuert",
"table_enrolled": "Ageschriwwen",
"table_points_earned": "Punkten verdéngt",
"table_points_redeemed": "Punkten agelées",
"table_transactions_30d": "Transaktiounen (30D)",
"admin_policy_settings": "Admin-Richtlinn-Astellungen",
"staff_pin_policy": "Personal-PIN-Richtlinn",
"self_enrollment": "Selbstumeldung",
"cross_location_redemption": "Standuertiwergreifend Aléisung",
"allowed": "Erlaabt",
"disabled": "Deaktivéiert",
"modify_policy": "Admin-Richtlinn änneren"
},
"merchant_settings": {
"title": "Händler-Treieastelllungen",
"loading": "Astellunge gi gelueden...",
"error_loading": "Feeler beim Luede vun den Astellungen",
"admin_controlled": "Admin-kontrolléiert Astellungen fir d'Treieprogramm vun dësem Händler",
"staff_pin_policy": "Personal-PIN-Richtlinn",
"staff_pin_description": "Bestëmmt ob Mataarbechter e PIN aginn mussen fir Treietransaktiounen ze veraarbechten.",
"required": "Erfuerderlech",
"required_desc": "Personal muss säin PIN bei all Transaktioun aginn. Recommandéiert fir d'Sécherheet.",
"optional": "Fakultativ",
"optional_desc": "Geschäfter kënne wielen ob PINe verlaangt ginn.",
"pin_disabled": "Deaktivéiert",
"pin_disabled_desc": "Personal-PINe ginn net benotzt. All Mataarbechter kann Transaktiounen veraarbechten.",
"pin_lockout_settings": "PIN-Sparrastellungen",
"max_failed_attempts": "Max. Fehlversich",
"max_failed_attempts_help": "Unzuel vu falschen Versich virun der Spär (3-10)",
"lockout_duration": "Spärauer (Minutten)",
"lockout_duration_help": "Wéi laang d'Spär no Fehlversich dauert (5-120 Minutten)",
"enrollment_settings": "Umeldungsastellungen",
"allow_self_enrollment": "Selbstumeldung erlaben",
"self_enrollment_desc": "Clienten kënne sech per QR-Code ouni Personal umellen",
"transaction_settings": "Transaktiouns-Astellungen",
"allow_cross_location": "Standuertiwergreifend Aléisung erlaben",
"cross_location_desc": "Clienten kënne Punkten an all Standuerter vum Händler aléisen",
"allow_void": "Stornéierungen erlaben",
"void_desc": "Personal kann Punkten/Stempelen bei Réckgaben stornéieren",
"save_settings": "Astellunge späicheren"
},
"analytics": {
"title": "Treie-Analytik",
"subtitle": "Plattformwäit Treieprogramm-Statistiken",
"loading": "Analytik gëtt gelueden...",
"error_loading": "Feeler beim Luede vun der Analytik",
"filter_by_merchant": "No Händler filtréieren",
"search_merchants_placeholder": "Händler no Numm sichen...",
"showing_stats_for": "Statistike fir:",
"wallet_status": "Wallet-Integratiounsstatus",
"google_wallet": "Google Wallet",
"apple_wallet": "Apple Wallet",
"connected": "Verbonnen",
"error": "Feeler",
"not_configured": "Net konfiguréiert",
"issuer_id": "Aussteller-ID",
"project": "Projet",
"wallet_objects": "Wallet-Objeten",
"loyalty_classes": "Treieklassen",
"pass_type_id": "Pass-Typ-ID",
"team_id": "Team-ID",
"active_passes": "Aktiv Päss",
"quick_actions": "Schnellaktiounen",
"view_all_programs": "All Programmer kucken",
"manage_merchants": "Händler verwalten"
},
"program_edit": {
"title": "Programmkonfiguratioun",
"loading": "Konfiguratioun gëtt gelueden...",
"error_loading": "Feeler beim Luede vun der Programmkonfiguratioun",
"create_subtitle": "Treieprogramm fir dësen Händler erstellen",
"edit_subtitle": "Programmkonfiguratioun beaarbechten",
"delete_title": "Treieprogramm läschen",
"delete_message": "D'Treieprogramm an all verbonnen Daten (Kaarten, Transaktiounen, Belounungen) ginn dauerhaft geläscht. Dëst kann net réckgängeg gemaach ginn.",
"delete_confirm": "Programm läschen"
}
},
"merchant": {
"program": {
"title": "Treieprogramm",
"subtitle": "Är Treieprogramm-Konfiguratioun.",
"edit_program": "Programm beaarbechten",
"no_program": "Keen Treieprogramm",
"no_program_desc": "Ärt Treieprogramm gouf nach net ageriicht. Erstellt eent fir Är Clienten ze belounegen.",
"create_program": "Programm erstellen"
},
"program_edit": {
"title": "Treie-Astellungen",
"page_title": "Treieprogramm-Astellungen",
"subtitle": "Konfiguréiert Ärt Treieprogramm",
"loading": "Astellunge gi gelueden...",
"error_loading": "Feeler beim Luede vun den Astellungen",
"delete_title": "Treieprogramm läschen",
"delete_message": "Ärt Treieprogramm an all verbonnen Daten (Kaarten, Transaktiounen, Belounungen) ginn dauerhaft geläscht. Dëst kann net réckgängeg gemaach ginn.",
"delete_confirm": "Programm läschen"
},
"analytics": {
"title": "Treie-Analytik",
"subtitle": "Treieprogramm-Statistiken fir all Är Geschäfter",
"loading": "Analytik gëtt gelueden...",
"error_loading": "Feeler beim Luede vun der Analytik",
"no_program": "Keen Treieprogramm",
"no_program_desc": "Riicht en Treieprogramm an fir hei Analytik ze gesinn.",
"create_program": "Programm erstellen",
"quick_actions": "Schnellaktiounen",
"view_program": "Programm kucken",
"edit_program": "Programm beaarbechten"
}
},
"store": {
"terminal": {
"title": "Treie-Terminal",
"subtitle": "Treietransaktiounen veraarbechten",
"members": "Memberen",
"analytics": "Analytik",
"loading": "Treie-Terminal gëtt gelueden...",
"error_loading": "Feeler beim Luede vum Terminal",
"not_setup": "Treieprogramm net ageriicht",
"not_setup_desc": "Ären Händler huet nach keen Treieprogramm konfiguréiert.",
"setup_program": "Treieprogramm ariichten",
"contact_admin": "Kontaktéiert Ären Administrateur fir d'Ariichtung ofzeschléissen.",
"find_customer": "Client fannen",
"search_placeholder": "E-Mail, Telefon oder Kaartnummer...",
"looking_up": "Sich leeft...",
"look_up_customer": "Client sichen",
"enroll_new_customer": "Neie Client umellen",
"customer_found": "Client fonnt",
"points_balance": "Punktestand",
"stamps": "Stempelen",
"x_more_for_reward": "Nach {count} fir d'Belounung",
"ready_to_redeem": "Prett fir anzeléisen!",
"add_stamp": "Stempel derbäisetzen",
"current": "Aktuell:",
"cooldown_active": "Waardezäit aktiv",
"redeem_stamps": "Stempelen aléisen",
"not_enough_stamps": "Nach net genuch Stempelen",
"earn_points": "Punkten sammelen",
"purchase_amount": "Akafsbetrag",
"points_to_award": "Punkten ze verginn:",
"award_points": "Punkten verginn",
"redeem_reward": "Belounung aléisen",
"select_reward": "Belounung wielen",
"select_reward_placeholder": "Belounung wielen...",
"points_after": "Punkten duerno:",
"search_to_process": "Sicht e Client fir eng Transaktioun ze veraarbechten",
"recent_transactions": "Rezent Transaktiounen un dësem Standuert",
"table_time": "Zäit",
"table_customer": "Client",
"table_type": "Typ",
"table_points": "Punkten",
"table_notes": "Notizen",
"no_recent_transactions": "Keng rezent Transaktiounen",
"enter_staff_pin": "Personal-PIN aginn",
"pin_authorize": "Gitt Äre Personal-PIN an fir dës Transaktioun ze autoriséieren.",
"clear": "Läschen",
"processing": "Veraarbechtung...",
"customer_not_found": "Client net fonnt. Dir kënnt hien als neit Member umellen.",
"error_lookup": "Feeler bei der Clientesich: {message}",
"transaction_failed": "Transaktioun feelgeschloen: {message}",
"stamp_added": "Stempel derbäigesat!",
"stamps_redeemed": "Stempelen agelées! Belounung kritt.",
"x_points_awarded": "{points} Punkten vergi!",
"reward_redeemed": "Belounung agelées: {name}"
},
"cards": {
"title": "Treie-Memberen",
"subtitle": "Memberen vun Ärem Treieprogramm kucken a verwalten",
"enroll_new": "Umellen",
"loading": "Membere gi gelueden...",
"error_loading": "Feeler beim Luede vun de Memberen",
"total_members": "Memberen insgesamt",
"active_30d": "Aktiv (30D)",
"new_this_month": "Nei dëse Mount",
"total_points_balance": "Gesamtpunktestand",
"search_placeholder": "No Numm, E-Mail, Telefon oder Kaart sichen...",
"all_status": "All Status",
"table_member": "Member",
"table_card_number": "Kaartnummer",
"table_points_balance": "Punktestand",
"table_last_activity": "Lescht Aktivitéit",
"table_status": "Status",
"table_actions": "Aktiounen",
"no_members": "Keng Membere fonnt",
"adjust_search": "Probéiert Är Sich unzepassen",
"enroll_first": "Mellt Ären éischte Client un"
},
"card_detail": {
"title": "Memberdetailer",
"loading": "Memberdetailer gi gelueden...",
"error_loading": "Feeler beim Luede vum Member",
"points_balance": "Punktestand",
"total_earned": "Insgesamt verdéngt",
"total_redeemed": "Insgesamt agelées",
"member_since": "Member zënter",
"customer_information": "Clienteninformatioun",
"name": "Numm",
"email": "E-Mail",
"phone": "Telefon",
"birthday": "Gebuertsdag",
"card_details": "Kaartdetailer",
"card_number": "Kaartnummer",
"status": "Status",
"last_activity": "Lescht Aktivitéit",
"enrolled_at": "Ugemellt um",
"transaction_history": "Transaktiounsverlaf",
"table_date": "Datum",
"table_type": "Typ",
"table_points": "Punkten",
"table_location": "Standuert",
"table_notes": "Notizen",
"no_transactions": "Nach keng Transaktiounen"
},
"enroll": {
"title": "Client umellen",
"page_title": "Neie Client umellen",
"subtitle": "Neit Member bei Ärem Treieprogramm derbäisetzen",
"loading": "Lueden...",
"error_loading": "Feeler beim Luede vum Umeldungsformular",
"customer_information": "Clienteninformatioun",
"first_name": "Virnumm",
"last_name": "Nonumm",
"email": "E-Mail",
"phone": "Telefon",
"birthday": "Gebuertsdag",
"birthday_help": "Fir Gebuertsdagsbelounungen (fakultativ)",
"communication_preferences": "Kommunikatiounsastellungen",
"send_emails": "Promotiounsmaile schécken",
"send_sms": "Promotiounssms schécken",
"welcome_bonus": "Wëllkommensbonus",
"welcome_bonus_desc": "Client kritt {points} Bonuspunkten!",
"enroll_customer": "Client umellen",
"enrolling": "Umeldung...",
"customer_enrolled": "Client ugemellt!",
"starting_balance": "Ufankssolde:",
"x_points": "{count} Punkten",
"back_to_terminal": "Zréck zum Terminal",
"enroll_another": "Weider umellen",
"enrollment_failed": "Umeldung feelgeschloen: {message}"
},
"analytics": {
"title": "Treie-Analytik",
"subtitle": "Verfollegt d'Performance vun Ärem Treieprogramm",
"loading": "Analytik gëtt gelueden...",
"error_loading": "Feeler beim Luede vun der Analytik",
"quick_actions": "Schnellaktiounen",
"open_terminal": "Terminal opmaachen",
"view_members": "Membere kucken",
"view_program": "Programm kucken"
},
"program": {
"title": "Treieprogramm",
"subtitle": "Är Treieprogramm-Konfiguratioun",
"edit_program": "Programm beaarbechten",
"loading": "Programm gëtt gelueden...",
"error_loading": "Feeler beim Luede vum Programm",
"no_program": "Keen Treieprogramm",
"no_program_desc": "Ären Händler huet nach keen Treieprogramm konfiguréiert.",
"create_program": "Programm erstellen",
"contact_admin": "Kontaktéiert Ären Administrateur fir en Treieprogramm anzerichten."
},
"settings": {
"title": "Treie-Astellungen",
"page_title": "Treieprogramm-Astellungen",
"subtitle": "Konfiguréiert Ärt Treieprogramm",
"back_to_program": "Zréck zum Programm",
"loading": "Astellunge gi gelueden...",
"error_loading": "Feeler beim Luede vun den Astellungen",
"access_restricted": "Zougang ageschränkt",
"owner_only": "Nëmmen den Geschäftseigentümer kann d'Treieprogramm-Astellungen verwalten.",
"delete_title": "Treieprogramm läschen",
"delete_message": "D'Treieprogramm an all verbonnen Daten (Kaarten, Transaktiounen, Belounungen) ginn dauerhaft geläscht. Dëst kann net réckgängeg gemaach ginn.",
"delete_confirm": "Programm läschen",
"program_created": "Programm erfollegräich erstellt",
"program_updated": "Programm erfollegräich aktualiséiert",
"program_deleted": "Treieprogramm geläscht",
"save_failed": "Späichere feelgeschloen: {message}",
"delete_failed": "Läsche feelgeschloen: {message}"
}
},
"storefront": {
"dashboard": {
"back_to_account": "Zréck zum Kont",
"my_loyalty": "Meng Treie",
"join_title": "Gitt Member vun eisem Belounungsprogramm!",
"join_subtitle": "Sammelt Punkten bei all Akaf an léist se géint Belounungen an.",
"join_now": "Elo bäitrieden",
"points_balance": "Punktestand",
"card_number": "Kaartnummer",
"show_card": "Kaart weisen",
"total_earned": "Insgesamt verdéngt",
"total_redeemed": "Insgesamt agelées",
"available_rewards": "Verfügbar Belounungen",
"no_rewards_yet": "Nach keng Belounungen verfügbar",
"ready_to_redeem": "Prett fir anzeléisen",
"x_more_to_go": "Nach {count}",
"redeem_hint": "Weist Är Kaart dem Personal fir Belounungen am Geschäft anzeléisen.",
"recent_activity": "Rezent Aktivitéit",
"view_all": "Alles kucken",
"no_transactions": "Nach keng Transaktiounen. Maacht en Akaf fir Punkten ze sammelen!",
"earn_redeem_locations": "Sammel- & Aléisungs-Standuerter",
"your_loyalty_card": "Är Treiekaart",
"show_to_staff": "Weist dëst dem Personal beim Akaf oder beim Aléise vu Belounungen."
},
"history": {
"back_to_loyalty": "Zréck zur Treie",
"title": "Transaktiounsverlaf",
"subtitle": "All Är Treiepunkttransaktiounen kucken",
"current_balance": "Aktuell Solde",
"total_earned": "Insgesamt verdéngt",
"total_redeemed": "Insgesamt agelées",
"no_transactions": "Nach keng Transaktiounen",
"balance": "Solde:",
"previous": "Zréck",
"next": "Weider",
"page_x_of_y": "Säit {page} vun {pages}"
}
},
"toasts": {
"program_activated": "Programm erfollegräich aktivéiert",
"program_deactivated": "Programm erfollegräich deaktivéiert",
"activate_failed": "Programm konnt net aktivéiert ginn: {message}",
"deactivate_failed": "Programm konnt net deaktivéiert ginn: {message}",
"program_deleted": "Programm erfollegräich geläscht",
"delete_failed": "Programm konnt net geläscht ginn: {message}",
"program_created": "Programm erfollegräich erstellt",
"program_updated": "Programm erfollegräich aktualiséiert",
"loyalty_program_created": "Treieprogramm erstellt",
"loyalty_program_deleted": "Treieprogramm geläscht",
"settings_saved": "Astellungen erfollegräich gespäichert",
"save_failed": "Späichere feelgeschloen: {message}",
"settings_save_failed": "Astellunge konnten net gespäichert ginn: {message}",
"create_failed": "Programm konnt net erstellt ginn: {message}",
"logo_required": "Logo-URL ass erfuerderlech fir d'Wallet-Integratioun."
}
}

View File

@@ -190,12 +190,12 @@ function adminLoyaltyMerchantDetail() {
};
const response = await apiClient.post(`/admin/loyalty/merchants/${this.merchantId}/program`, data);
this.program = response;
Utils.showToast('Loyalty program created', 'success');
Utils.showToast(I18n.t('loyalty.toasts.program_created'), 'success');
loyaltyMerchantDetailLog.info('Program created for merchant', this.merchantId);
// Reload stats
await this.loadStats();
} catch (error) {
Utils.showToast(`Failed to create program: ${error.message}`, 'error');
Utils.showToast(I18n.t('loyalty.toasts.create_failed', {message: error.message}), 'error');
loyaltyMerchantDetailLog.error('Failed to create program:', error);
}
},
@@ -207,10 +207,10 @@ function adminLoyaltyMerchantDetail() {
try {
const response = await apiClient.post(`/admin/loyalty/programs/${this.program.id}/${action}`);
this.program.is_active = response.is_active;
Utils.showToast(`Program ${action}d successfully`, 'success');
Utils.showToast(I18n.t(`loyalty.toasts.program_${action}d`), 'success');
loyaltyMerchantDetailLog.info(`Program ${action}d`);
} catch (error) {
Utils.showToast(`Failed to ${action} program: ${error.message}`, 'error');
Utils.showToast(I18n.t(`loyalty.toasts.${action}_failed`, {message: error.message}), 'error');
loyaltyMerchantDetailLog.error(`Failed to ${action} program:`, error);
}
},
@@ -227,12 +227,12 @@ function adminLoyaltyMerchantDetail() {
await apiClient.delete(`/admin/loyalty/programs/${this.program.id}`);
this.program = null;
this.showDeleteModal = false;
Utils.showToast('Loyalty program deleted', 'success');
Utils.showToast(I18n.t('loyalty.toasts.program_deleted'), 'success');
loyaltyMerchantDetailLog.info('Program deleted');
// Reload stats
await this.loadStats();
} catch (error) {
Utils.showToast(`Failed to delete program: ${error.message}`, 'error');
Utils.showToast(I18n.t('loyalty.toasts.delete_failed', {message: error.message}), 'error');
loyaltyMerchantDetailLog.error('Failed to delete program:', error);
this.showDeleteModal = false;
}

View File

@@ -150,14 +150,14 @@ function adminLoyaltyMerchantSettings() {
if (response) {
loyaltyMerchantSettingsLog.info('Settings saved successfully');
Utils.showToast('Settings saved successfully', 'success');
Utils.showToast(I18n.t('loyalty.toasts.settings_saved'), 'success');
// Navigate back to merchant detail
window.location.href = this.backUrl;
}
} catch (error) {
loyaltyMerchantSettingsLog.error('Failed to save settings:', error);
Utils.showToast(`Failed to save settings: ${error.message}`, 'error');
Utils.showToast(I18n.t('loyalty.toasts.settings_save_failed', {message: error.message}), 'error');
} finally {
this.saving = false;
}

View File

@@ -109,19 +109,19 @@ function adminLoyaltyProgramEdit() {
);
this.programId = response.id;
this.isNewProgram = false;
Utils.showToast('Program created successfully', 'success');
Utils.showToast(I18n.t('loyalty.toasts.program_created'), 'success');
} else {
await apiClient.patch(
`/admin/loyalty/programs/${this.programId}`,
payload
);
Utils.showToast('Program updated successfully', 'success');
Utils.showToast(I18n.t('loyalty.toasts.program_updated'), 'success');
}
loyaltyProgramEditLog.info('Program saved');
window.location.href = this.backUrl;
} catch (error) {
Utils.showToast(`Failed to save: ${error.message}`, 'error');
Utils.showToast(I18n.t('loyalty.toasts.save_failed', {message: error.message}), 'error');
loyaltyProgramEditLog.error('Save failed:', error);
} finally {
this.saving = false;
@@ -134,11 +134,11 @@ function adminLoyaltyProgramEdit() {
try {
await apiClient.delete(`/admin/loyalty/programs/${this.programId}`);
Utils.showToast('Program deleted', 'success');
Utils.showToast(I18n.t('loyalty.toasts.program_deleted'), 'success');
loyaltyProgramEditLog.info('Program deleted');
window.location.href = this.backUrl;
} catch (error) {
Utils.showToast(`Failed to delete: ${error.message}`, 'error');
Utils.showToast(I18n.t('loyalty.toasts.delete_failed', {message: error.message}), 'error');
loyaltyProgramEditLog.error('Delete failed:', error);
} finally {
this.deleting = false;

View File

@@ -249,10 +249,10 @@ function adminLoyaltyPrograms() {
try {
const response = await apiClient.post(`/admin/loyalty/programs/${program.id}/${action}`);
program.is_active = response.is_active;
Utils.showToast(`Program ${action}d successfully`, 'success');
Utils.showToast(I18n.t(`loyalty.toasts.program_${action}d`), 'success');
loyaltyProgramsLog.info(`Program ${program.id} ${action}d`);
} catch (error) {
Utils.showToast(`Failed to ${action} program: ${error.message}`, 'error');
Utils.showToast(I18n.t(`loyalty.toasts.${action}_failed`, {message: error.message}), 'error');
loyaltyProgramsLog.error(`Failed to ${action} program:`, error);
}
},
@@ -267,13 +267,13 @@ function adminLoyaltyPrograms() {
if (!this.deletingProgram) return;
try {
await apiClient.delete(`/admin/loyalty/programs/${this.deletingProgram.id}`);
Utils.showToast('Program deleted successfully', 'success');
Utils.showToast(I18n.t('loyalty.toasts.program_deleted'), 'success');
loyaltyProgramsLog.info('Program deleted:', this.deletingProgram.id);
this.showDeleteModal = false;
this.deletingProgram = null;
await Promise.all([this.loadPrograms(), this.loadStats()]);
} catch (error) {
Utils.showToast(`Failed to delete program: ${error.message}`, 'error');
Utils.showToast(I18n.t('loyalty.toasts.delete_failed', {message: error.message}), 'error');
loyaltyProgramsLog.error('Failed to delete program:', error);
this.showDeleteModal = false;
this.deletingProgram = null;

View File

@@ -61,10 +61,10 @@ function merchantLoyaltySettings() {
await apiClient.patch('/merchants/loyalty/program', payload);
}
Utils.showToast('Settings saved successfully', 'success');
Utils.showToast(I18n.t('loyalty.toasts.settings_saved'), 'success');
loyaltySettingsLog.info('Settings saved');
} catch (error) {
Utils.showToast(`Failed to save: ${error.message}`, 'error');
Utils.showToast(I18n.t('loyalty.toasts.save_failed', {message: error.message}), 'error');
loyaltySettingsLog.error('Save failed:', error);
} finally {
this.saving = false;
@@ -76,11 +76,11 @@ function merchantLoyaltySettings() {
try {
await apiClient.delete('/merchants/loyalty/program');
Utils.showToast('Loyalty program deleted', 'success');
Utils.showToast(I18n.t('loyalty.toasts.program_deleted'), 'success');
loyaltySettingsLog.info('Program deleted');
window.location.href = '/merchants/loyalty/program';
} catch (error) {
Utils.showToast(`Failed to delete: ${error.message}`, 'error');
Utils.showToast(I18n.t('loyalty.toasts.delete_failed', {message: error.message}), 'error');
loyaltySettingsLog.error('Delete failed:', error);
} finally {
this.deleting = false;

View File

@@ -99,7 +99,7 @@ function createProgramFormMixin() {
if (!payload.card_name) payload.card_name = null;
if (!payload.card_secondary_color) payload.card_secondary_color = null;
if (!payload.logo_url) {
this.error = 'Logo URL is required for wallet integration.';
this.error = I18n.t('loyalty.toasts.logo_required');
return null;
}
if (!payload.hero_image_url) payload.hero_image_url = null;

View File

@@ -80,7 +80,7 @@ function storeLoyaltyEnroll() {
loyaltyEnrollLog.info('Customer enrolled successfully:', response.card_number);
}
} catch (error) {
Utils.showToast(`Enrollment failed: ${error.message}`, 'error');
Utils.showToast(I18n.t('loyalty.store.enroll.enrollment_failed', {message: error.message}), 'error');
loyaltyEnrollLog.error('Enrollment failed:', error);
} finally {
this.enrolling = false;

View File

@@ -87,10 +87,10 @@ function loyaltySettings() {
let response;
if (this.isNewProgram) {
response = await apiClient.post('/store/loyalty/program', payload);
Utils.showToast('Program created successfully', 'success');
Utils.showToast(I18n.t('loyalty.store.settings.program_created'), 'success');
} else {
response = await apiClient.put('/store/loyalty/program', payload);
Utils.showToast('Program updated successfully', 'success');
Utils.showToast(I18n.t('loyalty.store.settings.program_updated'), 'success');
}
this.populateSettings(response);
@@ -98,7 +98,7 @@ function loyaltySettings() {
loyaltySettingsLog.info('Program saved:', response.display_name);
} catch (error) {
Utils.showToast(`Failed to save: ${error.message}`, 'error');
Utils.showToast(I18n.t('loyalty.store.settings.save_failed', {message: error.message}), 'error');
loyaltySettingsLog.error('Save failed:', error);
} finally {
this.saving = false;
@@ -110,13 +110,13 @@ function loyaltySettings() {
try {
await apiClient.delete('/store/loyalty/program');
Utils.showToast('Loyalty program deleted', 'success');
Utils.showToast(I18n.t('loyalty.store.settings.program_deleted'), 'success');
loyaltySettingsLog.info('Program deleted');
// Redirect to terminal page
const storeCode = window.location.pathname.split('/')[2];
window.location.href = `/store/${storeCode}/loyalty/program`;
} catch (error) {
Utils.showToast(`Failed to delete: ${error.message}`, 'error');
Utils.showToast(I18n.t('loyalty.store.settings.delete_failed', {message: error.message}), 'error');
loyaltySettingsLog.error('Delete failed:', error);
} finally {
this.deleting = false;

View File

@@ -136,9 +136,9 @@ function storeLoyaltyTerminal() {
}
} catch (error) {
if (error.status === 404) {
Utils.showToast('Customer not found. You can enroll them as a new member.', 'warning');
Utils.showToast(I18n.t('loyalty.store.terminal.customer_not_found'), 'warning');
} else {
Utils.showToast(`Error looking up customer: ${error.message}`, 'error');
Utils.showToast(I18n.t('loyalty.store.terminal.error_lookup', {message: error.message}), 'error');
}
loyaltyTerminalLog.error('Lookup failed:', error);
} finally {
@@ -213,7 +213,7 @@ function storeLoyaltyTerminal() {
await this.loadRecentTransactions();
} catch (error) {
Utils.showToast(`Transaction failed: ${error.message}`, 'error');
Utils.showToast(I18n.t('loyalty.store.terminal.transaction_failed', {message: error.message}), 'error');
loyaltyTerminalLog.error('Transaction failed:', error);
} finally {
this.processing = false;
@@ -229,7 +229,7 @@ function storeLoyaltyTerminal() {
staff_pin: this.pinDigits
});
Utils.showToast('Stamp added!', 'success');
Utils.showToast(I18n.t('loyalty.store.terminal.stamp_added'), 'success');
},
// Redeem stamps
@@ -241,7 +241,7 @@ function storeLoyaltyTerminal() {
staff_pin: this.pinDigits
});
Utils.showToast('Stamps redeemed! Reward earned.', 'success');
Utils.showToast(I18n.t('loyalty.store.terminal.stamps_redeemed'), 'success');
},
// Earn points
@@ -255,7 +255,7 @@ function storeLoyaltyTerminal() {
});
const pointsEarned = response.points_earned || Math.floor(this.earnAmount * (this.program?.points_per_euro || 1));
Utils.showToast(`${pointsEarned} points awarded!`, 'success');
Utils.showToast(I18n.t('loyalty.store.terminal.x_points_awarded', {points: pointsEarned}), 'success');
this.earnAmount = null;
},
@@ -273,7 +273,7 @@ function storeLoyaltyTerminal() {
staff_pin: this.pinDigits
});
Utils.showToast(`Reward redeemed: ${reward.name}`, 'success');
Utils.showToast(I18n.t('loyalty.store.terminal.reward_redeemed', {name: reward.name}), 'success');
this.selectedReward = '';
},
@@ -292,21 +292,11 @@ function storeLoyaltyTerminal() {
// Format number
getTransactionLabel(tx) {
const labels = {
'card_created': 'Enrolled',
'welcome_bonus': 'Welcome Bonus',
'stamp_earned': 'Stamp Earned',
'stamp_redeemed': 'Stamp Redeemed',
'stamp_voided': 'Stamp Voided',
'stamp_adjustment': 'Stamp Adjusted',
'points_earned': 'Points Earned',
'points_redeemed': 'Points Redeemed',
'points_voided': 'Points Voided',
'points_adjustment': 'Points Adjusted',
'points_expired': 'Points Expired',
'card_deactivated': 'Deactivated',
};
return labels[tx.transaction_type] || tx.transaction_type?.replace(/_/g, ' ') || 'Unknown';
const type = tx.transaction_type;
if (type) {
return I18n.t('loyalty.transactions.' + type, {defaultValue: type.replace(/_/g, ' ')});
}
return I18n.t('loyalty.common.unknown');
},
getTransactionColor(tx) {

View File

@@ -46,7 +46,7 @@ function customerLoyaltyEnroll() {
this.program = null;
} else {
console.error('Failed to load program:', error);
this.error = 'Failed to load program information';
this.error = I18n.t('loyalty.enrollment.errors.load_failed');
}
} finally {
this.loading = false;
@@ -89,9 +89,9 @@ function customerLoyaltyEnroll() {
} catch (error) {
console.error('Enrollment failed:', error);
if (error.message?.includes('already')) {
this.error = 'This email is already registered in our loyalty program.';
this.error = I18n.t('loyalty.enrollment.errors.email_exists');
} else {
this.error = error.message || 'Enrollment failed. Please try again.';
this.error = error.message || I18n.t('loyalty.enrollment.errors.failed');
}
} finally {
this.enrolling = false;

View File

@@ -84,16 +84,10 @@ function customerLoyaltyHistory() {
getTransactionLabel(tx) {
const type = tx.transaction_type || '';
const labels = {
'points_earned': 'Points Earned',
'points_redeemed': 'Reward Redeemed',
'points_voided': 'Points Voided',
'welcome_bonus': 'Welcome Bonus',
'points_expired': 'Points Expired',
'stamp_earned': 'Stamp Earned',
'stamp_redeemed': 'Stamp Redeemed'
};
return labels[type] || type.replace(/_/g, ' ');
if (type) {
return I18n.t('loyalty.transactions.' + type, {defaultValue: type.replace(/_/g, ' ')});
}
return type;
},
formatNumber(num) {

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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>

View File

@@ -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 %}

View File

@@ -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>

View File

@@ -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="'&euro;' + ((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">

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 %}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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>