WFDASH-WEBAPP/scripts/regenerate-delivery-note-pdfs.ts
Dave 6344948b0b feat: add driverless collection confirm and new order tooling
This commit adds several key features, improvements, and cleanups across the codebase:
- Add `order_items.confirm_collected` MCP/REST endpoint to mark order items as collected without requiring driver assignment
- Add standalone order creation and order split APIs for flexible order management
- Update order list sort to oldest-first to align with user workflow of working through oldest orders first
- Improve order search to show cross-tab matches and display source tab indicators on search results
- Add customer name similarity check to warn about mismatched QuickBooks customers during invoice creation
- Remove supplier name column from delivery note PDFs and clean up related data passing across delivery note routes
- Add validation and debug logging to ItemIntelPopover for missing item ID on apply actions
- Remove unused OrderItemContextPopoverTrigger component from order detail page
- Add delivery note regeneration script and update test suites for new and existing functionality
- Update documentation and add test coverage for the new collection confirm tool
2026-08-12 01:13:46 +02:00

148 lines
5.9 KiB
TypeScript

import "./smoke/_env";
import { writeFile, mkdir } from "node:fs/promises";
import path from "node:path";
import { getAdminPocketBase } from "../src/lib/server/pocketbase-admin";
import { renderDeliveryNotePdf } from "../src/lib/pdf/render";
import { resolveDeliveryNoteLineQty } from "../src/lib/pdf/delivery-note-document";
import {
fetchLetterhead,
letterheadForCompany,
loadLetterheadLogoDataUri,
} from "../src/lib/letterhead";
type Item = {
id: string;
description?: string | null;
delivery_note_number?: string | null;
order_item_details?: unknown;
order_qty?: number | null;
qty_collected?: number | null;
order_item_attachements?: unknown;
expand?: {
order?: {
order_number?: string | null;
expand?: {
company?: { company?: string | null };
customer?: { customer?: string | null };
};
};
};
};
type Attachment = { token: string; filename: string };
type Result = {
deliveryNoteNumber: string;
items: number;
regenerated: number;
errors: string[];
};
function readDateField(value: unknown, key: string): string {
if (!value || typeof value !== "object" || Array.isArray(value)) return "";
const candidate = (value as Record<string, unknown>)[key];
return typeof candidate === "string" && candidate.length >= 10 ? candidate.slice(0, 10) : "";
}
function readAttachments(value: unknown): string[] {
if (typeof value === "string" && value.trim()) return [value.trim()];
if (Array.isArray(value)) return value.filter((entry): entry is string => typeof entry === "string" && entry.trim().length > 0);
return [];
}
function filenameFromToken(token: string): string {
const match = token.match(/^[^_]+_[^_]+_(.+)$/);
return match?.[1] ?? token;
}
function isDeliveryNotePdf(filename: string, deliveryNoteNumber: string): boolean {
return filename.replace(/_[a-z0-9]{10,}(?=\.pdf$)/i, "").toLowerCase() === `${deliveryNoteNumber.toLowerCase()}.pdf`;
}
function parseArgs() {
const args = process.argv.slice(2);
const dryRun = args.includes("--dry-run");
const limitIndex = args.indexOf("--limit");
const limit = limitIndex >= 0 ? Number(args[limitIndex + 1]) : Infinity;
const explicit = args.flatMap((arg, index) => (arg === "--dn" ? [args[index + 1]] : []));
return { dryRun, limit: Number.isFinite(limit) && limit > 0 ? limit : Infinity, explicit };
}
async function main() {
const { dryRun, limit, explicit } = parseArgs();
const pb = await getAdminPocketBase();
const items = await pb.collection("order_items").getFullList<Item>({
filter: 'delivery_note_number != ""',
sort: "delivery_note_number,created",
expand: "order.company,order.customer",
});
const grouped = new Map<string, Item[]>();
for (const item of items) {
const number = item.delivery_note_number?.trim();
if (!number || (explicit.length > 0 && !explicit.includes(number))) continue;
const group = grouped.get(number) ?? [];
group.push(item);
grouped.set(number, group);
}
const groups = [...grouped.entries()].slice(0, limit);
if (groups.length === 0) throw new Error("No existing delivery notes matched.");
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
const reportDir = path.resolve(process.cwd(), "reports");
await mkdir(reportDir, { recursive: true });
const results: Result[] = [];
for (const [deliveryNoteNumber, noteItems] of groups) {
const result: Result = { deliveryNoteNumber, items: noteItems.length, regenerated: 0, errors: [] };
try {
const first = noteItems[0];
const order = first.expand?.order;
const companyName = order?.expand?.company?.company ?? "";
const letterheadCode = letterheadForCompany(companyName) ?? "CNL";
const [letterhead, logoDataUri] = await Promise.all([
fetchLetterhead(letterheadCode),
loadLetterheadLogoDataUri(letterheadCode),
]);
const pdfBuffer = await renderDeliveryNotePdf({
header: {
deliveryNoteNumber,
dateIso: readDateField(first.order_item_details, "deliveryNoteDate") || new Date().toISOString().slice(0, 10),
orderNumber: order?.order_number ?? "",
companyName,
customerName: order?.expand?.customer?.customer ?? "",
},
lines: noteItems.map((item) => ({
id: item.id,
description: (item.description ?? "").trim() || "(no description)",
qty: resolveDeliveryNoteLineQty({ orderQty: item.order_qty, qtyCollected: item.qty_collected }),
})),
letterhead,
logoDataUri,
});
if (!dryRun) {
for (const item of noteItems) {
const existing: Attachment[] = readAttachments(item.order_item_attachements).map((token) => ({ token, filename: filenameFromToken(token) }));
const preserved = existing.filter((attachment) => !isDeliveryNotePdf(attachment.filename, deliveryNoteNumber));
const form = new FormData();
for (const attachment of preserved) form.append("order_item_attachements", attachment.token);
form.append("order_item_attachements", new Blob([new Uint8Array(pdfBuffer)], { type: "application/pdf" }), `${deliveryNoteNumber}.pdf`);
await pb.collection("order_items").update(item.id, form);
result.regenerated += 1;
}
}
} catch (error) {
result.errors.push(error instanceof Error ? error.message : String(error));
}
results.push(result);
process.stdout.write(`${dryRun ? "Would regenerate" : "Regenerated"} ${deliveryNoteNumber}: ${result.items} item(s)${result.errors.length ? ` — ${result.errors.join("; ")}` : ""}\n`);
}
await writeFile(path.join(reportDir, `delivery-note-regen-${timestamp}.json`), JSON.stringify({ dryRun, results }, null, 2));
if (results.some((result) => result.errors.length > 0)) process.exitCode = 1;
}
void main().catch((error) => {
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
process.exitCode = 1;
});