permisos de agenda: selector de acciones por usuario (ver, agendar, editar, eliminar, visitas)

- roles.py: AGENDA_PERMS, AGENDA_PERM_KEYS, WRITE_RANK_MIN, agenda_defaults_for,
  resolve_agenda_perms, AGENDA_STATUS_PERM, AGENDA_STATE_PERM, _agenda_status_perm
- frontend_user.py: campo agenda_perms, _agenda_perms_list(), to_public_dict()
  expone agenda_perms y agenda_perms_custom
- auth.py: require_agenda(perm) decorador + agenda_perm_error()
- frontend.py: @require_agenda aplicado a 22 endpoints + branch en
  update_appointment_status por accion
- auth_users_update: whitelist acepta agenda_perms
- Odoo upgrade: columna skeen_frontend_user.agenda_perms
- Frontend: types AgendaPerm, AGENDA_PERM_ITEMS, canAgenda() en auth.tsx
- UsuariosPanel: checkboxes de permisos de agenda en modal de permisos
- Agenda.tsx: gates por permiso en crear/editar/eliminar/visitas/dragdrop/bloqueos
- EstadoChecklist/BloqueosModal/ListaEspera: hide actions without permission

Verificacion API: 18/18 tests OK (admin hereda, recorte=403, lect=403, restore=null)
This commit is contained in:
2026-09-30 09:46:51 +00:00
parent 85a281add1
commit 169864958d
11 changed files with 401 additions and 39 deletions

View File

@@ -1,6 +1,6 @@
import { createContext, useContext, useEffect, useState, useCallback } from 'react';
import type { FC, ReactNode } from 'react';
import { odooApi, type FrontendUser, type FrontendRole } from '../services/odoo';
import { odooApi, type FrontendUser, type FrontendRole, type AgendaPerm } from '../services/odoo';
const TOKEN_KEY = 'skeen_token';
const USER_KEY = 'skeen_user';
@@ -75,6 +75,7 @@ interface AuthContextValue {
logout: () => Promise<void>;
hasRole: (min: FrontendRole) => boolean;
canSee: (href: string) => boolean;
canAgenda: (perm: AgendaPerm) => boolean;
}
const AuthContext = createContext<AuthContextValue | undefined>(undefined);
@@ -219,8 +220,19 @@ export const AuthProvider: FC<{ children: ReactNode }> = ({ children }) => {
return true;
};
// Acciones dentro de la Agenda. El backend resuelve la herencia del rol y
// manda la lista efectiva en user.agenda_perms; el fallback por rango cubre
// la sesión cacheada anterior al despliegue.
const canAgenda = (perm: AgendaPerm): boolean => {
if (!user) return false;
const todas: AgendaPerm[] = ['ver', 'agendar', 'editar', 'eliminar', 'visitas'];
const perms = user.agenda_perms
?? (ROLE_RANK[user.role] >= ROLE_RANK.recepcion ? todas : ['ver']);
return perms.includes(perm);
};
return (
<AuthContext.Provider value={{ user, token, loading, bootError, retryBoot, login, logout, hasRole, canSee }}>
<AuthContext.Provider value={{ user, token, loading, bootError, retryBoot, login, logout, hasRole, canSee, canAgenda }}>
{children}
</AuthContext.Provider>
);