fix: send datetime fields as RFC3339, not the picker's local string

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) <noreply@anthropic.com>
This commit is contained in:
Yashas 2026-08-17 18:33:35 +05:30
parent 046c4951db
commit 8e846d6df3

View File

@ -266,13 +266,21 @@ function FieldControl({
onChange={onChange} 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': case 'datetime':
return ( return (
<Input <Input
type="datetime-local" type="datetime-local"
disabled={readOnly} disabled={readOnly}
value={String(value ?? '').slice(0, 16)} value={isoToLocalInput(value)}
onChange={onChange} onChange={(v) => onChange(localInputToIso(v))}
/> />
); );
case 'longtext': case 'longtext':
@ -412,3 +420,28 @@ function OcrUpload({
</div> </div>
); );
} }
/**
* `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
* "<field>(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())}`;
}