feat(ux): 17 mejoras de experiencia de usuario
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.
This commit is contained in:
106
apps/alta-clientes/src/components/ChecklistDocumentos.tsx
Normal file
106
apps/alta-clientes/src/components/ChecklistDocumentos.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user