diff --git a/src/api/client.ts b/src/api/client.ts index 828b80c..5261e18 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -133,6 +133,7 @@ export class ZinoClient { org_id: String(p.org_id ?? ''), name: p.name ?? '', email: p.email ?? '', + mobile: String(p.mobile ?? p.mobile_number ?? p.phone ?? p.phone_number ?? p.user_mobile ?? ''), roles: p.roles ?? [], groups: p.groups ?? [], }; diff --git a/src/api/types.ts b/src/api/types.ts index 80d867a..6328468 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -10,6 +10,7 @@ export interface User { org_id: string; name: string; email: string; + mobile?: string; roles: string[]; groups: string[]; } diff --git a/src/components/forms/DynamicForm.tsx b/src/components/forms/DynamicForm.tsx index ba844b0..186e9c8 100644 --- a/src/components/forms/DynamicForm.tsx +++ b/src/components/forms/DynamicForm.tsx @@ -178,6 +178,43 @@ export function DynamicForm({ client, activityId: initialActivityId, instanceId: } } } + + 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 = { + 'templateid': '192', + 'x-pipeline-version': 'draft', + 'orgid': '57', + 'groupid': '25' + }; + + const summaryRes = await client.request( + '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); diff --git a/src/components/forms/LogVisitForm.tsx b/src/components/forms/LogVisitForm.tsx new file mode 100644 index 0000000..b62b9a3 --- /dev/null +++ b/src/components/forms/LogVisitForm.tsx @@ -0,0 +1,555 @@ +import React, { useState, useEffect, useCallback, useRef } from 'react'; +import type { ZinoClient } from '../../api/client'; +import type { FormScreenResponse } from '../../api/types'; +import { ORDER_BOOKING, PIPELINE } 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 [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) & 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); + } + + // Execute mobile pipeline prefill + const user = client.currentUser(); + const userMobile = user?.mobile || localStorage.getItem('krishna_sales_user_mobile') || user?.email; + if (userMobile) { + try { + const headers: Record = { + 'templateid': '192', + 'x-pipeline-version': 'draft', + 'orgid': '57', + 'groupid': '25' + }; + + const summaryRes = await client.request( + '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; + } + Object.assign(initialValues, summaryData); + } catch (e) { + console.warn('Failed to load productive call summary for Log Visit', e); + } + } + + 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/forms/index.ts b/src/components/forms/index.ts new file mode 100644 index 0000000..0f4d23d --- /dev/null +++ b/src/components/forms/index.ts @@ -0,0 +1,2 @@ +export * from './DynamicForm'; +export * from './LogVisitForm'; diff --git a/src/components/reusable/Select.tsx b/src/components/reusable/Select.tsx index c1461e4..1df7961 100644 --- a/src/components/reusable/Select.tsx +++ b/src/components/reusable/Select.tsx @@ -20,6 +20,7 @@ export interface SelectProps extends SelectHTMLAttributes { className?: string; /** Disable search header filter if set to false */ searchable?: boolean; + onDropdownOpen?: () => void; } /** Custom searchable select component using React Portal to prevent container clipping. */ @@ -33,6 +34,7 @@ export function Select({ disabled, placeholder, searchable = true, + onDropdownOpen, ...rest }: SelectProps) { const [isOpen, setIsOpen] = useState(false); @@ -123,7 +125,13 @@ export function Select({ {/* Trigger Box */}
!disabled && setIsOpen(!isOpen)} + onClick={() => { + if (!disabled) { + const next = !isOpen; + setIsOpen(next); + if (next) 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", diff --git a/src/screens/CallsPage.tsx b/src/screens/CallsPage.tsx index 3af2529..e1bfa8b 100644 --- a/src/screens/CallsPage.tsx +++ b/src/screens/CallsPage.tsx @@ -5,7 +5,7 @@ import { CallDetail } from '../components/dv'; import { useEffect, useState } from 'react'; import { Button } from '../components/buttons/Button'; import { Plus } from 'lucide-react'; -import { DynamicForm } from '../components/forms/DynamicForm'; +import { DynamicForm, LogVisitForm } from '../components/forms'; import { ORDER_BOOKING } from '../api/config'; import { orderBookingClient } from '../api/clients'; @@ -185,10 +185,8 @@ export function CallsPage() { title={createTitle} width="md" > - { setIsCreating(false); setRefreshKey(k => k + 1); diff --git a/src/screens/MyCallsPage.tsx b/src/screens/MyCallsPage.tsx index 6660e81..3c76e4b 100644 --- a/src/screens/MyCallsPage.tsx +++ b/src/screens/MyCallsPage.tsx @@ -5,7 +5,7 @@ import { CallDetail } from '../components/dv'; import { useEffect, useState } from 'react'; import { Button } from '../components/buttons/Button'; import { Plus } from 'lucide-react'; -import { DynamicForm } from '../components/forms/DynamicForm'; +import { DynamicForm, LogVisitForm } from '../components/forms'; import { ORDER_BOOKING } from '../api/config'; import { orderBookingClient } from '../api/clients'; import { useAuth } from '../auth/context'; @@ -190,10 +190,8 @@ export function MyCallsPage() { title={createTitle} width="md" > - { setIsCreating(false); setRefreshKey(k => k + 1); diff --git a/src/screens/MyOrdersPage.tsx b/src/screens/MyOrdersPage.tsx index 9c32c5d..d3a519c 100644 --- a/src/screens/MyOrdersPage.tsx +++ b/src/screens/MyOrdersPage.tsx @@ -5,8 +5,7 @@ import { OrderDetail } from '../components/dv'; import { useState } from 'react'; import { Button } from '../components/buttons/Button'; import { Plus } from 'lucide-react'; -import { DynamicForm } from '../components/forms/DynamicForm'; -import { ORDER_BOOKING } from '../api/config'; +import { LogVisitForm } from '../components/forms'; import { orderBookingClient } from '../api/clients'; import { useAuth } from '../auth/context'; @@ -46,10 +45,8 @@ export function MyOrdersPage() { title={createTitle} width="md" > - { setIsCreating(false); setRefreshKey(k => k + 1); diff --git a/src/screens/OrdersPage.tsx b/src/screens/OrdersPage.tsx index 4a3115c..d870cee 100644 --- a/src/screens/OrdersPage.tsx +++ b/src/screens/OrdersPage.tsx @@ -5,8 +5,7 @@ import { OrderDetail } from '../components/dv'; import { useState } from 'react'; import { Button } from '../components/buttons/Button'; import { Plus } from 'lucide-react'; -import { DynamicForm } from '../components/forms/DynamicForm'; -import { ORDER_BOOKING } from '../api/config'; +import { LogVisitForm } from '../components/forms'; import { orderBookingClient } from '../api/clients'; export function OrdersPage() { @@ -40,10 +39,8 @@ export function OrdersPage() { title={createTitle} width="md" > - { setIsCreating(false); setRefreshKey(k => k + 1);