diff --git a/apps/mi-dia/src/components/DetalleLeadModal.tsx b/apps/mi-dia/src/components/DetalleLeadModal.tsx index bc89c10..b97c6ea 100644 --- a/apps/mi-dia/src/components/DetalleLeadModal.tsx +++ b/apps/mi-dia/src/components/DetalleLeadModal.tsx @@ -1,7 +1,7 @@ import { useCallback, useEffect, useState, type FormEvent } from 'react' +import { X, User, Calendar, CheckSquare, FileText, Users, Search } from 'lucide-react' import type { LeadMiDia, OportunidadMiDia, ApartadoMiDia } from '../lib/mi-dia' import { formatoFechaCorta } from '../lib/mi-dia' -import TimelineLead, { type EventoLead } from './TimelineLead' import { listarRecordatorios, crearRecordatorio, type Recordatorio } from '../services/recordatorios' type LeadItem = LeadMiDia | OportunidadMiDia | ApartadoMiDia @@ -9,30 +9,24 @@ type LeadItem = LeadMiDia | OportunidadMiDia | ApartadoMiDia function esLead(item: LeadItem): item is LeadMiDia { return 'email' in item && 'origen' in item && !('modeloInteres' in item) } - function esOportunidad(item: LeadItem): item is OportunidadMiDia { return 'modeloInteres' in item } -export default function DetalleLeadModal({ - item, - onCerrar, -}: { - item: LeadItem - onCerrar: () => void -}) { +type TabId = 'info' | 'cita' | 'tareas' | 'notas' | 'miembros' + +const TABS: { id: TabId; titulo: string; icono: typeof User }[] = [ + { id: 'info', titulo: 'Información del cliente potencial', icono: User }, + { id: 'cita', titulo: 'Reserve o actualice una cita', icono: Calendar }, + { id: 'tareas', titulo: 'Tareas', icono: CheckSquare }, + { id: 'notas', titulo: 'Notas', icono: FileText }, + { id: 'miembros', titulo: 'Objetos de los miembros', icono: Users }, +] + +export default function DetalleLeadModal({ item, onCerrar }: { item: LeadItem; onCerrar: () => void }) { const esOp = esOportunidad(item) const esLd = esLead(item) - - // --- Historial --- - const eventos: EventoLead[] = item.ultimoCambio - ? [{ - fechaISO: item.ultimoCambio.fechaISO, - usuario: item.ultimoCambio.usuario, - detalle: item.ultimoCambio.detalle, - fuente: esLd ? item.fuente : esOp ? undefined : undefined, - }] - : [] + const [tab, setTab] = useState('info') // --- Notas --- const [notas, setNotas] = useState([]) @@ -40,155 +34,193 @@ export default function DetalleLeadModal({ const [notaError, setNotaError] = useState('') const [nuevaNota, setNuevaNota] = useState('') const [enviandoNota, setEnviandoNota] = useState(false) + const [busquedaNota, setBusquedaNota] = useState('') const cargarNotas = useCallback(async () => { setCargandoNotas(true) - setNotaError('') try { const todas = await listarRecordatorios() setNotas(todas.filter((r) => r.leadId === item.id)) - } catch { - setNotaError('No se pudieron cargar las notas.') - } finally { - setCargandoNotas(false) - } + } catch { setNotaError('Error al cargar notas.') } + finally { setCargandoNotas(false) } }, [item.id]) - useEffect(() => { - void cargarNotas() - }, [cargarNotas]) + useEffect(() => { void cargarNotas() }, [cargarNotas]) async function agregarNota(e: FormEvent) { e.preventDefault() - const texto = nuevaNota.trim() - if (texto === '') return + if (!nuevaNota.trim()) return setEnviandoNota(true) - setNotaError('') try { - await crearRecordatorio({ - titulo: texto, - leadId: item.id, - fechaRecordar: new Date().toISOString(), - }) + await crearRecordatorio({ titulo: nuevaNota.trim(), leadId: item.id, fechaRecordar: new Date().toISOString() }) setNuevaNota('') await cargarNotas() - } catch { - setNotaError('No se pudo guardar la nota.') - } finally { - setEnviandoNota(false) - } + } catch { setNotaError('No se pudo guardar.') } + finally { setEnviandoNota(false) } } - // --- Cerrar con Escape --- useEffect(() => { - function onKey(e: KeyboardEvent) { - if (e.key === 'Escape') onCerrar() - } - window.addEventListener('keydown', onKey) - return () => window.removeEventListener('keydown', onKey) + const h = (e: KeyboardEvent) => { if (e.key === 'Escape') onCerrar() } + window.addEventListener('keydown', h) + return () => window.removeEventListener('keydown', h) }, [onCerrar]) + const notasFiltradas = notas.filter((n) => + busquedaNota === '' || n.titulo.toLowerCase().includes(busquedaNota.toLowerCase()) + ) + return (
-
+
{/* Header */} -
-

Detalle del lead

- +
+
+

Editar "{item.nombre}"

+

Añadir y editar oportunidad Detalles, Actividades, notas y Cita.

+
+
- {/* Body scrollable */} -
- {/* Sección Información */} -
-

Información

-
- - {esLd && item.telefono && } - {esOp && item.telefono && } - {esLd && item.email && } - {esOp && item.email && } - {esLd && item.desarrollo && } - {esOp && item.desarrollo && } - {esOp && item.modeloInteres && } - {esOp && item.unidadCodigo && } - {esLd && item.origen && } - - {esOp && item.fechaCita && ( - - )} - {!esLd && !esOp && (item as ApartadoMiDia).unidadCodigo && ( - - )} - {!esLd && !esOp && (item as ApartadoMiDia).fechaLimite && ( - - )} - {item.ultimoCambio && ( -
-

- Último cambio: {item.ultimoCambio.detalle} · {item.ultimoCambio.usuario ?? '—'} ·{' '} - {formatoFechaCorta(item.ultimoCambio.fechaISO.slice(0, 10))} -

+
+ {/* Sidebar izquierda */} + + + {/* Contenido */} +
+ {tab === 'info' && ( +
+ + {(esLd || esOp) && item.telefono && } + {(esLd || esOp) && item.email && } + {(esLd || esOp) && item.desarrollo && } + {esOp && item.modeloInteres && } + {esOp && item.unidadCodigo && } + {esLd && item.origen && } + + {esOp && item.fechaCita && } + {!esLd && !esOp && (item as ApartadoMiDia).unidadCodigo && } + {!esLd && !esOp && (item as ApartadoMiDia).fechaLimite && } + {item.ultimoCambio && ( +
+

Último cambio: {item.ultimoCambio.detalle} · {item.ultimoCambio.usuario ?? '—'} · {formatoFechaCorta(item.ultimoCambio.fechaISO.slice(0, 10))}

+
+ )} +
+ )} + + {tab === 'cita' && ( +
+

Reserve o actualice una cita

+ {esOp && item.fechaCita ? ( +
+

Cita programada

+

{item.fechaCita.replace('T', ' a las ')}

+
+ ) : ( +

No hay cita programada. Use los botones de fase en la lista para agendar una.

+ )} +
+ )} + + {tab === 'tareas' && ( +
+

Tareas pendientes

+ {notasFiltradas.filter((n) => !n.completado).length === 0 ? ( +

No hay tareas pendientes.

+ ) : ( +
    + {notasFiltradas.filter((n) => !n.completado).map((n) => ( +
  • + + {n.titulo} + {formatoFechaCorta(n.createdAt.slice(0, 10))} +
  • + ))} +
+ )} +
+ )} + + {tab === 'notas' && ( +
+ {/* Barra de herramientas */} +
+

Notas

+
+
+ + setBusquedaNota(e.target.value)} + placeholder="Buscar notas" + className="w-48 rounded-lg border border-[#D1D5DB] bg-white pl-8 pr-3 py-1.5 text-sm dark:border-[#334155] dark:bg-[#0F172A] dark:text-[#E2E8F0]" /> +
+
- )} -
-
- {/* Sección Notas */} -
-

Notas

-
- setNuevaNota(e.target.value)} - placeholder="Escribe una nota…" - disabled={enviandoNota} - className="flex-1 rounded-lg border border-[#D1D5DB] bg-white px-3 dark:border-[#334155] dark:bg-[#0F172A] dark:text-[#E2E8F0] py-2 text-sm transition-colors focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/20 disabled:opacity-50" - /> - -
- {notaError && ( -

{notaError}

- )} - {cargandoNotas ? ( -

Cargando notas…

- ) : notas.length === 0 ? ( -

Sin notas registradas.

- ) : ( -
    - {[...notas].reverse().map((n) => ( -
  • -

    {n.titulo}

    - {n.descripcion && ( -

    {n.descripcion}

    - )} -

    - {formatoFechaCorta(n.createdAt.slice(0, 10))} -

    -
  • - ))} -
- )} -
+ {/* Botón añadir */} + - {/* Sección Historial */} -
-

Historial

- -
+ {notaError &&

{notaError}

} + + {/* Formulario inline */} +
+ setNuevaNota(e.target.value)} + placeholder="Escribe tu nota…" disabled={enviandoNota} + className="flex-1 rounded-lg border border-[#D1D5DB] bg-white px-3 py-2 text-sm dark:border-[#334155] dark:bg-[#0F172A] dark:text-[#E2E8F0] focus:border-accent focus:ring-2 focus:ring-accent/20 disabled:opacity-50" /> + +
+ + {/* Lista de notas */} + {cargandoNotas ? ( +

Cargando notas…

+ ) : notasFiltradas.length === 0 ? ( +
+ +

Todavía no hay notas

+

Mantenga un registro de los detalles importantes añadiendo su primera nota

+
+ ) : ( +
    + {[...notasFiltradas].reverse().map((n) => ( +
  • +

    {n.titulo}

    + {n.descripcion &&

    {n.descripcion}

    } +

    {formatoFechaCorta(n.createdAt.slice(0, 10))}

    +
  • + ))} +
+ )} +
+ )} + + {tab === 'miembros' && ( +
+

Objetos de los miembros

+

Información de los miembros del equipo asignados a este lead.

+
+

Vendedor: {(item as any).vendedor ?? '—'}

+

Origen: {esLd ? item.origen : '—'}

+

Último cambio: {item.ultimoCambio ? `${item.ultimoCambio.detalle} por ${item.ultimoCambio.usuario ?? '—'}` : 'Sin cambios registrados'}

+
+
+ )} +
@@ -197,9 +229,9 @@ export default function DetalleLeadModal({ function InfoCampo({ etiqueta, valor }: { etiqueta: string; valor: string }) { return ( -
-

{etiqueta}

-

{valor || '—'}

+
+

{etiqueta}

+

{valor || '—'}

) }