feat(mi-dia): captura rapida arriba de actividades, filtro admin por vendedor, hora en notas, orden Intento->Contactado y kanban de oportunidades alineado
This commit is contained in:
@@ -13,7 +13,7 @@
|
|||||||
export const VOCABULARIO = {
|
export const VOCABULARIO = {
|
||||||
// Estados de la columna status del board Leads: ESTADOS_LEAD_PERMITIDOS del
|
// Estados de la columna status del board Leads: ESTADOS_LEAD_PERMITIDOS del
|
||||||
// API + NUEVO (captura) y DESCARTADO (terminal)
|
// API + NUEVO (captura) y DESCARTADO (terminal)
|
||||||
FASES_LEAD: ['NUEVO', 'CONTACTADO', 'Intento de contacto', 'INTERESADO', 'AGENDA CITA', 'CALIFICADO', 'DESCARTADO'],
|
FASES_LEAD: ['NUEVO', 'Intento de contacto', 'CONTACTADO', 'INTERESADO', 'AGENDA CITA', 'CALIFICADO', 'DESCARTADO'],
|
||||||
// Etapas del funnel de Oportunidades (labels REALES de la columna status 'Estado')
|
// Etapas del funnel de Oportunidades (labels REALES de la columna status 'Estado')
|
||||||
FASES_OPORTUNIDAD: ['Agendo Cita', 'Afluencia', 'Cotizador', 'Aparto', 'No Asistio', 'Abandono'],
|
FASES_OPORTUNIDAD: ['Agendo Cita', 'Afluencia', 'Cotizador', 'Aparto', 'No Asistio', 'Abandono'],
|
||||||
// Fases del pipeline Enkontrol del tablero Apartado (labels REALES de la
|
// Fases del pipeline Enkontrol del tablero Apartado (labels REALES de la
|
||||||
|
|||||||
@@ -85,6 +85,8 @@ export interface ActividadMiDia {
|
|||||||
tipo: string
|
tipo: string
|
||||||
fecha: string
|
fecha: string
|
||||||
estado: string
|
estado: string
|
||||||
|
/** Captura (lead/oportunidad/apartado) a la que pertenece la actividad; null si no está ligada */
|
||||||
|
leadCapturaId: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApartadoPorVencer {
|
export interface ApartadoPorVencer {
|
||||||
@@ -450,6 +452,7 @@ export function resumenMiDiaLocal(
|
|||||||
tipo: a.tipo,
|
tipo: a.tipo,
|
||||||
fecha: a.fechaVencimiento ? a.fechaVencimiento.toISOString().slice(0, 10) : '',
|
fecha: a.fechaVencimiento ? a.fechaVencimiento.toISOString().slice(0, 10) : '',
|
||||||
estado: a.estado,
|
estado: a.estado,
|
||||||
|
leadCapturaId: a.leadCapturaId,
|
||||||
}))
|
}))
|
||||||
const todasMisActividades: ActividadMiDia[] = actividadesAbiertasOrdenadas
|
const todasMisActividades: ActividadMiDia[] = actividadesAbiertasOrdenadas
|
||||||
const misActividadesHoy: ActividadMiDia[] = actividadesAbiertasOrdenadas
|
const misActividadesHoy: ActividadMiDia[] = actividadesAbiertasOrdenadas
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useCallback, useEffect, useRef, useState, type FormEvent } from 'react'
|
import { useCallback, useEffect, useRef, useState, type FormEvent } from 'react'
|
||||||
import { X, User, Calendar, CheckSquare, FileText, Users, Search } from 'lucide-react'
|
import { X, User, Calendar, CheckSquare, FileText, Users, Search } from 'lucide-react'
|
||||||
import type { LeadMiDia, OportunidadMiDia, ApartadoMiDia } from '../lib/mi-dia'
|
import type { LeadMiDia, OportunidadMiDia, ApartadoMiDia } from '../lib/mi-dia'
|
||||||
import { formatoFechaCorta, urlWhatsApp } from '../lib/mi-dia'
|
import { formatoFechaCorta, formatoFechaHora, urlWhatsApp } from '../lib/mi-dia'
|
||||||
import { listarRecordatorios, crearRecordatorio, completarRecordatorio, type Recordatorio } from '../services/recordatorios'
|
import { listarRecordatorios, crearRecordatorio, completarRecordatorio, type Recordatorio } from '../services/recordatorios'
|
||||||
import { getSesion } from '../services/auth'
|
import { getSesion } from '../services/auth'
|
||||||
|
|
||||||
@@ -251,7 +251,7 @@ export default function DetalleLeadModal({
|
|||||||
onChange={() => void completarTarea(n.id)}
|
onChange={() => void completarTarea(n.id)}
|
||||||
/>
|
/>
|
||||||
<span className="text-sm text-[#111827] dark:text-[#E2E8F0]">{n.titulo}</span>
|
<span className="text-sm text-[#111827] dark:text-[#E2E8F0]">{n.titulo}</span>
|
||||||
<span className="ml-auto text-[10px] text-[#94A3B8]">{formatoFechaCorta(n.createdAt.slice(0, 10))}</span>
|
<span className="ml-auto text-[10px] text-[#94A3B8]">{formatoFechaHora(n.createdAt)}</span>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -311,7 +311,7 @@ export default function DetalleLeadModal({
|
|||||||
<li key={n.id} className="rounded-lg border border-[#E5E7EB] dark:border-[#334155] bg-[#F8FAFC] dark:bg-[#1E293B] px-4 py-3">
|
<li key={n.id} className="rounded-lg border border-[#E5E7EB] dark:border-[#334155] bg-[#F8FAFC] dark:bg-[#1E293B] px-4 py-3">
|
||||||
<p className="text-sm font-medium text-[#111827] dark:text-[#E2E8F0]">{n.titulo}</p>
|
<p className="text-sm font-medium text-[#111827] dark:text-[#E2E8F0]">{n.titulo}</p>
|
||||||
{n.descripcion && <p className="mt-1 text-xs text-[#64748B] dark:text-[#94A3B8]">{n.descripcion}</p>}
|
{n.descripcion && <p className="mt-1 text-xs text-[#64748B] dark:text-[#94A3B8]">{n.descripcion}</p>}
|
||||||
<p className="mt-1.5 text-[10px] text-[#94A3B8]">{formatoFechaCorta(n.createdAt.slice(0, 10))}</p>
|
<p className="mt-1.5 text-[10px] text-[#94A3B8]">{formatoFechaHora(n.createdAt)}</p>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ export interface ActividadMiDia {
|
|||||||
tipo: string
|
tipo: string
|
||||||
fecha: string // 'YYYY-MM-DD'
|
fecha: string // 'YYYY-MM-DD'
|
||||||
estado: string
|
estado: string
|
||||||
|
/** Captura (lead/oportunidad/apartado) ligada a la actividad; null si no aplica */
|
||||||
|
leadCapturaId: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApartadoPorVencer {
|
export interface ApartadoPorVencer {
|
||||||
@@ -127,7 +129,7 @@ export interface ResumenMiDia {
|
|||||||
export const VOCABULARIO = {
|
export const VOCABULARIO = {
|
||||||
// Estados de la columna status del board Leads (ESTADOS_LEAD_PERMITIDOS del
|
// Estados de la columna status del board Leads (ESTADOS_LEAD_PERMITIDOS del
|
||||||
// API + NUEVO/DESCARTADO)
|
// API + NUEVO/DESCARTADO)
|
||||||
FASES_LEAD: ['NUEVO', 'CONTACTADO', 'Intento de contacto', 'INTERESADO', 'AGENDA CITA', 'CALIFICADO', 'DESCARTADO'],
|
FASES_LEAD: ['NUEVO', 'Intento de contacto', 'CONTACTADO', 'INTERESADO', 'AGENDA CITA', 'CALIFICADO', 'DESCARTADO'],
|
||||||
// Etapas del funnel de Oportunidades (labels REALES del board; el API las valida tal cual)
|
// Etapas del funnel de Oportunidades (labels REALES del board; el API las valida tal cual)
|
||||||
FASES_OPORTUNIDAD: ['Agendo Cita', 'Afluencia', 'Cotizador', 'Aparto', 'No Asistio', 'Abandono'],
|
FASES_OPORTUNIDAD: ['Agendo Cita', 'Afluencia', 'Cotizador', 'Aparto', 'No Asistio', 'Abandono'],
|
||||||
// Fases del pipeline Enkontrol del tablero Apartado (labels REALES del board)
|
// Fases del pipeline Enkontrol del tablero Apartado (labels REALES del board)
|
||||||
@@ -303,6 +305,18 @@ export function formatoFechaCorta(fecha: string): string {
|
|||||||
return `${d}/${m}`
|
return `${d}/${m}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** ISO → 'dd/mm/aaaa hh:mm' (hora local): bitácoras y notas con hora exacta */
|
||||||
|
export function formatoFechaHora(iso: string): string {
|
||||||
|
const d = new Date(iso)
|
||||||
|
if (Number.isNaN(d.getTime())) return iso
|
||||||
|
const dd = String(d.getDate()).padStart(2, '0')
|
||||||
|
const m = String(d.getMonth() + 1).padStart(2, '0')
|
||||||
|
const y = d.getFullYear()
|
||||||
|
const hh = String(d.getHours()).padStart(2, '0')
|
||||||
|
const mm = String(d.getMinutes()).padStart(2, '0')
|
||||||
|
return `${dd}/${m}/${y} ${hh}:${mm}`
|
||||||
|
}
|
||||||
|
|
||||||
/** Texto de urgencia de un apartado según los días que faltan */
|
/** Texto de urgencia de un apartado según los días que faltan */
|
||||||
export function textoDiasRestantes(dias: number): string {
|
export function textoDiasRestantes(dias: number): string {
|
||||||
if (dias <= 0) return 'Vence hoy'
|
if (dias <= 0) return 'Vence hoy'
|
||||||
|
|||||||
@@ -761,12 +761,12 @@ function FilaLead({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3 flex flex-col gap-2 sm:flex-row sm:flex-wrap sm:items-center">
|
<div className="mt-3 flex flex-col gap-2 sm:flex-row sm:flex-wrap sm:items-center">
|
||||||
<button type="button" disabled={enviando} onClick={() => onAccion('CONTACTADO')} className={CLASE_BOTON_ACCION}>
|
|
||||||
Contactado
|
|
||||||
</button>
|
|
||||||
<button type="button" disabled={enviando} onClick={() => onAccion('Intento de contacto')} className={CLASE_BOTON_ACCION}>
|
<button type="button" disabled={enviando} onClick={() => onAccion('Intento de contacto')} className={CLASE_BOTON_ACCION}>
|
||||||
Intento
|
Intento
|
||||||
</button>
|
</button>
|
||||||
|
<button type="button" disabled={enviando} onClick={() => onAccion('CONTACTADO')} className={CLASE_BOTON_ACCION}>
|
||||||
|
Contactado
|
||||||
|
</button>
|
||||||
<button type="button" disabled={enviando} onClick={() => onAccion('INTERESADO')} className={CLASE_BOTON_ACCION}>
|
<button type="button" disabled={enviando} onClick={() => onAccion('INTERESADO')} className={CLASE_BOTON_ACCION}>
|
||||||
Interesado
|
Interesado
|
||||||
</button>
|
</button>
|
||||||
@@ -844,6 +844,10 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
|
|||||||
})
|
})
|
||||||
const filtros = filtrosPorTab[tabCrm]
|
const filtros = filtrosPorTab[tabCrm]
|
||||||
const actualizarFiltros = (f: FiltrosCRM) => setFiltrosPorTab((prev) => ({ ...prev, [tabCrm]: f }))
|
const actualizarFiltros = (f: FiltrosCRM) => setFiltrosPorTab((prev) => ({ ...prev, [tabCrm]: f }))
|
||||||
|
// Filtro por vendedor: SOLO administradores. Un solo selector que aplica a
|
||||||
|
// las cuatro pestañas (leads / oportunidades / apartados / escrituración).
|
||||||
|
const esAdmin = rol === 'admin' || rol === 'manager'
|
||||||
|
const [filtroVendedor, setFiltroVendedor] = useState('')
|
||||||
// Estados posibles por tab: alimenta el select "Estado" de FiltrosAvanzados
|
// Estados posibles por tab: alimenta el select "Estado" de FiltrosAvanzados
|
||||||
const estadosPorTab: Partial<Record<TabCrm, readonly string[]>> = {
|
const estadosPorTab: Partial<Record<TabCrm, readonly string[]>> = {
|
||||||
leads: ESTADOS_LEAD,
|
leads: ESTADOS_LEAD,
|
||||||
@@ -861,21 +865,22 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
|
|||||||
// columnas de solo-display: su drop abre el modal de descarte o no hace nada.
|
// columnas de solo-display: su drop abre el modal de descarte o no hace nada.
|
||||||
const columnasLeads = [
|
const columnasLeads = [
|
||||||
{ id: 'NUEVO', titulo: 'Nuevo', color: 'border-t-gray-400' },
|
{ id: 'NUEVO', titulo: 'Nuevo', color: 'border-t-gray-400' },
|
||||||
{ id: 'CONTACTADO', titulo: 'Contactado', color: 'border-t-blue-500' },
|
|
||||||
{ id: 'Intento de contacto', titulo: 'Intento de contacto', color: 'border-t-sky-500' },
|
{ id: 'Intento de contacto', titulo: 'Intento de contacto', color: 'border-t-sky-500' },
|
||||||
|
{ id: 'CONTACTADO', titulo: 'Contactado', color: 'border-t-blue-500' },
|
||||||
{ id: 'INTERESADO', titulo: 'Interesado', color: 'border-t-yellow-500' },
|
{ id: 'INTERESADO', titulo: 'Interesado', color: 'border-t-yellow-500' },
|
||||||
{ id: 'AGENDA CITA', titulo: 'Agenda Cita', color: 'border-t-green-500' },
|
{ id: 'AGENDA CITA', titulo: 'Agenda Cita', color: 'border-t-green-500' },
|
||||||
{ id: 'CALIFICADO', titulo: 'Calificado', color: 'border-t-teal-500' },
|
{ id: 'CALIFICADO', titulo: 'Calificado', color: 'border-t-teal-500' },
|
||||||
{ id: 'DESCARTADO', titulo: 'Descartado', color: 'border-t-red-500' },
|
{ id: 'DESCARTADO', titulo: 'Descartado', color: 'border-t-red-500' },
|
||||||
]
|
]
|
||||||
|
// Mismo orden y mismas etapas que la vista de lista: Confirmó Cita →
|
||||||
|
// Afluencia → Cotizar → Apartó → Descartar. PERDIDO es solo-display: su
|
||||||
|
// drop abre el modal de descarte (mismo comportamiento que el botón).
|
||||||
const columnasOportunidades = [
|
const columnasOportunidades = [
|
||||||
|
{ id: 'Agendo Cita', titulo: 'Confirmó Cita', color: 'border-t-green-500' },
|
||||||
{ id: 'Afluencia', titulo: 'Afluencia', color: 'border-t-purple-500' },
|
{ id: 'Afluencia', titulo: 'Afluencia', color: 'border-t-purple-500' },
|
||||||
{ id: 'Cotizador', titulo: 'Cotizador', color: 'border-t-blue-500' },
|
{ id: 'Cotizador', titulo: 'Cotizar', 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: 'Aparto', titulo: 'Apartó', color: 'border-t-emerald-600' },
|
||||||
{ id: 'No Asistio', titulo: 'No Asistió', color: 'border-t-orange-500' },
|
{ id: 'PERDIDO', titulo: 'Descartar', color: 'border-t-red-500' },
|
||||||
{ id: 'Abandono', titulo: 'Abandono', color: 'border-t-yellow-600' },
|
|
||||||
{ id: 'PERDIDO', titulo: 'Perdido', color: 'border-t-red-500' },
|
|
||||||
]
|
]
|
||||||
const columnasApartados = [
|
const columnasApartados = [
|
||||||
{ id: 'Pago Apartado', titulo: 'Pago Apartado', color: 'border-t-blue-500' },
|
{ id: 'Pago Apartado', titulo: 'Pago Apartado', color: 'border-t-blue-500' },
|
||||||
@@ -910,11 +915,11 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
|
|||||||
const obtenerColumnaOp = (op: OportunidadMiDia) => {
|
const obtenerColumnaOp = (op: OportunidadMiDia) => {
|
||||||
const e = (op.etapa ?? '').toUpperCase()
|
const e = (op.etapa ?? '').toUpperCase()
|
||||||
if (e.includes('PERDIDO') || e.includes('PERDI')) return 'PERDIDO'
|
if (e.includes('PERDIDO') || e.includes('PERDI')) return 'PERDIDO'
|
||||||
if (e.includes('ASISTIO')) return 'No Asistio'
|
|
||||||
if (e.includes('ABANDONO')) return 'Abandono'
|
|
||||||
if (e.includes('APARTO')) return 'Aparto'
|
if (e.includes('APARTO')) return 'Aparto'
|
||||||
if (e.includes('COTIZ')) return 'Cotizador'
|
if (e.includes('COTIZ')) return 'Cotizador'
|
||||||
if (e.includes('AGENDO') || e.includes('CITA')) return 'Agendo Cita'
|
// 'No Asistio' vuelve a Confirmó Cita (se reagenda); 'Abandono' es
|
||||||
|
// terminal y ya no aparece en la lista de trabajo
|
||||||
|
if (e.includes('AGENDO') || e.includes('CITA') || e.includes('ASISTIO')) return 'Agendo Cita'
|
||||||
return 'Afluencia'
|
return 'Afluencia'
|
||||||
}
|
}
|
||||||
const obtenerColumnaApartado = (a: ApartadoMiDia) => {
|
const obtenerColumnaApartado = (a: ApartadoMiDia) => {
|
||||||
@@ -1074,6 +1079,24 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
|
|||||||
}, 'No se pudo mover el lead')
|
}, 'No se pudo mover el lead')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Click en una actividad de "Mis actividades": abre la bitácora del cliente
|
||||||
|
* ligado (lead, oportunidad o apartado). Si no hay ficha ligada, solo avisa.
|
||||||
|
*/
|
||||||
|
function abrirDetalleDesdeActividad(leadCapturaId: string | null) {
|
||||||
|
if (!resumen) return
|
||||||
|
const item =
|
||||||
|
leadCapturaId !== null
|
||||||
|
? resumen.misLeads.find((l) => l.id === leadCapturaId) ??
|
||||||
|
resumen.misOportunidades.items.find((o) => o.id === leadCapturaId) ??
|
||||||
|
resumen.misApartados.find((a) => a.id === leadCapturaId) ??
|
||||||
|
(resumen.todosApartados ?? []).find((a) => a.id === leadCapturaId) ??
|
||||||
|
null
|
||||||
|
: null
|
||||||
|
if (item) setLeadSeleccionado(item)
|
||||||
|
else toast.error('Esta actividad no tiene una ficha de cliente ligada')
|
||||||
|
}
|
||||||
|
|
||||||
async function enviarCaptura(e: FormEvent) {
|
async function enviarCaptura(e: FormEvent) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setCapEnviando(true)
|
setCapEnviando(true)
|
||||||
@@ -1111,9 +1134,11 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Listas filtradas por los filtros avanzados; las usan el render por pestaña
|
// Listas filtradas por los filtros avanzados; las usan el render por pestaña
|
||||||
// Y el botón de exportar (para que la exportación respete los filtros activos)
|
// Y el botón de exportar (para que la exportación respete los filtros activos).
|
||||||
|
// Admin: filtroVendedor ('' = todos) recorta además por vendedor asignado.
|
||||||
|
const coincideVendedor = (v: string) => filtroVendedor === '' || v === filtroVendedor
|
||||||
const leadsFiltrados = resumen
|
const leadsFiltrados = resumen
|
||||||
? aplicarFiltros(resumen.misLeads.filter((l) => l.fuente === 'lead'), filtros, {
|
? aplicarFiltros(resumen.misLeads.filter((l) => l.fuente === 'lead' && coincideVendedor(l.vendedor)), filtros, {
|
||||||
texto: (l) => `${l.nombre} ${l.telefono} ${l.email}`,
|
texto: (l) => `${l.nombre} ${l.telefono} ${l.email}`,
|
||||||
origen: (l) => l.origen,
|
origen: (l) => l.origen,
|
||||||
estado: (l) => l.estado,
|
estado: (l) => l.estado,
|
||||||
@@ -1121,7 +1146,7 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
|
|||||||
})
|
})
|
||||||
: []
|
: []
|
||||||
const opsFiltradas = resumen
|
const opsFiltradas = resumen
|
||||||
? aplicarFiltros(resumen.misOportunidades.items, filtros, {
|
? aplicarFiltros(resumen.misOportunidades.items.filter((o) => coincideVendedor(o.vendedor)), filtros, {
|
||||||
texto: (o) => `${o.nombre} ${o.telefono} ${o.email}`,
|
texto: (o) => `${o.nombre} ${o.telefono} ${o.email}`,
|
||||||
desarrollo: (o) => o.desarrollo,
|
desarrollo: (o) => o.desarrollo,
|
||||||
estado: (o) => o.etapa,
|
estado: (o) => o.etapa,
|
||||||
@@ -1129,7 +1154,7 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
|
|||||||
})
|
})
|
||||||
: []
|
: []
|
||||||
const apartadosFiltrados = resumen
|
const apartadosFiltrados = resumen
|
||||||
? aplicarFiltros(resumen.misApartados, filtros, {
|
? aplicarFiltros(resumen.misApartados.filter((a) => coincideVendedor(a.vendedor)), filtros, {
|
||||||
texto: (a) => a.nombre,
|
texto: (a) => a.nombre,
|
||||||
estado: (a) => a.estado,
|
estado: (a) => a.estado,
|
||||||
fecha: (a) => a.fechaLimite,
|
fecha: (a) => a.fechaLimite,
|
||||||
@@ -1140,6 +1165,7 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
|
|||||||
// para que el conteo del tab refleje la lista filtrada de escrituración.
|
// para que el conteo del tab refleje la lista filtrada de escrituración.
|
||||||
const enEscrituracion = resumen
|
const enEscrituracion = resumen
|
||||||
? (resumen.todosApartados || resumen.misApartados).filter((a) => {
|
? (resumen.todosApartados || resumen.misApartados).filter((a) => {
|
||||||
|
if (!coincideVendedor(a.vendedor)) return false
|
||||||
const e = (a.estado ?? '').toUpperCase()
|
const e = (a.estado ?? '').toUpperCase()
|
||||||
return (
|
return (
|
||||||
e.includes('FIRMA') ||
|
e.includes('FIRMA') ||
|
||||||
@@ -1163,6 +1189,20 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
|
|||||||
fecha: (a) => a.fechaLimite,
|
fecha: (a) => a.fechaLimite,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Vendedores con registros visibles: alimenta el filtro por vendedor (admin)
|
||||||
|
const vendedoresOpciones = resumen
|
||||||
|
? [
|
||||||
|
...new Set(
|
||||||
|
[
|
||||||
|
...resumen.misLeads.map((l) => l.vendedor),
|
||||||
|
...resumen.misOportunidades.items.map((o) => o.vendedor),
|
||||||
|
...resumen.misApartados.map((a) => a.vendedor),
|
||||||
|
...(resumen.todosApartados ?? []).map((a) => a.vendedor),
|
||||||
|
].filter((v) => v.trim() !== ''),
|
||||||
|
),
|
||||||
|
].sort((a, b) => a.localeCompare(b))
|
||||||
|
: []
|
||||||
|
|
||||||
const hoy = hoyLocal()
|
const hoy = hoyLocal()
|
||||||
const iniciales = nombreVendedor
|
const iniciales = nombreVendedor
|
||||||
.split(' ')
|
.split(' ')
|
||||||
@@ -1294,6 +1334,19 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
|
|||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<FiltrosAvanzados valor={filtros} onChange={actualizarFiltros} estados={estadosPorTab[tabCrm]} />
|
<FiltrosAvanzados valor={filtros} onChange={actualizarFiltros} estados={estadosPorTab[tabCrm]} />
|
||||||
</div>
|
</div>
|
||||||
|
{esAdmin && (
|
||||||
|
<select
|
||||||
|
aria-label="Filtrar por vendedor"
|
||||||
|
value={filtroVendedor}
|
||||||
|
onChange={(e) => setFiltroVendedor(e.target.value)}
|
||||||
|
className="shrink-0 rounded-lg border border-[#D1D5DB] bg-white px-2 py-2 text-xs font-medium text-[#374151] shadow-sm transition-colors focus:border-accent focus:outline-none dark:bg-[#0F172A] dark:border-[#334155] dark:text-[#CBD5E1]"
|
||||||
|
>
|
||||||
|
<option value="">Todos los vendedores</option>
|
||||||
|
{vendedoresOpciones.map((v) => (
|
||||||
|
<option key={v} value={v}>{v}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -1340,7 +1393,7 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
|
|||||||
onDescartar={() => setDescartando(lead)}
|
onDescartar={() => setDescartando(lead)}
|
||||||
onVerDetalle={() => setLeadSeleccionado(lead)}
|
onVerDetalle={() => setLeadSeleccionado(lead)}
|
||||||
acciones={acciones}
|
acciones={acciones}
|
||||||
esAdmin={rol === 'admin' || rol === 'manager'}
|
esAdmin={esAdmin}
|
||||||
vendedores={resumen.equipo?.porVendedor.map((v) => ({ id: v.usuarioId, nombre: v.nombre ?? '' }))}
|
vendedores={resumen.equipo?.porVendedor.map((v) => ({ id: v.usuarioId, nombre: v.nombre ?? '' }))}
|
||||||
onReasignar={(usuarioId) => void reasignar(lead, usuarioId)}
|
onReasignar={(usuarioId) => void reasignar(lead, usuarioId)}
|
||||||
onMover={() => void mover(lead)}
|
onMover={() => void mover(lead)}
|
||||||
@@ -1615,7 +1668,7 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<aside className={`order-1 flex flex-col gap-6 ${vistaKanban && tabCrm === 'leads' ? 'hidden' : 'md:order-2 lg:sticky lg:top-20 lg:self-start'}`}>
|
<aside className={`order-1 flex flex-col gap-6 ${vistaKanban && tabCrm === 'leads' ? 'hidden' : 'md:order-2 lg:sticky lg:top-20 lg:self-start'}`}>
|
||||||
<section aria-label="Mis actividades">
|
<section aria-label="Mis actividades" className="order-2">
|
||||||
<TituloSeccion>Mis actividades</TituloSeccion>
|
<TituloSeccion>Mis actividades</TituloSeccion>
|
||||||
{resumen.misActividadesHoy.length === 0 ? (
|
{resumen.misActividadesHoy.length === 0 ? (
|
||||||
<EstadoVacio mensaje="Sin actividades para hoy" />
|
<EstadoVacio mensaje="Sin actividades para hoy" />
|
||||||
@@ -1624,19 +1677,26 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
|
|||||||
{resumen.misActividadesHoy.map((a) => (
|
{resumen.misActividadesHoy.map((a) => (
|
||||||
<li
|
<li
|
||||||
key={a.id}
|
key={a.id}
|
||||||
className="flex items-center justify-between gap-3 rounded-xl border border-[#E5E7EB] bg-white px-4 py-3 shadow-sm dark:bg-[#1E293B] dark:border-[#1E293B]"
|
className="rounded-xl border border-[#E5E7EB] bg-white shadow-sm dark:bg-[#1E293B] dark:border-[#1E293B]"
|
||||||
>
|
>
|
||||||
<p className="text-sm">
|
<button
|
||||||
{a.nombre}
|
type="button"
|
||||||
{a.tipo ? <span className="ml-2 text-xs uppercase tracking-wider text-[#94A3B8]">{a.tipo}</span> : null}
|
onClick={() => abrirDetalleDesdeActividad(a.leadCapturaId ?? null)}
|
||||||
</p>
|
title={a.leadCapturaId ? 'Ver información del cliente' : 'Actividad sin cliente ligado'}
|
||||||
<span
|
className="flex w-full items-center justify-between gap-3 px-4 py-3 text-left transition-colors hover:bg-[#F9FAFB] dark:hover:bg-[#0F172A]"
|
||||||
className={`text-xs font-medium ${
|
|
||||||
esActividadVencida(a.fecha, hoy) ? 'text-red-600' : 'text-[#64748B] dark:text-[#94A3B8]'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{formatoFechaCorta(a.fecha)}
|
<p className="text-sm">
|
||||||
</span>
|
{a.nombre}
|
||||||
|
{a.tipo ? <span className="ml-2 text-xs uppercase tracking-wider text-[#94A3B8]">{a.tipo}</span> : null}
|
||||||
|
</p>
|
||||||
|
<span
|
||||||
|
className={`shrink-0 text-xs font-medium ${
|
||||||
|
esActividadVencida(a.fecha, hoy) ? 'text-red-600' : 'text-[#64748B] dark:text-[#94A3B8]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{formatoFechaCorta(a.fecha)}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -1644,7 +1704,7 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{resumen.apartadosPorVencer.length > 0 && (
|
{resumen.apartadosPorVencer.length > 0 && (
|
||||||
<section aria-label="Apartados por vencer">
|
<section aria-label="Apartados por vencer" className="order-3">
|
||||||
<TituloSeccion>Apartados por vencer</TituloSeccion>
|
<TituloSeccion>Apartados por vencer</TituloSeccion>
|
||||||
<ul className="space-y-2">
|
<ul className="space-y-2">
|
||||||
{resumen.apartadosPorVencer.map((a) => (
|
{resumen.apartadosPorVencer.map((a) => (
|
||||||
@@ -1669,7 +1729,7 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
|
|||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<section aria-label="Captura rápida de lead">
|
<section aria-label="Captura rápida de lead" className="order-1">
|
||||||
<TituloSeccion>Captura rápida de lead</TituloSeccion>
|
<TituloSeccion>Captura rápida de lead</TituloSeccion>
|
||||||
<form
|
<form
|
||||||
onSubmit={enviarCaptura}
|
onSubmit={enviarCaptura}
|
||||||
|
|||||||
Reference in New Issue
Block a user