diff --git a/app/modules/loyalty/definition.py b/app/modules/loyalty/definition.py index d7a6a742..92fcd884 100644 --- a/app/modules/loyalty/definition.py +++ b/app/modules/loyalty/definition.py @@ -131,11 +131,12 @@ loyalty_module = ModuleDefinition( FrontendType.STORE: [ "terminal", # Loyalty terminal "cards", # Customer cards - "stats", # Store stats + "loyalty-program", # Program config + "loyalty-analytics", # Store analytics ], FrontendType.MERCHANT: [ - "loyalty-overview", # Merchant loyalty overview - "loyalty-settings", # Merchant loyalty settings + "loyalty-program", # Merchant loyalty program + "loyalty-analytics", # Merchant loyalty analytics ], }, # New module-driven menu definitions @@ -188,10 +189,18 @@ loyalty_module = ModuleDefinition( requires_permission="loyalty.view_programs", ), MenuItemDefinition( - id="stats", - label_key="loyalty.menu.statistics", + id="loyalty-program", + label_key="loyalty.menu.program", + icon="cog", + route="/store/{store_code}/loyalty/program", + order=25, + requires_permission="loyalty.view_programs", + ), + MenuItemDefinition( + id="loyalty-analytics", + label_key="loyalty.menu.analytics", icon="chart-bar", - route="/store/{store_code}/loyalty/stats", + route="/store/{store_code}/loyalty/analytics", order=30, requires_permission="loyalty.view_programs", ), @@ -206,17 +215,17 @@ loyalty_module = ModuleDefinition( order=60, items=[ MenuItemDefinition( - id="loyalty-overview", - label_key="loyalty.menu.overview", + id="loyalty-program", + label_key="loyalty.menu.program", icon="gift", - route="/merchants/loyalty/overview", + route="/merchants/loyalty/program", order=10, ), MenuItemDefinition( - id="loyalty-settings", - label_key="loyalty.menu.settings", - icon="cog", - route="/merchants/loyalty/settings", + id="loyalty-analytics", + label_key="loyalty.menu.analytics", + icon="chart-bar", + route="/merchants/loyalty/analytics", order=20, ), ], diff --git a/app/modules/loyalty/locales/en.json b/app/modules/loyalty/locales/en.json index 4ee32e3c..8f5d1c9c 100644 --- a/app/modules/loyalty/locales/en.json +++ b/app/modules/loyalty/locales/en.json @@ -88,6 +88,7 @@ "terminal": "Terminal", "customer_cards": "Customer Cards", "statistics": "Statistics", + "program": "Program", "overview": "Overview", "settings": "Settings" }, diff --git a/app/modules/loyalty/locales/fr.json b/app/modules/loyalty/locales/fr.json index bc78e580..71896325 100644 --- a/app/modules/loyalty/locales/fr.json +++ b/app/modules/loyalty/locales/fr.json @@ -78,6 +78,7 @@ "terminal": "Terminal", "customer_cards": "Cartes clients", "statistics": "Statistiques", + "program": "Programme", "overview": "Aperçu", "settings": "Paramètres" }, diff --git a/app/modules/loyalty/routes/pages/merchant.py b/app/modules/loyalty/routes/pages/merchant.py index 9bc6a5b4..9737c3b0 100644 --- a/app/modules/loyalty/routes/pages/merchant.py +++ b/app/modules/loyalty/routes/pages/merchant.py @@ -3,7 +3,9 @@ Loyalty Merchant Page Routes (HTML rendering). Merchant portal pages for: -- Loyalty overview (aggregate stats across all stores) +- Loyalty program (read-only view) +- Loyalty program edit +- Loyalty analytics (aggregate stats across all stores) Authentication: merchant_token cookie or Authorization header. @@ -60,23 +62,22 @@ def _get_merchant_context( # ============================================================================ -# LOYALTY OVERVIEW +# LOYALTY PROGRAM (Read-only view) # ============================================================================ -@router.get("/overview", response_class=HTMLResponse, include_in_schema=False) -async def merchant_loyalty_overview( +@router.get("/program", response_class=HTMLResponse, include_in_schema=False) +async def merchant_loyalty_program( request: Request, current_user: UserContext = Depends(get_current_merchant_from_cookie_or_header), merchant: Merchant = Depends(get_merchant_for_current_user_page), db: Session = Depends(get_db), ): """ - Render merchant loyalty overview page. + Render merchant loyalty program view page. - Shows aggregate loyalty program stats across all merchant stores. + Shows read-only program configuration with link to edit. """ - # Get merchant stats server-side merchant_id = merchant.id stats = {} try: @@ -95,25 +96,25 @@ async def merchant_loyalty_overview( merchant_id=merchant_id, ) return templates.TemplateResponse( - "loyalty/merchant/overview.html", + "loyalty/merchant/program.html", context, ) # ============================================================================ -# LOYALTY SETTINGS +# LOYALTY PROGRAM EDIT # ============================================================================ -@router.get("/settings", response_class=HTMLResponse, include_in_schema=False) -async def merchant_loyalty_settings( +@router.get("/program/edit", response_class=HTMLResponse, include_in_schema=False) +async def merchant_loyalty_program_edit( request: Request, current_user: UserContext = Depends(get_current_merchant_from_cookie_or_header), merchant: Merchant = Depends(get_merchant_for_current_user_page), db: Session = Depends(get_db), ): """ - Render merchant loyalty settings page. + Render merchant loyalty program edit page. Allows merchant to create, configure, and manage their loyalty program. """ @@ -124,6 +125,46 @@ async def merchant_loyalty_settings( merchant_id=merchant.id, ) return templates.TemplateResponse( - "loyalty/merchant/settings.html", + "loyalty/merchant/program-edit.html", + context, + ) + + +# ============================================================================ +# LOYALTY ANALYTICS +# ============================================================================ + + +@router.get("/analytics", response_class=HTMLResponse, include_in_schema=False) +async def merchant_loyalty_analytics( + request: Request, + current_user: UserContext = Depends(get_current_merchant_from_cookie_or_header), + merchant: Merchant = Depends(get_merchant_for_current_user_page), + db: Session = Depends(get_db), +): + """ + Render merchant loyalty analytics page. + + Shows aggregate loyalty program stats across all merchant stores. + """ + merchant_id = merchant.id + stats = {} + try: + stats = program_service.get_merchant_stats(db, merchant_id) + except Exception: + logger.warning( + f"Failed to load loyalty stats for merchant {merchant_id}", + exc_info=True, + ) + + context = _get_merchant_context( + request, + db, + current_user, + loyalty_stats=stats, + merchant_id=merchant_id, + ) + return templates.TemplateResponse( + "loyalty/merchant/analytics.html", context, ) diff --git a/app/modules/loyalty/routes/pages/store.py b/app/modules/loyalty/routes/pages/store.py index 6c62a779..62a3f0f1 100644 --- a/app/modules/loyalty/routes/pages/store.py +++ b/app/modules/loyalty/routes/pages/store.py @@ -5,8 +5,9 @@ Loyalty Store Page Routes (HTML rendering). Store pages for: - Loyalty terminal (primary daily interface for staff) - Loyalty members management -- Program settings -- Stats dashboard +- Program view (read-only) +- Program edit (settings) +- Analytics dashboard Routes follow the standard store convention: /loyalty/... so they match the menu URLs in definition.py. @@ -183,49 +184,49 @@ async def store_loyalty_card_detail( # ============================================================================ -# STATS DASHBOARD +# PROGRAM VIEW (Read-only) # ============================================================================ @router.get( - "/loyalty/stats", + "/loyalty/program", response_class=HTMLResponse, include_in_schema=False, ) -async def store_loyalty_stats( +async def store_loyalty_program( request: Request, store_code: str = Depends(get_resolved_store_code), current_user: User = Depends(get_current_store_from_cookie_or_header), db: Session = Depends(get_db), ): """ - Render loyalty statistics dashboard. - Shows store's loyalty program metrics and trends. + Render loyalty program view page (read-only). + Shows program configuration. Edit button shown to merchant owners only. """ return templates.TemplateResponse( - "loyalty/store/stats.html", + "loyalty/store/program.html", get_store_context(request, db, current_user, store_code), ) # ============================================================================ -# SETTINGS (Merchant Owner) +# PROGRAM EDIT (Merchant Owner) # ============================================================================ @router.get( - "/loyalty/settings", + "/loyalty/program/edit", response_class=HTMLResponse, include_in_schema=False, ) -async def store_loyalty_settings( +async def store_loyalty_program_edit( request: Request, store_code: str = Depends(get_resolved_store_code), current_user: User = Depends(get_current_store_from_cookie_or_header), db: Session = Depends(get_db), ): """ - Render loyalty program settings page. + Render loyalty program edit page. Allows merchant owners to create or edit their loyalty program. """ return templates.TemplateResponse( @@ -234,6 +235,32 @@ async def store_loyalty_settings( ) +# ============================================================================ +# ANALYTICS DASHBOARD +# ============================================================================ + + +@router.get( + "/loyalty/analytics", + response_class=HTMLResponse, + include_in_schema=False, +) +async def store_loyalty_analytics( + request: Request, + store_code: str = Depends(get_resolved_store_code), + current_user: User = Depends(get_current_store_from_cookie_or_header), + db: Session = Depends(get_db), +): + """ + Render loyalty analytics dashboard. + Shows store's loyalty program metrics and trends. + """ + return templates.TemplateResponse( + "loyalty/store/analytics.html", + get_store_context(request, db, current_user, store_code), + ) + + # ============================================================================ # ENROLLMENT # ============================================================================ diff --git a/app/modules/loyalty/services/program_service.py b/app/modules/loyalty/services/program_service.py index 145efc2f..9ff29386 100644 --- a/app/modules/loyalty/services/program_service.py +++ b/app/modules/loyalty/services/program_service.py @@ -826,6 +826,20 @@ class ProgramService: "minimum_redemption_points": program.minimum_redemption_points, "points_expiration_days": program.points_expiration_days, "is_active": program.is_active, + "stamps_target": program.stamps_target, + "stamps_reward_description": program.stamps_reward_description, + "stamps_reward_value_cents": program.stamps_reward_value_cents, + "minimum_purchase_cents": program.minimum_purchase_cents, + "cooldown_minutes": program.cooldown_minutes, + "max_daily_stamps": program.max_daily_stamps, + "require_staff_pin": program.require_staff_pin, + "card_color": program.card_color, + "card_secondary_color": program.card_secondary_color, + "logo_url": program.logo_url, + "hero_image_url": program.hero_image_url, + "terms_text": program.terms_text, + "privacy_url": program.privacy_url, + "points_rewards": program.points_rewards, } thirty_days_ago = datetime.now(UTC) - timedelta(days=30) diff --git a/app/modules/loyalty/static/merchant/js/loyalty-settings.js b/app/modules/loyalty/static/merchant/js/loyalty-settings.js index 1f3727d9..b2e0bc3c 100644 --- a/app/modules/loyalty/static/merchant/js/loyalty-settings.js +++ b/app/modules/loyalty/static/merchant/js/loyalty-settings.js @@ -6,26 +6,11 @@ const loyaltySettingsLog = window.LogConfig.loggers.loyaltySettings || window.Lo function merchantLoyaltySettings() { return { ...data(), - currentPage: 'loyalty-settings', - - settings: { - loyalty_type: 'points', - points_per_euro: 1, - welcome_bonus_points: 0, - minimum_redemption_points: 100, - points_expiration_days: null, - points_rewards: [], - card_name: '', - card_color: '#4F46E5', - is_active: true - }, + ...createProgramFormMixin(), + currentPage: 'loyalty-program', loading: false, - saving: false, - deleting: false, error: null, - isNewProgram: false, - showDeleteModal: false, async init() { loyaltySettingsLog.info('=== MERCHANT LOYALTY SETTINGS PAGE INITIALIZING ==='); @@ -46,17 +31,7 @@ function merchantLoyaltySettings() { try { const response = await apiClient.get('/merchants/loyalty/program'); if (response) { - this.settings = { - loyalty_type: response.loyalty_type || 'points', - points_per_euro: response.points_per_euro || 1, - welcome_bonus_points: response.welcome_bonus_points || 0, - minimum_redemption_points: response.minimum_redemption_points || 100, - points_expiration_days: response.points_expiration_days || null, - points_rewards: response.points_rewards || [], - card_name: response.card_name || '', - card_color: response.card_color || '#4F46E5', - is_active: response.is_active !== false - }; + this.populateSettings(response); this.isNewProgram = false; loyaltySettingsLog.info('Settings loaded'); } @@ -76,19 +51,13 @@ function merchantLoyaltySettings() { this.saving = true; try { - // Ensure rewards have IDs - this.settings.points_rewards = this.settings.points_rewards.map((r, i) => ({ - ...r, - id: r.id || `reward_${i + 1}`, - is_active: r.is_active !== false - })); + const payload = this.buildPayload(); - let response; if (this.isNewProgram) { - response = await apiClient.post('/merchants/loyalty/program', this.settings); + await apiClient.post('/merchants/loyalty/program', payload); this.isNewProgram = false; } else { - response = await apiClient.patch('/merchants/loyalty/program', this.settings); + await apiClient.patch('/merchants/loyalty/program', payload); } Utils.showToast('Settings saved successfully', 'success'); @@ -101,10 +70,6 @@ function merchantLoyaltySettings() { } }, - confirmDelete() { - this.showDeleteModal = true; - }, - async deleteProgram() { this.deleting = true; @@ -112,8 +77,7 @@ function merchantLoyaltySettings() { await apiClient.delete('/merchants/loyalty/program'); Utils.showToast('Loyalty program deleted', 'success'); loyaltySettingsLog.info('Program deleted'); - // Redirect to overview - window.location.href = '/merchants/loyalty/overview'; + window.location.href = '/merchants/loyalty/program'; } catch (error) { Utils.showToast(`Failed to delete: ${error.message}`, 'error'); loyaltySettingsLog.error('Delete failed:', error); @@ -122,20 +86,6 @@ function merchantLoyaltySettings() { this.showDeleteModal = false; } }, - - addReward() { - this.settings.points_rewards.push({ - id: `reward_${Date.now()}`, - name: '', - points_required: 100, - description: '', - is_active: true - }); - }, - - removeReward(index) { - this.settings.points_rewards.splice(index, 1); - } }; } diff --git a/app/modules/loyalty/static/store/js/loyalty-stats.js b/app/modules/loyalty/static/store/js/loyalty-analytics.js similarity index 73% rename from app/modules/loyalty/static/store/js/loyalty-stats.js rename to app/modules/loyalty/static/store/js/loyalty-analytics.js index b4848c02..9fb94444 100644 --- a/app/modules/loyalty/static/store/js/loyalty-stats.js +++ b/app/modules/loyalty/static/store/js/loyalty-analytics.js @@ -1,12 +1,12 @@ -// app/modules/loyalty/static/store/js/loyalty-stats.js +// app/modules/loyalty/static/store/js/loyalty-analytics.js // noqa: js-006 - async init pattern is safe, loadData has try/catch -const loyaltyStatsLog = window.LogConfig.loggers.loyaltyStats || window.LogConfig.createLogger('loyaltyStats'); +const loyaltyAnalyticsLog = window.LogConfig.loggers.loyaltyAnalytics || window.LogConfig.createLogger('loyaltyAnalytics'); -function storeLoyaltyStats() { +function storeLoyaltyAnalytics() { return { ...data(), - currentPage: 'loyalty-stats', + currentPage: 'loyalty-analytics', program: null, @@ -27,9 +27,9 @@ function storeLoyaltyStats() { error: null, async init() { - loyaltyStatsLog.info('=== LOYALTY STATS PAGE INITIALIZING ==='); - if (window._loyaltyStatsInitialized) return; - window._loyaltyStatsInitialized = true; + loyaltyAnalyticsLog.info('=== LOYALTY ANALYTICS PAGE INITIALIZING ==='); + if (window._loyaltyAnalyticsInitialized) return; + window._loyaltyAnalyticsInitialized = true; // IMPORTANT: Call parent init first to set storeCode from URL const parentInit = data().init; @@ -41,7 +41,7 @@ function storeLoyaltyStats() { if (this.program) { await this.loadStats(); } - loyaltyStatsLog.info('=== LOYALTY STATS PAGE INITIALIZATION COMPLETE ==='); + loyaltyAnalyticsLog.info('=== LOYALTY ANALYTICS PAGE INITIALIZATION COMPLETE ==='); }, async loadProgram() { @@ -72,10 +72,10 @@ function storeLoyaltyStats() { transactions_30d: response.transactions_30d || 0, avg_points_per_member: response.avg_points_per_member || 0 }; - loyaltyStatsLog.info('Stats loaded'); + loyaltyAnalyticsLog.info('Stats loaded'); } } catch (error) { - loyaltyStatsLog.error('Failed to load stats:', error); + loyaltyAnalyticsLog.error('Failed to load stats:', error); this.error = error.message; } finally { this.loading = false; @@ -88,7 +88,7 @@ function storeLoyaltyStats() { }; } -if (!window.LogConfig.loggers.loyaltyStats) { - window.LogConfig.loggers.loyaltyStats = window.LogConfig.createLogger('loyaltyStats'); +if (!window.LogConfig.loggers.loyaltyAnalytics) { + window.LogConfig.loggers.loyaltyAnalytics = window.LogConfig.createLogger('loyaltyAnalytics'); } -loyaltyStatsLog.info('Loyalty stats module loaded'); +loyaltyAnalyticsLog.info('Loyalty analytics module loaded'); diff --git a/app/modules/loyalty/static/store/js/loyalty-settings.js b/app/modules/loyalty/static/store/js/loyalty-settings.js index 3ebf4a85..3cd6efa9 100644 --- a/app/modules/loyalty/static/store/js/loyalty-settings.js +++ b/app/modules/loyalty/static/store/js/loyalty-settings.js @@ -10,38 +10,16 @@ function loyaltySettings() { return { // Inherit base layout functionality ...data(), + ...createProgramFormMixin(), // Page identifier - currentPage: 'loyalty-settings', + currentPage: 'loyalty-program', // State - program: null, loading: false, - saving: false, error: null, isOwner: false, - // Form data - form: { - loyalty_type: 'points', - stamps_target: 10, - stamps_reward_description: 'Free item', - stamps_reward_value_cents: null, - points_per_euro: 10, - welcome_bonus_points: 0, - minimum_redemption_points: 100, - minimum_purchase_cents: 0, - points_expiration_days: null, - points_rewards: [], - cooldown_minutes: 15, - max_daily_stamps: 5, - require_staff_pin: true, - card_name: '', - card_color: '#4F46E5', - logo_url: '', - terms_text: '', - }, - // Initialize async init() { loyaltySettingsLog.info('=== LOYALTY SETTINGS INITIALIZING ==='); @@ -85,83 +63,37 @@ function loyaltySettings() { const response = await apiClient.get('/store/loyalty/program'); if (response) { - this.program = response; - this.populateForm(response); + this.populateSettings(response); + this.isNewProgram = false; loyaltySettingsLog.info('Program loaded:', response.display_name); } } catch (error) { if (error.status === 404) { loyaltySettingsLog.info('No program configured — showing create form'); - this.program = null; + this.isNewProgram = true; } else { throw error; } } }, - populateForm(program) { - this.form.loyalty_type = program.loyalty_type || 'points'; - this.form.stamps_target = program.stamps_target || 10; - this.form.stamps_reward_description = program.stamps_reward_description || 'Free item'; - this.form.stamps_reward_value_cents = program.stamps_reward_value_cents || null; - this.form.points_per_euro = program.points_per_euro || 10; - this.form.welcome_bonus_points = program.welcome_bonus_points || 0; - this.form.minimum_redemption_points = program.minimum_redemption_points || 100; - this.form.minimum_purchase_cents = program.minimum_purchase_cents || 0; - this.form.points_expiration_days = program.points_expiration_days || null; - this.form.points_rewards = (program.points_rewards || []).map(r => ({ - id: r.id, - name: r.name, - points_required: r.points_required, - description: r.description || '', - is_active: r.is_active !== false, - })); - this.form.cooldown_minutes = program.cooldown_minutes ?? 15; - this.form.max_daily_stamps = program.max_daily_stamps || 5; - this.form.require_staff_pin = program.require_staff_pin !== false; - this.form.card_name = program.card_name || ''; - this.form.card_color = program.card_color || '#4F46E5'; - this.form.logo_url = program.logo_url || ''; - this.form.terms_text = program.terms_text || ''; - }, - - addReward() { - const id = 'reward_' + Date.now(); - this.form.points_rewards.push({ - id: id, - name: '', - points_required: 100, - description: '', - is_active: true, - }); - }, - - async saveProgram() { + async saveSettings() { this.saving = true; try { - const payload = { ...this.form }; - - // Clean up empty optional fields - if (!payload.stamps_reward_value_cents) payload.stamps_reward_value_cents = null; - if (!payload.points_expiration_days) payload.points_expiration_days = null; - if (!payload.card_name) payload.card_name = null; - if (!payload.logo_url) payload.logo_url = null; - if (!payload.terms_text) payload.terms_text = null; + const payload = this.buildPayload(); let response; - if (this.program) { - // Update existing - response = await apiClient.put('/store/loyalty/program', payload); - Utils.showToast('Program updated successfully', 'success'); - } else { - // Create new + if (this.isNewProgram) { response = await apiClient.post('/store/loyalty/program', payload); Utils.showToast('Program created successfully', 'success'); + } else { + response = await apiClient.put('/store/loyalty/program', payload); + Utils.showToast('Program updated successfully', 'success'); } - this.program = response; - this.populateForm(response); + this.populateSettings(response); + this.isNewProgram = false; loyaltySettingsLog.info('Program saved:', response.display_name); } catch (error) { @@ -171,6 +103,25 @@ function loyaltySettings() { this.saving = false; } }, + + async deleteProgram() { + this.deleting = true; + + try { + await apiClient.delete('/store/loyalty/program'); + Utils.showToast('Loyalty 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'); + loyaltySettingsLog.error('Delete failed:', error); + } finally { + this.deleting = false; + this.showDeleteModal = false; + } + }, }; } diff --git a/app/modules/loyalty/templates/loyalty/admin/merchant-detail.html b/app/modules/loyalty/templates/loyalty/admin/merchant-detail.html index a7e83a80..7f1b3191 100644 --- a/app/modules/loyalty/templates/loyalty/admin/merchant-detail.html +++ b/app/modules/loyalty/templates/loyalty/admin/merchant-detail.html @@ -111,58 +111,8 @@ -
-
-

- - Program Configuration -

-
- - -
-
-
-
-

Program Name

-

-

-
-
-

Points Per Euro

-

-

-
-
-

Welcome Bonus

-

-

-
-
-

Minimum Redemption

-

-

-
-
-

Points Expiration

-

-

-
-
-

Status

- - - -
-
-
+ {% set show_edit_button = false %} + {% include "loyalty/shared/program-view.html" %}
diff --git a/app/modules/loyalty/templates/loyalty/merchant/overview.html b/app/modules/loyalty/templates/loyalty/merchant/analytics.html similarity index 89% rename from app/modules/loyalty/templates/loyalty/merchant/overview.html rename to app/modules/loyalty/templates/loyalty/merchant/analytics.html index 3bc99f63..a752d213 100644 --- a/app/modules/loyalty/templates/loyalty/merchant/overview.html +++ b/app/modules/loyalty/templates/loyalty/merchant/analytics.html @@ -1,24 +1,17 @@ -{# app/modules/loyalty/templates/loyalty/merchant/overview.html #} +{# app/modules/loyalty/templates/loyalty/merchant/analytics.html #} {% extends "merchant/base.html" %} -{% block title %}Loyalty Overview{% endblock %} +{% block title %}Loyalty Analytics{% endblock %} {% block content %} -
+
-

Loyalty Overview

+

Loyalty Analytics

Loyalty program statistics across all your stores.

-
@@ -27,9 +20,9 @@

No Loyalty Program

- Your loyalty program hasn't been set up yet. Create one to start rewarding your customers. + Set up a loyalty program to see analytics here.

- Create Program @@ -120,10 +113,16 @@ {% block extra_scripts %} diff --git a/app/modules/loyalty/templates/loyalty/merchant/program-edit.html b/app/modules/loyalty/templates/loyalty/merchant/program-edit.html new file mode 100644 index 00000000..b9543b5f --- /dev/null +++ b/app/modules/loyalty/templates/loyalty/merchant/program-edit.html @@ -0,0 +1,42 @@ +{# app/modules/loyalty/templates/loyalty/merchant/program-edit.html #} +{% extends "merchant/base.html" %} +{% from 'shared/macros/headers.html' import page_header_flex %} +{% from 'shared/macros/alerts.html' import loading_state, error_state %} +{% from 'shared/macros/modals.html' import confirm_modal %} + +{% block title %}Loyalty Settings{% endblock %} + +{% block alpine_data %}merchantLoyaltySettings(){% endblock %} + +{% block content %} +{% call page_header_flex(title='Loyalty Program Settings', subtitle='Configure your loyalty program') %}{% endcall %} + +{{ loading_state('Loading settings...') }} +{{ error_state('Error loading settings') }} + +
+
+ {% set show_delete = true %} + {% set show_status = true %} + {% set cancel_url = '/merchants/loyalty/program' %} + {% include "loyalty/shared/program-form.html" %} +
+
+ + +{{ 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.', + 'deleteProgram()', + 'showDeleteModal', + 'Delete Program', + 'Cancel', + 'danger' +) }} +{% endblock %} + +{% block extra_scripts %} + + +{% endblock %} diff --git a/app/modules/loyalty/templates/loyalty/merchant/program.html b/app/modules/loyalty/templates/loyalty/merchant/program.html new file mode 100644 index 00000000..048cecfd --- /dev/null +++ b/app/modules/loyalty/templates/loyalty/merchant/program.html @@ -0,0 +1,67 @@ +{# app/modules/loyalty/templates/loyalty/merchant/program.html #} +{% extends "merchant/base.html" %} + +{% block title %}Loyalty Program{% endblock %} + +{% block content %} +
+ + +
+
+

Loyalty Program

+

Your loyalty program configuration.

+
+ +
+ + + + + + +
+{% endblock %} + +{% block extra_scripts %} + +{% endblock %} diff --git a/app/modules/loyalty/templates/loyalty/merchant/settings.html b/app/modules/loyalty/templates/loyalty/merchant/settings.html deleted file mode 100644 index 09fae302..00000000 --- a/app/modules/loyalty/templates/loyalty/merchant/settings.html +++ /dev/null @@ -1,189 +0,0 @@ -{# app/modules/loyalty/templates/loyalty/merchant/settings.html #} -{% extends "merchant/base.html" %} -{% from 'shared/macros/headers.html' import page_header_flex %} -{% from 'shared/macros/alerts.html' import loading_state, error_state %} - -{% block title %}Loyalty Settings{% endblock %} - -{% block alpine_data %}merchantLoyaltySettings(){% endblock %} - -{% block content %} -{% call page_header_flex(title='Loyalty Program Settings', subtitle='Configure your loyalty program') %}{% endcall %} - -{{ loading_state('Loading settings...') }} -{{ error_state('Error loading settings') }} - -
-
- -
-

- - Points Configuration -

-
-
- - -

1 EUR = point(s)

-
-
- - -

Bonus points awarded on enrollment

-
-
- - -
-
- - -

Days of inactivity before points expire (0 = never)

-
-
-
- - -
-
-

- - Redemption Rewards -

- -
-
- - -
-
- - -
-

- - Branding -

-
-
- - -
-
- -
- - -
-
-
-
- - -
-

- - Program Status -

- -
- - -
-
- -
- -
-
-
- - -
-
-

Delete Loyalty Program

-

- This will permanently delete your loyalty program and all associated data (cards, transactions, rewards). - This action cannot be undone. -

-
- - -
-
-
-{% endblock %} - -{% block extra_scripts %} - -{% endblock %} diff --git a/app/modules/loyalty/templates/loyalty/shared/program-view.html b/app/modules/loyalty/templates/loyalty/shared/program-view.html new file mode 100644 index 00000000..36cfaf41 --- /dev/null +++ b/app/modules/loyalty/templates/loyalty/shared/program-view.html @@ -0,0 +1,228 @@ +{# app/modules/loyalty/templates/loyalty/shared/program-view.html #} +{# + Read-only program configuration view partial. + Include with: + {% include "loyalty/shared/program-view.html" %} + + Expected Jinja2 variables (set before include): + - edit_url (str) — href for "Edit Program" button + - show_edit_button (bool, default true) — whether to show the edit button + + Expected Alpine.js state on the parent component: + - program.* — full program object (from API or stats.program) +#} + +
+ + + +
+
+

Program Name

+

-

+
+
+

Card Name

+

-

+
+
+ + + + + + + + + + + +
+

+ + Anti-Fraud +

+
+
+

Cooldown

+

-

+
+
+

Max Daily Stamps

+

-

+
+
+

Staff PIN Required

+ + + +
+
+
+ + +
+

+ + Branding +

+
+
+

Primary Color

+
+
+ +
+
+
+

Secondary Color

+
+
+ +
+
+
+

Logo URL

+

-

+
+
+

Hero Image URL

+

-

+
+
+
+ + +
+

+ + Terms & Privacy +

+
+
+

Terms & Conditions

+

-

+
+
+

Privacy Policy URL

+ + +
+
+
+
diff --git a/app/modules/loyalty/templates/loyalty/store/stats.html b/app/modules/loyalty/templates/loyalty/store/analytics.html similarity index 93% rename from app/modules/loyalty/templates/loyalty/store/stats.html rename to app/modules/loyalty/templates/loyalty/store/analytics.html index 84b44a1c..84755f4a 100644 --- a/app/modules/loyalty/templates/loyalty/store/stats.html +++ b/app/modules/loyalty/templates/loyalty/store/analytics.html @@ -1,21 +1,21 @@ -{# app/modules/loyalty/templates/loyalty/store/stats.html #} +{# app/modules/loyalty/templates/loyalty/store/analytics.html #} {% extends "store/base.html" %} {% from 'shared/macros/headers.html' import page_header_flex, refresh_button %} {% from 'shared/macros/alerts.html' import loading_state, error_state %} -{% block title %}Loyalty Stats{% endblock %} +{% block title %}Loyalty Analytics{% endblock %} -{% block alpine_data %}storeLoyaltyStats(){% endblock %} +{% block alpine_data %}storeLoyaltyAnalytics(){% endblock %} {% block content %} -{% call page_header_flex(title='Loyalty Statistics', subtitle='Track your loyalty program performance') %} +{% call page_header_flex(title='Loyalty Analytics', subtitle='Track your loyalty program performance') %}
{{ refresh_button(loading_var='loading', onclick='loadStats()', variant='secondary') }}
{% endcall %} -{{ loading_state('Loading statistics...') }} -{{ error_state('Error loading statistics') }} +{{ loading_state('Loading analytics...') }} +{{ error_state('Error loading analytics') }}
@@ -25,7 +25,7 @@

Loyalty Program Not Set Up

Your merchant doesn't have a loyalty program configured yet.

{% if user.role == 'merchant_owner' %} - Set Up Loyalty Program @@ -139,10 +139,10 @@ View Members - - Settings + Program
@@ -150,5 +150,5 @@ {% endblock %} {% block extra_scripts %} - + {% endblock %} diff --git a/app/modules/loyalty/templates/loyalty/store/cards.html b/app/modules/loyalty/templates/loyalty/store/cards.html index 9b82f3cc..71618886 100644 --- a/app/modules/loyalty/templates/loyalty/store/cards.html +++ b/app/modules/loyalty/templates/loyalty/store/cards.html @@ -34,7 +34,7 @@

Loyalty Program Not Set Up

Your merchant doesn't have a loyalty program configured yet.

{% if user.role == 'merchant_owner' %} - Set Up Loyalty Program diff --git a/app/modules/loyalty/templates/loyalty/store/program.html b/app/modules/loyalty/templates/loyalty/store/program.html new file mode 100644 index 00000000..e06a2b6d --- /dev/null +++ b/app/modules/loyalty/templates/loyalty/store/program.html @@ -0,0 +1,100 @@ +{# app/modules/loyalty/templates/loyalty/store/program.html #} +{% extends "store/base.html" %} +{% 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 alpine_data %}storeLoyaltyProgram(){% endblock %} + +{% block content %} +{% call page_header_flex(title='Loyalty Program', subtitle='Your loyalty program configuration') %} + +{% endcall %} + +{{ loading_state('Loading program...') }} +{{ error_state('Error loading program') }} + + +
+ +

No Loyalty Program

+

+ Your merchant doesn't have a loyalty program configured yet. +

+ {% if user.role == 'merchant_owner' %} + + + Create Program + + {% else %} +

Contact your administrator to set up a loyalty program.

+ {% endif %} +
+ + +
+ {% set edit_url = '/store/' ~ store_code ~ '/loyalty/program/edit' %} + {% if user.role == 'merchant_owner' %} + {% set show_edit_button = true %} + {% else %} + {% set show_edit_button = false %} + {% endif %} + {% include "loyalty/shared/program-view.html" %} +
+{% endblock %} + +{% block extra_scripts %} + +{% endblock %} diff --git a/app/modules/loyalty/templates/loyalty/store/settings.html b/app/modules/loyalty/templates/loyalty/store/settings.html index c20b34c5..81a52df1 100644 --- a/app/modules/loyalty/templates/loyalty/store/settings.html +++ b/app/modules/loyalty/templates/loyalty/store/settings.html @@ -2,6 +2,7 @@ {% extends "store/base.html" %} {% from 'shared/macros/headers.html' import page_header_flex %} {% from 'shared/macros/alerts.html' import loading_state, error_state %} +{% from 'shared/macros/modals.html' import confirm_modal %} {% block title %}Loyalty Settings{% endblock %} @@ -9,12 +10,12 @@ {% block content %} -{% call page_header_flex(title='Loyalty Settings', subtitle='Configure your loyalty program') %} +{% call page_header_flex(title='Loyalty Program Settings', subtitle='Configure your loyalty program') %} {% endcall %} @@ -35,212 +36,29 @@
-
- - -
-
-

- - Program Type -

-
-
-
- - - -
-
-
- - -
-
-

Stamps Configuration

-
-
-
- - -
-
- - -
-
- - -
-
-
- - -
-
-

Points Configuration

-
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
- -
- -
- -
-
- - -
-
-

Anti-Fraud Settings

-
-
-
- - -
-
- - -
-
- -
-
-
- - -
-
-

Branding

-
-
-
- - -
-
- -
- - -
-
-
- - -
-
- - -
-
-
- - -
- - Cancel - - -
+
+
+ {% set show_delete = true %} + {% set show_status = true %} + {% set cancel_url = '/store/' ~ store_code ~ '/loyalty/program' %} + {% include "loyalty/shared/program-form.html" %} +
+ + +{{ 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.', + 'deleteProgram()', + 'showDeleteModal', + 'Delete Program', + 'Cancel', + 'danger' +) }} {% endblock %} {% block extra_scripts %} + {% endblock %} diff --git a/app/modules/loyalty/templates/loyalty/store/terminal.html b/app/modules/loyalty/templates/loyalty/store/terminal.html index e2b2da31..114e027a 100644 --- a/app/modules/loyalty/templates/loyalty/store/terminal.html +++ b/app/modules/loyalty/templates/loyalty/store/terminal.html @@ -17,10 +17,10 @@ Members - - Stats + Analytics
{% endcall %} @@ -37,7 +37,7 @@

Loyalty Program Not Set Up

Your merchant doesn't have a loyalty program configured yet.

{% if user.role == 'merchant_owner' %} - Set Up Loyalty Program