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.
241 lines
7.8 KiB
TypeScript
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>
|
|
)
|
|
}
|