feat(kanban): vista kanban en todas las pestañas de Mi día

- KanbanView genérico: acepta items, columnas y extractor de columna
- Kanban en Mis leads (5 columnas: Nuevo/Contactado/Interesado/Agendo/Descartado)
- Kanban en Mis oportunidades (5 columnas: Afluencia/Cotizador/Agendó/Apartó/Perdido)
- Kanban en Mis apartados (7 columnas: Pago Apartado → Cancelación)
- Kanban en Escrituración (7 columnas: Seguimiento → Canceló)
- Drag & drop funcional en todas las pestañas
- Layout full-width en modo kanban
This commit is contained in:
2026-09-08 08:59:11 +00:00
parent d14749f389
commit febd636144
2 changed files with 172 additions and 114 deletions

View File

@@ -665,6 +665,78 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
// Kanban toggle
const [vistaKanban, setVistaKanban] = useState(false)
// Configuración de columnas por pestaña
const columnasLeads = [
{ id: 'NUEVO', titulo: 'Nuevo', color: 'border-t-gray-400' },
{ id: 'CONTACTADO', titulo: 'Contactado', color: 'border-t-blue-500' },
{ id: 'INTERESADO', titulo: 'Interesado', color: 'border-t-yellow-500' },
{ id: 'AGENDO', titulo: 'Agendo', color: 'border-t-green-500' },
{ id: 'DESCARTADO', titulo: 'Descartado', color: 'border-t-red-500' },
]
const columnasOportunidades = [
{ id: 'AFLUENCIA', titulo: 'Afluencia', color: 'border-t-purple-500' },
{ id: 'COTIZADOR', titulo: 'Cotizador', color: 'border-t-blue-500' },
{ id: 'AGENDO CITA', titulo: 'Agendó Cita', color: 'border-t-green-500' },
{ id: 'APARTO', titulo: 'Apartó', color: 'border-t-emerald-600' },
{ id: 'PERDIDO', titulo: 'Perdido', color: 'border-t-red-500' },
]
const columnasApartados = [
{ id: 'PAGO APARTADO', titulo: 'Pago Apartado', color: 'border-t-blue-500' },
{ id: 'ALTA ENKONTROL', titulo: 'Alta Enkontrol', color: 'border-t-yellow-500' },
{ id: 'ENTREGO DOCUMENTACION', titulo: 'Entregó Docs', color: 'border-t-orange-500' },
{ id: 'CITA CLIENTES', titulo: 'Cita Clientes', color: 'border-t-green-500' },
{ id: 'CONTRATO ENKONTROL', titulo: 'Contrato Enkontrol', color: 'border-t-emerald-600' },
{ id: 'FIRMA CLIENTE', titulo: 'Firma Cliente', color: 'border-t-accent' },
{ id: 'CANCELACION APARTADO', titulo: 'Cancelación', color: 'border-t-red-500' },
]
const columnasEscrituracion = [
{ id: 'SEGUIMIENTO', titulo: 'Seguimiento', color: 'border-t-gray-400' },
{ id: 'LLAMADA ESCRITURAS', titulo: 'Llamada Escrituras', color: 'border-t-blue-500' },
{ id: 'X ESCRITURAR', titulo: 'X Escriturar', color: 'border-t-yellow-500' },
{ id: 'BANCO/INFONAVIT', titulo: 'Banco/Infonavit', color: 'border-t-orange-500' },
{ id: 'X AUTORIZAR', titulo: 'X Autorizar', color: 'border-t-green-500' },
{ id: 'ESCRITURADO', titulo: 'Escriturado', color: 'border-t-emerald-600' },
{ id: 'CANCELO', titulo: 'Canceló', color: 'border-t-red-500' },
]
const obtenerColumnaLead = (lead: LeadMiDia) => {
const e = (lead.estado ?? '').toUpperCase()
if (e.includes('DESCART')) return 'DESCARTADO'
if (e.includes('AGENDO') || e.includes('CITA')) return 'AGENDO'
if (e.includes('INTERES')) return 'INTERESADO'
if (e.includes('CONTACT')) return 'CONTACTADO'
return 'NUEVO'
}
const obtenerColumnaOp = (op: OportunidadMiDia) => {
const e = (op.etapa ?? '').toUpperCase()
if (e.includes('APARTO')) return 'APARTO'
if (e.includes('PERDIDO') || e.includes('PERDI')) return 'PERDIDO'
if (e.includes('COTIZ')) return 'COTIZADOR'
if (e.includes('AGENDO') || e.includes('CITA')) return 'AGENDO CITA'
return 'AFLUENCIA'
}
const obtenerColumnaApartado = (a: ApartadoMiDia) => {
const e = (a.estado ?? '').toUpperCase()
if (e.includes('PAGO')) return 'PAGO APARTADO'
if (e.includes('ALTA') && e.includes('ENKONTROL')) return 'ALTA ENKONTROL'
if (e.includes('ENTREG') || e.includes('DOC')) return 'ENTREGO DOCUMENTACION'
if (e.includes('CITA')) return 'CITA CLIENTES'
if (e.includes('CONTRATO')) return 'CONTRATO ENKONTROL'
if (e.includes('FIRMA')) return 'FIRMA CLIENTE'
if (e.includes('CANCEL')) return 'CANCELACION APARTADO'
return 'PAGO APARTADO'
}
const obtenerColumnaEscrituracion = (a: ApartadoMiDia) => {
const e = (a.estado ?? '').toUpperCase()
if (e.includes('SEGUIMIENTO')) return 'SEGUIMIENTO'
if (e.includes('LLAMADA')) return 'LLAMADA ESCRITURAS'
if (e.includes('X ESCRIT') || e.includes('ESCRIT')) return 'X ESCRITURAR'
if (e.includes('BANCO') || e.includes('INFONAVIT')) return 'BANCO/INFONAVIT'
if (e.includes('X AUTORIZAR')) return 'X AUTORIZAR'
if (e.includes('ESCRITURADO')) return 'ESCRITURADO'
if (e.includes('CANCEL')) return 'CANCELO'
return 'SEGUIMIENTO'
}
// Timeline modal
const [leadTimeline, setLeadTimeline] = useState<{ nombre: string; eventos: EventoLead[] } | null>(null)
@@ -889,7 +961,7 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
</div>
)}
<main className={`px-4 py-6 sm:px-6 lg:px-8 ${vistaKanban && tabCrm === 'leads' ? 'w-full' : 'mx-auto max-w-[1280px]'}`}>
<main className={`px-4 py-6 sm:px-6 lg:px-8 ${vistaKanban ? 'w-full' : 'mx-auto max-w-[1280px]'}`}>
<div className="mb-6">
<h1 className="text-xl font-semibold text-[#111827] dark:text-[#E2E8F0]">{nombreVendedor}</h1>
<p className="mt-1 text-sm text-[#64748B] dark:text-[#94A3B8]">{formatoFechaCorta(hoy)}</p>
@@ -938,8 +1010,8 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
<KpiCard titulo="Leads nuevos esta semana" valor={resumen.conteos.leadsNuevosSemana} />
</section>
<div className={`mt-6 flex flex-col gap-6 ${vistaKanban && tabCrm === 'leads' ? '' : 'md:grid md:grid-cols-2 md:gap-6 lg:grid-cols-3'}`}>
<div className={`order-2 flex flex-col gap-6 ${vistaKanban && tabCrm === 'leads' ? '' : 'md:order-1 md:col-span-2 lg:col-span-2'}`}>
<div className={`mt-6 flex flex-col gap-6 ${vistaKanban ? '' : 'md:grid md:grid-cols-2 md:gap-6 lg:grid-cols-3'}`}>
<div className={`order-2 flex flex-col gap-6 ${vistaKanban ? '' : 'md:order-1 md:col-span-2 lg:col-span-2'}`}>
<section aria-label="CRM personal">
<TabsCrm
activa={tabCrm}
@@ -993,10 +1065,10 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
<EstadoVacio mensaje="Sin leads asignados" />
) : vistaKanban ? (
<KanbanView
leads={leadsFiltrados}
onCambiarEstado={(leadId, nuevoEstado, fuente) =>
void ejecutar(() => cambiarEstadoLead(leadId, nuevoEstado, undefined, fuente), 'No se pudo actualizar el lead')
}
items={leadsFiltrados}
columnas={columnasLeads}
obtenerColumna={obtenerColumnaLead}
onCambiarEstado={(id, estado) => void ejecutar(() => cambiarEstadoLead(id, estado, 'lead'), 'No se pudo actualizar')}
/>
) : (
<ul className="space-y-2">
@@ -1049,6 +1121,13 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
</div>
{opsFiltradas.length === 0 ? (
<EstadoVacio mensaje="Sin oportunidades asignadas" />
) : vistaKanban ? (
<KanbanView
items={opsFiltradas}
columnas={columnasOportunidades}
obtenerColumna={obtenerColumnaOp}
onCambiarEstado={(id, estado) => void ejecutar(() => cambiarEtapaOportunidad(id, estado), 'No se pudo actualizar')}
/>
) : (
<ul className="space-y-2">
{opsFiltradas.map((op) => (
@@ -1095,6 +1174,13 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
</div>
{apartadosFiltrados.length === 0 ? (
<EstadoVacio mensaje="Sin apartados asignados" />
) : vistaKanban ? (
<KanbanView
items={apartadosFiltrados}
columnas={columnasApartados}
obtenerColumna={obtenerColumnaApartado}
onCambiarEstado={(id, estado) => void ejecutar(() => cambiarEstadoApartado(id, estado), 'No se pudo actualizar')}
/>
) : (
<ul className="space-y-2">
{apartadosFiltrados.map((a) => (
@@ -1133,6 +1219,13 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
</p>
{resumen.misApartados.length === 0 ? (
<EstadoVacio mensaje="No hay apartados en escrituración" />
) : vistaKanban ? (
<KanbanView
items={resumen.misApartados}
columnas={columnasEscrituracion}
obtenerColumna={obtenerColumnaEscrituracion}
onCambiarEstado={(id, estado) => void ejecutar(() => cambiarEstadoApartado(id, estado), 'No se pudo actualizar')}
/>
) : (
<ul className="space-y-3">
{resumen.misApartados.map((apartado) => (