fix(notas): notas persisten entre etapas via clienteClave

- Campo clienteClave en RecordatorioVendedor (hash nombre+telefono)
- Al crear nota: se calcula clienteClave del lead/cliente
- Al buscar notas: busca por leadId O clienteClave (no por nombre suelto)
- Mantiene compatibilidad: notas viejas sin clienteClave siguen por leadId
- Migración SQL aplicada
This commit is contained in:
2026-09-10 22:55:11 +00:00
parent 5a50243dff
commit b1e408f435
6 changed files with 35 additions and 4 deletions

View File

@@ -0,0 +1,2 @@
ALTER TABLE "RecordatorioVendedor" ADD COLUMN "clienteClave" TEXT;
CREATE INDEX IF NOT EXISTS "RecordatorioVendedor_clienteClave_idx" ON "RecordatorioVendedor"("clienteClave");

View File

@@ -420,6 +420,7 @@ model RecordatorioVendedor {
id String @id @default(uuid()) id String @id @default(uuid())
usuarioId String usuarioId String
leadId String? leadId String?
clienteClave String?
titulo String titulo String
descripcion String? descripcion String?
fechaRecordar DateTime fechaRecordar DateTime

View File

@@ -9,6 +9,7 @@ const crearSchema = z.object({
titulo: z.string().min(1), titulo: z.string().min(1),
descripcion: z.string().optional(), descripcion: z.string().optional(),
leadId: z.string().optional(), leadId: z.string().optional(),
clienteClave: z.string().optional(),
fechaRecordar: z.string().min(1), fechaRecordar: z.string().min(1),
}) })
@@ -24,6 +25,7 @@ export async function crearRecordatorioEndpoint(req: Request, res: Response): Pr
titulo: body.titulo, titulo: body.titulo,
descripcion: body.descripcion ?? null, descripcion: body.descripcion ?? null,
leadId: body.leadId ?? null, leadId: body.leadId ?? null,
clienteClave: body.clienteClave ?? null,
fechaRecordar: body.fechaRecordar, fechaRecordar: body.fechaRecordar,
}) })
res.status(201).json(recordatorio) res.status(201).json(recordatorio)
@@ -38,9 +40,10 @@ export async function listarRecordatoriosEndpoint(req: Request, res: Response):
if (!usuarioId) throw new Error('Auth requerida') if (!usuarioId) throw new Error('Auth requerida')
const completadosStr = req.query['completados'] as string | undefined const completadosStr = req.query['completados'] as string | undefined
const opts: { completados?: boolean; desde?: string | null; hasta?: string | null } = { const opts: { completados?: boolean; desde?: string | null; hasta?: string | null; clienteClave?: string | null } = {
desde: (req.query['desde'] as string) ?? null, desde: (req.query['desde'] as string) ?? null,
hasta: (req.query['hasta'] as string) ?? null, hasta: (req.query['hasta'] as string) ?? null,
clienteClave: (req.query['clienteClave'] as string) ?? null,
} }
if (completadosStr === 'true') opts.completados = true if (completadosStr === 'true') opts.completados = true
else if (completadosStr === 'false') opts.completados = false else if (completadosStr === 'false') opts.completados = false
@@ -78,6 +81,7 @@ export function crearController(svc: RecordatorioVendedorService) {
titulo: body.titulo, titulo: body.titulo,
descripcion: body.descripcion ?? null, descripcion: body.descripcion ?? null,
leadId: body.leadId ?? null, leadId: body.leadId ?? null,
clienteClave: body.clienteClave ?? null,
fechaRecordar: body.fechaRecordar, fechaRecordar: body.fechaRecordar,
}) })
res.status(201).json(r) res.status(201).json(r)
@@ -92,6 +96,7 @@ export function crearController(svc: RecordatorioVendedorService) {
} }
if (completadosStr === 'true') opts.completados = true if (completadosStr === 'true') opts.completados = true
else if (completadosStr === 'false') opts.completados = false else if (completadosStr === 'false') opts.completados = false
opts.clienteClave = (req.query['clienteClave'] as string) ?? null
const items = await svc.listar(usuarioId, opts) const items = await svc.listar(usuarioId, opts)
res.json(items) res.json(items)
}, },

View File

