WFDASH-WEBAPP/scripts/migrations/2026-08-13-fix-delivery-notes-relations.ts
Dave 3b8bc0fc35 feat(delivery-notes, migrations): add tabs, grouping, markup, and fix DN relations
- Add shared env loader for migration scripts to load .env and .env.local files at module init
- Add migration script to fix broken delivery note relation fields and backfill missing company data
- Update delivery notes list page: add status filter tabs, company-based collapsible grouping, increased fetch limit, and improved global search
- Add per-item cost price, markup calculations, and total summary rows for cost, selling price, markup, and margin to delivery note detail pages
2026-08-14 09:41:14 +02:00

130 lines
No EOL
4.5 KiB
TypeScript

import "./_env-loader";
import { getAdminPocketBase } from "@/lib/server/pocketbase-admin";
/**
* Migration — 2026-08-13-fix-delivery-notes-relations.ts
*
* `delivery_notes` was created with relation fields (`customer`,
* `company`, `order`, `created_by`) but the live schema has
* `collectionId: null` on those fields, so any `expand=...` query
* silently returns nothing. The list page relies on
* `expand=company.company` for per-company grouping, so all DNs
* collapse into a single "No company" bucket — and crucially, the
* records remain in the table, but grouped wrong (and reported
* missing when the operator searches by number).
*
* This migration patches the relation targets so expansion resolves,
* then backfills `delivery_notes.company` from the parent order's
* company where it's currently empty (defensive — most records
* already have the right value from the original create path).
*
* Run:
* npx tsx scripts/migrations/2026-08-13-fix-delivery-notes-relations.ts
*/
const CUSTOMERS = "customers";
const COMPANIES = "companies";
const ORDERS = "orders";
const USERS = "users";
const DELIVERY_NOTES = "delivery_notes";
async function patchRelation(
pb: Awaited<ReturnType<typeof getAdminPocketBase>>,
collectionId: string,
fieldName: string,
targetCollectionId: string,
log: string[],
): Promise<void> {
const collection = await pb.collections.getOne(collectionId);
const field = (collection.fields ?? []).find((f) => f.name === fieldName);
if (!field) {
log.push(` ✗ ${fieldName} not found on ${collection.name}`);
return;
}
if (field.type !== "relation") {
log.push(` ✗ ${fieldName} is not a relation (type=${field.type})`);
return;
}
const opts = (field.options ?? {}) as { collectionId?: string | null };
if (opts.collectionId === targetCollectionId) {
log.push(` · ${fieldName}.collectionId already set — skip`);
return;
}
log.push(` → patching ${fieldName}.collectionId to ${targetCollectionId}`);
await pb.collections.update(collection.id, {
fields: [
{ ...field, options: { ...opts, collectionId: targetCollectionId } },
],
});
}
async function backfillCompany(
pb: Awaited<ReturnType<typeof getAdminPocketBase>>,
log: string[],
): Promise<{ updated: number; skipped: number; failed: number }> {
let updated = 0;
let skipped = 0;
let failed = 0;
const records = await pb.collection(DELIVERY_NOTES).getFullList<{
id: string;
company: string;
order: string;
}>({ filter: "company = '' || company = null" });
log.push(` · ${records.length} delivery note(s) need a company backfill`);
for (const dn of records) {
if (!dn.order) {
skipped += 1;
continue;
}
try {
const order = await pb.collection(ORDERS).getOne(dn.order, {
fields: "company",
});
const company = (order as unknown as { company?: string }).company;
if (!company) {
skipped += 1;
continue;
}
await pb.collection(DELIVERY_NOTES).update(dn.id, { company });
updated += 1;
} catch (err) {
failed += 1;
log.push(
` ✗ ${dn.id} (${dn.delivery_note_number || dn.id}): ${err instanceof Error ? err.message : String(err)}`,
);
}
}
return { updated, skipped, failed };
}
async function main() {
const log: string[] = [];
log.push("[migration:fix-delivery-notes-relations] starting");
const pb = await getAdminPocketBase();
const customersCol = await pb.collections.getOne(CUSTOMERS);
const companiesCol = await pb.collections.getOne(COMPANIES);
const ordersCol = await pb.collections.getOne(ORDERS);
const usersCol = await pb.collections.getOne(USERS);
log.push("[migration:fix-delivery-notes-relations] step 1 — patch relation targets");
await patchRelation(pb, DELIVERY_NOTES, "customer", customersCol.id, log);
await patchRelation(pb, DELIVERY_NOTES, "company", companiesCol.id, log);
await patchRelation(pb, DELIVERY_NOTES, "order", ordersCol.id, log);
await patchRelation(pb, DELIVERY_NOTES, "created_by", usersCol.id, log);
log.push("[migration:fix-delivery-notes-relations] step 2 — backfill empty company from order");
const { updated, skipped, failed } = await backfillCompany(pb, log);
log.push(
`[migration:fix-delivery-notes-relations] updated=${updated} skipped=${skipped} failed=${failed}`,
);
log.push("[migration:fix-delivery-notes-relations] done");
for (const line of log) console.log(line);
}
main().catch((err) => {
console.error("[migration:fix-delivery-notes-relations] fatal:", err);
process.exit(1);
});