Cargando permisos...
+ }
+
+ if (errorCatalogo || errorAsignados) {
+ return (
+ No hay permisos registrados en el sistema.
+ }
+
+ const grupos = groupByModulo(catalogo)
+
+ function toggle(codigo: string) {
+ setSelected((prev) => {
+ const next = new Set(prev)
+ if (next.has(codigo)) next.delete(codigo)
+ else next.add(codigo)
+ return next
+ })
+ setSaved(false)
+ }
+
+ function handleSave() {
+ assignMut.mutate(
+ { rolCodigo, payload: { codigos: Array.from(selected) } },
+ {
+ onSuccess: () => setSaved(true),
+ },
+ )
+ }
+
+ const saveErrMsg = assignMut.error
+ ? isAxiosError(assignMut.error)
+ ? assignMut.error.response?.status === 400
+ ? 'El rol "admin" no puede quedar sin permisos.'
+ : (assignMut.error.response?.data as { message?: string } | undefined)?.message ??
+ 'No se pudieron guardar los cambios.'
+ : 'No se pudieron guardar los cambios.'
+ : null
+
+ return (
+
+ {saveErrMsg && (
+
+
+ {saveErrMsg}
+
+ )}
+
+ {saved && (
+
+
+ Permisos guardados correctamente.
+
+ )}
+
+ {Array.from(grupos.entries()).map(([modulo, permisos]) => (
+
+ ))}
+
+
+
+ {assignMut.isPending ? 'Guardando...' : 'Guardar cambios'}
+
+
+
+ )
+}
diff --git a/src/web/src/features/permisos/hooks/useAssignPermisos.ts b/src/web/src/features/permisos/hooks/useAssignPermisos.ts
new file mode 100644
index 0000000..48ad6cc
--- /dev/null
+++ b/src/web/src/features/permisos/hooks/useAssignPermisos.ts
@@ -0,0 +1,23 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query'
+import { assignPermisos } from '../api/assignPermisos'
+import { permisosQueryKey } from './usePermisos'
+import { rolPermisosQueryKey } from './useRolPermisos'
+import type { AssignPermisosRequest } from '../api/types'
+
+interface AssignPermisosVariables {
+ rolCodigo: string
+ payload: AssignPermisosRequest
+}
+
+export function useAssignPermisos() {
+ const queryClient = useQueryClient()
+
+ return useMutation({
+ mutationFn: ({ rolCodigo, payload }: AssignPermisosVariables) =>
+ assignPermisos(rolCodigo, payload),
+ onSuccess: (_data, { rolCodigo }) => {
+ void queryClient.invalidateQueries({ queryKey: permisosQueryKey })
+ void queryClient.invalidateQueries({ queryKey: rolPermisosQueryKey(rolCodigo) })
+ },
+ })
+}
diff --git a/src/web/src/features/permisos/hooks/usePermisos.ts b/src/web/src/features/permisos/hooks/usePermisos.ts
new file mode 100644
index 0000000..d3afcd8
--- /dev/null
+++ b/src/web/src/features/permisos/hooks/usePermisos.ts
@@ -0,0 +1,12 @@
+import { useQuery } from '@tanstack/react-query'
+import { listPermisos } from '../api/listPermisos'
+
+export const permisosQueryKey = ['permisos'] as const
+
+export function usePermisos() {
+ return useQuery({
+ queryKey: permisosQueryKey,
+ queryFn: listPermisos,
+ staleTime: 60_000,
+ })
+}
diff --git a/src/web/src/features/permisos/hooks/useRolPermisos.ts b/src/web/src/features/permisos/hooks/useRolPermisos.ts
new file mode 100644
index 0000000..3fcb986
--- /dev/null
+++ b/src/web/src/features/permisos/hooks/useRolPermisos.ts
@@ -0,0 +1,15 @@
+import { useQuery } from '@tanstack/react-query'
+import { getRolPermisos } from '../api/getRolPermisos'
+
+export function rolPermisosQueryKey(rolCodigo: string) {
+ return ['permisos', 'rol', rolCodigo] as const
+}
+
+export function useRolPermisos(rolCodigo: string | null) {
+ return useQuery({
+ queryKey: rolPermisosQueryKey(rolCodigo ?? ''),
+ queryFn: () => getRolPermisos(rolCodigo!),
+ enabled: rolCodigo !== null && rolCodigo.length > 0,
+ staleTime: 30_000,
+ })
+}
diff --git a/src/web/src/features/permisos/pages/RolPermisosPage.tsx b/src/web/src/features/permisos/pages/RolPermisosPage.tsx
new file mode 100644
index 0000000..19dd823
--- /dev/null
+++ b/src/web/src/features/permisos/pages/RolPermisosPage.tsx
@@ -0,0 +1,77 @@
+import { useState } from 'react'
+import { useNavigate } from 'react-router-dom'
+import { useAuthStore } from '@/stores/authStore'
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from '@/components/ui/card'
+import { useRoles } from '../../roles/hooks/useRoles'
+import { RolPermisosEditor } from '../components/RolPermisosEditor'
+
+export function RolPermisosPage() {
+ const navigate = useNavigate()
+ const user = useAuthStore((s) => s.user)
+ const [selectedRol, setSelectedRol] = useState