feat+fix: 3 unidades SENDA faltantes + revisión a fondo de PDFs del alta

- SENDA-410-1-G102, SENDA-410-1-G201, SENDA-401-1-L41 agregadas a Monday,
  seeds y conocimiento IA (inventario 288 unidades; solo existían en el
  reporte de estatus de venta, todas VENDIDO)
- PDFs del alta verificados exhaustivamente (5 documentos × 4 variantes,
  contenido extraído con pdfplumber + renders visuales):
  - firma huérfana en página propia (PLD) corregida
  - encabezados de contacto huérfanos (checklist) corregidos
  - modelo duplicado y enums crudos en perfil corregidos
- Tests pdf 6→16; suites: alta 76, cotizador 71, api 168
This commit is contained in:
2026-08-28 05:23:19 +00:00
parent 9e04b76616
commit 26bf08e789
7 changed files with 341 additions and 25 deletions

View File

@@ -10,7 +10,7 @@ import {
REQUISITOS_CONTACTO_ENTREGA,
REQUISITOS_CONTACTO_CONTRATO,
} from '../../data/textosLegales'
import { MARGEN, finalY, parrafo, titulo, lineaFirma, numero, type DocumentoPdf } from './helpers'
import { MARGEN, finalY, parrafo, titulo, lineaFirma, numero, asegurarEspacio, type DocumentoPdf } from './helpers'
const vacio = (v: string | undefined | null): string => (v && v.trim() !== '' ? v : '—')
@@ -97,9 +97,12 @@ export function crearDocumento(alta: AltaCliente): DocumentoPdf {
})
y = finalY(doc) + 6
// Encabezado + bloque de contacto van juntos: si no caben, saltan a página nueva
y = asegurarEspacio(doc, y, 30)
y = parrafo(doc, y, 'ENTREGA DE DOCUMENTOS:', { negritas: true, tamano: 9 })
y = parrafo(doc, y, REQUISITOS_CONTACTO_ENTREGA)
y += 2
y = asegurarEspacio(doc, y, 30)
y = parrafo(doc, y, 'FIRMA DE CONTRATO:', { negritas: true, tamano: 9 })
y = parrafo(doc, y, REQUISITOS_CONTACTO_CONTRATO)
y += 12

View File

