feat(config): modulo Configuraciones admin + permisos por rol

- Tablas ConfiguracionSistema (7 configs) + RolConfiguracion (6 roles)
- Endpoints: config/sistema GET/PATCH, config/roles GET/PATCH,
  config/usuarios GET/PATCH, config/reset-roles POST, config/acciones GET
- modulosDeRol() async con fallback a MATRIZ estatica
- auth/me devuelve acciones[] segun rol del usuario
- saludMonday.estaDisponible() lee modo_local_forzado de DB (cache 30s)
- Landing: vista Configuraciones con 4 tabs (Modo, Roles, Usuarios, Sistema)
- Mi-dia: boton Descartar se deshabilita si !descartar_lead
- 630 API tests + 24 landing tests + 20 mi-dia tests verdes

Seed: npm run seed:config
This commit is contained in:
2026-09-06 09:46:19 +00:00
parent 50e6eca7da
commit b2634f97ab
35 changed files with 2572 additions and 33 deletions

View File

@@ -173,6 +173,7 @@ function FilaOportunidad({
onAfluencia,
onDescartar,
enviando,
acciones,
}: {
op: OportunidadMiDia
onEtapa: (etapa: string, fecha?: string) => void
@@ -180,6 +181,7 @@ function FilaOportunidad({
onAfluencia: () => void
onDescartar: () => void
enviando: boolean
acciones?: string[]
}) {
const [conCita, setConCita] = useState(false)
const [fechaCita, setFechaCita] = useState('')
@@ -264,7 +266,7 @@ function FilaOportunidad({
)}
<button
type="button"
disabled={enviando}
disabled={enviando || (acciones !== undefined && !acciones.includes('descartar_lead'))}
onClick={onDescartar}
className="rounded-lg border border-red-200 bg-white px-3 py-1.5 text-xs font-medium text-red-600 shadow-sm transition-all duration-100 ease-out hover:bg-red-50 disabled:opacity-50"
>
@@ -503,12 +505,14 @@ function FilaLead({
onConvertir,
onDescartar,
enviando,
acciones,
}: {
lead: LeadMiDia
onAccion: (estado: string, fecha?: string) => void
onConvertir: () => void
onDescartar: () => void
enviando: boolean
acciones?: string[]
}) {
return (
<li className="rounded-xl border border-[#E5E7EB] bg-white p-4 shadow-sm">
@@ -564,7 +568,7 @@ function FilaLead({
</button>
<button
type="button"
disabled={enviando}
disabled={enviando || (acciones !== undefined && !acciones.includes("descartar_lead"))}
onClick={onDescartar}
className="rounded-lg border border-red-200 bg-white px-3 py-1.5 text-xs font-medium text-red-600 shadow-sm transition-all duration-100 ease-out hover:bg-red-50 disabled:opacity-50"
>
@@ -578,6 +582,7 @@ function FilaLead({
export default function MiDiaPage({ accionesSesion }: { accionesSesion: AccionesSesion }) {
const nombreVendedor = getSesion()?.usuario.nombre ?? ''
const rol = getSesion()?.usuario.rol ?? 'vendedor'
const acciones = getSesion()?.usuario.acciones ?? []
const [resumen, setResumen] = useState<ResumenMiDia | null>(null)
const [error, setError] = useState<string | null>(null)
const [cargando, setCargando] = useState(true)
@@ -836,6 +841,7 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
onAccion={(estado, fecha) => void ejecutar(() => cambiarEstadoLead(lead.id, estado, fecha, lead.fuente), 'No se pudo actualizar el lead')}
onConvertir={() => void convertir(lead)}
onDescartar={() => setDescartando(lead)}
acciones={acciones}
/>
))}
</ul>
@@ -859,6 +865,7 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
window.location.href = urlAfluenciaParaOportunidad(op)
}}
onDescartar={() => setDescartando({ id: op.id, nombre: op.nombre, fuente: 'oportunidad' })}
acciones={acciones}
/>
))}
</ul>

View File

@@ -6,6 +6,7 @@ export interface UsuarioSesion {
nombre: string
rol: string
asignacion: string | null
acciones?: string[]
}
export interface Sesion {