From 8e846d6df322b707231e8d2d774da3aab462c65a Mon Sep 17 00:00:00 2001 From: Yashas Date: Mon, 17 Aug 2026 18:33:35 +0530 Subject: [PATCH] fix: send datetime fields as RFC3339, not the picker's local string MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Release Disbursal failed with: validation failed for activity hdfc-act-disburse: disbursed_at(type) An HTML datetime-local input yields "2026-08-17T18:31" — minute precision, no seconds, no timezone. The workflow validates the field as a timestamp and rejects that shape. The API smoke test passed because it sent RFC3339 ("2026-08-17T11:20:00Z"), so the gap only ever showed through the UI. Converts local wall-clock to RFC3339 UTC on the way out and back again on the way in. Storing the ISO string and slicing it for display would have shown the UTC time in the picker — 13:01 for an 18:31 selection — which reads as the control losing the input. An unparseable value is handed back untouched rather than blanked, so a value the server sent stays visible even if we misread it. Co-Authored-By: Claude Opus 5 (1M context) --- src/components/ActivityForm.tsx | 37 +++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/src/components/ActivityForm.tsx b/src/components/ActivityForm.tsx index a3b838e..58e1969 100644 --- a/src/components/ActivityForm.tsx +++ b/src/components/ActivityForm.tsx @@ -266,13 +266,21 @@ function FieldControl({ onChange={onChange} /> ); + // A `datetime-local` input yields "2026-08-17T18:31" — no seconds, no + // timezone. The server validates this field as a timestamp and rejects + // that shape outright: + // validation failed for activity hdfc-act-disburse: disbursed_at(type) + // So the browser value is converted to RFC3339 UTC on the way out, and + // back to local wall-clock on the way in. Storing the ISO string and + // slicing it for display would show the UTC time in the picker — 13:01 + // for an 18:31 selection — which looks like the control losing input. case 'datetime': return ( onChange(localInputToIso(v))} /> ); case 'longtext': @@ -412,3 +420,28 @@ function OcrUpload({ ); } + +/** + * `datetime-local` <-> RFC3339, the two conversions the datetime control needs. + * + * The input speaks local wall-clock with minute precision and no zone; the + * workflow's timestamp validator wants a full RFC3339 instant. Neither will + * accept the other's format, and getting it wrong fails as an opaque + * "(type)" validation error with no hint about which half is at fault. + */ +function localInputToIso(v: string): string { + if (!v) return ''; + const d = new Date(v); // parsed as LOCAL time, which is what the picker meant + return Number.isNaN(d.getTime()) ? v : d.toISOString(); +} + +function isoToLocalInput(v: unknown): string { + const s = String(v ?? ''); + if (!s) return ''; + const d = new Date(s); + // Not a parseable instant — hand it back untouched rather than blanking the + // field, so a value the server sent stays visible even if we misread it. + if (Number.isNaN(d.getTime())) return s.slice(0, 16); + const p = (n: number) => String(n).padStart(2, '0'); + return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}T${p(d.getHours())}:${p(d.getMinutes())}`; +}