Files
Doormilexpress_console/src/utils/bulkOrderColumns.js
2026-08-19 17:08:45 +05:30

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 };
};