Compare commits
5 Commits
5cc5123a0c
...
34a72e6f1a
| Author | SHA1 | Date | |
|---|---|---|---|
| 34a72e6f1a | |||
| 4c75a32186 | |||
| fdfe1072d8 | |||
| 6ff78b2150 | |||
| bff577b461 |
@@ -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",
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,12 +92,9 @@
|
|||||||
</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">
|
|
||||||
<!-- Search Input -->
|
|
||||||
<div class="sm:col-span-2">
|
|
||||||
<label class="tmf-label">
|
<label class="tmf-label">
|
||||||
<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">
|
<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">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
||||||
@@ -109,23 +106,47 @@
|
|||||||
placeholder="{{ _('Nome, codice o descrizione...') }}"
|
placeholder="{{ _('Nome, codice o descrizione...') }}"
|
||||||
class="tmf-input">
|
class="tmf-input">
|
||||||
</div>
|
</div>
|
||||||
|
</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"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user