Prefill the Ops delivery window from the ETA or laycan, and the offer validity with 24 hours

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Yashas 2026-10-27 13:22:35 +05:30
parent c3dba17ba9
commit 7103e7723f

View File

@ -20,6 +20,65 @@ function nowLocal(): string {
d.setMinutes(d.getMinutes() - d.getTimezoneOffset());
return d.toISOString().slice(0, 16);
}
const MONTHS: Record<string, number> = { jan: 0, feb: 1, mar: 2, apr: 3, may: 4, jun: 5, jul: 6, aug: 7, sep: 8, sept: 8, oct: 9, nov: 10, dec: 11 };
function toLocalInput(y: number, m: number, d: number, hh: number): string {
const dt = new Date(y, m, d, hh, 0, 0);
if (isNaN(dt.getTime())) return '';
dt.setMinutes(dt.getMinutes() - dt.getTimezoneOffset());
return dt.toISOString().slice(0, 16);
}
/**
* A delivery window suggested from the ETA / laycan text the customer gave
* ("ETA 14 Nov 2026", "Laycan 12-15 Nov 2026", "14/11/2026"). A single date
* becomes a 06:00-12:00 slot that day, a laycan runs 06:00 on the first day to
* 18:00 on the last. Anything unparseable leaves the fields empty for Ops.
*/
function suggestedWindow(text: string | null | undefined): { from: string; to: string } {
const none = { from: '', to: '' };
if (!text) return none;
const t = text.toLowerCase();
const now = new Date();
const year = (y?: string) => {
if (y) return y.length === 2 ? 2000 + Number(y) : Number(y);
return now.getFullYear();
};
const bump = (y: number, m: number, d: number) => (new Date(y, m, d + 1) < now && !/\d{4}/.test(t) ? y + 1 : y);
// "12-15 nov 2026", "12 - 15 nov", "12 to 15 nov 2026"
let m = t.match(/(\d{1,2})\s*(?:-|–|to)\s*(\d{1,2})\s+([a-z]{3,9})\.?,?\s*(\d{4}|\d{2})?/);
if (m && (MONTHS[m[3].slice(0, 4)] !== undefined || MONTHS[m[3].slice(0, 3)] !== undefined)) {
const mo = MONTHS[m[3].slice(0, 4)] ?? MONTHS[m[3].slice(0, 3)];
const y = bump(year(m[4]), mo, Number(m[1]));
return { from: toLocalInput(y, mo, Number(m[1]), 6), to: toLocalInput(y, mo, Number(m[2]), 18) };
}
// "14 nov 2026", "14 nov", "14th nov"
m = t.match(/(\d{1,2})(?:st|nd|rd|th)?\s+([a-z]{3,9})\.?,?\s*(\d{4}|\d{2})?/);
if (m && (MONTHS[m[2].slice(0, 4)] !== undefined || MONTHS[m[2].slice(0, 3)] !== undefined)) {
const mo = MONTHS[m[2].slice(0, 4)] ?? MONTHS[m[2].slice(0, 3)];
const y = bump(year(m[3]), mo, Number(m[1]));
return { from: toLocalInput(y, mo, Number(m[1]), 6), to: toLocalInput(y, mo, Number(m[1]), 12) };
}
// "nov 14, 2026", "nov 14"
m = t.match(/([a-z]{3,9})\.?\s+(\d{1,2})(?:st|nd|rd|th)?,?\s*(\d{4})?/);
if (m && (MONTHS[m[1].slice(0, 4)] !== undefined || MONTHS[m[1].slice(0, 3)] !== undefined)) {
const mo = MONTHS[m[1].slice(0, 4)] ?? MONTHS[m[1].slice(0, 3)];
const y = bump(year(m[3]), mo, Number(m[2]));
return { from: toLocalInput(y, mo, Number(m[2]), 6), to: toLocalInput(y, mo, Number(m[2]), 12) };
}
// "14/11/2026", "14-11-2026", "14.11.26" (day first, as the desk writes it)
m = t.match(/(\d{1,2})[\/.-](\d{1,2})[\/.-](\d{4}|\d{2})/);
if (m) {
const y = year(m[3]);
return { from: toLocalInput(y, Number(m[2]) - 1, Number(m[1]), 6), to: toLocalInput(y, Number(m[2]) - 1, Number(m[1]), 12) };
}
return none;
}
/** A default offer validity: 24 hours from now, on the hour. */
function defaultValidity(): string {
const d = new Date();
d.setHours(d.getHours() + 25, 0, 0, 0);
d.setMinutes(d.getMinutes() - d.getTimezoneOffset());
return d.toISOString().slice(0, 16);
}
/** A delivery window as the text the offer and the customer read: "12 Nov 2026, 06:00 - 12:00 IST". */
function fmtWindow(from: string, to: string): string {
const a = fmtDateTime(from);
@ -217,7 +276,8 @@ function Field({ label, required, children }: { label: string; required?: boolea
function OpsForm({ e, onDone }: { e: Enquiry; onDone: () => void }) {
const { busy, error, ok, submit } = useSubmit(e, onDone);
const [f, setF] = useState({ stock: 'available', mode: 'barge', from: '', to: '', qty: String(e.max_qty_mt ?? ''), remarks: '' });
const win = suggestedWindow(e.eta_laycan);
const [f, setF] = useState({ stock: 'available', mode: 'barge', from: win.from, to: win.to, qty: String(e.max_qty_mt ?? e.min_qty_mt ?? ''), remarks: '' });
const windowError = f.from && f.to && f.to <= f.from ? 'The window must end after it starts' : null;
const [regret, setRegret] = useState(false);
if (regret) return <ReasonForm e={e} onDone={onDone} activity={ACT.REGRET} field="lost_reason__regret" title="Regret the enquiry" cta="Send regret to customer" onCancel={() => setRegret(false)} />;
@ -256,7 +316,7 @@ function OpsForm({ e, onDone }: { e: Enquiry; onDone: () => void }) {
function OfferForm({ e, onDone, revise, onCancel }: { e: Enquiry; onDone: () => void; revise: boolean; onCancel?: () => void }) {
const { busy, error, ok, submit } = useSubmit(e, onDone);
const [f, setF] = useState({ price: revise && e.price_usd_mt ? String(e.price_usd_mt) : '', basis: e.price_basis ?? `delivered ex-${e.supply_mode === 'pipeline' ? 'wharf' : 'barge'} ${e.port ?? ''}`.trim(),
valid: '', terms: e.payment_terms ?? '30 days from date of delivery', remarks: '' });
valid: defaultValidity(), terms: e.payment_terms ?? '30 days from date of delivery', remarks: '' });
const [regret, setRegret] = useState(false);
if (regret) return <ReasonForm e={e} onDone={onDone} activity={ACT.REGRET} field="lost_reason__regret" title="Regret the enquiry" cta="Send regret to customer" onCancel={() => setRegret(false)} />;
const sfx = revise ? 'revise' : 'offer';