krishnasales_mobile/src/components/forms/DynamicForm.tsx

942 lines
37 KiB
TypeScript

import { useState, useEffect, useRef } from 'react';
import localforage from 'localforage';
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, SELECT_OPTION_LIMIT, PIPELINE, FORM_DRAFT_EXPIRY_MS, APP_VERSION } from '../../api/config';
export interface DynamicFormProps {
client: ZinoClient;
activityId: string;
instanceId?: number | string;
onSuccess?: () => void;
onCancel?: () => void;
ignorePrefill?: boolean;
customPrefillData?: Record<string, unknown>;
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<number | string | undefined>(initialInstanceId);
const [chainQueue, setChainQueue] = useState<Array<{ activity_uid: string; activity_name: string }>>([]);
const draftKey = `draft_form_${currentActivityId}_${currentInstanceId || 'new'}`;
useEffect(() => {
setCurrentActivityId(initialActivityId);
setCurrentInstanceId(initialInstanceId);
setChainQueue([]);
}, [initialActivityId, initialInstanceId]);
const [schema, setSchema] = useState<FormScreenResponse | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [chainedPrefillData, setChainedPrefillData] = useState<Record<string, unknown> | undefined>();
useEffect(() => {
let mounted = true;
setLoading(true);
client.formSchema(currentActivityId, currentInstanceId)
.then(async res => {
if (mounted) {
setSchema(res);
const defaultValues: Record<string, unknown> = {};
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<string, unknown>) => {
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<string, unknown>[]).map(row => {
const newRow: Record<string, unknown> = { ...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 summaryRes = await client.request<any>(
'POST',
PIPELINE.endpoints.productiveCallSummary,
{ email: userEmail }
);
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);
}
}
}
defaultValuesRef.current = { ...defaultValues };
// Load draft if available
try {
const draft: any = await localforage.getItem(`draft_form_${currentActivityId}_${currentInstanceId || 'new'}`);
if (draft && draft.version === APP_VERSION && draft.values) {
const now = Date.now();
if (now - draft.timestamp < FORM_DRAFT_EXPIRY_MS) {
Object.assign(defaultValues, draft.values);
} else {
await localforage.removeItem(`draft_form_${currentActivityId}_${currentInstanceId || 'new'}`);
}
}
} catch (e) {
console.error('Failed to load draft', 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 defaultValuesRef = useRef<Record<string, unknown>>({});
const [values, setValues] = useState<Record<string, unknown>>({});
const [submitting, setSubmitting] = useState(false);
const [submitError, setSubmitError] = useState<string | null>(null);
const [backendErrors, setBackendErrors] = useState<Record<string, string>>({});
const clickedActionRef = useRef<string | null>(null);
const hasSubmittedRef = useRef(false);
// Auto-save form draft whenever values change
useEffect(() => {
if (!loading && Object.keys(values).length > 0) {
localforage.setItem(draftKey, {
version: APP_VERSION,
timestamp: Date.now(),
values
}).catch(e => console.error('Failed to save draft', e));
}
}, [values, draftKey, loading]);
const handleReset = async () => {
if (window.confirm('Are you sure you want to clear the draft and reset this form?')) {
await localforage.removeItem(draftKey);
setValues({ ...defaultValuesRef.current });
}
};
const isLocalhost = typeof window !== 'undefined' && (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1');
const handleSeedData = () => {
if (!schema) return;
const next: Record<string, unknown> = { ...values };
schema.fields.forEach(f => {
if (f.properties?.disabled || HIDDEN_FORM_FIELDS.includes(f.id)) return;
if (next[f.id] != null && next[f.id] !== '') return;
const type = f.data_type;
const lowerName = (f.name || '').toLowerCase();
if (type.startsWith('select') || type.startsWith('multiselect') || type.startsWith('radio') || type === 'wf_lookup') {
const opts = f.properties?.options || (f as any).options;
if (opts && opts.length > 0) {
next[f.id] = opts[0].value;
}
} else if (type === 'number') {
if (lowerName.includes('pincode')) {
next[f.id] = '123456';
} else {
next[f.id] = 10;
}
} else if (type.startsWith('text') || type.startsWith('string') || type.startsWith('textarea') || type.startsWith('longtext')) {
if (lowerName.includes('pincode')) {
next[f.id] = '123456';
} else {
next[f.id] = 'Test ' + f.name;
}
} else if (type.startsWith('email')) {
next[f.id] = 'test@example.com';
} else if (type.startsWith('phone')) {
next[f.id] = '9999999999';
} else if (type.startsWith('date')) {
next[f.id] = new Date().toISOString().split('T')[0];
} else if (type.startsWith('time')) {
next[f.id] = '12:00';
} else if (type.startsWith('geolocation')) {
next[f.id] = { lat: 20.5937, lng: 78.9629, accuracy: 10 };
}
});
setValues(next);
};
const handleFieldChange = (fieldId: string, newVal: unknown, rawRow?: any) => {
setSubmitError(null);
setBackendErrors(prev => {
if (!prev[fieldId]) return prev;
const next = { ...prev };
delete next[fieldId];
return next;
});
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 <div className="p-8 flex justify-center"><Spinner label="Loading form..." /></div>;
}
if (error || !schema) {
return <div className="p-4 text-ruby-600">Failed to load form: {error}</div>;
}
// 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<HTMLFormElement>) => {
e.preventDefault();
const formElement = e.currentTarget;
if (!formElement.checkValidity()) {
const invalidElements = formElement.querySelectorAll(':invalid');
const newErrors: Record<string, string> = {};
invalidElements.forEach((el) => {
const inputEl = el as HTMLInputElement | HTMLSelectElement;
const wrapper = inputEl.closest('[id^="field-"]');
if (wrapper) {
const fieldId = wrapper.id.replace('field-', '');
let msg = 'This field is required.';
if (inputEl.validity.patternMismatch && inputEl.title) {
msg = inputEl.title;
} else if (inputEl.validity.typeMismatch || inputEl.validity.badInput) {
msg = inputEl.validationMessage || 'Please enter a valid value.';
} else if (inputEl.validity.valueMissing) {
msg = 'This field is required.';
} else if (inputEl.validationMessage) {
msg = inputEl.validationMessage;
}
newErrors[fieldId] = msg;
}
});
setBackendErrors(prev => ({ ...prev, ...newErrors }));
const firstInvalid = invalidElements[0] as HTMLElement;
if (firstInvalid) {
firstInvalid.scrollIntoView({ behavior: 'smooth', block: 'center' });
firstInvalid.focus();
}
setSubmitError(null); // Clear global submit error since we show field-specific ones
return;
}
setSubmitting(true);
setSubmitError(null);
setBackendErrors({});
try {
const payload: Record<string, unknown> = {};
const finalValues = { ...values };
if (actionField && clickedActionRef.current) {
finalValues[actionField.id] = clickedActionRef.current;
}
for (const f of fields) {
if (f.mandatory && f.data_type === 'grid') {
const val = finalValues[f.id];
if (!Array.isArray(val) || val.length === 0) {
setSubmitError(null);
setBackendErrors(prev => ({ ...prev, [f.id]: `Please add at least one item to ${f.name}` }));
// Scroll to the field
setTimeout(() => {
const fieldEl = document.getElementById(`field-${f.id}`);
if (fieldEl) {
fieldEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}, 100);
setSubmitting(false);
return;
}
}
}
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 select options are unselected/empty, explicitly send an empty string to the payload
if ((f.data_type === 'select' || f.data_type === 'multiselect' || f.data_type === 'wf_lookup' || f.data_type === 'radio') && val == null) {
val = '';
}
if (val == null) continue;
if (f.data_type === 'number' && typeof val === 'string') {
payload[f.id] = val === '' ? null : Number(val);
} else if (f.data_type.startsWith('geolocation') && val && typeof val === 'object') {
payload[f.id] = { lat: (val as any).lat, lng: (val as any).lng };
} else 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') {
const fileList = Array.isArray(val) ? val : [val];
const uploadedFiles = [];
for (const fileItem of fileList) {
if (fileItem instanceof File || fileItem instanceof Blob) {
let fileToUpload = fileItem as File;
if (!(fileToUpload instanceof File)) {
fileToUpload = new File([fileItem], (fileItem as any).name || "image.jpg", { type: fileItem.type || 'image/jpeg' });
}
const fileMeta = await client.uploadFile(fileToUpload, { activityId: currentActivityId, fieldId: f.id, instanceId: currentInstanceId });
uploadedFiles.push(fileMeta);
} else {
// Already uploaded (e.g., { uuid: '...' }) or just a string URL
uploadedFiles.push(fileItem);
}
}
payload[f.id] = uploadedFiles;
} else {
if (f.data_type === 'grid' && Array.isArray(val)) {
val = val.map(row => {
const newRow = { ...row };
f.columns?.forEach(col => {
if ((col.data_type === 'select' || col.data_type === 'multiselect' || col.data_type === 'wf_lookup' || col.data_type === 'radio') && newRow[col.id] == null) {
newRow[col.id] = '';
}
});
return newRow;
});
}
payload[f.id] = val;
}
}
let res;
if (currentInstanceId != null) {
res = await client.performActivity(currentInstanceId, currentActivityId, payload);
} else {
const isStoreInit = currentActivityId === STORE.activities.INIT.uid;
const versionOpts = isStoreInit ? { version_uuid: STORE.versionUuid, version_id: 30034 } : {};
res = await client.startInstance(currentActivityId, payload, versionOpts);
}
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));
// Clear draft since current activity is successfully submitted
localforage.removeItem(draftKey);
// 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: SELECT_OPTION_LIMIT
});
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',
PIPELINE.endpoints.potentialMining,
{
instance_id: String(res.instance_id ?? currentInstanceId),
store_code: storeCodeToSend
}
);
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');
if (err.field_errors && Array.isArray(err.field_errors)) {
const newErrors: Record<string, string> = {};
err.field_errors.forEach((fe: any) => {
if (fe.field_id && fe.message) {
let msg = fe.message;
if (msg.includes('PIN Code must be at least 100000')) {
msg = 'Please enter exactly 6 digits';
}
newErrors[fe.field_id] = msg;
}
});
setBackendErrors(newErrors);
if (err.field_errors.length > 0 && err.field_errors[0].field_id) {
const firstId = err.field_errors[0].field_id;
setTimeout(() => {
const el = document.getElementById(`field-${firstId}`);
if (el) {
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
const inputEl = el.querySelector('input, select, textarea, button') as HTMLElement;
if (inputEl) inputEl.focus();
}
}, 100);
}
}
} finally {
setSubmitting(false);
}
};
return (
<form onSubmit={handleSubmit} className="flex flex-col gap-4" noValidate>
{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 (
<WfLookupField
label={f.name}
required={f.mandatory}
value={(val as string | number) ?? ''}
client={client}
config={f.properties?.wf_lookup_config}
activityId={currentActivityId}
fieldId={f.id}
formData={values}
onChange={(newVal, rawRow) => handleFieldChange(f.id, newVal, rawRow)}
/>
);
}
if (type.startsWith('select') || type.startsWith('multiselect')) {
return (
<SelectField
label={f.name}
required={f.mandatory}
value={(val as string) ?? ''}
options={f.properties?.options || []}
onChange={(newVal) => handleFieldChange(f.id, newVal)}
/>
);
}
if (type === 'radio') {
return (
<RadioField
label={f.name}
required={f.mandatory}
value={(val as string) ?? ''}
options={f.properties?.options || []}
onChange={(newVal) => handleFieldChange(f.id, newVal)}
/>
);
}
if (type.startsWith('image') || type.startsWith('file')) {
return (
<FileInput
label={f.name}
type={type}
required={f.mandatory}
value={val}
onChange={(newVal) => handleFieldChange(f.id, newVal)}
/>
);
}
if (type.startsWith('grid')) {
return (
<SmartGridField
key={f.id}
label={f.name}
columns={f.columns || []}
value={(val as Record<string, unknown>[]) || []}
onChange={(newVal) => handleFieldChange(f.id, newVal)}
isActivityPotentialMining={currentActivityId === ORDER_BOOKING.activities.POTENTIAL_MINING.uid}
required={f.mandatory}
error={!!backendErrors[f.id]}
/>
);
}
if (type.startsWith('geolocation')) {
return (
<GeolocationInput
label={f.name}
required={f.mandatory}
value={(val as { lat: number; lng: number }) || null}
onChange={(newVal) => handleFieldChange(f.id, newVal)}
/>
);
}
if (type.startsWith('phone')) {
const phoneStr = typeof val === 'object' && val !== null ? (val as any).phone || '' : (val as string) ?? '';
return (
<PhoneInput
label={f.name}
required={f.mandatory}
value={phoneStr}
onChange={(newVal) => handleFieldChange(f.id, newVal)}
/>
);
}
if (type.startsWith('email')) {
return (
<EmailField
label={f.name}
required={f.mandatory}
value={(val as string) ?? ''}
onChange={(newVal) => handleFieldChange(f.id, newVal)}
/>
);
}
if (type.startsWith('textarea') || type.startsWith('text_area') || type.startsWith('longtext')) {
return (
<TextAreaField
label={f.name}
required={f.mandatory}
value={(val as string) ?? ''}
onChange={(newVal) => handleFieldChange(f.id, newVal)}
/>
);
}
if (type.startsWith('date')) {
return (
<DateField
label={f.name}
required={f.mandatory}
value={(val as string) ?? ''}
onChange={(newVal) => handleFieldChange(f.id, newVal)}
/>
);
}
if (type.startsWith('time')) {
return (
<TimeField
label={f.name}
required={f.mandatory}
value={(val as string) ?? ''}
onChange={(newVal) => handleFieldChange(f.id, newVal)}
/>
);
}
if (type === 'app_user') {
const user = client.currentUser();
const displayVal = user?.name || val;
return (
<TextField
label={f.name}
required={f.mandatory}
type="text"
value={(displayVal as string) ?? ''}
onChange={() => { }}
disabled
/>
);
}
return (
<TextField
label={f.name}
required={f.mandatory}
type={type}
value={(val as string) ?? ''}
onChange={(newVal) => handleFieldChange(f.id, newVal)}
/>
);
};
const content = renderField();
return isDisabled ? (
<fieldset key={f.id} id={`field-${f.id}`} disabled className="opacity-60 pointer-events-none">
{content}
</fieldset>
) : (
<div key={f.id} id={`field-${f.id}`} className="flex flex-col gap-1">
{content}
{backendErrors[f.id] && (
<div className="text-xs font-medium text-ruby-600 mt-0.5">{backendErrors[f.id]}</div>
)}
</div>
);
})}
{submitError && <div className="text-sm text-ruby-600 mt-2">{submitError}</div>}
<div className="flex items-center justify-between mt-4 pt-4 border-t border-border-subtle">
<div className="flex items-center gap-2">
<Button type="button" variant="ghost" className="text-ruby-600 px-2" onClick={handleReset} disabled={submitting}>
Reset Draft
</Button>
{isLocalhost && (
<Button type="button" variant="secondary" className="px-2 text-indigo-600 border-indigo-200 bg-indigo-50 hover:bg-indigo-100" onClick={handleSeedData} disabled={submitting}>
Seed Data
</Button>
)}
</div>
<div className="flex items-center gap-3">
{onCancel && (
<Button type="button" variant="secondary" onClick={() => {
if (hasSubmittedRef.current) onSuccess?.();
else onCancel?.();
}} disabled={submitting}>
Cancel
</Button>
)}
{actionField && actionField.properties?.options ? (
actionField.properties.options.map((opt: any) => (
<Button
key={opt.value}
type="submit"
variant="primary"
disabled={submitting}
onClick={() => { clickedActionRef.current = opt.value; }}
>
{submitting && clickedActionRef.current === opt.value ? 'Submitting...' : opt.label}
</Button>
))
) : (
<Button type="submit" variant="primary" disabled={submitting}>
{submitting ? 'Submitting...' : 'Submit'}
</Button>
)}
</div>
</div>
</form>
);
}