import { useState, useEffect, useRef } 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, STORE } from '../../api/config'; export interface DynamicFormProps { client: ZinoClient; activityId: string; instanceId?: number | string; onSuccess?: () => void; onCancel?: () => void; ignorePrefill?: boolean; customPrefillData?: Record; initialActivityName?: string; onActivityChange?: (name: string) => void; } export function DynamicForm({ client, activityId: initialActivityId, instanceId: initialInstanceId, onSuccess, onCancel, ignorePrefill, customPrefillData, initialActivityName, onActivityChange }: 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); const [chainedPrefillData, setChainedPrefillData] = useState | undefined>(); 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; }); } } if (customPrefillData) { Object.entries(customPrefillData).forEach(([k, v]) => { if (!imageFieldIds.has(k)) defaultValues[k] = v; }); } if (chainedPrefillData) { Object.entries(chainedPrefillData).forEach(([k, v]) => { if (!imageFieldIds.has(k)) defaultValues[k] = v; }); } setValues(defaultValues); setLoading(false); if (res.activity_name) { onActivityChange?.(res.activity_name); } } }) .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 clickedActionRef = useRef(null); const handleFieldChange = (fieldId: string, newVal: unknown, fullRow?: any) => { setValues(prev => { const next = { ...prev, [fieldId]: newVal }; if (fullRow) { next[`${fieldId}_row`] = fullRow; } // Auto-calculate order_details totals const getBaseIdForField = (id: string) => id.replace(/_\d+$/, ''); const fieldDef = schema?.fields.find(f => f.id === fieldId); const isOrderDetails = fieldId === 'order_details' || getBaseIdForField(fieldId) === 'order_details' || fieldDef?.mapped_workflow_field === 'order_details'; if (isOrderDetails && Array.isArray(newVal)) { let totalBags = 0; let totalKgs = 0; const getBaseId = (id: string) => id.replace(/_\d+$/, ''); const bagsColId = fieldDef?.columns?.find(c => c.id === 'bags' || getBaseId(c.id) === 'bags' || c.mapped_workflow_field === 'bags' || c.name.toLowerCase() === 'bags' )?.id || 'bags'; const rowKgsColId = fieldDef?.columns?.find(c => c.id === 'row_kgs' || getBaseId(c.id) === 'row_kgs' || c.id === 'rowkgs' || getBaseId(c.id) === 'rowkgs' || c.mapped_workflow_field === 'row_kgs' )?.id || 'row_kgs'; newVal.forEach(row => { totalBags += Number(row[bagsColId]) || 0; totalKgs += Number(row[rowKgsColId]) || 0; }); const tbField = schema?.fields.find(f => f.id === 'total_bags' || getBaseId(f.id) === 'total_bags' || f.mapped_workflow_field === 'total_bags'); const tkField = schema?.fields.find(f => f.id === 'total_kgs' || getBaseId(f.id) === 'total_kgs' || f.mapped_workflow_field === 'total_kgs'); if (tbField) next[tbField.id] = totalBags; else next['total_bags'] = totalBags; if (tkField) next[tkField.id] = totalKgs; else 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 actionField = fields.find(f => f.name.toLowerCase() === 'action' && f.data_type === 'radio'); const normalFields = fields.filter(f => f !== actionField); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setSubmitting(true); setSubmitError(null); try { const payload: Record = {}; const finalValues = { ...values }; if (actionField && clickedActionRef.current) { finalValues[actionField.id] = clickedActionRef.current; } for (const f of fields) { const val = finalValues[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, instanceId: currentInstanceId }); 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, instanceId: currentInstanceId }); 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: any) => 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) { let nextPrefillData = undefined; if (nextActivity.activity_uid === ORDER_BOOKING.activities.POTENTIAL_MINING.uid) { let storeCodeToSend = String((values['select_store_row'] as any)?.store_code || (chainedPrefillData?.['select_store_row'] as any)?.store_code || ''); if (!storeCodeToSend) { let storeId = values['select_store'] || chainedPrefillData?.['select_store'] || (schema?.prefill_data as any)?.select_store || (schema?.data as any)?.select_store; if (typeof storeId === 'object' && storeId !== null) { storeCodeToSend = (storeId as any).store_code || storeCodeToSend; storeId = (storeId as any).value || (storeId as any).instance_id || String(storeId); } if (!storeCodeToSend && storeId) { try { const detailRes = await client.detailView(STORE.detailViews.STORE, storeId); if (detailRes.data && detailRes.data.store_code) { storeCodeToSend = String(detailRes.data.store_code); } } catch(e) { try { const lookupRes = await client.wfLookupRecords({ activityId: ORDER_BOOKING.activities.LOG_VISIT.uid, fieldId: ORDER_BOOKING.activities.LOG_VISIT.fields.selectStore, formData: { ...(chainedPrefillData as any || {}), ...values, ...(schema?.prefill_data as any || {}) }, limit: 500 }); const arr = Array.isArray(lookupRes) ? lookupRes : (lookupRes.data || lookupRes.records || []); const row = arr.find((r: any) => String(r.instance_id || r.id) === String(storeId)); if (row && row.store_code) { storeCodeToSend = String(row.store_code); } } catch (err) { console.error("Failed to fetch store code:", err); } } } } if (!storeCodeToSend) { storeCodeToSend = String(values['select_store'] || chainedPrefillData?.['select_store'] || (schema?.prefill_data as any)?.select_store || ''); } try { const pmRes = await client.request<{ potential: { potential: any[] } }>( 'POST', '/api/papi2/potential-mining', { instance_id: String(res.instance_id ?? currentInstanceId), store_code: storeCodeToSend }, { 'TemplateID': '146' } ); const rawPotential = pmRes.potential?.potential || []; const mappedPotential = rawPotential.map((row: any) => { const cat = row.product_category || row.product_category_ || row.category; return { ...row, product_category_: cat, product_category: cat, productcategory: cat, category: cat, product_category_1: cat }; }); nextPrefillData = { potential: mappedPotential }; } catch (e) { console.error("Failed to fetch potential mining for chain", e); } } setChainedPrefillData(prev => ({ ...prev, ...values, ...(nextPrefillData || {}) })); setChainQueue(pending); setCurrentActivityId(nextActivity.activity_uid); setCurrentInstanceId(res.instance_id ?? currentInstanceId); onActivityChange?.(nextActivity.activity_name); } else { onSuccess?.(); } } catch (err: any) { setSubmitError(err.message || 'Failed to submit form'); } finally { setSubmitting(false); } }; return (
{normalFields.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, fullRow)} /> ); } 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 && ( )} {actionField && actionField.properties?.options ? ( actionField.properties.options.map((opt: any) => ( )) ) : ( )}
); }