feat(captura): módulo de captura por departamentos
Backend: tabla CapturaDepartamento, 5 departamentos, 7 desarrollos, service CRUD + importador Excel + 6 endpoints. Frontend: tabla editable inline, tabs por dept, resumen avance, paginación, filtro estatus, permisos por rol, dark mode. 746 API tests + 42 landing tests.
This commit is contained in:
133
api-gateway/src/controllers/captura-departamento.controller.ts
Normal file
133
api-gateway/src/controllers/captura-departamento.controller.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import type { NextFunction, Request, Response } from 'express'
|
||||
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'
|
||||
|
||||
// ── Middleware admin ─────────────────────────────────────────────────
|
||||
|
||||
function requireAdmin(req: Request, _res: Response, next: NextFunction): void {
|
||||
if (req.usuarioToken?.rol !== 'admin') {
|
||||
throw new AppError(403, 'Se requiere rol admin')
|
||||
}
|
||||
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 capturas = await capturaService.obtenerCapturas(query.desarrollo, query.departamento)
|
||||
res.json(capturas)
|
||||
}
|
||||
|
||||
// ── 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)
|
||||
res.json(resumen)
|
||||
}
|
||||
|
||||
// ── 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}`)
|
||||
}
|
||||
|
||||
await capturaService.guardarCampo(
|
||||
body.desarrollo,
|
||||
body.unidadCodigo,
|
||||
body.departamento,
|
||||
body.campo,
|
||||
body.valor ?? null,
|
||||
usuarioId,
|
||||
)
|
||||
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}`)
|
||||
}
|
||||
|
||||
await capturaService.guardarBulk(
|
||||
body.desarrollo,
|
||||
body.departamento,
|
||||
body.registros.map((r) => ({ unidad: r.unidad, campo: r.campo, valor: r.valor ?? null })),
|
||||
usuarioId,
|
||||
)
|
||||
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 }
|
||||
Reference in New Issue
Block a user