diff --git a/src/screens/Enquiry.tsx b/src/screens/Enquiry.tsx index 5130af0..0392920 100644 --- a/src/screens/Enquiry.tsx +++ b/src/screens/Enquiry.tsx @@ -20,6 +20,65 @@ function nowLocal(): string { d.setMinutes(d.getMinutes() - d.getTimezoneOffset()); return d.toISOString().slice(0, 16); } +const MONTHS: Record = { 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 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 setRegret(false)} />; const sfx = revise ? 'revise' : 'offer';