746 lines
28 KiB
TypeScript
746 lines
28 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 } from '../../api/config';
|
|
|
|
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(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 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<Record<string, unknown>>({});
|
|
const [submitting, setSubmitting] = 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-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>();
|
|
|
|
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<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(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<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')) {
|
|
const gridRows = (finalValues[f.id] as Record<string, unknown>[]) || [];
|
|
|
|
// 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<string, unknown> = {};
|
|
|
|
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 (
|
|
<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) => 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
|
|
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}
|
|
/>
|
|
);
|
|
}
|
|
|
|
if (type.startsWith('geolocation')) {
|
|
return (
|
|
<GeolocationInput
|
|
label={f.name}
|
|
required={f.mandatory}
|
|
value={(val as { latitude: number; longitude: 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;
|
|
|
|
return (
|
|
<TextField
|
|
label={f.name}
|
|
required={f.mandatory}
|
|
type={type}
|
|
value={(val as string) ?? ''}
|
|
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={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>
|
|
);
|
|
}
|