All checks were successful
Adds a `static_v(request, name, path=...)` Jinja helper that appends
?v=<commit-sha> from app.core.build_info, plus a CachedStaticFiles
subclass that serves Cache-Control: public, max-age=31536000, immutable
in production and no-cache in development. Browsers refetch JS/CSS
automatically on every deploy without the user having to hard-reload.
- New: app/core/static_files.py (CachedStaticFiles)
- Updated: app/templates_config.py (static_v helper)
- Updated: main.py (use CachedStaticFiles for *_static mounts)
- Codemod: 143 url_for('*_static', path='*.js'|'*.css') → static_v(...)
across 123 templates. Images/fonts/JSON locales intentionally
unchanged (out of scope).
- Arch rule: FE-024 (warning) flags raw url_for on JS/CSS to prevent
drift. Note: FE-008 was already taken by the number_stepper rule.
- docs/proposals/static-asset-cache-busting.md marked Done.
Closes plan from docs/proposals/static-asset-cache-busting.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
183 lines
12 KiB
HTML
183 lines
12 KiB
HTML
{# app/modules/loyalty/templates/loyalty/admin/merchant-settings.html #}
|
|
{% extends "admin/base.html" %}
|
|
{% from 'shared/macros/alerts.html' import loading_state, error_state %}
|
|
{% from 'shared/macros/headers.html' import detail_page_header %}
|
|
{% from 'shared/macros/forms.html' import form_section, form_actions %}
|
|
|
|
{% 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') %}
|
|
{{ _('loyalty.admin.merchant_settings.admin_controlled') }}
|
|
{% endcall %}
|
|
|
|
{{ loading_state(_('loyalty.admin.merchant_settings.loading')) }}
|
|
|
|
{{ error_state(_('loyalty.admin.merchant_settings.error_loading')) }}
|
|
|
|
<!-- Settings Form -->
|
|
<div x-show="!loading">
|
|
<form @submit.prevent="saveSettings">
|
|
<!-- Staff PIN Policy -->
|
|
<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>
|
|
{{ _('loyalty.admin.merchant_settings.staff_pin_policy') }}
|
|
</h3>
|
|
<p class="mb-4 text-sm text-gray-600 dark:text-gray-400">
|
|
{{ _('loyalty.admin.merchant_settings.staff_pin_description') }}
|
|
</p>
|
|
|
|
<div class="space-y-4">
|
|
<label class="flex items-start p-4 border rounded-lg cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700"
|
|
:class="settings.staff_pin_policy === 'required' ? 'border-purple-500 bg-purple-50 dark:bg-purple-900/20' : 'border-gray-200 dark:border-gray-600'">
|
|
<input type="radio" name="staff_pin_policy" value="required"
|
|
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">{{ _('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>
|
|
|
|
<label class="flex items-start p-4 border rounded-lg cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700"
|
|
:class="settings.staff_pin_policy === 'optional' ? 'border-purple-500 bg-purple-50 dark:bg-purple-900/20' : 'border-gray-200 dark:border-gray-600'">
|
|
<input type="radio" name="staff_pin_policy" value="optional"
|
|
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">{{ _('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>
|
|
|
|
<label class="flex items-start p-4 border rounded-lg cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700"
|
|
:class="settings.staff_pin_policy === 'disabled' ? 'border-purple-500 bg-purple-50 dark:bg-purple-900/20' : 'border-gray-200 dark:border-gray-600'">
|
|
<input type="radio" name="staff_pin_policy" value="disabled"
|
|
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">{{ _('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>
|
|
|
|
<!-- 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">
|
|
{{ _('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">
|
|
{{ _('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">{{ _('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">
|
|
{{ _('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">{{ _('loyalty.admin.merchant_settings.lockout_duration_help') }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Enrollment Settings -->
|
|
<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>
|
|
{{ _('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">{{ _('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"
|
|
class="sr-only peer">
|
|
<div @click="settings.allow_self_enrollment = !settings.allow_self_enrollment"
|
|
class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-purple-300 dark:peer-focus:ring-purple-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 cursor-pointer"
|
|
:class="settings.allow_self_enrollment ? 'bg-purple-600' : ''">
|
|
</div>
|
|
</div>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Transaction Settings -->
|
|
<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>
|
|
{{ _('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">{{ _('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"
|
|
class="sr-only peer">
|
|
<div @click="settings.allow_cross_location_redemption = !settings.allow_cross_location_redemption"
|
|
class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-purple-300 dark:peer-focus:ring-purple-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 cursor-pointer"
|
|
:class="settings.allow_cross_location_redemption ? 'bg-purple-600' : ''">
|
|
</div>
|
|
</div>
|
|
</label>
|
|
|
|
<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">{{ _('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"
|
|
class="sr-only peer">
|
|
<div @click="settings.allow_void_transactions = !settings.allow_void_transactions"
|
|
class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-purple-300 dark:peer-focus:ring-purple-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 cursor-pointer"
|
|
:class="settings.allow_void_transactions ? 'bg-purple-600' : ''">
|
|
</div>
|
|
</div>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Form Actions -->
|
|
<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">
|
|
{{ _('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 ? '{{ _('loyalty.common.saving')|replace("'", "\\'") }}' : '{{ _('loyalty.admin.merchant_settings.save_settings')|replace("'", "\\'") }}'"></span>
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_scripts %}
|
|
<script defer src="{{ static_v(request, 'loyalty_static', path='admin/js/loyalty-merchant-settings.js') }}"></script>
|
|
{% endblock %}
|