215 lines
8.0 KiB
TypeScript
215 lines
8.0 KiB
TypeScript
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<string> {
|
|
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<string, Record<string, string | null>>,
|
|
rol: string | undefined,
|
|
): Record<string, Record<string, string | null>> {
|
|
const permitidos = camposPermitidos(rol)
|
|
const salida: Record<string, Record<string, string | null>> = {}
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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 }
|