feat: form fields is stored in localstorage
This commit is contained in:
parent
10ca195e0c
commit
061bf9bbc5
25
package-lock.json
generated
25
package-lock.json
generated
@ -11,6 +11,7 @@
|
|||||||
"@react-google-maps/api": "^2.20.8",
|
"@react-google-maps/api": "^2.20.8",
|
||||||
"jspdf": "^4.2.1",
|
"jspdf": "^4.2.1",
|
||||||
"jspdf-autotable": "^5.0.8",
|
"jspdf-autotable": "^5.0.8",
|
||||||
|
"localforage": "^1.10.0",
|
||||||
"lucide-react": "^0.400.0",
|
"lucide-react": "^0.400.0",
|
||||||
"react": "^19.2.7",
|
"react": "^19.2.7",
|
||||||
"react-dom": "^19.2.7",
|
"react-dom": "^19.2.7",
|
||||||
@ -5751,6 +5752,12 @@
|
|||||||
"node": ">= 4"
|
"node": ">= 4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/immediate": {
|
||||||
|
"version": "3.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
||||||
|
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/immer": {
|
"node_modules/immer": {
|
||||||
"version": "11.1.11",
|
"version": "11.1.11",
|
||||||
"resolved": "https://registry.npmjs.org/immer/-/immer-11.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/immer/-/immer-11.1.11.tgz",
|
||||||
@ -6455,6 +6462,15 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lie": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"immediate": "~3.0.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/lightningcss": {
|
"node_modules/lightningcss": {
|
||||||
"version": "1.32.0",
|
"version": "1.32.0",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
|
||||||
@ -6716,6 +6732,15 @@
|
|||||||
"url": "https://opencollective.com/parcel"
|
"url": "https://opencollective.com/parcel"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/localforage": {
|
||||||
|
"version": "1.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz",
|
||||||
|
"integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"lie": "3.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/locate-path": {
|
"node_modules/locate-path": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
||||||
|
|||||||
@ -14,6 +14,7 @@
|
|||||||
"@react-google-maps/api": "^2.20.8",
|
"@react-google-maps/api": "^2.20.8",
|
||||||
"jspdf": "^4.2.1",
|
"jspdf": "^4.2.1",
|
||||||
"jspdf-autotable": "^5.0.8",
|
"jspdf-autotable": "^5.0.8",
|
||||||
|
"localforage": "^1.10.0",
|
||||||
"lucide-react": "^0.400.0",
|
"lucide-react": "^0.400.0",
|
||||||
"react": "^19.2.7",
|
"react": "^19.2.7",
|
||||||
"react-dom": "^19.2.7",
|
"react-dom": "^19.2.7",
|
||||||
|
|||||||
@ -271,3 +271,5 @@ export const HIDDEN_FORM_FIELDS = [
|
|||||||
export const SELECT_OPTION_LIMIT = 200;
|
export const SELECT_OPTION_LIMIT = 200;
|
||||||
export const PDF_MAX_SKUS_PER_CHUNK = 50;
|
export const PDF_MAX_SKUS_PER_CHUNK = 50;
|
||||||
|
|
||||||
|
export const APP_VERSION = '1.0.1';
|
||||||
|
export const FORM_DRAFT_EXPIRY_MS = 30 * 60 * 1000;
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { useState, useEffect, useRef } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
|
import localforage from 'localforage';
|
||||||
import type { ZinoClient } from '../../api/client';
|
import type { ZinoClient } from '../../api/client';
|
||||||
import type { FormScreenResponse } from '../../api/types';
|
import type { FormScreenResponse } from '../../api/types';
|
||||||
import { Button } from '../buttons/Button';
|
import { Button } from '../buttons/Button';
|
||||||
@ -17,7 +18,7 @@ import {
|
|||||||
WfLookupField,
|
WfLookupField,
|
||||||
RadioField,
|
RadioField,
|
||||||
} from './fields';
|
} from './fields';
|
||||||
import { ORDER_BOOKING, STORE, DAILY_REPORTS, HIDDEN_FORM_FIELDS, SELECT_OPTION_LIMIT, PIPELINE } from '../../api/config';
|
import { ORDER_BOOKING, STORE, DAILY_REPORTS, HIDDEN_FORM_FIELDS, SELECT_OPTION_LIMIT, PIPELINE, FORM_DRAFT_EXPIRY_MS, APP_VERSION } from '../../api/config';
|
||||||
|
|
||||||
export interface DynamicFormProps {
|
export interface DynamicFormProps {
|
||||||
client: ZinoClient;
|
client: ZinoClient;
|
||||||
@ -35,6 +36,8 @@ export function DynamicForm({ client, activityId: initialActivityId, instanceId:
|
|||||||
const [currentInstanceId, setCurrentInstanceId] = useState<number | string | undefined>(initialInstanceId);
|
const [currentInstanceId, setCurrentInstanceId] = useState<number | string | undefined>(initialInstanceId);
|
||||||
const [chainQueue, setChainQueue] = useState<Array<{ activity_uid: string; activity_name: string }>>([]);
|
const [chainQueue, setChainQueue] = useState<Array<{ activity_uid: string; activity_name: string }>>([]);
|
||||||
|
|
||||||
|
const draftKey = `draft_form_${currentActivityId}_${currentInstanceId || 'new'}`;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCurrentActivityId(initialActivityId);
|
setCurrentActivityId(initialActivityId);
|
||||||
setCurrentInstanceId(initialInstanceId);
|
setCurrentInstanceId(initialInstanceId);
|
||||||
@ -166,7 +169,22 @@ export function DynamicForm({ client, activityId: initialActivityId, instanceId:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defaultValuesRef.current = { ...defaultValues };
|
||||||
|
|
||||||
|
// Load draft if available
|
||||||
|
try {
|
||||||
|
const draft: any = await localforage.getItem(`draft_form_${currentActivityId}_${currentInstanceId || 'new'}`);
|
||||||
|
if (draft && draft.version === APP_VERSION && draft.values) {
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - draft.timestamp < FORM_DRAFT_EXPIRY_MS) {
|
||||||
|
Object.assign(defaultValues, draft.values);
|
||||||
|
} else {
|
||||||
|
await localforage.removeItem(`draft_form_${currentActivityId}_${currentInstanceId || 'new'}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to load draft', e);
|
||||||
|
}
|
||||||
|
|
||||||
setValues(defaultValues);
|
setValues(defaultValues);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@ -181,6 +199,7 @@ export function DynamicForm({ client, activityId: initialActivityId, instanceId:
|
|||||||
return () => { mounted = false; };
|
return () => { mounted = false; };
|
||||||
}, [client, currentActivityId, currentInstanceId]);
|
}, [client, currentActivityId, currentInstanceId]);
|
||||||
|
|
||||||
|
const defaultValuesRef = useRef<Record<string, unknown>>({});
|
||||||
const [values, setValues] = useState<Record<string, unknown>>({});
|
const [values, setValues] = useState<Record<string, unknown>>({});
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [submitError, setSubmitError] = useState<string | null>(null);
|
const [submitError, setSubmitError] = useState<string | null>(null);
|
||||||
@ -188,6 +207,24 @@ export function DynamicForm({ client, activityId: initialActivityId, instanceId:
|
|||||||
const clickedActionRef = useRef<string | null>(null);
|
const clickedActionRef = useRef<string | null>(null);
|
||||||
const hasSubmittedRef = useRef(false);
|
const hasSubmittedRef = useRef(false);
|
||||||
|
|
||||||
|
// Auto-save form draft whenever values change
|
||||||
|
useEffect(() => {
|
||||||
|
if (!loading && Object.keys(values).length > 0) {
|
||||||
|
localforage.setItem(draftKey, {
|
||||||
|
version: APP_VERSION,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
values
|
||||||
|
}).catch(e => console.error('Failed to save draft', e));
|
||||||
|
}
|
||||||
|
}, [values, draftKey, loading]);
|
||||||
|
|
||||||
|
const handleReset = async () => {
|
||||||
|
if (window.confirm('Are you sure you want to clear the draft and reset this form?')) {
|
||||||
|
await localforage.removeItem(draftKey);
|
||||||
|
setValues({ ...defaultValuesRef.current });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleFieldChange = (fieldId: string, newVal: unknown, rawRow?: any) => {
|
const handleFieldChange = (fieldId: string, newVal: unknown, rawRow?: any) => {
|
||||||
setValues(prev => {
|
setValues(prev => {
|
||||||
const next = { ...prev, [fieldId]: newVal };
|
const next = { ...prev, [fieldId]: newVal };
|
||||||
@ -370,15 +407,23 @@ export function DynamicForm({ client, activityId: initialActivityId, instanceId:
|
|||||||
phone: phoneNum,
|
phone: phoneNum,
|
||||||
phone_with_dial_code: `+91${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) {
|
} else if ((f.data_type === 'image' || f.data_type === 'file') && Array.isArray(val) && val.length > 0 && (val[0] instanceof File || val[0] instanceof Blob)) {
|
||||||
const uploadedFiles = [];
|
const uploadedFiles = [];
|
||||||
for (const file of val) {
|
for (const file of val) {
|
||||||
const fileMeta = await client.uploadFile(file, { activityId: currentActivityId, fieldId: f.id, instanceId: currentInstanceId });
|
let fileToUpload = file as File;
|
||||||
|
if (!(fileToUpload instanceof File)) {
|
||||||
|
fileToUpload = new File([file], (file as any).name || "image.jpg", { type: file.type || 'image/jpeg' });
|
||||||
|
}
|
||||||
|
const fileMeta = await client.uploadFile(fileToUpload, { activityId: currentActivityId, fieldId: f.id, instanceId: currentInstanceId });
|
||||||
uploadedFiles.push(fileMeta);
|
uploadedFiles.push(fileMeta);
|
||||||
}
|
}
|
||||||
payload[f.id] = uploadedFiles;
|
payload[f.id] = uploadedFiles;
|
||||||
} else if ((f.data_type === 'image' || f.data_type === 'file') && val instanceof File) {
|
} else if ((f.data_type === 'image' || f.data_type === 'file') && (val instanceof File || val instanceof Blob)) {
|
||||||
const fileMeta = await client.uploadFile(val, { activityId: currentActivityId, fieldId: f.id, instanceId: currentInstanceId });
|
let fileToUpload = val as File;
|
||||||
|
if (!(fileToUpload instanceof File)) {
|
||||||
|
fileToUpload = new File([val], (val as any).name || "image.jpg", { type: val.type || 'image/jpeg' });
|
||||||
|
}
|
||||||
|
const fileMeta = await client.uploadFile(fileToUpload, { activityId: currentActivityId, fieldId: f.id, instanceId: currentInstanceId });
|
||||||
payload[f.id] = [fileMeta];
|
payload[f.id] = [fileMeta];
|
||||||
} else {
|
} else {
|
||||||
if (f.data_type === 'grid' && Array.isArray(val)) {
|
if (f.data_type === 'grid' && Array.isArray(val)) {
|
||||||
@ -427,6 +472,9 @@ export function DynamicForm({ client, activityId: initialActivityId, instanceId:
|
|||||||
// Remove any existing occurrences from pending to avoid duplicates
|
// Remove any existing occurrences from pending to avoid duplicates
|
||||||
pending = pending.filter(p => !newActivities.some((n: any) => n.activity_uid === p.activity_uid));
|
pending = pending.filter(p => !newActivities.some((n: any) => n.activity_uid === p.activity_uid));
|
||||||
|
|
||||||
|
// Clear draft since current activity is successfully submitted
|
||||||
|
localforage.removeItem(draftKey);
|
||||||
|
|
||||||
// Prepend the new activities for depth-first execution (nested chaining)
|
// Prepend the new activities for depth-first execution (nested chaining)
|
||||||
pending = [...newActivities, ...pending];
|
pending = [...newActivities, ...pending];
|
||||||
|
|
||||||
@ -707,32 +755,37 @@ export function DynamicForm({ client, activityId: initialActivityId, instanceId:
|
|||||||
|
|
||||||
{submitError && <div className="text-sm text-ruby-600 mt-2">{submitError}</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">
|
<div className="flex items-center justify-between mt-4 pt-4 border-t border-border-subtle">
|
||||||
{onCancel && (
|
<Button type="button" variant="ghost" className="text-ruby-600 px-2" onClick={handleReset} disabled={submitting}>
|
||||||
<Button type="button" variant="secondary" onClick={() => {
|
Reset Draft
|
||||||
if (hasSubmittedRef.current) onSuccess?.();
|
</Button>
|
||||||
else onCancel?.();
|
<div className="flex items-center gap-3">
|
||||||
}} disabled={submitting}>
|
{onCancel && (
|
||||||
Cancel
|
<Button type="button" variant="secondary" onClick={() => {
|
||||||
</Button>
|
if (hasSubmittedRef.current) onSuccess?.();
|
||||||
)}
|
else onCancel?.();
|
||||||
{actionField && actionField.properties?.options ? (
|
}} disabled={submitting}>
|
||||||
actionField.properties.options.map((opt: any) => (
|
Cancel
|
||||||
<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>
|
||||||
))
|
)}
|
||||||
) : (
|
{actionField && actionField.properties?.options ? (
|
||||||
<Button type="submit" variant="primary" disabled={submitting}>
|
actionField.properties.options.map((opt: any) => (
|
||||||
{submitting ? 'Submitting...' : 'Submit'}
|
<Button
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
|
import localforage from 'localforage';
|
||||||
import type { ZinoClient } from '../../api/client';
|
import type { ZinoClient } from '../../api/client';
|
||||||
import type { FormScreenResponse } from '../../api/types';
|
import type { FormScreenResponse } from '../../api/types';
|
||||||
import { ORDER_BOOKING, HIDDEN_FORM_FIELDS, SELECT_OPTION_LIMIT } from '../../api/config';
|
import { ORDER_BOOKING, HIDDEN_FORM_FIELDS, SELECT_OPTION_LIMIT, FORM_DRAFT_EXPIRY_MS, APP_VERSION } from '../../api/config';
|
||||||
import { Button } from '../buttons/Button';
|
import { Button } from '../buttons/Button';
|
||||||
import { Select } from '../reusable/Select';
|
import { Select } from '../reusable/Select';
|
||||||
import { DateField, TimeField, FileInput, TextField } from './fields';
|
import { DateField, TimeField, FileInput, TextField } from './fields';
|
||||||
@ -39,6 +40,8 @@ export function LogVisitForm({ client, onSuccess, onCancel, onActivityChange }:
|
|||||||
const todayStr = new Date().toISOString().split('T')[0];
|
const todayStr = new Date().toISOString().split('T')[0];
|
||||||
const nowTimeStr = new Date().toTimeString().split(' ')[0].substring(0, 5);
|
const nowTimeStr = new Date().toTimeString().split(' ')[0].substring(0, 5);
|
||||||
|
|
||||||
|
const draftKey = `draft_form_${activityId}_new`;
|
||||||
|
|
||||||
const [schema, setSchema] = useState<FormScreenResponse | null>(null);
|
const [schema, setSchema] = useState<FormScreenResponse | null>(null);
|
||||||
const schemaRef = useRef<FormScreenResponse | null>(null);
|
const schemaRef = useRef<FormScreenResponse | null>(null);
|
||||||
useEffect(() => { schemaRef.current = schema; }, [schema]);
|
useEffect(() => { schemaRef.current = schema; }, [schema]);
|
||||||
@ -79,7 +82,7 @@ export function LogVisitForm({ client, onSuccess, onCancel, onActivityChange }:
|
|||||||
let mounted = true;
|
let mounted = true;
|
||||||
setLoadingSchema(true);
|
setLoadingSchema(true);
|
||||||
client.formSchema(activityId)
|
client.formSchema(activityId)
|
||||||
.then(res => {
|
.then(async res => {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setSchema(res);
|
setSchema(res);
|
||||||
schemaRef.current = res;
|
schemaRef.current = res;
|
||||||
@ -118,6 +121,20 @@ export function LogVisitForm({ client, onSuccess, onCancel, onActivityChange }:
|
|||||||
Object.assign(initialValues, res.data);
|
Object.assign(initialValues, res.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const draft: any = await localforage.getItem(draftKey);
|
||||||
|
if (draft && draft.version === APP_VERSION && draft.values) {
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - draft.timestamp < FORM_DRAFT_EXPIRY_MS) {
|
||||||
|
Object.assign(initialValues, draft.values);
|
||||||
|
} else {
|
||||||
|
await localforage.removeItem(draftKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to load draft', e);
|
||||||
|
}
|
||||||
|
|
||||||
setValues(prev => {
|
setValues(prev => {
|
||||||
const next = { ...initialValues, ...prev };
|
const next = { ...initialValues, ...prev };
|
||||||
valuesRef.current = next;
|
valuesRef.current = next;
|
||||||
@ -132,7 +149,37 @@ export function LogVisitForm({ client, onSuccess, onCancel, onActivityChange }:
|
|||||||
});
|
});
|
||||||
|
|
||||||
return () => { mounted = false; };
|
return () => { mounted = false; };
|
||||||
}, [client, activityId, todayStr, nowTimeStr]);
|
}, [client, activityId, todayStr, nowTimeStr, draftKey]);
|
||||||
|
|
||||||
|
// Auto-save form draft whenever values change
|
||||||
|
useEffect(() => {
|
||||||
|
if (!loadingSchema && Object.keys(values).length > 0) {
|
||||||
|
localforage.setItem(draftKey, {
|
||||||
|
version: APP_VERSION,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
values
|
||||||
|
}).catch(e => console.error('Failed to save draft', e));
|
||||||
|
}
|
||||||
|
}, [values, draftKey, loadingSchema]);
|
||||||
|
|
||||||
|
const handleReset = async () => {
|
||||||
|
if (window.confirm('Are you sure you want to clear the draft and reset this form?')) {
|
||||||
|
await localforage.removeItem(draftKey);
|
||||||
|
|
||||||
|
const resetValues = {
|
||||||
|
date_of_visit: todayStr,
|
||||||
|
time_of_visit: nowTimeStr,
|
||||||
|
select_store: '',
|
||||||
|
upload_image: [],
|
||||||
|
daily_log: '',
|
||||||
|
route_code: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
setValues(resetValues);
|
||||||
|
valuesRef.current = resetValues;
|
||||||
|
setStoreOptions([]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 2. Helper to fetch select_store options ONLY using updated prefilled formData
|
// 2. Helper to fetch select_store options ONLY using updated prefilled formData
|
||||||
const fetchStoreOptions = useCallback(async (formDataOverride?: Record<string, any>, searchStr: string = '') => {
|
const fetchStoreOptions = useCallback(async (formDataOverride?: Record<string, any>, searchStr: string = '') => {
|
||||||
@ -327,11 +374,15 @@ export function LogVisitForm({ client, onSuccess, onCancel, onActivityChange }:
|
|||||||
// Handle image upload if present
|
// Handle image upload if present
|
||||||
let uploadedFiles: any[] = [];
|
let uploadedFiles: any[] = [];
|
||||||
const imgVal = valuesRef.current.upload_image;
|
const imgVal = valuesRef.current.upload_image;
|
||||||
const fileList = Array.isArray(imgVal) ? imgVal : (imgVal instanceof File ? [imgVal] : []);
|
const fileList = Array.isArray(imgVal) ? imgVal : (imgVal instanceof File || imgVal instanceof Blob ? [imgVal] : []);
|
||||||
|
|
||||||
for (const fileItem of fileList) {
|
for (const fileItem of fileList) {
|
||||||
if (fileItem instanceof File) {
|
if (fileItem instanceof File || fileItem instanceof Blob) {
|
||||||
const fileMeta = await client.uploadFile(fileItem, {
|
let fileToUpload = fileItem as File;
|
||||||
|
if (!(fileToUpload instanceof File)) {
|
||||||
|
fileToUpload = new File([fileItem], (fileItem as any).name || "image.jpg", { type: fileItem.type || 'image/jpeg' });
|
||||||
|
}
|
||||||
|
const fileMeta = await client.uploadFile(fileToUpload, {
|
||||||
activityId,
|
activityId,
|
||||||
fieldId: 'upload_image',
|
fieldId: 'upload_image',
|
||||||
});
|
});
|
||||||
@ -344,6 +395,9 @@ export function LogVisitForm({ client, onSuccess, onCancel, onActivityChange }:
|
|||||||
|
|
||||||
const res: any = await client.startInstance(activityId, payload);
|
const res: any = await client.startInstance(activityId, payload);
|
||||||
|
|
||||||
|
// Clear draft since current activity is successfully submitted
|
||||||
|
localforage.removeItem(draftKey);
|
||||||
|
|
||||||
const chainSource = res?.activity_chain || schema?.activity_chain || [];
|
const chainSource = res?.activity_chain || schema?.activity_chain || [];
|
||||||
if (chainSource && chainSource.length > 0) {
|
if (chainSource && chainSource.length > 0) {
|
||||||
const nextAct = chainSource[0];
|
const nextAct = chainSource[0];
|
||||||
@ -519,15 +573,20 @@ export function LogVisitForm({ client, onSuccess, onCancel, onActivityChange }:
|
|||||||
|
|
||||||
{submitError && <div className="text-sm text-ruby-600 mt-2">{submitError}</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">
|
<div className="flex items-center justify-between mt-4 pt-4 border-t border-border-subtle">
|
||||||
{onCancel && (
|
<Button type="button" variant="ghost" className="text-ruby-600 px-2" onClick={handleReset} disabled={submitting}>
|
||||||
<Button type="button" variant="secondary" onClick={onCancel} disabled={submitting}>
|
Reset Draft
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Button type="submit" disabled={Boolean(submitting || fetchingDailyLog)}>
|
|
||||||
{submitting ? 'Submitting...' : 'Log Visit'}
|
|
||||||
</Button>
|
</Button>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{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>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -6,7 +6,7 @@ export function ImagePreview({ file }: { file: any }) {
|
|||||||
const [url, setUrl] = useState<string | null>(null);
|
const [url, setUrl] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (file instanceof File) {
|
if (file instanceof File || file instanceof Blob) {
|
||||||
const objectUrl = URL.createObjectURL(file);
|
const objectUrl = URL.createObjectURL(file);
|
||||||
setUrl(objectUrl);
|
setUrl(objectUrl);
|
||||||
return () => URL.revokeObjectURL(objectUrl);
|
return () => URL.revokeObjectURL(objectUrl);
|
||||||
|
|||||||
@ -3,6 +3,9 @@ import { createRoot } from 'react-dom/client'
|
|||||||
import { registerSW } from 'virtual:pwa-register'
|
import { registerSW } from 'virtual:pwa-register'
|
||||||
import './styles/styles.css'
|
import './styles/styles.css'
|
||||||
import App from './App.tsx'
|
import App from './App.tsx'
|
||||||
|
import { APP_VERSION } from './api/config.ts'
|
||||||
|
|
||||||
|
console.log(`Krishna Sales App Version: ${APP_VERSION}`);
|
||||||
|
|
||||||
registerSW({ immediate: true })
|
registerSW({ immediate: true })
|
||||||
|
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { useAuth } from '../auth/context';
|
|||||||
import { onAuthErrorAll, orderBookingClient } from '../api/clients';
|
import { onAuthErrorAll, orderBookingClient } from '../api/clients';
|
||||||
import { SCREENS } from './tabs';
|
import { SCREENS } from './tabs';
|
||||||
import { routeConfig } from '../routesConfig';
|
import { routeConfig } from '../routesConfig';
|
||||||
|
import { APP_VERSION } from '../api/config';
|
||||||
import logoUrl from '../assets/logo.png';
|
import logoUrl from '../assets/logo.png';
|
||||||
import './screen.css';
|
import './screen.css';
|
||||||
|
|
||||||
@ -203,6 +204,9 @@ export function ConsoleLayout() {
|
|||||||
<LogOut size={16} />
|
<LogOut size={16} />
|
||||||
<span>Sign out</span>
|
<span>Sign out</span>
|
||||||
</button>
|
</button>
|
||||||
|
<div className="text-center mt-3">
|
||||||
|
<span className="text-xs font-semibold text-foreground/60 tracking-wide">Version {APP_VERSION}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user