Roles ampliados (10) y matriz de permisos por rol

- Roles: admin, médico, recepción, enfermería, cosmetología, psicóloga,
  farmacia, entrenador, marketing + lectura (compatibilidad)
- Matriz de permisos por rol persistida en Odoo (ir.config_parameter)
  y editable en Configuración → Roles (secciones × roles)
- Prioridad: permisos personales del usuario > matriz por rol > defaults
- Protección: admin siempre conserva /configuracion
This commit is contained in:
2026-08-14 08:47:27 +00:00
parent be42974030
commit 281fb34588
9 changed files with 346 additions and 29 deletions

View File

@@ -4,12 +4,19 @@ import { odooApi, type FrontendUser, type FrontendRole } from '../services/odoo'
const TOKEN_KEY = 'skeen_token';
const USER_KEY = 'skeen_user';
const MENU_ROLES_KEY = 'skeen_menu_roles';
const ROLE_RANK: Record<FrontendRole, number> = {
lectura: 10,
admin: 100,
medico: 50,
recepcion: 50,
admin: 100,
enfermeria: 50,
cosmetologia: 50,
psicologa: 50,
farmacia: 50,
entrenador: 10,
marketing: 10,
lectura: 10,
};
// Visibilidad del menú por ruta. Si una ruta no está listada, se muestra a todos los roles.
@@ -60,12 +67,35 @@ 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);
// Matriz dinámica de permisos por rol (cache en localStorage; fallback a MENU_ROLES)
const [menuRoles, setMenuRoles] = useState<Record<string, FrontendRole[]> | null>(() => {
try {
const cached = localStorage.getItem(MENU_ROLES_KEY);
return cached ? JSON.parse(cached) : null;
} catch {
return null;
}
});
const refreshMenuRoles = useCallback(async () => {
try {
const res = await odooApi.getRolePermissions();
if (res.status === 'success' && res.matrix) {
setMenuRoles(res.matrix);
localStorage.setItem(MENU_ROLES_KEY, JSON.stringify(res.matrix));
}
} catch {
// fallback: MENU_ROLES hardcodeado
}
}, []);
const clear = useCallback(() => {
localStorage.removeItem(TOKEN_KEY);
localStorage.removeItem(USER_KEY);
localStorage.removeItem(MENU_ROLES_KEY);
setToken(null);
setUser(null);
setMenuRoles(null);
}, []);
// Validar token al arrancar
@@ -87,6 +117,7 @@ export const AuthProvider: FC<{ children: ReactNode }> = ({ children }) => {
if (res.status === 'success') {
setUser(res.user);
localStorage.setItem(USER_KEY, JSON.stringify(res.user));
refreshMenuRoles();
} else {
clear();
}
@@ -94,7 +125,7 @@ export const AuthProvider: FC<{ children: ReactNode }> = ({ children }) => {
.catch(() => { if (alive) clear(); })
.finally(() => { if (alive) setLoading(false); });
return () => { alive = false; };
}, [clear]);
}, [clear, refreshMenuRoles]);
const login = async (login: string, password: string): Promise<FrontendUser> => {
const res = await odooApi.login(login, password);
@@ -105,6 +136,7 @@ export const AuthProvider: FC<{ children: ReactNode }> = ({ children }) => {
localStorage.setItem(USER_KEY, JSON.stringify(res.user));
setToken(res.token);
setUser(res.user);
refreshMenuRoles();
return res.user;
};
@@ -120,11 +152,11 @@ export const AuthProvider: FC<{ children: ReactNode }> = ({ children }) => {
const canSee = (href: string): boolean => {
if (!user) return false;
// Permisos personalizados: lista no vacía manda sobre los defaults por rol
// Permisos personalizados: lista no vacía manda sobre la matriz por rol
if (user.allowed_menus && user.allowed_menus.length > 0) {
return user.allowed_menus.includes(href);
}
const allowed = MENU_ROLES[href];
const allowed = (menuRoles || MENU_ROLES)[href];
if (!allowed) return true;
return allowed.includes(user.role);
};
@@ -144,7 +176,13 @@ export const useAuth = (): AuthContextValue => {
export const ROLE_LABELS: Record<FrontendRole, string> = {
admin: 'Administrador',
recepcion: 'Recepción',
medico: 'Médico',
lectura: 'Solo lectura',
recepcion: 'Recepción',
enfermeria: 'Enfermería',
cosmetologia: 'Cosmetología',
psicologa: 'Psicóloga',
farmacia: 'Farmacia',
entrenador: 'Entrenador',
marketing: 'Marketing',
lectura: 'Lectura',
};