Initial commit - Route Commerce platform
This commit is contained in:
@@ -0,0 +1,275 @@
|
||||
// Pure CSV parsing utilities — no server-side logic
|
||||
|
||||
export type ParsedProductRow = {
|
||||
name: string;
|
||||
description: string;
|
||||
price: number;
|
||||
type: string;
|
||||
active: boolean;
|
||||
image_url?: string;
|
||||
_rowIndex: number;
|
||||
_warnings: string[];
|
||||
};
|
||||
|
||||
export function parseProductCSV(csvText: string): {
|
||||
success: true;
|
||||
rows: ParsedProductRow[];
|
||||
totalRows: number;
|
||||
errors: { row: number; error: string }[];
|
||||
} | {
|
||||
success: false;
|
||||
error: string;
|
||||
} {
|
||||
const lines = csvText.trim().split(/\r?\n/);
|
||||
if (lines.length < 2) {
|
||||
return { success: false, error: "CSV must have a header row and at least one data row" };
|
||||
}
|
||||
|
||||
const header = lines[0].split(",").map((h) => h.trim().toLowerCase());
|
||||
const required = ["name", "price", "type"];
|
||||
for (const col of required) {
|
||||
if (!header.includes(col)) {
|
||||
return { success: false, error: `Missing required column: ${col}` };
|
||||
}
|
||||
}
|
||||
|
||||
const nameIdx = header.indexOf("name");
|
||||
const descIdx = header.indexOf("description");
|
||||
const priceIdx = header.indexOf("price");
|
||||
const typeIdx = header.indexOf("type");
|
||||
const activeIdx = header.indexOf("active");
|
||||
const imgIdx = header.indexOf("image_url");
|
||||
|
||||
const rows: ParsedProductRow[] = [];
|
||||
const errors: { row: number; error: string }[] = [];
|
||||
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
const cols = lines[i].split(",").map((c) => c.trim());
|
||||
const name = cols[nameIdx] ?? "";
|
||||
|
||||
if (!name) {
|
||||
errors.push({ row: i + 1, error: "Name is required" });
|
||||
continue;
|
||||
}
|
||||
|
||||
const priceStr = cols[priceIdx] ?? "";
|
||||
const price = parseFloat(priceStr);
|
||||
if (isNaN(price) || price < 0) {
|
||||
errors.push({ row: i + 1, error: `Invalid price: ${priceStr}` });
|
||||
continue;
|
||||
}
|
||||
|
||||
const type = cols[typeIdx] ?? "";
|
||||
if (!["Pickup", "Shipping", "Pickup & Shipping"].includes(type)) {
|
||||
errors.push({ row: i + 1, error: `Invalid type: ${type}` });
|
||||
continue;
|
||||
}
|
||||
|
||||
const active = activeIdx >= 0
|
||||
? (cols[activeIdx]?.toLowerCase() !== "false")
|
||||
: true;
|
||||
|
||||
const warnings: string[] = [];
|
||||
if (descIdx >= 0 && !cols[descIdx]) warnings.push("description was empty");
|
||||
if (imgIdx >= 0 && !cols[imgIdx]) warnings.push("image_url was empty");
|
||||
|
||||
rows.push({
|
||||
name,
|
||||
description: descIdx >= 0 ? cols[descIdx] ?? "" : "",
|
||||
price,
|
||||
type,
|
||||
active,
|
||||
image_url: imgIdx >= 0 ? cols[imgIdx] : undefined,
|
||||
_rowIndex: i + 1,
|
||||
_warnings: warnings,
|
||||
});
|
||||
}
|
||||
|
||||
return { success: true, rows, totalRows: lines.length - 1, errors };
|
||||
}
|
||||
|
||||
// ── Stop CSV parser ─────────────────────────────────────────────────────────
|
||||
|
||||
export type ParsedStopRow = {
|
||||
city: string;
|
||||
state: string;
|
||||
location: string;
|
||||
date: string;
|
||||
time: string;
|
||||
address?: string;
|
||||
zip?: string;
|
||||
notes?: string;
|
||||
_rowIndex: number;
|
||||
_warnings: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Flexible CSV parser for stop schedules.
|
||||
* Auto-detects common column names: city/town, state, location/address,
|
||||
* date, time, zip, notes.
|
||||
*/
|
||||
export function parseStopCSV(csvText: string): {
|
||||
success: true;
|
||||
rows: ParsedStopRow[];
|
||||
totalRows: number;
|
||||
errors: { row: number; error: string }[];
|
||||
} | {
|
||||
success: false;
|
||||
error: string;
|
||||
} {
|
||||
const lines = csvText.trim().split(/\r?\n/);
|
||||
if (lines.length < 2) {
|
||||
return { success: false, error: "CSV must have a header row and at least one data row" };
|
||||
}
|
||||
|
||||
// Build column index map — common synonyms supported
|
||||
const rawHeader = lines[0].split(",").map((h) => h.trim());
|
||||
const header = rawHeader.map((h) => h.toLowerCase());
|
||||
|
||||
function idx(...names: string[]): number {
|
||||
for (const n of names) {
|
||||
const i = header.indexOf(n);
|
||||
if (i >= 0) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
const cityIdx = idx("city", "town", "location");
|
||||
const stateIdx = idx("state", "st");
|
||||
const locIdx = idx("location", "place", "stop name", "stop", "name");
|
||||
const dateIdx = idx("date", "day", "pickup date", "pickupdate");
|
||||
const timeIdx = idx("time", "hours", "pickup time", "pickuptime", "time slot");
|
||||
const addrIdx = idx("address", "street", "street address", "addr");
|
||||
const zipIdx = idx("zip", "zipcode", "zip code", "postal");
|
||||
const notesIdx = idx("notes", "note", "instructions", "driver notes");
|
||||
|
||||
const missing = [cityIdx, stateIdx, locIdx, dateIdx, timeIdx].filter((i) => i < 0);
|
||||
if (missing.length > 0) {
|
||||
return {
|
||||
success: false,
|
||||
error: "CSV is missing required columns. Need: city, state, location, date, time. Found: " + rawHeader.join(", "),
|
||||
};
|
||||
}
|
||||
|
||||
const rows: ParsedStopRow[] = [];
|
||||
const errors: { row: number; error: string }[] = [];
|
||||
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
const raw = lines[i].split(",").map((c) => c.trim());
|
||||
|
||||
const city = raw[cityIdx] ?? "";
|
||||
const state = raw[stateIdx] ?? "";
|
||||
const location = raw[locIdx] ?? "";
|
||||
const date = raw[dateIdx] ?? "";
|
||||
const time = raw[timeIdx] ?? "";
|
||||
const address = addrIdx >= 0 ? raw[addrIdx] : undefined;
|
||||
const zip = zipIdx >= 0 ? raw[zipIdx] : undefined;
|
||||
const notes = notesIdx >= 0 ? raw[notesIdx] : undefined;
|
||||
|
||||
const warnings: string[] = [];
|
||||
if (!city) warnings.push("city is empty");
|
||||
if (!state) warnings.push("state is empty");
|
||||
if (!location) warnings.push("location is empty");
|
||||
if (!date) warnings.push("date is empty");
|
||||
if (!time) warnings.push("time is empty");
|
||||
|
||||
rows.push({
|
||||
city,
|
||||
state,
|
||||
location,
|
||||
date,
|
||||
time,
|
||||
address,
|
||||
zip,
|
||||
notes,
|
||||
_rowIndex: i + 1,
|
||||
_warnings: warnings,
|
||||
});
|
||||
}
|
||||
|
||||
return { success: true, rows, totalRows: lines.length - 1, errors };
|
||||
}
|
||||
|
||||
export type ParsedOrderRow = {
|
||||
customer_name: string;
|
||||
customer_email: string;
|
||||
customer_phone: string;
|
||||
stop_id: string;
|
||||
items: Array<{ product_id: string; quantity: number; fulfillment: string }>;
|
||||
_rowIndex: number;
|
||||
_warnings: string[];
|
||||
};
|
||||
|
||||
export function parseOrderCSV(csvText: string): {
|
||||
success: true;
|
||||
rows: ParsedOrderRow[];
|
||||
totalRows: number;
|
||||
errors: { row: number; error: string }[];
|
||||
} | {
|
||||
success: false;
|
||||
error: string;
|
||||
} {
|
||||
const lines = csvText.trim().split(/\r?\n/);
|
||||
if (lines.length < 2) {
|
||||
return { success: false, error: "CSV must have a header row and at least one data row" };
|
||||
}
|
||||
|
||||
const header = lines[0].split(",").map((h) => h.trim().toLowerCase());
|
||||
const required = ["customer_name", "customer_email", "stop_id", "product_id", "quantity", "fulfillment"];
|
||||
for (const col of required) {
|
||||
if (!header.includes(col)) {
|
||||
return { success: false, error: `Missing required column: ${col}` };
|
||||
}
|
||||
}
|
||||
|
||||
const nameIdx = header.indexOf("customer_name");
|
||||
const emailIdx = header.indexOf("customer_email");
|
||||
const phoneIdx = header.indexOf("customer_phone");
|
||||
const stopIdx = header.indexOf("stop_id");
|
||||
const prodIdx = header.indexOf("product_id");
|
||||
const qtyIdx = header.indexOf("quantity");
|
||||
const fulfillIdx = header.indexOf("fulfillment");
|
||||
|
||||
const rows: ParsedOrderRow[] = [];
|
||||
const errors: { row: number; error: string }[] = [];
|
||||
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
const cols = lines[i].split(",").map((c) => c.trim());
|
||||
|
||||
const customer_name = cols[nameIdx] ?? "";
|
||||
const customer_email = cols[emailIdx] ?? "";
|
||||
const stop_id = cols[stopIdx] ?? "";
|
||||
const product_id = cols[prodIdx] ?? "";
|
||||
const quantity = parseInt(cols[qtyIdx] ?? "0", 10);
|
||||
const fulfillment = cols[fulfillIdx] ?? "pickup";
|
||||
|
||||
if (!customer_name) {
|
||||
errors.push({ row: i + 1, error: "Customer name is required" });
|
||||
continue;
|
||||
}
|
||||
if (!stop_id) {
|
||||
errors.push({ row: i + 1, error: "Stop ID is required" });
|
||||
continue;
|
||||
}
|
||||
if (!product_id) {
|
||||
errors.push({ row: i + 1, error: "Product ID is required" });
|
||||
continue;
|
||||
}
|
||||
if (isNaN(quantity) || quantity < 1) {
|
||||
errors.push({ row: i + 1, error: `Invalid quantity: ${cols[qtyIdx]}` });
|
||||
continue;
|
||||
}
|
||||
|
||||
rows.push({
|
||||
customer_name,
|
||||
customer_email,
|
||||
customer_phone: phoneIdx >= 0 ? cols[phoneIdx] ?? "" : "",
|
||||
stop_id,
|
||||
items: [{ product_id, quantity, fulfillment }],
|
||||
_rowIndex: i + 1,
|
||||
_warnings: [],
|
||||
});
|
||||
}
|
||||
|
||||
return { success: true, rows, totalRows: lines.length - 1, errors };
|
||||
}
|
||||
Reference in New Issue
Block a user