Files
orion/app/modules/loyalty/templates/loyalty/admin/merchant-settings.html
Samir Boulahtit f1e7baaa6c
Some checks failed
CI / ruff (push) Successful in 9s
CI / validate (push) Has been cancelled
CI / dependency-scanning (push) Has been cancelled
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / pytest (push) Has started running
feat(loyalty): add dedicated program edit page with full CRUD and tests
Add /admin/loyalty/merchants/{id}/program route for program configuration
with a dedicated Alpine.js edit page supporting create/edit/delete flows.
Restructure programs dashboard with create modal (merchant search +
duplicate detection) and delete confirmation. Rename "Loyalty Settings"
to "Admin Policy" for clearer separation of concerns.

Add integration tests for all admin page routes (12 tests) and program
list search/filter/pagination endpoints (9 tests).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 23:25:22 +01:00

181 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 %}Merchant Loyalty Settings{% 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
{% endcall %}
{{ loading_state('Loading settings...') }}
{{ error_state('Error loading settings') }}
<!-- 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>
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.
</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">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>
</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">Optional</p>
<p class="text-sm text-gray-500 dark:text-gray-400">Stores can choose whether to require PINs at their locations.</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">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>
</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">
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
</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>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Lockout Duration (minutes)
</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>
</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>
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>
</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>
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>
</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">Allow Void Transactions</p>
<p class="text-sm text-gray-500 dark:text-gray-400">Staff can void points/stamps for returns</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">
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>
</button>
</div>
</form>
</div>
{% endblock %}
{% block extra_scripts %}
<script defer src="{{ url_for('loyalty_static', path='admin/js/loyalty-merchant-settings.js') }}"></script>
{% endblock %}