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:
@@ -3,6 +3,7 @@ import os
|
|||||||
|
|
||||||
from flask import Flask, redirect, url_for, session, request
|
from flask import Flask, redirect, url_for, session, request
|
||||||
from flask_babel import Babel
|
from flask_babel import Babel
|
||||||
|
from flask_wtf.csrf import CSRFProtect
|
||||||
|
|
||||||
from config import Config
|
from config import Config
|
||||||
|
|
||||||
@@ -23,6 +24,9 @@ def create_app() -> Flask:
|
|||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.config.from_object(Config)
|
app.config.from_object(Config)
|
||||||
|
|
||||||
|
# Initialize CSRF protection
|
||||||
|
csrf = CSRFProtect(app)
|
||||||
|
|
||||||
# Initialize Flask-Babel
|
# Initialize Flask-Babel
|
||||||
Babel(app, locale_selector=get_locale)
|
Babel(app, locale_selector=get_locale)
|
||||||
|
|
||||||
@@ -44,6 +48,13 @@ def create_app() -> Flask:
|
|||||||
return redirect(url_for("measure.select_recipe"))
|
return redirect(url_for("measure.select_recipe"))
|
||||||
return redirect(url_for("auth.login"))
|
return redirect(url_for("auth.login"))
|
||||||
|
|
||||||
|
@app.route("/set-language/<lang>")
|
||||||
|
def set_language(lang):
|
||||||
|
"""Set user's preferred language and store in session."""
|
||||||
|
if lang in Config.LANGUAGES:
|
||||||
|
session["language"] = lang
|
||||||
|
return redirect(request.referrer or url_for("auth.login"))
|
||||||
|
|
||||||
@app.context_processor
|
@app.context_processor
|
||||||
def inject_globals():
|
def inject_globals():
|
||||||
"""Inject global variables into all templates."""
|
"""Inject global variables into all templates."""
|
||||||
@@ -52,6 +63,7 @@ def create_app() -> Flask:
|
|||||||
"current_theme": session.get("theme", "light"),
|
"current_theme": session.get("theme", "light"),
|
||||||
"current_language": get_locale(),
|
"current_language": get_locale(),
|
||||||
"languages": Config.LANGUAGES,
|
"languages": Config.LANGUAGES,
|
||||||
|
"company_logo": session.get("company_logo"),
|
||||||
}
|
}
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|||||||
+123
-13
@@ -1,31 +1,141 @@
|
|||||||
"""Authentication blueprint - login, logout, profile."""
|
"""Authentication blueprint - login, logout, profile."""
|
||||||
from flask import Blueprint, render_template, request, redirect, url_for, session, flash
|
from functools import wraps
|
||||||
|
|
||||||
auth_bp = Blueprint("auth", __name__)
|
from flask import Blueprint, flash, redirect, render_template, request, session, url_for
|
||||||
|
from flask_babel import gettext as _
|
||||||
|
|
||||||
|
from services.api_client import api_client
|
||||||
|
|
||||||
|
auth_bp = Blueprint("auth", __name__, url_prefix="/auth")
|
||||||
|
|
||||||
|
|
||||||
|
def login_required(f):
|
||||||
|
"""Decorator to require login for protected routes."""
|
||||||
|
@wraps(f)
|
||||||
|
def decorated(*args, **kwargs):
|
||||||
|
if not session.get("api_key"):
|
||||||
|
flash(_("Effettua il login per continuare"), "warning")
|
||||||
|
return redirect(url_for("auth.login"))
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
return decorated
|
||||||
|
|
||||||
|
|
||||||
@auth_bp.route("/login", methods=["GET", "POST"])
|
@auth_bp.route("/login", methods=["GET", "POST"])
|
||||||
def login():
|
def login():
|
||||||
"""Login page."""
|
"""Login page and form handler."""
|
||||||
|
# Se già loggato, redirect a home
|
||||||
|
if session.get("api_key"):
|
||||||
|
try:
|
||||||
|
return redirect(url_for("measure.select_recipe"))
|
||||||
|
except Exception:
|
||||||
|
return redirect(url_for("auth.profile"))
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
# TODO: Implement API call to server /api/auth/login
|
username = request.form.get("username", "").strip()
|
||||||
pass
|
password = request.form.get("password", "")
|
||||||
|
|
||||||
|
if not username or not password:
|
||||||
|
flash(_("Inserisci username e password"), "error")
|
||||||
|
return render_template("auth/login.html")
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = api_client.post("/api/auth/login", data={"username": username, "password": password})
|
||||||
|
|
||||||
|
if response.get("error"):
|
||||||
|
flash(response.get("detail", _("Credenziali non valide")), "error")
|
||||||
|
return render_template("auth/login.html")
|
||||||
|
|
||||||
|
# Salva in sessione
|
||||||
|
session["api_key"] = response["api_key"]
|
||||||
|
session["user"] = response["user"]
|
||||||
|
session["user_id"] = response["user"]["id"]
|
||||||
|
|
||||||
|
# Applica preferenze utente
|
||||||
|
user = response["user"]
|
||||||
|
if user.get("language_pref"):
|
||||||
|
session["language"] = user["language_pref"]
|
||||||
|
if user.get("theme_pref"):
|
||||||
|
session["theme"] = user["theme_pref"]
|
||||||
|
|
||||||
|
# Carica logo aziendale dalle impostazioni di sistema
|
||||||
|
settings_resp = api_client.get("/api/settings")
|
||||||
|
if not settings_resp.get("error"):
|
||||||
|
logo_path = settings_resp.get("company_logo_path")
|
||||||
|
if logo_path:
|
||||||
|
session["company_logo"] = logo_path
|
||||||
|
|
||||||
|
flash(_("Benvenuto, %(name)s!", name=user.get("display_name", username)), "success")
|
||||||
|
|
||||||
|
# Redirect dopo login
|
||||||
|
try:
|
||||||
|
return redirect(url_for("measure.select_recipe"))
|
||||||
|
except Exception:
|
||||||
|
return redirect(url_for("auth.profile"))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
flash(_("Errore di connessione al server: %(error)s", error=str(e)), "error")
|
||||||
|
return render_template("auth/login.html")
|
||||||
|
|
||||||
return render_template("auth/login.html")
|
return render_template("auth/login.html")
|
||||||
|
|
||||||
|
|
||||||
@auth_bp.route("/logout")
|
@auth_bp.route("/logout", methods=["GET", "POST"])
|
||||||
def logout():
|
def logout():
|
||||||
"""Logout - clear session."""
|
"""Clear session and redirect to login."""
|
||||||
|
# Invalida il token sul server
|
||||||
|
if session.get("api_key"):
|
||||||
|
try:
|
||||||
|
api_client.post("/api/auth/logout")
|
||||||
|
except Exception:
|
||||||
|
pass # Ignora errori durante logout
|
||||||
|
|
||||||
session.clear()
|
session.clear()
|
||||||
|
flash(_("Logout effettuato"), "info")
|
||||||
return redirect(url_for("auth.login"))
|
return redirect(url_for("auth.login"))
|
||||||
|
|
||||||
|
|
||||||
@auth_bp.route("/profile", methods=["GET", "POST"])
|
@auth_bp.route("/profile", methods=["GET", "POST"])
|
||||||
|
@login_required
|
||||||
def profile():
|
def profile():
|
||||||
"""User profile - change display name, language, theme."""
|
"""User profile page - change display name, language, theme."""
|
||||||
if "user" not in session:
|
|
||||||
return redirect(url_for("auth.login"))
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
# TODO: Implement API call to server /api/auth/me PUT
|
display_name = request.form.get("display_name", "").strip()
|
||||||
pass
|
language_pref = request.form.get("language_pref", "").strip()
|
||||||
return render_template("auth/profile.html")
|
theme_pref = request.form.get("theme_pref", "").strip()
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = {}
|
||||||
|
if display_name:
|
||||||
|
data["display_name"] = display_name
|
||||||
|
if language_pref:
|
||||||
|
data["language_pref"] = language_pref
|
||||||
|
if theme_pref:
|
||||||
|
data["theme_pref"] = theme_pref
|
||||||
|
|
||||||
|
response = api_client.put("/api/auth/me", data=data)
|
||||||
|
|
||||||
|
if response.get("error"):
|
||||||
|
flash(response.get("detail", _("Errore durante l'aggiornamento del profilo")), "error")
|
||||||
|
else:
|
||||||
|
# Aggiorna sessione
|
||||||
|
session["user"] = response
|
||||||
|
if language_pref:
|
||||||
|
session["language"] = language_pref
|
||||||
|
if theme_pref:
|
||||||
|
session["theme"] = theme_pref
|
||||||
|
|
||||||
|
flash(_("Profilo aggiornato con successo"), "success")
|
||||||
|
return redirect(url_for("auth.profile"))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
flash(_("Errore di connessione al server: %(error)s", error=str(e)), "error")
|
||||||
|
|
||||||
|
# GET - carica dati profilo
|
||||||
|
user_data = api_client.get("/api/auth/me")
|
||||||
|
if user_data.get("error"):
|
||||||
|
flash(_("Errore nel caricamento del profilo: %(error)s", error=user_data.get("detail", "")), "error")
|
||||||
|
user_data = session.get("user", {})
|
||||||
|
else:
|
||||||
|
session["user"] = user_data
|
||||||
|
|
||||||
|
return render_template("auth/profile.html", user=user_data)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# Flask
|
# Flask
|
||||||
flask>=3.0.0
|
flask>=3.0.0
|
||||||
flask-babel>=4.0.0
|
flask-babel>=4.0.0
|
||||||
|
flask-wtf>=1.2.0
|
||||||
|
|
||||||
# HTTP Client (to call FastAPI server)
|
# HTTP Client (to call FastAPI server)
|
||||||
requests>=2.31.0
|
requests>=2.31.0
|
||||||
|
|||||||
@@ -23,58 +23,108 @@ class APIClient:
|
|||||||
headers["X-API-Key"] = api_key
|
headers["X-API-Key"] = api_key
|
||||||
return headers
|
return headers
|
||||||
|
|
||||||
|
def _handle_response(self, response: requests.Response) -> dict[str, Any]:
|
||||||
|
"""Parse response and return normalized dict.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- If 2xx and not 204: response JSON
|
||||||
|
- If 204 No Content: empty dict
|
||||||
|
- If 4xx/5xx: {"error": True, "status_code": ..., "detail": "..."}
|
||||||
|
"""
|
||||||
|
if response.ok:
|
||||||
|
if response.status_code == 204:
|
||||||
|
return {}
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
# Non-OK response (4xx/5xx)
|
||||||
|
try:
|
||||||
|
error_body = response.json()
|
||||||
|
detail = error_body.get("detail", str(error_body))
|
||||||
|
except Exception:
|
||||||
|
detail = response.text or f"HTTP {response.status_code}"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"error": True,
|
||||||
|
"status_code": response.status_code,
|
||||||
|
"detail": detail
|
||||||
|
}
|
||||||
|
|
||||||
def get(self, endpoint: str, params: dict | None = None) -> dict[str, Any]:
|
def get(self, endpoint: str, params: dict | None = None) -> dict[str, Any]:
|
||||||
"""GET request to API server."""
|
"""GET request to API server."""
|
||||||
response = requests.get(
|
try:
|
||||||
f"{self.base_url}{endpoint}",
|
response = requests.get(
|
||||||
headers=self._headers,
|
f"{self.base_url}{endpoint}",
|
||||||
params=params,
|
headers=self._headers,
|
||||||
timeout=self.timeout,
|
params=params,
|
||||||
)
|
timeout=self.timeout,
|
||||||
response.raise_for_status()
|
)
|
||||||
return response.json()
|
return self._handle_response(response)
|
||||||
|
except (requests.ConnectionError, requests.Timeout) as e:
|
||||||
|
return {
|
||||||
|
"error": True,
|
||||||
|
"status_code": 0,
|
||||||
|
"detail": f"Errore di connessione al server: {str(e)}"
|
||||||
|
}
|
||||||
|
|
||||||
def post(self, endpoint: str, data: dict | None = None, files: dict | None = None) -> dict[str, Any]:
|
def post(self, endpoint: str, data: dict | None = None, files: dict | None = None) -> dict[str, Any]:
|
||||||
"""POST request to API server."""
|
"""POST request to API server."""
|
||||||
if files:
|
try:
|
||||||
headers = {"X-API-Key": session.get("api_key", "")}
|
if files:
|
||||||
response = requests.post(
|
headers = {"X-API-Key": session.get("api_key", "")}
|
||||||
f"{self.base_url}{endpoint}",
|
response = requests.post(
|
||||||
headers=headers,
|
f"{self.base_url}{endpoint}",
|
||||||
data=data,
|
headers=headers,
|
||||||
files=files,
|
data=data,
|
||||||
timeout=self.timeout,
|
files=files,
|
||||||
)
|
timeout=self.timeout,
|
||||||
else:
|
)
|
||||||
response = requests.post(
|
else:
|
||||||
|
response = requests.post(
|
||||||
|
f"{self.base_url}{endpoint}",
|
||||||
|
headers=self._headers,
|
||||||
|
json=data,
|
||||||
|
timeout=self.timeout,
|
||||||
|
)
|
||||||
|
return self._handle_response(response)
|
||||||
|
except (requests.ConnectionError, requests.Timeout) as e:
|
||||||
|
return {
|
||||||
|
"error": True,
|
||||||
|
"status_code": 0,
|
||||||
|
"detail": f"Errore di connessione al server: {str(e)}"
|
||||||
|
}
|
||||||
|
|
||||||
|
def put(self, endpoint: str, data: dict | None = None) -> dict[str, Any]:
|
||||||
|
"""PUT request to API server."""
|
||||||
|
try:
|
||||||
|
response = requests.put(
|
||||||
f"{self.base_url}{endpoint}",
|
f"{self.base_url}{endpoint}",
|
||||||
headers=self._headers,
|
headers=self._headers,
|
||||||
json=data,
|
json=data,
|
||||||
timeout=self.timeout,
|
timeout=self.timeout,
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
return self._handle_response(response)
|
||||||
return response.json()
|
except (requests.ConnectionError, requests.Timeout) as e:
|
||||||
|
return {
|
||||||
def put(self, endpoint: str, data: dict | None = None) -> dict[str, Any]:
|
"error": True,
|
||||||
"""PUT request to API server."""
|
"status_code": 0,
|
||||||
response = requests.put(
|
"detail": f"Errore di connessione al server: {str(e)}"
|
||||||
f"{self.base_url}{endpoint}",
|
}
|
||||||
headers=self._headers,
|
|
||||||
json=data,
|
|
||||||
timeout=self.timeout,
|
|
||||||
)
|
|
||||||
response.raise_for_status()
|
|
||||||
return response.json()
|
|
||||||
|
|
||||||
def delete(self, endpoint: str) -> dict[str, Any]:
|
def delete(self, endpoint: str) -> dict[str, Any]:
|
||||||
"""DELETE request to API server."""
|
"""DELETE request to API server."""
|
||||||
response = requests.delete(
|
try:
|
||||||
f"{self.base_url}{endpoint}",
|
response = requests.delete(
|
||||||
headers=self._headers,
|
f"{self.base_url}{endpoint}",
|
||||||
timeout=self.timeout,
|
headers=self._headers,
|
||||||
)
|
timeout=self.timeout,
|
||||||
response.raise_for_status()
|
)
|
||||||
return response.json()
|
return self._handle_response(response)
|
||||||
|
except (requests.ConnectionError, requests.Timeout) as e:
|
||||||
|
return {
|
||||||
|
"error": True,
|
||||||
|
"status_code": 0,
|
||||||
|
"detail": f"Errore di connessione al server: {str(e)}"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
api_client = APIClient()
|
api_client = APIClient()
|
||||||
|
|||||||
@@ -0,0 +1,374 @@
|
|||||||
|
/* ============================================================
|
||||||
|
TieMeasureFlow - Theme System
|
||||||
|
CSS Custom Properties for Light / Dark mode
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
/* ---- Light Theme (Default) ---- */
|
||||||
|
:root {
|
||||||
|
--color-primary: #2563EB;
|
||||||
|
--color-primary-dark: #1E40AF;
|
||||||
|
--color-primary-light: #3B82F6;
|
||||||
|
--color-secondary: #64748B;
|
||||||
|
--color-accent: #1E40AF;
|
||||||
|
|
||||||
|
--color-pass: #059669;
|
||||||
|
--color-warning: #D97706;
|
||||||
|
--color-fail: #DC2626;
|
||||||
|
|
||||||
|
--bg-primary: #F8FAFC;
|
||||||
|
--bg-secondary: #F1F5F9;
|
||||||
|
--bg-card: #FFFFFF;
|
||||||
|
--bg-card-hover: #F8FAFC;
|
||||||
|
|
||||||
|
--text-primary: #0F172A;
|
||||||
|
--text-secondary: #475569;
|
||||||
|
--text-muted: #94A3B8;
|
||||||
|
|
||||||
|
--border-color: #E2E8F0;
|
||||||
|
--border-focus: #2563EB;
|
||||||
|
|
||||||
|
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||||
|
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.07), 0 2px 4px -2px rgba(0, 0, 0, 0.05);
|
||||||
|
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.08), 0 4px 6px -4px rgba(0, 0, 0, 0.04);
|
||||||
|
|
||||||
|
--scrollbar-track: #F1F5F9;
|
||||||
|
--scrollbar-thumb: #CBD5E1;
|
||||||
|
--scrollbar-thumb-hover: #94A3B8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Dark Theme ---- */
|
||||||
|
.dark {
|
||||||
|
--bg-primary: #0F172A;
|
||||||
|
--bg-secondary: #1E293B;
|
||||||
|
--bg-card: #334155;
|
||||||
|
--bg-card-hover: #3B4A63;
|
||||||
|
|
||||||
|
--text-primary: #F1F5F9;
|
||||||
|
--text-secondary: #94A3B8;
|
||||||
|
--text-muted: #64748B;
|
||||||
|
|
||||||
|
--border-color: #475569;
|
||||||
|
--border-focus: #3B82F6;
|
||||||
|
|
||||||
|
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.3);
|
||||||
|
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4), 0 2px 4px -2px rgba(0, 0, 0, 0.3);
|
||||||
|
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.5), 0 4px 6px -4px rgba(0, 0, 0, 0.3);
|
||||||
|
|
||||||
|
--scrollbar-track: #1E293B;
|
||||||
|
--scrollbar-thumb: #475569;
|
||||||
|
--scrollbar-thumb-hover: #64748B;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
Global Transitions
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
transition-property: background-color, border-color, color, box-shadow;
|
||||||
|
transition-duration: 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Enable smooth transitions only after page load (avoid flash) */
|
||||||
|
html.theme-transitions *,
|
||||||
|
html.theme-transitions *::before,
|
||||||
|
html.theme-transitions *::after {
|
||||||
|
transition-duration: 0.3s;
|
||||||
|
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
Focus Ring
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
*:focus-visible {
|
||||||
|
outline: 2px solid var(--border-focus);
|
||||||
|
outline-offset: 2px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus-visible,
|
||||||
|
select:focus-visible,
|
||||||
|
textarea:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 2px var(--bg-card), 0 0 0 4px var(--border-focus);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
Custom Scrollbar
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
/* Webkit (Chrome, Safari, Edge) */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: var(--scrollbar-track);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--scrollbar-thumb);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--scrollbar-thumb-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Firefox */
|
||||||
|
* {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
Utility: Card
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
.tmf-card {
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
transition: box-shadow 0.2s ease, border-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tmf-card:hover {
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tmf-card-header {
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tmf-card-body {
|
||||||
|
padding: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tmf-card-footer {
|
||||||
|
padding: 0.75rem 1.25rem;
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-radius: 0 0 0.75rem 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
Utility: Badges (Pass / Warning / Fail)
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
padding: 0.125rem 0.625rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 9999px;
|
||||||
|
line-height: 1.5;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-pass {
|
||||||
|
background: rgba(5, 150, 105, 0.1);
|
||||||
|
color: #059669;
|
||||||
|
border: 1px solid rgba(5, 150, 105, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .badge-pass {
|
||||||
|
background: rgba(5, 150, 105, 0.15);
|
||||||
|
color: #34D399;
|
||||||
|
border-color: rgba(5, 150, 105, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-warning {
|
||||||
|
background: rgba(217, 119, 6, 0.1);
|
||||||
|
color: #D97706;
|
||||||
|
border: 1px solid rgba(217, 119, 6, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .badge-warning {
|
||||||
|
background: rgba(217, 119, 6, 0.15);
|
||||||
|
color: #FBBF24;
|
||||||
|
border-color: rgba(217, 119, 6, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-fail {
|
||||||
|
background: rgba(220, 38, 38, 0.1);
|
||||||
|
color: #DC2626;
|
||||||
|
border: 1px solid rgba(220, 38, 38, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .badge-fail {
|
||||||
|
background: rgba(220, 38, 38, 0.15);
|
||||||
|
color: #F87171;
|
||||||
|
border-color: rgba(220, 38, 38, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-neutral {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
Utility: Buttons
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: var(--color-primary);
|
||||||
|
color: #FFFFFF;
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover:not(:disabled) {
|
||||||
|
background: var(--color-primary-dark);
|
||||||
|
border-color: var(--color-primary-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
border-color: var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover:not(:disabled) {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
border-color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: var(--color-fail);
|
||||||
|
color: #FFFFFF;
|
||||||
|
border-color: var(--color-fail);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:hover:not(:disabled) {
|
||||||
|
background: #B91C1C;
|
||||||
|
border-color: #B91C1C;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
Utility: Form Inputs
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
.tmf-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: var(--text-primary);
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tmf-input::placeholder {
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tmf-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--border-focus);
|
||||||
|
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .tmf-input:focus {
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tmf-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
Utility: Measurement Value Display
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
.measure-value {
|
||||||
|
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: -0.025em;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
Utility: Table
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
.tmf-table {
|
||||||
|
width: 100%;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tmf-table th {
|
||||||
|
padding: 0.625rem 1rem;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tmf-table td {
|
||||||
|
padding: 0.625rem 1rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tmf-table tbody tr:hover {
|
||||||
|
background: var(--bg-card-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
Alpine.js x-cloak (hide until Alpine loads)
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
[x-cloak] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
|
||||||
|
<!-- Stylized Caliper Icon for Favicon -->
|
||||||
|
|
||||||
|
<!-- Caliper outer frame -->
|
||||||
|
<rect x="2" y="3" width="5" height="26" rx="1.5" fill="#2563EB"/>
|
||||||
|
<rect x="2" y="3" width="19" height="5" rx="1.5" fill="#2563EB"/>
|
||||||
|
<rect x="2" y="24" width="19" height="5" rx="1.5" fill="#2563EB"/>
|
||||||
|
|
||||||
|
<!-- Sliding jaw -->
|
||||||
|
<rect x="12" y="8" width="4.5" height="6.5" rx="1" fill="#1E40AF"/>
|
||||||
|
<rect x="12" y="17.5" width="4.5" height="6.5" rx="1" fill="#1E40AF"/>
|
||||||
|
|
||||||
|
<!-- Depth rod -->
|
||||||
|
<rect x="4" y="14" width="12" height="3.5" rx="1" fill="#3B82F6" opacity="0.55"/>
|
||||||
|
|
||||||
|
<!-- Scale markings -->
|
||||||
|
<rect x="8" y="5" width="1" height="2.5" rx="0.5" fill="#FFFFFF" opacity="0.65"/>
|
||||||
|
<rect x="11" y="5" width="1" height="2.5" rx="0.5" fill="#FFFFFF" opacity="0.65"/>
|
||||||
|
<rect x="14" y="5" width="1" height="2.5" rx="0.5" fill="#FFFFFF" opacity="0.65"/>
|
||||||
|
<rect x="17" y="5" width="1" height="2.5" rx="0.5" fill="#FFFFFF" opacity="0.65"/>
|
||||||
|
|
||||||
|
<!-- Flow arrow -->
|
||||||
|
<path d="M22 16 C25 16, 26.5 11, 29 11"
|
||||||
|
stroke="#3B82F6" stroke-width="2.2" stroke-linecap="round" fill="none"/>
|
||||||
|
<path d="M27 8.5 L29.5 11 L27 13.5"
|
||||||
|
stroke="#3B82F6" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,41 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 220 44" fill="none">
|
||||||
|
<!-- Stylized Caliper Icon -->
|
||||||
|
<g transform="translate(2, 2)">
|
||||||
|
<!-- Caliper outer frame -->
|
||||||
|
<rect x="0" y="4" width="6" height="32" rx="1.5" fill="#2563EB"/>
|
||||||
|
<rect x="0" y="4" width="24" height="6" rx="1.5" fill="#2563EB"/>
|
||||||
|
<rect x="0" y="30" width="24" height="6" rx="1.5" fill="#2563EB"/>
|
||||||
|
|
||||||
|
<!-- Caliper sliding jaw -->
|
||||||
|
<rect x="14" y="10" width="5" height="8" rx="1" fill="#1E40AF"/>
|
||||||
|
<rect x="14" y="22" width="5" height="8" rx="1" fill="#1E40AF"/>
|
||||||
|
|
||||||
|
<!-- Caliper depth rod -->
|
||||||
|
<rect x="2" y="18" width="16" height="4" rx="1" fill="#3B82F6" opacity="0.6"/>
|
||||||
|
|
||||||
|
<!-- Scale markings -->
|
||||||
|
<rect x="7" y="7" width="1" height="3" rx="0.5" fill="#FFFFFF" opacity="0.7"/>
|
||||||
|
<rect x="10" y="7" width="1" height="3" rx="0.5" fill="#FFFFFF" opacity="0.7"/>
|
||||||
|
<rect x="13" y="7" width="1" height="3" rx="0.5" fill="#FFFFFF" opacity="0.7"/>
|
||||||
|
<rect x="16" y="7" width="1" height="3" rx="0.5" fill="#FFFFFF" opacity="0.7"/>
|
||||||
|
<rect x="19" y="7" width="1" height="3" rx="0.5" fill="#FFFFFF" opacity="0.7"/>
|
||||||
|
|
||||||
|
<!-- Flow arrow (smooth curve) -->
|
||||||
|
<path d="M26 20 C30 20, 32 14, 36 14 C40 14, 40 20, 36 20"
|
||||||
|
stroke="#2563EB" stroke-width="2.5" stroke-linecap="round" fill="none"/>
|
||||||
|
<path d="M34 17 L37 20 L34 23" stroke="#2563EB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Text: TieMeasureFlow -->
|
||||||
|
<g transform="translate(48, 0)">
|
||||||
|
<!-- "Tie" in bold primary -->
|
||||||
|
<text x="0" y="28" font-family="Inter, system-ui, sans-serif" font-size="20" font-weight="700" fill="#2563EB"
|
||||||
|
letter-spacing="-0.5">Tie</text>
|
||||||
|
<!-- "Measure" in medium dark -->
|
||||||
|
<text x="32" y="28" font-family="Inter, system-ui, sans-serif" font-size="20" font-weight="500" fill="#1E40AF"
|
||||||
|
letter-spacing="-0.5">Measure</text>
|
||||||
|
<!-- "Flow" in bold primary -->
|
||||||
|
<text x="120" y="28" font-family="Inter, system-ui, sans-serif" font-size="20" font-weight="700" fill="#2563EB"
|
||||||
|
letter-spacing="-0.5">Flow</text>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.1 KiB |
@@ -0,0 +1,126 @@
|
|||||||
|
/**
|
||||||
|
* TieMeasureFlow - Alpine.js Theme Initialization
|
||||||
|
*
|
||||||
|
* This script MUST load before Alpine.js (which uses defer).
|
||||||
|
* It reads the saved theme from localStorage or the system preference,
|
||||||
|
* applies the 'dark' class to <html> immediately (no flash), and
|
||||||
|
* registers an Alpine.store('theme') for reactive toggling.
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// ---- Immediate theme application (before paint) ----
|
||||||
|
|
||||||
|
var STORAGE_KEY = 'tmf-theme';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the initial dark-mode state.
|
||||||
|
* Priority: localStorage > system preference > light (default).
|
||||||
|
*/
|
||||||
|
function getInitialDark() {
|
||||||
|
var stored = null;
|
||||||
|
try {
|
||||||
|
stored = localStorage.getItem(STORAGE_KEY);
|
||||||
|
} catch (_) {
|
||||||
|
// localStorage may be unavailable (private browsing, etc.)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stored === 'dark') return true;
|
||||||
|
if (stored === 'light') return false;
|
||||||
|
|
||||||
|
// Fall back to OS preference
|
||||||
|
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var isDark = getInitialDark();
|
||||||
|
|
||||||
|
// Apply immediately to prevent flash of wrong theme
|
||||||
|
if (isDark) {
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove('dark');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Enable smooth transitions after first paint ----
|
||||||
|
|
||||||
|
// Use requestAnimationFrame to wait one frame, then enable transitions
|
||||||
|
// so the initial theme application does not animate.
|
||||||
|
if (typeof requestAnimationFrame === 'function') {
|
||||||
|
requestAnimationFrame(function () {
|
||||||
|
requestAnimationFrame(function () {
|
||||||
|
document.documentElement.classList.add('theme-transitions');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Alpine.js Store Registration ----
|
||||||
|
|
||||||
|
// Alpine.store is registered via the alpine:init event, which fires
|
||||||
|
// when Alpine begins initialization but before it processes x-data.
|
||||||
|
|
||||||
|
document.addEventListener('alpine:init', function () {
|
||||||
|
Alpine.store('theme', {
|
||||||
|
dark: isDark,
|
||||||
|
|
||||||
|
toggle: function () {
|
||||||
|
this.dark = !this.dark;
|
||||||
|
this._apply();
|
||||||
|
},
|
||||||
|
|
||||||
|
set: function (dark) {
|
||||||
|
this.dark = dark;
|
||||||
|
this._apply();
|
||||||
|
},
|
||||||
|
|
||||||
|
_apply: function () {
|
||||||
|
if (this.dark) {
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove('dark');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
localStorage.setItem(STORAGE_KEY, this.dark ? 'dark' : 'light');
|
||||||
|
} catch (_) {
|
||||||
|
// Silently ignore storage errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---- Listen for system preference changes ----
|
||||||
|
|
||||||
|
if (window.matchMedia) {
|
||||||
|
try {
|
||||||
|
var mq = window.matchMedia('(prefers-color-scheme: dark)');
|
||||||
|
mq.addEventListener('change', function (e) {
|
||||||
|
// Only auto-switch if the user hasn't manually set a preference
|
||||||
|
var stored = null;
|
||||||
|
try {
|
||||||
|
stored = localStorage.getItem(STORAGE_KEY);
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
if (!stored) {
|
||||||
|
var newDark = e.matches;
|
||||||
|
if (typeof Alpine !== 'undefined' && Alpine.store('theme')) {
|
||||||
|
Alpine.store('theme').set(newDark);
|
||||||
|
} else {
|
||||||
|
// Alpine not loaded yet, apply directly
|
||||||
|
if (newDark) {
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove('dark');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (_) {
|
||||||
|
// Older browsers may not support addEventListener on matchMedia
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
})();
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
{
|
||||||
|
"numpad": {
|
||||||
|
"title": "Numeric Keypad",
|
||||||
|
"clear": "Clear",
|
||||||
|
"submit": "Confirm",
|
||||||
|
"decimal": "Decimal",
|
||||||
|
"close": "Close"
|
||||||
|
},
|
||||||
|
"measurement": {
|
||||||
|
"pass": "Pass",
|
||||||
|
"warning": "Warning",
|
||||||
|
"fail": "Fail",
|
||||||
|
"value": "Value",
|
||||||
|
"next": "Next measurement"
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"loading": "Loading...",
|
||||||
|
"save": "Save",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"confirm": "Confirm",
|
||||||
|
"delete": "Delete",
|
||||||
|
"edit": "Edit",
|
||||||
|
"search": "Search...",
|
||||||
|
"noResults": "No results",
|
||||||
|
"close": "Close",
|
||||||
|
"error": "Error",
|
||||||
|
"success": "Success",
|
||||||
|
"warning": "Warning",
|
||||||
|
"info": "Info",
|
||||||
|
"page": "Page",
|
||||||
|
"of": "of",
|
||||||
|
"previous": "Previous",
|
||||||
|
"next": "Next"
|
||||||
|
},
|
||||||
|
"theme": {
|
||||||
|
"light": "Light",
|
||||||
|
"dark": "Dark",
|
||||||
|
"toggle": "Toggle theme"
|
||||||
|
},
|
||||||
|
"language": {
|
||||||
|
"switch": "Switch language",
|
||||||
|
"it": "Italian",
|
||||||
|
"en": "English"
|
||||||
|
},
|
||||||
|
"auth": {
|
||||||
|
"login": "Sign in to your account",
|
||||||
|
"username": "Username",
|
||||||
|
"password": "Password",
|
||||||
|
"signIn": "Sign In",
|
||||||
|
"invalidCredentials": "Invalid credentials",
|
||||||
|
"enterCredentials": "Enter username and password",
|
||||||
|
"forgotPassword": "Forgot password? Contact administrator",
|
||||||
|
"welcome": "Welcome, {name}!",
|
||||||
|
"logoutSuccess": "Logged out successfully",
|
||||||
|
"loginRequired": "Please log in to continue"
|
||||||
|
},
|
||||||
|
"nav": {
|
||||||
|
"measurements": "Measurements",
|
||||||
|
"recipes": "Recipes",
|
||||||
|
"statistics": "Statistics",
|
||||||
|
"admin": "Admin",
|
||||||
|
"users": "Users",
|
||||||
|
"settings": "Settings",
|
||||||
|
"profile": "Profile",
|
||||||
|
"logout": "Logout"
|
||||||
|
},
|
||||||
|
"profile": {
|
||||||
|
"title": "User Profile",
|
||||||
|
"displayName": "Display Name",
|
||||||
|
"language": "Language",
|
||||||
|
"theme": "Theme",
|
||||||
|
"roles": "Roles",
|
||||||
|
"saveChanges": "Save Changes",
|
||||||
|
"updateSuccess": "Profile updated successfully"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
{
|
||||||
|
"numpad": {
|
||||||
|
"title": "Tastierino Numerico",
|
||||||
|
"clear": "Cancella",
|
||||||
|
"submit": "Conferma",
|
||||||
|
"decimal": "Virgola",
|
||||||
|
"close": "Chiudi"
|
||||||
|
},
|
||||||
|
"measurement": {
|
||||||
|
"pass": "Conforme",
|
||||||
|
"warning": "Attenzione",
|
||||||
|
"fail": "Non Conforme",
|
||||||
|
"value": "Valore",
|
||||||
|
"next": "Prossima misura"
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"loading": "Caricamento...",
|
||||||
|
"save": "Salva",
|
||||||
|
"cancel": "Annulla",
|
||||||
|
"confirm": "Conferma",
|
||||||
|
"delete": "Elimina",
|
||||||
|
"edit": "Modifica",
|
||||||
|
"search": "Cerca...",
|
||||||
|
"noResults": "Nessun risultato",
|
||||||
|
"close": "Chiudi",
|
||||||
|
"error": "Errore",
|
||||||
|
"success": "Successo",
|
||||||
|
"warning": "Attenzione",
|
||||||
|
"info": "Info",
|
||||||
|
"page": "Pagina",
|
||||||
|
"of": "di",
|
||||||
|
"previous": "Precedente",
|
||||||
|
"next": "Successivo"
|
||||||
|
},
|
||||||
|
"theme": {
|
||||||
|
"light": "Chiaro",
|
||||||
|
"dark": "Scuro",
|
||||||
|
"toggle": "Cambia tema"
|
||||||
|
},
|
||||||
|
"language": {
|
||||||
|
"switch": "Cambia lingua",
|
||||||
|
"it": "Italiano",
|
||||||
|
"en": "English"
|
||||||
|
},
|
||||||
|
"auth": {
|
||||||
|
"login": "Accedi al sistema",
|
||||||
|
"username": "Username",
|
||||||
|
"password": "Password",
|
||||||
|
"signIn": "Accedi",
|
||||||
|
"invalidCredentials": "Credenziali non valide",
|
||||||
|
"enterCredentials": "Inserisci username e password",
|
||||||
|
"forgotPassword": "Hai dimenticato la password? Contatta l'amministratore",
|
||||||
|
"welcome": "Benvenuto, {name}!",
|
||||||
|
"logoutSuccess": "Logout effettuato",
|
||||||
|
"loginRequired": "Effettua il login per continuare"
|
||||||
|
},
|
||||||
|
"nav": {
|
||||||
|
"measurements": "Misure",
|
||||||
|
"recipes": "Ricette",
|
||||||
|
"statistics": "Statistiche",
|
||||||
|
"admin": "Admin",
|
||||||
|
"users": "Utenti",
|
||||||
|
"settings": "Impostazioni",
|
||||||
|
"profile": "Profilo",
|
||||||
|
"logout": "Esci"
|
||||||
|
},
|
||||||
|
"profile": {
|
||||||
|
"title": "Profilo Utente",
|
||||||
|
"displayName": "Nome Visualizzato",
|
||||||
|
"language": "Lingua",
|
||||||
|
"theme": "Tema",
|
||||||
|
"roles": "Ruoli",
|
||||||
|
"saveChanges": "Salva Modifiche",
|
||||||
|
"updateSuccess": "Profilo aggiornato con successo"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Login — TieMeasureFlow{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-900 dark:to-slate-800 px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="max-w-md w-full space-y-8">
|
||||||
|
<!-- Card -->
|
||||||
|
<div class="bg-white dark:bg-slate-800 shadow-lg rounded-xl p-8">
|
||||||
|
<!-- Logo -->
|
||||||
|
<div class="text-center mb-8">
|
||||||
|
<img src="{{ url_for('static', filename='img/tmflow-logo.svg') }}"
|
||||||
|
alt="TieMeasureFlow Logo"
|
||||||
|
class="mx-auto h-16 w-auto mb-4"
|
||||||
|
onerror="this.style.display='none'">
|
||||||
|
<h2 class="text-3xl font-bold text-slate-900 dark:text-white mb-2">
|
||||||
|
TieMeasureFlow
|
||||||
|
</h2>
|
||||||
|
<p class="text-sm text-slate-600 dark:text-slate-400">
|
||||||
|
{{ _('Accedi al sistema') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Login Form -->
|
||||||
|
<form method="POST" action="{{ url_for('auth.login') }}" class="space-y-6">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
|
<!-- Username -->
|
||||||
|
<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"
|
||||||
|
name="username"
|
||||||
|
required
|
||||||
|
autofocus
|
||||||
|
placeholder="{{ _('Username') }}"
|
||||||
|
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>
|
||||||
|
|
||||||
|
<!-- Password -->
|
||||||
|
<div>
|
||||||
|
<label for="password" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
|
||||||
|
{{ _('Password') }}
|
||||||
|
</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="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<input type="password"
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
required
|
||||||
|
placeholder="{{ _('Password') }}"
|
||||||
|
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>
|
||||||
|
|
||||||
|
<!-- Submit Button -->
|
||||||
|
<div>
|
||||||
|
<button type="submit"
|
||||||
|
class="w-full flex justify-center py-3 px-4 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-5 w-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1" />
|
||||||
|
</svg>
|
||||||
|
{{ _('Accedi') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Help Text -->
|
||||||
|
<div class="mt-6 text-center">
|
||||||
|
<p class="text-xs text-slate-500 dark:text-slate-400">
|
||||||
|
{{ _('Hai dimenticato la password?') }}
|
||||||
|
<br>
|
||||||
|
{{ _('Contatta l\'amministratore') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<div class="text-center text-xs text-slate-500 dark:text-slate-400">
|
||||||
|
<p>TieMeasureFlow © 2025 - {{ _('Sistema di misurazione industriale') }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -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 %}
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="{{ current_language|default('it') }}"
|
||||||
|
:class="{ 'dark': $store.theme.dark }"
|
||||||
|
class="h-full">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="description" content="TieMeasureFlow - Sistema di gestione misurazioni industriali">
|
||||||
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||||
|
|
||||||
|
<title>{% block title %}TieMeasureFlow{% endblock %}</title>
|
||||||
|
|
||||||
|
<!-- Favicon -->
|
||||||
|
<link rel="icon" type="image/svg+xml" href="{{ url_for('static', filename='img/tmflow-icon.svg') }}">
|
||||||
|
|
||||||
|
<!-- Google Fonts: Inter + JetBrains Mono -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- TailwindCSS CDN -->
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<script>
|
||||||
|
tailwind.config = {
|
||||||
|
darkMode: 'class',
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
primary: {
|
||||||
|
DEFAULT: '#2563EB',
|
||||||
|
dark: '#1E40AF',
|
||||||
|
light: '#3B82F6',
|
||||||
|
50: '#EFF6FF',
|
||||||
|
100: '#DBEAFE',
|
||||||
|
200: '#BFDBFE',
|
||||||
|
500: '#3B82F6',
|
||||||
|
600: '#2563EB',
|
||||||
|
700: '#1D4ED8',
|
||||||
|
800: '#1E40AF',
|
||||||
|
900: '#1E3A8A',
|
||||||
|
},
|
||||||
|
steel: {
|
||||||
|
DEFAULT: '#64748B',
|
||||||
|
light: '#94A3B8',
|
||||||
|
dark: '#475569',
|
||||||
|
50: '#F8FAFC',
|
||||||
|
100: '#F1F5F9',
|
||||||
|
200: '#E2E8F0',
|
||||||
|
300: '#CBD5E1',
|
||||||
|
400: '#94A3B8',
|
||||||
|
500: '#64748B',
|
||||||
|
600: '#475569',
|
||||||
|
700: '#334155',
|
||||||
|
800: '#1E293B',
|
||||||
|
900: '#0F172A',
|
||||||
|
},
|
||||||
|
measure: {
|
||||||
|
pass: '#059669',
|
||||||
|
warning: '#D97706',
|
||||||
|
fail: '#DC2626',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fontFamily: {
|
||||||
|
sans: ['Inter', 'system-ui', '-apple-system', 'sans-serif'],
|
||||||
|
mono: ['JetBrains Mono', 'ui-monospace', 'monospace'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Theme CSS Variables -->
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/themes.css') }}">
|
||||||
|
|
||||||
|
<!-- Alpine.js Theme Init (before Alpine loads) -->
|
||||||
|
<script src="{{ url_for('static', filename='js/alpine-init.js') }}"></script>
|
||||||
|
|
||||||
|
{% block extra_head %}{% endblock %}
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="h-full bg-[var(--bg-primary)] text-[var(--text-primary)] font-sans antialiased transition-colors duration-300">
|
||||||
|
|
||||||
|
<!-- Alpine.js App Wrapper -->
|
||||||
|
<div x-data class="min-h-full flex flex-col">
|
||||||
|
|
||||||
|
<!-- Navbar -->
|
||||||
|
{% include "components/navbar.html" %}
|
||||||
|
|
||||||
|
<!-- Flash Messages -->
|
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% if messages %}
|
||||||
|
<div class="fixed top-16 right-4 z-50 flex flex-col gap-2 max-w-md w-full"
|
||||||
|
x-data="{ messages: {{ messages|tojson }} }"
|
||||||
|
x-init="setTimeout(() => { $el.remove() }, 8000)">
|
||||||
|
{% for category, message in messages %}
|
||||||
|
<div x-data="{ show: true }"
|
||||||
|
x-show="show"
|
||||||
|
x-transition:enter="transition ease-out duration-300"
|
||||||
|
x-transition:enter-start="opacity-0 translate-x-4"
|
||||||
|
x-transition:enter-end="opacity-100 translate-x-0"
|
||||||
|
x-transition:leave="transition ease-in duration-200"
|
||||||
|
x-transition:leave-start="opacity-100 translate-x-0"
|
||||||
|
x-transition:leave-end="opacity-0 translate-x-4"
|
||||||
|
class="flex items-center gap-3 px-4 py-3 rounded-lg shadow-lg border
|
||||||
|
{% if category == 'success' %}
|
||||||
|
bg-emerald-50 dark:bg-emerald-900/30 border-emerald-200 dark:border-emerald-800 text-emerald-800 dark:text-emerald-200
|
||||||
|
{% elif category == 'error' %}
|
||||||
|
bg-red-50 dark:bg-red-900/30 border-red-200 dark:border-red-800 text-red-800 dark:text-red-200
|
||||||
|
{% elif category == 'warning' %}
|
||||||
|
bg-amber-50 dark:bg-amber-900/30 border-amber-200 dark:border-amber-800 text-amber-800 dark:text-amber-200
|
||||||
|
{% else %}
|
||||||
|
bg-blue-50 dark:bg-blue-900/30 border-blue-200 dark:border-blue-800 text-blue-800 dark:text-blue-200
|
||||||
|
{% endif %}">
|
||||||
|
|
||||||
|
<!-- Icon -->
|
||||||
|
{% if category == 'success' %}
|
||||||
|
<svg class="w-5 h-5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||||
|
</svg>
|
||||||
|
{% elif category == 'error' %}
|
||||||
|
<svg class="w-5 h-5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||||
|
</svg>
|
||||||
|
{% elif category == 'warning' %}
|
||||||
|
<svg class="w-5 h-5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4.5c-.77-.833-2.694-.833-3.464 0L3.34 16.5c-.77.833.192 2.5 1.732 2.5z"/>
|
||||||
|
</svg>
|
||||||
|
{% else %}
|
||||||
|
<svg class="w-5 h-5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||||
|
</svg>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<span class="text-sm font-medium flex-1">{{ message }}</span>
|
||||||
|
|
||||||
|
<!-- Dismiss -->
|
||||||
|
<button @click="show = false" class="shrink-0 hover:opacity-70 transition-opacity">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<main class="flex-1">
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer class="py-4 text-center border-t border-[var(--border-color)] transition-colors duration-300">
|
||||||
|
<p class="text-xs text-steel dark:text-steel-light tracking-wide">
|
||||||
|
Powered by <span class="font-semibold">TieMeasureFlow</span> — Tielogic
|
||||||
|
</p>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Alpine.js CDN (defer) -->
|
||||||
|
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||||
|
|
||||||
|
{% block extra_js %}{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,324 @@
|
|||||||
|
<!-- TieMeasureFlow Navbar -->
|
||||||
|
<nav class="sticky top-0 z-40 bg-[var(--bg-card)] border-b border-[var(--border-color)] shadow-sm transition-colors duration-300"
|
||||||
|
x-data="{ mobileOpen: false, userDropdown: false, adminDropdown: false }">
|
||||||
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="flex items-center justify-between h-14">
|
||||||
|
|
||||||
|
<!-- Left: Logo -->
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<a href="{{ url_for('index') }}" class="flex items-center gap-2.5 group">
|
||||||
|
{% if company_logo %}
|
||||||
|
<img src="{{ url_for('static', filename='img/' ~ company_logo) }}" alt="Logo" class="h-8 w-auto"
|
||||||
|
onerror="this.src='{{ url_for('static', filename='img/tmflow-logo.svg') }}'">
|
||||||
|
{% else %}
|
||||||
|
<img src="{{ url_for('static', filename='img/tmflow-logo.svg') }}"
|
||||||
|
alt="TieMeasureFlow"
|
||||||
|
class="h-8 w-auto">
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Center: Navigation Links (Desktop) -->
|
||||||
|
{% if current_user %}
|
||||||
|
<div class="hidden md:flex items-center gap-1">
|
||||||
|
|
||||||
|
{# MeasurementTec: Misure #}
|
||||||
|
{% if current_user.get('roles') and 'MeasurementTec' in current_user.roles %}
|
||||||
|
<a href="{{ url_for('measure.select_recipe') }}"
|
||||||
|
class="nav-link group flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium
|
||||||
|
text-[var(--text-secondary)] hover:text-primary hover:bg-primary-50 dark:hover:bg-primary-900/20
|
||||||
|
transition-colors duration-200
|
||||||
|
{% if request.endpoint and request.endpoint.startswith('measure.') %}
|
||||||
|
text-primary bg-primary-50 dark:bg-primary-900/20
|
||||||
|
{% endif %}">
|
||||||
|
<!-- Clipboard Icon -->
|
||||||
|
<svg class="w-4.5 h-4.5" fill="none" stroke="currentColor" stroke-width="1.75" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"/>
|
||||||
|
</svg>
|
||||||
|
<span>Misure</span>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{# Maker: Ricette #}
|
||||||
|
{% if current_user.get('roles') and 'Maker' in current_user.roles %}
|
||||||
|
<a href="{{ url_for('maker.recipe_list') }}"
|
||||||
|
class="nav-link group flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium
|
||||||
|
text-[var(--text-secondary)] hover:text-primary hover:bg-primary-50 dark:hover:bg-primary-900/20
|
||||||
|
transition-colors duration-200
|
||||||
|
{% if request.endpoint and request.endpoint.startswith('maker.') %}
|
||||||
|
text-primary bg-primary-50 dark:bg-primary-900/20
|
||||||
|
{% endif %}">
|
||||||
|
<!-- Edit/Pencil Icon -->
|
||||||
|
<svg class="w-4.5 h-4.5" fill="none" stroke="currentColor" stroke-width="1.75" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
|
||||||
|
</svg>
|
||||||
|
<span>Ricette</span>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{# Metrologist: Statistiche #}
|
||||||
|
{% if current_user.get('roles') and 'Metrologist' in current_user.roles %}
|
||||||
|
<a href="{{ url_for('statistics.dashboard') }}"
|
||||||
|
class="nav-link group flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium
|
||||||
|
text-[var(--text-secondary)] hover:text-primary hover:bg-primary-50 dark:hover:bg-primary-900/20
|
||||||
|
transition-colors duration-200
|
||||||
|
{% if request.endpoint and request.endpoint.startswith('statistics.') %}
|
||||||
|
text-primary bg-primary-50 dark:bg-primary-900/20
|
||||||
|
{% endif %}">
|
||||||
|
<!-- Chart Icon -->
|
||||||
|
<svg class="w-4.5 h-4.5" fill="none" stroke="currentColor" stroke-width="1.75" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
|
||||||
|
</svg>
|
||||||
|
<span>Statistiche</span>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{# Admin: dropdown #}
|
||||||
|
{% if current_user.get('is_admin') %}
|
||||||
|
<div class="relative" x-data="{ open: false }" @click.outside="open = false">
|
||||||
|
<button @click="open = !open"
|
||||||
|
class="nav-link group flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium
|
||||||
|
text-[var(--text-secondary)] hover:text-primary hover:bg-primary-50 dark:hover:bg-primary-900/20
|
||||||
|
transition-colors duration-200"
|
||||||
|
:class="{ 'text-primary bg-primary-50 dark:bg-primary-900/20': open }">
|
||||||
|
<!-- Cog Icon -->
|
||||||
|
<svg class="w-4.5 h-4.5" fill="none" stroke="currentColor" stroke-width="1.75" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||||
|
</svg>
|
||||||
|
<span>Admin</span>
|
||||||
|
<svg class="w-3.5 h-3.5 transition-transform duration-200" :class="{ 'rotate-180': open }"
|
||||||
|
fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Admin Dropdown -->
|
||||||
|
<div x-show="open"
|
||||||
|
x-transition:enter="transition ease-out duration-150"
|
||||||
|
x-transition:enter-start="opacity-0 scale-95 -translate-y-1"
|
||||||
|
x-transition:enter-end="opacity-100 scale-100 translate-y-0"
|
||||||
|
x-transition:leave="transition ease-in duration-100"
|
||||||
|
x-transition:leave-start="opacity-100 scale-100"
|
||||||
|
x-transition:leave-end="opacity-0 scale-95"
|
||||||
|
class="absolute left-0 mt-1 w-48 rounded-lg bg-[var(--bg-card)] border border-[var(--border-color)]
|
||||||
|
shadow-lg py-1 z-50">
|
||||||
|
<a href="#" class="flex items-center gap-2.5 px-4 py-2.5 text-sm text-[var(--text-secondary)]
|
||||||
|
hover:text-primary hover:bg-primary-50 dark:hover:bg-primary-900/20 transition-colors">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="1.75" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"/>
|
||||||
|
</svg>
|
||||||
|
Utenti
|
||||||
|
</a>
|
||||||
|
<a href="#" class="flex items-center gap-2.5 px-4 py-2.5 text-sm text-[var(--text-secondary)]
|
||||||
|
hover:text-primary hover:bg-primary-50 dark:hover:bg-primary-900/20 transition-colors">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="1.75" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"/>
|
||||||
|
</svg>
|
||||||
|
Impostazioni
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Right: Controls -->
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
|
||||||
|
<!-- Language Toggle -->
|
||||||
|
<div class="flex items-center border border-[var(--border-color)] rounded-lg overflow-hidden">
|
||||||
|
<a href="{{ url_for('set_language', lang='it') }}"
|
||||||
|
class="px-2 py-1.5 text-xs font-semibold transition-colors duration-200
|
||||||
|
{% if current_language == 'it' %}
|
||||||
|
bg-primary text-white
|
||||||
|
{% else %}
|
||||||
|
text-[var(--text-secondary)] hover:bg-[var(--bg-secondary)]
|
||||||
|
{% endif %}">
|
||||||
|
IT
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('set_language', lang='en') }}"
|
||||||
|
class="px-2 py-1.5 text-xs font-semibold transition-colors duration-200
|
||||||
|
{% if current_language == 'en' %}
|
||||||
|
bg-primary text-white
|
||||||
|
{% else %}
|
||||||
|
text-[var(--text-secondary)] hover:bg-[var(--bg-secondary)]
|
||||||
|
{% endif %}">
|
||||||
|
EN
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Theme Toggle -->
|
||||||
|
<button @click="$store.theme.toggle()"
|
||||||
|
class="p-2 rounded-lg text-[var(--text-secondary)] hover:text-primary
|
||||||
|
hover:bg-[var(--bg-secondary)] transition-colors duration-200"
|
||||||
|
:title="$store.theme.dark ? 'Tema chiaro' : 'Tema scuro'">
|
||||||
|
<!-- Sun (shown in dark mode) -->
|
||||||
|
<svg x-show="$store.theme.dark" x-cloak class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="1.75" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"/>
|
||||||
|
</svg>
|
||||||
|
<!-- Moon (shown in light mode) -->
|
||||||
|
<svg x-show="!$store.theme.dark" class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="1.75" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- User Menu (when logged in) -->
|
||||||
|
{% if current_user %}
|
||||||
|
<div class="relative" x-data="{ open: false }" @click.outside="open = false">
|
||||||
|
<button @click="open = !open"
|
||||||
|
class="flex items-center gap-2 pl-2 pr-3 py-1.5 rounded-lg
|
||||||
|
hover:bg-[var(--bg-secondary)] transition-colors duration-200">
|
||||||
|
<!-- Avatar -->
|
||||||
|
<div class="w-7 h-7 rounded-full bg-primary/10 border border-primary/20
|
||||||
|
flex items-center justify-center text-primary font-semibold text-xs">
|
||||||
|
{{ current_user.get('display_name', current_user.get('username', '?'))[0]|upper }}
|
||||||
|
</div>
|
||||||
|
<span class="hidden sm:block text-sm font-medium text-[var(--text-primary)] max-w-[120px] truncate">
|
||||||
|
{{ current_user.get('display_name', current_user.get('username', '')) }}
|
||||||
|
</span>
|
||||||
|
<svg class="w-3.5 h-3.5 text-[var(--text-secondary)] transition-transform duration-200"
|
||||||
|
:class="{ 'rotate-180': open }"
|
||||||
|
fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- User Dropdown -->
|
||||||
|
<div x-show="open"
|
||||||
|
x-transition:enter="transition ease-out duration-150"
|
||||||
|
x-transition:enter-start="opacity-0 scale-95 -translate-y-1"
|
||||||
|
x-transition:enter-end="opacity-100 scale-100 translate-y-0"
|
||||||
|
x-transition:leave="transition ease-in duration-100"
|
||||||
|
x-transition:leave-start="opacity-100 scale-100"
|
||||||
|
x-transition:leave-end="opacity-0 scale-95"
|
||||||
|
class="absolute right-0 mt-1 w-52 rounded-lg bg-[var(--bg-card)] border border-[var(--border-color)]
|
||||||
|
shadow-lg py-1 z-50">
|
||||||
|
<!-- User Info Header -->
|
||||||
|
<div class="px-4 py-2.5 border-b border-[var(--border-color)]">
|
||||||
|
<p class="text-sm font-semibold text-[var(--text-primary)] truncate">
|
||||||
|
{{ current_user.get('display_name', current_user.get('username', '')) }}
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-[var(--text-secondary)] mt-0.5">
|
||||||
|
{{ current_user.get('roles', [])|join(', ') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="{{ url_for('auth.profile') }}"
|
||||||
|
class="flex items-center gap-2.5 px-4 py-2.5 text-sm text-[var(--text-secondary)]
|
||||||
|
hover:text-primary hover:bg-primary-50 dark:hover:bg-primary-900/20 transition-colors">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="1.75" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" 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>
|
||||||
|
Profilo
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="border-t border-[var(--border-color)] my-1"></div>
|
||||||
|
|
||||||
|
<a href="{{ url_for('auth.logout') }}"
|
||||||
|
class="flex items-center gap-2.5 px-4 py-2.5 text-sm text-measure-fail
|
||||||
|
hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="1.75" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" 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>
|
||||||
|
Logout
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Mobile Hamburger (when logged in) -->
|
||||||
|
{% if current_user %}
|
||||||
|
<button @click="mobileOpen = !mobileOpen"
|
||||||
|
class="md:hidden p-2 rounded-lg text-[var(--text-secondary)]
|
||||||
|
hover:bg-[var(--bg-secondary)] transition-colors duration-200">
|
||||||
|
<svg x-show="!mobileOpen" class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="1.75" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h16M4 18h16"/>
|
||||||
|
</svg>
|
||||||
|
<svg x-show="mobileOpen" x-cloak class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="1.75" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mobile Menu -->
|
||||||
|
{% if current_user %}
|
||||||
|
<div x-show="mobileOpen"
|
||||||
|
x-transition:enter="transition ease-out duration-200"
|
||||||
|
x-transition:enter-start="opacity-0 -translate-y-2"
|
||||||
|
x-transition:enter-end="opacity-100 translate-y-0"
|
||||||
|
x-transition:leave="transition ease-in duration-150"
|
||||||
|
x-transition:leave-start="opacity-100 translate-y-0"
|
||||||
|
x-transition:leave-end="opacity-0 -translate-y-2"
|
||||||
|
x-cloak
|
||||||
|
class="md:hidden border-t border-[var(--border-color)] bg-[var(--bg-card)]">
|
||||||
|
<div class="px-4 py-3 space-y-1">
|
||||||
|
|
||||||
|
{% if current_user.get('roles') and 'MeasurementTec' in current_user.roles %}
|
||||||
|
<a href="{{ url_for('measure.select_recipe') }}"
|
||||||
|
class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium
|
||||||
|
text-[var(--text-secondary)] hover:text-primary hover:bg-primary-50 dark:hover:bg-primary-900/20
|
||||||
|
transition-colors">
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="1.75" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"/>
|
||||||
|
</svg>
|
||||||
|
Misure
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if current_user.get('roles') and 'Maker' in current_user.roles %}
|
||||||
|
<a href="{{ url_for('maker.recipe_list') }}"
|
||||||
|
class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium
|
||||||
|
text-[var(--text-secondary)] hover:text-primary hover:bg-primary-50 dark:hover:bg-primary-900/20
|
||||||
|
transition-colors">
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="1.75" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
|
||||||
|
</svg>
|
||||||
|
Ricette
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if current_user.get('roles') and 'Metrologist' in current_user.roles %}
|
||||||
|
<a href="{{ url_for('statistics.dashboard') }}"
|
||||||
|
class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium
|
||||||
|
text-[var(--text-secondary)] hover:text-primary hover:bg-primary-50 dark:hover:bg-primary-900/20
|
||||||
|
transition-colors">
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="1.75" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
|
||||||
|
</svg>
|
||||||
|
Statistiche
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if current_user.get('is_admin') %}
|
||||||
|
<div class="border-t border-[var(--border-color)] my-2"></div>
|
||||||
|
<p class="px-3 py-1 text-xs font-semibold text-[var(--text-secondary)] uppercase tracking-wider">Admin</p>
|
||||||
|
<a href="#"
|
||||||
|
class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium
|
||||||
|
text-[var(--text-secondary)] hover:text-primary hover:bg-primary-50 dark:hover:bg-primary-900/20
|
||||||
|
transition-colors">
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="1.75" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"/>
|
||||||
|
</svg>
|
||||||
|
Utenti
|
||||||
|
</a>
|
||||||
|
<a href="#"
|
||||||
|
class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium
|
||||||
|
text-[var(--text-secondary)] hover:text-primary hover:bg-primary-50 dark:hover:bg-primary-900/20
|
||||||
|
transition-colors">
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="1.75" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"/>
|
||||||
|
</svg>
|
||||||
|
Impostazioni
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</nav>
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
[python: *.py]
|
||||||
[python: blueprints/**.py]
|
[python: blueprints/**.py]
|
||||||
|
[python: services/**.py]
|
||||||
[jinja2: templates/**.html]
|
[jinja2: templates/**.html]
|
||||||
encoding = utf-8
|
encoding = utf-8
|
||||||
|
|||||||
@@ -0,0 +1,247 @@
|
|||||||
|
# English translations for TieMeasureFlow
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: TieMeasureFlow 1.0\n"
|
||||||
|
"Language: en\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
|
|
||||||
|
# Login Page
|
||||||
|
msgid "Accedi al sistema"
|
||||||
|
msgstr "Sign in to your account"
|
||||||
|
|
||||||
|
msgid "Username"
|
||||||
|
msgstr "Username"
|
||||||
|
|
||||||
|
msgid "Password"
|
||||||
|
msgstr "Password"
|
||||||
|
|
||||||
|
msgid "Accedi"
|
||||||
|
msgstr "Sign In"
|
||||||
|
|
||||||
|
msgid "Credenziali non valide"
|
||||||
|
msgstr "Invalid credentials"
|
||||||
|
|
||||||
|
msgid "Inserisci username e password"
|
||||||
|
msgstr "Enter username and password"
|
||||||
|
|
||||||
|
msgid "Hai dimenticato la password?"
|
||||||
|
msgstr "Forgot password?"
|
||||||
|
|
||||||
|
msgid "Contatta l'amministratore"
|
||||||
|
msgstr "Contact administrator"
|
||||||
|
|
||||||
|
# Navbar
|
||||||
|
msgid "Misure"
|
||||||
|
msgstr "Measurements"
|
||||||
|
|
||||||
|
msgid "Ricette"
|
||||||
|
msgstr "Recipes"
|
||||||
|
|
||||||
|
msgid "Statistiche"
|
||||||
|
msgstr "Statistics"
|
||||||
|
|
||||||
|
msgid "Admin"
|
||||||
|
msgstr "Admin"
|
||||||
|
|
||||||
|
msgid "Utenti"
|
||||||
|
msgstr "Users"
|
||||||
|
|
||||||
|
msgid "Impostazioni"
|
||||||
|
msgstr "Settings"
|
||||||
|
|
||||||
|
msgid "Profilo"
|
||||||
|
msgstr "Profile"
|
||||||
|
|
||||||
|
msgid "Esci"
|
||||||
|
msgstr "Logout"
|
||||||
|
|
||||||
|
# Profile Page
|
||||||
|
msgid "Profilo Utente"
|
||||||
|
msgstr "User Profile"
|
||||||
|
|
||||||
|
msgid "Nome Visualizzato"
|
||||||
|
msgstr "Display Name"
|
||||||
|
|
||||||
|
msgid "Lingua"
|
||||||
|
msgstr "Language"
|
||||||
|
|
||||||
|
msgid "Tema"
|
||||||
|
msgstr "Theme"
|
||||||
|
|
||||||
|
msgid "Chiaro"
|
||||||
|
msgstr "Light"
|
||||||
|
|
||||||
|
msgid "Scuro"
|
||||||
|
msgstr "Dark"
|
||||||
|
|
||||||
|
msgid "Ruoli"
|
||||||
|
msgstr "Roles"
|
||||||
|
|
||||||
|
msgid "Salva Modifiche"
|
||||||
|
msgstr "Save Changes"
|
||||||
|
|
||||||
|
msgid "Profilo aggiornato con successo"
|
||||||
|
msgstr "Profile updated successfully"
|
||||||
|
|
||||||
|
# Flash Messages
|
||||||
|
msgid "Benvenuto, %(name)s!"
|
||||||
|
msgstr "Welcome, %(name)s!"
|
||||||
|
|
||||||
|
msgid "Logout effettuato"
|
||||||
|
msgstr "Logged out successfully"
|
||||||
|
|
||||||
|
msgid "Effettua il login per continuare"
|
||||||
|
msgstr "Please log in to continue"
|
||||||
|
|
||||||
|
msgid "Errore di connessione al server"
|
||||||
|
msgstr "Server connection error"
|
||||||
|
|
||||||
|
# Common UI
|
||||||
|
msgid "Caricamento..."
|
||||||
|
msgstr "Loading..."
|
||||||
|
|
||||||
|
msgid "Salva"
|
||||||
|
msgstr "Save"
|
||||||
|
|
||||||
|
msgid "Annulla"
|
||||||
|
msgstr "Cancel"
|
||||||
|
|
||||||
|
msgid "Conferma"
|
||||||
|
msgstr "Confirm"
|
||||||
|
|
||||||
|
msgid "Elimina"
|
||||||
|
msgstr "Delete"
|
||||||
|
|
||||||
|
msgid "Modifica"
|
||||||
|
msgstr "Edit"
|
||||||
|
|
||||||
|
msgid "Cerca..."
|
||||||
|
msgstr "Search..."
|
||||||
|
|
||||||
|
msgid "Nessun risultato"
|
||||||
|
msgstr "No results"
|
||||||
|
|
||||||
|
msgid "Errore"
|
||||||
|
msgstr "Error"
|
||||||
|
|
||||||
|
msgid "Successo"
|
||||||
|
msgstr "Success"
|
||||||
|
|
||||||
|
msgid "Attenzione"
|
||||||
|
msgstr "Warning"
|
||||||
|
|
||||||
|
msgid "Info"
|
||||||
|
msgstr "Info"
|
||||||
|
|
||||||
|
msgid "Pagina"
|
||||||
|
msgstr "Page"
|
||||||
|
|
||||||
|
msgid "di"
|
||||||
|
msgstr "of"
|
||||||
|
|
||||||
|
msgid "Precedente"
|
||||||
|
msgstr "Previous"
|
||||||
|
|
||||||
|
msgid "Successivo"
|
||||||
|
msgstr "Next"
|
||||||
|
|
||||||
|
# Measurements
|
||||||
|
msgid "Conforme"
|
||||||
|
msgstr "Pass"
|
||||||
|
|
||||||
|
msgid "Non Conforme"
|
||||||
|
msgstr "Fail"
|
||||||
|
|
||||||
|
msgid "Valore"
|
||||||
|
msgstr "Value"
|
||||||
|
|
||||||
|
msgid "Prossima misura"
|
||||||
|
msgstr "Next measurement"
|
||||||
|
|
||||||
|
# Numpad
|
||||||
|
msgid "Tastierino Numerico"
|
||||||
|
msgstr "Numeric Keypad"
|
||||||
|
|
||||||
|
msgid "Cancella"
|
||||||
|
msgstr "Clear"
|
||||||
|
|
||||||
|
msgid "Virgola"
|
||||||
|
msgstr "Decimal"
|
||||||
|
|
||||||
|
msgid "Chiudi"
|
||||||
|
msgstr "Close"
|
||||||
|
|
||||||
|
# Theme
|
||||||
|
msgid "Cambia tema"
|
||||||
|
msgstr "Toggle theme"
|
||||||
|
|
||||||
|
# Language
|
||||||
|
msgid "Cambia lingua"
|
||||||
|
msgstr "Switch language"
|
||||||
|
|
||||||
|
msgid "Italiano"
|
||||||
|
msgstr "Italian"
|
||||||
|
|
||||||
|
msgid "English"
|
||||||
|
msgstr "English"
|
||||||
|
|
||||||
|
# Additional Login Page
|
||||||
|
msgid "Sistema di misurazione industriale"
|
||||||
|
msgstr "Industrial measurement system"
|
||||||
|
|
||||||
|
# Additional Profile Page
|
||||||
|
msgid "Gestisci le tue informazioni e preferenze"
|
||||||
|
msgstr "Manage your information and preferences"
|
||||||
|
|
||||||
|
msgid "Informazioni Account"
|
||||||
|
msgstr "Account Information"
|
||||||
|
|
||||||
|
msgid "Il nome utente non può essere modificato"
|
||||||
|
msgstr "Username cannot be changed"
|
||||||
|
|
||||||
|
msgid "Nome da visualizzare nell'interfaccia"
|
||||||
|
msgstr "Name to display in the interface"
|
||||||
|
|
||||||
|
msgid "Lingua Preferita"
|
||||||
|
msgstr "Preferred Language"
|
||||||
|
|
||||||
|
msgid "Tema Preferito"
|
||||||
|
msgstr "Preferred Theme"
|
||||||
|
|
||||||
|
msgid "Indietro"
|
||||||
|
msgstr "Back"
|
||||||
|
|
||||||
|
msgid "Azioni Account"
|
||||||
|
msgstr "Account Actions"
|
||||||
|
|
||||||
|
msgid "Esci dal sistema"
|
||||||
|
msgstr "Logout from system"
|
||||||
|
|
||||||
|
msgid "Termina la sessione corrente e torna alla schermata di login"
|
||||||
|
msgstr "End current session and return to login screen"
|
||||||
|
|
||||||
|
msgid "Preferenze"
|
||||||
|
msgstr "Preferences"
|
||||||
|
|
||||||
|
# Error Messages
|
||||||
|
msgid "Errore durante l'aggiornamento del profilo"
|
||||||
|
msgstr "Error updating profile"
|
||||||
|
|
||||||
|
msgid "Errore di connessione al server: %(error)s"
|
||||||
|
msgstr "Server connection error: %(error)s"
|
||||||
|
|
||||||
|
msgid "Errore nel caricamento del profilo: %(error)s"
|
||||||
|
msgstr "Error loading profile: %(error)s"
|
||||||
|
|
||||||
|
# Roles
|
||||||
|
msgid "Maker"
|
||||||
|
msgstr "Maker"
|
||||||
|
|
||||||
|
msgid "MeasurementTec"
|
||||||
|
msgstr "Measurement Technician"
|
||||||
|
|
||||||
|
msgid "Metrologist"
|
||||||
|
msgstr "Metrologist"
|
||||||
@@ -0,0 +1,247 @@
|
|||||||
|
# Italian translations for TieMeasureFlow
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: TieMeasureFlow 1.0\n"
|
||||||
|
"Language: it\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
|
|
||||||
|
# Login Page
|
||||||
|
msgid "Accedi al sistema"
|
||||||
|
msgstr "Accedi al sistema"
|
||||||
|
|
||||||
|
msgid "Username"
|
||||||
|
msgstr "Username"
|
||||||
|
|
||||||
|
msgid "Password"
|
||||||
|
msgstr "Password"
|
||||||
|
|
||||||
|
msgid "Accedi"
|
||||||
|
msgstr "Accedi"
|
||||||
|
|
||||||
|
msgid "Credenziali non valide"
|
||||||
|
msgstr "Credenziali non valide"
|
||||||
|
|
||||||
|
msgid "Inserisci username e password"
|
||||||
|
msgstr "Inserisci username e password"
|
||||||
|
|
||||||
|
msgid "Hai dimenticato la password?"
|
||||||
|
msgstr "Hai dimenticato la password?"
|
||||||
|
|
||||||
|
msgid "Contatta l'amministratore"
|
||||||
|
msgstr "Contatta l'amministratore"
|
||||||
|
|
||||||
|
# Navbar
|
||||||
|
msgid "Misure"
|
||||||
|
msgstr "Misure"
|
||||||
|
|
||||||
|
msgid "Ricette"
|
||||||
|
msgstr "Ricette"
|
||||||
|
|
||||||
|
msgid "Statistiche"
|
||||||
|
msgstr "Statistiche"
|
||||||
|
|
||||||
|
msgid "Admin"
|
||||||
|
msgstr "Admin"
|
||||||
|
|
||||||
|
msgid "Utenti"
|
||||||
|
msgstr "Utenti"
|
||||||
|
|
||||||
|
msgid "Impostazioni"
|
||||||
|
msgstr "Impostazioni"
|
||||||
|
|
||||||
|
msgid "Profilo"
|
||||||
|
msgstr "Profilo"
|
||||||
|
|
||||||
|
msgid "Esci"
|
||||||
|
msgstr "Esci"
|
||||||
|
|
||||||
|
# Profile Page
|
||||||
|
msgid "Profilo Utente"
|
||||||
|
msgstr "Profilo Utente"
|
||||||
|
|
||||||
|
msgid "Nome Visualizzato"
|
||||||
|
msgstr "Nome Visualizzato"
|
||||||
|
|
||||||
|
msgid "Lingua"
|
||||||
|
msgstr "Lingua"
|
||||||
|
|
||||||
|
msgid "Tema"
|
||||||
|
msgstr "Tema"
|
||||||
|
|
||||||
|
msgid "Chiaro"
|
||||||
|
msgstr "Chiaro"
|
||||||
|
|
||||||
|
msgid "Scuro"
|
||||||
|
msgstr "Scuro"
|
||||||
|
|
||||||
|
msgid "Ruoli"
|
||||||
|
msgstr "Ruoli"
|
||||||
|
|
||||||
|
msgid "Salva Modifiche"
|
||||||
|
msgstr "Salva Modifiche"
|
||||||
|
|
||||||
|
msgid "Profilo aggiornato con successo"
|
||||||
|
msgstr "Profilo aggiornato con successo"
|
||||||
|
|
||||||
|
# Flash Messages
|
||||||
|
msgid "Benvenuto, %(name)s!"
|
||||||
|
msgstr "Benvenuto, %(name)s!"
|
||||||
|
|
||||||
|
msgid "Logout effettuato"
|
||||||
|
msgstr "Logout effettuato"
|
||||||
|
|
||||||
|
msgid "Effettua il login per continuare"
|
||||||
|
msgstr "Effettua il login per continuare"
|
||||||
|
|
||||||
|
msgid "Errore di connessione al server"
|
||||||
|
msgstr "Errore di connessione al server"
|
||||||
|
|
||||||
|
# Common UI
|
||||||
|
msgid "Caricamento..."
|
||||||
|
msgstr "Caricamento..."
|
||||||
|
|
||||||
|
msgid "Salva"
|
||||||
|
msgstr "Salva"
|
||||||
|
|
||||||
|
msgid "Annulla"
|
||||||
|
msgstr "Annulla"
|
||||||
|
|
||||||
|
msgid "Conferma"
|
||||||
|
msgstr "Conferma"
|
||||||
|
|
||||||
|
msgid "Elimina"
|
||||||
|
msgstr "Elimina"
|
||||||
|
|
||||||
|
msgid "Modifica"
|
||||||
|
msgstr "Modifica"
|
||||||
|
|
||||||
|
msgid "Cerca..."
|
||||||
|
msgstr "Cerca..."
|
||||||
|
|
||||||
|
msgid "Nessun risultato"
|
||||||
|
msgstr "Nessun risultato"
|
||||||
|
|
||||||
|
msgid "Errore"
|
||||||
|
msgstr "Errore"
|
||||||
|
|
||||||
|
msgid "Successo"
|
||||||
|
msgstr "Successo"
|
||||||
|
|
||||||
|
msgid "Attenzione"
|
||||||
|
msgstr "Attenzione"
|
||||||
|
|
||||||
|
msgid "Info"
|
||||||
|
msgstr "Info"
|
||||||
|
|
||||||
|
msgid "Pagina"
|
||||||
|
msgstr "Pagina"
|
||||||
|
|
||||||
|
msgid "di"
|
||||||
|
msgstr "di"
|
||||||
|
|
||||||
|
msgid "Precedente"
|
||||||
|
msgstr "Precedente"
|
||||||
|
|
||||||
|
msgid "Successivo"
|
||||||
|
msgstr "Successivo"
|
||||||
|
|
||||||
|
# Measurements
|
||||||
|
msgid "Conforme"
|
||||||
|
msgstr "Conforme"
|
||||||
|
|
||||||
|
msgid "Non Conforme"
|
||||||
|
msgstr "Non Conforme"
|
||||||
|
|
||||||
|
msgid "Valore"
|
||||||
|
msgstr "Valore"
|
||||||
|
|
||||||
|
msgid "Prossima misura"
|
||||||
|
msgstr "Prossima misura"
|
||||||
|
|
||||||
|
# Numpad
|
||||||
|
msgid "Tastierino Numerico"
|
||||||
|
msgstr "Tastierino Numerico"
|
||||||
|
|
||||||
|
msgid "Cancella"
|
||||||
|
msgstr "Cancella"
|
||||||
|
|
||||||
|
msgid "Virgola"
|
||||||
|
msgstr "Virgola"
|
||||||
|
|
||||||
|
msgid "Chiudi"
|
||||||
|
msgstr "Chiudi"
|
||||||
|
|
||||||
|
# Theme
|
||||||
|
msgid "Cambia tema"
|
||||||
|
msgstr "Cambia tema"
|
||||||
|
|
||||||
|
# Language
|
||||||
|
msgid "Cambia lingua"
|
||||||
|
msgstr "Cambia lingua"
|
||||||
|
|
||||||
|
msgid "Italiano"
|
||||||
|
msgstr "Italiano"
|
||||||
|
|
||||||
|
msgid "English"
|
||||||
|
msgstr "English"
|
||||||
|
|
||||||
|
# Additional Login Page
|
||||||
|
msgid "Sistema di misurazione industriale"
|
||||||
|
msgstr "Sistema di misurazione industriale"
|
||||||
|
|
||||||
|
# Additional Profile Page
|
||||||
|
msgid "Gestisci le tue informazioni e preferenze"
|
||||||
|
msgstr "Gestisci le tue informazioni e preferenze"
|
||||||
|
|
||||||
|
msgid "Informazioni Account"
|
||||||
|
msgstr "Informazioni Account"
|
||||||
|
|
||||||
|
msgid "Il nome utente non può essere modificato"
|
||||||
|
msgstr "Il nome utente non può essere modificato"
|
||||||
|
|
||||||
|
msgid "Nome da visualizzare nell'interfaccia"
|
||||||
|
msgstr "Nome da visualizzare nell'interfaccia"
|
||||||
|
|
||||||
|
msgid "Lingua Preferita"
|
||||||
|
msgstr "Lingua Preferita"
|
||||||
|
|
||||||
|
msgid "Tema Preferito"
|
||||||
|
msgstr "Tema Preferito"
|
||||||
|
|
||||||
|
msgid "Indietro"
|
||||||
|
msgstr "Indietro"
|
||||||
|
|
||||||
|
msgid "Azioni Account"
|
||||||
|
msgstr "Azioni Account"
|
||||||
|
|
||||||
|
msgid "Esci dal sistema"
|
||||||
|
msgstr "Esci dal sistema"
|
||||||
|
|
||||||
|
msgid "Termina la sessione corrente e torna alla schermata di login"
|
||||||
|
msgstr "Termina la sessione corrente e torna alla schermata di login"
|
||||||
|
|
||||||
|
msgid "Preferenze"
|
||||||
|
msgstr "Preferenze"
|
||||||
|
|
||||||
|
# Error Messages
|
||||||
|
msgid "Errore durante l'aggiornamento del profilo"
|
||||||
|
msgstr "Errore durante l'aggiornamento del profilo"
|
||||||
|
|
||||||
|
msgid "Errore di connessione al server: %(error)s"
|
||||||
|
msgstr "Errore di connessione al server: %(error)s"
|
||||||
|
|
||||||
|
msgid "Errore nel caricamento del profilo: %(error)s"
|
||||||
|
msgstr "Errore nel caricamento del profilo: %(error)s"
|
||||||
|
|
||||||
|
# Roles
|
||||||
|
msgid "Maker"
|
||||||
|
msgstr "Maker"
|
||||||
|
|
||||||
|
msgid "MeasurementTec"
|
||||||
|
msgstr "Tecnico di Misura"
|
||||||
|
|
||||||
|
msgid "Metrologist"
|
||||||
|
msgstr "Metrologo"
|
||||||
Reference in New Issue
Block a user