feat: ecosistema FRASA — apps Cotizador, Alta Clientes, Cobranza y Landing
- Cotizador (/quote): 4 propuestas con formulas validadas del Excel, comparador, combobox de unidades, vigencia, PDF ANEXO A - Alta Clientes (/client): wizard 8 pasos, validacion CURP RENAPO, OCR de INE, checklist de expediente, firma digital, 5 PDFs legales - Cobranza (/pay): planes de enganche, mora, vista cobranza del dia, recibos PDF, bitacora, dashboard con filtros - Landing (/): entrada al ecosistema - Design system StudioBlank (monocromo, acento #1B4332) en las 4 apps - 123 tests vitest
This commit is contained in:
172
apps/alta-clientes/src/forms/FirmasForm.tsx
Normal file
172
apps/alta-clientes/src/forms/FirmasForm.tsx
Normal file
@@ -0,0 +1,172 @@
|
||||
import { useState } from 'react'
|
||||
import type { AltaCliente, DatosFirma } from '../types'
|
||||
import { firmaSchema } from '../lib/schemas'
|
||||
import { formatoMoneda, formatoFechaCorta } from '../lib/formatters'
|
||||
import { calcularTotalIngresos } from '../lib/calculations'
|
||||
import { estadoExpediente } from '../lib/expediente'
|
||||
import SectionCard from '../components/SectionCard'
|
||||
import SignaturePad from '../components/SignaturePad'
|
||||
import ChecklistExpediente from '../components/ChecklistExpediente'
|
||||
import * as perfilCliente from '../lib/pdf/perfilCliente'
|
||||
import * as identificacionCliente from '../lib/pdf/identificacionCliente'
|
||||
import * as avisoPrivacidad from '../lib/pdf/avisoPrivacidad'
|
||||
import * as cartaAntilavado from '../lib/pdf/cartaAntilavado'
|
||||
import * as checklistExpediente from '../lib/pdf/checklistExpediente'
|
||||
|
||||
interface Props {
|
||||
alta: AltaCliente
|
||||
onCambiarFirma: (firma: DatosFirma) => void
|
||||
onAnterior: () => void
|
||||
}
|
||||
|
||||
const GENERADORES: { tipo: string; descargar: (alta: AltaCliente) => void }[] = [
|
||||
{ tipo: 'Perfil del cliente', descargar: perfilCliente.descargar },
|
||||
{ tipo: 'Identificación de cliente (PLD)', descargar: identificacionCliente.descargar },
|
||||
{ tipo: 'Aviso de privacidad', descargar: avisoPrivacidad.descargar },
|
||||
{ tipo: 'Carta anti-lavado', descargar: cartaAntilavado.descargar },
|
||||
{ tipo: 'Checklist de expediente', descargar: checklistExpediente.descargar },
|
||||
]
|
||||
|
||||
/** Paso 8: firma del cliente, nombre del asesor, resumen y generación de PDFs */
|
||||
export default function FirmasForm({ alta, onCambiarFirma, onAnterior }: Props) {
|
||||
const [error, setError] = useState('')
|
||||
const [avisoExpediente, setAvisoExpediente] = useState('')
|
||||
const { datosGenerales: g, viviendaCredito: v, economicos, firma } = alta
|
||||
const proyecto = v.proyecto === 'OTRO' ? v.proyectoOtro : v.proyecto
|
||||
const expediente = estadoExpediente(alta.documentos, firma.firmaDataUrl)
|
||||
|
||||
const validar = (): boolean => {
|
||||
const r = firmaSchema.safeParse(firma)
|
||||
setError(r.success ? '' : (r.error.issues[0]?.message ?? 'Revisa la firma'))
|
||||
return r.success
|
||||
}
|
||||
|
||||
// Advierte (sin bloquear) si el expediente está incompleto al generar PDFs
|
||||
const avisarExpediente = () => {
|
||||
setAvisoExpediente(
|
||||
expediente.completoTodo
|
||||
? ''
|
||||
: `Expediente incompleto (${expediente.completos}/${expediente.total}): puedes generar los PDFs, pero faltan documentos o la firma.`,
|
||||
)
|
||||
}
|
||||
|
||||
const generar = (descargar: (a: AltaCliente) => void) => {
|
||||
avisarExpediente()
|
||||
if (validar()) descargar(alta)
|
||||
}
|
||||
|
||||
const generarTodos = () => {
|
||||
avisarExpediente()
|
||||
if (!validar()) return
|
||||
for (const g2 of GENERADORES) g2.descargar(alta)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-16">
|
||||
<SectionCard titulo="Resumen del alta">
|
||||
<dl className="grid grid-cols-2 gap-x-6 gap-y-4 text-sm sm:col-span-2 lg:col-span-3 lg:grid-cols-3">
|
||||
<div>
|
||||
<dt className="text-xs font-bold uppercase tracking-widest text-neutral-500">Cliente</dt>
|
||||
<dd className="mt-1 font-light text-neutral-900">{g.nombreCompleto}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-xs font-bold uppercase tracking-widest text-neutral-500">CURP</dt>
|
||||
<dd className="mt-1 font-light text-neutral-900">{g.curp}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-xs font-bold uppercase tracking-widest text-neutral-500">RFC</dt>
|
||||
<dd className="mt-1 font-light text-neutral-900">{g.rfc}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-xs font-bold uppercase tracking-widest text-neutral-500">Proyecto</dt>
|
||||
<dd className="mt-1 font-light text-neutral-900">{proyecto || '—'}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-xs font-bold uppercase tracking-widest text-neutral-500">Ubicación / Modelo</dt>
|
||||
<dd className="mt-1 font-light text-neutral-900">
|
||||
{v.ubicacion || '—'} {v.modelo}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-xs font-bold uppercase tracking-widest text-neutral-500">Valor venta</dt>
|
||||
<dd className="mt-1 font-light text-neutral-900">{formatoMoneda(Number(v.valorVenta) || 0)}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-xs font-bold uppercase tracking-widest text-neutral-500">Total ingresos</dt>
|
||||
<dd className="mt-1 font-light text-neutral-900">{formatoMoneda(calcularTotalIngresos(economicos))}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-xs font-bold uppercase tracking-widest text-neutral-500">Fecha</dt>
|
||||
<dd className="mt-1 font-light text-neutral-900">{formatoFechaCorta(g.fecha)}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-xs font-bold uppercase tracking-widest text-neutral-500">Documentos</dt>
|
||||
<dd className="mt-1 font-light text-neutral-900">{alta.documentos.length} de 5</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</SectionCard>
|
||||
|
||||
<ChecklistExpediente documentos={alta.documentos} firmaDataUrl={firma.firmaDataUrl} />
|
||||
|
||||
<SectionCard titulo="Firma del cliente" descripcion="La firma se incrusta en los PDFs generados.">
|
||||
<div className="sm:col-span-2">
|
||||
<SignaturePad onFirma={(dataUrl) => onCambiarFirma({ ...firma, firmaDataUrl: dataUrl })} />
|
||||
</div>
|
||||
<label htmlFor="asesorNombre" className="block">
|
||||
<span className="mb-1 block text-xs font-bold uppercase tracking-widest text-neutral-500">
|
||||
Nombre del asesor <span className="text-red-700">*</span>
|
||||
</span>
|
||||
<input
|
||||
id="asesorNombre"
|
||||
type="text"
|
||||
value={firma.asesorNombre}
|
||||
onChange={(e) => onCambiarFirma({ ...firma, asesorNombre: e.target.value.toUpperCase() })}
|
||||
className="w-full border border-neutral-300 bg-white px-3 py-2.5 text-sm uppercase transition-colors duration-150 focus:border-neutral-900 focus:outline-none"
|
||||
/>
|
||||
</label>
|
||||
</SectionCard>
|
||||
|
||||
{error && <p className="text-xs text-red-700">{error}</p>}
|
||||
|
||||
<SectionCard titulo="Generar documentos PDF">
|
||||
{avisoExpediente && (
|
||||
<p
|
||||
role="alert"
|
||||
className="border border-amber-300 bg-amber-50 px-4 py-3 text-xs text-amber-700 sm:col-span-2 lg:col-span-3"
|
||||
>
|
||||
{avisoExpediente}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex flex-wrap gap-3 sm:col-span-2 lg:col-span-3">
|
||||
{GENERADORES.map((g3) => (
|
||||
<button
|
||||
key={g3.tipo}
|
||||
type="button"
|
||||
onClick={() => generar(g3.descargar)}
|
||||
className="border border-neutral-900 bg-transparent px-5 py-2.5 text-xs font-bold uppercase tracking-widest text-neutral-900 transition-colors duration-150 hover:bg-neutral-900 hover:text-white"
|
||||
>
|
||||
{g3.tipo}
|
||||
</button>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
onClick={generarTodos}
|
||||
className="bg-accent px-5 py-2.5 text-xs font-bold uppercase tracking-widest text-white transition-colors duration-150 hover:bg-neutral-900 disabled:opacity-40"
|
||||
>
|
||||
Generar todos
|
||||
</button>
|
||||
</div>
|
||||
</SectionCard>
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onAnterior}
|
||||
className="border border-neutral-900 bg-transparent px-5 py-2.5 text-xs font-bold uppercase tracking-widest text-neutral-900 transition-colors duration-150 hover:bg-neutral-900 hover:text-white"
|
||||
>
|
||||
Anterior
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user