5 Commits

Author SHA1 Message Date
Adriano Dal Pastro 34a72e6f1a fix(measure): skip completion overlay during START, auto-advance task
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>
2026-06-05 09:36:29 +00:00
Adriano Dal Pastro 4c75a32186 fix(measure): START cycle advances to next task instead of blocking
When all subtasks of a task were measured, the completion overlay only
offered "Riepilogo" (which leaves the work cycle) or "Conferma ciclo" (which
keeps the operator stuck on the same task and starts the interval timer).
During the START run — before production is started — neither lets the
operator simply move to the next task.

Make the overlay phase-aware:
- Production started: keep "Conferma ciclo".
- START, not the last task: primary action "Task successivo" -> goToNextTask().
- START, last task: "Avvio Produzione" -> startProductionFromOverlay(), which
  starts production and kicks off the interval cycle.

Add isLastTask getter and startProductionFromOverlay(); EN translation for the
new label.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 09:30:52 +00:00
Adriano Dal Pastro fdfe1072d8 fix(measure): supervisor modal — backspace works and fields reset
Two issues with the "Autorizzazione capoturno" modal:

1. Backspace and digits did not work in the username/password fields. The
   numpad component installs a window-level keydown handler (for the USB
   caliper / keyboard wedge) that preventDefault()s digits, Backspace, Enter
   etc. It swallowed those keys inside the modal inputs. Guard handleKeydown
   to ignore events whose target is an editable field (INPUT/TEXTAREA/SELECT/
   contentEditable); the numpad has no text inputs of its own.

2. The modal kept the previous username/password: opening only set the flag,
   and closing via backdrop click did not clear the fields. Add
   openSupervisorModal()/closeSupervisorModal() that always reset username,
   password and error, and use them for every open (fermo_linea,
   fine_produzione, out_of_tolerance) and close (backdrop, Annulla).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 09:11:31 +00:00
Adriano Dal Pastro 6ff78b2150 feat(roles): add dedicated Supervisor (capoturno) role
Until now the out-of-tolerance authorization ("Autorizzazione capoturno")
required is_admin, conflating shift-leader authority with full system
administration. Introduce a combinable Supervisor role so a capoturno can
authorize overrides without admin privileges (roadmap D-0.8 / Phase 2).

Backend:
- users router: add "Supervisor" to the allowed roles (centralized as
  VALID_ROLES, deduplicating the create/update validation).
- middleware: add require_supervisor dependency (admins still bypass).

Frontend:
- measure.validate_supervisor: authorize users with the Supervisor role OR
  is_admin (was is_admin only).
- admin/users: Supervisor checkbox + orange role badge.
- profile: explicit Supervisor badge styling.
- EN translation for the new role label.

roles is a free JSON column, so no migration is required.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 09:02:24 +00:00
Adriano Dal Pastro bff577b461 fix(measure): separate traceability inputs from recipe search
On the recipe selection page the Lotto/Seriale fields shared a single
filter bar with the "Cerca ricetta" search box, so operators read them as
search filters instead of data-entry fields and never filled them in.

