import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; import type { FC, ReactNode } from 'react'; import { odooApi, GLOSSARY_KEYS, type GlossaryKey, type GlossaryPack, type GlossaryTerms } from '../services/odoo'; import { useAuth } from './auth'; // Terminología por defecto = pack 'clinica' del backend, que coincide con los // literales que usa hoy producción. Es también el respaldo si el endpoint falla, // para que la interfaz nunca se rompa por un glosario no disponible. export const GLOSARIO_CLINICA: GlossaryTerms = { patient_singular: 'Paciente', patient_plural: 'Pacientes', appointment_singular: 'Cita', appointment_plural: 'Citas', visit_singular: 'Visita', visit_plural: 'Visitas', record_singular: 'Expediente', record_plural: 'Expedientes', doctor_singular: 'Médico', doctor_plural: 'Médicos', staff_singular: 'Personal', staff_plural: 'Personal', branch_singular: 'Sucursal', branch_plural: 'Sucursales', room_singular: 'Cabina', room_plural: 'Cabinas', service_singular: 'Servicio', service_plural: 'Servicios', package_singular: 'Paquete', package_plural: 'Paquetes', prescription_singular: 'Receta', prescription_plural: 'Recetas', treatment_singular: 'Tratamiento', treatment_plural: 'Tratamientos', }; const GLOSARIO_CACHE_KEY = 'skeen_glosario'; interface GlosarioContextValue { terms: GlossaryTerms; pack: string; packs: GlossaryPack[]; loading: boolean; /** Traduce una clave del glosario; cae al valor del pack clínica y luego a la clave. */ term: (key: GlossaryKey) => string; /** Reemplaza los términos en memoria tras un PUT en Configuración. */ setTerms: (pack: string, terms: GlossaryTerms) => void; refresh: () => void; } const GlosarioContext = createContext(undefined); const leerCache = (): { pack: string; terms: GlossaryTerms } => { try { const raw = localStorage.getItem(GLOSARIO_CACHE_KEY); if (!raw) return { pack: 'clinica', terms: GLOSARIO_CLINICA }; const parsed = JSON.parse(raw) as { pack?: string; terms?: Partial }; const terms: GlossaryTerms = { ...GLOSARIO_CLINICA }; for (const k of GLOSSARY_KEYS) { const v = parsed.terms?.[k]; if (v) terms[k] = v; } return { pack: parsed.pack || 'clinica', terms }; } catch { return { pack: 'clinica', terms: GLOSARIO_CLINICA }; } }; export const GlosarioProvider: FC<{ children: ReactNode }> = ({ children }) => { const { token } = useAuth(); const cached = useMemo(leerCache, []); const [terms, setTermsState] = useState(cached.terms); const [pack, setPack] = useState(cached.pack); const [packs, setPacks] = useState([]); const [loading, setLoading] = useState(false); const [attempt, setAttempt] = useState(0); const refresh = useCallback(() => setAttempt((n) => n + 1), []); const setTerms = useCallback((nextPack: string, nextTerms: GlossaryTerms) => { setPack(nextPack); setTermsState(nextTerms); try { localStorage.setItem(GLOSARIO_CACHE_KEY, JSON.stringify({ pack: nextPack, terms: nextTerms })); } catch { // localStorage bloqueado: seguimos en memoria } }, []); // Carga el glosario vigente al iniciar sesión y refresca al reintentar. useEffect(() => { if (!token) return; let alive = true; setLoading(true); odooApi.getGlosario() .then((res) => { if (!alive || res.status !== 'success') return; const next: GlossaryTerms = { ...GLOSARIO_CLINICA }; for (const k of GLOSSARY_KEYS) { if (res.terms?.[k]) next[k] = res.terms[k]; } setTerms(res.pack || 'clinica', next); }) .catch(() => { // fallback: caché o pack clínica }) .finally(() => { if (alive) setLoading(false); }); return () => { alive = false; }; }, [token, attempt, setTerms]); // Packs disponibles solo hacen falta en Configuración; se piden con la sesión. useEffect(() => { if (!token) return; let alive = true; odooApi.getGlosarioPacks() .then((res) => { if (alive && res.status === 'success' && res.packs) setPacks(res.packs); }) .catch(() => {}); return () => { alive = false; }; }, [token]); const term = useCallback( (key: GlossaryKey): string => terms[key] || GLOSARIO_CLINICA[key] || key, [terms], ); const value = useMemo( () => ({ terms, pack, packs, loading, term, setTerms, refresh }), [terms, pack, packs, loading, term, setTerms, refresh], ); return {children}; }; export const useGlosario = (): GlosarioContextValue => { const ctx = useContext(GlosarioContext); if (!ctx) throw new Error('useGlosario debe usarse dentro de GlosarioProvider'); return ctx; }; // Etiquetas del menú lateral que se traducen al glosario. Solo se listan las que // hoy coinciden con un término clínico (Pacientes, Visitas, Médicos...); así con // la configuración por defecto el menú se ve igual que siempre. Los nombres de // módulo (Agenda, Dashboard, Punto de Venta...) no se tocan. export const MENU_LABEL_TERMS: Record = { Pacientes: 'patient_plural', Visitas: 'visit_plural', Expedientes: 'record_plural', Médicos: 'doctor_plural', Servicios: 'service_plural', Paquetes: 'package_plural', }; /** Devuelve la etiqueta del menú traducida según el glosario vigente. */ export const traducirEtiqueta = (label: string, term: (key: GlossaryKey) => string): string => { const key = MENU_LABEL_TERMS[label]; return key ? term(key) : label; }; /** Copia superficial del menú (grupos con items) aplicando el glosario a las etiquetas. */ export const traducirMenu = ( groups: T[], term: (key: GlossaryKey) => string, ): T[] => groups.map((g) => ({ ...g, items: g.items.map((it) => ({ ...it, label: traducirEtiqueta(it.label, term) })), }));