feat(measure): l'operatore entra nella sequenza, non in un elenco
Punto 10. Sceglere una ricetta apriva la lista completa dei task e chiedeva all'operatore di decidere da dove cominciare prima ancora di aver fatto qualcosa. Ora AVVIA IN SEQUENZA porta dentro il primo task; la lista resta, un livello sotto, per quando serve vederla tutta o tornare indietro. - nuova rotta /measure/start/<ricetta>: memorizza lotto e seriale (prima lo faceva la lista, che ora si salta), verifica la tracciabilità obbligatoria e apre il primo task - GET /api/measurements/task-progress: quante quote ha già preso ogni task di misura, contate per quota e non per tentativo, delimitate dalla produzione aperta o, fuori produzione, dall'operatore - la lista distingue «Incompiuto 1/3» da «Completato 3/3»: un task lasciato a metà non somigliava più a uno mai aperto - «Visualizza singolo TASK» non è più riservato al Maker — dire «incompiuto» senza dare la strada per tornarci sarebbe una lamentela, non una funzione - nel task «Riepilogo» diventa «Completato» e si apre solo quando il ciclo è chiuso: altrimenti sarebbe la scorciatoia che rende facoltative le quote - «Fine ciclo misura» si vede da subito, spento, e dice quante quote mancano; prima compariva a task già finito, quando non serviva più saperlo Il pulsante verde del footer diventa «Task successivo»: due bottoni con la stessa parola addosso sono uno di troppo. Test: 334 (erano 309). Il side effect dei mock del client Flask ora risponde per endpoint invece che per turno — una lista posizionale si rompe appena una pagina fa una domanda in più al server, che è come crescono tutte. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -119,6 +119,112 @@ def select_recipe():
|
||||
)
|
||||
|
||||
|
||||
def _remember_traceability() -> tuple[str, str]:
|
||||
"""Carry lot and serial from the query string into the session."""
|
||||
lot_number = request.args.get("lot_number", session.get("lot_number", ""))
|
||||
serial_number = request.args.get(
|
||||
"serial_number", session.get("serial_number", ""),
|
||||
)
|
||||
if lot_number:
|
||||
session["lot_number"] = lot_number
|
||||
if serial_number:
|
||||
session["serial_number"] = serial_number
|
||||
return lot_number, serial_number
|
||||
|
||||
|
||||
def _missing_traceability(recipe: dict, lot: str, serial: str) -> list[str]:
|
||||
"""The compulsory fields this recipe is still owed."""
|
||||
missing = []
|
||||
if recipe.get("requires_lot") and not (lot or "").strip():
|
||||
missing.append(_("lotto"))
|
||||
if recipe.get("requires_serial") and not (serial or "").strip():
|
||||
missing.append(_("seriale"))
|
||||
return missing
|
||||
|
||||
|
||||
def _ordered_tasks(recipe_id: int) -> list[dict] | None:
|
||||
"""Tasks of a recipe's current version in execution order, or None on error."""
|
||||
resp = api_client.get(f"/api/recipes/{recipe_id}/tasks")
|
||||
if isinstance(resp, dict) and resp.get("error"):
|
||||
return None
|
||||
tasks = resp if isinstance(resp, list) else resp.get("items", [])
|
||||
return sorted(tasks, key=lambda t: (t.get("order_index", 0), t.get("id", 0)))
|
||||
|
||||
|
||||
def _task_progress(version_id, run_id) -> dict[int, dict]:
|
||||
"""Quotes already taken per measurement task, keyed by task id.
|
||||
|
||||
Never fatal: a list that cannot say which task was left half done is still a
|
||||
usable list, and losing the whole page over a badge would be the worse trade.
|
||||
"""
|
||||
if not version_id:
|
||||
return {}
|
||||
params = {"version_id": version_id}
|
||||
if run_id:
|
||||
params["production_run_id"] = run_id
|
||||
resp = api_client.get("/api/measurements/task-progress", params=params)
|
||||
if not isinstance(resp, dict) or resp.get("error"):
|
||||
return {}
|
||||
return {row["task_id"]: row for row in resp.get("tasks", [])}
|
||||
|
||||
|
||||
def _current_run_id() -> int | None:
|
||||
"""The production open at this station, if there is one."""
|
||||
station_code, _overridden = _current_station()
|
||||
if not station_code:
|
||||
return None
|
||||
# The endpoint answers with the run itself, or null when nothing is running.
|
||||
resp = api_client.get(
|
||||
"/api/production-runs/current", params={"station_code": station_code},
|
||||
)
|
||||
if not isinstance(resp, dict) or resp.get("error"):
|
||||
return None
|
||||
return resp.get("id")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Route: start the guided sequence
|
||||
# ---------------------------------------------------------------------------
|
||||
@measure_bp.route("/start/<int:recipe_id>")
|
||||
@login_required
|
||||
@role_required("MeasurementTec")
|
||||
def start_sequence(recipe_id: int):
|
||||
"""Open the first task of a recipe and let the operator walk the sequence.
|
||||
|
||||
Choosing a recipe used to land on the full task list, which asked the operator
|
||||
to pick a starting point before they had done anything. The sequence is the
|
||||
normal way through; the list is still there, one level down, for when they need
|
||||
to see the whole of it.
|
||||
"""
|
||||
lot_number, serial_number = _remember_traceability()
|
||||
|
||||
recipe_resp = api_client.get(f"/api/recipes/{recipe_id}")
|
||||
if recipe_resp.get("error"):
|
||||
flash(
|
||||
_("Ricetta non trovata: %(detail)s",
|
||||
detail=recipe_resp.get("detail", "")),
|
||||
"error",
|
||||
)
|
||||
return redirect(url_for("measure.select_recipe"))
|
||||
|
||||
missing = _missing_traceability(recipe_resp, lot_number, serial_number)
|
||||
if missing:
|
||||
flash(
|
||||
_("Compila prima: %(fields)s", fields=", ".join(missing)),
|
||||
"error",
|
||||
)
|
||||
return redirect(url_for(
|
||||
"measure.select_recipe", recipe=recipe_resp.get("code", ""),
|
||||
))
|
||||
|
||||
tasks = _ordered_tasks(recipe_id)
|
||||
if not tasks:
|
||||
flash(_("Questa ricetta non ha ancora task definiti."), "error")
|
||||
return redirect(url_for("measure.task_list", recipe_id=recipe_id))
|
||||
|
||||
return redirect(url_for("measure.task_execute", task_id=tasks[0]["id"]))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Route: Task list for a recipe
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -127,17 +233,7 @@ def select_recipe():
|
||||
@role_required("MeasurementTec")
|
||||
def task_list(recipe_id: int):
|
||||
"""Task list for selected recipe."""
|
||||
# Persist lot/serial from query params into session
|
||||
lot_number = request.args.get(
|
||||
"lot_number", session.get("lot_number", ""),
|
||||
)
|
||||
serial_number = request.args.get(
|
||||
"serial_number", session.get("serial_number", ""),
|
||||
)
|
||||
if lot_number:
|
||||
session["lot_number"] = lot_number
|
||||
if serial_number:
|
||||
session["serial_number"] = serial_number
|
||||
lot_number, serial_number = _remember_traceability()
|
||||
|
||||
# Load recipe details
|
||||
recipe_resp = api_client.get(f"/api/recipes/{recipe_id}")
|
||||
@@ -149,22 +245,19 @@ def task_list(recipe_id: int):
|
||||
)
|
||||
return redirect(url_for("measure.select_recipe"))
|
||||
|
||||
# Load tasks for this recipe
|
||||
tasks_resp = api_client.get(f"/api/recipes/{recipe_id}/tasks")
|
||||
if isinstance(tasks_resp, dict) and tasks_resp.get("error"):
|
||||
flash(
|
||||
_("Errore nel caricamento dei task: %(detail)s",
|
||||
detail=tasks_resp.get("detail", "")),
|
||||
"error",
|
||||
)
|
||||
tasks = _ordered_tasks(recipe_id)
|
||||
if tasks is None:
|
||||
flash(_("Errore nel caricamento dei task."), "error")
|
||||
tasks = []
|
||||
else:
|
||||
tasks = tasks_resp if isinstance(tasks_resp, list) else tasks_resp.get("items", [])
|
||||
|
||||
version = recipe_resp.get("current_version") or {}
|
||||
progress = _task_progress(version.get("id"), _current_run_id())
|
||||
|
||||
return render_template(
|
||||
"measure/task_list.html",
|
||||
recipe=recipe_resp,
|
||||
tasks=tasks,
|
||||
progress=progress,
|
||||
lot_number=lot_number,
|
||||
serial_number=serial_number,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user