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:
@@ -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) => {
|
||||
{descartando && (() => {
|
||||
const lead = descartando
|
||||
setDescartando(null)
|
||||
void ejecutar(
|
||||
() => descartarLead(lead.id, motivo, comentario === '' ? undefined : comentario, lead.fuente, subcategoria),
|
||||
() => 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
|
||||
|
||||
Reference in New Issue
Block a user