Phase 8: Project Management, find bar, presence and motion
The Project Management screen (/pm, view only) with Overview, Projects & units, Bottlenecks & aging, Hours vs target, Quality & issues and Find jobs, the period picker, health per unit and per project, drill down, Excel (CSV) per table and for all, and the PESCO production status report PDF with the controlled-copy stamp; every export logged as "report exported". The find bar on every home screen: the full search set, the dropdowns, quick flags with Mine, the flag kinds, Board and List with sorts, filters saved per browser; every filter applied by the server on the list and the boards, with facets. Presence in packets with role, lock and revoke drop; the motion switch with reduced motion honoured, card glides, glow and count-up; Back restores the scroll spot; sheet placement and the iPad layout pass. The data book compared page by page with the old build and six layout differences fixed. Tests for the PM aggregates, search, filters, presence and exports; Playwright scenarios for the PM screen, the find bar and presence at desktop and iPad viewports. Docs updated; PLAN.md questions 38 to 42. Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
"""Phase 8 server smoke: one small world through every new read (the find bar on the list and the boards, the PM
|
||||
reports, the exports) so the server agent's shapes are exercised end to end. The detailed checks live in
|
||||
test_search.py, test_filters.py, test_pm_api.py, test_pm_export.py and test_presence.py."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
from collections.abc import Iterator
|
||||
|
||||
import pytest
|
||||
from fakedir import FakeDirectory
|
||||
from pshelpers import app_conn, needs_pg
|
||||
from pypdf import PdfReader
|
||||
from shophelpers import Shop, draft, make_unit, ok, open_shop, ready_packet, run, set_running_since, start, to_station
|
||||
from shophelpers import to_warehouse as send_to_wh
|
||||
|
||||
from packetstation.settings import Settings
|
||||
|
||||
pytestmark = needs_pg
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def shop(pg_settings: Settings, fake_dir: FakeDirectory, dev_visual: None) -> Iterator[Shop]:
|
||||
yield from open_shop(pg_settings, fake_dir)
|
||||
|
||||
|
||||
def test_find_bar_boards_and_pm_reports(shop: Shop) -> None:
|
||||
settings = shop.settings
|
||||
project_id, u1 = make_unit(settings, "L670", "052")
|
||||
with app_conn(settings) as conn:
|
||||
u2 = conn.execute(
|
||||
"INSERT INTO units (project_id, lot, name, status, due) VALUES (%s, '053', 'Unit two', 'open',"
|
||||
" current_date - 2) RETURNING id",
|
||||
(project_id,),
|
||||
).fetchone()[0]
|
||||
conn.execute("UPDATE projects SET due = current_date + 30 WHERE id = %s", (project_id,))
|
||||
|
||||
p_draft = draft(shop, u1, title="Inlet nozzle =SUM(A1)")
|
||||
p_wh = send_to_wh(shop, draft(shop, u1, drawing="D-7000"))
|
||||
ok(
|
||||
shop("wh.wade").post(
|
||||
f"/api/packets/{p_wh['id']}/wh",
|
||||
json={"stage": "hold", "reason": "Waiting on purchasing / PO", "expect_stage": "incoming"},
|
||||
)
|
||||
)
|
||||
p_st = start(shop, to_station(shop, ready_packet(shop, u2)))
|
||||
set_running_since(settings, p_st["id"], 6 * 3600)
|
||||
with app_conn(settings) as conn:
|
||||
line = conn.execute("SELECT id FROM bom_lines WHERE packet_id = %s AND item = 2", (p_wh["id"],)).fetchone()[0]
|
||||
conn.execute(
|
||||
"INSERT INTO bom_shorts (packet_id, bom_line_id, qty, qty_open, reason, ref, eta)"
|
||||
" VALUES (%s, %s, 2, 2, 'Not in stock', 'PO-881', current_date - 1)",
|
||||
(p_wh["id"], line),
|
||||
)
|
||||
conn.execute(
|
||||
"INSERT INTO qc_history (packet_id, at, result, inspector, note, dept_code, snapshot)"
|
||||
" VALUES (%s, now() - interval '3 days', 'rejected', 'Quinn QC', 'NCR-12 · undercut', '03', '{}')",
|
||||
(p_st["id"],),
|
||||
)
|
||||
co = conn.execute(
|
||||
"INSERT INTO change_orders (packet_id, co_no, origin, review, ecn, reason, issued_at)"
|
||||
" VALUES (%s, 1, 'manual', 'approved', 'ECN-77', 'Rev C', now() - interval '4 days') RETURNING id",
|
||||
(p_st["id"],),
|
||||
).fetchone()[0]
|
||||
conn.execute("INSERT INTO co_required_depts (co_id, dept_code) VALUES (%s, '03')", (co,))
|
||||
run(settings, "UPDATE packets SET wps = 'WPS-99' WHERE id = %s", (p_draft["id"],))
|
||||
|
||||
sup = shop("sup.wes")
|
||||
# The find bar on the list
|
||||
got = ok(sup.get("/api/packets", params={"q": "lot 52", "project_id": project_id, "facets": 1}))
|
||||
ids = {c["id"] for c in got["packets"]}
|
||||
assert p_draft["id"] in ids and p_st["id"] not in ids, ids
|
||||
assert len(got["facets"]["workers"]) == 1 and len(got["facets"]["stations"]) == 1
|
||||
assert ok(sup.get("/api/packets", params={"q": "HT-20"}))["packets"][0]["id"] == p_st["id"]
|
||||
assert [c["id"] for c in ok(sup.get("/api/packets", params={"q": "ECN-77"}))["packets"]] == [p_st["id"]]
|
||||
assert [c["id"] for c in ok(sup.get("/api/packets", params={"status": "station"}))["packets"]] == [p_st["id"]]
|
||||
shorts = ok(sup.get("/api/packets", params={"flags": "short"}))["packets"]
|
||||
assert [c["id"] for c in shorts] == [p_wh["id"]]
|
||||
mine = ok(sup.get("/api/packets", params={"flags": "mine", "mine_screen": "supervisor"}))["packets"]
|
||||
assert {c["id"] for c in mine} == {p_draft["id"], p_wh["id"], p_st["id"]}
|
||||
over = ok(sup.get("/api/packets", params={"flags": "overdue"}))["packets"]
|
||||
assert [c["id"] for c in over] == [p_st["id"]]
|
||||
for sort in ("updated", "project", "due", "status", "dept"):
|
||||
assert len(ok(sup.get("/api/packets", params={"sort": sort, "project_id": project_id}))["packets"]) == 3
|
||||
hand = ok(sup.get("/api/packets", params={"flag": "hand"}))["packets"]
|
||||
assert {c["id"] for c in hand} == {p_draft["id"]}
|
||||
|
||||
# The boards keep their KPIs on the project scope
|
||||
b = ok(sup.get("/api/shop/board", params={"q": "nothing-matches-this"}))
|
||||
assert b["kpis"]["at_stations"] == 1 and not b["depts"][0]["stations"][0]["packets"]
|
||||
assert "facets" in b
|
||||
wh = ok(shop("wh.wade").get("/api/warehouse/board", params={"flags": "short"}))
|
||||
assert [c["id"] for c in wh["columns"]["hold"]] == [p_wh["id"]] and "facets" in wh
|
||||
qc = ok(shop("qc.quinn").get("/api/qc/home", params={"q": "zzz"}))
|
||||
assert qc["kpis"]["waiting"] == 0 and "facets" in qc
|
||||
stall = ok(shop("weld.andy").get("/api/shop/stall", params={"station_id": shop.stall(), "flags": "mine"}))
|
||||
assert [c["id"] for c in stall["mine"]] == [p_st["id"]]
|
||||
|
||||
# Project Management
|
||||
pmc = shop("pm.paula")
|
||||
ov = ok(pmc.get("/api/pm/overview", params={"period": "30"}))
|
||||
k = {x["key"]: x["n"] for x in ov["kpis"]}
|
||||
assert k["open"] == 3 and k["holds"] == 1 and k["shorts"] == 1 and k["cos"] == 1 and k["rejects"] == 1, k
|
||||
assert k["late_units"] == 1, k
|
||||
assert ov["scope"]["label"] == "All projects" and ov["summary"][0].startswith("3 open packets")
|
||||
assert sum(s["count"] for s in ov["pipeline"]) == 3
|
||||
assert [(r["cells"][0], r["health"]) for r in ov["attention"]["rows"]] == [("L670052", "risk"), ("L670053", "late")]
|
||||
pr = ok(pmc.get("/api/pm/projects", params={"expand": "all"}))
|
||||
assert [r["lvl"] for r in pr["tables"][0]["rows"]][:3] == [0, 1, 1]
|
||||
ag = ok(pmc.get("/api/pm/aging"))
|
||||
assert [t["key"] for t in ag["tables"]] == ["aging_steps", "aging_open", "aging_history"]
|
||||
hr = ok(pmc.get("/api/pm/hours"))
|
||||
over_rows = hr["tables"][3]["rows"]
|
||||
assert [r["pid"] for r in over_rows] == [p_st["id"]], hr
|
||||
ql = ok(pmc.get("/api/pm/quality"))
|
||||
assert [len(t["rows"]) for t in ql["tables"]][:4] == [1, 1, 1, 1], ql
|
||||
fd = ok(pmc.get("/api/pm/find", params={"sort": "project", "project_id": project_id}))
|
||||
assert fd["scope"]["label"] == "Project L670" and len(fd["packets"]) == 3
|
||||
|
||||
r = pmc.get("/api/pm/export.csv", params={"table": "all"})
|
||||
assert r.status_code == 200, r.text
|
||||
text = r.content.decode("utf-8")
|
||||
assert text.startswith("\ufeffPESCO Packet Station — All reports\r\n")
|
||||
assert "attachment" in r.headers["content-disposition"]
|
||||
assert pmc.get("/api/pm/export.csv", params={"table": "nope"}).status_code == 404
|
||||
r = pmc.get("/api/pm/report.pdf")
|
||||
assert r.status_code == 200 and r.content.startswith(b"%PDF")
|
||||
|
||||
reader = PdfReader(io.BytesIO(r.content))
|
||||
assert len(reader.pages) > 5
|
||||
assert "PRODUCTION STATUS REPORT" in reader.pages[0].extract_text()
|
||||
assert all("Controlled copy" in pg.extract_text() for pg in reader.pages)
|
||||
with app_conn(settings) as conn:
|
||||
kinds = [x[0] for x in conn.execute("SELECT kind FROM print_log ORDER BY id")]
|
||||
acts = [x[0] for x in conn.execute("SELECT action FROM audit_events WHERE action = 'report exported'")]
|
||||
assert kinds == ["report-csv", "report-pdf"] and len(acts) == 2
|
||||
assert shop("clerk.clara").get("/api/pm/overview").status_code == 200
|
||||
Reference in New Issue
Block a user