- 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.
206 lines
6.5 KiB
TypeScript
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);
|
|
});
|