Files
FRASA-CAS/apps/landing/src/components/config/RolesPermisos.tsx
Consult AS Technology 7d19e0da38 fix(landing): dark mode completo en todos los componentes
14 archivos corregidos:
- Body: dark:bg-[#0F172A] dark:text-[#E2E8F0]
- Navbar: badges, botones, hamburger
- ConfiguracionCuenta: botones, textos
- VistaContactos: botones, filas hover
- ModalContacto: borders, secciones, badges, items
- ConfigPage: botones
- ModoTrabajo: botón reconexión
- RolesPermisos: botones, headers tabla
- Usuarios: empty state
- Sistema: labels, toggles
- AuditoriaLog: todo (tabla, filtros, paginación)
- CommandPalette: botón cerrar

28 tests landing verdes.
2026-09-07 06:57:12 +00:00

241 lines
7.8 KiB
TypeScript

import { useEffect, useState, useCallback } from 'react'
import {
getRolesConfig,
patchRolConfig,
resetRoles,
type RolConfig,
} from '../../services/config'
const MODULOS_DISPONIBLES = [
'mi-dia',
'cotizador',
'alta-cliente',
'alta-afluencia',
'cobranza',
'chat',
'mi-tablero',
'tablero-comercial',
'contactos',
]
const ACCIONES_DISPONIBLES = [
'descartar_lead',
'editar_cotizacion',
'asignar_vendedor',
'ver_reportes',
'exportar_pdf',
'ver_precios',
]
/** Descripción amigable de cada acción */
const ACCION_LABELS: Record<string, string> = {
descartar_lead: 'Descartar leads',
editar_cotizacion: 'Editar cotizaciones',
asignar_vendedor: 'Asignar vendedor',
ver_reportes: 'Ver reportes',
exportar_pdf: 'Exportar PDF',
ver_precios: 'Ver precios',
}
/** Descripción amigable de cada módulo */
const MODULO_LABELS: Record<string, string> = {
'mi-dia': 'Mi día',
cotizador: 'Cotizador',
'alta-cliente': 'Alta cliente',
'alta-afluencia': 'Alta afluencia',
cobranza: 'Cobranza',
chat: 'Chat IA',
'mi-tablero': 'Mi tablero',
'tablero-comercial': 'Tablero comercial',
contactos: 'Contactos',
}
interface RolEstado {
modulos: string[]
acciones: Record<string, boolean>
}
export function RolesPermisos() {
const [roles, setRoles] = useState<RolConfig[]>([])
const [edicion, setEdicion] = useState<Record<string, RolEstado>>({})
const [cargando, setCargando] = useState(true)
const [guardando, setGuardando] = useState(false)
const [aviso, setAviso] = useState('')
const [error, setError] = useState('')
const cargar = useCallback(async () => {
setCargando(true)
setError('')
try {
const data = await getRolesConfig()
setRoles(data)
// Construir estado de edición
const estado: Record<string, RolEstado> = {}
for (const r of data) {
estado[r.rol] = {
modulos: Array.isArray(r.modulos) ? (r.modulos as string[]) : JSON.parse(r.modulos as string) as string[],
acciones: r.acciones && typeof r.acciones === 'object' ? (r.acciones as Record<string, boolean>) : JSON.parse(r.acciones as string) as Record<string, boolean>,
}
}
setEdicion(estado)
} catch {
setError('No se pudieron cargar los roles.')
} finally {
setCargando(false)
}
}, [])
useEffect(() => { void cargar() }, [cargar])
const toggleModulo = (rol: string, modulo: string) => {
setEdicion((prev) => {
const actual = prev[rol]
if (!actual) return prev
const modulos = actual.modulos.includes(modulo)
? actual.modulos.filter((m) => m !== modulo)
: [...actual.modulos, modulo]
return { ...prev, [rol]: { ...actual, modulos } }
})
}
const toggleAccion = (rol: string, accion: string) => {
setEdicion((prev) => {
const actual = prev[rol]
if (!actual) return prev
const acciones = { ...actual.acciones, [accion]: !actual.acciones[accion] }
return { ...prev, [rol]: { ...actual, acciones } }
})
}
const guardarTodo = async () => {
setGuardando(true)
setAviso('')
setError('')
try {
await Promise.all(
Object.entries(edicion).map(([rol, estado]) =>
patchRolConfig(rol, {
modulos: estado.modulos,
acciones: estado.acciones,
}),
),
)
setAviso('Todos los roles se actualizaron correctamente.')
} catch {
setError('No se pudieron guardar los cambios. Intenta de nuevo.')
} finally {
setGuardando(false)
}
}
const restaurar = async () => {
if (!window.confirm('¿Restaurar todos los roles a sus valores por defecto?')) return
setGuardando(true)
setAviso('')
setError('')
try {
await resetRoles()
await cargar()
setAviso('Roles restaurados a valores por defecto.')
} catch {
setError('No se pudieron restaurar los roles.')
} finally {
setGuardando(false)
}
}
if (cargando) {
return (
<div className="rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-sm dark:border-[#1E293B] dark:bg-[#1E293B]">
<p className="text-sm text-[#64748B] dark:text-[#94A3B8]">Cargando configuración de roles…</p>
</div>
)
}
return (
<div className="space-y-4">
{/* Botones de acción */}
<div className="flex flex-wrap gap-3">
<button
type="button"
onClick={() => void guardarTodo()}
disabled={guardando}
className="rounded-lg bg-[#2563EB] px-4 py-2 text-sm font-medium text-white shadow-sm transition-all duration-100 ease-out hover:bg-[#1D4ED8] disabled:opacity-50"
>
{guardando ? 'Guardando…' : 'Guardar cambios'}
</button>
<button
type="button"
onClick={() => void restaurar()}
disabled={guardando}
className="rounded-lg border border-[#D1D5DB] bg-white px-4 py-2 text-sm font-medium text-[#374151] shadow-sm transition-all duration-100 ease-out hover:bg-[#F9FAFB] disabled:opacity-50 dark:bg-[#0F172A] dark:border-[#334155] dark:text-[#CBD5E1] dark:hover:bg-[#334155]"
>
Restaurar valores por defecto
</button>
</div>
{/* Feedback */}
{aviso && (
<p role="status" className="text-sm font-medium text-green-600">{aviso}</p>
)}
{error && (
<p role="alert" className="text-sm text-red-600">{error}</p>
)}
{/* Tabla de roles */}
<div className="overflow-x-auto rounded-xl border border-[#E5E7EB] bg-white shadow-sm dark:border-[#1E293B] dark:bg-[#1E293B]">
<table className="w-full text-left text-sm">
<thead>
<tr className="border-b border-[#E5E7EB] bg-[#F9FAFB] dark:border-[#334155] dark:bg-[#1E293B]">
<th className="px-4 py-3 font-medium text-[#111827] dark:text-[#E2E8F0]">Rol</th>
{MODULOS_DISPONIBLES.map((m) => (
<th key={m} className="px-3 py-3 text-center text-xs font-medium text-[#64748B] dark:text-[#94A3B8]">
{MODULO_LABELS[m] ?? m}
</th>
))}
{ACCIONES_DISPONIBLES.map((a) => (
<th key={a} className="px-3 py-3 text-center text-xs font-medium text-[#64748B] dark:text-[#94A3B8]">
{ACCION_LABELS[a] ?? a}
</th>
))}
</tr>
</thead>
<tbody>
{roles.map((r) => {
const estado = edicion[r.rol]
if (!estado) return null
return (
<tr key={r.rol} className="border-b border-[#E5E7EB] dark:border-[#334155] last:border-0">
<td className="whitespace-nowrap px-4 py-3 font-medium text-[#111827] dark:text-[#E2E8F0]">
{r.rol}
</td>
{MODULOS_DISPONIBLES.map((m) => (
<td key={m} className="px-3 py-3 text-center">
<input
type="checkbox"
checked={estado.modulos.includes(m)}
onChange={() => toggleModulo(r.rol, m)}
className="h-4 w-4 rounded border-[#D1D5DB] text-accent focus:ring-accent/20"
/>
</td>
))}
{ACCIONES_DISPONIBLES.map((a) => (
<td key={a} className="px-3 py-3 text-center">
<input
type="checkbox"
checked={!!estado.acciones[a]}
onChange={() => toggleAccion(r.rol, a)}
className="h-4 w-4 rounded border-[#D1D5DB] text-accent focus:ring-accent/20"
/>
</td>
))}
</tr>
)
})}
</tbody>
</table>
</div>
</div>
)
}