import type { NextFunction, Request, Response } from 'express' import { z } from 'zod' import * as capturaService from '../services/captura-departamento.service.js' import { CAMPOS_POR_DEPARTAMENTO, DEPARTAMENTOS, departamentosDeRol, rolPuedeDepartamento, } from '../config/campos-departamento.js' import { AppError, BadRequestError } from '../utils/errors.js' import { esAdminOManager } from '../utils/roles.js' // ── Permisos por rol ───────────────────────────────────────────────── /** * El rol debe tener permitido el departamento (matriz DEPARTAMENTOS_POR_ROL): * un vendedor sólo captura VENTAS aunque el front la oculte; sin esta guarda * un token de vendedor podía escribir en OBRA o FINANZAS. */ function requireDepartamentoPermitido(rol: string | undefined, departamento: string): void { if (!rolPuedeDepartamento(rol, departamento)) { throw new AppError(403, `Tu rol no tiene acceso al departamento ${departamento}`) } } /** Campos visibles para el rol (unión de los campos de sus departamentos) */ function camposPermitidos(rol: string | undefined): Set { const campos = departamentosDeRol(rol).flatMap((d) => (CAMPOS_POR_DEPARTAMENTO[d] ?? []).map((c) => c.campo)) return new Set(campos) } /** * La lectura también respeta la matriz: con un token de rol limitado sólo se * devuelven los campos de sus departamentos (el front ya filtra las pestañas, * esto evita que los datos lleguen al cliente). */ function filtrarPorRol( datos: Record>, rol: string | undefined, ): Record> { const permitidos = camposPermitidos(rol) const salida: Record> = {} for (const [unidad, campos] of Object.entries(datos)) { const visibles = Object.fromEntries(Object.entries(campos).filter(([campo]) => permitidos.has(campo))) if (Object.keys(visibles).length > 0) salida[unidad] = visibles } return salida } // ── Validación de campos por departamento ──────────────────────────── /** * Cada campo debe pertenecer al departamento que se está guardando: si el * cliente manda un campo de otro departamento (p. ej. una captura de Obra * enviada como Ventas) se rechaza en vez de crear una fila basura. */ function validarCamposDelDepartamento( departamento: string, registros: { unidad: string; campo: string; valor: string | null }[], ): void { const permitidos = new Set((CAMPOS_POR_DEPARTAMENTO[departamento] ?? []).map((c) => c.campo)) const invalidos = registros.filter((r) => !permitidos.has(r.campo)) if (invalidos.length > 0) { const lista = invalidos.slice(0, 3).map((r) => `${r.campo} (unidad ${r.unidad})`).join(', ') throw new BadRequestError( `El campo no pertenece al departamento ${departamento}: ${lista}${invalidos.length > 3 ? '…' : ''}`, ) } } // ── Middleware admin ───────────────────────────────────────────────── function requireAdmin(req: Request, _res: Response, next: NextFunction): void { if (!esAdminOManager(req.usuarioToken?.rol)) { throw new AppError(403, 'Se requiere rol admin o manager') } next() } // ── GET /api/captura?desarrollo=X&departamento=Y ───────────────────── const listarSchema = z.object({ desarrollo: z.string().min(1, 'Falta desarrollo'), departamento: z.string().optional(), }) export async function listarEndpoint(req: Request, res: Response): Promise { const query = listarSchema.parse(req.query) const rol = req.usuarioToken?.rol if (query.departamento !== undefined) { if (!DEPARTAMENTOS.includes(query.departamento as (typeof DEPARTAMENTOS)[number])) { throw new BadRequestError(`Departamento inválido: ${query.departamento}`) } requireDepartamentoPermitido(rol, query.departamento) } const capturas = await capturaService.obtenerCapturas(query.desarrollo, query.departamento) res.json(filtrarPorRol(capturas, rol)) } // ── GET /api/captura/unidades?desarrollo=X ─────────────────────────── const unidadesSchema = z.object({ desarrollo: z.string().min(1, 'Falta desarrollo'), }) export async function unidadesEndpoint(req: Request, res: Response): Promise { const query = unidadesSchema.parse(req.query) const unidades = await capturaService.obtenerUnidades(query.desarrollo) res.json(unidades) } // ── GET /api/captura/resumen?desarrollo=X ──────────────────────────── const resumenSchema = z.object({ desarrollo: z.string().min(1, 'Falta desarrollo'), }) export async function resumenEndpoint(req: Request, res: Response): Promise { const query = resumenSchema.parse(req.query) const resumen = await capturaService.resumenAvance(query.desarrollo) const rol = req.usuarioToken?.rol res.json(resumen.filter((r) => rolPuedeDepartamento(rol, r.departamento))) } // ── POST /api/captura — guardar campo individual ───────────────────── const guardarSchema = z.object({ desarrollo: z.string().min(1), unidadCodigo: z.string().min(1), departamento: z.string().min(1), campo: z.string().min(1), valor: z.string().nullable().optional(), }) export async function guardarEndpoint(req: Request, res: Response): Promise { const body = guardarSchema.parse(req.body) const usuarioId = req.usuarioToken?.sub ?? 'desconocido' if (!DEPARTAMENTOS.includes(body.departamento as typeof DEPARTAMENTOS[number])) { throw new BadRequestError(`Departamento inválido: ${body.departamento}`) } requireDepartamentoPermitido(req.usuarioToken?.rol, body.departamento) validarCamposDelDepartamento(body.departamento, [{ unidad: body.unidadCodigo, campo: body.campo, valor: body.valor ?? null }]) await capturaService.guardarCampo( body.desarrollo, body.unidadCodigo, body.departamento, body.campo, body.valor ?? null, usuarioId, req.usuarioToken?.rol ?? '', ) res.status(204).end() } // ── POST /api/captura/bulk — guardar múltiple ──────────────────────── const bulkSchema = z.object({ desarrollo: z.string().min(1), departamento: z.string().min(1), registros: z.array( z.object({ unidad: z.string().min(1), campo: z.string().min(1), valor: z.string().nullable().optional(), }), ), }) export async function bulkEndpoint(req: Request, res: Response): Promise { const body = bulkSchema.parse(req.body) const usuarioId = req.usuarioToken?.sub ?? 'desconocido' if (!DEPARTAMENTOS.includes(body.departamento as typeof DEPARTAMENTOS[number])) { throw new BadRequestError(`Departamento inválido: ${body.departamento}`) } requireDepartamentoPermitido(req.usuarioToken?.rol, body.departamento) validarCamposDelDepartamento( body.departamento, body.registros.map((r) => ({ unidad: r.unidad, campo: r.campo, valor: r.valor ?? null })), ) await capturaService.guardarBulk( body.desarrollo, body.departamento, body.registros.map((r) => ({ unidad: r.unidad, campo: r.campo, valor: r.valor ?? null })), usuarioId, req.usuarioToken?.rol ?? '', ) res.status(204).end() } // ── POST /api/captura/importar — importar desde Excel (admin only) ─── const importarSchema = z.object({ desarrollo: z.string().min(1), datos: z.array(z.array(z.any())), }) export async function importarEndpoint(req: Request, res: Response): Promise { const body = importarSchema.parse(req.body) const usuarioId = req.usuarioToken?.sub ?? 'desconocido' const resultado = await capturaService.importarDesdeExcel( body.desarrollo, body.datos as string[][], usuarioId, ) res.json(resultado) } // Exportar requireAdmin para usarlo como middleware en routes export { requireAdmin as requireAdminCaptura }