zurich_kotak/src/components/ActivityForm.jsx
Yashas 5ea627b064 Make the console operable: add a lead, work a lead
The pipeline could be looked at but not driven. Adds the three pieces
that make the machine walkable end to end from the UI.

- ActivityForm renders whatever /view/form-screens returns — labels,
  types, select options, mandatory flags — and submits it back. No form
  is defined in this repo, so a field added in Studio appears here with
  no code change.
- AddLead presents the three doors. Each is a separate INIT activity with
  its own permissions; the bank one lands further along because the bank
  already did CKYC.
- Lead shows the file grouped in the order it was worked, ending with
  what the sourcing agent earns, plus the activities this state allows
  and who normally performs each.

The "who normally performs this" label is presentational only. Nothing
here enforces anything — the workflow refuses server-side and the form
reports what it said, including a note when a 403 is the platform
declining rather than the console misbehaving.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 16:11:28 +05:30

114 lines
4.5 KiB
JavaScript

import { useEffect, useState } from 'react'
import { useZino } from '../api/provider.jsx'
import './ActivityForm.css'
/**
* Renders whatever /view/form-screens returns for an activity — labels, types,
* select options and which fields are mandatory — and submits it straight back.
*
* Nothing about this form is defined in the frontend. Add a field to the
* activity in Studio, redeploy, and it appears here with no code change. That
* is the point: the workflow is the source of truth, and a hardcoded form would
* quietly drift from it.
*/
export default function ActivityForm({ activityUid, instanceId, onDone, onCancel }) {
const { client } = useZino()
const [schema, setSchema] = useState(null)
const [values, setValues] = useState({})
const [error, setError] = useState(null)
const [busy, setBusy] = useState(false)
useEffect(() => {
let dead = false
setSchema(null); setError(null); setValues({})
client.formSchema(activityUid, instanceId)
.then((s) => { if (!dead) setSchema(s) })
.catch((e) => { if (!dead) setError(e) })
return () => { dead = true }
}, [client, activityUid, instanceId])
function set(id, v) { setValues((p) => ({ ...p, [id]: v })) }
async function submit(e) {
e.preventDefault()
setBusy(true); setError(null)
// Send only fields the activity defines. A submission is schema-validated
// and an unknown field is fatal, so empties are dropped rather than sent.
const payload = {}
for (const f of schema.fields) {
const v = values[f.id]
if (v === undefined || v === '' || (Array.isArray(v) && v.length === 0)) continue
payload[f.id] = f.data_type === 'number' ? Number(v) : v
}
try {
const res = instanceId
? await client.activity(instanceId, activityUid, payload)
: await client.start(activityUid, payload)
onDone?.(res)
} catch (err) {
setError(err)
} finally {
setBusy(false)
}
}
if (error && !schema) return <div className="af__err">Could not load the form {error.status} {error.message}</div>
if (!schema) return <p className="af__loading">Loading the form</p>
return (
<form className="af" onSubmit={submit}>
<div className="af__grid">
{schema.fields.map((f) => {
const opts = f.properties?.options || []
const v = values[f.id] ?? (f.data_type === 'multiselect' ? [] : '')
return (
<label key={f.uid} className={'af__field' + (f.data_type === 'longtext' ? ' af__field--wide' : '')}>
<span>{f.name}{f.mandatory ? <em className="af__req">required</em> : null}</span>
{f.data_type === 'select' ? (
<select value={v} onChange={(e) => set(f.id, e.target.value)}>
<option value=""></option>
{opts.map((o) => <option key={o.value} value={o.value}>{o.label}</option>)}
</select>
) : f.data_type === 'multiselect' ? (
<div className="af__multi">
{opts.map((o) => (
<button
key={o.value} type="button"
className={'af__chip' + (v.includes(o.value) ? ' is-on' : '')}
onClick={() => set(f.id, v.includes(o.value) ? v.filter((x) => x !== o.value) : [...v, o.value])}
>{o.label}</button>
))}
</div>
) : f.data_type === 'longtext' ? (
<textarea rows={3} value={v} onChange={(e) => set(f.id, e.target.value)} />
) : (
<input
type={f.data_type === 'number' ? 'number' : f.data_type === 'date' ? 'date' : 'text'}
value={v} onChange={(e) => set(f.id, e.target.value)}
/>
)}
</label>
)
})}
</div>
{error ? (
<div className="af__err">
<strong>{error.status}</strong> {error.message}
{error.status === 403 || String(error.message).includes('permission denied') ? (
<p>This is the workflow refusing, not the console. The signed-in role does not hold this activity.</p>
) : null}
</div>
) : null}
<div className="af__actions">
{onCancel ? <button type="button" className="af__ghost" onClick={onCancel}>Cancel</button> : null}
<button type="submit" className="af__submit" disabled={busy}>
{busy ? 'Submitting…' : 'Submit'}
</button>
</div>
</form>
)
}