- 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
130 lines
No EOL
4.5 KiB
TypeScript
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);
|
|
}); |