import { useState, useEffect } from 'react'; import type { ZinoClient } from '../../api/client'; import type { FormScreenResponse } from '../../api/types'; import { Button } from '../buttons/Button'; import { Spinner } from '../reusable/Spinner'; import { FileInput, SmartGridField, GeolocationInput, PhoneInput, SelectField, TextField, EmailField, TextAreaField, DateField, TimeField, WfLookupField, RadioField, } from './fields'; import { ORDER_BOOKING } from '../../api/config'; export interface DynamicFormProps { client: ZinoClient; activityId: string; instanceId?: number | string; onSuccess?: () => void; onCancel?: () => void; ignorePrefill?: boolean; } export function DynamicForm({ client, activityId: initialActivityId, instanceId: initialInstanceId, onSuccess, onCancel, ignorePrefill }: DynamicFormProps) { const [currentActivityId, setCurrentActivityId] = useState(initialActivityId); const [currentInstanceId, setCurrentInstanceId] = useState(initialInstanceId); const [chainQueue, setChainQueue] = useState>([]); useEffect(() => { setCurrentActivityId(initialActivityId); setCurrentInstanceId(initialInstanceId); setChainQueue([]); }, [initialActivityId, initialInstanceId]); const [schema, setSchema] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { let mounted = true; setLoading(true); client.formSchema(currentActivityId, currentInstanceId) .then(res => { if (mounted) { setSchema(res); const defaultValues: Record = {}; if (res.field_defaults) { Object.entries(res.field_defaults).forEach(([fieldId, def]) => { if (def.value != null) { defaultValues[fieldId] = def.value; } else if (def.prefill) { if (def.prefill.value === 'current_date') { defaultValues[fieldId] = new Date().toISOString().split('T')[0]; } else if (def.prefill.value === 'current_time') { defaultValues[fieldId] = new Date().toTimeString().split(' ')[0].substring(0, 5); } else if (def.prefill.value === 'current_user_id') { const user = client.currentUser(); defaultValues[fieldId] = user ? Number(user.id) : ''; } else { defaultValues[fieldId] = def.prefill.value; } } }); } const imageFieldIds = new Set( res.fields.filter(f => f.data_type === 'image' || f.data_type === 'file').map(f => f.id) ); if (!ignorePrefill) { if (res.prefill_data) { Object.entries(res.prefill_data).forEach(([k, v]) => { if (!imageFieldIds.has(k)) defaultValues[k] = v; }); } else if (res.data) { Object.entries(res.data).forEach(([k, v]) => { if (!imageFieldIds.has(k)) defaultValues[k] = v; }); } } setValues(defaultValues); setLoading(false); } }) .catch((err: any) => { if (mounted) { setError(err?.message || 'Failed to load schema'); setLoading(false); } }); return () => { mounted = false; }; }, [client, currentActivityId, currentInstanceId]); const [values, setValues] = useState>({}); const [submitting, setSubmitting] = useState(false); const [submitError, setSubmitError] = useState(null); const handleFieldChange = (fieldId: string, newVal: unknown) => { setValues(prev => { const next = { ...prev, [fieldId]: newVal }; // Auto-calculate order_details totals if (fieldId === 'order_details' && Array.isArray(newVal)) { let totalBags = 0; let totalKgs = 0; newVal.forEach(row => { totalBags += Number(row.bags) || 0; totalKgs += Number(row.row_kgs) || 0; }); next['total_bags'] = totalBags; next['total_kgs'] = totalKgs; } return next; }); }; if (loading) { return
; } if (error || !schema) { return
Failed to load form: {error}
; } // Filter out disabled fields (usually server-generated IDs) const fields = schema.fields.filter(f => !f.properties?.disabled); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setSubmitting(true); setSubmitError(null); try { const payload: Record = {}; for (const f of fields) { const val = values[f.id]; if (val == null) continue; if (f.data_type === 'phone' && typeof val === 'string') { const phoneNum = val.replace(/^\+91\s*/, '').trim(); payload[f.id] = { dial_code: '+91', phone: phoneNum, phone_with_dial_code: `+91${phoneNum}` }; } else if ((f.data_type === 'image' || f.data_type === 'file') && Array.isArray(val) && val.length > 0 && val[0] instanceof File) { const uploadedFiles = []; for (const file of val) { const fileMeta = await client.uploadFile(file, { activityId: currentActivityId, fieldId: f.id }); uploadedFiles.push(fileMeta); } payload[f.id] = uploadedFiles; } else if ((f.data_type === 'image' || f.data_type === 'file') && val instanceof File) { const fileMeta = await client.uploadFile(val, { activityId: currentActivityId, fieldId: f.id }); payload[f.id] = [fileMeta]; } else { payload[f.id] = val; } } let res; if (currentInstanceId != null) { res = await client.performActivity(currentInstanceId, currentActivityId, payload); } else { res = await client.startInstance(currentActivityId, payload); } let pending = [...chainQueue]; let chainSource = (res as any).activity_chain || schema.activity_chain || []; // Centralized activity chaining logic if (currentActivityId === ORDER_BOOKING.activities.PRODUCTIVITY_OF_VISIT.uid) { const actionValue = String(payload[ORDER_BOOKING.activities.PRODUCTIVITY_OF_VISIT.fields.action] || '').toLowerCase().trim(); const normalized = actionValue.replace(/[\s_]+/g, ''); if (normalized === 'order') { chainSource = [{ activity_uid: ORDER_BOOKING.activities.PLACE_ORDER.uid, activity_name: 'Place Order' }]; } else if (normalized === 'noorder') { chainSource = [{ activity_uid: ORDER_BOOKING.activities.POTENTIAL_MINING.uid, activity_name: 'Potential Mining' }]; } } const newActivities = chainSource.filter( (a: any) => a.activity_uid !== currentActivityId ); // Remove any existing occurrences from pending to avoid duplicates pending = pending.filter(p => !newActivities.some(n => n.activity_uid === p.activity_uid)); // Prepend the new activities for depth-first execution (nested chaining) pending = [...newActivities, ...pending]; const nextActivity = pending.shift(); if (nextActivity) { setChainQueue(pending); setCurrentActivityId(nextActivity.activity_uid); setCurrentInstanceId(res.instance_id ?? currentInstanceId); } else { onSuccess?.(); } } catch (err: any) { setSubmitError(err.message || 'Failed to submit form'); } finally { setSubmitting(false); } }; return (
{fields.map(f => { const type = f.data_type; const val = values[f.id]; const isDisabled = schema.field_defaults?.[f.id]?.disabled; const renderField = () => { if (type === 'wf_lookup') { return ( handleFieldChange(f.id, newVal)} /> ); } if (type.startsWith('select') || type.startsWith('multiselect')) { return ( handleFieldChange(f.id, newVal)} /> ); } if (type === 'radio') { return ( handleFieldChange(f.id, newVal)} /> ); } if (type.startsWith('image') || type.startsWith('file')) { return ( handleFieldChange(f.id, newVal)} /> ); } if (type.startsWith('grid')) { return ( []) || []} onChange={(newVal) => handleFieldChange(f.id, newVal)} /> ); } if (type.startsWith('geolocation')) { return ( handleFieldChange(f.id, newVal)} /> ); } if (type.startsWith('phone')) { const phoneStr = typeof val === 'object' && val !== null ? (val as any).phone || '' : (val as string) ?? ''; return ( handleFieldChange(f.id, newVal)} /> ); } if (type.startsWith('email')) { return ( handleFieldChange(f.id, newVal)} /> ); } if (type.startsWith('textarea') || type.startsWith('text_area') || type.startsWith('longtext')) { return ( handleFieldChange(f.id, newVal)} /> ); } if (type.startsWith('date')) { return ( handleFieldChange(f.id, newVal)} /> ); } if (type.startsWith('time')) { return ( handleFieldChange(f.id, newVal)} /> ); } return ( handleFieldChange(f.id, newVal)} /> ); }; const content = renderField(); return isDisabled ? (
{content}
) : (
{content}
); })} {submitError &&
{submitError}
}
{onCancel && ( )}
); }