fix(collection-confirm): widen NoteEntry.body and dedup attachments within row
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
parent
7e1f22f363
commit
529e08eb2a
3 changed files with 49 additions and 5 deletions
|
|
@ -22,6 +22,7 @@ import {
|
|||
buildConfirmPatch,
|
||||
buildReturnPatch,
|
||||
canConfirmRow,
|
||||
formatNoteBody,
|
||||
groupRowsByPurchaseOrder,
|
||||
parseNotesFeed,
|
||||
readInvoiceDetails,
|
||||
|
|
@ -1412,7 +1413,7 @@ function ConfirmCard({
|
|||
<span>·</span>
|
||||
<span className="font-semibold uppercase tracking-wide">{categoryLabel(entry.category)}</span>
|
||||
</div>
|
||||
<p className="mt-1 text-foreground">{entry.body}</p>
|
||||
<p className="mt-1 text-foreground">{formatNoteBody(entry.body)}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
|
|
|||
|
|
@ -410,6 +410,16 @@ describe("appendCostNote", () => {
|
|||
]);
|
||||
expect(appendCostNote(existing, { id: "u-1", name: "sj" }, 10, 10)).toBe(existing);
|
||||
});
|
||||
|
||||
it("survives a parseNotesFeed round-trip with the prev/new object body intact", () => {
|
||||
const stored = appendCostNote("", { id: "u-1", name: "sj" }, 45, 50);
|
||||
const parsed = parseNotesFeed(stored, { id: "row-1", drv_collected_at: null });
|
||||
expect(parsed).toHaveLength(1);
|
||||
expect(parsed[0]).toMatchObject({
|
||||
category: "cost",
|
||||
body: { prevCost: 45, newCost: 50 },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("dedupeAttachments", () => {
|
||||
|
|
@ -443,4 +453,9 @@ describe("dedupeAttachments", () => {
|
|||
expect(dedupeAttachments([{ id: "a" }, { id: "b" }])).toEqual([]);
|
||||
expect(dedupeAttachments([])).toEqual([]);
|
||||
});
|
||||
|
||||
it("does not push the same item id twice when a row lists the same filename twice", () => {
|
||||
const out = dedupeAttachments([{ id: "a", order_item_attachements: ["INV.pdf", "INV.pdf"] }]);
|
||||
expect(out).toEqual([{ filename: "INV.pdf", itemIds: ["a"] }]);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,11 +1,13 @@
|
|||
export type NoteCategory = "collection" | "note" | "problem" | "quantity" | "confirm" | "cost";
|
||||
|
||||
export type NoteBody = string | { prevCost?: number; newCost?: number; [k: string]: unknown };
|
||||
|
||||
export type NoteEntry = {
|
||||
ts: string;
|
||||
actorId: string;
|
||||
actorName: string;
|
||||
category: NoteCategory;
|
||||
body: string;
|
||||
body: NoteBody;
|
||||
};
|
||||
|
||||
export type ConfirmActor = {
|
||||
|
|
@ -63,11 +65,14 @@ const NOTE_CATEGORIES: NoteCategory[] = ["collection", "note", "problem", "quant
|
|||
function isNoteEntry(value: unknown): value is NoteEntry {
|
||||
if (!value || typeof value !== "object") return false;
|
||||
const candidate = value as Record<string, unknown>;
|
||||
const bodyOk =
|
||||
typeof candidate.body === "string" ||
|
||||
(candidate.body !== null && typeof candidate.body === "object");
|
||||
return (
|
||||
typeof candidate.ts === "string" &&
|
||||
typeof candidate.actorId === "string" &&
|
||||
typeof candidate.actorName === "string" &&
|
||||
typeof candidate.body === "string" &&
|
||||
bodyOk &&
|
||||
typeof candidate.category === "string" &&
|
||||
NOTE_CATEGORIES.includes(candidate.category as NoteCategory)
|
||||
);
|
||||
|
|
@ -285,8 +290,10 @@ export function dedupeAttachments(
|
|||
): DedupedAttachment[] {
|
||||
const byFilename = new Map<string, string[]>();
|
||||
for (const row of rows) {
|
||||
const list = row.order_item_attachements ?? [];
|
||||
for (const filename of list) {
|
||||
const seen = new Set<string>();
|
||||
for (const filename of row.order_item_attachements ?? []) {
|
||||
if (seen.has(filename)) continue;
|
||||
seen.add(filename);
|
||||
const bucket = byFilename.get(filename) ?? [];
|
||||
if (row.id) bucket.push(row.id);
|
||||
byFilename.set(filename, bucket);
|
||||
|
|
@ -294,3 +301,24 @@ export function dedupeAttachments(
|
|||
}
|
||||
return Array.from(byFilename.entries()).map(([filename, itemIds]) => ({ filename, itemIds }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a `NoteBody` as a human-readable string. Most categories
|
||||
* (`collection`, `note`, `problem`, `quantity`, `confirm`) carry a
|
||||
* plain string body; `cost` carries `{ prevCost, newCost }`. The UI
|
||||
* just needs a string it can drop into a `<p>` tag.
|
||||
*/
|
||||
export function formatNoteBody(body: NoteBody): string {
|
||||
if (typeof body === "string") return body;
|
||||
if (body && typeof body === "object") {
|
||||
if ("prevCost" in body || "newCost" in body) {
|
||||
const prev = (body as { prevCost?: number }).prevCost;
|
||||
const next = (body as { newCost?: number }).newCost;
|
||||
if (typeof prev === "number" && typeof next === "number") {
|
||||
return `Cost changed: ${prev} → ${next}`;
|
||||
}
|
||||
}
|
||||
return JSON.stringify(body);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue