feat(measure): l'operatore entra nella sequenza, non in un elenco

Punto 10. Sceglere una ricetta apriva la lista completa dei task e chiedeva
all'operatore di decidere da dove cominciare prima ancora di aver fatto
qualcosa. Ora AVVIA IN SEQUENZA porta dentro il primo task; la lista resta,
un livello sotto, per quando serve vederla tutta o tornare indietro.

- nuova rotta /measure/start/<ricetta>: memorizza lotto e seriale (prima lo
  faceva la lista, che ora si salta), verifica la tracciabilità obbligatoria
  e apre il primo task
- GET /api/measurements/task-progress: quante quote ha già preso ogni task di
  misura, contate per quota e non per tentativo, delimitate dalla produzione
  aperta o, fuori produzione, dall'operatore
- la lista distingue «Incompiuto 1/3» da «Completato 3/3»: un task lasciato a
  metà non somigliava più a uno mai aperto
- «Visualizza singolo TASK» non è più riservato al Maker — dire «incompiuto»
  senza dare la strada per tornarci sarebbe una lamentela, non una funzione
- nel task «Riepilogo» diventa «Completato» e si apre solo quando il ciclo è
  chiuso: altrimenti sarebbe la scorciatoia che rende facoltative le quote
- «Fine ciclo misura» si vede da subito, spento, e dice quante quote mancano;
  prima compariva a task già finito, quando non serviva più saperlo

Il pulsante verde del footer diventa «Task successivo»: due bottoni con la
stessa parola addosso sono uno di troppo.

Test: 334 (erano 309). Il side effect dei mock del client Flask ora risponde
per endpoint invece che per turno — una lista posizionale si rompe appena una
pagina fa una domanda in più al server, che è come crescono tutte.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-07-28 21:31:42 +00:00
parent 919ee7ac79
commit 98b1c5ae85
15 changed files with 1656 additions and 659 deletions
@@ -96,7 +96,16 @@
</h1>
</div>
{# Lista task + Riepilogo buttons #}
{#
Lista task + Completato.
The list is the second level now: the operator arrives inside the sequence
and comes here when they want the whole of it. What used to sit beside it
was «Riepilogo», which jumped out of the sequence to a recap - the wrong
thing to offer someone halfway through a task. The way forward is
«Completato», and on a measurement task it stays shut until the cycle is
closed, which is the same rule the footer enforces.
#}
<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">
@@ -105,12 +114,15 @@
</svg>
{{ _('Lista task') }}
</a>
<button @click="goToSummary()"
<button @click="goToNextTask()"
:disabled="!canLeaveTask"
:title="canLeaveTask ? '' : blockedReason"
:class="canLeaveTask ? '' : 'opacity-40 cursor-not-allowed'"
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"/>
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/>
</svg>
{{ _('Riepilogo') }}
{{ _('Completato') }}
</button>
</div>
@@ -630,18 +642,30 @@
x-text="Math.round(progressPercent) + '%'"></span>
</div>
{# Right: Fine ciclo misura (measurement tasks) / Completato (non-measurement or after cycle) #}
{# Right: Fine ciclo misura (measurement tasks) / Task successivo (after cycle) #}
<template x-if="isMeasureTask && subtasks.length > 0">
<div class="flex items-center gap-1.5">
<button x-show="isComplete && !cycleConfirmed"
{#
Shown from the start, and greyed until every quote has a value. It used
to appear only once the task was finished, which left the operator with
no sign that the button existed or what would bring it out - a control
that is missing teaches nothing, one that is disabled says what is left.
#}
<button x-show="!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">
:disabled="!isComplete"
:title="isComplete ? '' : missingQuotesLabel"
:class="isComplete
? 'bg-primary text-white hover:bg-primary-700 shadow-md'
: 'bg-steel-200 dark:bg-steel-700 text-steel-500 dark:text-steel-400 cursor-not-allowed'"
class="btn text-xs shrink-0 gap-1 py-1.5 px-3">
<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') }}
<span x-text="isComplete
? {{ _('Fine ciclo misura')|tojson_attr }}
: missingQuotesLabel"></span>
</button>
{# Girare il pezzo e rimisurare, senza chiudere il ciclo: a second reading
of the same part is not a new one, and must not restart the interval. #}
@@ -654,6 +678,9 @@
</svg>
{{ _('Rimisura') }}
</button>
{# The cycle is closed; this only walks on. «Completato» is the header's
word for finishing a task, and two buttons wearing it would be one too
many. #}
<button x-show="cycleConfirmed"
x-transition
@click="goToNextTask()"
@@ -662,7 +689,7 @@
<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') }}
{{ _('Task successivo') }}
<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>
@@ -988,6 +1015,40 @@ function taskExecute() {
: 0;
},
/* How many quotes are still owed, worded rather than counted down in silence.
This is what the greyed «Fine ciclo misura» says instead of nothing. */
get missingQuotes() {
return Math.max(0, this.totalSubtasks - this.completedCount);
},
get missingQuotesLabel() {
const n = this.missingQuotes;
return n === 1
? {{ _('Manca 1 quota')|tojson }}
: {{ _('Mancano')|tojson }} + ' ' + n + ' ' + {{ _('quote')|tojson }};
},
/* Whether «Completato» can take the operator on.
A documental task is done when they say it is. A measurement task is done
when its cycle is closed - otherwise the button would be a way round «Fine
ciclo misura», and the quotes it exists to collect would be optional. */
get canLeaveTask() {
if (this.blockedByTolerance) return false;
if (this.isMeasureTask && this.subtasks.length > 0) {
return this.cycleConfirmed;
}
return true;
},
get blockedReason() {
if (this.blockedByTolerance) {
return {{ _("Serve l'autorizzazione del capoturno")|tojson }};
}
if (!this.isComplete) return this.missingQuotesLabel;
return {{ _('Chiudi prima il ciclo di misura')|tojson }};
},
// ---- Pass/fail logic ----
get passFailStatus() {
if (this.currentValue === null || !this.currentSubtask) return null;