- Frontend React (SKEEN Brand) con Vite, TypeScript, Tailwind - Frontend Homenest (versión alternativa) - Módulos Odoo 17 custom (citas, pacientes, monedero, pagos, ventas, inventario, whatsapp) - WACRM fork (Next.js 16 + Supabase) - Hermes + Bridge + Skills (Qwen3.6 via Nan Builders) - Scripts de migración y operación - Documentación extensiva en docs/
136 lines
4.2 KiB
TypeScript
136 lines
4.2 KiB
TypeScript
import { createContext, useContext, useEffect, useState, useCallback } from 'react';
|
|
import type { FC, ReactNode } from 'react';
|
|
import { odooApi, type FrontendUser, type FrontendRole } from '../services/odoo';
|
|
|
|
const TOKEN_KEY = 'skeen_token';
|
|
const USER_KEY = 'skeen_user';
|
|
|
|
const ROLE_RANK: Record<FrontendRole, number> = {
|
|
lectura: 10,
|
|
medico: 50,
|
|
recepcion: 50,
|
|
admin: 100,
|
|
};
|
|
|
|
// Visibilidad del menú por ruta. Si una ruta no está listada, se muestra a todos los roles.
|
|
const MENU_ROLES: Record<string, FrontendRole[]> = {
|
|
'/': ['admin', 'recepcion', 'medico', 'lectura'],
|
|
'/agenda': ['admin', 'recepcion', 'medico'],
|
|
'/pacientes': ['admin', 'recepcion', 'medico', 'lectura'],
|
|
'/medicos': ['admin', 'recepcion'],
|
|
'/servicios': ['admin', 'recepcion', 'medico', 'lectura'],
|
|
'/productos': ['admin', 'recepcion', 'lectura'],
|
|
'/ventas': ['admin', 'recepcion'],
|
|
'/pagos': ['admin', 'recepcion'],
|
|
'/monedero': ['admin', 'recepcion', 'lectura'],
|
|
'/inventario': ['admin', 'recepcion', 'lectura'],
|
|
'/cortes': ['admin', 'recepcion'],
|
|
'/reportes': ['admin', 'recepcion', 'lectura'],
|
|
'/configuracion': ['admin'],
|
|
'/usuarios': ['admin'],
|
|
'/cumpleanos': ['admin', 'recepcion'],
|
|
'/wacrm/messages': ['admin', 'recepcion'],
|
|
'/wacrm/leads': ['admin', 'recepcion'],
|
|
};
|
|
|
|
interface AuthContextValue {
|
|
user: FrontendUser | null;
|
|
token: string | null;
|
|
loading: boolean;
|
|
login: (login: string, password: string) => Promise<FrontendUser>;
|
|
logout: () => Promise<void>;
|
|
hasRole: (min: FrontendRole) => boolean;
|
|
canSee: (href: string) => boolean;
|
|
}
|
|
|
|
const AuthContext = createContext<AuthContextValue | undefined>(undefined);
|
|
|
|
export const AuthProvider: FC<{ children: ReactNode }> = ({ children }) => {
|
|
const [user, setUser] = useState<FrontendUser | null>(null);
|
|
const [token, setToken] = useState<string | null>(() => localStorage.getItem(TOKEN_KEY));
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
const clear = useCallback(() => {
|
|
localStorage.removeItem(TOKEN_KEY);
|
|
localStorage.removeItem(USER_KEY);
|
|
setToken(null);
|
|
setUser(null);
|
|
}, []);
|
|
|
|
// Validar token al arrancar
|
|
useEffect(() => {
|
|
let alive = true;
|
|
const t = localStorage.getItem(TOKEN_KEY);
|
|
if (!t) {
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
// Hidratar usuario cacheado para evitar parpadeo
|
|
const cached = localStorage.getItem(USER_KEY);
|
|
if (cached) {
|
|
try { setUser(JSON.parse(cached)); } catch { /* ignore */ }
|
|
}
|
|
odooApi.me()
|
|
.then((res) => {
|
|
if (!alive) return;
|
|
if (res.status === 'success') {
|
|
setUser(res.user);
|
|
localStorage.setItem(USER_KEY, JSON.stringify(res.user));
|
|
} else {
|
|
clear();
|
|
}
|
|
})
|
|
.catch(() => { if (alive) clear(); })
|
|
.finally(() => { if (alive) setLoading(false); });
|
|
return () => { alive = false; };
|
|
}, [clear]);
|
|
|
|
const login = async (login: string, password: string): Promise<FrontendUser> => {
|
|
const res = await odooApi.login(login, password);
|
|
if (res.status !== 'success' || !res.token) {
|
|
throw new Error(res.message || 'Credenciales inválidas');
|
|
}
|
|
localStorage.setItem(TOKEN_KEY, res.token);
|
|
localStorage.setItem(USER_KEY, JSON.stringify(res.user));
|
|
setToken(res.token);
|
|
setUser(res.user);
|
|
return res.user;
|
|
};
|
|
|
|
const logout = async () => {
|
|
try { await odooApi.logout(); } catch { /* ignore */ }
|
|
clear();
|
|
};
|
|
|
|
const hasRole = (min: FrontendRole): boolean => {
|
|
if (!user) return false;
|
|
return (ROLE_RANK[user.role] ?? 0) >= (ROLE_RANK[min] ?? 0);
|
|
};
|
|
|
|
const canSee = (href: string): boolean => {
|
|
if (!user) return false;
|
|
const allowed = MENU_ROLES[href];
|
|
if (!allowed) return true;
|
|
return allowed.includes(user.role);
|
|
};
|
|
|
|
return (
|
|
<AuthContext.Provider value={{ user, token, loading, login, logout, hasRole, canSee }}>
|
|
{children}
|
|
</AuthContext.Provider>
|
|
);
|
|
};
|
|
|
|
export const useAuth = (): AuthContextValue => {
|
|
const ctx = useContext(AuthContext);
|
|
if (!ctx) throw new Error('useAuth debe usarse dentro de AuthProvider');
|
|
return ctx;
|
|
};
|
|
|
|
export const ROLE_LABELS: Record<FrontendRole, string> = {
|
|
admin: 'Administrador',
|
|
recepcion: 'Recepción',
|
|
medico: 'Médico',
|
|
lectura: 'Solo lectura',
|
|
};
|