@@ -19,6 +19,15 @@ export interface OpcionesParrafo {
alinear?: 'left' | 'center' | 'right'
}
/** Si no caben `altoNecesario` mm antes del pie, salta de página. Devuelve la Y utilizable. */
export function asegurarEspacio(doc: jsPDF, y: number, altoNecesario: number): number {
if (y + altoNecesario > LIMITE_PAGINA) {
doc.addPage()
return MARGEN
}
return y
}
/** Escribe un párrafo con salto de página automático. Devuelve la nueva Y. */
export function parrafo(doc: jsPDF, y: number, texto: string, opciones: OpcionesParrafo = {}): number {
const tamano = opciones.tamano ?? 8

View File

@@ -39,7 +39,7 @@ export function crearDocumento(alta: AltaCliente): DocumentoPdf {
autoTable(doc, {
startY: y,
margin: { left: MARGEN, right: MARGEN },
styles: { fontSize: 7, cellPadding: 1 },
styles: { fontSize: 7, cellPadding: 0.7 },
headStyles: { fillColor: [4, 120, 87] },
head: [['1 - Nombre y apellidos del cliente']],
body: [[g.nombreCompleto]],
@@ -48,7 +48,7 @@ export function crearDocumento(alta: AltaCliente): DocumentoPdf {
autoTable(doc, {
startY: y,
margin: { left: MARGEN, right: MARGEN },
styles: { fontSize: 7, cellPadding: 1 },
styles: { fontSize: 7, cellPadding: 0.7 },
headStyles: { fillColor: [4, 120, 87] },
head: [['2 - Fecha de Nacimiento', '3 - Lugar de Nacimiento']],
body: [[formatoFechaCorta(g.fechaNacimiento), vacio(g.lugarNacimiento)]],
@@ -57,7 +57,7 @@ export function crearDocumento(alta: AltaCliente): DocumentoPdf {
autoTable(doc, {
startY: y,
margin: { left: MARGEN, right: MARGEN },
styles: { fontSize: 7, cellPadding: 1 },
styles: { fontSize: 7, cellPadding: 0.7 },
headStyles: { fillColor: [4, 120, 87] },
head: [['4 - Nacionalizado o Residente', '4 - País']],
body: [[vacio(g.nacionalidad), 'MEXICO']],
@@ -66,7 +66,7 @@ export function crearDocumento(alta: AltaCliente): DocumentoPdf {
autoTable(doc, {
startY: y,
margin: { left: MARGEN, right: MARGEN },
styles: { fontSize: 7, cellPadding: 1 },
styles: { fontSize: 7, cellPadding: 0.7 },
headStyles: { fillColor: [4, 120, 87] },
head: [['5 - Actividad, Ocupación o Profesión']],
body: [[vacio(g.actividadOcupacion)]],
@@ -76,7 +76,7 @@ export function crearDocumento(alta: AltaCliente): DocumentoPdf {
autoTable(doc, {
startY: y,
margin: { left: MARGEN, right: MARGEN },
styles: { fontSize: 7, cellPadding: 1 },
styles: { fontSize: 7, cellPadding: 0.7 },
headStyles: { fillColor: [4, 120, 87] },
head: [['6 - Domicilio Particular', '']],
body: [
@@ -90,7 +90,7 @@ export function crearDocumento(alta: AltaCliente): DocumentoPdf {
autoTable(doc, {
startY: y,
margin: { left: MARGEN, right: MARGEN },
styles: { fontSize: 7, cellPadding: 1 },
styles: { fontSize: 7, cellPadding: 0.7 },
headStyles: { fillColor: [4, 120, 87] },
head: [['6 - Domicilio Fiscal (Art 29 y 29A CFF)', '']],
body: [
@@ -105,7 +105,7 @@ export function crearDocumento(alta: AltaCliente): DocumentoPdf {
autoTable(doc, {
startY: y,
margin: { left: MARGEN, right: MARGEN },
styles: { fontSize: 7, cellPadding: 1 },
styles: { fontSize: 7, cellPadding: 0.7 },
headStyles: { fillColor: [4, 120, 87] },
head: [['7 - Número Telefónico Casa', '7 - Número Telefónico Celular']],
body: [[vacio(g.telCasa), vacio(g.celular)]],
@@ -114,7 +114,7 @@ export function crearDocumento(alta: AltaCliente): DocumentoPdf {
autoTable(doc, {
startY: y,
margin: { left: MARGEN, right: MARGEN },
styles: { fontSize: 7, cellPadding: 1 },
styles: { fontSize: 7, cellPadding: 0.7 },
headStyles: { fillColor: [4, 120, 87] },
head: [['8 - Correo Electrónico Válido', '9 - CURP']],
body: [[g.email, g.curp]],
@@ -123,7 +123,7 @@ export function crearDocumento(alta: AltaCliente): DocumentoPdf {
autoTable(doc, {
startY: y,
margin: { left: MARGEN, right: MARGEN },
styles: { fontSize: 7, cellPadding: 1 },
styles: { fontSize: 7, cellPadding: 0.7 },
headStyles: { fillColor: [4, 120, 87] },
head: [['10 - RFC (SAT)']],
body: [[g.rfc]],
@@ -132,7 +132,7 @@ export function crearDocumento(alta: AltaCliente): DocumentoPdf {
autoTable(doc, {
startY: y,
margin: { left: MARGEN, right: MARGEN },
styles: { fontSize: 7, cellPadding: 1 },
styles: { fontSize: 7, cellPadding: 0.7 },
headStyles: { fillColor: [4, 120, 87] },
head: [['11 - Datos de la Identificación Oficial Vigente', '']],
body: [
@@ -153,7 +153,7 @@ export function crearDocumento(alta: AltaCliente): DocumentoPdf {
y = parrafo(doc, y, PLD_TEXTO_BENEFICIARIO)
y = parrafo(doc, y, PLD_TEXTO_RESPONSABILIDAD)
y = parrafo(doc, y, PLD_TEXTO_OTRO_FORMATO)
y += 10
y += 6
y = lineaFirma(doc, y, MARGEN + 40, 100, `NOMBRE Y FIRMA DEL CLIENTE\n${g.nombreCompleto}`, firma.firmaDataUrl)
// Página 2: beneficiario controlador
@@ -166,7 +166,7 @@ export function crearDocumento(alta: AltaCliente): DocumentoPdf {
autoTable(doc, {
startY: y,
margin: { left: MARGEN, right: MARGEN },
styles: { fontSize: 7, cellPadding: 1 },
styles: { fontSize: 7, cellPadding: 0.7 },
headStyles: { fillColor: [4, 120, 87] },
head: [['Nombre', 'Nacionalidad']],
body: [
@@ -177,7 +177,7 @@ export function crearDocumento(alta: AltaCliente): DocumentoPdf {
autoTable(doc, {
startY: y,
margin: { left: MARGEN, right: MARGEN },
styles: { fontSize: 7, cellPadding: 1 },
styles: { fontSize: 7, cellPadding: 0.7 },
headStyles: { fillColor: [4, 120, 87] },
head: [['Fecha Nacimiento', 'Lugar Nacimiento']],
body: [[formatoFechaCorta(g.fechaNacimiento), vacio(g.lugarNacimiento)]],
@@ -186,7 +186,7 @@ export function crearDocumento(alta: AltaCliente): DocumentoPdf {
autoTable(doc, {
startY: y,
margin: { left: MARGEN, right: MARGEN },
styles: { fontSize: 7, cellPadding: 1 },
styles: { fontSize: 7, cellPadding: 0.7 },
headStyles: { fillColor: [4, 120, 87] },
head: [['Domicilio']],
body: [[`${vacio(g.direccion)} ${vacio(g.codigoPostal)} ${vacio(g.delegacionMunicipio)} BC MEXICO`.trim()]],
@@ -195,7 +195,7 @@ export function crearDocumento(alta: AltaCliente): DocumentoPdf {
autoTable(doc, {
startY: y,
margin: { left: MARGEN, right: MARGEN },
styles: { fontSize: 7, cellPadding: 1 },
styles: { fontSize: 7, cellPadding: 0.7 },
headStyles: { fillColor: [4, 120, 87] },
head: [['Teléfono Fijo', 'Teléfono Celular']],
body: [[vacio(g.telCasa), vacio(g.celular)]],
@@ -204,7 +204,7 @@ export function crearDocumento(alta: AltaCliente): DocumentoPdf {
autoTable(doc, {
startY: y,
margin: { left: MARGEN, right: MARGEN },
styles: { fontSize: 7, cellPadding: 1 },
styles: { fontSize: 7, cellPadding: 0.7 },
headStyles: { fillColor: [4, 120, 87] },
head: [['Correo Electrónico', 'Ocupación']],
body: [[g.email, vacio(g.actividadOcupacion)]],
@@ -213,7 +213,7 @@ export function crearDocumento(alta: AltaCliente): DocumentoPdf {
autoTable(doc, {
startY: y,
margin: { left: MARGEN, right: MARGEN },
styles: { fontSize: 7, cellPadding: 1 },
styles: { fontSize: 7, cellPadding: 0.7 },
headStyles: { fillColor: [4, 120, 87] },
head: [['C.U.R.P', 'R.F.C']],
body: [[g.curp, g.rfc]],

View File

@@ -1,4 +1,6 @@
import { describe, it, expect } from 'vitest'
import { jsPDF } from 'jspdf'
import type { AltaCliente } from '../../types'
import { fixtureAlta } from '../fixture'
import * as perfilCliente from './perfilCliente'
import * as identificacionCliente from './identificacionCliente'
@@ -31,3 +33,297 @@ describe('generadores de PDF (smoke test con fixture)', () => {
expect(checklistExpediente.crearDocumento(fixtureAlta).doc.getNumberOfPages()).toBeGreaterThanOrEqual(2)
})
})
// ---------------------------------------------------------------------------
// Tests de contenido: jsPDF genera los streams sin comprimir, así que el texto
// se puede extraer del output crudo (cadenas "(...) Tj" en cada página).
// ---------------------------------------------------------------------------
// Bytes 0x80-0x9F de WinAnsiEncoding que no coinciden con latin1
const WINANSI: Record<number, string> = {
0x80: '€', 0x85: '…', 0x88: 'ˆ', 0x8c: 'Œ', 0x91: '‘', 0x92: '’',
0x93: '“', 0x94: '”', 0x95: '•', 0x96: '–', 0x97: '—', 0x98: '˜',
0x99: '™', 0x9c: 'œ',
}
/** Decodifica una cadena literal del PDF: escapes octales y WinAnsi */
function decodificarCadenaPdf(cadena: string): string {
return cadena
.replace(/\\([0-7]{1,3})/g, (_, oct: string) => String.fromCharCode(Number.parseInt(oct, 8)))
.replace(/\\([\\()nrt])/g, (_, c: string) => ({ n: '\n', r: '\r', t: '\t' })[c as string] ?? c)
.replace(/[\x80-\x9f]/g, (c) => WINANSI[c.charCodeAt(0)] ?? c)
}
interface Captura {
/** Texto por página (en orden) */
paginas: string[]
/** Todo el texto del documento */
texto: string
/** Imágenes XObject embebidas (p. ej. la firma) */
numImagenes: number
}
/** Extrae el contenido textual del PDF generado, página por página. */
function capturar(doc: jsPDF): Captura {
const bin = new TextDecoder('latin1').decode(doc.output('arraybuffer'))
const streams = [...bin.matchAll(/stream\r?\n([\s\S]*?)\r?\nendstream/g)]
.map((m) => m[1])
.filter((s) => s.includes(' Tj'))
const paginas = streams.map((stream) => {
const partes: string[] = []
for (const m of stream.matchAll(/\((?:\\.|[^\\()])*\)/g)) {
partes.push(decodificarCadenaPdf(m[0].slice(1, -1)))
}
return partes.join(' ')
})
return {
paginas,
texto: paginas.join('\n'),
numImagenes: (bin.match(/\/Subtype\s*\/Image/g) ?? []).length,
}
}
const capturarAlta = (crear: (alta: AltaCliente) => { doc: jsPDF }, alta: AltaCliente): Captura =>
capturar(crear(alta).doc)
const PATRON_DEFECTO = /\bundefined\b|\bnull\b|\bNaN\b|\[object Object\]/
const clonar = (a: AltaCliente): AltaCliente => JSON.parse(JSON.stringify(a)) as AltaCliente
function conCoacreditado(): AltaCliente {
const a = clonar(fixtureAlta)
a.datosGenerales.nombreCompleto = 'MARÍA FERNANDA ÑÚÑEZ GÜEMES'
a.coacreditado.tieneCoacreditado = true
a.coacreditado.generales = {
...a.coacreditado.generales,
nombreCompleto: 'JOSÉ ÁNGEL MÜLLER IBARGÜENGOITIA',
rfc: 'MUIJ850101ABC',
curp: 'MUIJ850101HBCLRN09',
fechaNacimiento: '1985-01-01',
}
a.coacreditado.economicos = {
ingresosMensuales: 40000.25,
otrosIngresos: 500.5,
gastosLuz: 300.33,
gastosOtros: 1200.67,
dependientes: { personas: '2', parentesco: 'HIJOS', edad: '5 Y 8' },
}
a.referencias = [
{
nombre: 'REFERENCIA CON NOMBRE EXTREMADAMENTE LARGO PARA PROBAR ANCHO DE CELDA',
telefono: '6641887799',
parentesco: 'HERMANO',
direccion: 'PRIVADA DE LAS BUGAMBILIAS MANZANA 27 LOTE 114 COLONIA PLAYAS DE TIJUANA CÓDIGO POSTAL 22504',
},
]
return a
}
function minimos(): AltaCliente {
const a = clonar(fixtureAlta)
a.datosGenerales.telCasa = ''
a.empleo.ext = ''
a.viviendaCredito.enganche = ''
a.viviendaCredito.idVigencia = ''
a.viviendaCredito.proyecto = 'OTRO'
a.viviendaCredito.proyectoOtro = 'RESIDENCIAL LAS ÁGUILAS'
a.viviendaCredito.tiposCredito = []
a.referencias = []
a.economicos = {
ingresosMensuales: 0,
otrosIngresos: 0,
gastosLuz: 0,
gastosOtros: 0,
dependientes: { personas: '', parentesco: '', edad: '' },
}
a.firma = { firmaDataUrl: '', asesorNombre: '' }
return a
}
describe('contenido de los PDFs', () => {
it('ningún generador emite undefined/null/NaN/[object Object] (fixture ni casos borde)', () => {
for (const alta of [fixtureAlta, conCoacreditado(), minimos()]) {
for (const [, modulo] of generadores) {
expect(capturarAlta(modulo.crearDocumento, alta).texto).not.toMatch(PATRON_DEFECTO)
}
}
})
it('perfil: datos personales, empleo, económicos, referencias y desarrollo con formato moneda', () => {
const t = capturarAlta(perfilCliente.crearDocumento, fixtureAlta).texto
for (const frag of [
'PASCUAL MARTIN TELLEZ CHIGUINDO',
'TECP891124HBCLHS07',
'TECP8911246B4',
'24/11/1989',
'RIO EUFRATES EDIF 1 7145 203',
'FISCALIA GENERAL DE LA REPUBLICA',
'12 AÑOS 10 MESES',
'$54,988.00',
'$5,500.00',
'$2,700,000.00',
'JOSE ARNOLDO MENDOZA',
'CYNTHIA ANAHI GARCIA',
'CAÑON OASIS',
'409-1-11-11',
'LAUREL',
'Bancario',
'ESMERALDA BECERRA DOMINGUEZ',
]) {
expect(t).toContain(frag)
}
})
it('perfil: catálogos con etiqueta legible y modelo sin duplicar en PROYECTO', () => {
const t = capturarAlta(perfilCliente.crearDocumento, fixtureAlta).texto
for (const crudo of ['VIVIR_AHI', 'E_MEXICANA', 'ASALARIADO', 'SOCIEDAD_CONYUGAL']) {
expect(t).not.toContain(crudo)
}
for (const etiqueta of ['Vivir ahí', 'Economía mexicana', 'Asalariado', 'Propia']) {
expect(t).toContain(etiqueta)
}
expect(t).toContain('11 11 SENDA')
expect(t).not.toContain('11 11 SENDA LAUREL')
})
it('perfil: sección co-acreditado solo cuando tieneCoacreditado', () => {
const sin = capturarAlta(perfilCliente.crearDocumento, fixtureAlta).texto
expect(sin).not.toContain('CO-ACREDITADO')
const con = capturarAlta(perfilCliente.crearDocumento, conCoacreditado()).texto
for (const frag of [
'CO-ACREDITADO O CÓNYUGE',
'JOSÉ ÁNGEL MÜLLER IBARGÜENGOITIA',
'MUIJ850101HBCLRN09',
'MUIJ850101ABC',
'$40,500.75',
'$1,501.00',
'MARÍA FERNANDA ÑÚÑEZ GÜEMES',
]) {
expect(con).toContain(frag)
}
})
it('perfil: textos largos de referencias no se truncan', () => {
const t = capturarAlta(perfilCliente.crearDocumento, conCoacreditado()).texto
for (const palabra of ['EXTREMADAMENTE', 'ANCHO DE CELDA', 'BUGAMBILIAS', 'MANZANA 27', 'LOTE 114', '22504']) {
expect(t).toContain(palabra)
}
})
it('identificación PLD: campos, textos legales completos y firma no huérfana', () => {
const { doc } = identificacionCliente.crearDocumento(fixtureAlta)
const c = capturar(doc)
for (const frag of [
'FORMATO DE IDENTIFICACION DE CLIENTES',
'PASCUAL MARTIN TELLEZ CHIGUINDO',
'TECP891124HBCLHS07',
'TECP8911246B4',
'AGENTE DEL MINISTERIO PUBLICO FEDERAL',
'FRAY PEDRO DE GANTE',
'TIJUANA OTAY',
'BAJA CALIFORNIA 22508 MEXICO',
'INSTITUTO NACIONAL ELECTORAL',
'IDMEX2311049617',
'31/12/2032',
'Artículos 8, 15, 16, 17',
'Artículo 62 de la Ley Federal',
'FORMATO PARA LA IDENTIFICACIÓN DEL',
'BENEFICIARIO CONTROLADOR',
'soy el único Beneficiario Controlador',
'en la ciudad de Tijuana, B.C.',
]) {
expect(c.texto).toContain(frag)
}
// La firma del formato PLD no debe quedar sola en una página
expect(doc.getNumberOfPages()).toBe(2)
expect(c.paginas[0]).toContain('Llenar otro formato')
expect(c.paginas[0]).toContain('NOMBRE Y FIRMA DEL CLIENTE')
// Firma embebida en ambas páginas
expect(c.numImagenes).toBeGreaterThanOrEqual(2)
})
it('aviso de privacidad: texto íntegro hasta el último párrafo y firma embebida', () => {
const c = capturarAlta(avisoPrivacidad.crearDocumento, fixtureAlta)
for (const frag of [
'AVISO DE PRIVACIDAD',
'Ley Federal de Protección de Datos Personales en Posesión de los Particulares',
'Información Confidencial',
'cancelación u oposición (ARCO)',
'www.frasa.com.mx/aviso-de-privacidad/',
'revisar periódicamente el contenido del mismo.',
'El suscrito (a) PASCUAL MARTIN TELLEZ CHIGUINDO',
'AUTORIZO EL USO DE MIS DATOS',
]) {
expect(c.texto).toContain(frag)
}
expect(c.numImagenes).toBeGreaterThanOrEqual(1)
})
it('carta antilavado: cuerpo completo, límite de efectivo y fundamento legal', () => {
const c = capturarAlta(cartaAntilavado.crearDocumento, fixtureAlta)
for (const frag of [
'Estimado (a) PASCUAL MARTIN TELLEZ CHIGUINDO',
'proyecto SENDA',
'$600,000.00',
'SEISCIENTOS MIL PESOS',
'Artículo 32, fracción I',
'A T E N T A M E N T E',
'Frasa Desarrollos SAPI C.V.',
'DE CONFORMIDAD',
'En mi carácter de “COMPRADOR”',
]) {
expect(c.texto).toContain(frag)
}
expect(c.numImagenes).toBeGreaterThanOrEqual(1)
})
it('checklist: listas, firmas de recepción y contactos sin encabezados huérfanos', () => {
const c = capturarAlta(checklistExpediente.crearDocumento, fixtureAlta)
for (const frag of [
'FORMATO DE RECEPCIÓN Y',
'CONTROL DE EXPEDIENTE',
'PASCUAL MARTIN TELLEZ CHIGUINDO',
'$2,700,000.00',
'Formatos Generales***',
'Buró de crédito',
'GLADIS VALENZUELA',
'DEYSI LORENA ALEMAN FIGUEROA',
'DANIEL ALVARADO LUQUE',
'REQUISITOS',
'GENERALES',
'Solicitud de crédito hipotecario',
'3 Referencias en U.S.A.',
'Graciela Bautista Velasco',
'ALEJANDRA GUERRERO SANCHEZ',
'gbautista@frasa.com.mx',
'máximo de 48h',
]) {
expect(c.texto).toContain(frag)
}
// El encabezado de cada bloque de contacto está en la misma página que su contenido
const paginaDe = (pagina: string, frag: string) => pagina.includes(frag)
for (const pagina of c.paginas) {
if (paginaDe(pagina, 'ENTREGA DE DOCUMENTOS:')) {
expect(pagina).toContain('Graciela Bautista Velasco')
}
if (paginaDe(pagina, 'FIRMA DE CONTRATO:')) {
expect(pagina).toContain('ALEJANDRA GUERRERO SANCHEZ')
}
}
})
it('casos borde: proyecto OTRO, sin referencias, montos en cero y sin firma', () => {
const alta = minimos()
for (const [, modulo] of generadores) {
expect(() => modulo.crearDocumento(alta)).not.toThrow()
}
const perfil = capturarAlta(perfilCliente.crearDocumento, alta)
expect(perfil.texto).toContain('RESIDENCIAL LAS ÁGUILAS')
expect(perfil.texto).toContain('$0.00')
// Sin firma no se incrusta ninguna imagen
expect(perfil.numImagenes).toBe(0)
const carta = capturarAlta(cartaAntilavado.crearDocumento, alta)
expect(carta.texto).toContain('proyecto RESIDENCIAL LAS ÁGUILAS')
})
})

View File

@@ -3,11 +3,15 @@ import autoTable from 'jspdf-autotable'
import type { AltaCliente } from '../../types'
import { formatoMoneda, formatoFechaCorta, nombreArchivoPdf } from '../formatters'
import { calcularTotalIngresos, calcularGastosTotal } from '../calculations'
import { TIPOS_CREDITO } from '../../data/constantes'
import { TIPOS_CREDITO, TIPOS_EMPLEO, TIPOS_ECONOMIA, TIPOS_VIVIENDA, TIPOS_COMPRA, REGIMENES_BIENES } from '../../data/constantes'
import { MARGEN, finalY, titulo, lineaFirma, numero, type DocumentoPdf } from './helpers'
const vacio = (v: string | undefined | null): string => (v && v.trim() !== '' ? v : '—')
/** Etiqueta legible de un catálogo; si el valor no está en el catálogo se muestra tal cual */
const etiqueta = (catalogo: readonly { valor: string; etiqueta: string }[], valor: string): string =>
catalogo.find((o) => o.valor === valor)?.etiqueta ?? vacio(valor)
function seccion(doc: jsPDF, y: number, texto: string): number {
if (y > 260) {
doc.addPage()
@@ -62,7 +66,7 @@ export function crearDocumento(alta: AltaCliente): DocumentoPdf {
styles: { fontSize: 7, cellPadding: 1 },
headStyles: { fillColor: [4, 120, 87] },
head: [['ESTADO CIVIL', 'RÉGIMEN BIENES', 'PESO', 'ESTATURA']],
body: [[vacio(g.estadoCivil), vacio(g.regimenBienes), vacio(g.peso), vacio(g.estatura)]],
body: [[vacio(g.estadoCivil), etiqueta(REGIMENES_BIENES, g.regimenBienes), vacio(g.peso), vacio(g.estatura)]],
})
y = finalY(doc)
autoTable(doc, {
@@ -81,7 +85,7 @@ export function crearDocumento(alta: AltaCliente): DocumentoPdf {
headStyles: { fillColor: [4, 120, 87] },
head: [['DELEG./MUNICIPIO', 'C.P.', 'ANTIGÜEDAD', 'TIPO DE VIVIENDA', 'TIPO DE COMPRA']],
body: [
[vacio(g.delegacionMunicipio), vacio(g.codigoPostal), vacio(g.antiguedadDomicilio), vacio(g.tipoVivienda), vacio(g.tipoCompra)],
[vacio(g.delegacionMunicipio), vacio(g.codigoPostal), vacio(g.antiguedadDomicilio), etiqueta(TIPOS_VIVIENDA, g.tipoVivienda), etiqueta(TIPOS_COMPRA, g.tipoCompra)],
],
})
y = finalY(doc) + 6
@@ -96,8 +100,8 @@ export function crearDocumento(alta: AltaCliente): DocumentoPdf {
head: [['TIPO DE EMPLEO', 'TIPO DE ECONOMÍA', 'COMPAÑÍA', 'ANTIGÜEDAD']],
body: [
[
vacio(e.tipoEmpleo),
vacio(e.tipoEconomia),
etiqueta(TIPOS_EMPLEO, e.tipoEmpleo),
etiqueta(TIPOS_ECONOMIA, e.tipoEconomia),
vacio(e.compania),
`${vacio(e.antiguedadAnios)} AÑOS ${vacio(e.antiguedadMeses)} MESES`,
],
@@ -207,7 +211,8 @@ export function crearDocumento(alta: AltaCliente): DocumentoPdf {
head: [['PROYECTO', 'UBICACIÓN', 'PROTOTIPO']],
body: [
[
[`${vacio(v.manzana)} ${vacio(v.lote)}`.replace(/—/g, '').trim(), proyecto, vacio(v.modelo)].join(' ').trim(),
// Manzana/Lote + nombre del proyecto (el modelo va en su propia columna PROTOTIPO)
[`${vacio(v.manzana)} ${vacio(v.lote)}`.replace(/—/g, '').trim(), proyecto].filter(Boolean).join(' '),
vacio(v.ubicacion),
vacio(v.modelo),
],

View File

@@ -110,6 +110,9 @@ export const UNIDADES_SENDA: Unidad[] = [
{ codigo: 'SENDA-410-1-F202', modelo: 'ESMERALDA PA', desarrollo: 'SENDA', fechaEntrega: null, supEstacionamiento: null, supConstruccion: 55.78, supExcedente: null, precioSinAcabados: 1850000, precioConAcabados: 1850000, precioLista: 1850000, status: 'DISPONIBLE', mesesParaFirma: null, extras: { prototipo: 'CUADRUPLEX', manzana: 410, lote: 1, cajones: 1 } },
{ codigo: 'SENDA-410-1-G101', modelo: 'ESMERALDA PB', desarrollo: 'SENDA', fechaEntrega: null, supEstacionamiento: null, supConstruccion: 55.78, supExcedente: null, precioSinAcabados: 1948000, precioConAcabados: 1948000, precioLista: 1948000, status: 'DISPONIBLE', mesesParaFirma: null, extras: { prototipo: 'CUADRUPLEX', manzana: 410, lote: 1, cajones: 1 } },
{ codigo: 'SENDA-410-1-G202', modelo: 'ESMERALDA PA', desarrollo: 'SENDA', fechaEntrega: null, supEstacionamiento: null, supConstruccion: 55.78, supExcedente: null, precioSinAcabados: 1850000, precioConAcabados: 1850000, precioLista: 1850000, status: 'VENDIDO', mesesParaFirma: null, extras: { prototipo: 'CUADRUPLEX', manzana: 410, lote: 1, cajones: 1 } },
{ codigo: 'SENDA-410-1-G102', modelo: 'ESMERALDA PB', desarrollo: 'SENDA', fechaEntrega: null, supEstacionamiento: null, supConstruccion: 55.78, supExcedente: null, precioSinAcabados: 1948000, precioConAcabados: 1948000, precioLista: 1948000, status: 'VENDIDO', mesesParaFirma: null, extras: { prototipo: 'CUADRUPLEX', manzana: 410, lote: 1, cajones: 1 } },
{ codigo: 'SENDA-410-1-G201', modelo: 'ESMERALDA PA', desarrollo: 'SENDA', fechaEntrega: null, supEstacionamiento: null, supConstruccion: 55.78, supExcedente: null, precioSinAcabados: 1850000, precioConAcabados: 1850000, precioLista: 1850000, status: 'VENDIDO', mesesParaFirma: null, extras: { prototipo: 'CUADRUPLEX', manzana: 410, lote: 1, cajones: 1 } },
{ codigo: 'SENDA-401-1-L41', modelo: 'SANTIAGO', desarrollo: 'SENDA', fechaEntrega: null, supEstacionamiento: null, supConstruccion: 63.95, supExcedente: null, precioSinAcabados: 2750000, precioConAcabados: 2750000, precioLista: 2750000, status: 'VENDIDO', mesesParaFirma: null, extras: { prototipo: 'LAUREL', manzana: 401, lote: 1, cajones: 2 } },
{ codigo: 'SENDA-409-5-1', modelo: 'SANTIAGO', desarrollo: 'SENDA', fechaEntrega: '2027-11-20', supEstacionamiento: null, supConstruccion: 63.95, supExcedente: null, precioSinAcabados: 2700000, precioConAcabados: 2700000, precioLista: 2700000, status: 'VENDIDO', mesesParaFirma: null, extras: { prototipo: 'LAUREL', manzana: 409, lote: 5, cajones: 2 } },
{ codigo: 'SENDA-409-4-1', modelo: 'SANTIAGO', desarrollo: 'SENDA', fechaEntrega: '2027-11-27', supEstacionamiento: null, supConstruccion: 63.95, supExcedente: null, precioSinAcabados: 2750000, precioConAcabados: 2750000, precioLista: 2750000, status: 'VENDIDO', mesesParaFirma: null, extras: { prototipo: 'LAUREL', manzana: 409, lote: 4, cajones: 2 } },
{ codigo: 'SENDA-409-1-1', modelo: 'SANTIAGO', desarrollo: 'SENDA', fechaEntrega: '2027-08-28', supEstacionamiento: null, supConstruccion: 63.95, supExcedente: null, precioSinAcabados: 2750000, precioConAcabados: 2750000, precioLista: 2750000, status: 'VENDIDO', mesesParaFirma: null, extras: { prototipo: 'LAUREL', manzana: 409, lote: 1, cajones: 2 } },