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:
2026-08-21 22:27:36 +00:00
commit ecd4edac02
147 changed files with 27345 additions and 0 deletions

View File

@@ -0,0 +1,148 @@
import { useState } from 'react'
import type { AltaCliente } from '../types'
import { fechaISOHoy } from '../lib/formatters'
import { ASESOR_DEFAULT, AUTORIDAD_ID_DEFAULT } from '../data/constantes'
import { useDocumentos } from './useDocumentos'
const datosGeneralesVacios = (): AltaCliente['datosGenerales'] => ({
campaniaVenta: '',
fecha: fechaISOHoy(),
nombreCompleto: '',
nacionalidad: 'MEXICANA',
nivelEstudios: '',
curp: '',
rfc: '',
nss: '',
delegacionMunicipio: '',
direccion: '',
codigoPostal: '',
antiguedadDomicilio: '',
fechaNacimiento: '',
lugarNacimiento: '',
estadoCivil: '',
regimenBienes: '',
peso: '',
estatura: '',
email: '',
telCasa: '',
celular: '',
viviendaAnios: '',
tipoVivienda: '',
tipoCompra: '',
actividadOcupacion: '',
})
const empleoVacio = (): AltaCliente['empleo'] => ({
tipoEmpleo: '',
tipoEconomia: '',
compania: '',
antiguedadAnios: '',
antiguedadMeses: '',
direccion: '',
colonia: '',
numero: '',
codigoPostal: '',
puesto: '',
telefono: '',
ext: '',
delegacion: '',
})
const economicosVacios = (): AltaCliente['economicos'] => ({
ingresosMensuales: 0,
otrosIngresos: 0,
gastosLuz: 0,
gastosOtros: 0,
dependientes: { personas: '', parentesco: '', edad: '' },
})
const estadoInicial = (): Omit<AltaCliente, 'documentos'> => ({
datosGenerales: datosGeneralesVacios(),
empleo: empleoVacio(),
economicos: economicosVacios(),
coacreditado: {
tieneCoacreditado: false,
generales: datosGeneralesVacios(),
empleo: empleoVacio(),
economicos: economicosVacios(),
},
referencias: [
{ nombre: '', telefono: '', parentesco: '', direccion: '' },
{ nombre: '', telefono: '', parentesco: '', direccion: '' },
{ nombre: '', telefono: '', parentesco: '', direccion: '' },
{ nombre: '', telefono: '', parentesco: '', direccion: '' },
],
viviendaCredito: {
proyecto: '',
proyectoOtro: '',
modelo: '',
manzana: '',
lote: '',
ubicacion: '',
superficieM2: '',
valorVenta: '',
enganche: '',
equipamientoAdicional: '',
fechaPagoEnganche: '',
firmaContrato: '',
tiposCredito: [],
montoCreditoInfoFovi: '',
ahorroVoluntarioSubcuenta: '',
tipoProducto: '',
descuentoMensualInfoFovissste: '',
idAutoridadEmisora: AUTORIDAD_ID_DEFAULT,
idNumero: '',
idVigencia: '',
domicilioFiscal: {
calle: '',
numExt: '',
numInt: '',
colonia: '',
municipio: '',
estado: '',
cp: '',
pais: 'MEXICO',
},
},
firma: { firmaDataUrl: '', asesorNombre: ASESOR_DEFAULT },
})
export const PASOS = [
'Datos generales',
'Empleo',
'Económicos',
'Co-acreditado',
'Referencias',
'Vivienda y crédito',
'Documentos',
'Firmas y generar',
] as const
/** Estado global del wizard de alta de clientes */
export function useAltaCliente() {
const [paso, setPaso] = useState(0)
const [datos, setDatos] = useState(estadoInicial)
const { documentos, agregarDocumento, quitarDocumento } = useDocumentos()
const alta: AltaCliente = { ...datos, documentos }
const actualizar = <K extends keyof typeof datos>(seccion: K, valores: (typeof datos)[K]) => {
setDatos((prev) => ({ ...prev, [seccion]: valores }))
}
const siguiente = () => setPaso((p) => Math.min(p + 1, PASOS.length - 1))
const anterior = () => setPaso((p) => Math.max(p - 1, 0))
const irAPaso = (p: number) => setPaso(p)
return {
paso,
siguiente,
anterior,
irAPaso,
datos,
alta,
actualizar,
agregarDocumento,
quitarDocumento,
}
}

View File

@@ -0,0 +1,17 @@
import { useCallback, useState } from 'react'
import type { DocumentoCargado, SlotDocumento } from '../types'
/** Maneja los documentos del expediente en memoria (uno por slot) */
export function useDocumentos() {
const [documentos, setDocumentos] = useState<DocumentoCargado[]>([])
const agregarDocumento = useCallback((doc: DocumentoCargado) => {
setDocumentos((prev) => [...prev.filter((d) => d.slot !== doc.slot), doc])
}, [])
const quitarDocumento = useCallback((slot: SlotDocumento) => {
setDocumentos((prev) => prev.filter((d) => d.slot !== slot))
}, [])
return { documentos, agregarDocumento, quitarDocumento }
}