feat(measure): timer cicalino con countdown e buzzer dopo fine ciclo misura
- Blueprint: passa measurement_interval_minutes dalla ricetta al template task_execute - Dopo "Fine ciclo misura": countdown MM:SS con banner amber animato sopra footer - Allo scadere: 3 beep sonori (Web Audio API 880Hz) + reset misurazioni per nuovo ciclo - Contatore cicli visibile nel banner timer Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -135,11 +135,15 @@ def task_execute(task_id: int):
|
|||||||
# Load all task IDs for this recipe (ordered) for auto-advance
|
# Load all task IDs for this recipe (ordered) for auto-advance
|
||||||
recipe_id = task_resp.get("recipe_id")
|
recipe_id = task_resp.get("recipe_id")
|
||||||
all_task_ids = []
|
all_task_ids = []
|
||||||
|
measurement_interval_minutes = None
|
||||||
if recipe_id:
|
if recipe_id:
|
||||||
tasks_resp = api_client.get(f"/api/recipes/{recipe_id}/tasks")
|
tasks_resp = api_client.get(f"/api/recipes/{recipe_id}/tasks")
|
||||||
if isinstance(tasks_resp, list):
|
if isinstance(tasks_resp, list):
|
||||||
sorted_tasks = sorted(tasks_resp, key=lambda t: t.get("order_index", 0))
|
sorted_tasks = sorted(tasks_resp, key=lambda t: t.get("order_index", 0))
|
||||||
all_task_ids = [t["id"] for t in sorted_tasks]
|
all_task_ids = [t["id"] for t in sorted_tasks]
|
||||||
|
recipe_resp = api_client.get(f"/api/recipes/{recipe_id}")
|
||||||
|
if not recipe_resp.get("error"):
|
||||||
|
measurement_interval_minutes = recipe_resp.get("measurement_interval_minutes")
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"measure/task_execute.html",
|
"measure/task_execute.html",
|
||||||
@@ -147,6 +151,7 @@ def task_execute(task_id: int):
|
|||||||
lot_number=lot_number,
|
lot_number=lot_number,
|
||||||
serial_number=serial_number,
|
serial_number=serial_number,
|
||||||
all_task_ids=all_task_ids,
|
all_task_ids=all_task_ids,
|
||||||
|
measurement_interval_minutes=measurement_interval_minutes,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -406,6 +406,28 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{# ================================================================
|
||||||
|
MEASUREMENT TIMER BANNER
|
||||||
|
================================================================ #}
|
||||||
|
<div x-show="timerActive"
|
||||||
|
x-transition
|
||||||
|
x-cloak
|
||||||
|
class="shrink-0 bg-amber-50 dark:bg-amber-900/20 border-t border-amber-300 dark:border-amber-700 px-4 py-2">
|
||||||
|
<div class="flex items-center justify-center gap-3">
|
||||||
|
<svg class="w-5 h-5 text-amber-600 animate-pulse" 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>
|
||||||
|
<span class="text-sm font-medium text-amber-800 dark:text-amber-200">
|
||||||
|
{{ _('Prossima misurazione tra') }}
|
||||||
|
</span>
|
||||||
|
<span class="text-lg font-bold font-mono text-amber-900 dark:text-amber-100 bg-amber-100 dark:bg-amber-900/40 px-2 py-0.5 rounded"
|
||||||
|
x-text="timerDisplay"></span>
|
||||||
|
<span class="text-xs text-amber-600 dark:text-amber-400">
|
||||||
|
({{ _('Ciclo') }} #<span x-text="cycleCount"></span>)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{# ================================================================
|
{# ================================================================
|
||||||
FOOTER — Progress bar + navigation
|
FOOTER — Progress bar + navigation
|
||||||
================================================================ #}
|
================================================================ #}
|
||||||
@@ -629,6 +651,13 @@ function taskExecute() {
|
|||||||
errorMessage: '',
|
errorMessage: '',
|
||||||
showCompletionOverlay: false,
|
showCompletionOverlay: false,
|
||||||
|
|
||||||
|
// ---- Measurement timer ----
|
||||||
|
measurementIntervalMinutes: {{ measurement_interval_minutes|tojson if measurement_interval_minutes else 'null' }},
|
||||||
|
timerActive: false,
|
||||||
|
timerRemaining: 0,
|
||||||
|
_timerInterval: null,
|
||||||
|
cycleCount: 0,
|
||||||
|
|
||||||
// ---- Cycle & workflow state ----
|
// ---- Cycle & workflow state ----
|
||||||
cycleConfirmed: false,
|
cycleConfirmed: false,
|
||||||
showSupervisorModal: false,
|
showSupervisorModal: false,
|
||||||
@@ -852,6 +881,68 @@ function taskExecute() {
|
|||||||
confirmCycle() {
|
confirmCycle() {
|
||||||
this.cycleConfirmed = true;
|
this.cycleConfirmed = true;
|
||||||
this.showCompletionOverlay = false;
|
this.showCompletionOverlay = false;
|
||||||
|
this.cycleCount++;
|
||||||
|
|
||||||
|
// Start measurement timer if recipe has an interval
|
||||||
|
if (this.measurementIntervalMinutes && this.measurementIntervalMinutes > 0) {
|
||||||
|
this.startMeasurementTimer();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// ---- Measurement timer ----
|
||||||
|
startMeasurementTimer() {
|
||||||
|
this.stopMeasurementTimer();
|
||||||
|
this.timerRemaining = this.measurementIntervalMinutes * 60;
|
||||||
|
this.timerActive = true;
|
||||||
|
var self = this;
|
||||||
|
this._timerInterval = setInterval(function () {
|
||||||
|
self.timerRemaining--;
|
||||||
|
if (self.timerRemaining <= 0) {
|
||||||
|
self.onTimerExpired();
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
},
|
||||||
|
|
||||||
|
stopMeasurementTimer() {
|
||||||
|
if (this._timerInterval) {
|
||||||
|
clearInterval(this._timerInterval);
|
||||||
|
this._timerInterval = null;
|
||||||
|
}
|
||||||
|
this.timerActive = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
onTimerExpired() {
|
||||||
|
this.stopMeasurementTimer();
|
||||||
|
this.playBuzzer();
|
||||||
|
// Reset for new measurement cycle
|
||||||
|
this.cycleConfirmed = false;
|
||||||
|
this.measurements = [];
|
||||||
|
this.currentIndex = 0;
|
||||||
|
this.currentValue = null;
|
||||||
|
},
|
||||||
|
|
||||||
|
playBuzzer() {
|
||||||
|
try {
|
||||||
|
var ctx = new (window.AudioContext || window.webkitAudioContext)();
|
||||||
|
// 3 short beeps
|
||||||
|
[0, 0.25, 0.5].forEach(function (delay) {
|
||||||
|
var osc = ctx.createOscillator();
|
||||||
|
var gain = ctx.createGain();
|
||||||
|
osc.connect(gain);
|
||||||
|
gain.connect(ctx.destination);
|
||||||
|
osc.frequency.value = 880;
|
||||||
|
osc.type = 'square';
|
||||||
|
gain.gain.value = 0.3;
|
||||||
|
osc.start(ctx.currentTime + delay);
|
||||||
|
osc.stop(ctx.currentTime + delay + 0.15);
|
||||||
|
});
|
||||||
|
} catch (_) {}
|
||||||
|
},
|
||||||
|
|
||||||
|
get timerDisplay() {
|
||||||
|
var m = Math.floor(this.timerRemaining / 60);
|
||||||
|
var s = this.timerRemaining % 60;
|
||||||
|
return (m < 10 ? '0' : '') + m + ':' + (s < 10 ? '0' : '') + s;
|
||||||
},
|
},
|
||||||
|
|
||||||
// ---- Navigate to next task (Completato) ----
|
// ---- Navigate to next task (Completato) ----
|
||||||
|
|||||||
@@ -759,6 +759,12 @@ msgstr "seconds due to inactivity."
|
|||||||
msgid "Continua a lavorare"
|
msgid "Continua a lavorare"
|
||||||
msgstr "Continue working"
|
msgstr "Continue working"
|
||||||
|
|
||||||
|
msgid "Prossima misurazione tra"
|
||||||
|
msgstr "Next measurement in"
|
||||||
|
|
||||||
|
msgid "Ciclo"
|
||||||
|
msgstr "Cycle"
|
||||||
|
|
||||||
msgid "Intervallo misura (minuti)"
|
msgid "Intervallo misura (minuti)"
|
||||||
msgstr "Measurement interval (minutes)"
|
msgstr "Measurement interval (minutes)"
|
||||||
|
|
||||||
|
|||||||
@@ -789,6 +789,12 @@ msgstr "secondi per inattività."
|
|||||||
msgid "Continua a lavorare"
|
msgid "Continua a lavorare"
|
||||||
msgstr "Continua a lavorare"
|
msgstr "Continua a lavorare"
|
||||||
|
|
||||||
|
msgid "Prossima misurazione tra"
|
||||||
|
msgstr "Prossima misurazione tra"
|
||||||
|
|
||||||
|
msgid "Ciclo"
|
||||||
|
msgstr "Ciclo"
|
||||||
|
|
||||||
msgid "Intervallo misura (minuti)"
|
msgid "Intervallo misura (minuti)"
|
||||||
msgstr "Intervallo misura (minuti)"
|
msgstr "Intervallo misura (minuti)"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user