Build Seth's changes C1 to C11 (Gitea issues 1, 8, 18, 23, 24, 25, 27, 31, 32, 33, 36)
- 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:
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
@@ -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. */
|
||||
|
||||
@@ -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 };
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user