diff --git a/src/api/client.ts b/src/api/client.ts index b0e0454..291f3b2 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -150,7 +150,7 @@ export class ZinoClient { ...(alias ? { preset_alias: alias } : {}), search_query: { page: params.page ?? 1, - limit: params.limit ?? 50, + limit: params.limit ?? 10, sort_by: params.sortBy ?? '', sort_dir: params.sortDir ?? 'desc', search: params.search ?? '', diff --git a/src/components/forms/DynamicForm.tsx b/src/components/forms/DynamicForm.tsx index f0a2a35..c2313d6 100644 --- a/src/components/forms/DynamicForm.tsx +++ b/src/components/forms/DynamicForm.tsx @@ -178,55 +178,7 @@ export function DynamicForm({ client, activityId: initialActivityId, instanceId: } } - if (currentActivityId === ORDER_BOOKING.activities.LOG_VISIT.uid || currentActivityId === '42b7f47d-96f0-4289-a6d9-38f455ad57dd') { - const dateVal = (defaultValues['date_of_visit'] as string) || (defaultValues['date_of_visit_1'] as string) || new Date().toISOString().split('T')[0]; - const timeVal = (defaultValues['time_of_visit'] as string) || (defaultValues['time_of_visit_1'] as string) || new Date().toTimeString().split(' ')[0].substring(0, 5); - if (!defaultValues['date_of_visit']) defaultValues['date_of_visit'] = dateVal; - if (!defaultValues['time_of_visit']) defaultValues['time_of_visit'] = timeVal; - - const dailyLogField = res.fields.find( - f => f.id === 'daily_log' || f.uid === 'field_1785225403902' || f.name.toLowerCase() === 'daily log' - ); - const dailyLogFieldId = dailyLogField?.uid || dailyLogField?.id || 'field_1785225403902'; - - try { - const dailyLogLookupRes = await client.wfLookupRecords({ - activityId: currentActivityId, - fieldId: dailyLogFieldId, - formData: { - date_of_visit: dateVal, - time_of_visit: timeVal, - ...defaultValues - }, - limit: 200 - }); - 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 = firstLog.route_code || firstLog.route_code_1 || firstLog.route_code_2 || firstLog.route_code_3 || firstLog.route || ''; - - if (dailyLogInstanceId) { - defaultValues[dailyLogFieldId] = dailyLogInstanceId; - defaultValues['daily_log'] = dailyLogInstanceId; - defaultValues['field_1785225403902'] = dailyLogInstanceId; - } - - if (routeCode) { - console.log("[Log Visit] Initial daily_log lookup route_code fetched:", routeCode); - defaultValues['route_code'] = routeCode; - defaultValues['field_1785311859486'] = routeCode; - const routeField = res.fields.find(f => f.id === 'route_code' || f.uid === 'field_1785311859486'); - if (routeField) { - defaultValues[routeField.id] = routeCode; - } - } - } - } catch (e) { - console.warn('Failed to load initial daily_log lookup for Log Visit', e); - } - } setValues(defaultValues); setLoading(false); @@ -343,30 +295,7 @@ export function DynamicForm({ client, activityId: initialActivityId, instanceId: } } - if ((currentActivityId === ORDER_BOOKING.activities.LOG_VISIT.uid || currentActivityId === '42b7f47d-96f0-4289-a6d9-38f455ad57dd') && (fieldId.includes('date_of_visit') || fieldId.includes('time_of_visit'))) { - const updatedFormData = { ...next }; - client.wfLookupRecords({ - activityId: currentActivityId, - fieldId: 'field_1', - formData: updatedFormData, - limit: 200 - }).then(lookupRes => { - const arr = Array.isArray(lookupRes) ? lookupRes : (lookupRes?.data || lookupRes?.records || []); - if (arr.length > 0) { - const firstRow = arr[0]; - const routeCode = firstRow.route_code || firstRow.route_code_1 || firstRow.route_code_2 || firstRow.route_code_3 || firstRow.route; - if (routeCode) { - setValues(p => ({ - ...p, - route_code: routeCode, - route_code_1: routeCode, - route_code_2: routeCode, - route_code_3: routeCode, - })); - } - } - }).catch(e => console.warn('Dynamic route_code lookup failed:', e)); - } + const isOrderDetails = fieldId === 'order_details' || getBaseIdForField(fieldId) === 'order_details' || fieldDef?.mapped_workflow_field === 'order_details'; @@ -589,19 +518,6 @@ export function DynamicForm({ client, activityId: initialActivityId, instanceId: return null; } - const isLogVisit = currentActivityId === ORDER_BOOKING.activities.LOG_VISIT.uid || currentActivityId === '42b7f47d-96f0-4289-a6d9-38f455ad57dd'; - const isHiddenLogVisitField = isLogVisit && ( - f.id === 'daily_log' || - f.uid === 'field_1785225403902' || - f.id === 'route_code' || - f.uid === 'field_1785311859486' || - f.name.toLowerCase() === 'daily log' || - f.name.toLowerCase() === 'route code' - ); - - if (isHiddenLogVisitField) { - return null; - } const renderField = () => { if (type === 'wf_lookup') { @@ -762,6 +678,8 @@ export function DynamicForm({ client, activityId: initialActivityId, instanceId: }; const content = renderField(); + + return isDisabled ? (
{content} diff --git a/src/components/forms/LogVisitForm.tsx b/src/components/forms/LogVisitForm.tsx new file mode 100644 index 0000000..1c55d2f --- /dev/null +++ b/src/components/forms/LogVisitForm.tsx @@ -0,0 +1,520 @@ +import { useState, useEffect, useCallback, useRef } from 'react'; +import type { ZinoClient } from '../../api/client'; +import type { FormScreenResponse } from '../../api/types'; +import { ORDER_BOOKING } 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, + * 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 [fetchingDailyLog, setFetchingDailyLog] = useState(false); + const [fetchingStores, setFetchingStores] = useState(false); + const fetchingStoresRef = useRef(false); + + const [chainedActivity, setChainedActivity] = useState<{ + activityId: string; + instanceId?: number | string; + prefillData?: Record; + } | null>(null); + + const [submitting, setSubmitting] = useState(false); + const [submitError, setSubmitError] = useState(null); + + // 1. Fetch form schema from API (/app/434/view/form-screens) + useEffect(() => { + let mounted = true; + setLoadingSchema(true); + client.formSchema(activityId) + .then(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) => { + if (fetchingStoresRef.current) return; + fetchingStoresRef.current = true; + setFetchingStores(true); + + try { + const formDataToSend = cleanFormData(formDataOverride || valuesRef.current); + console.log('[LogVisitForm] Executing select_store lookup with prefilled formData:', formDataToSend); + + const lookupRes = await client.wfLookupRecords({ + activityId, + fieldId: 'select_store', + formData: formDataToSend, + limit: 200, + }); + + 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) { + console.error('Failed to load select_store options:', e); + } finally { + setFetchingStores(false); + fetchingStoresRef.current = false; + } + }, [client, activityId]); + + // 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, + }); + + console.log('[LogVisitForm] Fetching initial daily_log lookup with payload:', payload); + + const dailyLogLookupRes = await client.wfLookupRecords({ + activityId, + fieldId: 'daily_log', + formData: payload, + limit: 200, + }); + + const arr = Array.isArray(dailyLogLookupRes) + ? dailyLogLookupRes + : dailyLogLookupRes?.data || dailyLogLookupRes?.records || []; + + if (arr.length > 0) { + const firstLog = arr[0]; + console.log('[LogVisitForm] Successfully fetched daily log record:', firstLog); + + 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); + console.log('[LogVisitForm] Daily log prefill complete:', 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]; + 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; + + console.log('[LogVisitForm] Submitting clean startInstance payload:', payload); + + 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]; + console.log('[LogVisitForm] Activity chain detected, transitioning to DynamicForm:', nextAct); + 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 ( +
+ onChange(e.target.value)} diff --git a/src/components/forms/fields/TimeField.tsx b/src/components/forms/fields/TimeField.tsx index 37dcf6a..eca933f 100644 --- a/src/components/forms/fields/TimeField.tsx +++ b/src/components/forms/fields/TimeField.tsx @@ -3,11 +3,13 @@ import { Input } from '../../reusable/Input'; export function TimeField({ label, required, + disabled, value, onChange, }: { label: string; required?: boolean; + disabled?: boolean; value: string; onChange: (val: string) => void; }) { @@ -15,6 +17,7 @@ export function TimeField({ onChange(e.target.value)} diff --git a/src/components/reusable/Select.tsx b/src/components/reusable/Select.tsx index 3fa0b27..06d43e8 100644 --- a/src/components/reusable/Select.tsx +++ b/src/components/reusable/Select.tsx @@ -19,6 +19,8 @@ export interface SelectProps extends SelectHTMLAttributes { className?: string; /** Disable search header filter if set to false */ searchable?: boolean; + /** Callback fired when dropdown opens */ + onDropdownOpen?: () => void; } /** Custom searchable select component using React Portal to prevent container clipping. */ @@ -32,6 +34,7 @@ export function Select({ disabled, placeholder, searchable = true, + onDropdownOpen, ...rest }: SelectProps) { const [isOpen, setIsOpen] = useState(false); @@ -63,7 +66,9 @@ export function Select({ setDropdownStyle({ position: 'fixed', left: `${rect.left}px`, - width: `${rect.width}px`, + minWidth: `${rect.width}px`, + width: 'max-content', + maxWidth: 'min(92vw, 450px)', zIndex: 999999, ...(openUpwards ? { bottom: `${window.innerHeight - rect.top + 4}px` } @@ -122,7 +127,15 @@ export function Select({ {/* Trigger Box */}
!disabled && setIsOpen(!isOpen)} + onClick={() => { + if (!disabled) { + const nextState = !isOpen; + setIsOpen(nextState); + if (nextState && onDropdownOpen) { + onDropdownOpen(); + } + } + }} className={cn( "relative bg-card rounded-md h-[42px] border px-3 flex items-center justify-between cursor-pointer transition-all duration-150 select-none", disabled && "opacity-60 cursor-not-allowed bg-slate-50", @@ -178,7 +191,7 @@ export function Select({ isSelected ? "bg-navy-50/20 text-navy-700 font-semibold" : "hover:bg-black/5 text-foreground" )} > - {opt.label} + {opt.label} {isSelected && }
); @@ -285,7 +298,9 @@ export function MultiSelect({ setDropdownStyle({ position: 'fixed', left: `${rect.left}px`, - width: `${rect.width}px`, + minWidth: `${rect.width}px`, + width: 'max-content', + maxWidth: 'min(92vw, 450px)', zIndex: 999999, ...(openUpwards ? { bottom: `${window.innerHeight - rect.top + 4}px` } @@ -420,7 +435,7 @@ export function MultiSelect({ onChange={() => {}} className="rounded border-slate-300 text-navy-600 focus:ring-navy-500 pointer-events-none h-3.5 w-3.5" /> - {opt.label} + {opt.label}
); }) diff --git a/src/components/rv/RecordView.tsx b/src/components/rv/RecordView.tsx index 96336d4..96ad2ab 100644 --- a/src/components/rv/RecordView.tsx +++ b/src/components/rv/RecordView.tsx @@ -24,7 +24,7 @@ export interface RecordViewProps { columns?: string[]; /** Columns to hide. */ omitColumns?: string[]; - /** Rows per page. @default 20 */ + /** Rows per page. @default 10 */ pageSize?: number; /** Click handler — receives the raw row + index. */ onRowClick?: (row: Record, index: number) => void; @@ -62,7 +62,7 @@ export function RecordView({ title = 'Records', columns, omitColumns, - pageSize = 20, + pageSize = 10, onRowClick, rowKey, headerActions, diff --git a/src/screens/CallsPage.tsx b/src/screens/CallsPage.tsx index d0c4a85..fc58421 100644 --- a/src/screens/CallsPage.tsx +++ b/src/screens/CallsPage.tsx @@ -5,6 +5,7 @@ import { CallDetail } from '../components/dv'; import { useState } from 'react'; import { Phone, Plus } from 'lucide-react'; import { Button } from '../components/buttons/Button'; +import { LogVisitForm } from '../components/forms/LogVisitForm'; import { DynamicForm } from '../components/forms/DynamicForm'; import { ORDER_BOOKING } from '../api/config'; import { orderBookingClient } from '../api/clients'; @@ -179,9 +180,8 @@ export function CallsPage() { title={createTitle} width="md" > - { setIsCreating(false); setRefreshKey(k => k + 1); diff --git a/src/screens/OrdersPage.tsx b/src/screens/OrdersPage.tsx index 0e66b3a..6fbbd3f 100644 --- a/src/screens/OrdersPage.tsx +++ b/src/screens/OrdersPage.tsx @@ -6,6 +6,7 @@ import { useState } from 'react'; import { Plus, ShoppingBag } from 'lucide-react'; import { Button } from '../components/buttons/Button'; +import { LogVisitForm } from '../components/forms/LogVisitForm'; import { DynamicForm } from '../components/forms/DynamicForm'; import { ORDER_BOOKING } from '../api/config'; import { orderBookingClient } from '../api/clients'; @@ -180,15 +181,13 @@ export function OrdersPage() { title={createTitle} width="md" > - { setIsCreating(false); setRefreshKey(k => k + 1); }} onCancel={() => setIsCreating(false)} - onActivityChange={(name) => setCreateTitle(name)} />