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:
@@ -1,131 +1,96 @@
|
|||||||
import { useState, useCallback } from 'react'
|
import { useState, useCallback } from 'react'
|
||||||
import type { LeadMiDia } from '../lib/mi-dia'
|
|
||||||
import ScoreBadge from './ScoreBadge'
|
import ScoreBadge from './ScoreBadge'
|
||||||
|
|
||||||
/** Columnas del kanban */
|
/** Columna genérica del kanban */
|
||||||
const COLUMNAS = [
|
interface ColumnaDef {
|
||||||
{ id: 'NUEVO', titulo: 'Nuevo', color: 'border-t-gray-400' },
|
id: string
|
||||||
{ id: 'CONTACTADO', titulo: 'Contactado', color: 'border-t-blue-500' },
|
titulo: string
|
||||||
{ id: 'INTERESADO', titulo: 'Interesado', color: 'border-t-yellow-500' },
|
color: string
|
||||||
{ id: 'AGENDO', titulo: 'Agendo', color: 'border-t-green-500' },
|
|
||||||
{ id: 'DESCARTADO', titulo: 'Descartado', color: 'border-t-red-500' },
|
|
||||||
] as const
|
|
||||||
|
|
||||||
type ColumnaId = (typeof COLUMNAS)[number]['id']
|
|
||||||
|
|
||||||
function columnaLead(lead: LeadMiDia): ColumnaId {
|
|
||||||
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'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface KanbanViewProps {
|
interface KanbanViewProps<T extends { id: string; nombre: string; estado?: string; telefono?: string; email?: string; unidadCodigo?: string; fuente?: string; pendienteSync?: boolean }> {
|
||||||
leads: LeadMiDia[]
|
items: T[]
|
||||||
onCambiarEstado: (leadId: string, nuevoEstado: string, fuente?: 'lead' | 'oportunidad') => void
|
columnas: ColumnaDef[]
|
||||||
|
obtenerColumna: (item: T) => string
|
||||||
|
onCambiarEstado: (itemId: string, nuevoEstado: string, fuente?: string) => void
|
||||||
|
renderCard?: (item: T) => React.ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function KanbanView({ leads, onCambiarEstado }: KanbanViewProps) {
|
export default function KanbanView<T extends { id: string; nombre: string; estado?: string; telefono?: string; email?: string; unidadCodigo?: string; fuente?: string; pendienteSync?: boolean }>({
|
||||||
const [dragId, setDragId] = useState<string | null>(null)
|
items,
|
||||||
|
columnas,
|
||||||
|
obtenerColumna,
|
||||||
|
onCambiarEstado,
|
||||||
|
renderCard,
|
||||||
|
}: KanbanViewProps<T>) {
|
||||||
|
const [arrastrando, setArrastrando] = useState<string | null>(null)
|
||||||
|
|
||||||
const onDragStart = useCallback((leadId: string) => {
|
const onDragStart = useCallback((e: React.DragEvent, itemId: string) => {
|
||||||
setDragId(leadId)
|
setArrastrando(itemId)
|
||||||
|
e.dataTransfer.effectAllowed = 'move'
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const onDragOver = useCallback((e: React.DragEvent) => {
|
const onDragOver = useCallback((e: React.DragEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
e.dataTransfer.dropEffect = 'move'
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const onDrop = useCallback(
|
const onDrop = useCallback((e: React.DragEvent, columnaId: string) => {
|
||||||
(columnaId: ColumnaId) => {
|
e.preventDefault()
|
||||||
if (!dragId) return
|
const itemId = e.dataTransfer.getData('text/plain')
|
||||||
onCambiarEstado(dragId, columnaId, 'lead')
|
if (itemId && itemId !== arrastrando) {
|
||||||
setDragId(null)
|
onCambiarEstado(itemId, columnaId)
|
||||||
},
|
|
||||||
[dragId, onCambiarEstado],
|
|
||||||
)
|
|
||||||
|
|
||||||
// Agrupar leads por columna
|
|
||||||
const grupos: Record<ColumnaId, LeadMiDia[]> = {
|
|
||||||
NUEVO: [], CONTACTADO: [], INTERESADO: [], AGENDO: [], DESCARTADO: [],
|
|
||||||
}
|
|
||||||
for (const lead of leads) {
|
|
||||||
const col = columnaLead(lead)
|
|
||||||
grupos[col].push(lead)
|
|
||||||
}
|
}
|
||||||
|
setArrastrando(null)
|
||||||
|
}, [arrastrando, onCambiarEstado])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-4 overflow-x-auto scroll-smooth pb-4">
|
<div className="flex gap-3 overflow-x-auto pb-4 scroll-smooth">
|
||||||
{COLUMNAS.map((col) => (
|
{columnas.map((col) => {
|
||||||
|
const itemsCol = items.filter((item) => obtenerColumna(item) === col.id)
|
||||||
|
return (
|
||||||
<div
|
<div
|
||||||
key={col.id}
|
key={col.id}
|
||||||
className={`flex min-w-[220px] flex-1 flex-col rounded-xl border border-[#E5E7EB] bg-[#F8FAFC] dark:border-[#334155] dark:bg-[#0F172A] border-t-4 ${col.color}`}
|
className={`flex min-w-[220px] flex-1 flex-col rounded-xl border border-[#E5E7EB] bg-[#F8FAFC] dark:border-[#334155] dark:bg-[#0F172A] border-t-4 ${col.color}`}
|
||||||
onDragOver={onDragOver}
|
onDragOver={onDragOver}
|
||||||
onDrop={() => onDrop(col.id)}
|
onDrop={(e) => void onDrop(e, col.id)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between px-3 pt-3 pb-2 sm:px-4">
|
<div className="flex items-center justify-between px-3 pt-3 pb-2 sm:px-4">
|
||||||
<h4 className="text-xs font-semibold uppercase tracking-wider text-[#64748B] dark:text-[#94A3B8]">
|
<h3 className="text-xs font-semibold uppercase tracking-wider text-[#64748B] dark:text-[#94A3B8]">
|
||||||
{col.titulo}
|
{col.titulo}
|
||||||
</h4>
|
</h3>
|
||||||
<span className="rounded-full bg-gray-200 dark:bg-[#334155] px-2 py-0.5 text-[10px] font-semibold text-[#64748B] dark:text-[#94A3B8]">
|
<span className="rounded-full bg-[#E5E7EB] px-2 py-0.5 text-[10px] font-medium text-[#64748B] dark:bg-[#334155] dark:text-[#94A3B8]">
|
||||||
{grupos[col.id].length}
|
{itemsCol.length}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-1 flex-col gap-2 px-3 pb-3">
|
<div className="flex flex-1 flex-col gap-2 px-2 pb-3 sm:px-3">
|
||||||
{grupos[col.id].length === 0 ? (
|
{itemsCol.map((item) => (
|
||||||
<p className="py-4 text-center text-[11px] text-[#94A3B8]">Sin leads</p>
|
|
||||||
) : (
|
|
||||||
grupos[col.id].map((lead) => (
|
|
||||||
<div
|
<div
|
||||||
key={lead.id}
|
key={item.id}
|
||||||
draggable
|
draggable
|
||||||
onDragStart={() => onDragStart(lead.id)}
|
onDragStart={(e) => void onDragStart(e, item.id)}
|
||||||
onDragEnd={() => setDragId(null)}
|
className={`rounded-lg border border-[#E5E7EB] bg-white p-2.5 shadow-sm transition-opacity dark:border-[#334155] dark:bg-[#1E293B] ${arrastrando === item.id ? 'opacity-50' : ''}`}
|
||||||
className={`rounded-lg border border-[#E5E7EB] bg-white p-2.5 shadow-sm dark:border-[#1E293B] dark:bg-[#1E293B] cursor-grab transition-opacity sm:p-3 ${
|
|
||||||
dragId === lead.id ? 'opacity-50' : ''
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
|
{renderCard ? renderCard(item) : (
|
||||||
|
<>
|
||||||
<div className="flex items-start justify-between gap-2">
|
<div className="flex items-start justify-between gap-2">
|
||||||
<p className="text-sm font-medium text-[#111827] dark:text-[#E2E8F0] truncate">
|
<p className="text-sm font-medium text-[#111827] dark:text-[#E2E8F0]">{item.nombre}</p>
|
||||||
{lead.nombre}
|
{'score' in item && <ScoreBadge score={Number((item as Record<string, unknown>).score) || 0} />}
|
||||||
</p>
|
|
||||||
<ScoreBadge score={calcularScoreRapido(lead)} />
|
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-1 text-[11px] text-[#94A3B8] truncate">
|
{item.telefono && <p className="mt-1 text-xs text-[#94A3B8]">{item.telefono}</p>}
|
||||||
{lead.telefono || 'Sin teléfono'}
|
{item.unidadCodigo && <p className="mt-0.5 text-xs text-[#94A3B8]">{item.unidadCodigo}</p>}
|
||||||
{lead.origen ? ` · ${lead.origen}` : ''}
|
{item.pendienteSync && <span className="mt-1 inline-block rounded-full bg-amber-50 px-1.5 py-0.5 text-[10px] text-amber-700 dark:bg-amber-900/30 dark:text-amber-400">⏳ sync</span>}
|
||||||
</p>
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))
|
))}
|
||||||
|
{itemsCol.length === 0 && (
|
||||||
|
<p className="py-8 text-center text-xs text-[#94A3B8] dark:text-[#64748B]">Sin items</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
)
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Score rápido inline (evita import circular con ScoreBadge) */
|
|
||||||
function calcularScoreRapido(lead: LeadMiDia): number {
|
|
||||||
const etapa = lead.estado ?? ''
|
|
||||||
const normalizada = etapa.toUpperCase()
|
|
||||||
let base = 20
|
|
||||||
if (normalizada.includes('AGENDO') || normalizada.includes('CITA')) base = 60
|
|
||||||
else if (normalizada.includes('INTERES')) base = 50
|
|
||||||
else if (normalizada.includes('CONTACTAD')) base = 40
|
|
||||||
else if (normalizada.includes('APARTO')) base = 90
|
|
||||||
else if (normalizada.includes('DESCART') || normalizada.includes('NO ASIST')) base = 5
|
|
||||||
let recencia = 0
|
|
||||||
if (lead.updatedAt) {
|
|
||||||
const diffHoras = (Date.now() - new Date(lead.updatedAt).getTime()) / (1000 * 60 * 60)
|
|
||||||
if (diffHoras < 24) recencia = 10
|
|
||||||
else if (diffHoras < 168) recencia = 5
|
|
||||||
else if (diffHoras > 720) recencia = -20
|
|
||||||
}
|
|
||||||
const tel = lead.telefono?.trim() ? 5 : 0
|
|
||||||
const em = lead.email?.trim() ? 5 : 0
|
|
||||||
const ori = lead.origen?.trim() ? 5 : 0
|
|
||||||
return Math.max(0, Math.min(100, base + recencia + tel + em + ori))
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -665,6 +665,78 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
|
|||||||
|
|
||||||
// Kanban toggle
|
// Kanban toggle
|
||||||
const [vistaKanban, setVistaKanban] = useState(false)
|
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
|
// Timeline modal
|
||||||
const [leadTimeline, setLeadTimeline] = useState<{ nombre: string; eventos: EventoLead[] } | null>(null)
|
const [leadTimeline, setLeadTimeline] = useState<{ nombre: string; eventos: EventoLead[] } | null>(null)
|
||||||
@@ -889,7 +961,7 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
|
|||||||
</div>
|
</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">
|
<div className="mb-6">
|
||||||
<h1 className="text-xl font-semibold text-[#111827] dark:text-[#E2E8F0]">{nombreVendedor}</h1>
|
<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>
|
<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} />
|
<KpiCard titulo="Leads nuevos esta semana" valor={resumen.conteos.leadsNuevosSemana} />
|
||||||
</section>
|
</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={`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 && tabCrm === 'leads' ? '' : 'md:order-1 md:col-span-2 lg:col-span-2'}`}>
|
<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">
|
<section aria-label="CRM personal">
|
||||||
<TabsCrm
|
<TabsCrm
|
||||||
activa={tabCrm}
|
activa={tabCrm}
|
||||||
@@ -993,10 +1065,10 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
|
|||||||
<EstadoVacio mensaje="Sin leads asignados" />
|
<EstadoVacio mensaje="Sin leads asignados" />
|
||||||
) : vistaKanban ? (
|
) : vistaKanban ? (
|
||||||
<KanbanView
|
<KanbanView
|
||||||
leads={leadsFiltrados}
|
items={leadsFiltrados}
|
||||||
onCambiarEstado={(leadId, nuevoEstado, fuente) =>
|
columnas={columnasLeads}
|
||||||
void ejecutar(() => cambiarEstadoLead(leadId, nuevoEstado, undefined, fuente), 'No se pudo actualizar el lead')
|
obtenerColumna={obtenerColumnaLead}
|
||||||
}
|
onCambiarEstado={(id, estado) => void ejecutar(() => cambiarEstadoLead(id, estado, 'lead'), 'No se pudo actualizar')}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ul className="space-y-2">
|
<ul className="space-y-2">
|
||||||
@@ -1049,6 +1121,13 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
|
|||||||
</div>
|
</div>
|
||||||
{opsFiltradas.length === 0 ? (
|
{opsFiltradas.length === 0 ? (
|
||||||
<EstadoVacio mensaje="Sin oportunidades asignadas" />
|
<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">
|
<ul className="space-y-2">
|
||||||
{opsFiltradas.map((op) => (
|
{opsFiltradas.map((op) => (
|
||||||
@@ -1095,6 +1174,13 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
|
|||||||
</div>
|
</div>
|
||||||
{apartadosFiltrados.length === 0 ? (
|
{apartadosFiltrados.length === 0 ? (
|
||||||
<EstadoVacio mensaje="Sin apartados asignados" />
|
<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">
|
<ul className="space-y-2">
|
||||||
{apartadosFiltrados.map((a) => (
|
{apartadosFiltrados.map((a) => (
|
||||||
@@ -1133,6 +1219,13 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
|
|||||||
</p>
|
</p>
|
||||||
{resumen.misApartados.length === 0 ? (
|
{resumen.misApartados.length === 0 ? (
|
||||||
<EstadoVacio mensaje="No hay apartados en escrituración" />
|
<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">
|
<ul className="space-y-3">
|
||||||
{resumen.misApartados.map((apartado) => (
|
{resumen.misApartados.map((apartado) => (
|
||||||
|
|||||||
Reference in New Issue
Block a user