WFDASH-WEBAPP/scripts/nss-fasteners-revert-name-description.ts
Dave d8e8714720 feat(stock): update pricing logic and transform script for fasteners
- Enhanced `nss-fasteners-pb-import.json` to compute `selling_price` as `cost_price * (1 + 80%)` and included `markup` in the records.
- Updated `nss-fasteners-pb-transform.ts` to allow configurable CSV and output file names via environment variables, improving flexibility for different product runs.
- Added support for uploading photos in the repair detail page, including validation for file types and sizes, and implemented a dialog for creating customer quotes linked to repairs.
- Introduced a new `customer_quote` field in the `Repair` type to link generated quotes back to their respective repairs.

These changes streamline the pricing process and enhance the user experience in managing repairs and quotes.
2026-09-21 12:32:44 +02:00

206 lines
6.5 KiB
TypeScript

/**
* Revert the name/description swap I did in the previous fix:
*
* The 99 NSS-imported rows currently have
* name = "Hex Nut DIN934" (was SKU code originally)
* description = "Hex Nut DIN934" (unchanged throughout)
* notes = "PosCode: <SKU code> | …" (PosCode prepended)
*
* What they should be is the ORIGINAL ingest shape:
* name = SKU code, e.g. "M48 NUT 8 PLN" (the PosCode)
* description = SKU code, e.g. "M48 NUT 8 PLN" (matches name now)
* notes = original notes without the PosCode prefix
*
* Net result: the POS reads `description` and shows the SKU code
* (e.g. "M48 NUT 8 PLN") because the SKU code is now the
* canonical label — and the original `name` is back where it was.
*
* The SKU code is sourced from `scripts/nss-fasteners-pb-import.json`,
* which is the original ingest payload (the `name` field there is
* the SKU code we want back in PB).
*
* Idempotent: if `name == description == <SKU code>` and `notes`
* no longer has the `PosCode:` prefix, the row is skipped.
*
* Run:
* npx tsx scripts/nss-fasteners-revert-name-description.ts --dry-run
* npx tsx scripts/nss-fasteners-revert-name-description.ts --limit 5
* npx tsx scripts/nss-fasteners-revert-name-description.ts
*/
import "./_env"; // side-effect import: loads .env.local
import { getAdminPocketBase } from "../src/lib/server/pocketbase-admin";
import { readFileSync } from "node:fs";
import { join } from "node:path";
const NSS_LOCATION =
"NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)";
const IMPORT_JSON = join(import.meta.dirname, "nss-fasteners-pb-import.json");
const args = process.argv.slice(2);
const dryRun = args.includes("--dry-run");
const limitArg = args.find((a) => a.startsWith("--limit="));
const limit = limitArg ? Number(limitArg.split("=")[1]) : Infinity;
type ImportRecord = {
sku: string;
name: string; // original SKU code, e.g. "M48 NUT 8 PLN"
description: string;
notes: string;
};
type PbRow = {
id: string;
sku: string;
name: string;
description: string;
notes: string;
};
/**
* Strip the "PosCode: <X> | " prefix that the previous fix added.
* If the prefix isn't there (e.g. the row was already reverted, or
* was never touched), notes is returned unchanged.
*
* Anchored to the literal "PosCode:" tag — we never trim anything
* else, so the original notes content (Material / Grade / etc.) is
* preserved exactly.
*/
function stripPosCodePrefix(notes: string): string {
// Match "PosCode: <something> | " at the very start.
const m = notes.match(/^PosCode:\s*[^\|]+\|\s*/);
if (!m) return notes;
return notes.substring(m[0].length);
}
async function main(): Promise<void> {
const t0 = Date.now();
const importDoc = JSON.parse(readFileSync(IMPORT_JSON, "utf8")) as {
records: ImportRecord[];
};
const skuCodeBySku = new Map(
importDoc.records.map((r) => [r.sku, r.name] as const),
);
const pb = await getAdminPocketBase();
const allRows = await pb.collection("stock_items").getFullList<PbRow>({
filter: `location = "${NSS_LOCATION}"`,
fields: "id,sku,name,description,notes",
batch: 200,
sort: "sku",
});
console.log(`Found ${allRows.length} NSS-imported rows in PB.`);
const rowsToProcess = Number.isFinite(limit)
? allRows.slice(0, limit)
: allRows;
if (rowsToProcess.length < allRows.length) {
console.log(` --limit applied: processing first ${rowsToProcess.length}`);
}
// Build the to-do list. Idempotency check: skip rows that already
// have `name == description == <skuCode>` and `notes` without the
// PosCode prefix.
const todo: Array<{
id: string;
sku: string;
skuCode: string;
currName: string;
currDesc: string;
newName: string;
newDesc: string;
newNotes: string;
}> = [];
const already = { name: 0, desc: 0, notes: 0 };
for (const r of rowsToProcess) {
const skuCode = skuCodeBySku.get(r.sku) ?? r.name; // safe fallback
const desiredName = skuCode;
const desiredDesc = skuCode;
const desiredNotes = stripPosCodePrefix(r.notes ?? "");
const nameOk = r.name === desiredName;
const descOk = r.description === desiredDesc;
const notesOk = desiredNotes === (r.notes ?? "");
if (nameOk) already.name++;
if (descOk) already.desc++;
if (notesOk) already.notes++;
if (nameOk && descOk && notesOk) continue;
todo.push({
id: r.id,
sku: r.sku,
skuCode,
currName: r.name,
currDesc: r.description,
newName: desiredName,
newDesc: desiredDesc,
newNotes: desiredNotes,
});
}
console.log(`Already up-to-date: name=${already.name}, description=${already.desc}, notes=${already.notes} (of ${rowsToProcess.length}).`);
console.log(`To update: ${todo.length}`);
if (todo.length > 0) {
console.log("\nSample diffs:");
for (const t of todo.slice(0, 5)) {
console.log(` ${t.sku}`);
console.log(` name: "${t.currName}" → "${t.newName}"`);
console.log(` description: "${t.currDesc}" → "${t.newDesc}"`);
console.log(` notes: drops "PosCode: ${t.skuCode} | " prefix`);
}
if (todo.length > 5) {
console.log(` … and ${todo.length - 5} more`);
}
}
if (dryRun) {
console.log(`\nDRY RUN — no writes. duration: ${((Date.now() - t0) / 1000).toFixed(1)}s`);
return;
}
let updated = 0;
let failed = 0;
const failures: Array<{ sku: string; error: string }> = [];
for (const t of todo) {
try {
await pb.collection("stock_items").update(t.id, {
name: t.newName,
description: t.newDesc,
notes: t.newNotes,
});
updated++;
if (updated % 25 === 0 || updated === todo.length) {
console.log(` ... ${updated}/${todo.length} updated (last: ${t.sku})`);
}
} catch (err) {
failed++;
const message = err instanceof Error ? err.message : String(err);
failures.push({ sku: t.sku, error: message });
}
}
console.log("\n=== Revert summary ===");
console.log(` Updated: ${updated}`);
console.log(` Failed: ${failed}`);
console.log(` Skipped (already correct): ${rowsToProcess.length - todo.length}`);
console.log(` Duration: ${((Date.now() - t0) / 1000).toFixed(1)}s`);
if (failures.length > 0) {
console.log("\nFailures:");
for (const f of failures) {
console.log(` ${f.sku} → ${f.error}`);
}
process.exitCode = 1;
}
}
main().catch((err) => {
console.error("FATAL:", err);
process.exit(1);
});