routewise store selection is done
This commit is contained in:
parent
d2498ee68d
commit
5097d183fa
@ -150,7 +150,7 @@ export class ZinoClient {
|
|||||||
...(alias ? { preset_alias: alias } : {}),
|
...(alias ? { preset_alias: alias } : {}),
|
||||||
search_query: {
|
search_query: {
|
||||||
page: params.page ?? 1,
|
page: params.page ?? 1,
|
||||||
limit: params.limit ?? 50,
|
limit: params.limit ?? 10,
|
||||||
sort_by: params.sortBy ?? '',
|
sort_by: params.sortBy ?? '',
|
||||||
sort_dir: params.sortDir ?? 'desc',
|
sort_dir: params.sortDir ?? 'desc',
|
||||||
search: params.search ?? '',
|
search: params.search ?? '',
|
||||||
|
|||||||
@ -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);
|
setValues(defaultValues);
|
||||||
setLoading(false);
|
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';
|
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;
|
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 = () => {
|
const renderField = () => {
|
||||||
if (type === 'wf_lookup') {
|
if (type === 'wf_lookup') {
|
||||||
@ -762,6 +678,8 @@ export function DynamicForm({ client, activityId: initialActivityId, instanceId:
|
|||||||
};
|
};
|
||||||
|
|
||||||
const content = renderField();
|
const content = renderField();
|
||||||
|
|
||||||
|
|
||||||
return isDisabled ? (
|
return isDisabled ? (
|
||||||
<fieldset key={f.id} disabled className="opacity-60 pointer-events-none">
|
<fieldset key={f.id} disabled className="opacity-60 pointer-events-none">
|
||||||
{content}
|
{content}
|
||||||
|
|||||||
520
src/components/forms/LogVisitForm.tsx
Normal file
520
src/components/forms/LogVisitForm.tsx
Normal file
@ -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<string, any>) => {
|
||||||
|
const result: Record<string, any> = {};
|
||||||
|
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<FormScreenResponse | null>(null);
|
||||||
|
const schemaRef = useRef<FormScreenResponse | null>(null);
|
||||||
|
useEffect(() => { schemaRef.current = schema; }, [schema]);
|
||||||
|
|
||||||
|
const [loadingSchema, setLoadingSchema] = useState(true);
|
||||||
|
|
||||||
|
const [values, setValues] = useState<Record<string, any>>({
|
||||||
|
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<string, unknown>;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
const [submitError, setSubmitError] = useState<string | null>(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<string, any> = {
|
||||||
|
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<string, any>) => {
|
||||||
|
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<string, any> = { ...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<string, any> = {};
|
||||||
|
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 (
|
||||||
|
<DynamicForm
|
||||||
|
client={client}
|
||||||
|
activityId={chainedActivity.activityId}
|
||||||
|
instanceId={chainedActivity.instanceId}
|
||||||
|
customPrefillData={chainedActivity.prefillData}
|
||||||
|
onSuccess={onSuccess}
|
||||||
|
onCancel={onCancel}
|
||||||
|
onActivityChange={onActivityChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loadingSchema) {
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center items-center py-12">
|
||||||
|
<Spinner size={24} label="Loading Log Visit form..." />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fields = schema?.fields || [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
|
||||||
|
{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 (
|
||||||
|
<div key={fieldId} className="relative">
|
||||||
|
<Select
|
||||||
|
label={f.name}
|
||||||
|
required={f.mandatory}
|
||||||
|
value={(val as string) || ''}
|
||||||
|
options={[{ value: '', label: `Select ${f.name}...` }, ...storeOptions]}
|
||||||
|
onDropdownOpen={handleStoreDropdownOpen}
|
||||||
|
onChange={e => handleStoreSelect(fieldId, e.target.value)}
|
||||||
|
disabled={isDisabled}
|
||||||
|
/>
|
||||||
|
{fetchingStores && (
|
||||||
|
<div className="absolute right-3 top-9">
|
||||||
|
<Spinner size={16} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type.startsWith('date')) {
|
||||||
|
return (
|
||||||
|
<DateField
|
||||||
|
key={fieldId}
|
||||||
|
label={f.name}
|
||||||
|
required={f.mandatory}
|
||||||
|
disabled={isDisabled}
|
||||||
|
value={(val as string) || ''}
|
||||||
|
onChange={v => {
|
||||||
|
setValues(p => {
|
||||||
|
const next = { ...p, [fieldId]: v, date_of_visit: v };
|
||||||
|
valuesRef.current = next;
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type.startsWith('time')) {
|
||||||
|
return (
|
||||||
|
<TimeField
|
||||||
|
key={fieldId}
|
||||||
|
label={f.name}
|
||||||
|
required={f.mandatory}
|
||||||
|
disabled={isDisabled}
|
||||||
|
value={(val as string) || ''}
|
||||||
|
onChange={v => {
|
||||||
|
setValues(p => {
|
||||||
|
const next = { ...p, [fieldId]: v, time_of_visit: v };
|
||||||
|
valuesRef.current = next;
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'image' || type === 'file') {
|
||||||
|
return (
|
||||||
|
<FileInput
|
||||||
|
key={fieldId}
|
||||||
|
label={f.name}
|
||||||
|
type={type}
|
||||||
|
required={f.mandatory}
|
||||||
|
value={val || []}
|
||||||
|
onChange={v => {
|
||||||
|
setValues(p => {
|
||||||
|
const next = { ...p, [fieldId]: v, upload_image: v };
|
||||||
|
valuesRef.current = next;
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TextField
|
||||||
|
key={fieldId}
|
||||||
|
label={f.name}
|
||||||
|
required={f.mandatory}
|
||||||
|
type={type}
|
||||||
|
value={(val as string) || ''}
|
||||||
|
onChange={v => {
|
||||||
|
setValues(p => {
|
||||||
|
const next = { ...p, [fieldId]: v };
|
||||||
|
valuesRef.current = next;
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{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>
|
||||||
|
)}
|
||||||
|
<Button type="submit" disabled={Boolean(submitting || fetchingDailyLog)}>
|
||||||
|
{submitting ? 'Submitting...' : 'Log Visit'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -3,11 +3,13 @@ import { Input } from '../../reusable/Input';
|
|||||||
export function DateField({
|
export function DateField({
|
||||||
label,
|
label,
|
||||||
required,
|
required,
|
||||||
|
disabled,
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
}: {
|
}: {
|
||||||
label: string;
|
label: string;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
value: string;
|
value: string;
|
||||||
onChange: (val: string) => void;
|
onChange: (val: string) => void;
|
||||||
}) {
|
}) {
|
||||||
@ -15,6 +17,7 @@ export function DateField({
|
|||||||
<Input
|
<Input
|
||||||
label={label}
|
label={label}
|
||||||
required={required}
|
required={required}
|
||||||
|
disabled={disabled}
|
||||||
type="date"
|
type="date"
|
||||||
value={value ?? ''}
|
value={value ?? ''}
|
||||||
onChange={(e) => onChange(e.target.value)}
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
|||||||
@ -3,11 +3,13 @@ import { Input } from '../../reusable/Input';
|
|||||||
export function TimeField({
|
export function TimeField({
|
||||||
label,
|
label,
|
||||||
required,
|
required,
|
||||||
|
disabled,
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
}: {
|
}: {
|
||||||
label: string;
|
label: string;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
value: string;
|
value: string;
|
||||||
onChange: (val: string) => void;
|
onChange: (val: string) => void;
|
||||||
}) {
|
}) {
|
||||||
@ -15,6 +17,7 @@ export function TimeField({
|
|||||||
<Input
|
<Input
|
||||||
label={label}
|
label={label}
|
||||||
required={required}
|
required={required}
|
||||||
|
disabled={disabled}
|
||||||
type="time"
|
type="time"
|
||||||
value={value ?? ''}
|
value={value ?? ''}
|
||||||
onChange={(e) => onChange(e.target.value)}
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
|||||||
@ -19,6 +19,8 @@ export interface SelectProps extends SelectHTMLAttributes<HTMLSelectElement> {
|
|||||||
className?: string;
|
className?: string;
|
||||||
/** Disable search header filter if set to false */
|
/** Disable search header filter if set to false */
|
||||||
searchable?: boolean;
|
searchable?: boolean;
|
||||||
|
/** Callback fired when dropdown opens */
|
||||||
|
onDropdownOpen?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Custom searchable select component using React Portal to prevent container clipping. */
|
/** Custom searchable select component using React Portal to prevent container clipping. */
|
||||||
@ -32,6 +34,7 @@ export function Select({
|
|||||||
disabled,
|
disabled,
|
||||||
placeholder,
|
placeholder,
|
||||||
searchable = true,
|
searchable = true,
|
||||||
|
onDropdownOpen,
|
||||||
...rest
|
...rest
|
||||||
}: SelectProps) {
|
}: SelectProps) {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
@ -63,7 +66,9 @@ export function Select({
|
|||||||
setDropdownStyle({
|
setDropdownStyle({
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
left: `${rect.left}px`,
|
left: `${rect.left}px`,
|
||||||
width: `${rect.width}px`,
|
minWidth: `${rect.width}px`,
|
||||||
|
width: 'max-content',
|
||||||
|
maxWidth: 'min(92vw, 450px)',
|
||||||
zIndex: 999999,
|
zIndex: 999999,
|
||||||
...(openUpwards
|
...(openUpwards
|
||||||
? { bottom: `${window.innerHeight - rect.top + 4}px` }
|
? { bottom: `${window.innerHeight - rect.top + 4}px` }
|
||||||
@ -122,7 +127,15 @@ export function Select({
|
|||||||
|
|
||||||
{/* Trigger Box */}
|
{/* Trigger Box */}
|
||||||
<div
|
<div
|
||||||
onClick={() => !disabled && setIsOpen(!isOpen)}
|
onClick={() => {
|
||||||
|
if (!disabled) {
|
||||||
|
const nextState = !isOpen;
|
||||||
|
setIsOpen(nextState);
|
||||||
|
if (nextState && onDropdownOpen) {
|
||||||
|
onDropdownOpen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
className={cn(
|
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",
|
"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",
|
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"
|
isSelected ? "bg-navy-50/20 text-navy-700 font-semibold" : "hover:bg-black/5 text-foreground"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span className="truncate">{opt.label}</span>
|
<span className="whitespace-normal break-words leading-tight flex-1">{opt.label}</span>
|
||||||
{isSelected && <Check size={14} className="text-navy-600 shrink-0 ml-2" />}
|
{isSelected && <Check size={14} className="text-navy-600 shrink-0 ml-2" />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -285,7 +298,9 @@ export function MultiSelect({
|
|||||||
setDropdownStyle({
|
setDropdownStyle({
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
left: `${rect.left}px`,
|
left: `${rect.left}px`,
|
||||||
width: `${rect.width}px`,
|
minWidth: `${rect.width}px`,
|
||||||
|
width: 'max-content',
|
||||||
|
maxWidth: 'min(92vw, 450px)',
|
||||||
zIndex: 999999,
|
zIndex: 999999,
|
||||||
...(openUpwards
|
...(openUpwards
|
||||||
? { bottom: `${window.innerHeight - rect.top + 4}px` }
|
? { bottom: `${window.innerHeight - rect.top + 4}px` }
|
||||||
@ -420,7 +435,7 @@ export function MultiSelect({
|
|||||||
onChange={() => {}}
|
onChange={() => {}}
|
||||||
className="rounded border-slate-300 text-navy-600 focus:ring-navy-500 pointer-events-none h-3.5 w-3.5"
|
className="rounded border-slate-300 text-navy-600 focus:ring-navy-500 pointer-events-none h-3.5 w-3.5"
|
||||||
/>
|
/>
|
||||||
<span className="truncate flex-1">{opt.label}</span>
|
<span className="whitespace-normal break-words leading-tight flex-1">{opt.label}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|||||||
@ -24,7 +24,7 @@ export interface RecordViewProps {
|
|||||||
columns?: string[];
|
columns?: string[];
|
||||||
/** Columns to hide. */
|
/** Columns to hide. */
|
||||||
omitColumns?: string[];
|
omitColumns?: string[];
|
||||||
/** Rows per page. @default 20 */
|
/** Rows per page. @default 10 */
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
/** Click handler — receives the raw row + index. */
|
/** Click handler — receives the raw row + index. */
|
||||||
onRowClick?: (row: Record<string, unknown>, index: number) => void;
|
onRowClick?: (row: Record<string, unknown>, index: number) => void;
|
||||||
@ -62,7 +62,7 @@ export function RecordView({
|
|||||||
title = 'Records',
|
title = 'Records',
|
||||||
columns,
|
columns,
|
||||||
omitColumns,
|
omitColumns,
|
||||||
pageSize = 20,
|
pageSize = 10,
|
||||||
onRowClick,
|
onRowClick,
|
||||||
rowKey,
|
rowKey,
|
||||||
headerActions,
|
headerActions,
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { CallDetail } from '../components/dv';
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Phone, Plus } from 'lucide-react';
|
import { Phone, Plus } from 'lucide-react';
|
||||||
import { Button } from '../components/buttons/Button';
|
import { Button } from '../components/buttons/Button';
|
||||||
|
import { LogVisitForm } from '../components/forms/LogVisitForm';
|
||||||
import { DynamicForm } from '../components/forms/DynamicForm';
|
import { DynamicForm } from '../components/forms/DynamicForm';
|
||||||
import { ORDER_BOOKING } from '../api/config';
|
import { ORDER_BOOKING } from '../api/config';
|
||||||
import { orderBookingClient } from '../api/clients';
|
import { orderBookingClient } from '../api/clients';
|
||||||
@ -179,9 +180,8 @@ export function CallsPage() {
|
|||||||
title={createTitle}
|
title={createTitle}
|
||||||
width="md"
|
width="md"
|
||||||
>
|
>
|
||||||
<DynamicForm
|
<LogVisitForm
|
||||||
client={orderBookingClient}
|
client={orderBookingClient}
|
||||||
activityId={ORDER_BOOKING.activities.LOG_VISIT.uid}
|
|
||||||
onSuccess={() => {
|
onSuccess={() => {
|
||||||
setIsCreating(false);
|
setIsCreating(false);
|
||||||
setRefreshKey(k => k + 1);
|
setRefreshKey(k => k + 1);
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { useState } from 'react';
|
|||||||
|
|
||||||
import { Plus, ShoppingBag } from 'lucide-react';
|
import { Plus, ShoppingBag } from 'lucide-react';
|
||||||
import { Button } from '../components/buttons/Button';
|
import { Button } from '../components/buttons/Button';
|
||||||
|
import { LogVisitForm } from '../components/forms/LogVisitForm';
|
||||||
import { DynamicForm } from '../components/forms/DynamicForm';
|
import { DynamicForm } from '../components/forms/DynamicForm';
|
||||||
import { ORDER_BOOKING } from '../api/config';
|
import { ORDER_BOOKING } from '../api/config';
|
||||||
import { orderBookingClient } from '../api/clients';
|
import { orderBookingClient } from '../api/clients';
|
||||||
@ -180,15 +181,13 @@ export function OrdersPage() {
|
|||||||
title={createTitle}
|
title={createTitle}
|
||||||
width="md"
|
width="md"
|
||||||
>
|
>
|
||||||
<DynamicForm
|
<LogVisitForm
|
||||||
client={orderBookingClient}
|
client={orderBookingClient}
|
||||||
activityId={ORDER_BOOKING.activities.LOG_VISIT.uid}
|
|
||||||
onSuccess={() => {
|
onSuccess={() => {
|
||||||
setIsCreating(false);
|
setIsCreating(false);
|
||||||
setRefreshKey(k => k + 1);
|
setRefreshKey(k => k + 1);
|
||||||
}}
|
}}
|
||||||
onCancel={() => setIsCreating(false)}
|
onCancel={() => setIsCreating(false)}
|
||||||
onActivityChange={(name) => setCreateTitle(name)}
|
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user