krishna_sales/src/components/forms/DynamicForm.tsx

1084 lines
43 KiB
TypeScript

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, DAILY_REPORTS, PIPELINE, HIDDEN_FORM_FIELDS } from '../../api/config';
import { isCategoryColumn, isProductColumn } from './fields/gridUtils';
export interface DynamicFormProps {
client: ZinoClient;
activityId: string;
instanceId?: number | string;
onSuccess?: () => void;
onCancel?: () => void;
ignorePrefill?: boolean;
customPrefillData?: Record<string, unknown>;
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<number | string | undefined>(initialInstanceId);
const [chainQueue, setChainQueue] = useState<Array<{ activity_uid: string; activity_name: string }>>([]);
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];
}
}
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 headers: Record<string, string> = {
'templateid': '192',
'x-pipeline-version': 'latest',
'orgid': '57',
'groupid': '25'
};
const summaryRes = await client.request<any>(
'POST',
PIPELINE.endpoints.productiveCallSummary,
{ 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);
}
}
}
if (currentActivityId === ORDER_BOOKING.activities.LOG_VISIT.uid) {
const user = client.currentUser();
const userMobile = user?.mobile || localStorage.getItem('krishna_sales_user_mobile') || user?.email;
if (userMobile) {
try {
const headers: Record<string, string> = {
'templateid': '192',
'x-pipeline-version': 'latest',
'orgid': '57',
'groupid': '25'
};
const summaryRes = await client.request<any>(
'POST',
PIPELINE.endpoints.productiveCallSummary,
{ mobile: userMobile },
headers
);
let summaryData = summaryRes.data || summaryRes;
if (Array.isArray(summaryData)) {
summaryData = summaryData[0] || {};
}
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 for Log Visit', e);
}
}
}
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<Record<string, unknown>>({});
const [submitting, setSubmitting] = useState(false);
const [hasSubmitted, setHasSubmitted] = useState(false);
const [submitError, setSubmitError] = useState<string | null>(null);
const [showErrors, setShowErrors] = useState(false);
const clickedActionRef = useRef<string | null>(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-fill mapped fields based on the raw row data only for route_name
if (fieldId === 'route_name_2' || fieldId.startsWith('route_name')) {
schema?.fields.forEach(f => {
if (f.id !== fieldId) {
const mappedName = (f.mapped_workflow_field || '').toLowerCase();
const nameFallback = (f.name || '').toLowerCase().replace(/\s+/g, '_');
const fieldName = mappedName || nameFallback;
let matchKey = Object.keys(fullRow).find(k => k.toLowerCase() === mappedName || k.toLowerCase() === nameFallback);
// Special handling for route_name -> distributor details
if (!matchKey) {
matchKey = Object.keys(fullRow).find(k =>
fieldName === `distributor_${k.toLowerCase()}` ||
(k.toLowerCase() === 'phone' && fieldName === 'distributor_phone_number') ||
(k.toLowerCase() === 'email' && fieldName === 'distributor_email')
);
}
if (matchKey && fullRow[matchKey] !== undefined && fullRow[matchKey] !== null) {
if (matchKey.toLowerCase() === 'owner_name' && fieldName === 'owner_name') {
return;
}
if (matchKey.toLowerCase() === 'email' && fieldName === 'email') {
return;
}
if (matchKey.toLowerCase() === 'phone' && (fieldName === 'phone' || fieldName === 'phone_number')) {
return;
}
let fillVal = fullRow[matchKey];
// If the target field is a select, try to match option value case-insensitively
if (f.data_type === 'select' && f.properties?.options) {
const matchedOpt = f.properties.options.find((o: any) => String(o.value).toLowerCase() === String(fillVal).toLowerCase());
if (matchedOpt) {
fillVal = matchedOpt.value;
}
}
next[f.id] = fillVal;
}
}
});
}
}
// 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 <div className="p-8 flex justify-center"><Spinner label={initialActivityName || 'Loading...'} /></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);
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<string, unknown>[]) || [];
if (rawGrid.length > 0) {
const merged: Record<string, unknown>[] = [];
const indexMap = new Map<string, number>();
const getGridVal = (obj: Record<string, unknown>, searchTokens: string[], excludeTokens: string[] = []) => {
for (const k of Object.keys(obj)) {
const lowerK = k.toLowerCase();
if (searchTokens.some(t => lowerK.includes(t)) && !excludeTokens.some(t => lowerK.includes(t))) {
return obj[k];
}
}
return undefined;
};
for (const r of rawGrid) {
const cat = String(getGridVal(r, ['category', 'cat']) || '').trim().toLowerCase();
const prod = String(getGridVal(r, ['product', 'name'], ['category', 'cat']) || '').trim().toLowerCase();
const currentBags = Number(getGridVal(r, ['bags', 'quantity'])) || 0;
const currentSku = Number(getGridVal(r, ['sku'], ['code'])) || 0;
if (cat && prod) {
const key = `${cat}::${prod}`;
if (indexMap.has(key) && currentBags > 0) {
const targetIdx = indexMap.get(key)!;
const existingRow = merged[targetIdx];
const existingBags = Number(getGridVal(existingRow, ['bags', 'quantity'])) || 0;
const addedBags = currentBags;
const newBags = existingBags + addedBags;
const skuVal = Number(getGridVal(existingRow, ['sku'], ['code']) || currentSku || 0);
merged[targetIdx] = {
...existingRow,
bags: newBags,
quantity: newBags,
row_kgs: skuVal * newBags,
};
continue;
} else if (!indexMap.has(key)) {
indexMap.set(key, merged.length);
}
}
merged.push(r);
}
// Clean virtual keys
const cleanGrid = merged.map((r) => {
const clean: Record<string, unknown> = {};
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(getGridVal(r, ['bags', 'quantity'])) || 0;
const sku = Number(getGridVal(r, ['sku'], ['code'])) || 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<string, unknown>, 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')) {
if (currentActivityId === 'af8ac8df-d868-4f7d-88b2-123955d69c56') {
continue;
}
const gridRows = (finalValues[f.id] as Record<string, unknown>[]) || [];
const hasProdCol = f.columns?.some(c =>
c.id.toLowerCase().includes('product_name') ||
c.id.toLowerCase() === 'product' ||
c.id.toLowerCase() === 'name' ||
(c.name || '').toLowerCase().includes('product name') ||
(c.name || '').toLowerCase() === 'product' ||
((c.name || '').toLowerCase().includes('product') && !(c.name || '').toLowerCase().includes('category'))
);
// Check if grid has at least one valid row
const validRows = gridRows.filter((r) => {
const { cat, prod, bags } = extractGridRowDetails(r, f.columns);
if (hasProdCol) {
return cat && prod && bags > 0;
}
return cat && bags > 0;
});
if (f.mandatory && validRows.length === 0) {
setSubmitError(`Please add at least one complete row for ${f.name}.`);
return;
}
// Check if any row is partially filled
const hasIncompleteRow = gridRows.some((r) => {
const { cat, prod, bags } = extractGridRowDetails(r, f.columns);
if (!hasProdCol) {
if (cat && bags <= 0) return true;
if (bags > 0 && !cat) return true;
return false;
}
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) {
if (!hasProdCol) {
setSubmitError(`Please complete all required details (Category and Quantity) for ${f.name} before submitting.`);
} else {
setSubmitError('Please complete all required product details (Category, 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<string, unknown> = {};
for (const f of fields) {
const val = finalValues[f.id];
const isRemark = (f.name || '').toLowerCase().includes('remark') || (f.name || '').toLowerCase().includes('note');
if (isRemark && (val == null || val === '')) {
payload[f.id] = '';
continue;
}
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)) {
const uploadedFiles = [];
for (const file of val) {
if (file instanceof File) {
const fileMeta = await client.uploadFile(file, { activityId: currentActivityId, fieldId: f.id, instanceId: currentInstanceId });
uploadedFiles.push(fileMeta);
} else {
uploadedFiles.push(file);
}
}
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 if ((f.data_type === 'image' || f.data_type === 'file') && val && typeof val === 'object') {
payload[f.id] = [val];
} else if (f.data_type.startsWith('grid') || f.data_type === 'smart_grid') {
const gridRows = Array.isArray(val) ? val : [];
const getFormattedGridVal = (colKey: string, rawVal: unknown, colDef?: FormScreenField) => {
if (rawVal === undefined || rawVal === null || String(rawVal).trim() === '') return undefined;
const keyLower = colKey.toLowerCase();
const colNameLower = (colDef?.name || '').toLowerCase();
const dataType = colDef?.data_type;
// 1. Explicit number data_type from form screen API
if (dataType === 'number') {
const num = Number(rawVal);
return !isNaN(num) ? num : rawVal;
}
// 2. Explicit string/select/id_gen data_type from API or SKU/code identifier columns
const isStringCol = dataType === 'select' ||
dataType === 'text' ||
dataType === 'string' ||
dataType === 'id_gen' ||
keyLower.includes('sku') ||
keyLower.includes('code') ||
keyLower.includes('br_code') ||
keyLower === 'br' ||
colNameLower.includes('sku') ||
colNameLower.includes('code');
if (isStringCol) {
return String(rawVal);
}
// 3. Bags / Quantity / Row Kgs quantity columns without explicit text colDef
const isNumericQuantity = keyLower === 'bags' ||
keyLower === 'quantity' ||
keyLower === 'row_kgs' ||
colNameLower === 'bags' ||
colNameLower === 'quantity';
if (isNumericQuantity) {
const num = Number(rawVal);
return !isNaN(num) ? num : rawVal;
}
return rawVal;
};
const cleanRows = gridRows
.filter(r => {
return Object.keys(r).some(k => {
const v = r[k];
return v !== undefined && v !== null && String(v).trim() !== '';
});
})
.map(r => {
const formattedRow: Record<string, unknown> = {};
// 1. Process all existing keys in row r
for (const [k, v] of Object.entries(r)) {
if (v === undefined || v === null || String(v).trim() === '') continue;
const colDef = f.columns?.find(c =>
c.id === k ||
(c.name || '').toLowerCase() === k.toLowerCase() ||
c.mapped_workflow_field === k
);
const formattedVal = getFormattedGridVal(k, v, colDef);
if (formattedVal !== undefined) {
formattedRow[k] = formattedVal;
if (colDef && colDef.id && colDef.id !== k) {
formattedRow[colDef.id] = formattedVal;
}
}
}
// 2. Also map any schema defined columns from f.columns
if (f.columns && f.columns.length > 0) {
for (const col of f.columns) {
if (formattedRow[col.id] === undefined) {
const matchKey = Object.keys(r).find(rk =>
rk.toLowerCase() === col.id.toLowerCase() ||
rk.toLowerCase() === (col.name || '').toLowerCase() ||
(col.mapped_workflow_field && rk.toLowerCase() === col.mapped_workflow_field.toLowerCase()) ||
(isCategoryColumn(col.id, col.name) && (rk.toLowerCase().includes('category') || rk === 'cat')) ||
(isProductColumn(col.id, col.name) && (rk.toLowerCase().includes('product') || rk === 'productName')) ||
(col.name?.toLowerCase().includes('bags') && (rk.toLowerCase().includes('bags') || rk.toLowerCase().includes('quantity')))
);
if (matchKey && r[matchKey] !== undefined && r[matchKey] !== null && String(r[matchKey]).trim() !== '') {
const formattedVal = getFormattedGridVal(matchKey, r[matchKey], col);
if (formattedVal !== undefined) {
formattedRow[col.id] = formattedVal;
}
}
}
}
}
return formattedRow;
});
payload[f.id] = cleanRows;
} else if (f.data_type === 'number') {
if (val !== undefined && val !== null && val !== '') {
const num = Number(val);
payload[f.id] = !isNaN(num) ? num : val;
} else {
payload[f.id] = val;
}
} else {
payload[f.id] = val;
}
}
let res;
if (currentInstanceId != null) {
res = await client.performActivity(currentInstanceId, currentActivityId, payload);
} else {
res = await client.startInstance(currentActivityId, payload);
}
setHasSubmitted(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 || (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;
}
if (HIDDEN_FORM_FIELDS.includes(f.id) || HIDDEN_FORM_FIELDS.includes(f.name)) {
return false;
}
return true;
});
return (
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
{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 (
<WfLookupField
label={f.name}
required={f.mandatory}
value={(val as string | number) ?? ''}
client={client}
config={f.properties?.wf_lookup_config || f.properties?.lookup_config}
properties={f.properties}
activityId={currentActivityId}
fieldId={f.id}
formData={values}
onChange={(newVal, fullRow) => handleFieldChange(f.id, newVal, fullRow)}
/>
);
}
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, fullRow) => handleFieldChange(f.id, newVal, fullRow)}
/>
);
}
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')) {
const isPotentialMiningGrid = currentActivityId === 'af8ac8df-d868-4f7d-88b2-123955d69c56';
return (
<SmartGridField
label={f.name}
columns={f.columns || []}
value={(val as Record<string, unknown>[]) || []}
onChange={(newVal) => handleFieldChange(f.id, newVal)}
totalBagsValue={tbField ? Number(values[tbField.id]) || 0 : undefined}
totalKgsValue={tkField ? Number(values[tkField.id]) || 0 : undefined}
showErrors={showErrors}
disableAddRow={isPotentialMiningGrid}
hideTotal={isPotentialMiningGrid}
readOnlyCols={isPotentialMiningGrid ? ['product_category_', 'actual_potential', 'total_ordered', 'difference'] : []}
isPotentialMining={isPotentialMiningGrid}
/>
);
}
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)}
/>
);
}
const isValEmpty = val === undefined || val === null || String(val).trim() === '';
const currentFieldError = showErrors && f.mandatory && isValEmpty ? 'Required' : undefined;
let displayVal = (val as string) ?? '';
if (schema.field_defaults?.[f.id]?.prefill?.value === 'current_user_id') {
const user = client.currentUser();
if (user && String(val) === String(user.id)) {
displayVal = user.name;
}
}
return (
<TextField
label={f.name}
required={f.mandatory}
type={type}
value={displayVal}
onChange={(newVal) => handleFieldChange(f.id, newVal)}
error={currentFieldError}
/>
);
};
const content = renderField();
return isDisabled ? (
<fieldset key={f.id} disabled className="opacity-60 pointer-events-none">
{content}
</fieldset>
) : (
<div key={f.id}>{content}</div>
);
})}
{submitError && <div className="text-sm text-ruby-600 mt-2">{submitError}</div>}
<div className="flex items-center justify-end gap-3 mt-4 pt-4 border-t border-border-subtle">
{onCancel && (
<Button type="button" variant="secondary" onClick={() => {
if (hasSubmitted) {
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>
</form>
);
}