- 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.
143 lines
No EOL
3.9 KiB
TypeScript
143 lines
No EOL
3.9 KiB
TypeScript
/**
|
||
* Maps scripts/nss-grease-nipples-pilot.csv into PocketBase
|
||
* `stock_items` records for review.
|
||
*
|
||
* Output: scripts/nss-grease-nipples-pb-import.json
|
||
*
|
||
* Run: npx tsx scripts/nss-grease-nipples-pb-transform.ts
|
||
*
|
||
* Differences vs nss-spring-pins-iso8752-pb-transform.ts:
|
||
* - The product family is "Grease Nipples" (DIN71412A/B/C, DIN3404)
|
||
* not Spring Pins.
|
||
* - The CSV has `thread` instead of `length` (spring pins are
|
||
* diameter×length, grease nipples are diameter+thread+head angle).
|
||
* - The source URL is the unfiltered landing page (no query string).
|
||
* - Location string and SOURCE_LABEL match the grease-nipple catalogue.
|
||
*/
|
||
import { readFileSync, writeFileSync } from "node:fs";
|
||
import { join } from "node:path";
|
||
|
||
const SOURCE_CSV = join(
|
||
import.meta.dirname,
|
||
"nss-grease-nipples-pilot.csv",
|
||
);
|
||
const OUTPUT_JSON = join(
|
||
import.meta.dirname,
|
||
"nss-grease-nipples-pb-import.json",
|
||
);
|
||
const SOURCE_URL =
|
||
"https://nssfasteners.co.za/grease-nipples-and-pipe-plugs/grease-nipples.html";
|
||
const SOURCE_LABEL = "NSS Fasteners price list - grease nipples";
|
||
const EXTRACTED_AT = "2026-09-16";
|
||
|
||
const SUPPLIER_NSS_ID = "6xuu7a0pmmubtqd";
|
||
const CATEGORY_FASTENERS_ID = "4emvaanzogj0586";
|
||
const MARKUP_PERCENT = 80;
|
||
|
||
type CsvRow = {
|
||
sku: string;
|
||
posDescription: string;
|
||
description: string;
|
||
material: string;
|
||
grade: string;
|
||
diameter: string;
|
||
thread: string;
|
||
finish: string;
|
||
pack: string;
|
||
unitPrice: string;
|
||
url: string;
|
||
};
|
||
|
||
const parsePrice = (s: string): number | null => {
|
||
const m = s.match(/([\d.]+)/);
|
||
if (!m) return null;
|
||
const n = parseFloat(m[1]);
|
||
return Number.isFinite(n) ? n : null;
|
||
};
|
||
|
||
const round2 = (n: number) => Math.round(n * 100) / 100;
|
||
|
||
const toPbRecord = (r: CsvRow) => {
|
||
const costPrice = parsePrice(r.unitPrice);
|
||
const notes = [
|
||
`Material: ${r.material}`,
|
||
`Diameter: ${r.diameter}`,
|
||
`Thread: ${r.thread}`,
|
||
r.grade ? `Grade: ${r.grade}` : null,
|
||
`Finish: ${r.finish}`,
|
||
r.pack ? `Pack: ${r.pack}` : null,
|
||
`Source: ${SOURCE_LABEL}`,
|
||
]
|
||
.filter(Boolean)
|
||
.join(" | ");
|
||
|
||
const costHistory = [
|
||
{
|
||
price: costPrice,
|
||
currency: "ZAR",
|
||
source: "nssfasteners.co.za",
|
||
extracted_at: EXTRACTED_AT,
|
||
source_url: SOURCE_URL,
|
||
qty_basis: 1,
|
||
extraction_method: "browser_automation",
|
||
notes: SOURCE_LABEL,
|
||
},
|
||
];
|
||
|
||
return {
|
||
name: r.posDescription,
|
||
description: r.posDescription,
|
||
sku: r.sku,
|
||
location: SOURCE_LABEL,
|
||
notes,
|
||
cost_history: costHistory,
|
||
cost_price: costPrice,
|
||
selling_price:
|
||
costPrice != null ? round2(costPrice * (1 + MARKUP_PERCENT / 100)) : 0,
|
||
markup: MARKUP_PERCENT,
|
||
unit: "Each",
|
||
current_qty: 0,
|
||
min_qty: 0,
|
||
target_qty: 0,
|
||
is_active: true,
|
||
supplier: SUPPLIER_NSS_ID,
|
||
category: CATEGORY_FASTENERS_ID,
|
||
};
|
||
};
|
||
|
||
const csv = readFileSync(SOURCE_CSV, "utf8").trim();
|
||
const [headerLine, ...dataLines] = csv.split("\n");
|
||
const headers = headerLine.split(",") as (keyof CsvRow)[];
|
||
|
||
const rows: CsvRow[] = dataLines.map((line) => {
|
||
const cols = line.split(",");
|
||
const obj = {} as CsvRow;
|
||
headers.forEach((h, i) => {
|
||
(obj as Record<string, string>)[h] = cols[i] ?? "";
|
||
});
|
||
return obj;
|
||
});
|
||
|
||
const records = rows.map(toPbRecord);
|
||
|
||
writeFileSync(
|
||
OUTPUT_JSON,
|
||
JSON.stringify(
|
||
{
|
||
meta: {
|
||
source_csv: "scripts/nss-grease-nipples-pilot.csv",
|
||
source_url: SOURCE_URL,
|
||
extracted_at: EXTRACTED_AT,
|
||
row_count: records.length,
|
||
pb_collection: "stock_items",
|
||
notes:
|
||
"selling_price = cost_price * (1 + 80%). name=description both carry the PosCode (e.g., 'BSP 1/4 GREASE NIPPLE 180'). unit=Each. Catalogue spans DIN71412A 180°, DIN71412B 45°, DIN71412C 90°, DIN3404 Button, and DIN71412A Long (180° long). Sub-pages walked separately; final CSV is the deduped union.",
|
||
},
|
||
records,
|
||
},
|
||
null,
|
||
2,
|
||
),
|
||
);
|
||
|
||
console.log(`Wrote ${records.length} records to ${OUTPUT_JSON}`); |