Files
orion/app/templates/admin/email-templates.html
Samir Boulahtit 5155ef7445 fix: resolve all architecture validation errors (62 -> 0)
Major refactoring to achieve zero architecture violations:

API Layer:
- vendor/settings.py: Move validation to Pydantic field validators
  (tax rate, delivery method, boost sort, preorder days, languages, locales)
- admin/email_templates.py: Add Pydantic response models
  (TemplateListResponse, CategoriesResponse)
- shop/auth.py: Move password reset logic to CustomerService

Service Layer:
- customer_service.py: Add password reset methods
  (get_customer_for_password_reset, validate_and_reset_password)

Exception Layer:
- customer.py: Add InvalidPasswordResetTokenException,
  PasswordTooShortException

Frontend:
- admin/email-templates.js: Use apiClient, Utils.showToast()
- vendor/email-templates.js: Use apiClient, parent init pattern

Templates:
- admin/email-templates.html: Fix block name to extra_scripts
- shop/base.html: Add language default filter

Tooling:
- validate_architecture.py: Fix LANG-001 false positive for
  SUPPORTED_LANGUAGES and SUPPORTED_LOCALES blocks

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 18:48:59 +01:00

367 lines
20 KiB
HTML

{% extends "admin/base.html" %}
{% block title %}Email Templates{% endblock %}
{% block alpine_data %}emailTemplatesPage(){% endblock %}
{% block content %}
<div class="py-6">
<!-- Header -->
<div class="mb-8">
<h2 class="text-2xl font-semibold text-gray-700 dark:text-gray-200">
Email Templates
</h2>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
Manage platform email templates. Vendors can override non-platform-only templates.
</p>
</div>
<!-- Loading State -->
<div x-show="loading" class="flex justify-center py-12">
<span x-html="$icon('spinner', 'h-8 w-8 text-purple-600')"></span>
</div>
<div x-show="!loading" x-cloak>
<!-- Category Tabs -->
<div class="border-b border-gray-200 dark:border-gray-700 mb-6">
<nav class="-mb-px flex space-x-8 overflow-x-auto">
<button
@click="selectedCategory = null"
:class="{
'border-purple-500 text-purple-600 dark:text-purple-400': selectedCategory === null,
'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300': selectedCategory !== null
}"
class="whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm transition-colors">
All Templates
</button>
<template x-for="cat in categories" :key="cat.code">
<button
@click="selectedCategory = cat.code"
:class="{
'border-purple-500 text-purple-600 dark:text-purple-400': selectedCategory === cat.code,
'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300': selectedCategory !== cat.code
}"
class="whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm transition-colors">
<span x-text="cat.name"></span>
</button>
</template>
</nav>
</div>
<!-- Templates List -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gray-50 dark:bg-gray-700">
<tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Template
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Category
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Languages
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Type
</th>
<th scope="col" class="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
<template x-for="template in filteredTemplates" :key="template.code">
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<span x-html="$icon('mail', 'h-5 w-5 text-gray-400 mr-3')"></span>
<div>
<div class="text-sm font-medium text-gray-900 dark:text-white" x-text="template.name"></div>
<div class="text-sm text-gray-500 dark:text-gray-400 font-mono" x-text="template.code"></div>
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="px-2 py-1 text-xs rounded-full"
:class="getCategoryClass(template.category)"
x-text="template.category"></span>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex space-x-1">
<template x-for="lang in template.languages" :key="lang">
<span class="px-2 py-0.5 text-xs bg-gray-100 dark:bg-gray-600 text-gray-600 dark:text-gray-300 rounded uppercase"
x-text="lang"></span>
</template>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span x-show="template.is_platform_only"
class="px-2 py-1 text-xs bg-orange-100 dark:bg-orange-900 text-orange-800 dark:text-orange-200 rounded-full">
Platform Only
</span>
<span x-show="!template.is_platform_only"
class="px-2 py-1 text-xs bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 rounded-full">
Overridable
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<button @click="editTemplate(template)"
class="text-purple-600 hover:text-purple-900 dark:text-purple-400 dark:hover:text-purple-300 mr-3">
Edit
</button>
<button @click="previewTemplate(template)"
class="text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-300">
Preview
</button>
</td>
</tr>
</template>
<tr x-show="filteredTemplates.length === 0">
<td colspan="5" class="px-6 py-12 text-center text-gray-500 dark:text-gray-400">
No templates found
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Edit Template Modal -->
<div x-show="showEditModal"
x-cloak
class="fixed inset-0 z-50 overflow-y-auto"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0">
<div class="flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:p-0">
<!-- Backdrop -->
<div class="fixed inset-0 bg-gray-500 dark:bg-gray-900 bg-opacity-75 dark:bg-opacity-75 transition-opacity"
@click="closeEditModal()"></div>
<!-- Modal Panel -->
<div class="relative bg-white dark:bg-gray-800 rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:max-w-4xl sm:w-full">
<!-- Header -->
<div class="bg-gray-50 dark:bg-gray-700 px-6 py-4 border-b border-gray-200 dark:border-gray-600">
<div class="flex items-center justify-between">
<div>
<h3 class="text-lg font-medium text-gray-900 dark:text-white" x-text="editingTemplate?.name || 'Edit Template'"></h3>
<p class="text-sm text-gray-500 dark:text-gray-400 font-mono" x-text="editingTemplate?.code"></p>
</div>
<button @click="closeEditModal()" class="text-gray-400 hover:text-gray-500">
<span x-html="$icon('x', 'h-6 w-6')"></span>
</button>
</div>
<!-- Language Tabs -->
<div class="mt-4 flex space-x-2">
<template x-for="lang in ['en', 'fr', 'de', 'lb']" :key="lang">
<button
@click="editLanguage = lang; loadTemplateLanguage()"
:class="{
'bg-purple-600 text-white': editLanguage === lang,
'bg-gray-200 dark:bg-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-500': editLanguage !== lang
}"
class="px-3 py-1 text-sm font-medium rounded-md uppercase transition-colors">
<span x-text="lang"></span>
</button>
</template>
</div>
</div>
<!-- Body -->
<div class="px-6 py-4 max-h-[60vh] overflow-y-auto">
<!-- Loading -->
<div x-show="loadingTemplate" class="flex justify-center py-8">
<span x-html="$icon('spinner', 'h-6 w-6 text-purple-600')"></span>
</div>
<div x-show="!loadingTemplate" class="space-y-4">
<!-- Subject -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Subject
</label>
<input type="text"
x-model="editForm.subject"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-purple-500 focus:border-purple-500 dark:bg-gray-700 dark:text-white text-sm">
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
Supports Jinja2 variables like {{ '{{' }} customer_name {{ '}}' }}
</p>
</div>
<!-- HTML Body -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
HTML Body
</label>
<textarea x-model="editForm.body_html"
rows="12"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-purple-500 focus:border-purple-500 dark:bg-gray-700 dark:text-white text-sm font-mono"></textarea>
</div>
<!-- Plain Text Body -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Plain Text Body
</label>
<textarea x-model="editForm.body_text"
rows="6"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-purple-500 focus:border-purple-500 dark:bg-gray-700 dark:text-white text-sm font-mono"></textarea>
</div>
<!-- Variables Reference -->
<div class="bg-gray-50 dark:bg-gray-700 rounded-lg p-4">
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Available Variables</h4>
<div class="flex flex-wrap gap-2">
<template x-for="variable in editForm.variables || []" :key="variable">
<span class="px-2 py-1 bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200 rounded text-xs font-mono"
x-text="'{{ ' + variable + ' }}'"></span>
</template>
<span x-show="!editForm.variables || editForm.variables.length === 0"
class="text-gray-500 dark:text-gray-400 text-sm">No variables defined</span>
</div>
</div>
</div>
</div>
<!-- Footer -->
<div class="bg-gray-50 dark:bg-gray-700 px-6 py-4 border-t border-gray-200 dark:border-gray-600 flex justify-between">
<div>
<button @click="sendTestEmail()"
:disabled="sendingTest"
class="inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-600 hover:bg-gray-50 dark:hover:bg-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500">
<span x-show="sendingTest" x-html="$icon('spinner', '-ml-1 mr-2 h-4 w-4')"></span>
<span x-html="$icon('mail', '-ml-1 mr-2 h-4 w-4')"></span>
<span x-text="sendingTest ? 'Sending...' : 'Send Test Email'"></span>
</button>
</div>
<div class="flex space-x-3">
<button @click="closeEditModal()"
class="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-600 hover:bg-gray-50 dark:hover:bg-gray-500">
Cancel
</button>
<button @click="saveTemplate()"
:disabled="saving"
class="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-purple-600 hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500">
<span x-show="saving" x-html="$icon('spinner', '-ml-1 mr-2 h-4 w-4 text-white')"></span>
<span x-text="saving ? 'Saving...' : 'Save Changes'"></span>
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Preview Modal -->
<div x-show="showPreviewModal"
x-cloak
class="fixed inset-0 z-50 overflow-y-auto"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0">
<div class="flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:p-0">
<!-- Backdrop -->
<div class="fixed inset-0 bg-gray-500 dark:bg-gray-900 bg-opacity-75 dark:bg-opacity-75 transition-opacity"
@click="showPreviewModal = false"></div>
<!-- Modal Panel -->
<div class="relative bg-white dark:bg-gray-800 rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:max-w-4xl sm:w-full">
<!-- Header -->
<div class="bg-gray-50 dark:bg-gray-700 px-6 py-4 border-b border-gray-200 dark:border-gray-600">
<div class="flex items-center justify-between">
<div>
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Email Preview</h3>
<p class="text-sm text-gray-500 dark:text-gray-400" x-text="previewData?.subject"></p>
</div>
<button @click="showPreviewModal = false" class="text-gray-400 hover:text-gray-500">
<span x-html="$icon('x', 'h-6 w-6')"></span>
</button>
</div>
</div>
<!-- Body -->
<div class="p-6 max-h-[70vh] overflow-y-auto bg-gray-100 dark:bg-gray-900">
<div class="bg-white rounded-lg shadow-lg overflow-hidden">
<iframe :srcdoc="previewData?.body_html"
class="w-full h-96 border-0"
sandbox="allow-same-origin"></iframe>
</div>
</div>
<!-- Footer -->
<div class="bg-gray-50 dark:bg-gray-700 px-6 py-4 border-t border-gray-200 dark:border-gray-600 flex justify-end">
<button @click="showPreviewModal = false"
class="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-600 hover:bg-gray-50 dark:hover:bg-gray-500">
Close
</button>
</div>
</div>
</div>
</div>
<!-- Test Email Modal -->
<div x-show="showTestEmailModal"
x-cloak
class="fixed inset-0 z-50 overflow-y-auto"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0">
<div class="flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:p-0">
<!-- Backdrop -->
<div class="fixed inset-0 bg-gray-500 dark:bg-gray-900 bg-opacity-75 dark:bg-opacity-75 transition-opacity"
@click="showTestEmailModal = false"></div>
<!-- Modal Panel -->
<div class="relative bg-white dark:bg-gray-800 rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:max-w-md sm:w-full">
<!-- Header -->
<div class="bg-gray-50 dark:bg-gray-700 px-6 py-4 border-b border-gray-200 dark:border-gray-600">
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Send Test Email</h3>
</div>
<!-- Body -->
<div class="px-6 py-4">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Send to Email
</label>
<input type="email"
x-model="testEmailAddress"
placeholder="your@email.com"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-purple-500 focus:border-purple-500 dark:bg-gray-700 dark:text-white text-sm">
</div>
</div>
<!-- Footer -->
<div class="bg-gray-50 dark:bg-gray-700 px-6 py-4 border-t border-gray-200 dark:border-gray-600 flex justify-end space-x-3">
<button @click="showTestEmailModal = false"
class="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-600 hover:bg-gray-50 dark:hover:bg-gray-500">
Cancel
</button>
<button @click="confirmSendTestEmail()"
:disabled="!testEmailAddress || sendingTest"
class="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-purple-600 hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 disabled:opacity-50 disabled:cursor-not-allowed">
<span x-show="sendingTest" x-html="$icon('spinner', '-ml-1 mr-2 h-4 w-4 text-white')"></span>
<span x-text="sendingTest ? 'Sending...' : 'Send Test'"></span>
</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script src="{{ url_for('static', path='admin/js/email-templates.js') }}"></script>
{% endblock %}