feat(pendientes): cotizador (mensualidades, bloqueo de unidad, totales, numMensualidades), cobranza (recibos, atrasos, mora configurable, recordatorios), soporte (adjuntos, filtros, estados), clientes maestro (catalogos reales), contactos (ficha y duplicados) y permisos de captura por rol
This commit is contained in:
@@ -1,10 +1,52 @@
|
||||
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 } from '../config/campos-departamento.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 ────────────────────────────
|
||||
|
||||
/**
|
||||
@@ -44,8 +86,15 @@ const listarSchema = z.object({
|
||||
|
||||
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(capturas)
|
||||
res.json(filtrarPorRol(capturas, rol))
|
||||
}
|
||||
|
||||
// ── GET /api/captura/unidades?desarrollo=X ───────────────────────────
|
||||
@@ -69,7 +118,8 @@ const resumenSchema = z.object({
|
||||
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)
|
||||
const rol = req.usuarioToken?.rol
|
||||
res.json(resumen.filter((r) => rolPuedeDepartamento(rol, r.departamento)))
|
||||
}
|
||||
|
||||
// ── POST /api/captura — guardar campo individual ─────────────────────
|
||||
@@ -89,6 +139,7 @@ export async function guardarEndpoint(req: Request, res: Response): Promise<void
|
||||
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(
|
||||
@@ -124,6 +175,7 @@ export async function bulkEndpoint(req: Request, res: Response): Promise<void> {
|
||||
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 })),
|
||||
|
||||
Reference in New Issue
Block a user