175 lines
6.0 KiB
TypeScript
175 lines
6.0 KiB
TypeScript
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<GlosarioContextValue | undefined>(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<GlossaryTerms> };
|
|
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<GlossaryTerms>(cached.terms);
|
|
const [pack, setPack] = useState<string>(cached.pack);
|
|
const [packs, setPacks] = useState<GlossaryPack[]>([]);
|
|
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 <GlosarioContext.Provider value={value}>{children}</GlosarioContext.Provider>;
|
|
};
|
|
|
|
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<string, GlossaryKey> = {
|
|
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 = <T extends { label: string; items: { label: string }[] }>(
|
|
groups: T[],
|
|
term: (key: GlossaryKey) => string,
|
|
): T[] =>
|
|
groups.map((g) => ({
|
|
...g,
|
|
items: g.items.map((it) => ({ ...it, label: traducirEtiqueta(it.label, term) })),
|
|
}));
|