import { router, useForm } from '@inertiajs/react'; import { useRef, useState } from 'react'; type Values = Record; type Kind = 'investor' | 'contact' | 'lead' | 'task'; function object(value: unknown): value is Record { return !!value && typeof value === 'object' && !Array.isArray(value); } function savedRecord( props: Record, kind: Kind, id: string, ): Record | null { let candidate: unknown = kind === 'investor' ? props.investor : props.lead; if (kind === 'contact') candidate = Array.isArray(props.contacts) ? props.contacts.find((value) => object(value) && value.id === id) : null; if (kind === 'task') candidate = object(props.lead) && Array.isArray(props.lead.tasks) ? props.lead.tasks.find( (value) => object(value) && value.id === id, ) : null; return object(candidate) && candidate.id === id ? candidate : null; } /** Keeps deliberate edits, including a deliberate return to the original value. */ export function useCrmRecordEdit({ id, kind, version, values, blankAfterSave = [], }: { id: string; kind: Kind; version: number; values: Values; blankAfterSave?: string[]; }) { const form = useForm({ version, values }); const live = useRef(form.data); live.current = form.data; const identity = useRef(id); identity.current = id; const base = useRef({ id, version, values: { ...values } }); const edited = useRef(new Set()); const generation = useRef>({}); const [choices, setChoices] = useState>( {}, ); const [refreshing, setRefreshing] = useState(false); const keys = Object.keys(values); const stale = version !== form.data.version; const changed = keys.filter( (key) => values[key] !== base.current.values[key], ); const conflicts = changed.filter( (key) => edited.current.has(key) && form.data.values[key] !== values[key], ); const unresolved = conflicts.filter( (key) => choices[`${version}:${key}`] !== 'mine', ); const invalid = base.current.id !== id || !Number.isInteger(version) || version < 1; function change(key: string, value: string) { if (!keys.includes(key)) return; edited.current.add(key); generation.current[key] = (generation.current[key] ?? 0) + 1; setChoices((previous) => { const next = { ...previous }; delete next[`${version}:${key}`]; return next; }); live.current = { ...live.current, values: { ...live.current.values, [key]: value }, }; form.setData((previous) => ({ ...previous, values: { ...previous.values, [key]: value }, })); } function choose(key: string, choice: 'mine' | 'current') { if (choice === 'current') change(key, values[key]); setChoices((previous) => ({ ...previous, [`${version}:${key}`]: choice, })); } function adopt() { if (!stale || invalid || refreshing || unresolved.length) return; const next = Object.fromEntries( keys.map((key) => [ key, edited.current.has(key) ? live.current.values[key] : values[key], ]), ); base.current = { id, version, values: { ...values } }; live.current = { version, values: next }; form.setData(live.current); form.clearErrors('version'); setChoices({}); } function refresh() { if (refreshing || form.processing) return; setRefreshing(true); router.reload({ only: kind === 'lead' || kind === 'task' ? ['lead', 'owners', 'taskCreation', 'nextAction'] : ['investor', 'contacts'], onFinish: () => setRefreshing(false), }); } const blocked = invalid || stale || refreshing || !!form.errors.version; function submit(url: string) { if (blocked || form.processing) return; const submitted = { ...live.current.values, version: live.current.version, }; const sentGeneration = { ...generation.current }; form.transform(() => submitted); form.patch(url, { preserveScroll: true, onSuccess: (page) => { if (identity.current !== id) return; const record = savedRecord(page.props, kind, id); const actualVersion = record?.version_no; if ( !record || typeof actualVersion !== 'number' || !Number.isInteger(actualVersion) || actualVersion < 1 ) return; const next: Values = {}; for (const key of keys) { const value = record[key]; if ( blankAfterSave.includes(key) || value === null || value === undefined ) next[key] = ''; else if ( typeof value === 'string' || typeof value === 'number' ) next[key] = String(value); else return; } base.current = { id, version: actualVersion, values: { ...next }, }; edited.current.clear(); const merged = { ...next }; for (const key of keys) { if ( (generation.current[key] ?? 0) !== (sentGeneration[key] ?? 0) ) { merged[key] = live.current.values[key]; edited.current.add(key); } } setChoices({}); const saved = { version: actualVersion, values: next }; form.setDefaults(saved); live.current = { version: actualVersion, values: merged }; form.setData(live.current); form.clearErrors(); }, }); } function error(key: string): string | undefined { const value = Object.entries(form.errors).find( ([name]) => name === key, )?.[1]; return typeof value === 'string' ? value : undefined; } return { form, values: form.data.values, current: values, version, baseVersion: form.data.version, changed, conflicts, unresolved, stale, refreshing, invalid, blocked, change, choose, adopt, refresh, submit, error, }; } export type CrmRecordEditor = ReturnType;