feat(production): la misura torna a cercare l'operatore
Il conto alla rovescia non si ferma piu' a zero. Oltre la scadenza continua nell'altro senso e dice da quanto la linea e' in ritardo, in rosso, su tutte le schermate del percorso di misura: lista task, riepilogo, scelta ricetta. Un ritardo va letto, non dedotto. Allo scadere dell'intervallo la misura si ripropone da sola: cicalino, un avviso di cinque secondi, e l'operatore torna al primo task di misura da qualunque schermata si trovi. Un ciclo copre tutti i task di misura della ricetta, quindi solo l'ultimo lo chiude e fa ripartire l'intervallo. Farlo ripartire al primo avrebbe fatto misurare una ricetta con tre task di misura tre volte piu' spesso di come e' configurata. E' il server a decidere quale task chiude il ciclo, perche' e' lui a conoscere la sequenza: la lista dei task di misura viaggia con la produzione (measurement_task_ids), che e' anche cio' che permette a una schermata qualsiasi di sapere dove riportare l'operatore. Aggiunta la rimisura: si gira il pezzo e si misura di nuovo dentro lo stesso ciclo, senza chiudere niente e senza guadagnare tempo sulla scadenza. Le due letture restano entrambe in statistica, che e' il motivo per cui si prendono. Chi sta gia' misurando quando l'intervallo scade e' in ritardo, non perso: la banda diventa rossa e lo si lascia lavorare. Portarlo altrove a meta' ciclo cancellerebbe quote che ha davanti agli occhi senza guadagnare nulla. Sistemato anche il pulsante "Conferma ciclo" dell'overlay: alzava una bandierina locale e basta, quindi il ciclo non veniva mai registrato sul server da quella strada. Ora passa da confirmCycle come il pulsante della barra. Migrazione 008: il registro eventi della produzione impara task_measured e remeasure, e la colonna task_id — un task_measured che non dice quale task non registra niente di utile. I valori nuovi entrano tutti insieme perche' allargare una enum MySQL riscrive la tabella, stesso ragionamento dei tipi di task in 007. Verificata su SQLite (batch mode) e in MySQL con --sql. La logica dell'orologio — come si legge un ritardo, quando suona, dove sta la misura — vive in un solo posto (production-clock.js) e la schermata di misura la usa invece di riscriverla. Test: +10 (265). Coprono il ciclo che non riparte a meta', la rimisura che non sposta la scadenza, il rifiuto a linea ferma, la sequenza dei task di misura esposta dalla produzione, i proxy Flask e la validita' JS della lista task. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
{#
|
||||
The measurement clock, for every screen that is not the measurement itself.
|
||||
|
||||
Two requirements of 28/07 are in here. The countdown is always on screen while a
|
||||
production is open, and past zero it turns round and shows how long the line has
|
||||
been over the interval - in red, because a late measurement that looks like an
|
||||
early one is how it stays late. And when the interval expires the measurement
|
||||
comes back on its own: the operator is told, and taken there.
|
||||
|
||||
The measurement screen has its own component and does not include this: being
|
||||
already on the measurement is the one place there is nothing to announce.
|
||||
#}
|
||||
<script src="{{ url_for('static', filename='js/production-clock.js') }}"></script>
|
||||
|
||||
<div x-data="productionClock({
|
||||
currentUrl: '{{ url_for('measure.api_current_production') }}',
|
||||
executeUrl: '{{ url_for('measure.task_execute', task_id=0) }}'
|
||||
})"
|
||||
x-init="init()"
|
||||
x-cloak>
|
||||
|
||||
{# ---- Countdown bar: sticky, so scrolling never hides it ---- #}
|
||||
<div x-show="visible"
|
||||
x-transition
|
||||
class="sticky top-0 z-30 -mx-4 sm:-mx-6 lg:-mx-8 mb-4 px-4 py-2 border-b shadow-sm"
|
||||
:class="isDue
|
||||
? 'bg-red-50 dark:bg-red-900/30 border-red-400 dark:border-red-700'
|
||||
: paused
|
||||
? 'bg-amber-100 dark:bg-amber-900/40 border-amber-500'
|
||||
: 'bg-amber-50 dark:bg-amber-900/20 border-amber-300 dark:border-amber-700'">
|
||||
<div class="flex flex-wrap items-center justify-center gap-x-3 gap-y-1">
|
||||
|
||||
<svg class="w-5 h-5 shrink-0"
|
||||
:class="isDue ? 'text-red-600 animate-pulse' : 'text-amber-600'"
|
||||
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>
|
||||
|
||||
{# In time #}
|
||||
<template x-if="!isDue && !paused">
|
||||
<span class="text-sm font-medium text-amber-800 dark:text-amber-200">
|
||||
{{ _('Prossima misurazione tra') }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
{# Over the interval: the delay is the headline, not a footnote #}
|
||||
<template x-if="isDue">
|
||||
<span class="text-sm font-bold text-red-800 dark:text-red-200">
|
||||
{{ _('Misurazione in ritardo di') }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
{# Line stopped: the figure is frozen, and says so. Frozen past the interval
|
||||
it is a delay held in place, not a wait - the wording has to say which. #}
|
||||
<template x-if="paused && seconds > 0">
|
||||
<span class="text-sm font-semibold text-amber-900 dark:text-amber-100">
|
||||
{{ _('Linea ferma — conto alla rovescia congelato a') }}
|
||||
</span>
|
||||
</template>
|
||||
<template x-if="paused && seconds <= 0">
|
||||
<span class="text-sm font-semibold text-amber-900 dark:text-amber-100">
|
||||
{{ _('Linea ferma') }} — {{ _('Misurazione in ritardo di') }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<span class="text-lg font-bold font-mono px-2 py-0.5 rounded"
|
||||
:class="isDue
|
||||
? 'text-red-900 dark:text-red-100 bg-red-100 dark:bg-red-900/40'
|
||||
: 'text-amber-900 dark:text-amber-100 bg-amber-100 dark:bg-amber-900/40'"
|
||||
x-text="display"></span>
|
||||
|
||||
<span class="text-xs"
|
||||
:class="isDue ? 'text-red-600 dark:text-red-300' : 'text-amber-600 dark:text-amber-400'">
|
||||
({{ _('Ciclo') }} #<span x-text="run ? run.cycle_count : 0"></span>)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ---- Time to measure: announced, then taken there ---- #}
|
||||
<div x-show="announced && redirectIn > 0"
|
||||
x-transition
|
||||
class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
|
||||
<div class="bg-[var(--bg-card)] rounded-2xl shadow-2xl p-8 max-w-sm mx-4 text-center border-2 border-red-500">
|
||||
|
||||
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-red-100 dark:bg-red-900/30 mb-4">
|
||||
<svg class="w-8 h-8 text-red-600 animate-pulse" fill="none" stroke="currentColor" stroke-width="2.5" 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>
|
||||
</div>
|
||||
|
||||
<h3 class="text-lg font-bold text-[var(--text-primary)] mb-1">
|
||||
{{ _('È ora di misurare') }}
|
||||
</h3>
|
||||
<p class="text-sm text-[var(--text-secondary)] mb-5">
|
||||
{{ _('Ritorno alla misurazione tra') }}
|
||||
<span class="font-bold font-mono" x-text="redirectIn"></span> s
|
||||
</p>
|
||||
|
||||
<button @click="goToMeasurement()"
|
||||
class="btn w-full justify-center gap-2 bg-red-600 hover:bg-red-700 text-white font-bold py-2.5">
|
||||
{{ _('Vai alla misura') }}
|
||||
<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 5l7 7-7 7"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user