Files
Investidor-V2/resources/js/lib/desk-checkpoints.ts
T

155 lines
5.0 KiB
TypeScript

import type { DeskAnswer } from './desk-field-values';
export type DeskAnswers = Record<string, DeskAnswer>;
export type CheckpointRequest = {
id: string;
version: number;
status: string;
payload: DeskAnswers;
editable_fields: string[];
can_edit: boolean;
protocol: string | null;
analyst_id: number | null;
due_at: string | null;
};
export type CheckpointInput = {
version: number;
request_version: number;
operation_key: string;
payload: DeskAnswers;
edited_keys: string[];
reason: string;
};
export type DeskCheckpoint = {
version: number;
request_version: number;
updated_at: string;
payload: DeskAnswers;
base_payload: DeskAnswers;
edited_keys: string[];
reason: string;
};
export type CheckpointView = {
installed: boolean;
request: CheckpointRequest;
checkpoint: DeskCheckpoint | null;
};
export type RecoveryChoice = 'page' | 'saved' | 'current';
export function checkpointSignature(values: DeskAnswers): string {
return JSON.stringify(
Object.fromEntries(
Object.entries(values)
.sort(([a], [b]) => a.localeCompare(b))
.map(([key, value]) => [
key,
Array.isArray(value) ? [...value].sort() : (value ?? null),
]),
),
);
}
export const checkpointSame = (
a: DeskAnswer | undefined,
b: DeskAnswer | undefined,
) =>
checkpointSignature({ value: a ?? null }) ===
checkpointSignature({ value: b ?? null });
const contextKeys = [
'context_opportunity',
'context_opportunity_snapshot',
'context_reuse',
];
function contextLabel(values: DeskAnswers | undefined): string {
if (!values?.context_opportunity) return 'Sem oportunidade seleccionada';
try {
const snapshot = JSON.parse(
String(values.context_opportunity_snapshot ?? '{}'),
);
if (typeof snapshot.choice?.opportunity?.title === 'string')
return (
snapshot.choice.opportunity.title +
(Number.isInteger(snapshot.revision)
? ` · revisão ${snapshot.revision}`
: '')
);
} catch {
/* A legacy context still remains recoverable. */
}
return 'Oportunidade seleccionada; designação por recuperar';
}
/** Three sources remain explicit; a deliberate revert is still an edit. */
export function checkpointRecovery(
base: DeskAnswers,
page: DeskAnswers,
edited: readonly string[],
current: DeskAnswers,
saved: DeskCheckpoint | null,
allowed: readonly string[],
choices: Record<string, RecoveryChoice> = {},
) {
const payload = { ...current };
const conflicts: string[] = [];
const intent: string[] = [];
const rows: {
key: string;
page: DeskAnswer | null;
saved: DeskAnswer | null;
current: DeskAnswer | null;
conflict: boolean;
choice: RecoveryChoice;
}[] = [];
for (const key of allowed) {
if (contextKeys.includes(key) && key !== 'context_opportunity')
continue;
const group =
key === 'context_opportunity'
? contextKeys.filter((k) => allowed.includes(k))
: [key];
const same = (a: DeskAnswers | undefined, b: DeskAnswers | undefined) =>
group.every((k) => checkpointSame(a?.[k], b?.[k]));
const local =
group.some((k) => edited.includes(k)) || !same(page, base);
const remote =
!!saved &&
(group.some((k) => saved.edited_keys.includes(k)) ||
!same(saved.payload, saved.base_payload));
if (!local && !remote) continue;
const conflict =
(local && remote && !same(page, saved?.payload)) ||
(local && !same(base, current) && !same(page, current)) ||
(remote &&
!same(saved?.base_payload, current) &&
!same(saved?.payload, current));
const choice = choices[key] ?? (local ? 'page' : 'saved');
if (conflict && !choices[key]) conflicts.push(key);
for (const member of group)
payload[member] =
(choice === 'page'
? page[member]
: choice === 'saved'
? saved?.payload[member]
: current[member]) ?? null;
if (choice !== 'current') intent.push(...group);
rows.push({
key,
page:
key === 'context_opportunity'
? contextLabel(page)
: (page[key] ?? null),
saved:
key === 'context_opportunity'
? contextLabel(saved?.payload)
: (saved?.payload[key] ?? null),
current:
key === 'context_opportunity'
? contextLabel(current)
: (current[key] ?? null),
conflict,
choice,
});
}
return { payload, conflicts, intent, rows };
}