feat: timer intervallo misura su ricetta + auto-logout per inattività
- Recipe: nuovo campo measurement_interval_minutes (migration 003) con UI nel recipe editor - Auto-logout: impostazione di sistema configurabile da admin/settings, timer inattività JS con warning modale 60s prima del logout, tracking eventi utente throttled - Navbar: aggiunto link "Impostazioni" per admin (desktop + mobile) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ _('Impostazioni') }} - TieMeasureFlow{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-6"
|
||||
x-data="systemSettings()">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="mb-6">
|
||||
<h1 class="text-2xl font-bold text-[var(--text-primary)]">{{ _('Impostazioni di Sistema') }}</h1>
|
||||
<p class="mt-1 text-sm text-[var(--text-secondary)]">{{ _('Configura i parametri generali del sistema') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Settings Card -->
|
||||
<div class="tmf-card">
|
||||
<div class="tmf-card-header flex items-center gap-2">
|
||||
<svg class="w-4 h-4 text-[var(--text-muted)]" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
{{ _('Sessione e Sicurezza') }}
|
||||
</div>
|
||||
|
||||
<div class="tmf-card-body space-y-5">
|
||||
<!-- Auto-logout -->
|
||||
<div>
|
||||
<label class="tmf-label" for="auto-logout">{{ _('Auto-logout per inattività (minuti)') }}</label>
|
||||
<div class="flex items-center gap-3">
|
||||
<input id="auto-logout"
|
||||
type="number"
|
||||
x-model.number="autoLogoutMinutes"
|
||||
class="tmf-input w-32"
|
||||
min="1"
|
||||
max="480"
|
||||
placeholder="—">
|
||||
<button @click="autoLogoutMinutes = null"
|
||||
class="text-xs text-[var(--text-muted)] hover:text-red-500 transition-colors"
|
||||
x-show="autoLogoutMinutes">
|
||||
{{ _('Disabilita') }}
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-xs text-[var(--text-muted)] mt-1">
|
||||
{{ _('L\'operatore verrà disconnesso automaticamente dopo il periodo di inattività indicato. Lasciare vuoto per disabilitare.') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tmf-card-footer flex items-center justify-between">
|
||||
<span x-show="saved" x-transition class="text-sm text-emerald-600 flex items-center gap-1">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
{{ _('Salvato') }}
|
||||
</span>
|
||||
<span x-show="errorMessage" x-transition class="text-sm text-red-600" x-text="errorMessage"></span>
|
||||
<div class="flex-1"></div>
|
||||
<button @click="save()"
|
||||
:disabled="saving"
|
||||
class="btn btn-primary gap-1.5">
|
||||
<svg x-show="!saving" class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
<svg x-show="saving" class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"/>
|
||||
</svg>
|
||||
{{ _('Salva impostazioni') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
function systemSettings() {
|
||||
return {
|
||||
autoLogoutMinutes: {{ (settings.auto_logout_minutes or '')|tojson }},
|
||||
saving: false,
|
||||
saved: false,
|
||||
errorMessage: '',
|
||||
|
||||
async save() {
|
||||
this.saving = true;
|
||||
this.saved = false;
|
||||
this.errorMessage = '';
|
||||
|
||||
var csrfToken = document.querySelector('meta[name=csrf-token]')?.content;
|
||||
var payload = {
|
||||
auto_logout_minutes: this.autoLogoutMinutes ? String(this.autoLogoutMinutes) : ''
|
||||
};
|
||||
|
||||
try {
|
||||
var resp = await fetch('/admin/api/settings', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken || '' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
var data = await resp.json();
|
||||
if (data.error) {
|
||||
this.errorMessage = data.detail || data.error;
|
||||
} else {
|
||||
this.saved = true;
|
||||
setTimeout(() => { this.saved = false; }, 3000);
|
||||
}
|
||||
} catch (err) {
|
||||
this.errorMessage = {{ _('Errore di connessione al server')|tojson }};
|
||||
}
|
||||
this.saving = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user