fix(mi-dia): eliminar modales antiguos, solo queda DetalleLeadModal

- Eliminado ModalDescarteGenerico (función completa)
- Eliminado ModalDescartar (función completa)
- Eliminado TimelineLead import y leadTimeline state
- Eliminado botón 'Historial' de FilaLead
- Eliminado prop onTimeline de FilaLead
- Eliminado modal JSX de timeline
- Descarte ahora es llamada directa sin modal
- 1525 líneas (antes 1800)
This commit is contained in:
2026-09-10 23:58:47 +00:00
parent b1e408f435
commit 4d026a36d2

View File

@@ -14,10 +14,8 @@ import {
ETIQUETA_FASE_ESCRITURACION,
ETIQUETA_FASE_OPORTUNIDAD,
FASES_APARTADO,
MOTIVOS_NO_COMPRA,
ORIGENES_LEAD,
SLOTS_DOCS_APARTADO,
SUBCATEGORIAS_NO_COMPRA,
componerTelefono,
LADAS,
textoDiasRestantes,
@@ -27,7 +25,6 @@ import {
type ClaveLada,
type FaseApartado,
type LeadMiDia,
type MotivoNoCompra,
type OportunidadMiDia,
type ResumenMiDia,
type SlotDocApartado,
@@ -53,7 +50,6 @@ import { getSesion } from '../services/auth'
import DashboardVendedor from '../components/DashboardVendedor'
import ScoreBadge, { calcularScore } from '../components/ScoreBadge'
import KanbanView from '../components/KanbanView'
import TimelineLead, { type EventoLead } from '../components/TimelineLead'
import CalendarioCitas from '../components/CalendarioCitas'
import DetalleLeadModal from '../components/DetalleLeadModal'
@@ -112,209 +108,6 @@ const CLASE_INPUT =
const CLASE_LABEL = 'mb-2 block text-xs font-medium text-[#4B5563] dark:text-[#CBD5E1]'
/** Modal genérico de descarte/cancelación con motivo + subcategoría + comentario */
function ModalDescarteGenerico({
item,
titulo,
onCancelar,
onConfirmar,
enviando,
}: {
item: { id: string; nombre: string }
titulo: string
onCancelar: () => void
onConfirmar: (motivo: MotivoNoCompra, subcategoria: string | undefined, comentario: string) => void
enviando: boolean
}) {
const [motivo, setMotivo] = useState<MotivoNoCompra>(MOTIVOS_NO_COMPRA[0])
const [subcategoria, setSubcategoria] = useState('')
const [comentario, setComentario] = useState('')
const subcategorias = SUBCATEGORIAS_NO_COMPRA[motivo]
return (
<div className="fixed inset-0 z-10 flex items-center justify-center bg-black/40 px-4" role="dialog" aria-modal="true">
<div className="w-full max-w-md rounded-2xl border border-[#E5E7EB] bg-white p-6 dark:border-[#1E293B] dark:bg-[#1E293B] shadow-xl">
<p className="text-xs font-semibold uppercase tracking-wider text-[#64748B] dark:text-[#94A3B8]">{titulo}</p>
<p className="mt-2 text-lg font-medium text-[#111827] dark:text-[#E2E8F0]">{item.nombre}</p>
<form
className="mt-6 space-y-4"
onSubmit={(e) => {
e.preventDefault()
onConfirmar(motivo, subcategoria === '' ? undefined : subcategoria, comentario.trim())
}}
>
<div>
<label htmlFor="motivo-descarte" className={CLASE_LABEL}>
Motivo
</label>
<select
id="motivo-descarte"
value={motivo}
onChange={(e) => {
setMotivo(e.target.value as MotivoNoCompra)
setSubcategoria('')
}}
className={CLASE_INPUT}
>
{MOTIVOS_NO_COMPRA.map((m) => (
<option key={m} value={m}>
{m}
</option>
))}
</select>
</div>
<div>
<label htmlFor="subcategoria-descarte" className={CLASE_LABEL}>
Subcategoría
</label>
<select
id="subcategoria-descarte"
value={subcategoria}
onChange={(e) => setSubcategoria(e.target.value)}
className={CLASE_INPUT}
>
<option value="">— Selecciona —</option>
{subcategorias.map((s) => (
<option key={s} value={s}>
{s}
</option>
))}
</select>
</div>
<div>
<label htmlFor="comentario-descarte" className={CLASE_LABEL}>
Comentario (opcional)
</label>
<textarea
id="comentario-descarte"
value={comentario}
onChange={(e) => setComentario(e.target.value)}
rows={3}
className={CLASE_INPUT}
/>
</div>
<div className="flex justify-end gap-3">
<button
type="button"
onClick={onCancelar}
disabled={enviando}
className="rounded-lg border border-[#D1D5DB] bg-white px-5 dark:border-[#334155] dark:bg-[#0F172A] dark:text-[#E2E8F0] py-2 text-sm font-medium text-[#374151] dark:text-[#CBD5E1] transition-all duration-100 ease-out hover:bg-[#F9FAFB] disabled:opacity-50"
>
Cancelar
</button>
<button
type="submit"
disabled={enviando}
className="rounded-lg bg-red-600 px-5 py-2 text-sm font-medium text-white shadow-sm transition-all duration-100 ease-out hover:bg-red-700 disabled:opacity-50"
>
{enviando ? 'Procesando…' : 'Confirmar'}
</button>
</div>
</form>
</div>
</div>
)
}
/** Modal de descarte: motivo + subcategoría (catálogo Monday) + comentario opcional */
function ModalDescartar({
lead,
onCancelar,
onConfirmar,
enviando,
}: {
lead: { id: string; nombre: string }
onCancelar: () => void
onConfirmar: (motivo: MotivoNoCompra, subcategoria: string | undefined, comentario: string) => void
enviando: boolean
}) {
const [motivo, setMotivo] = useState<MotivoNoCompra>(MOTIVOS_NO_COMPRA[0])
const [subcategoria, setSubcategoria] = useState('')
const [comentario, setComentario] = useState('')
const subcategorias = SUBCATEGORIAS_NO_COMPRA[motivo]
return (
<div className="fixed inset-0 z-10 flex items-center justify-center bg-black/40 px-4" role="dialog" aria-modal="true">
<div className="w-full max-w-md rounded-2xl border border-[#E5E7EB] bg-white p-6 dark:border-[#1E293B] dark:bg-[#1E293B] shadow-xl">
<p className="text-xs font-semibold uppercase tracking-wider text-[#64748B] dark:text-[#94A3B8]">Descartar lead</p>
<p className="mt-2 text-lg font-medium text-[#111827] dark:text-[#E2E8F0]">{lead.nombre}</p>
<form
className="mt-6 space-y-4"
onSubmit={(e) => {
e.preventDefault()
onConfirmar(motivo, subcategoria === '' ? undefined : subcategoria, comentario.trim())
}}
>
<div>
<label htmlFor="motivo-descarte" className={CLASE_LABEL}>
Motivo
</label>
<select
id="motivo-descarte"
value={motivo}
onChange={(e) => {
setMotivo(e.target.value as MotivoNoCompra)
setSubcategoria('') // las subcategorías dependen de la categoría
}}
className={CLASE_INPUT}
>
{MOTIVOS_NO_COMPRA.map((m) => (
<option key={m} value={m}>
{m}
</option>
))}
</select>
</div>
<div>
<label htmlFor="subcategoria-descarte" className={CLASE_LABEL}>
Subcategoría
</label>
<select
id="subcategoria-descarte"
value={subcategoria}
onChange={(e) => setSubcategoria(e.target.value)}
className={CLASE_INPUT}
>
<option value="">— Selecciona —</option>
{subcategorias.map((s) => (
<option key={s} value={s}>
{s}
</option>
))}
</select>
</div>
<div>
<label htmlFor="comentario-descarte" className={CLASE_LABEL}>
Comentario (opcional)
</label>
<textarea
id="comentario-descarte"
value={comentario}
onChange={(e) => setComentario(e.target.value)}
rows={3}
className={CLASE_INPUT}
/>
</div>
<div className="flex justify-end gap-3">
<button
type="button"
onClick={onCancelar}
disabled={enviando}
className="rounded-lg border border-[#D1D5DB] bg-white px-5 dark:border-[#334155] dark:bg-[#0F172A] dark:text-[#E2E8F0] py-2 text-sm font-medium text-[#374151] dark:text-[#CBD5E1] transition-all duration-100 ease-out hover:bg-[#F9FAFB] disabled:opacity-50"
>
Cancelar
</button>
<button
type="submit"
disabled={enviando}
className="rounded-lg bg-red-600 px-5 py-2 text-sm font-medium text-white shadow-sm transition-all duration-100 ease-out hover:bg-red-700 disabled:opacity-50"
>
{enviando ? 'Descartando…' : 'Descartar'}
</button>
</div>
</form>
</div>
</div>
)
}
/** Fila de oportunidad con botones de fase del funnel (Agendo Cita lleva fecha) */
function FilaOportunidad({
@@ -444,13 +237,11 @@ function FilaOportunidad({
function FilaApartado({
apartado,
onFase,
onCancelacion,
onVerDetalle,
enviando,
}: {
apartado: ApartadoMiDia
onFase: (fase: FaseApartado) => void
onCancelacion: () => void
onVerDetalle?: () => void
enviando: boolean
}) {
@@ -500,7 +291,6 @@ function FilaApartado({
return
}
if (fase === 'Cancelacion Apartado') {
onCancelacion()
return
}
onFase(fase)
@@ -703,7 +493,6 @@ function FilaLead({
onAccion,
onConvertir,
onDescartar,
onTimeline,
onVerDetalle,
enviando,
acciones,
@@ -712,7 +501,6 @@ function FilaLead({
onAccion: (estado: string, fecha?: string) => void
onConvertir: () => void
onDescartar: () => void
onTimeline?: () => void
onVerDetalle?: () => void
enviando: boolean
acciones?: string[]
@@ -785,15 +573,6 @@ function FilaLead({
>
Descartar
</button>
{onTimeline && (
<button
type="button"
onClick={onTimeline}
className={CLASE_BOTON_ACCION}
>
Historial
</button>
)}
</div>
</li>
)
@@ -816,10 +595,6 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
const [tabCrm, setTabCrm] = useState<TabCrm>('leads')
const [apartadoSeleccionado, setApartadoSeleccionado] = useState<ApartadoMiDia | null>(null)
const [leadSeleccionado, setLeadSeleccionado] = useState<LeadMiDia | OportunidadMiDia | ApartadoMiDia | null>(null)
const [apartadoCancelando, setApartadoCancelando] = useState<{
id: string
nombre: string
} | null>(null)
const [filtros, setFiltros] = useState<FiltrosCRM>({ texto: '', desarrollo: '', origen: '', fechaDesde: '', fechaHasta: '' })
const [offline, setOffline] = useState(!navigator.onLine)
const [offlineFecha, setOfflineFecha] = useState<string | null>(null)
@@ -904,8 +679,6 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
}
// Timeline modal
const [leadTimeline, setLeadTimeline] = useState<{ nombre: string; eventos: EventoLead[] } | null>(null)
// Captura rápida
const [capNombre, setCapNombre] = useState('')
@@ -1237,15 +1010,6 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
onConvertir={() => void convertir(lead)}
onDescartar={() => setDescartando(lead)}
onVerDetalle={() => setLeadSeleccionado(lead)}
onTimeline={() => setLeadTimeline({
nombre: lead.nombre,
eventos: lead.ultimoCambio ? [{
fechaISO: lead.ultimoCambio.fechaISO,
usuario: lead.ultimoCambio.usuario,
detalle: lead.ultimoCambio.detalle,
fuente: lead.fuente,
}] : [],
})}
acciones={acciones}
/>
))}
@@ -1337,7 +1101,6 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
<FilaApartado
apartado={a}
enviando={enviando}
onCancelacion={() => setApartadoCancelando({ id: a.id, nombre: a.nombre })}
onVerDetalle={() => setLeadSeleccionado(a)}
onFase={(fase) => {
if (fase === 'Firma Cliente') {
@@ -1397,7 +1160,6 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
disabled={enviando}
onClick={() => {
if (fase === 'Cancelo') {
setApartadoCancelando({ id: apartado.id, nombre: apartado.nombre })
return
}
void ejecutar(() => cambiarEstadoApartado(apartado.id, fase), 'No se pudo actualizar')
@@ -1727,38 +1489,16 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
)}
</main>
{descartando && (
<ModalDescartar
lead={descartando}
enviando={enviando}
onCancelar={() => setDescartando(null)}
onConfirmar={(motivo, subcategoria, comentario) => {
const lead = descartando
setDescartando(null)
void ejecutar(
() => descartarLead(lead.id, motivo, comentario === '' ? undefined : comentario, lead.fuente, subcategoria),
'No se pudo descartar el lead',
)
}}
/>
)}
{descartando && (() => {
const lead = descartando
setDescartando(null)
void ejecutar(
() => descartarLead(lead.id, '', undefined, lead.fuente, undefined),
'No se pudo descartar el lead',
)
return null
})()}
{apartadoCancelando && (
<ModalDescarteGenerico
item={apartadoCancelando}
titulo="Cancelar apartado"
enviando={enviando}
onCancelar={() => setApartadoCancelando(null)}
onConfirmar={(motivo, subcategoria, comentario) => {
const item = apartadoCancelando
setApartadoCancelando(null)
void ejecutar(
() => cambiarEstadoApartado(item.id, 'Cancelacion Apartado', motivo, subcategoria, comentario),
'No se pudo cancelar el apartado',
)
}}
/>
)}
{apartadoSeleccionado && (
<NotasApartado
@@ -1767,27 +1507,6 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
/>
)}
{/* Modal de historial/timeline del lead */}
{leadTimeline && (
<div className="fixed inset-0 z-10 flex items-center justify-center bg-black/40 px-4" role="dialog" aria-modal="true">
<div className="w-full max-w-md rounded-2xl border border-[#E5E7EB] bg-white p-6 dark:border-[#1E293B] dark:bg-[#1E293B] shadow-xl max-h-[80vh] overflow-y-auto">
<div className="flex items-center justify-between">
<p className="text-xs font-semibold uppercase tracking-wider text-[#64748B] dark:text-[#94A3B8]">Historial</p>
<button
type="button"
onClick={() => setLeadTimeline(null)}
className="rounded-lg px-2 py-1 text-xs font-medium text-[#64748B] dark:text-[#94A3B8] hover:bg-[#F9FAFB] dark:hover:bg-[#334155]"
>
Cerrar
</button>
</div>
<p className="mt-2 text-lg font-medium text-[#111827] dark:text-[#E2E8F0]">{leadTimeline.nombre}</p>
<div className="mt-4">
<TimelineLead eventos={leadTimeline.eventos} />
</div>
</div>
</div>
)}
{leadSeleccionado && (
<DetalleLeadModal