@@ -7,10 +7,18 @@ import { NotFoundError } from '../utils/errors.js'
* muestra en "Mis recordatorios" de Mi día. * muestra en "Mis recordatorios" de Mi día.
*/ */
/** Calcula una clave estable del cliente: nombre normalizado + últimos 10 dígitos del teléfono */
export function calcularClienteClave(nombre?: string | null, telefono?: string | null): string {
const n = (nombre ?? '').trim().toLowerCase()
const t = (telefono ?? '').replace(/\s+/g, '').slice(-10)
return `${n}|${t}`
}
export interface DatosCrearRecordatorio { export interface DatosCrearRecordatorio {
titulo: string titulo: string
descripcion?: string | null descripcion?: string | null
leadId?: string | null leadId?: string | null
clienteClave?: string | null
fechaRecordar: string // ISO o YYYY-MM-DDTHH:mm fechaRecordar: string // ISO o YYYY-MM-DDTHH:mm
} }
@@ -18,6 +26,7 @@ export interface OpcionesListarRecordatorio {
completados?: boolean completados?: boolean
desde?: string | null // ISO desde?: string | null // ISO
hasta?: string | null // ISO hasta?: string | null // ISO
clienteClave?: string | null
} }
export class RecordatorioVendedorService { export class RecordatorioVendedorService {
@@ -28,6 +37,7 @@ export class RecordatorioVendedorService {
titulo: data.titulo.trim(), titulo: data.titulo.trim(),
descripcion: data.descripcion?.trim() ?? null, descripcion: data.descripcion?.trim() ?? null,
leadId: data.leadId ?? null, leadId: data.leadId ?? null,
clienteClave: data.clienteClave ?? null,
fechaRecordar: new Date(data.fechaRecordar), fechaRecordar: new Date(data.fechaRecordar),
}, },
}) })
@@ -36,6 +46,9 @@ export class RecordatorioVendedorService {
async listar(usuarioId: string, opciones: OpcionesListarRecordatorio = {}) { async listar(usuarioId: string, opciones: OpcionesListarRecordatorio = {}) {
const where: Record<string, unknown> = { usuarioId } const where: Record<string, unknown> = { usuarioId }
if (opciones.clienteClave) {
where['clienteClave'] = opciones.clienteClave
}
if (opciones.completados !== undefined) { if (opciones.completados !== undefined) {
where['completado'] = opciones.completados where['completado'] = opciones.completados
} }

View File

@@ -6,6 +6,13 @@ import { listarRecordatorios, crearRecordatorio, type Recordatorio } from '../se
type LeadItem = LeadMiDia | OportunidadMiDia | ApartadoMiDia type LeadItem = LeadMiDia | OportunidadMiDia | ApartadoMiDia
/** Calcula una clave estable del cliente: nombre normalizado + últimos 10 dígitos del teléfono */
function calcularClienteClave(nombre?: string | null, telefono?: string | null): string {
const n = (nombre ?? '').trim().toLowerCase()
const t = (telefono ?? '').replace(/\s+/g, '').slice(-10)
return `${n}|${t}`
}
function esLead(item: LeadItem): item is LeadMiDia { function esLead(item: LeadItem): item is LeadMiDia {
return 'email' in item && 'origen' in item && !('modeloInteres' in item) return 'email' in item && 'origen' in item && !('modeloInteres' in item)
} }
@@ -40,7 +47,8 @@ export default function DetalleLeadModal({ item, onCerrar }: { item: LeadItem; o
setCargandoNotas(true) setCargandoNotas(true)
try { try {
const todas = await listarRecordatorios() const todas = await listarRecordatorios()
setNotas(todas.filter((r) => r.leadId === item.id || r.titulo?.toLowerCase().includes(item.nombre.toLowerCase()) || r.descripcion?.toLowerCase().includes(item.nombre.toLowerCase()))) const clave = calcularClienteClave(item.nombre, (item as any).telefono)
setNotas(todas.filter((r) => r.leadId === item.id || (r.clienteClave && r.clienteClave === clave)))
} catch { setNotaError('Error al cargar notas.') } } catch { setNotaError('Error al cargar notas.') }
finally { setCargandoNotas(false) } finally { setCargandoNotas(false) }
}, [item.id]) }, [item.id])
@@ -52,7 +60,7 @@ export default function DetalleLeadModal({ item, onCerrar }: { item: LeadItem; o
if (!nuevaNota.trim()) return if (!nuevaNota.trim()) return
setEnviandoNota(true) setEnviandoNota(true)
try { try {
await crearRecordatorio({ titulo: nuevaNota.trim(), leadId: item.id, fechaRecordar: new Date().toISOString() }) await crearRecordatorio({ titulo: nuevaNota.trim(), leadId: item.id, clienteClave: calcularClienteClave(item.nombre, (item as any).telefono), fechaRecordar: new Date().toISOString() })
setNuevaNota('') setNuevaNota('')
await cargarNotas() await cargarNotas()
} catch { setNotaError('No se pudo guardar.') } } catch { setNotaError('No se pudo guardar.') }

View File

@@ -6,7 +6,8 @@ export interface Recordatorio {
id: string id: string
titulo: string titulo: string
descripcion: string | null descripcion: string | null
leadId: string | null leadId: string
clienteClave: string | null
fechaRecordar: string // ISO fechaRecordar: string // ISO
completado: boolean completado: boolean
createdAt: string createdAt: string
@@ -28,6 +29,7 @@ export async function crearRecordatorio(data: {
titulo: string titulo: string
descripcion?: string descripcion?: string
leadId: string leadId: string
clienteClave?: string
fechaRecordar: string fechaRecordar: string
}): Promise<Recordatorio> { }): Promise<Recordatorio> {
const res = await fetch(`${API_URL}/recordatorios`, conAuth({ const res = await fetch(`${API_URL}/recordatorios`, conAuth({