b7cb061fee
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
218 lines
11 KiB
HTML
218 lines
11 KiB
HTML
{% extends "base.html" %}
|
||
|
||
{% block title %}{{ _('Impostazioni') }} - TieMeasureFlow{% endblock %}
|
||
|
||
{% block content %}
|
||
<div class="tmf-page" x-data="{ tab: {{ active_tab|tojson_attr }} }">
|
||
|
||
<!-- Header -->
|
||
<div class="mb-4">
|
||
<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>
|
||
|
||
<!-- #50: tab. L'intera pagina e' gia' solo per admin (admin_required) -->
|
||
<div class="flex gap-1 mb-6 border-b border-[var(--border-color)]">
|
||
<button type="button" @click="tab = 'generale'"
|
||
:class="tab === 'generale' ? 'border-primary text-primary' : 'border-transparent text-[var(--text-secondary)] hover:text-[var(--text-primary)]'"
|
||
class="px-4 py-2 -mb-px text-sm font-medium border-b-2 transition-colors">{{ _('Generale') }}</button>
|
||
<button type="button" @click="tab = 'utenti'"
|
||
:class="tab === 'utenti' ? 'border-primary text-primary' : 'border-transparent text-[var(--text-secondary)] hover:text-[var(--text-primary)]'"
|
||
class="px-4 py-2 -mb-px text-sm font-medium border-b-2 transition-colors">{{ _('Utenti') }}</button>
|
||
</div>
|
||
|
||
<div x-show="tab === 'utenti'" x-cloak>
|
||
{% include "admin/_users_panel.html" %}
|
||
</div>
|
||
|
||
{# #65: niente larghezza propria, la scheda sta nella cornice .tmf-page #}
|
||
<div x-show="tab === 'generale'" x-data="systemSettings()">
|
||
<!-- Settings Card -->
|
||
<div class="tmf-card mb-6">
|
||
<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>
|
||
|
||
<!-- #47: durata loop predefinita delle ricette -->
|
||
<div class="pt-5 border-t border-[var(--border-color)]">
|
||
<h3 class="text-sm font-semibold text-[var(--text-primary)] mb-3">{{ _('Ricette') }}</h3>
|
||
<label class="tmf-label" for="default-interval">{{ _('Durata loop predefinita (minuti)') }}</label>
|
||
<input id="default-interval" type="number" x-model="defaultInterval" class="tmf-input w-32" min="1" max="1440" placeholder="—">
|
||
<p class="text-xs text-[var(--text-muted)] mt-1">{{ _('Proposta come intervallo di misura nelle nuove ricette (1-1440). Vuoto = nessun predefinito.') }}</p>
|
||
</div>
|
||
|
||
<!-- #48: tolleranze generali -->
|
||
<div class="pt-5 border-t border-[var(--border-color)]">
|
||
<h3 class="text-sm font-semibold text-[var(--text-primary)] mb-3">{{ _('Tolleranze generali') }}</h3>
|
||
<div class="flex items-center gap-6">
|
||
<div>
|
||
<label class="tmf-label" for="tol-plus">{{ _('Tolleranza +') }}</label>
|
||
<input id="tol-plus" type="text" inputmode="decimal" x-model="tolPlus" class="tmf-input w-32" placeholder="—">
|
||
</div>
|
||
<div>
|
||
<label class="tmf-label" for="tol-minus">{{ _('Tolleranza −') }}</label>
|
||
<input id="tol-minus" type="text" inputmode="decimal" x-model="tolMinus" class="tmf-input w-32" placeholder="—">
|
||
</div>
|
||
</div>
|
||
<p class="text-xs text-[var(--text-muted)] mt-1">{{ _('Valori positivi (zero o più) proposti come tolleranza nelle nuove misure. Vuoto = nessun predefinito.') }}</p>
|
||
</div>
|
||
|
||
<!-- #49: anagrafiche (liste di scelta) -->
|
||
<div class="pt-5 border-t border-[var(--border-color)]">
|
||
<h3 class="text-sm font-semibold text-[var(--text-primary)] mb-3">{{ _('Anagrafiche') }}</h3>
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||
<template x-for="lst in ['types', 'units']" :key="lst">
|
||
<div>
|
||
<label class="tmf-label" x-text="lst === 'types' ? {{ _('Tipi di misura')|tojson_attr }} : {{ _('Unità di misura')|tojson_attr }}"></label>
|
||
<ul class="space-y-1 mb-2">
|
||
<template x-for="(item, i) in lists[lst]" :key="lst + i">
|
||
<li class="flex items-center gap-1 text-sm">
|
||
<span class="flex-1 px-2 py-1 rounded bg-[var(--bg-secondary)] text-[var(--text-primary)]" x-text="item"></span>
|
||
<button type="button" @click="moveItem(lst, i, -1)" :disabled="i === 0" class="px-1.5 text-[var(--text-muted)] hover:text-primary disabled:opacity-30" title="{{ _('Su') }}">▲</button>
|
||
<button type="button" @click="moveItem(lst, i, 1)" :disabled="i === lists[lst].length - 1" class="px-1.5 text-[var(--text-muted)] hover:text-primary disabled:opacity-30" title="{{ _('Giù') }}">▼</button>
|
||
<button type="button" @click="removeItem(lst, i)" class="px-1.5 text-[var(--text-muted)] hover:text-red-500" title="{{ _('Rimuovi') }}">✕</button>
|
||
</li>
|
||
</template>
|
||
</ul>
|
||
<div class="flex gap-2">
|
||
<input type="text" x-model="newItem[lst]" @keydown.enter.prevent="addItem(lst)" class="tmf-input flex-1" placeholder="{{ _('Nuova voce') }}">
|
||
<button type="button" @click="addItem(lst)" class="btn btn-secondary">{{ _('Aggiungi') }}</button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
<p class="text-xs text-[var(--text-muted)] mt-2">{{ _('Massimo 50 voci per lista. Un elenco vuoto = nessuna scelta predefinita.') }}</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><!-- /tab generale -->
|
||
</div>
|
||
{% endblock %}
|
||
|
||
{% block extra_js %}
|
||
<script>
|
||
// #49: legge un array JSON di stringhe salvato come impostazione; vuoto/malformato = lista vuota
|
||
function tmfParseList(raw) {
|
||
try { var a = JSON.parse(raw || '[]'); return Array.isArray(a) ? a.map(String) : []; }
|
||
catch (e) { return []; }
|
||
}
|
||
function systemSettings() {
|
||
return {
|
||
autoLogoutMinutes: {{ (settings.auto_logout_minutes or '')|tojson }},
|
||
// #47-#49: nuove chiavi (valori stringa, vuoto = nessun default)
|
||
defaultInterval: {{ (settings.default_measurement_interval_minutes or '')|tojson }},
|
||
tolPlus: {{ (settings.general_tolerance_plus or '')|tojson }},
|
||
tolMinus: {{ (settings.general_tolerance_minus or '')|tojson }},
|
||
lists: {
|
||
types: tmfParseList({{ (settings.list_measurement_types or '')|tojson }}),
|
||
units: tmfParseList({{ (settings.list_units or '')|tojson }})
|
||
},
|
||
newItem: { types: '', units: '' },
|
||
saving: false,
|
||
saved: false,
|
||
errorMessage: '',
|
||
|
||
addItem(lst) {
|
||
var v = (this.newItem[lst] || '').trim();
|
||
if (!v) return;
|
||
if (this.lists[lst].length >= 50) { this.errorMessage = {{ _('Massimo 50 voci per lista')|tojson }}; return; }
|
||
if (this.lists[lst].indexOf(v) === -1) this.lists[lst].push(v);
|
||
this.newItem[lst] = '';
|
||
},
|
||
removeItem(lst, i) { this.lists[lst].splice(i, 1); },
|
||
moveItem(lst, i, d) {
|
||
var a = this.lists[lst].slice(), j = i + d;
|
||
if (j < 0 || j >= a.length) return;
|
||
var t = a[i]; a[i] = a[j]; a[j] = t;
|
||
this.lists[lst] = a;
|
||
},
|
||
|
||
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) : '',
|
||
default_measurement_interval_minutes: String(this.defaultInterval || '').trim(),
|
||
// virgola decimale accettata in input, il backend vuole il punto
|
||
general_tolerance_plus: String(this.tolPlus || '').trim().replace(',', '.'),
|
||
general_tolerance_minus: String(this.tolMinus || '').trim().replace(',', '.'),
|
||
list_measurement_types: this.lists.types.length ? JSON.stringify(this.lists.types) : '',
|
||
list_units: this.lists.units.length ? JSON.stringify(this.lists.units) : ''
|
||
};
|
||
|
||
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>
|
||
{% include "admin/_users_script.html" %}
|
||
{% endblock %}
|