feat: rol manager, arreglos de reportes (cancelacion/afluencias), credito unico, cliente enkontrol, vendedor en bitacora y actualizar afluencia existente

This commit is contained in:
2026-09-18 15:25:34 +00:00
parent 9630893974
commit 4965a4488a
35 changed files with 303 additions and 90 deletions

View File

@@ -5,6 +5,7 @@ import { z } from 'zod'
import { prisma } from '../services/prisma.js'
import { chat, indexarDocumento, type FuenteCitada } from '../services/ai.service.js'
import { AppError, NotFoundError } from '../utils/errors.js'
import { esAdminOManager } from '../utils/roles.js'
const chatSchema = z.object({
sessionId: z.string().uuid().nullish(),
@@ -68,7 +69,7 @@ export async function chatIAStream(req: Request, res: Response): Promise<void> {
export function filtroConversaciones(
token: { sub: string; rol: string } | undefined,
): { usuarioId: string } | undefined {
if (!token || token.rol === 'admin') return undefined
if (!token || esAdminOManager(token.rol)) return undefined
return { usuarioId: token.sub }
}
@@ -132,7 +133,7 @@ export async function eliminarConversacion(req: Request, res: Response): Promise
const conversacion = await prisma.aIConversacion.findUnique({ where: { id } })
if (!conversacion) throw new NotFoundError('Conversación no encontrada')
const token = req.usuarioToken
if (token && token.rol !== 'admin' && conversacion.usuarioId !== token.sub) {
if (token && !esAdminOManager(token.rol) && conversacion.usuarioId !== token.sub) {
throw new AppError(403, 'Solo puedes borrar tus conversaciones')
}
await prisma.aIConversacion.delete({ where: { id } }) // cascade: mensajes

View File

@@ -1,6 +1,7 @@
import type { Request, Response } from 'express'
import { z } from 'zod'
import { prisma } from '../services/prisma.js'
import { esAdminOManager } from '../utils/roles.js'
import { AppError, NotFoundError } from '../utils/errors.js'
// ---------- Borradores del alta de clientes ----------
@@ -15,7 +16,7 @@ const guardarBorradorSchema = z.object({
/** sub del JWT; con AUTH_ENABLED=false (tests) cae a un id fijo */
const usuarioDe = (req: Request): string => req.usuarioToken?.sub ?? 'anon'
const esAdmin = (req: Request): boolean => req.usuarioToken?.rol === 'admin'
const esAdmin = (req: Request): boolean => esAdminOManager(req.usuarioToken?.rol)
/** 404 si no existe, 403 si es de otro usuario (salvo admin) */
async function borradorPropio(req: Request, id: string) {

View File

@@ -3,12 +3,13 @@ import { z } from 'zod'
import * as capturaService from '../services/captura-departamento.service.js'
import { DEPARTAMENTOS } from '../config/campos-departamento.js'
import { AppError, BadRequestError } from '../utils/errors.js'
import { esAdminOManager } from '../utils/roles.js'
// ── Middleware admin ─────────────────────────────────────────────────
function requireAdmin(req: Request, _res: Response, next: NextFunction): void {
if (req.usuarioToken?.rol !== 'admin') {
throw new AppError(403, 'Se requiere rol admin')
if (!esAdminOManager(req.usuarioToken?.rol)) {
throw new AppError(403, 'Se requiere rol admin o manager')
}
next()
}

View File

@@ -1,20 +1,31 @@
/**
* Endpoints de administración de configuración.
*
* Protegidos con requireAdmin (rol === 'admin') excepto /acciones que
* cualquiera autenticado puede consultar.
* Protegidos con requireAdmin (admin o manager) excepto /acciones que
* cualquiera autenticado puede consultar. Las operaciones sensibles —crear
* usuarios y editar permisos de roles— usan requireAdminEstricto (solo admin):
* el rol manager no las puede hacer.
*/
import type { NextFunction, Request, Response } from 'express'
import { z } from 'zod'
import * as configService from '../services/config.service.js'
import { esRolValido } from '../config/roles.js'
import { esAdmin, esAdminOManager } from '../utils/roles.js'
import { AppError, BadRequestError } from '../utils/errors.js'
// ── Middleware admin ────────────────────────────────────────────────────
export function requireAdmin(req: Request, _res: Response, next: NextFunction): void {
if (req.usuarioToken?.rol !== 'admin') {
throw new AppError(403, 'Se requiere rol admin')
if (!esAdminOManager(req.usuarioToken?.rol)) {
throw new AppError(403, 'Se requiere rol admin o manager')
}
next()
}
/** Solo admin: crear usuarios y editar permisos de roles (manager NO) */
export function requireAdminEstricto(req: Request, _res: Response, next: NextFunction): void {
if (!esAdmin(req.usuarioToken?.rol)) {
throw new AppError(403, 'Solo un administrador puede hacer esta operación')
}
next()
}
@@ -86,6 +97,10 @@ export async function actualizarUsuarioEndpoint(req: Request, res: Response): Pr
const id = req.params.id as string
if (!id) throw new BadRequestError('Falta el parámetro :id')
const parsed = actualizarUsuarioSchema.parse(req.body)
// El rol es un permiso: solo el administrador puede cambiarlo (manager no)
if (parsed.rol !== undefined && !esAdmin(req.usuarioToken?.rol)) {
throw new AppError(403, 'Solo un administrador puede cambiar el rol de un usuario')
}
// Filtra undefined para enviar solo campos con valor
const body: { rol?: string; asignacion?: string | null; activo?: boolean } = {}
if (parsed.rol !== undefined) body.rol = parsed.rol

View File

@@ -20,7 +20,11 @@ export async function resumenMiDiaEndpoint(req: Request, res: Response): Promise
if (!usuario || !usuario.activo) throw new AppError(401, 'Credenciales inválidas')
// Capturas de afluencia: admin ve todas, otros solo las suyas
const esAdmin = usuario.rol === 'admin' || usuario.rol === 'direccion_general' || usuario.rol === 'director_ventas'
const esAdmin =
usuario.rol === 'admin' ||
usuario.rol === 'manager' ||
usuario.rol === 'direccion_general' ||
usuario.rol === 'director_ventas'
const capturas = await prisma.leadCaptura.findMany({
where: esAdmin ? {} : { usuarioId: usuario.id },
orderBy: { createdAt: 'desc' },

View File

@@ -16,7 +16,7 @@ import { VOCABULARIO } from '../config/vocabulario.js'
export function requireRolReportes(req: Request, _res: Response, next: NextFunction): void {
const rol = req.usuarioToken?.rol
if (rol !== 'admin' && rol !== 'director_ventas' && rol !== 'direccion_general') {
if (rol !== 'admin' && rol !== 'manager' && rol !== 'director_ventas' && rol !== 'direccion_general') {
throw new AppError(403, 'No tienes acceso a los reportes')
}
next()
@@ -145,8 +145,20 @@ export async function reporteAfluencias(req: Request, res: Response): Promise<vo
desarrollo: c.desarrollo ?? '',
modelo: c.modelo ?? '',
etapa: c.etapa,
califica: perfil['califica'] ?? '',
vendedor: nombrePorId.get(c.usuarioId) ?? '(sin asignar)',
// Perfilamiento completo capturado en el alta de afluencia
numDependientes: perfil['numDependientes'] ?? '',
edad: perfil['edad'] ?? '',
estadoCivil: perfil['estadoCivil'] ?? '',
tipoEmpleo: perfil['tipoEmpleo'] ?? '',
ocupacion: perfil['ocupacion'] ?? '',
rangoSalarial: perfil['rangoSalarial'] ?? '',
credito: perfil['credito'] ?? '',
tipoEconomia: perfil['tipoEconomia'] ?? '',
califica: perfil['califica'] ?? '',
loQuierePara: perfil['loQuierePara'] ?? '',
medioContacto: perfil['medioContacto'] ?? '',
notas: perfil['notas'] ?? '',
fecha: c.createdAt.toISOString(),
}
}),