Files
Investidor-V2/resources/js/lib/crm-record-edit.ts
T

214 lines
7.1 KiB
TypeScript

import { router, useForm } from '@inertiajs/react';
import { useRef, useState } from 'react';
type Values = Record<string, string>;
type Kind = 'investor' | 'contact' | 'lead' | 'task';
function object(value: unknown): value is Record<string, unknown> {
return !!value && typeof value === 'object' && !Array.isArray(value);
}
function savedRecord(
props: Record<string, unknown>,
kind: Kind,
id: string,
): Record<string, unknown> | 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<string>());
const generation = useRef<Record<string, number>>({});
const [choices, setChoices] = useState<Record<string, 'mine' | 'current'>>(
{},
);
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<typeof useCrmRecordEdit>;