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:
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE "RecordatorioVendedor" ADD COLUMN "clienteClave" TEXT;
|
||||||
|
CREATE INDEX IF NOT EXISTS "RecordatorioVendedor_clienteClave_idx" ON "RecordatorioVendedor"("clienteClave");
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.') }
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
Reference in New Issue
Block a user