Build Seth's changes C1 to C11 (Gitea issues 1, 8, 18, 23, 24, 25, 27, 31, 32, 33, 36)
ci / server (pull_request) Successful in 12m48s
ci / frontend (pull_request) Successful in 54s
ci / image (pull_request) Skipped
ci / e2e (pull_request) Successful in 4m38s

- C1 (issue 1): over target follows VISUAL ticket hours, not the timer.
- C2 (issue 24): a Q.C. reject always needs a reason; accept needs an NCR number after a reject without one.
- C3 (issue 25): an incomplete unit ITP blocks filing.
- C4 (issue 27): prints open the print dialog on the page, no new tab.
- C5 (issue 31): filing is blocked while VISUAL shows a work order open; the feed sets vclosed on VISUAL packets.
- C6 (issue 8): the data book leaves out the history pages.
- C7 (issue 18): the supervisor approves each received short; accept and filing wait for it.
- C8 (issue 23): per-department checklists, edited by the department supervisor or Admin, frozen at START.
- C9 (issue 32): Q.C. and Clerical may run the stamp check.
- C10 (issue 33): the PM screen and reports are for PS-ProjectMgmt and Admin only.
- C11 (issue 36): the Stall screen lists the packets assigned to the welder, at the supervisor's stall.

Migration 0011. Docs: PLAN (also issue 9 confirmed, issue 4 asked), POLICY, SECURITY, SCHEMA, FEED, README, CHANGELOG.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-30 16:36:49 -06:00
co-authored by Claude Opus 5.5
parent 150c507a31
commit 70a97398f1
101 changed files with 3871 additions and 1098 deletions
+3 -31
View File
@@ -1,5 +1,6 @@
// Phase 9 Admin: departments, profiles, checklists, security, access log, presence, stamp check, directory flags,
// the AD reconcile, and notifications. Field names follow the phase 9 design contract exactly.
// Phase 9 Admin: departments, profiles, security, access log, presence, directory flags, the AD reconcile, and
// notifications. The stamp check result type stays here; the calls are in stamp.ts (C9), the checklists in
// checklists.ts (C8). Field names follow the phase 9 design contract exactly.
import type { AdminUser } from "../auth/types";
import { api } from "./client";
import { qs } from "./query";
@@ -96,25 +97,6 @@ export interface ImportAnswer {
applied: boolean;
}
/* ---------- Checklists ---------- */
export interface ChecklistItem {
group: string;
key: string;
text: string;
}
export interface ChecklistLists {
weld: ChecklistItem[];
plain: ChecklistItem[];
}
export interface ChecklistsAnswer {
value: ChecklistLists;
defaults: ChecklistLists;
version: number;
}
/* ---------- Security ---------- */
export interface SecuritySettingsAnswer {
@@ -367,9 +349,6 @@ export const admin9Api = {
patchProfile: (id: string, patch: ProfilePatch) => api.patch<ProfileUser>(`${A}/users/${enc(id)}/profile`, patch),
importProfiles: (csv: string, apply: boolean) => api.post<ImportAnswer>(`${A}/users/import`, { csv, apply }),
checklists: (signal?: AbortSignal) => api.get<ChecklistsAnswer>(`${A}/checklists`, signal),
saveChecklists: (value: ChecklistLists, version: number) => api.put<ChecklistsAnswer>(`${A}/checklists`, { value, version }),
securitySettings: (signal?: AbortSignal) => api.get<SecuritySettingsAnswer>(`${A}/security/settings`, signal),
saveSecuritySettings: (values: Record<string, number>, version: number) =>
api.put<SecuritySettingsAnswer>(`${A}/security/settings`, { values, version }),
@@ -382,13 +361,6 @@ export const admin9Api = {
presence: (signal?: AbortSignal) => api.get<PresenceAnswer>(`${A}/presence`, signal),
stampVerify: (text: string) => api.post<StampResult>(`${A}/stamp-verify`, { text }),
stampVerifyPdf: (file: File) => {
const fd = new FormData();
fd.append("file", file);
return api.post<{ stamps: StampResult[] }>(`${A}/stamp-verify/pdf`, fd);
},
directoryFlags: (signal?: AbortSignal) => api.get<DirectoryFlagsAnswer>(`${A}/directory-flags`, signal),
refreshAd: () => api.post<RefreshRequested>(`${A}/ad/refresh`, {}),
reconcile: (signal?: AbortSignal) => api.get<ReconcileAnswer>(`${A}/ad/reconcile`, signal),
+59
View File
@@ -0,0 +1,59 @@
// The department checklists (change C8). A department supervisor edits their own department's list, Admin any.
// A department without its own list uses the default for its kind (weld or plain). A stage freezes the list at START,
// so a change applies to stages not started yet.
import { api } from "./client";
import { asList } from "./query";
export interface ChecklistItem {
group: string;
key: string;
text: string;
}
/** One row of GET /api/checklists, for every shop department. */
export interface DeptChecklistRow {
dept_code: string;
dept_name: string;
weld: boolean;
/** The department has its own list. False: it uses the default for its kind. */
custom: boolean;
version: number;
/** The caller may save this department's list. */
editable: boolean;
}
/** GET /api/checklists/{dept}, and the answer of a save. */
export interface DeptChecklist extends DeptChecklistRow {
items: ChecklistItem[];
/** The default list for the department's kind. */
defaults: ChecklistItem[];
}
const enc = encodeURIComponent;
function list<T>(raw: unknown): T[] {
return Array.isArray(raw) ? (raw as T[]) : [];
}
/** Every list present, so the screen never trips on a missing field. */
export function normaliseChecklist(raw: Partial<DeptChecklist> | null | undefined, dept = ""): DeptChecklist {
return {
dept_code: raw?.dept_code ?? dept,
dept_name: raw?.dept_name ?? "",
weld: !!raw?.weld,
custom: !!raw?.custom,
version: typeof raw?.version === "number" ? raw.version : 0,
editable: !!raw?.editable,
items: list<ChecklistItem>(raw?.items),
defaults: list<ChecklistItem>(raw?.defaults),
};
}
export const checklistsApi = {
list: async (signal?: AbortSignal) => asList<DeptChecklistRow>(await api.get<unknown>("/api/checklists", signal), "checklists"),
get: async (dept: string, signal?: AbortSignal) =>
normaliseChecklist(await api.get<Partial<DeptChecklist> | null>(`/api/checklists/${enc(dept)}`, signal), dept),
/** 409 with the current list when `base` is stale; 403 when the caller may not edit this department. */
save: async (dept: string, items: ChecklistItem[], base: number) =>
normaliseChecklist(await api.put<Partial<DeptChecklist> | null>(`/api/checklists/${enc(dept)}`, { items, base }), dept),
};
+4
View File
@@ -22,6 +22,7 @@ import type {
QcFields,
ReceiptBody,
SealView,
ShortApproveBody,
ShortBody,
StageFields,
TransitionBody,
@@ -104,6 +105,9 @@ export const packetsApi = {
api.delWithBody<PacketRead>(`${base(id)}/bom/${item}/short`, { ...body, ...g }),
receiveShort: (id: number, item: number, body: ReceiptBody, g: Guarded = {}) =>
api.post<PacketRead>(`${base(id)}/bom/${item}/short/receipts`, { ...body, ...g }),
/** Change C7: the department supervisor approves one received short. Signed. */
approveShortReceipt: (id: number, receiptId: number, body: ShortApproveBody = {}, g: Guarded = {}) =>
api.post<PacketRead>(`${base(id)}/shorts/receipts/${encodeURIComponent(String(receiptId))}/approve`, { ...body, ...g }),
setWhStage: (id: number, body: WhStageBody, g: Guarded = {}) => api.post<PacketRead>(`${base(id)}/wh`, { ...body, ...g }),
/* Phase 5: the shop. Every answer is the packet. */
+40
View File
@@ -0,0 +1,40 @@
// The Stall screen's own call (change C11): the packets assigned to me, across stations. Nobody picks a station on
// the Stall screen; each card carries the station the supervisor picked when assigning.
import { api } from "./client";
import { qs } from "./query";
import { normaliseCard, type ShopCard } from "./shop";
import { normaliseFacets, type BoardQuery, type Facets } from "./types";
/** A stall card: the shop card plus the station it is assigned to. */
export interface StallCard extends ShopCard {
station_id: number | null;
station_name: string | null;
}
/** GET /api/stall/mine: the stall board shape, without a station. */
export interface StallMine {
mine: StallCard[];
submitted: StallCard[];
facets?: Facets;
}
function cards(raw: unknown): StallCard[] {
if (!Array.isArray(raw)) return [];
return (raw as Array<Partial<StallCard> & ShopCard>).map((c) => {
const card = normaliseCard(c);
const id = c.station_id ?? c.station?.id ?? null;
const name = c.station_name ?? c.station?.name ?? null;
// The card's own station line reads `station`; fill it from the flat fields when the server sends only those.
const station = card.station ?? (id !== null ? { id, name: name ?? "" } : null);
return { ...card, station, station_id: id, station_name: name };
});
}
/** The find bar's query applies; `me` and the session decide whose packets. */
export async function getStallMine(q: BoardQuery = {}, signal?: AbortSignal): Promise<StallMine> {
const raw = await api.get<Partial<StallMine> | null>(`/api/stall/mine${qs({ ...q })}`, signal);
const facets = normaliseFacets(raw?.facets);
return { mine: cards(raw?.mine), submitted: cards(raw?.submitted), ...(facets ? { facets } : {}) };
}
export const stallApi = { getStallMine };
+15
View File
@@ -0,0 +1,15 @@
// The stamp check (change C9): Admin, Q.C. and Clerical check a printed or saved copy against the print log.
// /api/stamp-verify and /api/stamp-verify/pdf take the same bodies and answer the same as the /api/admin aliases.
import type { StampResult } from "./admin";
import { api } from "./client";
export type { StampResult };
export const stampApi = {
verify: (text: string) => api.post<StampResult>("/api/stamp-verify", { text }),
verifyPdf: (file: File) => {
const fd = new FormData();
fd.append("file", file);
return api.post<{ stamps: StampResult[] }>("/api/stamp-verify/pdf", fd);
},
};
+41 -3
View File
@@ -161,6 +161,8 @@ export interface PacketCard {
/** CO numbers, or a count. Both are handled. */
open_cos: number[] | number;
open_shorts: number;
/** Change C7: open shorts plus received shorts the supervisor has not approved. */
shorts_unapproved?: number;
wh_stage: string | null;
due_effective: string | null;
updated_at: string | null;
@@ -296,11 +298,43 @@ export interface BomShort {
}
export interface ShortReceipt {
/** Change C7: the short_receipts row id, for the supervisor's approval. */
id?: number;
qty: number | string;
heat: string | null;
note: string | null;
at: string | null;
by?: WhoRef | null;
/** Change C7: the department supervisor's approval of this receipt, or null while it waits. */
approval?: ShortApproval | null;
}
/** Change C7: PacketRead.shorts, every short of the packet, open or closed, with its BOM item and its receipts. */
export interface PacketShort {
id: number;
item: number;
qty: number | string;
qty_open: number | string;
reason: string | null;
ref: string | null;
eta: string | null;
note: string | null;
opened_at: string | null;
closed_at: string | null;
opened_by?: WhoRef | null;
receipts: ShortReceipt[];
}
/** Change C7: one supervisor approval of a received short. */
export interface ShortApproval {
at: string;
by: WhoRef | null;
note: string | null;
}
/** Change C7: POST /api/packets/{id}/shorts/receipts/{receipt_id}/approve. The signature rides in Guarded. */
export interface ShortApproveBody {
note?: string;
}
export interface BomLine {
@@ -551,6 +585,12 @@ export interface PacketRead {
itp?: ItpSummary | null;
/** Phase 6: open ITP hold points for this packet's department. */
itp_hold?: ItpHold | null;
/** Change C2: a Q.C. reject since the last accept has no NCR number, so the accept must carry one. */
ncr_required?: boolean;
/** Change C7: open shorts plus received shorts the supervisor has not approved. */
shorts_unapproved?: number;
/** Change C7: every short, open or closed, with who received and who approved each receipt. Not on a sealed read. */
shorts?: PacketShort[];
}
/* ---------- Phase 7: close, file, PDFs and prints ---------- */
@@ -831,10 +871,8 @@ export interface TransitionBody {
spool?: boolean;
location?: string;
for_sup_id?: string;
/** Phase 6: QC_REJECT. An NCR number like NCR-2026-0021. */
/** QC_REJECT, and QC_ACCEPT / QC_ACCEPT_AND_ROUTE when `ncr_required` (change C2). An NCR number like NCR-2026-0021. */
ncr?: string;
/** Phase 7: FILE. Files although the feed still shows work orders open in VISUAL. */
confirm_open_wos?: boolean;
sign?: SignBody;
it_reason?: string;
}