Phase 5: Supervisor and Stall screens, weld map, heat table, clock and CO verification
The shop at parity with the old build. Supervisor screen (department chips, CO headline, numbers, Change orders
on hold, station grid with clocks and CO tags, boards) and Stall screen (station picker, hero, My packets,
Submitted). Shop steps on the packet page: assign with station, welder picker and stamp; pull back; Start, Pause,
Resume; Submit with a PIN signature and the missing steps as links; review with Send to Q.C., Return to station
and Skip Q.C. route; a lead's read-only review. Weld map (pdf.js viewer with pan, pinch, zoom, full screen; marks
with leader lines; weld sheet; weld record with field CAS; carried marks). Heat map (heat table placed, dragged,
sized, removed; legacy amber tags; borrowed heat map read-only). Checklist tab with templates from settings.
VOID stamps on voided pages. Change orders: CO-n labels, supervisor verify with signature, approval copies the
heat table to the new sheet and the BOM merge keeps the shop's values. Server clock of record with VISUAL ticket
hours beside it. Presence per packet over SSE. Migration 0006 (bfx, bfy, carried_from, checklist_templates).
New endpoints /api/shop/board, /api/shop/stations, /api/shop/stall, /api/departments/{code}/people.
Tests: policy oracle for every shop action, role and status; clock arithmetic; concurrent inserts; CO apply;
frontend geometry, heat table, clock, checklist, weld sheet, screens; seven Playwright scenarios. Docs updated.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
@@ -1,14 +1,19 @@
|
||||
"""The department checklists and the Q.C. checks, ported from the old checklistFor(), qcChecksFor() and qcMissing().
|
||||
|
||||
Weld departments get the welding list, the others the plain list. The drawing, rev and WPS are filled into the text.
|
||||
The lists live in the settings row `checklist_templates` (migration 0006); the defaults here are the fallback.
|
||||
The keys are the old keys, so a checklist stored as {key: {done, at, by, stamp}} reads the same.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import re
|
||||
from collections.abc import Iterable, Mapping
|
||||
from typing import Any
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
NO_REV = "-"
|
||||
NDE_NONE = ("", "none", "none required")
|
||||
NDE_OK = ("acceptable", "accept", "accepted")
|
||||
@@ -19,31 +24,95 @@ def _item(group: str, key: str, text: str) -> dict[str, str]:
|
||||
return {"group": group, "key": key, "text": text}
|
||||
|
||||
|
||||
def checklist_for(weld: bool, drawing: str | None, rev: str | None, wps: str | None) -> list[dict[str, str]]:
|
||||
DWG_TEXT = "Drawing {drawing} Rev {rev} matches the packet"
|
||||
|
||||
# The defaults. The settings row `checklist_templates` holds the same lists (migration 0006); these are the fallback
|
||||
# when that row is missing or does not parse.
|
||||
DEFAULT_TEMPLATES: dict[str, list[dict[str, str]]] = {
|
||||
"weld": [
|
||||
_item("Before welding", "dwg", DWG_TEXT),
|
||||
_item("Before welding", "heat", "Heat numbers on material match the Materials tab"),
|
||||
_item("Before welding", "wps", "WPS reviewed: {wps}"),
|
||||
_item("Before welding", "fit", "Fit-up and dimensions checked against the drawing"),
|
||||
_item("Before welding", "pre", "Preheat and interpass temperature per WPS"),
|
||||
_item("After welding", "vt", "Visual inspection of all welds acceptable"),
|
||||
_item("After welding", "stamp", "Welds stamped with welder ID"),
|
||||
_item("After welding", "clean", "Slag and spatter removed"),
|
||||
_item("After welding", "qty", "Completed quantity matches each work order"),
|
||||
],
|
||||
"plain": [
|
||||
_item("Before starting", "dwg", DWG_TEXT),
|
||||
_item("Before starting", "prev", "Previous department's work complete and released"),
|
||||
_item("Before starting", "mat", "Material and parts on hand match the packet"),
|
||||
_item("When done", "work", "Work completed per drawing and procedure"),
|
||||
_item("When done", "insp", "Self-inspection done; no damage to finished surfaces"),
|
||||
_item("When done", "qty", "Completed quantity matches each work order"),
|
||||
],
|
||||
}
|
||||
SETTINGS_KEY = "checklist_templates"
|
||||
RX_KEY = re.compile(r"^[a-z][a-z0-9_]{0,29}$")
|
||||
RX_PLACEHOLDER = re.compile(r"\{(drawing|rev|wps)\}")
|
||||
TEXT_MAX = 200
|
||||
ITEMS_MAX = 40
|
||||
|
||||
|
||||
def _valid_list(value: Any) -> list[dict[str, str]] | None:
|
||||
"""A template list from settings, or None when it does not have the right shape."""
|
||||
if not isinstance(value, list) or not value or len(value) > ITEMS_MAX:
|
||||
return None
|
||||
out: list[dict[str, str]] = []
|
||||
seen: set[str] = set()
|
||||
for it in value:
|
||||
if not isinstance(it, Mapping):
|
||||
return None
|
||||
group, key, text = it.get("group"), it.get("key"), it.get("text")
|
||||
if not (isinstance(group, str) and isinstance(key, str) and isinstance(text, str)):
|
||||
return None
|
||||
if not RX_KEY.match(key) or key in seen or not group.strip() or not text.strip():
|
||||
return None
|
||||
seen.add(key)
|
||||
out.append(_item(group.strip()[:TEXT_MAX], key, text.strip()[:TEXT_MAX]))
|
||||
return out
|
||||
|
||||
|
||||
def parse_templates(value: Any) -> dict[str, list[dict[str, str]]] | None:
|
||||
"""The `checklist_templates` settings value, checked. None when it is malformed."""
|
||||
if not isinstance(value, Mapping):
|
||||
return None
|
||||
weld, plain = _valid_list(value.get("weld")), _valid_list(value.get("plain"))
|
||||
if weld is None or plain is None:
|
||||
return None
|
||||
return {"weld": weld, "plain": plain}
|
||||
|
||||
|
||||
def load_templates(conn: Any) -> dict[str, list[dict[str, str]]]:
|
||||
"""The department checklists from settings, else the defaults in code (with a warning when the row is bad)."""
|
||||
row = conn.execute("SELECT value FROM settings WHERE key = %s", (SETTINGS_KEY,)).fetchone()
|
||||
if row is None:
|
||||
return DEFAULT_TEMPLATES
|
||||
parsed = parse_templates(row[0])
|
||||
if parsed is None:
|
||||
log.warning("settings %s does not parse; using the default checklists", SETTINGS_KEY)
|
||||
return DEFAULT_TEMPLATES
|
||||
return parsed
|
||||
|
||||
|
||||
def _fill(text: str, drawing: str | None, rev: str | None, wps: str | None) -> str:
|
||||
values = {"drawing": drawing or "", "rev": rev or NO_REV, "wps": wps or "per drawing"}
|
||||
return " ".join(RX_PLACEHOLDER.sub(lambda m: values[m.group(1)], text).split())
|
||||
|
||||
|
||||
def checklist_for(
|
||||
weld: bool,
|
||||
drawing: str | None,
|
||||
rev: str | None,
|
||||
wps: str | None,
|
||||
templates: Mapping[str, list[dict[str, str]]] | None = None,
|
||||
) -> list[dict[str, str]]:
|
||||
"""The stage checklist for a department, as a flat list of {group, key, text} in display order."""
|
||||
dwg = f"Drawing {drawing or ''} Rev {rev or NO_REV} matches the packet".replace(" ", " ")
|
||||
if not weld:
|
||||
g1, g2 = "Before starting", "When done"
|
||||
return [
|
||||
_item(g1, "dwg", dwg),
|
||||
_item(g1, "prev", "Previous department's work complete and released"),
|
||||
_item(g1, "mat", "Material and parts on hand match the packet"),
|
||||
_item(g2, "work", "Work completed per drawing and procedure"),
|
||||
_item(g2, "insp", "Self-inspection done; no damage to finished surfaces"),
|
||||
_item(g2, "qty", "Completed quantity matches each work order"),
|
||||
]
|
||||
g1, g2 = "Before welding", "After welding"
|
||||
return [
|
||||
_item(g1, "dwg", dwg),
|
||||
_item(g1, "heat", "Heat numbers on material match the Materials tab"),
|
||||
_item(g1, "wps", f"WPS reviewed: {wps or 'per drawing'}"),
|
||||
_item(g1, "fit", "Fit-up and dimensions checked against the drawing"),
|
||||
_item(g1, "pre", "Preheat and interpass temperature per WPS"),
|
||||
_item(g2, "vt", "Visual inspection of all welds acceptable"),
|
||||
_item(g2, "stamp", "Welds stamped with welder ID"),
|
||||
_item(g2, "clean", "Slag and spatter removed"),
|
||||
_item(g2, "qty", "Completed quantity matches each work order"),
|
||||
]
|
||||
t = templates or DEFAULT_TEMPLATES
|
||||
items = t.get("weld" if weld else "plain") or DEFAULT_TEMPLATES["weld" if weld else "plain"]
|
||||
return [_item(i["group"], i["key"], _fill(i["text"], drawing, rev, wps)) for i in items]
|
||||
|
||||
|
||||
def qc_items_for(
|
||||
@@ -66,8 +135,8 @@ def qc_items_for(
|
||||
]
|
||||
|
||||
|
||||
def checklist_keys(weld: bool) -> list[str]:
|
||||
return [i["key"] for i in checklist_for(weld, None, None, None)]
|
||||
def checklist_keys(weld: bool, templates: Mapping[str, list[dict[str, str]]] | None = None) -> list[str]:
|
||||
return [i["key"] for i in checklist_for(weld, None, None, None, templates)]
|
||||
|
||||
|
||||
def qc_keys() -> list[str]:
|
||||
@@ -81,10 +150,12 @@ def is_done(value: Any) -> bool:
|
||||
return isinstance(value, Mapping) and value.get("done") is True
|
||||
|
||||
|
||||
def checklist_complete(checklist: Mapping[str, Any] | None, weld: bool) -> bool:
|
||||
def checklist_complete(
|
||||
checklist: Mapping[str, Any] | None, weld: bool, templates: Mapping[str, list[dict[str, str]]] | None = None
|
||||
) -> bool:
|
||||
"""Every key of the department checklist is done. An empty list counts as complete."""
|
||||
ck = checklist or {}
|
||||
return all(is_done(ck.get(k)) for k in checklist_keys(weld))
|
||||
return all(is_done(ck.get(k)) for k in checklist_keys(weld, templates))
|
||||
|
||||
|
||||
def _plural(n: int, word: str) -> str:
|
||||
|
||||
Reference in New Issue
Block a user