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,45 @@
import type { PlanPago } from '../types'
import { diasParaProximoVencimiento, statusPago, statusPlan } from '../lib/cobranza'
export type Semaforo = 'VERDE' | 'AMBAR' | 'ROJO' | 'GRIS'
const ETIQUETA: Record<Semaforo, string> = {
VERDE: 'Al corriente',
AMBAR: 'Por vencer / parcial',
ROJO: 'Vencido',
GRIS: 'Cerrado',
}
const CLASE_DOT: Record<Semaforo, string> = {
VERDE: 'border-green-300 bg-white text-green-700',
AMBAR: 'border-amber-300 bg-white text-amber-700',
ROJO: 'border-red-300 bg-white text-red-700',
GRIS: 'border-neutral-300 bg-white text-neutral-500',
}
/**
* Semáforo del plan:
* - gris: LIQUIDADO o CANCELADO
* - rojo: tiene pagos VENCIDOS
* - ámbar: tiene un pago PARCIAL o el próximo vencimiento es en 7 días o menos
* - verde: al corriente
*/
export function semaforoPlan(plan: PlanPago, hoy: Date = new Date()): Semaforo {
if (statusPlan(plan) !== 'ACTIVO') return 'GRIS'
const estados = plan.pagos.map((p) => statusPago(p, hoy))
if (estados.includes('VENCIDO')) return 'ROJO'
if (estados.includes('PARCIAL')) return 'AMBAR'
const dias = diasParaProximoVencimiento(plan, hoy)
if (dias !== null && dias <= 7) return 'AMBAR'
return 'VERDE'
}
export default function SemaforoDot({ valor }: { valor: Semaforo }) {
return (
<span
className={`inline-block border px-2 py-0.5 text-xs font-bold uppercase tracking-wider ${CLASE_DOT[valor]}`}
>
{ETIQUETA[valor]}
</span>
)
}