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:
parent
c3dba17ba9
commit
7103e7723f
@ -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';
|
||||
|
||||
Loading…
Reference in New Issue
Block a user