import { router, useForm } from '@inertiajs/react'; import { useEffect, useRef, useState } from 'react'; import { createOperationKey } from './operation-key'; export type EducationDeclaration = { id: string; qualification: string; quantity: number; status: string; version_no: number; period_start: string; period_end: string; reference_date?: string; valid_until: string; source: string; submitted_by_name?: string | null; reviewed_by_name?: string | null; review_reason?: string | null; employer?: string; can_review?: boolean; review_unavailable?: 'self' | 'expired' | null; review_context?: { actor_id: number; commands: { key: string; version: number }[]; }; }; export type EducationReviewProps = { declaration: EducationDeclaration; kind: 'demands' | 'capacities'; actorId: number; today: string; references?: Record; }; type Values = { version_no: number; status: string; review_reason: string }; type Snapshot = Pick< EducationDeclaration, | 'version_no' | 'qualification' | 'quantity' | 'status' | 'period_start' | 'period_end' | 'valid_until' | 'source' > & { reference_date: string; employer: string; references: Record; }; type Attempt = { values: Values & { client_key: string }; reasonGeneration: number; statusGeneration: number; base: Snapshot; }; const object = (value: unknown): value is Record => !!value && typeof value === 'object' && !Array.isArray(value); const positive = (value: unknown): value is number => typeof value === 'number' && Number.isSafeInteger(value) && value > 0; const snapshot = ( row: EducationDeclaration, references: Record = {}, ): Snapshot => ({ version_no: row.version_no, qualification: row.qualification, quantity: row.quantity, status: row.status, period_start: row.period_start, period_end: row.period_end, valid_until: row.valid_until, source: row.source, reference_date: row.reference_date ?? '', employer: row.employer ?? '', references: { ...references }, }); export function useEducationReview({ declaration: row, kind, actorId, references = {}, }: EducationReviewProps) { const form = useForm({ version_no: row.version_no, status: 'reviewed', review_reason: '', }); const live = useRef(form.data); live.current = form.data; const base = useRef(snapshot(row, references)), pending = useRef(null), alive = useRef(true); const generation = useRef({ status: 0, review_reason: 0 }); const [uncertain, setUncertain] = useState(false), [conflict, setConflict] = useState(false), [refreshing, setRefreshing] = useState(false), [accessBlocked, setAccessBlocked] = useState(false), [notice, setNotice] = useState(''); useEffect(() => { alive.current = true; return () => { alive.current = false; }; }, []); const invalid = !positive(actorId) || row.review_context?.actor_id !== actorId || !positive(row.version_no) || !Array.isArray(row.review_context?.commands); const stale = form.data.version_no !== row.version_no; function change(field: 'status' | 'review_reason', value: string) { generation.current[field]++; live.current = { ...live.current, [field]: value }; form.setData(live.current); } function read(props: unknown): EducationDeclaration | null { if ( !object(props) || props.actorId !== actorId || props.kind !== kind || !object(props.declaration) || props.declaration.id !== row.id ) return null; const value = props.declaration; if ( !positive(value.version_no) || !object(value.review_context) || value.review_context.actor_id !== actorId || !Array.isArray(value.review_context.commands) ) return null; return value as EducationDeclaration; } function returnedReferences( props: Record, ): Record | null { if ( !object(props.references) || Object.values(props.references).some( (value) => value !== null && typeof value !== 'string', ) ) return null; return props.references as Record; } function acknowledge( record: EducationDeclaration, currentReferences: Record = references, ) { const attempt = pending.current; if ( !alive.current || !attempt || record.review_context?.actor_id !== actorId ) return false; const receipt = record.review_context.commands.find( (item) => item.key === attempt.values.client_key, ); if ( !receipt || receipt.version !== attempt.values.version_no + 1 || record.version_no < receipt.version ) return false; base.current = record.version_no === receipt.version ? snapshot(record, currentReferences) : { ...attempt.base, version_no: receipt.version, status: attempt.values.status, }; live.current = { version_no: receipt.version, status: generation.current.status === attempt.statusGeneration ? 'reviewed' : live.current.status, review_reason: generation.current.review_reason === attempt.reasonGeneration ? '' : live.current.review_reason, }; form.setData(live.current); form.clearErrors(); pending.current = null; setUncertain(false); setConflict(false); setAccessBlocked(false); setNotice( 'Revisão registada. As alterações posteriores ao envio foram conservadas.', ); return true; } useEffect(() => { if (!invalid && pending.current) acknowledge(row); }, [row, actorId, invalid]); function submit() { if ( invalid || accessBlocked || refreshing || form.processing || (!pending.current && (row.can_review !== true || stale || conflict)) ) return; if (!pending.current) { let key: string; try { key = createOperationKey(); } catch { setNotice( 'Não foi possível preparar a decisão. Reabra o portal no endereço HTTPS configurado.', ); return; } pending.current = { values: { ...live.current, client_key: key }, reasonGeneration: generation.current.review_reason, statusGeneration: generation.current.status, base: { ...base.current }, }; } const attempt = pending.current; setUncertain(true); setNotice('A aguardar confirmação da revisão.'); form.transform(() => attempt.values); form.post(`/management/education/${kind}/${row.id}/review`, { preserveScroll: true, onSuccess: (page) => { if (!alive.current || pending.current !== attempt) return; const record = read(page.props), labels = returnedReferences(page.props); if (!record || !labels || !acknowledge(record, labels)) setNotice( 'A resposta não permitiu confirmar a revisão. Actualize a declaração ou recupere o mesmo envio.', ); }, onError: () => { if (!alive.current || pending.current !== attempt) return; pending.current = null; setUncertain(false); setNotice( 'Reveja os campos assinalados. A decisão preparada foi conservada.', ); }, onNetworkError: () => { if (alive.current && pending.current === attempt) setNotice( 'Sem confirmação da revisão. Recupere o mesmo envio antes de preparar outra decisão.', ); }, onCancel: () => { if (alive.current && pending.current === attempt) setNotice( 'Envio interrompido sem confirmação. Recupere o mesmo envio.', ); }, onHttpException: (response) => { if (!alive.current || pending.current !== attempt) return; if ([401, 403, 419].includes(response.status)) setAccessBlocked(true); if (response.status === 422) { pending.current = null; setUncertain(false); } if ([409, 422].includes(response.status)) setConflict(true); setNotice( 'Não foi possível confirmar a revisão. Actualize a declaração e o acesso antes de continuar. A decisão preparada foi conservada.', ); }, }); } function refresh() { if (refreshing || form.processing) return; setRefreshing(true); router.reload({ only: [ 'declaration', 'references', 'today', 'actorId', 'kind', 'history', ], onSuccess: (page) => { if (!alive.current) return; const record = read(page.props), labels = returnedReferences(page.props); if (!record || !labels) return; setAccessBlocked(false); if (pending.current) acknowledge(record, labels); }, onFinish: () => { if (alive.current) setRefreshing(false); }, }); } function adopt() { if ( invalid || !stale || row.can_review !== true || refreshing || form.processing ) return; const attempt = pending.current; if (attempt) { if (acknowledge(row)) return; if ( row.version_no <= attempt.values.version_no || row.review_context?.commands.some( (item) => item.key === attempt.values.client_key, ) ) return; pending.current = null; setUncertain(false); } base.current = snapshot(row, references); live.current = { ...live.current, version_no: row.version_no }; form.setData(live.current); setConflict(false); form.clearErrors('version_no'); setNotice( 'Versão actual confirmada. A sua decisão preparada foi conservada para rever antes de enviar.', ); } const guard = useRef(false); guard.current = uncertain || !!form.data.review_reason || form.data.status !== 'reviewed'; useEffect(() => { const remove = router.on('before', (event) => { if ( guard.current && event.detail.visit.method === 'get' && !event.detail.visit.preserveState && !window.confirm( 'Existe uma revisão preparada ou um envio sem confirmação. Sair desta página?', ) ) event.preventDefault(); }); const unload = (event: BeforeUnloadEvent) => { if (guard.current) { event.preventDefault(); event.returnValue = ''; } }; window.addEventListener('beforeunload', unload); return () => { remove(); window.removeEventListener('beforeunload', unload); }; }, []); return { form, change, submit, refresh, adopt, base: base.current, current: snapshot(row, references), stale, conflict, refreshing, uncertain, notice, canAdopt: !invalid && stale && row.can_review === true && !refreshing && !form.processing, blocked: invalid || accessBlocked || refreshing || (!pending.current && (row.can_review !== true || stale || conflict)), sent: pending.current?.values, }; }