import { Link, router, usePage } from '@inertiajs/react'; import { createContext, useContext, useEffect, useState, type ReactNode, } from 'react'; import { contextApi, type DossierContext } from '@/lib/investor-context'; export type NavigationView = { case: { id: string; title: string; version: number }; phases: Record; steps: { code: string; title: string; phase: number; state: string; applicability: string; }[]; next: { code: string; title: string; action: string; href: string } | null; facts: { land_needed: string }; sector_contexts: { id: number; name: string; slug: string }[]; zone_contexts: { zone_id: string; title: string; province: string | null; district: string | null; consulted_public_version: number; }[]; results: { id: string; engine: string; recorded_at: string; context_state?: string; }[]; requests: { id: string; protocol: string; status: string; version: number; }[]; documents: { id: string; file_name: string; status: string }[]; tools: { simulator: string | null; desk: string; land: string | null; guidance: string | null; }; investment_context: DossierContext | null; }; type Focus = { case_id: string; step_code: string | null; version: number }; type State = { active: string; actor?: number; cases: { id: string; title: string; version: number }[]; view: NavigationView | null; step: string | null; error: string; busy: boolean; choose: (id: string, step?: string | null) => void; refresh: () => void; }; const Context = createContext({ active: '', cases: [], view: null, step: null, error: '', busy: false, choose: () => {}, refresh: () => {}, }); export const useNavigationContext = () => useContext(Context); export function NavigationContextProvider({ children, }: { children: ReactNode; }) { const { props, url } = usePage<{ auth: { user: { id: number } | null }; investorPathEnabled?: boolean; }>(); const actor = props.auth?.user?.id; const [loaded, setLoaded] = useState<{ actor: number; cases: State['cases']; focus: Focus | null; view: NavigationView | null; } | null>(null); const [error, setError] = useState(''); const [busy, setBusy] = useState(false); const [reload, setReload] = useState(0); const [hash, setHash] = useState( typeof window === 'undefined' ? '' : window.location.hash, ); useEffect(() => { const changed = () => setHash(window.location.hash); window.addEventListener('hashchange', changed); return () => window.removeEventListener('hashchange', changed); }, []); useEffect(() => { setHash(window.location.hash); }, [url]); const current = loaded?.actor === actor ? loaded : null; const active = current?.focus?.case_id ?? ''; useEffect(() => { if (!actor || !props.investorPathEnabled) return; let stopped = false; setError(''); setBusy(true); void (async () => { const [listing, pref] = await Promise.all([ contextApi<{ contexts: State['cases'] }>('/investimentos'), contextApi<{ installed: boolean; focus: Focus | null; message?: string; }>('/investimentos/contexto-atual'), ]); if (stopped) return; if (!pref.installed) throw new Error( 'A instalação do contexto está pendente; os dossiers existentes permanecem disponíveis.', ); const direct = url.match( /^\/investimentos\/([a-f0-9-]{36})(?:[/?#]|$)/i, )?.[1] ?? new URL(url, 'https://context.invalid').searchParams.get( 'context', ); let focus = pref.focus; const code = /^#S(0[1-9]|1[0-9])$/.test(window.location.hash) ? window.location.hash.slice(1) : null; if ( direct && listing.contexts.some((c) => c.id === direct) && (focus?.case_id !== direct || (code && focus?.step_code !== code)) ) { focus = ( await contextApi<{ focus: Focus }>( '/investimentos/contexto-atual', { case_id: direct, step_code: code, version: focus?.version ?? 0, }, 'PUT', ) ).focus; } const view = focus ? await contextApi( `/investimentos/${focus.case_id}${direct === focus.case_id && new URL(url, 'https://context.invalid').searchParams.get('opportunity') ? '?opportunity=' + encodeURIComponent(new URL(url, 'https://context.invalid').searchParams.get('opportunity')!) : ''}`, ) : null; if (!stopped) { setLoaded({ actor, cases: listing.contexts, focus, view }); if (pref.message) setError(pref.message); } })() .catch((e) => { if (!stopped) { setLoaded(null); setError( e instanceof Error ? e.message : 'Contexto indisponível.', ); } }) .finally(() => { if (!stopped) setBusy(false); }); return () => { stopped = true; }; }, [actor, url, hash, reload, props.investorPathEnabled]); function choose(id: string, step: string | null = null) { if (!actor || !current?.cases.some((c) => c.id === id)) return; setBusy(true); setError(''); void contextApi( '/investimentos/contexto-atual', { case_id: id, step_code: step, version: current.focus?.version ?? 0, }, 'PUT', ) .then(() => { const target = `/investimentos/${id}${step ? '#' + step : ''}`; if (url !== target) router.visit(target); else setReload((x) => x + 1); }) .catch((e) => setError( e instanceof Error ? e.message : 'Não foi possível guardar a escolha.', ), ) .finally(() => setBusy(false)); } return ( setReload((x) => x + 1), }} > {children} ); } export function NavigationContextSummary() { const { actor, active, cases, view, step, error, busy, choose } = useNavigationContext(); const { url } = usePage(); const [open, setOpen] = useState(false); const [pending, setPending] = useState(''); if (!actor) return null; if (/^\/(management|perfis|minhas-oportunidades|dashboard)(\/|$)/.test(url)) return null; const saved = view?.investment_context; const ref = saved?.selection; const params = new URLSearchParams(url.split('?')[1] ?? ''); const scopedChoice = saved?.opportunity_choices?.find( (c) => c.opportunity.key === params.get('opportunity'), ); const multiple = (saved?.opportunity_choices?.length ?? 0) > 1; const currentStep = view?.steps.find((s) => s.code === step); const sector = scopedChoice ? scopedChoice.opportunity.fields.sector || 'Não indicado' : ref?.sector?.title || saved?.fields.sector || view?.sector_contexts.map((s) => s.name).join(' · ') || 'Não indicado'; const location = scopedChoice ? scopedChoice.territory?.title || 'Por definir' : multiple ? 'Consulte a localização de cada oportunidade' : saved?.fields.location || ref?.territory?.title || ref?.park?.title || view?.zone_contexts .map((z) => `${z.title} (v${z.consulted_public_version})`) .join(' · ') || 'Não indicada'; return ( ); }