151 lines
6.5 KiB
JavaScript
151 lines
6.5 KiB
JavaScript
// ==============================|| Bulk-order sheet columns ||============================== //
|
|
//
|
|
// The single definition of what a bulk-order spreadsheet may look like. Two
|
|
// consumers share it:
|
|
//
|
|
// • pages/nearle/orders/multipleOrders.js — the console's bulk-upload page,
|
|
// where this map originated.
|
|
// • pages/nearle/assistant/bulkFile.js — Doormile AI's file upload.
|
|
//
|
|
// It lives here so a sheet that uploads on the page also uploads in the bot,
|
|
// permanently. Copying it into the assistant was the alternative and was
|
|
// rejected for the obvious reason: five pages once carried private STATUS_META
|
|
// copies that drifted into disagreeing about the same order's label.
|
|
//
|
|
// SHEET_HEADER_MAP is reproduced from the page byte for byte, including the `*`
|
|
// suffix convention (a starred header is required) and the note about which
|
|
// mappings were never confirmed against a live tenant sheet. Do not "tidy" the
|
|
// keys — they are the tenant's actual column titles, spaces and all.
|
|
|
|
export const normalizeHeader = (header) => header?.toString().trim().toLowerCase().replace(/\s+/g, '');
|
|
|
|
// A receiver name arrives prefixed with the sheet's own row numbering
|
|
// ("12. Ravi Kumar") often enough that stripping it is part of parsing.
|
|
export const cleanReceiverName = (name) => (typeof name === 'string' ? name.replace(/^[\d.\s]+/, '').trim() : name);
|
|
|
|
export const SHEET_HEADER_MAP = {
|
|
'pickupdate(yyyy-mmm-dd)': 'date',
|
|
'sendername*': 'locationname',
|
|
'senderphone*': 'locationcontact',
|
|
'senderaddress*': 'locationaddress',
|
|
'receivername*': 'firstname',
|
|
receiverphone: 'contactno',
|
|
'receiveralternatephone*': 'altcontactno',
|
|
receiverfulladdress: 'address',
|
|
// Best-effort — no live sample sheet to confirm these are the tenant's
|
|
// actual column headers. Without them deliverycity/deliverypincode in
|
|
// the submit payload were always sent blank; if these names are wrong,
|
|
// behavior is unchanged from before (still blank), not worse.
|
|
receivercity: 'city',
|
|
receiverpincode: 'postcode',
|
|
receiverlatitude: 'latitude',
|
|
receiverlongitude: 'longitude',
|
|
'itemdescription*': 'description',
|
|
Quantity: 'quantity',
|
|
' Collect Cash': 'collectionamt'
|
|
};
|
|
|
|
// Starred headers are the ones the page reports as missing.
|
|
export const requiredSheetColumns = () => Object.keys(SHEET_HEADER_MAP).filter((k) => k.trim().endsWith('*'));
|
|
|
|
// ---- assistant-side translation ---------------------------------------------
|
|
//
|
|
// The page consumes the internal names above (`firstname`, `contactno`, …)
|
|
// because its row editors were built on them. The assistant's rows are shaped
|
|
// like a booking payload instead, so the two are bridged here rather than in
|
|
// either consumer.
|
|
const INTERNAL_TO_ROW = {
|
|
firstname: 'customer_name',
|
|
contactno: 'customer_phone',
|
|
address: 'deliveryaddress',
|
|
postcode: 'deliverypincode',
|
|
city: 'deliverycity',
|
|
latitude: 'deliverylatitude',
|
|
longitude: 'deliverylongitude',
|
|
description: 'itemdescription',
|
|
quantity: 'quantity'
|
|
};
|
|
|
|
// Headers someone typing their own sheet actually writes. Accepted in addition
|
|
// to the tenant's official titles, never instead of them.
|
|
//
|
|
// `collectionamt` (the sheet's "Collect Cash") is deliberately NOT treated as a
|
|
// price: it is cash to collect from the recipient, while `finalprice` is what
|
|
// the delivery costs. Mapping one onto the other would quietly bill the wrong
|
|
// number on every row.
|
|
const PLAIN_ALIASES = {
|
|
customer_name: ['name', 'customername', 'customer', 'recipient', 'recipientname', 'receiver', 'to'],
|
|
customer_phone: ['phone', 'mobile', 'phonenumber', 'mobilenumber', 'customerphone', 'contact', 'recipientphone'],
|
|
deliveryaddress: ['address', 'deliveryaddress', 'dropaddress', 'fulladdress', 'destination'],
|
|
deliverypincode: ['pincode', 'pin', 'postcode', 'zip', 'zipcode', 'deliverypincode'],
|
|
deliverycity: ['city', 'town', 'deliverycity'],
|
|
deliverylatitude: ['latitude', 'lat', 'deliverylatitude'],
|
|
deliverylongitude: ['longitude', 'lng', 'lon', 'deliverylongitude'],
|
|
itemdescription: ['description', 'item', 'itemdescription', 'contents', 'goods', 'particulars'],
|
|
itemcategory: ['category', 'itemcategory', 'type'],
|
|
quantity: ['quantity', 'qty', 'parcels', 'pieces', 'count'],
|
|
finalprice: ['price', 'amount', 'charge', 'deliverycharge', 'finalprice', 'rate', 'fare'],
|
|
weight: ['weight', 'kg', 'kgs']
|
|
};
|
|
|
|
// normalised header → assistant row field. Built once; the tenant's official
|
|
// titles win over a plain alias if a sheet somehow carries both.
|
|
const ROW_FIELD_BY_HEADER = (() => {
|
|
const out = {};
|
|
Object.entries(PLAIN_ALIASES).forEach(([field, headers]) => {
|
|
headers.forEach((h) => {
|
|
out[normalizeHeader(h)] = field;
|
|
});
|
|
});
|
|
Object.entries(SHEET_HEADER_MAP).forEach(([sheetHeader, internal]) => {
|
|
const field = INTERNAL_TO_ROW[internal];
|
|
if (!field) return;
|
|
out[normalizeHeader(sheetHeader)] = field;
|
|
out[normalizeHeader(sheetHeader).replace(/\*+$/, '')] = field;
|
|
});
|
|
return out;
|
|
})();
|
|
|
|
// The `*` in a sheet title is a "this one is required" annotation the tenant
|
|
// types, not part of the column's name — so `Receiver Phone*` and
|
|
// `ReceiverPhone` are the same column and both must resolve. Only the
|
|
// assistant's lookup is star-tolerant; normalizeHeader itself is untouched,
|
|
// because multipleOrders.js derives its missing-required warning from the star
|
|
// and loosening that would change the page's behaviour.
|
|
const unstar = (h) => normalizeHeader(h).replace(/\*+$/, '');
|
|
|
|
export const rowFieldForHeader = (header) => ROW_FIELD_BY_HEADER[normalizeHeader(header)] || ROW_FIELD_BY_HEADER[unstar(header)] || null;
|
|
|
|
// Every field the assistant can fill from a sheet, for the downloadable template.
|
|
export const TEMPLATE_HEADERS = [
|
|
'Receiver Name*',
|
|
'Receiver Phone*',
|
|
'Receiver Full Address*',
|
|
'Receiver Pincode*',
|
|
'Receiver City',
|
|
'Item Description*',
|
|
'Quantity',
|
|
'Price'
|
|
];
|
|
|
|
// ---- one sheet row → one assistant row --------------------------------------
|
|
//
|
|
// Unrecognised columns are RETURNED, not dropped silently: an operator whose
|
|
// price column is titled something unexpected needs to be told it was ignored,
|
|
// or they'll submit 200 orders priced from a column that was never read.
|
|
export const mapSheetRow = (raw) => {
|
|
const row = {};
|
|
const ignored = [];
|
|
|
|
Object.entries(raw || {}).forEach(([header, value]) => {
|
|
const field = rowFieldForHeader(header);
|
|
if (!field) {
|
|
if (String(header || '').trim() && String(value ?? '').trim()) ignored.push(String(header).trim());
|
|
return;
|
|
}
|
|
row[field] = field === 'customer_name' ? cleanReceiverName(value) : value;
|
|
});
|
|
|
|
return { row, ignored };
|
|
};
|