import type { DeskAnswer } from './desk-field-values'; export type DeskAnswers = Record; 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 = {}, ) { 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 }; }