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

@@ -884,6 +884,17 @@ export interface WacrmLead {
export type FrontendRole = 'admin' | 'medico' | 'recepcion' | 'enfermeria' | 'cosmetologia' | 'psicologa' | 'farmacia' | 'gerencia' | 'marketing' | 'lectura';
// Acciones de la Agenda (espejo de AGENDA_PERMS en skeen_whatsapp/roles.py).
export type AgendaPerm = 'ver' | 'agendar' | 'editar' | 'eliminar' | 'visitas';
export const AGENDA_PERM_ITEMS: { key: AgendaPerm; label: string; nota: string }[] = [
{ key: 'ver', label: 'Consultar agenda', nota: 'Siempre activa: sin ella no hay pantalla que ver.' },
{ key: 'agendar', label: 'Agendar cita', nota: 'Cita normal, cita rápida, lista de espera y cierres de agenda.' },
{ key: 'editar', label: 'Editar citas', nota: 'Reprogramar, mover de horario y marcar confirmaciones.' },
{ key: 'eliminar', label: 'Eliminar citas', nota: 'Borrar una cita o cancelarla.' },
{ key: 'visitas', label: 'Generar visitas', nota: 'Check-in (En cabina) y todo el expediente del día.' },
];
export interface FrontendUser {
id: number;
login: string;
@@ -893,6 +904,10 @@ export interface FrontendUser {
last_login?: string | null;
active?: boolean;
allowed_menus?: string[] | null;
// Acciones de Agenda que el backend resolvió (lista efectiva del usuario).
agenda_perms?: AgendaPerm[] | null;
// Lista propia del usuario; null = hereda del rol. La usa solo el editor.
agenda_perms_custom?: AgendaPerm[] | null;
// Solo rol 'medico': hr.employee vinculado (null si no hay vínculo)
employee_id?: number | null;
employee_name?: string | null;
@@ -1305,7 +1320,7 @@ export const odooApi = {
return data;
},
async updateFrontendUser(id: number, payload: { name?: string; login?: string; role?: FrontendRole; active?: boolean; password?: string; must_change_password?: boolean; allowed_menus?: string[] | null }): Promise<{ status: string; user: FrontendUser; message?: string }> {
async updateFrontendUser(id: number, payload: { name?: string; login?: string; role?: FrontendRole; active?: boolean; password?: string; must_change_password?: boolean; allowed_menus?: string[] | null; agenda_perms?: AgendaPerm[] | null }): Promise<{ status: string; user: FrontendUser; message?: string }> {
const { data } = await axios.put(`${ODOO_BASE}/skeen/frontend/v1/auth/users/${id}`, payload);
return data;
},