Ops input: delivery slot is a date-time window; conversation shows one bubble per message

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Yashas 2026-10-26 16:40:14 +05:30
parent 265c16f88b
commit 2e3c42df5e

View File

@ -8,6 +8,21 @@ import type { Enquiry } from '../api/types';
import { Badge, Card, Empty, ErrorNote, KV, StateBadge, qtyText, timeAgo, usd } from '../components/ui';
const MODE_LABEL: Record<string, string> = { barge: 'Barge', pipeline: 'Pipeline (ex-wharf)', truck: 'Truck' };
/** "2026-11-12T06:00" -> "12 Nov 2026, 06:00". */
function fmtDateTime(v: string): string {
const d = new Date(v);
if (!v || isNaN(d.getTime())) return '';
return d.toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' }) + ', ' + d.toTimeString().slice(0, 5);
}
/** 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);
if (!a) return '';
if (!to) return a + ' IST';
const b = fmtDateTime(to);
return (from.slice(0, 10) === to.slice(0, 10) ? a + ' - ' + b.split(', ')[1] : a + ' - ' + b) + ' IST';
}
const STOCK_LABEL: Record<string, string> = { available: 'Available', partial: 'Partially available', not_available: 'Not available' };
export function EnquiryPage() {
@ -120,8 +135,8 @@ function Outcome({ e }: { e: Enquiry }) {
/** conversation_log is "Customer: …" / "Desk: …" entries separated by blank lines. */
function Conversation({ log }: { log: string | null }) {
const entries = (log ?? '').split(/\n\n(?=(?:Customer|Desk)[^:\n]{0,24}: )/).filter(Boolean).map((chunk) => {
const m = chunk.match(/^(Customer|Desk)([^:\n]{0,24}): ([\s\S]*)$/);
const entries = (log ?? '').split(/\n+(?=(?:Customer|Desk)(?: \([^)\n]{1,24}\))?: )/).map((c) => c.trim()).filter(Boolean).map((chunk) => {
const m = chunk.match(/^(Customer|Desk)((?: \([^)\n]{1,24}\))?): ([\s\S]*)$/);
return m ? { who: m[1], tag: m[2].trim().replace(/[()]/g, ''), text: m[3] } : { who: 'Desk', tag: '', text: chunk };
});
return (
@ -194,13 +209,14 @@ 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', slot: '', qty: String(e.max_qty_mt ?? ''), remarks: '' });
const [f, setF] = useState({ stock: 'available', mode: 'barge', from: '', to: '', qty: String(e.max_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)} />;
return (
<Card title="Record operations input" action={<Badge tone="notice">Your action</Badge>}>
<form className="grid gap-3 sm:grid-cols-2" onSubmit={(ev) => { ev.preventDefault(); submit(ACT.OPS_INPUT, {
stock_status__ops: f.stock, supply_mode__ops: f.mode, delivery_slot__ops: f.slot,
<form className="grid gap-3 sm:grid-cols-2" onSubmit={(ev) => { ev.preventDefault(); if (windowError) return; submit(ACT.OPS_INPUT, {
stock_status__ops: f.stock, supply_mode__ops: f.mode, delivery_slot__ops: fmtWindow(f.from, f.to),
supply_qty_mt__ops: f.qty === '' ? null : Number(f.qty), ops_remarks__ops: f.remarks,
}); }}>
<Field label="Stock" required>
@ -213,8 +229,10 @@ function OpsForm({ e, onDone }: { e: Enquiry; onDone: () => void }) {
<option value="barge">Barge</option><option value="pipeline">Pipeline (ex-wharf)</option><option value="truck">Truck</option>
</select>
</Field>
<Field label="Delivery slot" required><input className="field-input" required placeholder="12 Nov, 06:00-12:00 IST" value={f.slot} onChange={(x) => setF({ ...f, slot: x.target.value })} /></Field>
<Field label="Delivery from" required><input className="field-input" type="datetime-local" required value={f.from} onChange={(x) => setF({ ...f, from: x.target.value })} /></Field>
<Field label="Delivery to"><input className="field-input" type="datetime-local" min={f.from || undefined} value={f.to} onChange={(x) => setF({ ...f, to: x.target.value })} /></Field>
<Field label="Quantity Ops can supply (MT)"><input className="field-input" type="number" min="0" value={f.qty} onChange={(x) => setF({ ...f, qty: x.target.value })} /></Field>
<div className="self-end pb-2 text-sm text-muted">{windowError ? <span className="text-negative">{windowError}</span> : f.from ? <>Slot: <span className="font-semibold text-ink">{fmtWindow(f.from, f.to)}</span></> : 'Times are IST.'}</div>
<div className="sm:col-span-2"><Field label="Remarks"><textarea className="field-input" rows={2} placeholder="Barge allotted, berth constraints, anything the trader should know" value={f.remarks} onChange={(x) => setF({ ...f, remarks: x.target.value })} /></Field></div>
{error && <div className="sm:col-span-2"><ErrorNote>{error}</ErrorNote></div>}
{ok && <p className="text-sm text-positive sm:col-span-2">{ok}</p>}