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:
2026-09-21 08:34:53 +00:00
parent b5a20aed4e
commit 2fc80e7f9e
66 changed files with 3792 additions and 436 deletions

View File

@@ -17,8 +17,10 @@
* - Pagos programados: numPagos mensualidades desde MES INICIO
* (montoPagado=0; el módulo deriva VENCIDO por fecha).
* - Celda de grid con valor > 0 = abono real del mes → AplicacionPago
* (fechaPago = mes de la columna) sobre la mensualidad de ese mes;
* si no hay mensualidad programada en ese mes → Pago standalone pagado.
* (fechaPago = mes de la columna) sobre la mensualidad de ese mes; si no
* hay mensualidad programada en ese mes, o el abono excede lo que falta de
* la mensualidad, el excedente se registra como Pago standalone ya cubierto
* (así ningún abono supera el monto de su pago).
*/
import { PrismaClient } from '@prisma/client'
@@ -57,6 +59,7 @@ function norm(h) {
}
const toStr = (v) => (v == null ? null : String(v).trim() || null)
const round2 = (x) => Math.round(x * 100) / 100
const toFloat = (v) => {
if (v == null || v === '') return null
if (typeof v === 'number') return v
@@ -230,23 +233,27 @@ async function escribir(planesLeidos) {
}
plan.engancheTotal = pagosProg.reduce((a, p) => a + p.monto, 0)
// 2) abonos del grid sobre la mensualidad del mismo mes; sobrantes → standalone
// 2) abonos del grid sobre la mensualidad del mismo mes. Un abono nunca
// puede exceder el monto de su pago: lo que sobra (y los meses sin
// mensualidad programada) se registra como pago nuevo ya cubierto, así
// Σ montoPagado == dinero recibido y ningún pago queda sobrepagado.
const aplicaciones = [] // { idxPago, fechaPago, monto }
const standalone = []
for (const ab of abonosGrid) {
for (const ab of [...abonosGrid].sort((a, b) => a.fecha - b.fecha)) {
// idx del arreglo == numPago (el inicial ocupa numPago 0 en el índice 0)
const idx = pagosProg.findIndex((p) => p.numPago >= 1 && claveMes(p.fechaVencimiento) === claveMes(ab.fecha))
let restante = ab.monto
if (idx !== -1) {
aplicaciones.push({ idx, fechaPago: ab.fecha, monto: ab.monto })
} else {
standalone.push({ fecha: ab.fecha, monto: ab.monto })
const p = pagosProg[idx]
const falta = Math.max(0, round2(p.monto - p.montoPagado))
const aplica = Math.min(restante, falta)
if (aplica > 0) {
aplicaciones.push({ idx, fechaPago: ab.fecha, monto: round2(aplica) })
p.montoPagado = round2(p.montoPagado + aplica)
restante = round2(restante - aplica)
}
}
}
// aplicar abonos al montoPagado (sin pasar del monto programado)
for (const ap of aplicaciones) {
const p = pagosProg[ap.idx]
p.montoPagado = Math.min(p.monto, p.montoPagado + ap.monto)
if (restante > 0) standalone.push({ fecha: ab.fecha, monto: round2(restante) })
}
// Invariante del módulo: Σ montos de TODOS los pagos del plan == engancheTotal