Backend (10 endpoints nuevos): - Recordatorios personales (CRUD + tabla RecordatorioVendedor) - Lead scoring 0-100 (etapa+recencia+datos) - Dashboard mora (pagos vencidos, monto, días atraso) - Timeline de pagos (pagado/vence/futuro) - Perfil completo contacto (pagos+cotizaciones+docs+score) - Exportar contactos a CSV Frontend Mi día: - Resumen matutino (saludo+KPIs) - Historial visual lead (timeline) - Score badge 0-100 (verde/amarillo/rojo) - Vista kanban (drag & drop) Frontend Cobranza: - Dashboard mora (tabla+WhatsApp) - Timeline pagos visual (nodos coloreados) Frontend Alta Clientes: - Checklist documentos visual - Foto cliente (cámara) Frontend Landing: - Perfil contacto completo - Exportar contactos CSV Frontend Mi día (extras): - Comparativa mensual - KPIs dashboard (leads hoy, ventas, conversión) 806 tests, 5 apps construidas.
107 lines
4.0 KiB
TypeScript
107 lines
4.0 KiB
TypeScript
import { useState } from 'react'
|
|
|
|
/** Documento requerido en el checklist */
|
|
export interface DocumentoChecklist {
|
|
tipo: string
|
|
nombre: string
|
|
obligatorio: boolean
|
|
}
|
|
|
|
const DOCUMENTOS_DEFAULT: DocumentoChecklist[] = [
|
|
{ tipo: 'INE', nombre: 'INE / Identificación oficial', obligatorio: true },
|
|
{ tipo: 'RFC', nombre: 'RFC', obligatorio: true },
|
|
{ tipo: 'CURP', nombre: 'CURP', obligatorio: true },
|
|
{ tipo: 'COMPROBANTE_DOMICILIO', nombre: 'Comprobante de domicilio', obligatorio: true },
|
|
{ tipo: 'ACTA_NACIMIENTO', nombre: 'Acta de nacimiento', obligatorio: true },
|
|
]
|
|
|
|
function iconoDoc(recibido: boolean, obligatorio: boolean): string {
|
|
if (recibido) return '✅'
|
|
return obligatorio ? '⏳' : '📄'
|
|
}
|
|
|
|
interface ChecklistDocumentosProps {
|
|
documentos?: DocumentoChecklist[]
|
|
recibidos?: string[] // tipos ya marcados como recibidos
|
|
onToggle?: (tipo: string, recibido: boolean) => void
|
|
soloLectura?: boolean
|
|
}
|
|
|
|
export default function ChecklistDocumentos({
|
|
documentos = DOCUMENTOS_DEFAULT,
|
|
recibidos: recibidosProp,
|
|
onToggle,
|
|
soloLectura = false,
|
|
}: ChecklistDocumentosProps) {
|
|
const [recibidosInterno, setRecibidosInterno] = useState<Set<string>>(() => new Set(recibidosProp ?? []))
|
|
const recibidos = recibidosProp ? new Set(recibidosProp) : recibidosInterno
|
|
|
|
const toggle = (tipo: string) => {
|
|
if (soloLectura) return
|
|
const nuevo = new Set(recibidosInterno)
|
|
if (nuevo.has(tipo)) nuevo.delete(tipo)
|
|
else nuevo.add(tipo)
|
|
setRecibidosInterno(nuevo)
|
|
onToggle?.(tipo, nuevo.has(tipo))
|
|
}
|
|
|
|
const total = documentos.length
|
|
const completados = documentos.filter((d) => recibidos.has(d.tipo)).length
|
|
|
|
return (
|
|
<div className="rounded-xl border border-[#E5E7EB] bg-white p-5 dark:border-[#1E293B] dark:bg-[#1E293B] shadow-sm">
|
|
<div className="flex items-center justify-between">
|
|
<h3 className="text-xs font-semibold uppercase tracking-wider text-[#64748B] dark:text-[#94A3B8]">
|
|
Documentos requeridos
|
|
</h3>
|
|
<span className="rounded-full bg-blue-50 dark:bg-blue-900/30 px-2.5 py-0.5 text-xs font-semibold text-blue-700 dark:text-blue-400">
|
|
{completados} de {total}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Barra de progreso */}
|
|
<div className="mt-3 h-2 rounded-full bg-gray-100 dark:bg-[#334155]">
|
|
<div
|
|
className="h-full rounded-full bg-green-500 transition-all duration-300"
|
|
style={{ width: `${total > 0 ? (completados / total) * 100 : 0}%` }}
|
|
/>
|
|
</div>
|
|
|
|
<ul className="mt-4 space-y-2">
|
|
{documentos.map((doc) => {
|
|
const recibido = recibidos.has(doc.tipo)
|
|
return (
|
|
<li
|
|
key={doc.tipo}
|
|
onClick={() => toggle(doc.tipo)}
|
|
className={`flex items-center gap-3 rounded-lg border px-4 py-3 transition-colors ${
|
|
recibido
|
|
? 'border-green-200 bg-green-50 dark:border-green-900/40 dark:bg-green-950/20'
|
|
: 'border-[#E5E7EB] bg-white dark:border-[#334155] dark:bg-[#0F172A]'
|
|
} ${soloLectura ? '' : 'cursor-pointer hover:border-accent'}`}
|
|
>
|
|
<span className="text-lg">{iconoDoc(recibido, doc.obligatorio)}</span>
|
|
<div className="flex-1">
|
|
<p className={`text-sm font-medium ${recibido ? 'text-green-700 dark:text-green-400 line-through' : 'text-[#111827] dark:text-[#E2E8F0]'}`}>
|
|
{doc.nombre}
|
|
</p>
|
|
{doc.obligatorio && !recibido && (
|
|
<p className="text-[10px] text-amber-600 dark:text-amber-400">Requerido</p>
|
|
)}
|
|
</div>
|
|
{!soloLectura && (
|
|
<input
|
|
type="checkbox"
|
|
checked={recibido}
|
|
onChange={() => toggle(doc.tipo)}
|
|
className="h-4 w-4 rounded border-gray-300 text-green-600 focus:ring-green-500"
|
|
/>
|
|
)}
|
|
</li>
|
|
)
|
|
})}
|
|
</ul>
|
|
</div>
|
|
)
|
|
}
|