pelagia-portal/App/lib/attachments.ts
Claude (auto-fix) 4e6175153d fix(po): show all attachments grouped by type on PO details
All PO attachments are stored as PODocument rows whose lifecycle stage
(submission vs delivery) is encoded in the storageKey prefix. The PO
details screen previously listed them in a single flat "Attachments"
block, giving no indication of which were submission documents (invoice,
quotation) versus delivery receipts.

Add lib/attachments.ts to derive a user-facing group from the storageKey
prefix (submission / payment / delivery / other) and render each
non-empty group as a labelled subsection on the PO details screen, in
lifecycle order. Unknown prefixes fall back to an "Other" group so
nothing is ever hidden.

Fixes #10
2026-06-19 04:43:44 +05:30

96 lines
2.7 KiB
TypeScript

/**
* Attachment grouping.
*
* All PO attachments are stored as `PODocument` rows. The lifecycle stage an
* attachment belongs to is encoded in the leading segment of its `storageKey`
* (see `buildStorageKey` in `lib/storage.ts`), e.g. `po-document/<poId>/...`
* or `receipt/<poId>/...`. This module derives a user-facing grouping from
* that prefix so the PO details screen can show every attachment grouped by
* type (submission, payment, delivery).
*/
export type AttachmentGroupKey = "submission" | "payment" | "delivery" | "other";
export interface AttachmentGroupMeta {
key: AttachmentGroupKey;
label: string;
description: string;
}
/** Display order for attachment groups (lifecycle order). */
export const ATTACHMENT_GROUP_ORDER: AttachmentGroupKey[] = [
"submission",
"payment",
"delivery",
"other",
];
export const ATTACHMENT_GROUP_META: Record<AttachmentGroupKey, AttachmentGroupMeta> = {
submission: {
key: "submission",
label: "Submission documents",
description: "Uploaded with the purchase order (e.g. invoice, quotation).",
},
payment: {
key: "payment",
label: "Payment documents",
description: "Uploaded at payment (e.g. payment proof).",
},
delivery: {
key: "delivery",
label: "Delivery receipts",
description: "Uploaded at delivery confirmation (e.g. delivery receipt).",
},
other: {
key: "other",
label: "Other attachments",
description: "",
},
};
/**
* Derive the lifecycle group of an attachment from its storage key prefix.
* Unknown prefixes fall back to "other" so nothing is ever hidden.
*/
export function categorizeAttachment(storageKey: string): AttachmentGroupKey {
const prefix = storageKey.split("/")[0];
switch (prefix) {
case "po-document":
return "submission";
case "payment-document":
case "payment":
return "payment";
case "receipt":
return "delivery";
default:
return "other";
}
}
export interface AttachmentGroup<T> {
meta: AttachmentGroupMeta;
items: T[];
}
/**
* Group attachments by lifecycle stage, returning only non-empty groups in
* canonical lifecycle order. Item order within each group is preserved.
*/
export function groupAttachments<T extends { storageKey: string }>(
documents: T[]
): AttachmentGroup<T>[] {
const buckets = new Map<AttachmentGroupKey, T[]>();
for (const doc of documents) {
const key = categorizeAttachment(doc.storageKey);
const bucket = buckets.get(key);
if (bucket) bucket.push(doc);
else buckets.set(key, [doc]);
}
return ATTACHMENT_GROUP_ORDER.flatMap((key) => {
const items = buckets.get(key);
return items && items.length > 0
? [{ meta: ATTACHMENT_GROUP_META[key], items }]
: [];
});
}