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:
@@ -31,8 +31,11 @@
|
||||
if (recipe.requires_serial && !this.serial_number.trim()) missing.push('{{ _('seriale') }}');
|
||||
return missing;
|
||||
},
|
||||
/* Straight into the first task. Choosing a recipe used to open the full
|
||||
task list, which asked the operator to decide where to begin before they
|
||||
had done anything; the list is still one tap away from inside the task. */
|
||||
buildTaskUrl(recipeId) {
|
||||
let url = '/measure/tasks/' + recipeId + '?';
|
||||
let url = '/measure/start/' + recipeId + '?';
|
||||
const params = [];
|
||||
if (this.lot_number) params.push('lot_number=' + encodeURIComponent(this.lot_number));
|
||||
if (this.serial_number) params.push('serial_number=' + encodeURIComponent(this.serial_number));
|
||||
@@ -285,13 +288,18 @@
|
||||
: 'group-hover:shadow-md'"
|
||||
class="btn btn-primary w-full justify-center text-sm font-semibold
|
||||
transition-shadow duration-200">
|
||||
<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="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>
|
||||
{{ _('Seleziona') }}
|
||||
<svg class="w-4 h-4 transition-transform group-hover:translate-x-0.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7"/>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5 3l14 9-14 9V3z"/>
|
||||
</svg>
|
||||
{{ _('AVVIA IN SEQUENZA') }}
|
||||
</a>
|
||||
|
||||
{# The whole list, for when the operator wants to see it or go back to a
|
||||
task left unfinished. Deliberately the quiet one of the two. #}
|
||||
<a :href="'/measure/tasks/' + recipe.id"
|
||||
class="mt-2 block text-center text-xs text-[var(--text-secondary)]
|
||||
hover:text-primary transition-colors">
|
||||
{{ _('Vedi tutti i task') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -97,14 +97,14 @@
|
||||
{{ _('Compila prima:') }} {{ missing_trace|join(', ') }}
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{{ url_for('measure.task_execute', task_id=tasks[0].id) }}"
|
||||
<a href="{{ url_for('measure.start_sequence', recipe_id=recipe.id) }}"
|
||||
class="btn gap-3 w-full sm:w-auto justify-center text-lg font-bold
|
||||
bg-red-600 hover:bg-red-700 text-white shadow-lg hover:shadow-xl
|
||||
px-8 py-3 rounded-xl transition-all duration-200">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5 3l14 9-14 9V3z"/>
|
||||
</svg>
|
||||
{{ _('AVVIA') }}
|
||||
{{ _('AVVIA IN SEQUENZA') }}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
@@ -162,7 +162,12 @@
|
||||
{% if tasks %}
|
||||
<div class="space-y-2.5">
|
||||
{% for task in tasks %}
|
||||
<div class="tmf-card hover:border-primary/30 transition-all duration-200 group">
|
||||
{# How far this task got. A task walked away from halfway through used to look
|
||||
exactly like one never opened; this is what tells them apart. #}
|
||||
{% set prog = (progress|default({})).get(task.id) %}
|
||||
<div class="tmf-card transition-all duration-200 group
|
||||
{% if prog and prog.state == 'partial' %}border-amber-300 dark:border-amber-700
|
||||
{% else %}hover:border-primary/30{% endif %}">
|
||||
<div class="p-3 sm:p-4">
|
||||
<div class="flex flex-row items-center gap-3">
|
||||
|
||||
@@ -208,7 +213,27 @@
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
{% if task.subtask_count is defined or task.subtasks %}
|
||||
{% if prog and prog.state in ('partial', 'complete') %}
|
||||
<span class="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[11px] font-semibold
|
||||
{% if prog.state == 'complete' %}
|
||||
bg-measure-pass/10 text-measure-pass
|
||||
{% else %}
|
||||
bg-amber-100 dark:bg-amber-900/30 text-amber-700 dark:text-amber-300
|
||||
{% endif %}">
|
||||
{% if prog.state == 'complete' %}
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
{{ _('Completato') }}
|
||||
{% else %}
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 8v4l2.5 2.5M12 21a9 9 0 110-18 9 9 0 010 18z"/>
|
||||
</svg>
|
||||
{{ _('Incompiuto') }}
|
||||
{% endif %}
|
||||
<span class="font-mono">{{ prog.measured }}/{{ prog.quotes }}</span>
|
||||
</span>
|
||||
{% elif task.subtask_count is defined or task.subtasks %}
|
||||
<span class="inline-flex items-center gap-1 text-xs text-[var(--text-secondary)]"
|
||||
title="{{ _('misurazioni') }}">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="1.75" viewBox="0 0 24 24">
|
||||
@@ -233,23 +258,23 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Action Button (Maker only — operators start the guided flow with AVVIA) -->
|
||||
{% if current_user and 'Maker' in current_user.get('roles', []) %}
|
||||
{#
|
||||
Opening one task on its own. The sequence is the normal way through the
|
||||
recipe - this is the way back to a task left unfinished, which is why the
|
||||
operator has it now and not only the Maker: a list that says "incompiuto"
|
||||
and gives you no way there would be a complaint, not a feature.
|
||||
#}
|
||||
<div class="shrink-0 sm:ml-4">
|
||||
<a href="{{ url_for('measure.task_execute', task_id=task.id) }}"
|
||||
class="btn btn-primary gap-2 w-full sm:w-auto justify-center
|
||||
class="btn btn-secondary gap-2 w-full sm:w-auto justify-center
|
||||
group-hover:shadow-md transition-shadow duration-200">
|
||||
<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.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
{{ _('Visualizza Task') }}
|
||||
<svg class="w-4 h-4 transition-transform group-hover:translate-x-0.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
|
||||
</svg>
|
||||
{{ _('Visualizza singolo TASK') }}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user