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

@@ -1,7 +1,7 @@
import { jsPDF } from 'jspdf'
import autoTable from 'jspdf-autotable'
import type { AplicacionPago, Pago, PlanPago } from '../../types'
import { round2 } from '../cobranza'
import { round2, saldoPlan } from '../cobranza'
import { formatoFechaCorta, formatoFechaLarga, formatoMoneda } from '../formatters'
import type { DocumentoPdf } from './estadoCuenta'
@@ -12,11 +12,25 @@ export function folioRecibo(aplicacion: AplicacionPago): string {
return aplicacion.id.slice(0, 8).toUpperCase()
}
/**
* Saldos que imprime el recibo, tomados de los TOTALES del plan y del pago
* (suma de montos − suma de pagado), no del historial de `AplicacionPago`:
* el enganche/apartado histórico vive en `Pago.montoPagado` sin fila de
* aplicación, así que acumular solo aplicaciones daba saldos falsos
* (incluso negativos). El saldo nunca puede ser negativo porque un abono
* jamás excede el restante de su pago.
*/
export function saldosDelRecibo(plan: PlanPago, pago: Pago): { restantePago: number; saldoPlan: number } {
return {
restantePago: round2(pago.monto - pago.montoPagado),
saldoPlan: saldoPlan(plan),
}
}
/**
* Construye el recibo de pago de una aplicación (abono) ya registrada.
* El restante del pago y el saldo del plan se calculan acumulando las
* aplicaciones registradas hasta esa aplicación (inclusive), de modo que el
* recibo refleja los montos vigentes justo tras el abono.
* El restante del pago y el saldo del plan son los totales vigentes del plan
* (ver `saldosDelRecibo`).
*/
export function crearDocumento(
plan: PlanPago,
@@ -28,23 +42,7 @@ export function crearDocumento(
const doc = new jsPDF()
let y = MARGEN
// Aplicado acumulado (plan y pago) hasta la aplicación indicada, en orden de registro
let aplicadoPlan = 0
let aplicadoPago = 0
let encontrada = false
for (const p of plan.pagos) {
for (const a of p.aplicaciones) {
aplicadoPlan += a.monto
if (p.id === pago.id) aplicadoPago += a.monto
if (a.id === aplicacion.id) {
encontrada = true
break
}
}
if (encontrada) break
}
const saldoTrasAbono = round2(plan.engancheTotal - aplicadoPlan)
const restanteTrasAbono = round2(pago.monto - aplicadoPago)
const { restantePago, saldoPlan: saldo } = saldosDelRecibo(plan, pago)
// 1. Encabezado
doc.setFontSize(14)
@@ -72,8 +70,8 @@ export function crearDocumento(
['FECHA DE PAGO', formatoFechaCorta(aplicacion.fechaPago)],
['MONTO DEL ABONO', formatoMoneda(aplicacion.monto)],
['REFERENCIA', aplicacion.referencia || '—'],
['RESTANTE DEL PAGO', formatoMoneda(restanteTrasAbono)],
['SALDO DEL PLAN TRAS EL ABONO', formatoMoneda(saldoTrasAbono)],
['RESTANTE DEL PAGO', formatoMoneda(restantePago)],
['SALDO DEL PLAN (a la fecha de emisión)', formatoMoneda(saldo)],
],
theme: 'plain',
columnStyles: { 0: { fontStyle: 'bold', cellWidth: 70 }, 1: { halign: 'left' } },