feat(measure): rework workflow operatore — AVVIA, capoturno, fine ciclo, blocco logout

- task_list: pulsante AVVIA sequenziale + rinomina "Inizia Misure" → "Visualizza Task"
- task_execute header: aggiunti "Lista task", "Riepilogo", "Fermo linea", "Fine Produzione"
- task_execute footer: "Fine ciclo misura" (attivo quando tutte le quote misurate) + "Completato"
- Rimosso auto-advance al task successivo, flusso ora esplicito via Completato
- Misura fuori tolleranza: blocco avanzamento + modale login capoturno (validate_supervisor)
- Blocco logout durante misurazioni attive (icona lucchetto su task_execute)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-05-23 15:54:37 +00:00
parent 0748ce9b1e
commit 1da7e5c7af
6 changed files with 386 additions and 32 deletions
@@ -84,7 +84,7 @@
</a>
{# Task badge + title #}
<div class="flex items-center gap-2 min-w-0 flex-1">
<div class="flex items-center gap-2 min-w-0">
<span class="shrink-0 inline-flex items-center px-2 py-0.5 rounded text-xs font-bold
bg-primary-50 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300
border border-primary-200 dark:border-primary-800">
@@ -95,6 +95,45 @@
</h1>
</div>
{# Lista task + Riepilogo buttons #}
<div class="shrink-0 flex items-center gap-1.5">
<a href="{{ url_for('measure.task_list', recipe_id=task.recipe_id or 0) }}"
class="btn btn-secondary text-xs py-1 px-2.5 gap-1">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 10h16M4 14h16M4 18h16"/>
</svg>
{{ _('Lista task') }}
</a>
<button @click="goToSummary()"
class="btn btn-secondary text-xs py-1 px-2.5 gap-1">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
{{ _('Riepilogo') }}
</button>
</div>
{# Fermo linea + Fine produzione (measurement tasks only) #}
<template x-if="subtasks.length > 0">
<div class="shrink-0 flex items-center gap-1.5">
<button @click="showSupervisorModal = true; supervisorAction = 'fermo_linea'"
class="btn text-xs py-1 px-2.5 gap-1 border-2 border-amber-500 text-amber-700 dark:text-amber-300 bg-amber-50 dark:bg-amber-900/20 hover:bg-amber-100 dark:hover:bg-amber-900/40">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M10 9v6m4-6v6m7-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
{{ _('Fermo linea') }}
</button>
<button @click="showSupervisorModal = true; supervisorAction = 'fine_produzione'"
class="btn text-xs py-1 px-2.5 gap-1 border-2 border-red-500 text-red-700 dark:text-red-300 bg-red-50 dark:bg-red-900/20 hover:bg-red-100 dark:hover:bg-red-900/40">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
<path stroke-linecap="round" stroke-linejoin="round" d="M9 10a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z"/>
</svg>
{{ _('Fine Produzione') }}
</button>
</div>
</template>
{# Lot + Serial badges #}
<div class="shrink-0 flex items-center gap-2">
{% if lot_number %}
@@ -397,19 +436,47 @@
x-text="Math.round(progressPercent) + '%'"></span>
</div>
{# Right: Summary button #}
<button x-show="isComplete"
x-transition
@click="goToSummary()"
class="btn btn-primary text-xs shrink-0 gap-1 py-1.5 px-2.5 shadow-md">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
{{ _('Riepilogo') }}
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M14 5l7 7m0 0l-7 7m7-7H3"/>
</svg>
</button>
{# Right: Fine ciclo misura (measurement tasks) / Completato (non-measurement or after cycle) #}
<template x-if="subtasks.length > 0">
<div class="flex items-center gap-1.5">
<button x-show="isComplete && !cycleConfirmed"
x-transition
@click="confirmCycle()"
class="btn text-xs shrink-0 gap-1 py-1.5 px-3 shadow-md
bg-primary text-white hover:bg-primary-700">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
{{ _('Fine ciclo misura') }}
</button>
<button x-show="cycleConfirmed"
x-transition
@click="goToNextTask()"
class="btn text-xs shrink-0 gap-1 py-1.5 px-3 shadow-md
bg-measure-pass text-white hover:opacity-90">
<svg class="w-3.5 h-3.5" 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>
{{ _('Completato') }}
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M14 5l7 7m0 0l-7 7m7-7H3"/>
</svg>
</button>
</div>
</template>
<template x-if="subtasks.length === 0">
<button @click="goToNextTask()"
class="btn text-xs shrink-0 gap-1 py-1.5 px-3 shadow-md
bg-measure-pass text-white hover:opacity-90">
<svg class="w-3.5 h-3.5" 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>
{{ _('Completato') }}
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M14 5l7 7m0 0l-7 7m7-7H3"/>
</svg>
</button>
</template>
</div>
</div>
@@ -456,13 +523,77 @@
</div>
</div>
<button @click="goToSummary()"
class="btn btn-primary w-full justify-center gap-2">
{{ _('Vai al Riepilogo') }}
<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="M14 5l7 7m0 0l-7 7m7-7H3"/>
</svg>
</button>
<div class="flex gap-2">
<button @click="goToSummary()"
class="btn btn-secondary flex-1 justify-center gap-2">
{{ _('Riepilogo') }}
</button>
<button @click="showCompletionOverlay = false; cycleConfirmed = true"
class="btn btn-primary flex-1 justify-center gap-2">
{{ _('Conferma ciclo') }}
<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>
</button>
</div>
</div>
</div>
{# ================================================================
SUPERVISOR LOGIN MODAL
================================================================ #}
<div x-show="showSupervisorModal"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
x-cloak
class="fixed inset-0 z-[60] flex items-center justify-center bg-black/50 backdrop-blur-sm"
@click.self="showSupervisorModal = false">
<div class="bg-[var(--bg-card)] rounded-2xl shadow-2xl p-6 max-w-sm mx-4 w-full border border-[var(--border-color)]">
<div class="flex items-center gap-3 mb-4">
<div class="w-10 h-10 rounded-full bg-amber-100 dark:bg-amber-900/30 flex items-center justify-center">
<svg class="w-5 h-5 text-amber-600" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" 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>
<div>
<h3 class="text-lg font-bold text-[var(--text-primary)]">{{ _('Autorizzazione capoturno') }}</h3>
<p class="text-xs text-[var(--text-secondary)]" x-text="supervisorReason"></p>
</div>
</div>
<div class="space-y-3 mb-5">
<div>
<label class="tmf-label text-xs">{{ _('Username') }}</label>
<input type="text" x-model="supervisorUsername" class="tmf-input text-sm"
placeholder="{{ _('Username capoturno') }}" @keydown.enter="validateSupervisor()">
</div>
<div>
<label class="tmf-label text-xs">{{ _('Password') }}</label>
<input type="password" x-model="supervisorPassword" class="tmf-input text-sm"
placeholder="••••••••" @keydown.enter="validateSupervisor()">
</div>
<p x-show="supervisorError" class="text-xs text-red-600" x-text="supervisorError"></p>
</div>
<div class="flex gap-2">
<button @click="showSupervisorModal = false; supervisorUsername = ''; supervisorPassword = ''; supervisorError = ''"
class="btn btn-secondary flex-1 text-sm">
{{ _('Annulla') }}
</button>
<button @click="validateSupervisor()"
:disabled="!supervisorUsername || !supervisorPassword || supervisorValidating"
class="btn btn-primary flex-1 text-sm gap-1">
<svg x-show="supervisorValidating" 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>
{{ _('Autorizza') }}
</button>
</div>
</div>
</div>
@@ -498,6 +629,16 @@ function taskExecute() {
errorMessage: '',
showCompletionOverlay: false,
// ---- Cycle & workflow state ----
cycleConfirmed: false,
showSupervisorModal: false,
supervisorAction: '', // 'out_of_tolerance', 'fermo_linea', 'fine_produzione'
supervisorUsername: '',
supervisorPassword: '',
supervisorError: '',
supervisorValidating: false,
pendingAdvance: false,
// ---- Value from numpad / caliper ----
currentValue: null,
@@ -650,15 +791,17 @@ function taskExecute() {
// Pause to show result feedback
await new Promise(r => setTimeout(r, 1000));
// Check if all done
// Out-of-tolerance: block advancement, require supervisor
if (pf === 'fail') {
this.pendingAdvance = true;
this.supervisorAction = 'out_of_tolerance';
this.showSupervisorModal = true;
return;
}
// Check if all done — show overlay but do NOT auto-navigate
if (this.completedCount >= this.totalSubtasks) {
const taskIds = window.__allTaskIds || [];
const currentIdx = taskIds.indexOf(this.task.id);
if (currentIdx >= 0 && currentIdx < taskIds.length - 1) {
window.location.href = '{{ url_for("measure.task_execute", task_id=0) }}'.replace('/0', '/' + taskIds[currentIdx + 1]);
} else {
this.showCompletionOverlay = true;
}
this.showCompletionOverlay = true;
return;
}
@@ -705,6 +848,73 @@ function taskExecute() {
}
},
// ---- Confirm measurement cycle (Fine ciclo misura) ----
confirmCycle() {
this.cycleConfirmed = true;
this.showCompletionOverlay = false;
},
// ---- Navigate to next task (Completato) ----
goToNextTask() {
const taskIds = window.__allTaskIds || [];
const currentIdx = taskIds.indexOf(this.task.id);
if (currentIdx >= 0 && currentIdx < taskIds.length - 1) {
window.location.href = '{{ url_for("measure.task_execute", task_id=0) }}'.replace('/0', '/' + taskIds[currentIdx + 1]);
} else {
this.goToSummary();
}
},
// ---- Supervisor reason text ----
get supervisorReason() {
if (this.supervisorAction === 'out_of_tolerance') return '{{ _("Misurazione fuori tolleranza") }}';
if (this.supervisorAction === 'fermo_linea') return '{{ _("Fermo linea richiesto") }}';
if (this.supervisorAction === 'fine_produzione') return '{{ _("Fine produzione richiesta") }}';
return '';
},
// ---- Validate supervisor credentials ----
async validateSupervisor() {
this.supervisorError = '';
this.supervisorValidating = true;
try {
const csrfToken = document.querySelector('meta[name=csrf-token]')?.content || '';
const resp = await fetch('{{ url_for("measure.validate_supervisor") }}', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken },
body: JSON.stringify({ username: this.supervisorUsername, password: this.supervisorPassword })
});
const data = await resp.json();
if (!resp.ok || data.error) {
this.supervisorError = data.detail || '{{ _("Credenziali non valide o utente non autorizzato") }}';
this.supervisorValidating = false;
return;
}
// Authorized — close modal and proceed
this.showSupervisorModal = false;
this.supervisorUsername = '';
this.supervisorPassword = '';
this.supervisorValidating = false;
if (this.supervisorAction === 'out_of_tolerance') {
this.pendingAdvance = false;
if (this.completedCount >= this.totalSubtasks) {
this.showCompletionOverlay = true;
} else {
this.advanceToNext();
}
}
// fermo_linea and fine_produzione are handled by GAIA integration (future)
} catch (err) {
this.supervisorError = '{{ _("Errore di connessione") }}';
this.supervisorValidating = false;
}
},
// ---- Go to summary ----
goToSummary() {
const recipeId = this.task.recipe_id || 0;