import { useEffect, useMemo, useState } from 'react'; import { CheckCircle2 } from 'lucide-react'; import { Button } from '../core/Button'; import { Input } from '../core/Input'; import { SelectField } from '../core/SelectField'; import { MultiSelect } from '../core/MultiSelect'; import { LookupField } from './LookupField'; import type { LookupValue } from './LookupField'; import { useQuery, useZino } from '../../api/provider'; import { schemaToFields } from '../../api/schema'; import type { ActivityMeta, FieldDef } from '../../api/forms'; import type { LeadRecord } from '../../api/types'; import type { ActivityResult } from '../../api/types'; export interface ActivityFormProps { /** Activity display copy + uid; fields are fetched live from the schema. */ meta: ActivityMeta; /** Omit for the INIT activity (Capture Lead) — submits via /start. */ instanceId?: number | string; /** Seed values from an existing record. */ record?: LeadRecord | null; onSuccess?: (res: ActivityResult) => void; /** Override the green confirmation line. */ successMessage?: string; } // datetime-local ("YYYY-MM-DDTHH:MM") → RFC 3339 with local TZ offset, the // format the workflow engine validates datetime fields against. function toRfc3339(local: string): string { const d = new Date(local); if (Number.isNaN(d.getTime())) return local; const pad = (n: number) => String(n).padStart(2, '0'); const off = -d.getTimezoneOffset(); const sign = off >= 0 ? '+' : '-'; const abs = Math.abs(off); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}${sign}${pad(Math.floor(abs / 60))}:${pad(abs % 60)}`; } function seedFor(field: FieldDef, record?: LeadRecord | null): unknown { const key = field.seedKey ?? field.id.replace(/_input$/, ''); const fromRecord = record ? (record as Record)[key] : undefined; if (fromRecord !== undefined && fromRecord !== null) return fromRecord; switch (field.type) { case 'multiselect': return []; case 'boolean': return false; default: return ''; } } /** Renders an activity's fields from the live form-screens schema and submits * via performActivity (or startInstance when no instanceId is given). */ export function ActivityForm({ meta, instanceId, record, onSuccess, successMessage }: ActivityFormProps) { const { client } = useZino(); const schemaQ = useQuery( () => client.formSchema(meta.uid, instanceId), [meta.uid, instanceId], ); const fields = useMemo( () => (schemaQ.data ? schemaToFields(schemaQ.data) : []), [schemaQ.data], ); const [values, setValues] = useState>({}); const [submitting, setSubmitting] = useState(false); const [result, setResult] = useState<{ ok: boolean; msg: string } | null>(null); // Seed once the schema loads (and re-seed if the record changes). useEffect(() => { if (!fields.length) return; const init: Record = {}; fields.forEach((f) => (init[f.id] = seedFor(f, record))); setValues(init); }, [fields, record]); const set = (id: string, v: unknown) => setValues((p) => ({ ...p, [id]: v })); const missing = useMemo( () => fields.filter((f) => { if (!f.required) return false; const v = values[f.id]; if (Array.isArray(v)) return v.length === 0; return v === '' || v === null || v === undefined; }), [fields, values], ); async function submit() { if (missing.length) { setResult({ ok: false, msg: `Fill required: ${missing.map((m) => m.label).join(', ')}` }); return; } setSubmitting(true); setResult(null); // Drop empty optional values so we don't overwrite with blanks. const data: Record = {}; for (const f of fields) { const v = values[f.id]; if (v === '' || v === null || v === undefined) continue; if (Array.isArray(v) && v.length === 0) continue; data[f.id] = f.type === 'number' ? Number(v) || 0 : f.type === 'datetime' ? toRfc3339(String(v)) : v; } try { const res = instanceId != null ? await client.performActivity(instanceId, meta.uid, data) : await client.startInstance(meta.uid, data); setResult({ ok: true, msg: successMessage ?? `${meta.name} ${meta.done}.` }); onSuccess?.(res); } catch (e) { setResult({ ok: false, msg: e instanceof Error ? e.message : ((e as { message?: string })?.message ?? 'Submit failed') }); } finally { setSubmitting(false); } } if (schemaQ.loading) { return
Loading form…
; } if (schemaQ.error || !fields.length) { return (
{schemaQ.error ? `Couldn't load the form: ${schemaQ.error}` : 'This activity has no input fields.'}
); } return (
{fields.map((f) => ( set(f.id, v)} activityId={meta.uid} instanceId={instanceId} /> ))}
{result && (
{result.ok && } {result.msg}
)}
); } function Field({ field, value, onChange, activityId, instanceId, }: { field: FieldDef; value: unknown; onChange: (v: unknown) => void; activityId: string; instanceId?: number | string; }) { const full = field.type === 'longtext' || field.type === 'multiselect' || field.type === 'lookup'; const wrap = full ? 'col-span-full' : ''; switch (field.type) { case 'longtext': return (