import React, { useState, useEffect, useCallback, useRef } from 'react'; import type { ZinoClient } from '../../api/client'; import type { FormScreenResponse } from '../../api/types'; import { ORDER_BOOKING, LOOKUP_RECORD_LIMIT } from '../../api/config'; import { Button } from '../buttons/Button'; import { Select } from '../reusable/Select'; import { DateField, TimeField, FileInput, TextField } from './fields'; import { Spinner } from '../reusable/Spinner'; import { DynamicForm } from './DynamicForm'; export interface LogVisitFormProps { client: ZinoClient; onSuccess?: () => void; onCancel?: () => void; onActivityChange?: (name: string) => void; } // Utility to clean empty values from form data before sending API requests const cleanFormData = (data: Record) => { const result: Record = {}; Object.entries(data).forEach(([k, v]) => { if (v !== '' && v !== null && v !== undefined && !(Array.isArray(v) && v.length === 0)) { result[k] = v; } }); return result; }; /** * Dedicated form component for the Log Visit activity. * Calls client.formSchema() to retrieve form screen fields dynamically, * pre-fills daily_log & route_code from initial daily log lookup, * lazy-fetches select_store options after daily log completion, * handles mobile pipeline prefill, * AND wires activity chaining seamlessly into DynamicForm for subsequent activities. */ export function LogVisitForm({ client, onSuccess, onCancel, onActivityChange }: LogVisitFormProps) { const activityId = ORDER_BOOKING.activities.LOG_VISIT.uid; const todayStr = new Date().toISOString().split('T')[0]; const nowTimeStr = new Date().toTimeString().split(' ')[0].substring(0, 5); const [schema, setSchema] = useState(null); const schemaRef = useRef(null); useEffect(() => { schemaRef.current = schema; }, [schema]); const [loadingSchema, setLoadingSchema] = useState(true); const [values, setValues] = useState>({ date_of_visit: todayStr, time_of_visit: nowTimeStr, select_store: '', upload_image: [], daily_log: '', route_code: '', }); const valuesRef = useRef(values); useEffect(() => { valuesRef.current = values; }, [values]); const [storeOptions, setStoreOptions] = useState<{ value: string; label: string; _raw?: any }[]>([]); const [storeSearchQuery, setStoreSearchQuery] = useState(''); const [fetchingDailyLog, setFetchingDailyLog] = useState(false); const [fetchingStores, setFetchingStores] = useState(false); const [chainedActivity, setChainedActivity] = useState<{ activityId: string; instanceId?: number | string; prefillData?: Record; } | null>(null); const [submitting, setSubmitting] = useState(false); const [submitError, setSubmitError] = useState(null); const [storeError, setStoreError] = useState(null); // 1. Fetch form schema from API (/app/434/view/form-screens) & Mobile Pipeline prefill useEffect(() => { let mounted = true; setLoadingSchema(true); client.formSchema(activityId) .then(async (res) => { if (!mounted) return; setSchema(res); schemaRef.current = res; const initialValues: Record = { date_of_visit: todayStr, time_of_visit: nowTimeStr, select_store: '', upload_image: [], daily_log: '', route_code: '', }; if (res.field_defaults) { Object.entries(res.field_defaults).forEach(([fieldId, def]) => { if (def.value != null) { initialValues[fieldId] = def.value; } else if (def.prefill) { if (def.prefill.value === 'current_date') { initialValues[fieldId] = new Date().toISOString().split('T')[0]; } else if (def.prefill.value === 'current_time') { initialValues[fieldId] = new Date().toTimeString().split(' ')[0].substring(0, 5); } else if (def.prefill.value === 'current_user_id') { const user = client.currentUser(); initialValues[fieldId] = user ? Number(user.id) : ''; } else { initialValues[fieldId] = def.prefill.value; } } }); } if (res.prefill_data && Object.keys(res.prefill_data).length > 0) { Object.assign(initialValues, res.prefill_data); } else if (res.data && Object.keys(res.data).length > 0) { Object.assign(initialValues, res.data); } setValues(prev => { const next = { ...initialValues, ...prev }; valuesRef.current = next; return next; }); }) .catch(err => { console.error('Failed to load Log Visit form schema:', err); }) .finally(() => { if (mounted) setLoadingSchema(false); }); return () => { mounted = false; }; }, [client, activityId, todayStr, nowTimeStr]); // 2. Helper to fetch select_store options ONLY using updated prefilled formData const fetchStoreOptions = useCallback(async (formDataOverride?: Record, search: string = '') => { setFetchingStores(true); setStoreError(null); try { const formDataToSend = cleanFormData(formDataOverride || valuesRef.current); const lookupRes = await client.wfLookupRecords({ activityId, fieldId: 'select_store', formData: formDataToSend, search, limit: LOOKUP_RECORD_LIMIT, }); const arr = Array.isArray(lookupRes) ? lookupRes : lookupRes?.data || lookupRes?.records || []; // Find display_fields configured in schema for select_store const selectStoreField = schemaRef.current?.fields.find( f => f.id === 'select_store' || f.uid === 'field_1783057892381' || f.name.toLowerCase().includes('select store') ); const displayFields = (selectStoreField?.properties?.wf_lookup_config as any)?.display_fields || []; const opts = arr.map((row: any) => { let labelText = ''; if (displayFields.length > 0) { const labelParts = displayFields .map((df: any) => row[df.field_id]) .filter((v: any) => v != null && v !== ''); if (labelParts.length > 0) { labelText = labelParts.join(' - '); } } if (!labelText) { const storeName = row.business_name_2 || row.store_name || row.name || row.store; const storeCode = row.store_code || row.code; labelText = storeName ? `${storeCode ? `${storeCode} - ` : ''}${storeName}` : `Store #${row.instance_id || row.id}`; } return { value: String(row.instance_id || row.id), label: labelText, _raw: row, }; }); setStoreOptions(opts); } catch (e: any) { console.error('Failed to load select_store options:', e); const msg = e?.message || e?.data?.message || String(e); if (msg.toLowerCase().includes('route_code')) { setStoreError('Please punch-in in Daily Logs'); } else { setStoreError('Failed to load stores'); } } finally { setFetchingStores(false); } }, [client, activityId]); useEffect(() => { const timer = setTimeout(() => { fetchStoreOptions(undefined, storeSearchQuery); }, 300); return () => clearTimeout(timer); }, [storeSearchQuery, fetchStoreOptions]); // 3. Fetch Daily Log lookup data initially & prefill form state const loadDailyLogData = useCallback(async (dateVal: string, timeVal: string) => { setFetchingDailyLog(true); try { const payload = cleanFormData({ date_of_visit: dateVal, time_of_visit: timeVal, }); const dailyLogLookupRes = await client.wfLookupRecords({ activityId, fieldId: 'daily_log', formData: payload, limit: LOOKUP_RECORD_LIMIT, }); const arr = Array.isArray(dailyLogLookupRes) ? dailyLogLookupRes : dailyLogLookupRes?.data || dailyLogLookupRes?.records || []; if (arr.length > 0) { const firstLog = arr[0]; const dailyLogInstanceId = String(firstLog.instance_id || firstLog.id || ''); const routeCode = String( firstLog.route_code || firstLog.route_code_1 || firstLog.route_code_2 || firstLog.route_code_3 || firstLog.route || '' ); const updated = { ...valuesRef.current }; // Copy raw fields from daily log into state Object.keys(firstLog).forEach(key => { if (firstLog[key] != null && firstLog[key] !== '') { updated[key] = firstLog[key]; } }); if (dailyLogInstanceId) { updated['daily_log'] = dailyLogInstanceId; updated['field_1785225403902'] = dailyLogInstanceId; updated['instance_id'] = firstLog.instance_id || firstLog.id; } if (routeCode) { updated['route_code'] = routeCode; updated['route_code_1'] = routeCode; updated['route_code_2'] = routeCode; updated['route_code_3'] = routeCode; updated['field_1785311859486'] = routeCode; } valuesRef.current = updated; setValues(updated); } else { console.warn('[LogVisitForm] No daily log records found for date/time:', dateVal, timeVal); } } catch (e) { console.warn('Failed to load daily_log lookup for Log Visit', e); } finally { setFetchingDailyLog(false); } }, [client, activityId]); useEffect(() => { loadDailyLogData(values.date_of_visit, values.time_of_visit); }, [loadDailyLogData, values.date_of_visit, values.time_of_visit]); const handleStoreDropdownOpen = () => { fetchStoreOptions(); }; const handleStoreSelect = (fieldId: string, val: string) => { const selectedOpt = storeOptions.find(o => String(o.value) === String(val)); const rawRow = selectedOpt?._raw || {}; setValues(prev => { const next: Record = { ...prev, [fieldId]: val, select_store: val, field_1: val }; // Map raw row fields into values Object.keys(rawRow).forEach(key => { next[key] = rawRow[key]; }); valuesRef.current = next; return next; }); }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setSubmitting(true); setSubmitError(null); try { const validFields = schema?.fields || []; const validFieldIds = new Set(validFields.map(f => f.id)); // Always include standard Log Visit field IDs ['select_store', 'date_of_visit', 'time_of_visit', 'upload_image', 'daily_log', 'route_code'].forEach(id => validFieldIds.add(id)); const payload: Record = {}; validFieldIds.forEach(fieldId => { const val = valuesRef.current[fieldId]; const isRemark = fieldId.toLowerCase().includes('remark') || fieldId.toLowerCase().includes('note'); if (isRemark && (val === undefined || val === null || val === '')) { payload[fieldId] = ''; } else if (val !== undefined && val !== null && val !== '') { payload[fieldId] = val; } }); // Handle image upload if present let uploadedFiles: any[] = []; const imgVal = valuesRef.current.upload_image; const fileList = Array.isArray(imgVal) ? imgVal : (imgVal instanceof File ? [imgVal] : []); for (const fileItem of fileList) { if (fileItem instanceof File) { const fileMeta = await client.uploadFile(fileItem, { activityId, fieldId: 'upload_image', }); uploadedFiles.push(fileMeta); } else { uploadedFiles.push(fileItem); } } payload['upload_image'] = uploadedFiles; const res: any = await client.startInstance(activityId, payload); const chainSource = res?.activity_chain || schema?.activity_chain || []; if (chainSource && chainSource.length > 0) { const nextAct = chainSource[0]; onActivityChange?.(nextAct.activity_name); setChainedActivity({ activityId: nextAct.activity_uid, instanceId: res?.instance_id, prefillData: { ...valuesRef.current }, }); } else { onSuccess?.(); } } catch (err: any) { setSubmitError(err?.message || 'Failed to log visit.'); } finally { setSubmitting(false); } }; // If activity chain triggered (e.g. Productivity of Visit), render DynamicForm seamlessly if (chainedActivity) { return ( ); } if (loadingSchema) { return (
); } const fields = schema?.fields || []; return (
{fields.map(f => { const fieldId = f.id; const lowerId = fieldId.toLowerCase(); const lowerName = f.name.toLowerCase(); const isHidden = schema?.field_defaults?.[fieldId]?.hidden === true || (f.properties as any)?.hidden === true; // Skip daily_log and route_code (or hidden fields) from visual rendering if ( isHidden || lowerId === 'daily_log' || lowerId === 'field_1785225403902' || lowerId === 'route_code' || lowerId === 'field_1785311859486' || lowerName === 'daily log' || lowerName === 'route code' ) { return null; } const type = f.data_type; const val = values[fieldId]; const isDisabled = schema?.field_defaults?.[fieldId]?.disabled === true || f.properties?.disabled === true; if (type === 'wf_lookup' || lowerId === 'select_store' || lowerName.includes('select store')) { return (