feat(clientes-maestro): importar y exportar clientes del archivo maestro

- Tabla ClienteMaestro (27 campos, unique numCliente)
- Importador Excel: 1535 clientes del Reporte maestro
- API: listar/filtros/exportar/detalle/importar
- Frontend: tabla paginada, filtros, exportar Excel
- Dark mode completo, 52 tests verdes
This commit is contained in:
2026-09-07 09:11:09 +00:00
parent c2a8f4e929
commit 9021733455
18 changed files with 1428 additions and 10 deletions

View File

@@ -0,0 +1,134 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { Request, Response } from 'express'
// ---------- Servicio mockeado ----------
const servicioMock = vi.hoisted(() => ({
listar: vi.fn(),
obtener: vi.fn(),
contar: vi.fn(),
exportar: vi.fn(),
importarDesdeExcel: vi.fn(),
}))
vi.mock('../services/cliente-maestro.service.js', () => servicioMock)
import {
listarEndpoint,
exportarEndpoint,
obtenerEndpoint,
importarEndpoint,
} from './cliente-maestro.controller.js'
function reqFake(overrides: Partial<Request> = {}): Request {
return {
body: {},
params: {},
query: {},
usuarioToken: { sub: 'u-1', username: 'admin', rol: 'admin' },
...overrides,
} as Request
}
function resFake() {
const json = vi.fn()
const end = vi.fn()
const status = vi.fn().mockReturnValue({ json, end })
return { res: { json, status, end } as unknown as Response, status, json, end }
}
beforeEach(() => {
vi.clearAllMocks()
})
// ── GET /api/clientes-maestro ────────────────────────────────────────
describe('listarEndpoint', () => {
it('200 con datos paginados', async () => {
servicioMock.listar.mockResolvedValue({
datos: [{ id: '1', numCliente: 'C001' }],
total: 1,
pagina: 1,
limite: 50,
})
const { res, json } = resFake()
await listarEndpoint(reqFake(), res)
expect(json).toHaveBeenCalledWith({
datos: [{ id: '1', numCliente: 'C001' }],
total: 1,
pagina: 1,
limite: 50,
})
})
it('pasa filtros de query al servicio', async () => {
servicioMock.listar.mockResolvedValue({ datos: [], total: 0, pagina: 1, limite: 50 })
const { res } = resFake()
await listarEndpoint(
reqFake({ query: { busqueda: 'Juan', desarrollo: 'SENDA', estatus: 'VENDIDO', pagina: '2', limite: '25' } }),
res,
)
expect(servicioMock.listar).toHaveBeenCalledWith({
busqueda: 'Juan',
desarrollo: 'SENDA',
estatus: 'VENDIDO',
pagina: 2,
limite: 25,
})
})
})
// ── GET /api/clientes-maestro/exportar ───────────────────────────────
describe('exportarEndpoint', () => {
it('200 con array de datos', async () => {
servicioMock.exportar.mockResolvedValue([{ id: '1', numCliente: 'C001' }])
const { res, json } = resFake()
await exportarEndpoint(reqFake(), res)
expect(json).toHaveBeenCalledWith([{ id: '1', numCliente: 'C001' }])
})
})
// ── GET /api/clientes-maestro/:numCliente ────────────────────────────
describe('obtenerEndpoint', () => {
it('200 con el cliente', async () => {
servicioMock.obtener.mockResolvedValue({ id: '1', numCliente: 'C001', nombre: 'Juan' })
const { res, json } = resFake()
await obtenerEndpoint(reqFake({ params: { numCliente: 'C001' } }), res)
expect(json).toHaveBeenCalledWith({ id: '1', numCliente: 'C001', nombre: 'Juan' })
})
it('404 si no existe', async () => {
servicioMock.obtener.mockResolvedValue(null)
const { res } = resFake()
await expect(
obtenerEndpoint(reqFake({ params: { numCliente: 'NOPE' } }), res),
).rejects.toThrow('Cliente no encontrado')
})
it('400 si numCliente vacío', async () => {
const { res } = resFake()
await expect(obtenerEndpoint(reqFake({ params: { numCliente: '' } }), res)).rejects.toThrow('numCliente requerido')
})
})
// ── POST /api/clientes-maestro/importar ──────────────────────────────
describe('importarEndpoint', () => {
it('200 con resultado de importación', async () => {
servicioMock.importarDesdeExcel.mockResolvedValue({ insertados: 5, actualizados: 2, errores: 1 })
const { res, json } = resFake()
await importarEndpoint(reqFake({ body: [{ numCliente: 'C001', nombre: 'Juan' }] }), res)
expect(json).toHaveBeenCalledWith({ insertados: 5, actualizados: 2, errores: 1 })
})
it('400 si body no es array', async () => {
const { res } = resFake()
await expect(importarEndpoint(reqFake({ body: {} }), res)).rejects.toThrow('Se esperaba un array')
})
it('400 si array vacío', async () => {
const { res } = resFake()
await expect(importarEndpoint(reqFake({ body: [] }), res)).rejects.toThrow('Se esperaba un array')
})
})

View File

@@ -0,0 +1,54 @@
import type { Request, Response } from 'express'
import * as clienteMaestroService from '../services/cliente-maestro.service.js'
import { NotFoundError, BadRequestError } from '../utils/errors.js'
import { requireAdmin } from './config.controller.js'
// ── GET /api/clientes-maestro ────────────────────────────────────────
export async function listarEndpoint(req: Request, res: Response): Promise<void> {
const busqueda = (req.query.busqueda as string) ?? undefined
const desarrollo = (req.query.desarrollo as string) ?? undefined
const estatus = (req.query.estatus as string) ?? undefined
const pagina = req.query.pagina ? Number(req.query.pagina) : undefined
const limite = req.query.limite ? Number(req.query.limite) : undefined
const resultado = await clienteMaestroService.listar({ busqueda, desarrollo, estatus, pagina, limite })
res.json(resultado)
}
// ── GET /api/clientes-maestro/exportar ───────────────────────────────
export async function exportarEndpoint(req: Request, res: Response): Promise<void> {
const busqueda = (req.query.busqueda as string) ?? undefined
const desarrollo = (req.query.desarrollo as string) ?? undefined
const estatus = (req.query.estatus as string) ?? undefined
const datos = await clienteMaestroService.exportar({ busqueda, desarrollo, estatus })
res.json(datos)
}
// ── GET /api/clientes-maestro/:numCliente ────────────────────────────
export async function obtenerEndpoint(req: Request, res: Response): Promise<void> {
const numCliente = String(req.params['numCliente'] ?? '')
if (!numCliente) throw new BadRequestError('numCliente requerido')
const cliente = await clienteMaestroService.obtener(numCliente)
if (!cliente) throw new NotFoundError('Cliente no encontrado')
res.json(cliente)
}
// ── POST /api/clientes-maestro/importar ──────────────────────────────
export async function importarEndpoint(req: Request, res: Response): Promise<void> {
const datos = req.body
if (!Array.isArray(datos) || datos.length === 0) {
throw new BadRequestError('Se esperaba un array de clientes')
}
const resultado = await clienteMaestroService.importarDesdeExcel(datos)
res.json(resultado)
}
// Re-exportar requireAdmin para las rutas
export { requireAdmin }