import { useState, useEffect, useRef } from 'react'; import type { ZinoClient } from '../../api/client'; import type { FormScreenResponse, FormScreenField } 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 [showErrors, setShowErrors] = useState(false); const clickedActionRef = useRef(null); const handleFieldChange = (fieldId: string, newVal: unknown, fullRow?: any) => { if (submitError) { setSubmitError(null); } 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 => { const bags = Number(row[bagsColId] ?? row.bags ?? row.quantity) || 0; const sku = Number(row.sku) || 0; const rowKgs = Number(row[rowKgsColId] ?? row.row_kgs) || (bags * sku); totalBags += bags; totalKgs += rowKgs; }); 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(); setShowErrors(true); setSubmitError(null); const finalValues = { ...values }; if (actionField && clickedActionRef.current) { finalValues[actionField.id] = clickedActionRef.current; } // 1. Pre-submit consolidation for grid fields & total fields normalFields.forEach((f) => { if (f.data_type.startsWith('grid')) { const rawGrid = (finalValues[f.id] as Record[]) || []; if (rawGrid.length > 0) { const merged: Record[] = []; const indexMap = new Map(); for (const r of rawGrid) { const cat = String(r.category || r.product_category || '').trim().toLowerCase(); const prod = String(r.productName || r.product_name || r.product || '').trim().toLowerCase(); const currentBags = Number(r.bags || r.quantity || 0); if (cat && prod) { const key = `${cat}::${prod}`; if (indexMap.has(key)) { const targetIdx = indexMap.get(key)!; const existingRow = merged[targetIdx]; const existingBags = Number(existingRow.bags || existingRow.quantity || 0); const addedBags = currentBags > 0 ? currentBags : 1; const newBags = existingBags + addedBags; const skuVal = Number(existingRow.sku || r.sku || 0); merged[targetIdx] = { ...existingRow, bags: newBags, quantity: newBags, row_kgs: skuVal * newBags, }; continue; } else { indexMap.set(key, merged.length); } } merged.push(r); } // Clean virtual keys const cleanGrid = merged.map((r) => { const clean: Record = {}; Object.keys(r).forEach((k) => { if (k !== '_row_total_kgs' && k !== 'row_kgs' && k !== 'row_total') { clean[k] = r[k]; } }); return clean; }); finalValues[f.id] = cleanGrid; // Recalculate total_bags and total_kgs let totalB = 0; let totalK = 0; cleanGrid.forEach((r) => { const b = Number(r.bags || r.quantity) || 0; const sku = Number(r.sku) || 0; totalB += b; totalK += b * sku; }); const getBaseId = (id: string) => id.replace(/_\d+$/, ''); normalFields.forEach((tf) => { const baseId = getBaseId(tf.id); const mapped = (tf.mapped_workflow_field || '').toLowerCase(); if (baseId === 'total_bags' || mapped === 'total_bags' || tf.id === 'total_bags') { finalValues[tf.id] = Math.round(totalB); } if (baseId === 'total_kgs' || mapped === 'total_kgs' || tf.id === 'total_kgs') { finalValues[tf.id] = Math.round(totalK); } }); } } }); // 2. Validate grid fields (Order Details) const extractGridRowDetails = (r: Record, columns?: FormScreenField[]) => { let cat = String(r.category || r.product_category || r.cat || '').trim(); let prod = String(r.productName || r.product_name || r.product || r.name || '').trim(); let bagsNum = 0; if (r.bags !== undefined && r.bags !== null && r.bags !== '') bagsNum = Number(r.bags); else if (r.quantity !== undefined && r.quantity !== null && r.quantity !== '') bagsNum = Number(r.quantity); else if (r.actual_potential !== undefined && r.actual_potential !== null && r.actual_potential !== '') bagsNum = Number(r.actual_potential); else if (r.store_potential !== undefined && r.store_potential !== null && r.store_potential !== '') bagsNum = Number(r.store_potential); if (columns && columns.length > 0) { for (const c of columns) { const idL = c.id.toLowerCase(); const nL = (c.name || '').toLowerCase(); const mL = (c.mapped_workflow_field || '').toLowerCase(); if (!cat && (idL.includes('category') || nL.includes('category') || mL.includes('category'))) { cat = String(r[c.id] || '').trim(); } if (!prod && (idL.includes('product') || nL.includes('product') || mL.includes('product') || idL.includes('name') || nL.includes('name'))) { prod = String(r[c.id] || '').trim(); } if (bagsNum <= 0 && (idL.includes('bags') || nL.includes('bags') || mL.includes('bags') || idL.includes('quantity') || nL.includes('quantity') || idL.includes('potential') || nL.includes('potential'))) { const val = Number(r[c.id]); if (!isNaN(val) && val > 0) bagsNum = val; } } } if (bagsNum <= 0) { for (const k of Object.keys(r)) { const kL = k.toLowerCase(); if ((kL.includes('bags') || kL.includes('quantity') || kL.includes('potential')) && !kL.includes('total') && !kL.includes('kgs')) { const val = Number(r[k]); if (!isNaN(val) && val > 0) { bagsNum = val; break; } } } } return { cat, prod, bags: isNaN(bagsNum) ? 0 : bagsNum }; }; for (const f of normalFields) { if (f.data_type.startsWith('grid')) { const gridRows = (finalValues[f.id] as Record[]) || []; // Check if grid has at least one valid row const validRows = gridRows.filter((r) => { const { cat, prod, bags } = extractGridRowDetails(r, f.columns); return cat && prod && bags > 0; }); if (validRows.length === 0) { setSubmitError('Please add at least one complete product with Category, Product Name, and Bags.'); return; } // Check if any row is partially filled const hasIncompleteRow = gridRows.some((r) => { const { cat, prod, bags } = extractGridRowDetails(r, f.columns); if (!cat && !prod && bags <= 0) return false; if (cat && (!prod || bags <= 0)) return true; if (prod && (!cat || bags <= 0)) return true; if (bags > 0 && (!cat || !prod)) return true; return false; }); if (hasIncompleteRow) { setSubmitError('Please complete all required product details (Product Name and Bags) before submitting.'); return; } } } // 3. Validate mandatory normal fields using finalValues const getBaseId = (id: string) => id.replace(/_\d+$/, ''); for (const f of normalFields) { if (f.mandatory) { if (f.data_type.startsWith('grid')) continue; const baseId = getBaseId(f.id); const mapped = (f.mapped_workflow_field || '').toLowerCase(); if (baseId === 'total_bags' || mapped === 'total_bags' || f.id === 'total_bags' || baseId === 'total_kgs' || mapped === 'total_kgs' || f.id === 'total_kgs') { continue; // Total fields are calculated automatically from grid } const v = finalValues[f.id]; if (v === undefined || v === null || String(v).trim() === '') { setSubmitError(`Please fill in required field: ${f.name}`); return; } } } setSubmitting(true); try { const payload: Record = {}; 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); } }; const getBaseIdForField = (id: string) => id.replace(/_\d+$/, ''); const hasGridField = normalFields.some(f => f.data_type.startsWith('grid')); const tbField = normalFields.find(f => f.id === 'total_bags' || getBaseIdForField(f.id) === 'total_bags' || f.mapped_workflow_field === 'total_bags'); const tkField = normalFields.find(f => f.id === 'total_kgs' || getBaseIdForField(f.id) === 'total_kgs' || f.mapped_workflow_field === 'total_kgs'); const displayFields = normalFields.filter(f => { if (hasGridField) { const baseId = getBaseIdForField(f.id); const mapped = (f.mapped_workflow_field || '').toLowerCase(); const isTotalField = baseId === 'total_bags' || baseId === 'total_kgs' || mapped === 'total_bags' || mapped === 'total_kgs' || f.id === 'total_bags' || f.id === 'total_kgs'; if (isTotalField) return false; } return true; }); return (
{displayFields.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)} totalBagsValue={tbField ? Number(values[tbField.id]) || 0 : undefined} totalKgsValue={tkField ? Number(values[tkField.id]) || 0 : undefined} showErrors={showErrors} /> ); } 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)} /> ); } const isValEmpty = val === undefined || val === null || String(val).trim() === ''; const currentFieldError = showErrors && f.mandatory && isValEmpty ? 'Required' : undefined; return ( handleFieldChange(f.id, newVal)} error={currentFieldError} /> ); }; const content = renderField(); return isDisabled ? (
{content}
) : (
{content}
); })} {submitError &&
{submitError}
}
{onCancel && ( )} {actionField && actionField.properties?.options ? ( actionField.properties.options.map((opt: any) => ( )) ) : ( )}
); }