feat: FASE 2 - Client Base (layout, login, navbar, tema, i18n)

Implementazione completa del frontend Flask:
- Layout base.html con TailwindCSS CDN, dark/light theme, flash messages
- Navbar responsive role-based (Maker, MeasurementTec, Metrologist, Admin)
- Login page professionale con form + API integration
- Profilo utente: nome, lingua, tema, badge ruoli
- Sistema tema dark/light: CSS variables + Alpine.js store + localStorage
- i18n completo IT/EN: Flask-Babel (.po) + alpinejs-i18n (JSON)
- API Client riscritto: error handling normalizzato, no crash su 4xx/5xx
- CSRF protection con Flask-WTF su tutti i form
- Logo aziendale dinamico da system_settings
- Asset SVG: tmflow-logo.svg + tmflow-icon.svg (favicon)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Adriano
2026-02-07 01:10:13 +01:00
parent d6508e0ae8
commit edd4580a5a
17 changed files with 2230 additions and 52 deletions
+203
View File
@@ -0,0 +1,203 @@
{% extends "base.html" %}
{% block title %}{{ _('Profilo') }} — TieMeasureFlow{% endblock %}
{% block content %}
<div class="container mx-auto px-4 sm:px-6 lg:px-8 py-8 max-w-4xl">
<!-- Page Header -->
<div class="mb-8">
<div class="flex items-center space-x-3">
<svg class="h-8 w-8 text-primary-600 dark:text-primary-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
<h1 class="text-3xl font-bold text-slate-900 dark:text-white">
{{ _('Profilo Utente') }}
</h1>
</div>
<p class="mt-2 text-sm text-slate-600 dark:text-slate-400">
{{ _('Gestisci le tue informazioni e preferenze') }}
</p>
</div>
<!-- Profile Card -->
<div class="bg-white dark:bg-slate-800 shadow-lg rounded-xl overflow-hidden">
<form method="POST" action="{{ url_for('auth.profile') }}" class="divide-y divide-slate-200 dark:divide-slate-700">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<!-- Account Information Section -->
<div class="p-6 sm:p-8">
<h2 class="text-lg font-semibold text-slate-900 dark:text-white mb-6">
{{ _('Informazioni Account') }}
</h2>
<div class="space-y-6">
<!-- Username (readonly) -->
<div>
<label for="username" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
{{ _('Username') }}
</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
</div>
<input type="text"
id="username"
value="{{ user.username if user else '' }}"
disabled
class="block w-full pl-10 pr-3 py-2.5 border border-slate-200 dark:border-slate-700 rounded-lg bg-slate-50 dark:bg-slate-900 text-slate-500 dark:text-slate-400 cursor-not-allowed">
</div>
<p class="mt-1 text-xs text-slate-500 dark:text-slate-400">
{{ _('Il nome utente non può essere modificato') }}
</p>
</div>
<!-- Display Name -->
<div>
<label for="display_name" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
{{ _('Nome Visualizzato') }}
</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
</svg>
</div>
<input type="text"
id="display_name"
name="display_name"
value="{{ user.display_name if user else '' }}"
placeholder="{{ _('Nome da visualizzare nell\'interfaccia') }}"
class="block w-full pl-10 pr-3 py-2.5 border border-slate-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500 dark:focus:ring-primary-400 dark:focus:border-primary-400 bg-white dark:bg-slate-700 text-slate-900 dark:text-white placeholder-slate-400 dark:placeholder-slate-500 transition-colors">
</div>
</div>
<!-- Roles (readonly badges) -->
{% if user and user.roles %}
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
{{ _('Ruoli') }}
</label>
<div class="flex flex-wrap gap-2">
{% for role in user.roles %}
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium
{% if role == 'Maker' %}
bg-indigo-100 dark:bg-indigo-900/30 text-indigo-800 dark:text-indigo-200 border border-indigo-200 dark:border-indigo-800
{% elif role == 'MeasurementTec' %}
bg-blue-100 dark:bg-blue-900/30 text-blue-800 dark:text-blue-200 border border-blue-200 dark:border-blue-800
{% elif role == 'Metrologist' %}
bg-purple-100 dark:bg-purple-900/30 text-purple-800 dark:text-purple-200 border border-purple-200 dark:border-purple-800
{% else %}
bg-slate-100 dark:bg-slate-700 text-slate-800 dark:text-slate-200 border border-slate-200 dark:border-slate-600
{% endif %}">
<svg class="h-3 w-3 mr-1.5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
</svg>
{{ _(role) }}
</span>
{% endfor %}
</div>
</div>
{% endif %}
</div>
</div>
<!-- Preferences Section -->
<div class="p-6 sm:p-8">
<h2 class="text-lg font-semibold text-slate-900 dark:text-white mb-6">
{{ _('Preferenze') }}
</h2>
<div class="space-y-6">
<!-- Language Preference -->
<div>
<label for="language_pref" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
{{ _('Lingua Preferita') }}
</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5h12M9 3v2m1.048 9.5A18.022 18.022 0 016.412 9m6.088 9h7M11 21l5-10 5 10M12.751 5C11.783 10.77 8.07 15.61 3 18.129" />
</svg>
</div>
<select id="language_pref"
name="language_pref"
class="block w-full pl-10 pr-10 py-2.5 border border-slate-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500 dark:focus:ring-primary-400 dark:focus:border-primary-400 bg-white dark:bg-slate-700 text-slate-900 dark:text-white transition-colors">
<option value="it" {% if user and user.language_pref == 'it' %}selected{% endif %}>{{ _('Italiano') }}</option>
<option value="en" {% if user and user.language_pref == 'en' %}selected{% endif %}>{{ _('English') }}</option>
</select>
</div>
</div>
<!-- Theme Preference -->
<div>
<label for="theme_pref" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
{{ _('Tema Preferito') }}
</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01" />
</svg>
</div>
<select id="theme_pref"
name="theme_pref"
class="block w-full pl-10 pr-10 py-2.5 border border-slate-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500 dark:focus:ring-primary-400 dark:focus:border-primary-400 bg-white dark:bg-slate-700 text-slate-900 dark:text-white transition-colors">
<option value="light" {% if user and user.theme_pref == 'light' %}selected{% endif %}>{{ _('Chiaro') }}</option>
<option value="dark" {% if user and user.theme_pref == 'dark' %}selected{% endif %}>{{ _('Scuro') }}</option>
</select>
</div>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="px-6 py-4 sm:px-8 bg-slate-50 dark:bg-slate-900/50 flex items-center justify-between">
<a href="/"
class="inline-flex items-center px-4 py-2 border border-slate-300 dark:border-slate-600 rounded-lg shadow-sm text-sm font-medium text-slate-700 dark:text-slate-300 bg-white dark:bg-slate-800 hover:bg-slate-50 dark:hover:bg-slate-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 transition-all duration-200">
<svg class="h-4 w-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
{{ _('Indietro') }}
</a>
<button type="submit"
class="inline-flex items-center px-6 py-2 border border-transparent rounded-lg shadow-sm text-sm font-semibold text-white bg-primary-600 hover:bg-primary-700 dark:bg-primary-500 dark:hover:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 transition-all duration-200">
<svg class="h-4 w-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
{{ _('Salva Modifiche') }}
</button>
</div>
</form>
</div>
<!-- Account Actions -->
<div class="mt-8 bg-white dark:bg-slate-800 shadow-lg rounded-xl overflow-hidden">
<div class="p-6 sm:p-8">
<h2 class="text-lg font-semibold text-slate-900 dark:text-white mb-4">
{{ _('Azioni Account') }}
</h2>
<div class="flex items-start space-x-4">
<div class="flex-shrink-0">
<svg class="h-6 w-6 text-red-600 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
</svg>
</div>
<div class="flex-1">
<h3 class="text-sm font-medium text-slate-900 dark:text-white">{{ _('Esci dal sistema') }}</h3>
<p class="mt-1 text-sm text-slate-500 dark:text-slate-400">
{{ _('Termina la sessione corrente e torna alla schermata di login') }}
</p>
<div class="mt-3">
<a href="{{ url_for('auth.logout') }}"
class="inline-flex items-center px-4 py-2 border border-red-300 dark:border-red-700 rounded-lg shadow-sm text-sm font-medium text-red-700 dark:text-red-300 bg-white dark:bg-slate-800 hover:bg-red-50 dark:hover:bg-red-900/20 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 transition-all duration-200">
{{ _('Logout') }}
</a>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}