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, DAILY_REPORTS, HIDDEN_FORM_FIELDS } from '../../api/config'; export interface DynamicFormProps { client: ZinoClient; activityId: string; instanceId?: number | string; onSuccess?: () => void; onCancel?: () => void; ignorePrefill?: boolean; customPrefillData?: Record; onActivityChange?: (activityName: string) => void; } export function DynamicForm({ client, activityId: initialActivityId, instanceId: initialInstanceId, onSuccess, onCancel, ignorePrefill, customPrefillData, 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(async 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 mapPrefillData = (sourceData: Record) => { res.fields.forEach(f => { const getBaseId = (id: string) => id.replace(/_\d+$/, ''); if (sourceData[f.id] !== undefined) { defaultValues[f.id] = sourceData[f.id]; } else if (f.mapped_workflow_field && sourceData[f.mapped_workflow_field] !== undefined) { defaultValues[f.id] = sourceData[f.mapped_workflow_field]; } else if (sourceData[getBaseId(f.id)] !== undefined) { defaultValues[f.id] = sourceData[getBaseId(f.id)]; } else { const matchingDataKey = Object.keys(sourceData).find(k => getBaseId(k) === f.id || getBaseId(k) === getBaseId(f.id) || k === f.mapped_workflow_field); if (matchingDataKey) { defaultValues[f.id] = sourceData[matchingDataKey]; } } // Special logic for Grid: we must map the keys inside each row to match the grid's column IDs! if (f.data_type === 'grid' && Array.isArray(defaultValues[f.id])) { defaultValues[f.id] = (defaultValues[f.id] as Record[]).map(row => { const newRow: Record = { ...row }; f.columns?.forEach(col => { const colBaseId = getBaseId(col.id); if (row[col.id] !== undefined) { newRow[col.id] = row[col.id]; } else if (col.mapped_workflow_field && row[col.mapped_workflow_field] !== undefined) { newRow[col.id] = row[col.mapped_workflow_field]; } else if (row[colBaseId] !== undefined) { newRow[col.id] = row[colBaseId]; } else { const matchingRowKey = Object.keys(row).find(k => getBaseId(k) === col.id || getBaseId(k) === colBaseId || k === col.mapped_workflow_field); if (matchingRowKey) { newRow[col.id] = row[matchingRowKey]; } } }); return newRow; }); } }); }; if (!ignorePrefill) { if (res.prefill_data && Object.keys(res.prefill_data).length > 0) { mapPrefillData(res.prefill_data); } else if (res.data) { mapPrefillData(res.data); } } if (customPrefillData) { Object.entries(customPrefillData).forEach(([k, v]) => { defaultValues[k] = v; }); } if (chainedPrefillData) { Object.entries(chainedPrefillData).forEach(([k, v]) => { defaultValues[k] = v; }); } if (currentActivityId === DAILY_REPORTS.activities.PUNCH_OUT.uid) { const user = client.currentUser(); const userEmail = user?.email; if (userEmail) { try { const headers: Record = { 'templateid': '192', 'x-pipeline-version': 'draft' }; headers['orgid'] = '57'; headers['groupid'] = '25'; const summaryRes = await client.request( 'POST', '/api/papi2/productive-call-summary', { email: userEmail }, headers ); let summaryData = summaryRes.data || summaryRes; if (Array.isArray(summaryData)) { summaryData = summaryData[0] || {}; } // Map the API response keys to match the form field keys if (summaryData.productive_call !== undefined) { summaryData.total_productive_calls = summaryData.productive_call; } if (summaryData.non_productive_call !== undefined) { summaryData.total_non_productive_calls = summaryData.non_productive_call; } mapPrefillData(summaryData); } catch (e) { console.warn('Failed to load productive call summary', e); } } } 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 clickedActionRef = useRef(null); const hasSubmittedRef = useRef(false); const handleFieldChange = (fieldId: string, newVal: unknown, rawRow?: any) => { setValues(prev => { const next = { ...prev, [fieldId]: newVal }; const getBaseIdForField = (id: string) => id.replace(/_\d+$/, ''); const fieldDef = schema?.fields.find(f => f.id === fieldId); if (rawRow && typeof rawRow === 'object') { const getBaseId = (id: string) => id.replace(/_\d+$/, ''); for (const key of Object.keys(rawRow)) { const lowerKey = key.toLowerCase(); let targetCol = schema?.fields.find(c => c.id.toLowerCase() === lowerKey || (c.mapped_workflow_field || '').toLowerCase() === lowerKey || getBaseId(c.id).toLowerCase() === lowerKey || c.name.toLowerCase().replace(/\s+/g, '') === lowerKey.replace(/_/g, '') ); if (targetCol && targetCol.id !== fieldId) { next[targetCol.id] = rawRow[key]; } else if (!targetCol && key !== fieldId) { next[key] = rawRow[key]; } } } // Auto-fill dataset keys from _raw for select/multiselect/wf_lookup fields if (fieldDef && (fieldDef.data_type === 'select' || fieldDef.data_type === 'multiselect' || fieldDef.data_type === 'wf_lookup')) { const fieldOptions = fieldDef.properties?.options || (fieldDef as any).options; const option = fieldOptions?.find((o: any) => String(o.value) === String(newVal)); if (option && option._raw) { const rawData = option._raw; const getBaseId = (id: string) => id.replace(/_\d+$/, ''); const baseFieldId = getBaseId(fieldId); const prefixMatch = baseFieldId.match(/^([a-z]+)_/); const prefix = prefixMatch ? prefixMatch[1] + '_' : ''; for (const key of Object.keys(rawData)) { const lowerKey = key.toLowerCase(); let targetCol = schema?.fields.find(c => c.id.toLowerCase() === lowerKey || (c.mapped_workflow_field || '').toLowerCase() === lowerKey || getBaseId(c.id).toLowerCase() === lowerKey || c.name.toLowerCase().replace(/\s+/g, '') === lowerKey.replace(/_/g, '') ); if (fieldId.startsWith('route_name') && targetCol) { const baseTargetId = getBaseId(targetCol.id).toLowerCase(); if (baseTargetId === 'owner_name' || baseTargetId === 'phone' || baseTargetId === 'phone_number' || baseTargetId === 'email') { targetCol = undefined; } } if (!targetCol && (fieldId === 'route_name_2' || fieldId.startsWith('route_name'))) { targetCol = schema?.fields.find(c => { const mappedName = (c.mapped_workflow_field || '').toLowerCase(); const nameFallback = (c.name || '').toLowerCase().replace(/\s+/g, '_'); const fieldName = mappedName || nameFallback; return fieldName === `distributor_${lowerKey}` || (lowerKey === 'phone' && fieldName === 'distributor_phone_number') || (lowerKey === 'email' && fieldName === 'distributor_email'); }); } if (!targetCol) { const pKey = prefix + lowerKey; targetCol = schema?.fields.find(c => c.id.toLowerCase() === pKey || (c.mapped_workflow_field || '').toLowerCase() === pKey || getBaseId(c.id).toLowerCase() === pKey || c.name.toLowerCase().replace(/\s+/g, '') === pKey.replace(/_/g, '') ); } if (targetCol && targetCol.id !== fieldId) { let fillVal = rawData[key]; if ((targetCol.data_type === 'select' || targetCol.data_type === 'multiselect') && targetCol.properties?.options) { const matchedOpt = targetCol.properties.options.find( (o: any) => String(o.value).toLowerCase() === String(fillVal).toLowerCase() ); if (matchedOpt) { fillVal = matchedOpt.value; } } console.log(`[Auto-fill] Mapping _raw key "${key}" to form field "${targetCol.id}" with value:`, fillVal); next[targetCol.id] = fillVal; } else if (!targetCol && key !== fieldId) { console.log(`[Auto-fill] Mapping _raw key "${key}" directly to form state with value:`, rawData[key]); next[key] = rawData[key]; } } } } 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 && !HIDDEN_FORM_FIELDS.includes(f.id)); 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) { let val = finalValues[f.id]; const isNotesOrRemarks = f.id.toLowerCase().includes('notes') || f.id.toLowerCase().includes('remarks') || (f.name && (f.name.toLowerCase().includes('notes') || f.name.toLowerCase().includes('remarks'))); if (isNotesOrRemarks && val == null) { val = ''; } 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); } hasSubmittedRef.current = true; 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_2 || (chainedPrefillData?.['select_store_row'] as any)?.store_code_2 || ''); 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_2 || 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_2 || detailRes.data.store_code)) { storeCodeToSend = String(detailRes.data.store_code_2 || 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_2 || row.store_code)) { storeCodeToSend = String(row.store_code_2 || 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 || f.properties?.disabled; const isPlaceOrder = currentActivityId === ORDER_BOOKING.activities.PLACE_ORDER.uid; const isOrderId = f.name.toLowerCase() === 'order id' || f.id.toLowerCase().includes('order_id') || f.mapped_workflow_field === 'order_id'; if (isPlaceOrder && isOrderId) { return null; } const renderField = () => { if (type === 'wf_lookup') { return ( handleFieldChange(f.id, newVal, rawRow)} /> ); } 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)} isActivityPotentialMining={currentActivityId === ORDER_BOOKING.activities.POTENTIAL_MINING.uid} /> ); } 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)} /> ); } if (type === 'app_user') { const user = client.currentUser(); const displayVal = user?.name || val; return ( { }} disabled /> ); } 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) => ( )) ) : ( )}
); }