155 lines
5.0 KiB
TypeScript
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 };
|
|
}
|