34a72e6f1a
The previous change made the overlay phase-aware but it still popped up after every task during the START run. Operators want to flow straight to the next task. Skip showing the "Misurazioni Complete" overlay when production has not started and this is not the last task — call goToNextTask() directly. The overlay still appears on the last START task (for "Avvio Produzione") and in production (for "Conferma ciclo"). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1126 lines
49 KiB
HTML
1126 lines
49 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}{{ task.title or 'Task' }} — {{ _('Misure') }} — TieMeasureFlow{% endblock %}
|
|
|
|
{# Full-screen layout: no body scroll, no footer, main fills available space #}
|
|
{% block body_class %}h-screen overflow-hidden{% endblock %}
|
|
{% block wrapper_class %}h-full{% endblock %}
|
|
{% block main_class %}flex-1 min-h-0{% endblock %}
|
|
{% block footer %}{% endblock %}
|
|
|
|
{% block extra_head %}
|
|
<style>
|
|
/* Annotation viewer container */
|
|
.annotation-container {
|
|
position: relative;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: var(--bg-secondary);
|
|
overflow: hidden;
|
|
}
|
|
.annotation-container canvas {
|
|
display: block;
|
|
max-width: 100%;
|
|
height: auto;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
/* Tolerance bar gradient background */
|
|
.tolerance-bar-bg {
|
|
background: linear-gradient(to right,
|
|
rgba(220, 38, 38, 0.15) 0%,
|
|
rgba(217, 119, 6, 0.15) 15%,
|
|
rgba(5, 150, 105, 0.15) 30%,
|
|
rgba(5, 150, 105, 0.15) 70%,
|
|
rgba(217, 119, 6, 0.15) 85%,
|
|
rgba(220, 38, 38, 0.15) 100%
|
|
);
|
|
}
|
|
|
|
/* Pulse animation for active marker */
|
|
@keyframes markerPulse {
|
|
0%, 100% { box-shadow: 0 0 0 0 rgba(37, 99, 235, 0.4); }
|
|
50% { box-shadow: 0 0 0 6px rgba(37, 99, 235, 0); }
|
|
}
|
|
.marker-active-pulse {
|
|
animation: markerPulse 2s ease-in-out infinite;
|
|
}
|
|
|
|
/* Left sidebar scrollbar */
|
|
.sidebar-markers::-webkit-scrollbar { width: 3px; }
|
|
.sidebar-markers::-webkit-scrollbar-thumb { background: var(--border-color); border-radius: 3px; }
|
|
|
|
/* Right panel scrollbar */
|
|
.right-panel::-webkit-scrollbar { width: 4px; }
|
|
.right-panel::-webkit-scrollbar-thumb { background: var(--border-color); border-radius: 4px; }
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<script>
|
|
window.__taskData = {{ task|tojson }};
|
|
window.__taskSubtasks = {{ task.subtasks|tojson if task.subtasks else '[]' }};
|
|
window.__taskAnnotations = {{ task.annotations_json|default('null')|tojson }};
|
|
window.__allTaskIds = {{ all_task_ids|tojson }};
|
|
</script>
|
|
|
|
<div class="h-full flex flex-col overflow-hidden"
|
|
x-data="taskExecute()"
|
|
x-init="init()"
|
|
@numpad-confirm.window="handleMeasurement($event.detail.value, $event.detail.inputMethod)"
|
|
@marker-click.window="goToSubtaskByMarker($event.detail.marker_number)">
|
|
|
|
{# ================================================================
|
|
HEADER — Compact task info bar
|
|
================================================================ #}
|
|
<div class="shrink-0 bg-[var(--bg-card)] border-b border-[var(--border-color)] shadow-sm z-20">
|
|
<div class="px-3 py-2 flex items-center gap-3">
|
|
|
|
{# Back button #}
|
|
<a href="{{ url_for('measure.task_list', recipe_id=task.recipe_id or 0) }}"
|
|
class="shrink-0 p-1.5 rounded-lg hover:bg-[var(--bg-secondary)] transition-colors text-[var(--text-muted)] hover:text-[var(--text-primary)]">
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
|
|
</svg>
|
|
</a>
|
|
|
|
{# Task badge + title #}
|
|
<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">
|
|
Task {{ (task.order_index or 0) + 1 }}
|
|
</span>
|
|
<h1 class="text-sm font-bold text-[var(--text-primary)] truncate">
|
|
{{ task.title or _('Task di misurazione') }}
|
|
</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="openSupervisorModal('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="openSupervisorModal('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 %}
|
|
<div class="inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs
|
|
bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800
|
|
text-amber-800 dark:text-amber-200">
|
|
<svg class="w-3 h-3 shrink-0" fill="none" stroke="currentColor" stroke-width="1.75" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"/>
|
|
</svg>
|
|
<span class="font-mono font-semibold">{{ lot_number }}</span>
|
|
</div>
|
|
{% endif %}
|
|
{% if serial_number %}
|
|
<div class="inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs
|
|
bg-indigo-50 dark:bg-indigo-900/20 border border-indigo-200 dark:border-indigo-800
|
|
text-indigo-800 dark:text-indigo-200">
|
|
<svg class="w-3 h-3 shrink-0" fill="none" stroke="currentColor" stroke-width="1.75" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M7 20l4-16m2 16l4-16M6 9h14M4 15h14"/>
|
|
</svg>
|
|
<span class="font-mono font-semibold">{{ serial_number }}</span>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{# Caliper status #}
|
|
{% include "components/caliper_status.html" %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{# ================================================================
|
|
MAIN — 3-column layout
|
|
================================================================ #}
|
|
<div class="flex-1 flex overflow-hidden">
|
|
|
|
{# ──────────────────────────────────────────────
|
|
LEFT SIDEBAR — Marker list (vertical, hidden when no subtasks)
|
|
────────────────────────────────────────────── #}
|
|
<div x-show="subtasks.length > 0"
|
|
class="shrink-0 w-14 md:w-16 bg-[var(--bg-card)] border-r border-[var(--border-color)] flex flex-col sidebar-markers overflow-y-auto">
|
|
<template x-for="(st, idx) in subtasks" :key="st.id">
|
|
<button @click="goToSubtask(idx)"
|
|
class="relative flex flex-col items-center justify-center py-2.5 px-1 border-b border-[var(--border-color)] transition-all duration-200"
|
|
:class="idx === currentIndex
|
|
? 'bg-primary-50 dark:bg-primary-900/20 border-l-[3px] border-l-primary'
|
|
: 'hover:bg-[var(--bg-secondary)] border-l-[3px] border-l-transparent'">
|
|
|
|
{# Marker circle #}
|
|
<span class="inline-flex items-center justify-center w-8 h-8 rounded-full text-xs font-bold transition-all"
|
|
:class="idx === currentIndex
|
|
? 'bg-primary text-white shadow-md marker-active-pulse'
|
|
: getMeasurementStatus(st.id) === 'pass'
|
|
? 'bg-measure-pass/20 text-measure-pass border border-measure-pass/40'
|
|
: getMeasurementStatus(st.id) === 'fail'
|
|
? 'bg-red-100 dark:bg-red-900/30 text-measure-fail border border-measure-fail/40'
|
|
: getMeasurementStatus(st.id) === 'warning'
|
|
? 'bg-amber-100 dark:bg-amber-900/30 text-measure-warning border border-measure-warning/40'
|
|
: 'bg-[var(--bg-secondary)] text-[var(--text-muted)] border border-[var(--border-color)]'"
|
|
x-text="st.marker_number"></span>
|
|
|
|
{# Status icon below circle #}
|
|
<template x-if="getMeasurementStatus(st.id) === 'pass'">
|
|
<svg class="w-3 h-3 mt-0.5 text-measure-pass" fill="none" stroke="currentColor" stroke-width="3" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/>
|
|
</svg>
|
|
</template>
|
|
<template x-if="getMeasurementStatus(st.id) === 'fail'">
|
|
<svg class="w-3 h-3 mt-0.5 text-measure-fail" fill="none" stroke="currentColor" stroke-width="3" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/>
|
|
</svg>
|
|
</template>
|
|
<template x-if="getMeasurementStatus(st.id) === 'warning'">
|
|
<svg class="w-3 h-3 mt-0.5 text-measure-warning" fill="currentColor" viewBox="0 0 24 24">
|
|
<path d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z"/>
|
|
</svg>
|
|
</template>
|
|
</button>
|
|
</template>
|
|
</div>
|
|
|
|
{# ──────────────────────────────────────────────
|
|
CENTER — Image area
|
|
────────────────────────────────────────────── #}
|
|
<div class="flex-1 flex flex-col overflow-hidden bg-[var(--bg-secondary)]">
|
|
|
|
{# Main image area #}
|
|
<div class="flex-1 overflow-hidden relative">
|
|
|
|
{# Option A: Subtask has its own image → plain <img> #}
|
|
<div x-show="showSubtaskImage" class="absolute inset-0 flex items-center justify-center p-2">
|
|
<img :src="'/measure/api/files/' + currentSubtaskImage"
|
|
alt="{{ _('Immagine dettaglio misura') }}"
|
|
class="max-w-full max-h-full object-contain rounded-lg shadow-sm">
|
|
</div>
|
|
|
|
{# Option B: Task image with annotations → annotation-viewer (kept in DOM via x-show) #}
|
|
<div x-show="showTaskImage" class="absolute inset-0">
|
|
{% if task.file_path %}
|
|
<div class="annotation-container"
|
|
x-data="annotationViewer()"
|
|
x-init="
|
|
imageUrl = '/measure/api/files/{{ task.file_path }}';
|
|
annotations = window.__taskAnnotations;
|
|
$nextTick(() => init());
|
|
"
|
|
x-effect="setActiveMarker(currentSubtask?.marker_number || 0)">
|
|
<canvas x-ref="annotationCanvas"
|
|
@click="handleClick($event)"
|
|
class="cursor-pointer"></canvas>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
{# Option C: No image → placeholder #}
|
|
<div x-show="!showSubtaskImage && !showTaskImage" class="absolute inset-0 flex items-center justify-center">
|
|
<div class="text-center">
|
|
<svg class="w-16 h-16 mx-auto text-[var(--text-muted)] mb-3 opacity-30" fill="none" stroke="currentColor" stroke-width="1" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/>
|
|
</svg>
|
|
<p class="text-sm font-medium text-[var(--text-muted)]">{{ _('Nessuna immagine allegata') }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{# ──────────────────────────────────────────────
|
|
RIGHT PANEL — Info + tolerances + numpad (hidden when no subtasks)
|
|
────────────────────────────────────────────── #}
|
|
<div x-show="subtasks.length > 0"
|
|
class="shrink-0 w-72 lg:w-80 bg-[var(--bg-card)] border-l border-[var(--border-color)] flex flex-col right-panel overflow-y-auto">
|
|
|
|
{# ---- Subtask header ---- #}
|
|
<div class="p-3 border-b border-[var(--border-color)]" x-show="currentSubtask">
|
|
<div class="flex items-center gap-2">
|
|
<span class="inline-flex items-center justify-center w-8 h-8 rounded-full
|
|
bg-primary text-white font-bold text-sm shadow marker-active-pulse"
|
|
x-text="currentSubtask?.marker_number"></span>
|
|
<div class="min-w-0 flex-1">
|
|
<h2 class="font-semibold text-[var(--text-primary)] text-sm leading-tight truncate"
|
|
x-text="currentSubtask?.description || '{{ _('Misurazione') }}'"></h2>
|
|
<span class="text-[11px] text-[var(--text-muted)]"
|
|
x-text="(currentSubtask?.measurement_type || '{{ _('Misura') }}') + ' (' + (currentSubtask?.unit || 'mm') + ')'"></span>
|
|
</div>
|
|
<span class="shrink-0 text-[11px] font-mono text-[var(--text-muted)] bg-[var(--bg-secondary)] px-1.5 py-0.5 rounded">
|
|
<span x-text="currentIndex + 1"></span>/<span x-text="totalSubtasks"></span>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{# ---- Tolerance parameters (compact grid) ---- #}
|
|
<div class="px-3 py-2 border-b border-[var(--border-color)]" x-show="currentSubtask">
|
|
{# Nominal row #}
|
|
<div class="flex items-center justify-between py-1 px-2 rounded bg-primary-50 dark:bg-primary-900/20 border border-primary-100 dark:border-primary-800 mb-1.5">
|
|
<span class="text-[10px] font-medium text-primary-700 dark:text-primary-300 uppercase tracking-wider">{{ _('Nominale') }}</span>
|
|
<span class="font-mono font-bold text-primary text-sm"
|
|
x-text="currentSubtask?.nominal?.toFixed(3) || '—'"></span>
|
|
</div>
|
|
|
|
{# 2x2 tolerance grid #}
|
|
<div class="grid grid-cols-2 gap-1 text-[11px]">
|
|
<div class="flex items-center justify-between py-0.5 px-2 rounded bg-red-50/50 dark:bg-red-900/10">
|
|
<span class="text-measure-fail font-medium">UTL</span>
|
|
<span class="font-mono font-semibold text-measure-fail" x-text="currentSubtask?.utl?.toFixed(3) || '—'"></span>
|
|
</div>
|
|
<div class="flex items-center justify-between py-0.5 px-2 rounded bg-red-50/50 dark:bg-red-900/10">
|
|
<span class="text-measure-fail font-medium">LTL</span>
|
|
<span class="font-mono font-semibold text-measure-fail" x-text="currentSubtask?.ltl?.toFixed(3) || '—'"></span>
|
|
</div>
|
|
<div class="flex items-center justify-between py-0.5 px-2 rounded bg-amber-50/50 dark:bg-amber-900/10">
|
|
<span class="text-measure-warning font-medium">UWL</span>
|
|
<span class="font-mono font-semibold text-measure-warning" x-text="currentSubtask?.uwl?.toFixed(3) || '—'"></span>
|
|
</div>
|
|
<div class="flex items-center justify-between py-0.5 px-2 rounded bg-amber-50/50 dark:bg-amber-900/10">
|
|
<span class="text-measure-warning font-medium">LWL</span>
|
|
<span class="font-mono font-semibold text-measure-warning" x-text="currentSubtask?.lwl?.toFixed(3) || '—'"></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{# ---- Tolerance bar (compact) ---- #}
|
|
<div class="px-3 py-2 border-b border-[var(--border-color)]" x-show="currentSubtask">
|
|
<div class="tolerance-bar-bg h-5 rounded-full relative overflow-hidden border border-[var(--border-color)]">
|
|
{# Zone labels #}
|
|
<div class="absolute inset-0 flex items-center justify-between px-1.5 text-[8px] font-mono text-[var(--text-muted)]">
|
|
<span x-text="currentSubtask?.ltl?.toFixed(2)"></span>
|
|
<span x-text="currentSubtask?.nominal?.toFixed(2)"></span>
|
|
<span x-text="currentSubtask?.utl?.toFixed(2)"></span>
|
|
</div>
|
|
{# Center line #}
|
|
<div class="absolute top-0 bottom-0 w-px bg-[var(--text-muted)]/30" style="left: 50%;"></div>
|
|
{# Value needle #}
|
|
<div x-show="currentValue !== null"
|
|
x-transition
|
|
class="absolute top-0 bottom-0 w-1 rounded-full transition-all duration-300"
|
|
:class="{
|
|
'bg-measure-pass': passFailStatus === 'pass',
|
|
'bg-measure-warning': passFailStatus === 'warning',
|
|
'bg-measure-fail': passFailStatus === 'fail'
|
|
}"
|
|
:style="'left: calc(' + progressWidth + '% - 2px)'">
|
|
<div class="absolute -top-0.5 left-1/2 -translate-x-1/2 w-2.5 h-2.5 rounded-full border-2 border-white dark:border-slate-800 shadow"
|
|
:class="{
|
|
'bg-measure-pass': passFailStatus === 'pass',
|
|
'bg-measure-warning': passFailStatus === 'warning',
|
|
'bg-measure-fail': passFailStatus === 'fail'
|
|
}"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{# ---- Already measured indicator ---- #}
|
|
<div x-show="isMeasured(currentSubtask?.id)"
|
|
class="mx-3 mt-2 p-2 rounded-lg bg-measure-pass/10 border border-measure-pass/20">
|
|
<div class="flex items-center gap-1.5 text-xs text-measure-pass font-medium">
|
|
<svg class="w-3.5 h-3.5 shrink-0" 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>
|
|
<span>{{ _('Registrata') }}:</span>
|
|
<span class="font-mono font-bold" x-text="getMeasuredValue(currentSubtask?.id)?.toFixed(3) || '—'"></span>
|
|
<span x-text="currentSubtask?.unit || 'mm'"></span>
|
|
</div>
|
|
</div>
|
|
|
|
{# ---- Measurement feedback ---- #}
|
|
<div class="px-3 pt-2" x-show="currentSubtask" x-data="{ get nominal() { return currentSubtask?.nominal || 0; },
|
|
get utl() { return currentSubtask?.utl || 0; },
|
|
get uwl() { return currentSubtask?.uwl || 0; },
|
|
get lwl() { return currentSubtask?.lwl || 0; },
|
|
get ltl() { return currentSubtask?.ltl || 0; },
|
|
get unit() { return currentSubtask?.unit || 'mm'; } }">
|
|
{% include "components/measurement_feedback.html" %}
|
|
</div>
|
|
|
|
{# ---- Numpad (only when a subtask is active) ---- #}
|
|
<div class="px-3 py-2 relative flex-1" x-show="currentSubtask">
|
|
{# Saving overlay #}
|
|
<div x-show="saving"
|
|
x-transition
|
|
class="absolute inset-0 z-10 bg-white/70 dark:bg-slate-900/70 flex items-center justify-center backdrop-blur-sm rounded">
|
|
<div class="flex items-center gap-2 text-primary font-medium text-sm">
|
|
<svg class="animate-spin w-4 h-4" fill="none" viewBox="0 0 24 24">
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
|
|
</svg>
|
|
{{ _('Salvataggio...') }}
|
|
</div>
|
|
</div>
|
|
|
|
{% include "components/numpad.html" %}
|
|
</div>
|
|
|
|
{# ---- Next measurement indicator ---- #}
|
|
<div class="px-3 pb-2" x-show="currentSubtask">
|
|
{% include "components/next_measurement.html" %}
|
|
</div>
|
|
|
|
|
|
{# ---- Error message ---- #}
|
|
<div x-show="errorMessage"
|
|
x-transition
|
|
x-cloak
|
|
class="mx-3 mb-2 p-2 rounded-lg bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 text-xs text-measure-fail">
|
|
<div class="flex items-center gap-1.5">
|
|
<svg class="w-3.5 h-3.5 shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
</svg>
|
|
<span x-text="errorMessage"></span>
|
|
</div>
|
|
</div>
|
|
</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>
|
|
|
|
{# ================================================================
|
|
AVVIO PRODUZIONE — visible after first cycle, before production started
|
|
================================================================ #}
|
|
<div x-show="cycleConfirmed && cycleCount === 1 && !productionStarted && subtasks.length > 0"
|
|
x-transition
|
|
x-cloak
|
|
class="shrink-0 border-t border-[var(--border-color)] bg-[var(--bg-card)] px-4 py-3">
|
|
<div class="flex items-center justify-center">
|
|
<button @click="startProduction()"
|
|
class="btn gap-2 px-6 py-2.5 text-sm font-bold
|
|
bg-emerald-600 hover:bg-emerald-700 text-white shadow-lg hover:shadow-xl
|
|
rounded-xl transition-all duration-200">
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M5 3l14 9-14 9V3z"/>
|
|
</svg>
|
|
{{ _('Avvio Produzione') }}
|
|
</button>
|
|
</div>
|
|
<p class="text-xs text-center text-[var(--text-muted)] mt-1.5">
|
|
{{ _('Invia segnale al gestionale per avviare il timer della linea') }}
|
|
</p>
|
|
</div>
|
|
<div x-show="productionStarted && subtasks.length > 0"
|
|
x-transition
|
|
x-cloak
|
|
class="shrink-0 border-t border-emerald-200 dark:border-emerald-800 bg-emerald-50 dark:bg-emerald-900/20 px-4 py-1.5">
|
|
<div class="flex items-center justify-center gap-2 text-xs text-emerald-700 dark:text-emerald-300">
|
|
<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>
|
|
<span class="font-medium">{{ _('Produzione avviata') }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{# ================================================================
|
|
FOOTER — Progress bar + navigation
|
|
================================================================ #}
|
|
<div class="shrink-0 bg-[var(--bg-card)] border-t border-[var(--border-color)] shadow-[0_-2px_10px_rgba(0,0,0,0.05)] z-20">
|
|
<div class="px-3 py-2 flex items-center gap-3">
|
|
|
|
{# Left: Back #}
|
|
<a href="{{ url_for('measure.task_list', recipe_id=task.recipe_id or 0) }}"
|
|
class="btn btn-secondary text-xs shrink-0 gap-1 py-1.5 px-2.5">
|
|
<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 19l-7-7m0 0l7-7m-7 7h18"/>
|
|
</svg>
|
|
<span class="hidden sm:inline">{{ _('Task') }}</span>
|
|
</a>
|
|
|
|
{# Center: Progress bar #}
|
|
<div class="flex-1 flex items-center gap-2">
|
|
<span class="text-xs font-medium text-[var(--text-secondary)] shrink-0 font-mono">
|
|
<span x-text="completedCount"></span>/<span x-text="totalSubtasks"></span>
|
|
</span>
|
|
<div class="flex-1 h-2 rounded-full bg-[var(--bg-secondary)] border border-[var(--border-color)] overflow-hidden">
|
|
<div class="h-full rounded-full transition-all duration-500 ease-out"
|
|
:class="isComplete ? 'bg-measure-pass' : 'bg-primary'"
|
|
:style="'width: ' + progressPercent + '%'"></div>
|
|
</div>
|
|
<span class="text-xs font-bold shrink-0"
|
|
:class="isComplete ? 'text-measure-pass' : 'text-primary'"
|
|
x-text="Math.round(progressPercent) + '%'"></span>
|
|
</div>
|
|
|
|
{# 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>
|
|
|
|
{# ================================================================
|
|
COMPLETION OVERLAY
|
|
================================================================ #}
|
|
<div x-show="showCompletionOverlay"
|
|
x-transition:enter="transition ease-out duration-300"
|
|
x-transition:enter-start="opacity-0"
|
|
x-transition:enter-end="opacity-100"
|
|
x-transition:leave="transition ease-in duration-200"
|
|
x-transition:leave-start="opacity-100"
|
|
x-transition:leave-end="opacity-0"
|
|
x-cloak
|
|
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
|
|
<div class="bg-[var(--bg-card)] rounded-2xl shadow-2xl p-8 max-w-sm mx-4 text-center"
|
|
x-transition:enter="transition ease-out duration-300 delay-100"
|
|
x-transition:enter-start="opacity-0 scale-90"
|
|
x-transition:enter-end="opacity-100 scale-100">
|
|
|
|
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-measure-pass/10 mb-4">
|
|
<svg class="w-8 h-8 text-measure-pass" fill="none" stroke="currentColor" stroke-width="2.5" 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>
|
|
</div>
|
|
|
|
<h3 class="text-lg font-bold text-[var(--text-primary)] mb-1">{{ _('Misurazioni Complete') }}</h3>
|
|
<p class="text-sm text-[var(--text-secondary)] mb-2">
|
|
{{ _('Tutte le') }} <span class="font-bold font-mono" x-text="totalSubtasks"></span> {{ _('misurazioni sono state registrate.') }}
|
|
</p>
|
|
|
|
<div class="flex justify-center gap-4 mb-5">
|
|
<div class="text-center">
|
|
<div class="text-xl font-bold font-mono text-measure-pass" x-text="passCount"></div>
|
|
<div class="text-[10px] uppercase tracking-wider text-[var(--text-muted)]">{{ _('Conformi') }}</div>
|
|
</div>
|
|
<div class="text-center" x-show="warningCount > 0">
|
|
<div class="text-xl font-bold font-mono text-measure-warning" x-text="warningCount"></div>
|
|
<div class="text-[10px] uppercase tracking-wider text-[var(--text-muted)]">{{ _('Attenzione') }}</div>
|
|
</div>
|
|
<div class="text-center" x-show="failCount > 0">
|
|
<div class="text-xl font-bold font-mono text-measure-fail" x-text="failCount"></div>
|
|
<div class="text-[10px] uppercase tracking-wider text-[var(--text-muted)]">{{ _('Non Conf.') }}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex gap-2">
|
|
<button @click="goToSummary()"
|
|
class="btn btn-secondary flex-1 justify-center gap-2">
|
|
{{ _('Riepilogo') }}
|
|
</button>
|
|
|
|
{# Production phase: confirm the cycle (starts the interval timer) #}
|
|
<button x-show="productionStarted"
|
|
@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>
|
|
|
|
{# START phase, more tasks to go: advance without blocking or exiting #}
|
|
<button x-show="!productionStarted && !isLastTask"
|
|
@click="goToNextTask()"
|
|
class="btn btn-primary flex-1 justify-center gap-2">
|
|
{{ _('Task successivo') }}
|
|
<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>
|
|
|
|
{# START phase, last task: start production #}
|
|
<button x-show="!productionStarted && isLastTask"
|
|
@click="startProductionFromOverlay()"
|
|
class="btn flex-1 justify-center gap-2 bg-emerald-600 hover:bg-emerald-700 text-white">
|
|
{{ _('Avvio Produzione') }}
|
|
<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 3l14 9-14 9V3z"/>
|
|
</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="closeSupervisorModal()">
|
|
<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="closeSupervisorModal()"
|
|
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>
|
|
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
|
|
<script>
|
|
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
|
|
</script>
|
|
<script src="{{ url_for('static', filename='js/numpad.js') }}"></script>
|
|
<script src="{{ url_for('static', filename='js/annotation-viewer.js') }}?v=6"></script>
|
|
<script src="{{ url_for('static', filename='js/caliper.js') }}"></script>
|
|
|
|
<script>
|
|
/**
|
|
* Task Execute - Main Alpine.js component
|
|
* Manages measurement workflow state, save logic, and navigation.
|
|
* Layout: 3-column (marker sidebar | image center | info+numpad right)
|
|
*/
|
|
function taskExecute() {
|
|
return {
|
|
// ---- Data from server ----
|
|
task: window.__taskData,
|
|
subtasks: window.__taskSubtasks,
|
|
lotNumber: '{{ lot_number or '' }}',
|
|
serialNumber: '{{ serial_number or '' }}',
|
|
// ---- Measurement state ----
|
|
currentIndex: 0,
|
|
measurements: [], // [{subtask_id, value, pass_fail, deviation}, ...]
|
|
saving: false,
|
|
errorMessage: '',
|
|
showCompletionOverlay: false,
|
|
|
|
// ---- Measurement timer ----
|
|
measurementIntervalMinutes: {{ measurement_interval_minutes|tojson if measurement_interval_minutes else 'null' }},
|
|
timerActive: false,
|
|
timerRemaining: 0,
|
|
_timerInterval: null,
|
|
cycleCount: 0,
|
|
productionStarted: 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,
|
|
|
|
// ---- Image switching logic ----
|
|
get currentSubtaskImage() {
|
|
return this.currentSubtask?.image_path || null;
|
|
},
|
|
get showSubtaskImage() {
|
|
return !!this.currentSubtaskImage;
|
|
},
|
|
get showTaskImage() {
|
|
return !this.currentSubtaskImage && !!this.task.file_path;
|
|
},
|
|
|
|
// ---- Computed properties ----
|
|
get currentSubtask() {
|
|
return this.subtasks[this.currentIndex] || null;
|
|
},
|
|
|
|
get nextSubtask() {
|
|
return this.subtasks[this.currentIndex + 1] || null;
|
|
},
|
|
|
|
get totalSubtasks() {
|
|
return this.subtasks.length;
|
|
},
|
|
|
|
get completedCount() {
|
|
return this.measurements.length;
|
|
},
|
|
|
|
get isComplete() {
|
|
return this.completedCount >= this.totalSubtasks;
|
|
},
|
|
|
|
get progressPercent() {
|
|
return this.totalSubtasks > 0
|
|
? (this.completedCount / this.totalSubtasks * 100)
|
|
: 0;
|
|
},
|
|
|
|
// ---- Pass/fail logic ----
|
|
get passFailStatus() {
|
|
if (this.currentValue === null || !this.currentSubtask) return null;
|
|
const v = this.currentValue;
|
|
const s = this.currentSubtask;
|
|
if (s.utl != null && v > s.utl) return 'fail';
|
|
if (s.ltl != null && v < s.ltl) return 'fail';
|
|
if (s.uwl != null && v > s.uwl) return 'warning';
|
|
if (s.lwl != null && v < s.lwl) return 'warning';
|
|
return 'pass';
|
|
},
|
|
|
|
get deviation() {
|
|
if (this.currentValue === null || !this.currentSubtask) return null;
|
|
return this.currentValue - (this.currentSubtask.nominal || 0);
|
|
},
|
|
|
|
get progressWidth() {
|
|
if (!this.currentSubtask || this.currentValue === null) return 50;
|
|
const s = this.currentSubtask;
|
|
if (s.utl == null || s.ltl == null) return 50;
|
|
const range = s.utl - s.ltl;
|
|
if (range <= 0) return 50;
|
|
const pos = (this.currentValue - s.ltl) / range * 100;
|
|
return Math.max(0, Math.min(100, pos));
|
|
},
|
|
|
|
// ---- Summary counts ----
|
|
get passCount() {
|
|
return this.measurements.filter(m => m.pass_fail === 'pass').length;
|
|
},
|
|
get warningCount() {
|
|
return this.measurements.filter(m => m.pass_fail === 'warning').length;
|
|
},
|
|
get failCount() {
|
|
return this.measurements.filter(m => m.pass_fail === 'fail').length;
|
|
},
|
|
|
|
// ---- Init ----
|
|
init() {
|
|
this.subtasks.sort((a, b) => (a.order_index || 0) - (b.order_index || 0));
|
|
},
|
|
|
|
// ---- Check if a subtask has been measured ----
|
|
isMeasured(subtaskId) {
|
|
return this.measurements.some(m => m.subtask_id === subtaskId);
|
|
},
|
|
|
|
getMeasuredValue(subtaskId) {
|
|
const m = this.measurements.find(m => m.subtask_id === subtaskId);
|
|
return m ? m.value : null;
|
|
},
|
|
|
|
getMeasurementStatus(subtaskId) {
|
|
const m = this.measurements.find(m => m.subtask_id === subtaskId);
|
|
return m ? m.pass_fail : null;
|
|
},
|
|
|
|
// ---- Handle numpad confirm ----
|
|
async handleMeasurement(value, inputMethod) {
|
|
if (!this.currentSubtask || this.saving) return;
|
|
|
|
this.currentValue = value;
|
|
this.errorMessage = '';
|
|
|
|
const pf = this.passFailStatus;
|
|
const dev = this.deviation;
|
|
|
|
this.saving = true;
|
|
|
|
try {
|
|
const csrfToken = document.querySelector('meta[name=csrf-token]')?.content || '';
|
|
|
|
const response = await fetch('{{ url_for("measure.save_measurement") }}', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRFToken': csrfToken,
|
|
},
|
|
body: JSON.stringify({
|
|
subtask_id: this.currentSubtask.id,
|
|
version_id: this.task.version_id,
|
|
value: value,
|
|
lot_number: this.lotNumber,
|
|
serial_number: this.serialNumber,
|
|
input_method: inputMethod || 'manual',
|
|
}),
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (!response.ok || result.error) {
|
|
this.errorMessage = result.detail || '{{ _("Errore nel salvataggio della misurazione") }}';
|
|
this.saving = false;
|
|
return;
|
|
}
|
|
|
|
// Record measurement locally
|
|
const existingIdx = this.measurements.findIndex(m => m.subtask_id === this.currentSubtask.id);
|
|
const mEntry = { subtask_id: this.currentSubtask.id, value, pass_fail: pf, deviation: dev };
|
|
if (existingIdx !== -1) {
|
|
this.measurements.splice(existingIdx, 1, mEntry);
|
|
} else {
|
|
this.measurements.push(mEntry);
|
|
}
|
|
|
|
this.saving = false;
|
|
|
|
// Pause to show result feedback
|
|
await new Promise(r => setTimeout(r, 1000));
|
|
|
|
// Out-of-tolerance: block advancement, require supervisor
|
|
if (pf === 'fail') {
|
|
this.pendingAdvance = true;
|
|
this.openSupervisorModal('out_of_tolerance');
|
|
return;
|
|
}
|
|
|
|
// Check if all done
|
|
if (this.completedCount >= this.totalSubtasks) {
|
|
// START run (production not yet started): don't block with the
|
|
// "Misurazioni Complete" overlay — just advance to the next task.
|
|
// The overlay is shown only on the last task (to offer "Avvio
|
|
// Produzione") or once production is running ("Conferma ciclo").
|
|
if (!this.productionStarted && !this.isLastTask) {
|
|
this.goToNextTask();
|
|
return;
|
|
}
|
|
this.showCompletionOverlay = true;
|
|
return;
|
|
}
|
|
|
|
this.advanceToNext();
|
|
|
|
} catch (err) {
|
|
console.error('Save measurement error:', err);
|
|
this.errorMessage = '{{ _("Errore di rete. Riprovare.") }}';
|
|
this.saving = false;
|
|
}
|
|
},
|
|
|
|
// ---- Advance to next unmeasured subtask ----
|
|
advanceToNext() {
|
|
this.currentValue = null;
|
|
|
|
for (let i = this.currentIndex + 1; i < this.totalSubtasks; i++) {
|
|
if (!this.isMeasured(this.subtasks[i].id)) {
|
|
this.currentIndex = i;
|
|
return;
|
|
}
|
|
}
|
|
for (let i = 0; i < this.currentIndex; i++) {
|
|
if (!this.isMeasured(this.subtasks[i].id)) {
|
|
this.currentIndex = i;
|
|
return;
|
|
}
|
|
}
|
|
},
|
|
|
|
// ---- Navigation ----
|
|
goToSubtask(index) {
|
|
if (index >= 0 && index < this.totalSubtasks) {
|
|
this.currentIndex = index;
|
|
this.currentValue = null;
|
|
this.errorMessage = '';
|
|
}
|
|
},
|
|
|
|
goToSubtaskByMarker(markerNumber) {
|
|
const idx = this.subtasks.findIndex(s => s.marker_number === markerNumber);
|
|
if (idx !== -1) {
|
|
this.goToSubtask(idx);
|
|
}
|
|
},
|
|
|
|
// ---- Confirm measurement cycle (Fine ciclo misura) ----
|
|
confirmCycle() {
|
|
this.cycleConfirmed = true;
|
|
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 (_) {}
|
|
},
|
|
|
|
// ---- Avvio Produzione (GAIA placeholder) ----
|
|
startProduction() {
|
|
this.productionStarted = true;
|
|
// TODO: integrazione GAIA — inviare segnale per avviare timer linea
|
|
// await fetch('/measure/api/gaia/start-production', { method: 'POST', ... });
|
|
},
|
|
|
|
// Is the current task the last one in the recipe sequence?
|
|
get isLastTask() {
|
|
const taskIds = window.__allTaskIds || [];
|
|
const idx = taskIds.indexOf(this.task.id);
|
|
return idx === -1 || idx >= taskIds.length - 1;
|
|
},
|
|
|
|
// Start production from the completion overlay (last START task) and begin
|
|
// the measurement-interval cycle if the recipe defines one.
|
|
startProductionFromOverlay() {
|
|
this.showCompletionOverlay = false;
|
|
this.startProduction();
|
|
this.cycleConfirmed = true;
|
|
this.cycleCount++;
|
|
if (this.measurementIntervalMinutes && this.measurementIntervalMinutes > 0) {
|
|
this.startMeasurementTimer();
|
|
}
|
|
},
|
|
|
|
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) ----
|
|
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();
|
|
}
|
|
},
|
|
|
|
// ---- Open/close supervisor modal (always reset fields) ----
|
|
openSupervisorModal(action) {
|
|
this.supervisorAction = action;
|
|
this.supervisorUsername = '';
|
|
this.supervisorPassword = '';
|
|
this.supervisorError = '';
|
|
this.showSupervisorModal = true;
|
|
},
|
|
|
|
closeSupervisorModal() {
|
|
this.showSupervisorModal = false;
|
|
this.supervisorUsername = '';
|
|
this.supervisorPassword = '';
|
|
this.supervisorError = '';
|
|
},
|
|
|
|
// ---- 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;
|
|
window.location.href = '{{ url_for("measure.task_complete", recipe_id=0) }}'.replace('/0', '/' + recipeId) +
|
|
'?version_id=' + encodeURIComponent(this.task.version_id || '');
|
|
},
|
|
};
|
|
}
|
|
</script>
|
|
{% endblock %}
|