Split into two cards: a search filter, and a distinct "Tracciabilità"
section with heading, helper text, accent border and monospace inputs with
amber/indigo icons matching the lot/serial badges shown downstream. Add EN
translations for the new strings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 08:48:54 +00:00
10 changed files with 167 additions and 38 deletions
+2
View File
@@ -6,6 +6,7 @@ from src.backend.api.middleware.api_key import (
require_maker, require_maker,
require_measurement_tec, require_measurement_tec,
require_metrologist, require_metrologist,
require_supervisor,
require_admin_user, require_admin_user,
) )
from src.backend.api.middleware.logging import AccessLogMiddleware from src.backend.api.middleware.logging import AccessLogMiddleware
@@ -19,6 +20,7 @@ __all__ = [
"require_maker", "require_maker",
"require_measurement_tec", "require_measurement_tec",
"require_metrologist", "require_metrologist",
"require_supervisor",
"require_admin_user", "require_admin_user",
"AccessLogMiddleware", "AccessLogMiddleware",
"RateLimitMiddleware", "RateLimitMiddleware",
+1
View File
@@ -68,4 +68,5 @@ def require_admin():
require_maker = require_role("Maker") require_maker = require_role("Maker")
require_measurement_tec = require_role("MeasurementTec") require_measurement_tec = require_role("MeasurementTec")
require_metrologist = require_role("Metrologist") require_metrologist = require_role("Metrologist")
require_supervisor = require_role("Supervisor")
require_admin_user = require_admin() require_admin_user = require_admin()
+8 -6
View File
@@ -11,6 +11,10 @@ from src.backend.services.auth_service import create_user, hash_password, regene
router = APIRouter(prefix="/api/users", tags=["users"]) router = APIRouter(prefix="/api/users", tags=["users"])
# Combinable user roles. Supervisor (capoturno) authorizes out-of-tolerance
# overrides during measurement without needing full admin privileges.
VALID_ROLES = {"Maker", "MeasurementTec", "Metrologist", "Supervisor"}
@router.get("", response_model=list[UserResponse]) @router.get("", response_model=list[UserResponse])
async def list_users( async def list_users(
@@ -40,12 +44,11 @@ async def create_new_user(
detail=f"Username '{data.username}' already exists", detail=f"Username '{data.username}' already exists",
) )
# Validate roles # Validate roles
valid_roles = {"Maker", "MeasurementTec", "Metrologist"}
for role in data.roles: for role in data.roles:
if role not in valid_roles: if role not in VALID_ROLES:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=f"Invalid role '{role}'. Valid roles: {valid_roles}", detail=f"Invalid role '{role}'. Valid roles: {VALID_ROLES}",
) )
user = await create_user( user = await create_user(
db, db,
@@ -77,12 +80,11 @@ async def update_user(
update_data = data.model_dump(exclude_unset=True) update_data = data.model_dump(exclude_unset=True)
# Validate roles if provided # Validate roles if provided
if "roles" in update_data: if "roles" in update_data:
valid_roles = {"Maker", "MeasurementTec", "Metrologist"}
for role in update_data["roles"]: for role in update_data["roles"]:
if role not in valid_roles: if role not in VALID_ROLES:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=f"Invalid role '{role}'. Valid roles: {valid_roles}", detail=f"Invalid role '{role}'. Valid roles: {VALID_ROLES}",
) )
for field, value in update_data.items(): for field, value in update_data.items():
setattr(user, field, value) setattr(user, field, value)
+2 -1
View File
@@ -347,7 +347,8 @@ def validate_supervisor():
return jsonify({"error": True, "detail": "Credenziali non valide"}), 401 return jsonify({"error": True, "detail": "Credenziali non valide"}), 401
user = resp.get("user", {}) user = resp.get("user", {})
if not user.get("is_admin"): is_supervisor = "Supervisor" in (user.get("roles") or [])
if not (is_supervisor or user.get("is_admin")):
return jsonify({"error": True, "detail": "Utente non autorizzato (richiesto capoturno)"}), 403 return jsonify({"error": True, "detail": "Utente non autorizzato (richiesto capoturno)"}), 403
return jsonify({"authorized": True, "supervisor": user.get("display_name", username)}), 200 return jsonify({"authorized": True, "supervisor": user.get("display_name", username)}), 200
@@ -163,6 +163,17 @@ function numpad() {
* @param {KeyboardEvent} e - The keyboard event * @param {KeyboardEvent} e - The keyboard event
*/ */
handleKeydown(e) { handleKeydown(e) {
// Ignore keystrokes aimed at an editable field (e.g. the supervisor
// login modal). The numpad has no text inputs of its own, so this
// window-level capture is only meant for the USB caliper / keyboard
// wedge when no field is focused. Without this guard the numpad would
// swallow digits and Backspace inside those inputs.
const t = e.target;
if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' ||
t.tagName === 'SELECT' || t.isContentEditable)) {
return;
}
// Number keys // Number keys
if (e.key >= '0' && e.key <= '9') { if (e.key >= '0' && e.key <= '9') {
e.preventDefault(); e.preventDefault();
@@ -78,7 +78,8 @@
:class="{ :class="{
'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300': role === 'Maker', 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300': role === 'Maker',
'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300': role === 'MeasurementTec', 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300': role === 'MeasurementTec',
'bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300': role === 'Metrologist' 'bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300': role === 'Metrologist',
'bg-orange-100 dark:bg-orange-900/30 text-orange-700 dark:text-orange-300': role === 'Supervisor'
}" }"
x-text="role"></span> x-text="role"></span>
</template> </template>
@@ -233,6 +234,11 @@
class="rounded border-[var(--border-color)] text-primary focus:ring-primary/30"> class="rounded border-[var(--border-color)] text-primary focus:ring-primary/30">
<span class="text-sm text-[var(--text-primary)]">Metrologist</span> <span class="text-sm text-[var(--text-primary)]">Metrologist</span>
</label> </label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" x-model="form.roles" value="Supervisor"
class="rounded border-[var(--border-color)] text-primary focus:ring-primary/30">
<span class="text-sm text-[var(--text-primary)]">{{ _('Supervisor (capoturno)') }}</span>
</label>
</div> </div>
</div> </div>
@@ -87,6 +87,8 @@
bg-blue-100 dark:bg-blue-900/30 text-blue-800 dark:text-blue-200 border border-blue-200 dark:border-blue-800 bg-blue-100 dark:bg-blue-900/30 text-blue-800 dark:text-blue-200 border border-blue-200 dark:border-blue-800
{% elif role == 'Metrologist' %} {% elif role == 'Metrologist' %}
bg-purple-100 dark:bg-purple-900/30 text-purple-800 dark:text-purple-200 border border-purple-200 dark:border-purple-800 bg-purple-100 dark:bg-purple-900/30 text-purple-800 dark:text-purple-200 border border-purple-200 dark:border-purple-800
{% elif role == 'Supervisor' %}
bg-orange-100 dark:bg-orange-900/30 text-orange-800 dark:text-orange-200 border border-orange-200 dark:border-orange-800
{% else %} {% else %}
bg-slate-100 dark:bg-slate-700 text-slate-800 dark:text-slate-200 border border-slate-200 dark:border-slate-600 bg-slate-100 dark:bg-slate-700 text-slate-800 dark:text-slate-200 border border-slate-200 dark:border-slate-600
{% endif %}"> {% endif %}">
@@ -92,40 +92,61 @@
</div> </div>
</div> </div>
<!-- Search + Traceability Bar --> <!-- Search Bar (filter) -->
<div class="tmf-card mb-6"> <div class="tmf-card mb-4">
<div class="p-4 sm:p-5"> <div class="p-4 sm:p-5">
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4"> <label class="tmf-label">
<!-- Search Input --> <svg class="w-3.5 h-3.5 inline-block mr-1 -mt-0.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<div class="sm:col-span-2"> <path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
<label class="tmf-label"> </svg>
<svg class="w-3.5 h-3.5 inline-block mr-1 -mt-0.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"> {{ _('Cerca ricetta') }}
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/> </label>
</svg> <input type="text"
{{ _('Cerca ricetta') }} x-model="search"
</label> placeholder="{{ _('Nome, codice o descrizione...') }}"
<input type="text" class="tmf-input">
x-model="search" </div>
placeholder="{{ _('Nome, codice o descrizione...') }}" </div>
class="tmf-input">
</div>
<!-- Traceability (data entry, NOT a search filter) -->
<div class="tmf-card mb-6 border-l-4 border-l-primary">
<div class="p-4 sm:p-5">
<div class="flex items-center gap-2 mb-3">
<svg class="w-5 h-5 text-primary 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>
<div>
<h2 class="text-sm font-semibold text-[var(--text-primary)] leading-tight">{{ _('Tracciabilità') }}</h2>
<p class="text-xs text-[var(--text-muted)] leading-tight">{{ _('Dati del pezzo da misurare — compila prima di selezionare la ricetta') }}</p>
</div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<!-- Lot Number --> <!-- Lot Number -->
<div> <div>
<label class="tmf-label">{{ _('Numero Lotto') }}</label> <label class="tmf-label flex items-center gap-1.5">
<svg class="w-3.5 h-3.5 text-amber-500" fill="none" stroke="currentColor" stroke-width="2" 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>
{{ _('Numero Lotto') }}
</label>
<input type="text" <input type="text"
x-model="lot_number" x-model="lot_number"
placeholder="{{ _('Opzionale') }}" placeholder="{{ _('Es. LOT-2026-001 (opzionale)') }}"
class="tmf-input"> class="tmf-input font-mono">
</div> </div>
<!-- Serial Number --> <!-- Serial Number -->
<div> <div>
<label class="tmf-label">{{ _('Numero Seriale') }}</label> <label class="tmf-label flex items-center gap-1.5">
<svg class="w-3.5 h-3.5 text-indigo-500" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M7 20l4-16m2 16l4-16M6 9h14M4 15h14"/>
</svg>
{{ _('Numero Seriale') }}
</label>
<input type="text" <input type="text"
x-model="serial_number" x-model="serial_number"
placeholder="{{ _('Opzionale') }}" placeholder="{{ _('Es. SN-000123 (opzionale)') }}"
class="tmf-input"> class="tmf-input font-mono">
</div> </div>
</div> </div>
</div> </div>
@@ -116,14 +116,14 @@
{# Fermo linea + Fine produzione (measurement tasks only) #} {# Fermo linea + Fine produzione (measurement tasks only) #}
<template x-if="subtasks.length > 0"> <template x-if="subtasks.length > 0">
<div class="shrink-0 flex items-center gap-1.5"> <div class="shrink-0 flex items-center gap-1.5">
<button @click="showSupervisorModal = true; supervisorAction = 'fermo_linea'" <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"> 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"> <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"/> <path stroke-linecap="round" stroke-linejoin="round" d="M10 9v6m4-6v6m7-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg> </svg>
{{ _('Fermo linea') }} {{ _('Fermo linea') }}
</button> </button>
<button @click="showSupervisorModal = true; supervisorAction = 'fine_produzione'" <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"> 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"> <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="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
@@ -584,13 +584,36 @@
class="btn btn-secondary flex-1 justify-center gap-2"> class="btn btn-secondary flex-1 justify-center gap-2">
{{ _('Riepilogo') }} {{ _('Riepilogo') }}
</button> </button>
<button @click="showCompletionOverlay = false; cycleConfirmed = true"
{# 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"> class="btn btn-primary flex-1 justify-center gap-2">
{{ _('Conferma ciclo') }} {{ _('Conferma ciclo') }}
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"> <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"/> <path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/>
</svg> </svg>
</button> </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> </div>
</div> </div>
@@ -607,7 +630,7 @@
x-transition:leave-end="opacity-0" x-transition:leave-end="opacity-0"
x-cloak x-cloak
class="fixed inset-0 z-[60] flex items-center justify-center bg-black/50 backdrop-blur-sm" class="fixed inset-0 z-[60] flex items-center justify-center bg-black/50 backdrop-blur-sm"
@click.self="showSupervisorModal = false"> @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="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="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"> <div class="w-10 h-10 rounded-full bg-amber-100 dark:bg-amber-900/30 flex items-center justify-center">
@@ -636,7 +659,7 @@
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
<button @click="showSupervisorModal = false; supervisorUsername = ''; supervisorPassword = ''; supervisorError = ''" <button @click="closeSupervisorModal()"
class="btn btn-secondary flex-1 text-sm"> class="btn btn-secondary flex-1 text-sm">
{{ _('Annulla') }} {{ _('Annulla') }}
</button> </button>
@@ -858,13 +881,20 @@ function taskExecute() {
// Out-of-tolerance: block advancement, require supervisor // Out-of-tolerance: block advancement, require supervisor
if (pf === 'fail') { if (pf === 'fail') {
this.pendingAdvance = true; this.pendingAdvance = true;
this.supervisorAction = 'out_of_tolerance'; this.openSupervisorModal('out_of_tolerance');
this.showSupervisorModal = true;
return; return;
} }
// Check if all done — show overlay but do NOT auto-navigate // Check if all done
if (this.completedCount >= this.totalSubtasks) { 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; this.showCompletionOverlay = true;
return; return;
} }
@@ -981,6 +1011,25 @@ function taskExecute() {
// await fetch('/measure/api/gaia/start-production', { method: 'POST', ... }); // 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() { get timerDisplay() {
var m = Math.floor(this.timerRemaining / 60); var m = Math.floor(this.timerRemaining / 60);
var s = this.timerRemaining % 60; var s = this.timerRemaining % 60;
@@ -998,6 +1047,22 @@ function taskExecute() {
} }
}, },
// ---- 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 ---- // ---- Supervisor reason text ----
get supervisorReason() { get supervisorReason() {
if (this.supervisorAction === 'out_of_tolerance') return '{{ _("Misurazione fuori tolleranza") }}'; if (this.supervisorAction === 'out_of_tolerance') return '{{ _("Misurazione fuori tolleranza") }}';
@@ -2173,3 +2173,21 @@ msgstr "Error generating report"
#~ msgid "Segui le istruzioni nella direttiva" #~ msgid "Segui le istruzioni nella direttiva"
#~ msgstr "Follow the instructions in the directive" #~ msgstr "Follow the instructions in the directive"
msgid "Tracciabilità"
msgstr "Traceability"
msgid "Dati del pezzo da misurare — compila prima di selezionare la ricetta"
msgstr "Part data to be measured — fill in before selecting the recipe"
msgid "Es. LOT-2026-001 (opzionale)"
msgstr "E.g. LOT-2026-001 (optional)"
msgid "Es. SN-000123 (opzionale)"
msgstr "E.g. SN-000123 (optional)"
msgid "Supervisor (capoturno)"
msgstr "Supervisor (shift leader)"
msgid "Task successivo"
msgstr "Next task"