diff --git a/api-gateway/src/config/vocabulario.ts b/api-gateway/src/config/vocabulario.ts
index 5e3cbc2..35c4fd5 100644
--- a/api-gateway/src/config/vocabulario.ts
+++ b/api-gateway/src/config/vocabulario.ts
@@ -13,7 +13,7 @@
export const VOCABULARIO = {
// Estados de la columna status del board Leads: ESTADOS_LEAD_PERMITIDOS del
// 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')
FASES_OPORTUNIDAD: ['Agendo Cita', 'Afluencia', 'Cotizador', 'Aparto', 'No Asistio', 'Abandono'],
// Fases del pipeline Enkontrol del tablero Apartado (labels REALES de la
diff --git a/api-gateway/src/services/mi-dia.service.ts b/api-gateway/src/services/mi-dia.service.ts
index 01632d5..95161fa 100644
--- a/api-gateway/src/services/mi-dia.service.ts
+++ b/api-gateway/src/services/mi-dia.service.ts
@@ -85,6 +85,8 @@ export interface ActividadMiDia {
tipo: string
fecha: string
estado: string
+ /** Captura (lead/oportunidad/apartado) a la que pertenece la actividad; null si no está ligada */
+ leadCapturaId: string | null
}
export interface ApartadoPorVencer {
@@ -450,6 +452,7 @@ export function resumenMiDiaLocal(
tipo: a.tipo,
fecha: a.fechaVencimiento ? a.fechaVencimiento.toISOString().slice(0, 10) : '',
estado: a.estado,
+ leadCapturaId: a.leadCapturaId,
}))
const todasMisActividades: ActividadMiDia[] = actividadesAbiertasOrdenadas
const misActividadesHoy: ActividadMiDia[] = actividadesAbiertasOrdenadas
diff --git a/apps/mi-dia/src/components/DetalleLeadModal.tsx b/apps/mi-dia/src/components/DetalleLeadModal.tsx
index d2b0502..8b0e36e 100644
--- a/apps/mi-dia/src/components/DetalleLeadModal.tsx
+++ b/apps/mi-dia/src/components/DetalleLeadModal.tsx
@@ -1,7 +1,7 @@
import { useCallback, useEffect, useRef, 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, urlWhatsApp } from '../lib/mi-dia'
+import { formatoFechaCorta, formatoFechaHora, urlWhatsApp } from '../lib/mi-dia'
import { listarRecordatorios, crearRecordatorio, completarRecordatorio, type Recordatorio } from '../services/recordatorios'
import { getSesion } from '../services/auth'
@@ -251,7 +251,7 @@ export default function DetalleLeadModal({
onChange={() => void completarTarea(n.id)}
/>
{n.titulo}
- {formatoFechaCorta(n.createdAt.slice(0, 10))}
+ {formatoFechaHora(n.createdAt)}
))}
@@ -311,7 +311,7 @@ export default function DetalleLeadModal({
{n.titulo}
{n.descripcion && {n.descripcion}
}
- {formatoFechaCorta(n.createdAt.slice(0, 10))}
+ {formatoFechaHora(n.createdAt)}
))}
diff --git a/apps/mi-dia/src/lib/mi-dia.ts b/apps/mi-dia/src/lib/mi-dia.ts
index ac3d8ad..afc5ab8 100644
--- a/apps/mi-dia/src/lib/mi-dia.ts
+++ b/apps/mi-dia/src/lib/mi-dia.ts
@@ -23,6 +23,8 @@ export interface ActividadMiDia {
tipo: string
fecha: string // 'YYYY-MM-DD'
estado: string
+ /** Captura (lead/oportunidad/apartado) ligada a la actividad; null si no aplica */
+ leadCapturaId: string | null
}
export interface ApartadoPorVencer {
@@ -127,7 +129,7 @@ export interface ResumenMiDia {
export const VOCABULARIO = {
// Estados de la columna status del board Leads (ESTADOS_LEAD_PERMITIDOS del
// 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)
FASES_OPORTUNIDAD: ['Agendo Cita', 'Afluencia', 'Cotizador', 'Aparto', 'No Asistio', 'Abandono'],
// Fases del pipeline Enkontrol del tablero Apartado (labels REALES del board)
@@ -303,6 +305,18 @@ export function formatoFechaCorta(fecha: string): string {
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 */
export function textoDiasRestantes(dias: number): string {
if (dias <= 0) return 'Vence hoy'
diff --git a/apps/mi-dia/src/pages/MiDiaPage.tsx b/apps/mi-dia/src/pages/MiDiaPage.tsx
index b234c49..a617b8e 100644
--- a/apps/mi-dia/src/pages/MiDiaPage.tsx
+++ b/apps/mi-dia/src/pages/MiDiaPage.tsx
@@ -761,12 +761,12 @@ function FilaLead({
-
+
@@ -844,6 +844,10 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
})
const filtros = filtrosPorTab[tabCrm]
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
const estadosPorTab: Partial
> = {
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.
const columnasLeads = [
{ 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: 'CONTACTADO', titulo: 'Contactado', color: 'border-t-blue-500' },
{ id: 'INTERESADO', titulo: 'Interesado', color: 'border-t-yellow-500' },
{ id: 'AGENDA CITA', titulo: 'Agenda Cita', color: 'border-t-green-500' },
{ id: 'CALIFICADO', titulo: 'Calificado', color: 'border-t-teal-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 = [
+ { id: 'Agendo Cita', titulo: 'Confirmó Cita', color: 'border-t-green-500' },
{ 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: 'Cotizador', titulo: 'Cotizar', color: 'border-t-blue-500' },
{ id: 'Aparto', titulo: 'Apartó', color: 'border-t-emerald-600' },
- { id: 'No Asistio', titulo: 'No Asistió', color: 'border-t-orange-500' },
- { id: 'Abandono', titulo: 'Abandono', color: 'border-t-yellow-600' },
- { id: 'PERDIDO', titulo: 'Perdido', color: 'border-t-red-500' },
+ { id: 'PERDIDO', titulo: 'Descartar', color: 'border-t-red-500' },
]
const columnasApartados = [
{ 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 e = (op.etapa ?? '').toUpperCase()
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('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'
}
const obtenerColumnaApartado = (a: ApartadoMiDia) => {
@@ -1074,6 +1079,24 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
}, '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) {
e.preventDefault()
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
- // 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
- ? 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}`,
origen: (l) => l.origen,
estado: (l) => l.estado,
@@ -1121,7 +1146,7 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
})
: []
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}`,
desarrollo: (o) => o.desarrollo,
estado: (o) => o.etapa,
@@ -1129,7 +1154,7 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
})
: []
const apartadosFiltrados = resumen
- ? aplicarFiltros(resumen.misApartados, filtros, {
+ ? aplicarFiltros(resumen.misApartados.filter((a) => coincideVendedor(a.vendedor)), filtros, {
texto: (a) => a.nombre,
estado: (a) => a.estado,
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.
const enEscrituracion = resumen
? (resumen.todosApartados || resumen.misApartados).filter((a) => {
+ if (!coincideVendedor(a.vendedor)) return false
const e = (a.estado ?? '').toUpperCase()
return (
e.includes('FIRMA') ||
@@ -1163,6 +1189,20 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
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 iniciales = nombreVendedor
.split(' ')
@@ -1294,6 +1334,19 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
+ {esAdmin && (
+
+ )}