feat(mejoras): 14 mejoras al CRM Frasa
Fase 1 - UI: dark mode, skeletons, toasts Fase 2 - Filtros: avanzados Mi día, búsqueda global Ctrl+K Fase 3 - Datos: export Excel, notas apartados, auditoría Fase 4 - Dashboard: gráficas vendedor + métricas uso Fase 5 - Responsive: tablets optimizadas Fase 6 - PWA: IndexedDB offline, push notifications Backend: 3 tablas nuevas, 6 services, 6 endpoints nuevos 721 API tests + fronts verdes.
This commit is contained in:
111
api-gateway/src/services/buscar.service.ts
Normal file
111
api-gateway/src/services/buscar.service.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { prisma } from './prisma.js'
|
||||
|
||||
export interface BuscadoItem {
|
||||
tipo: 'lead' | 'oportunidad' | 'contacto' | 'unidad'
|
||||
id: string
|
||||
nombre: string
|
||||
detalle: string
|
||||
url: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Búsqueda global: LeadCaptura (leads), Cliente (contactos), Cotizacion
|
||||
* (oportunidades con cotización), y Unidad (inventario).
|
||||
* Case-insensitive sobre los campos indicados; limita a `limite` resultados.
|
||||
*/
|
||||
export async function buscar(termino: string, limite = 20): Promise<BuscadoItem[]> {
|
||||
const q = termino.trim()
|
||||
if (q === '') return []
|
||||
const pat = `%${q}%`
|
||||
|
||||
const [leads, clientes, cotizaciones, unidades] = await Promise.all([
|
||||
prisma.leadCaptura.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{ nombreLead: { contains: q } },
|
||||
{ telefono: { contains: q } },
|
||||
{ email: { contains: q } },
|
||||
],
|
||||
},
|
||||
orderBy: { updatedAt: 'desc' },
|
||||
take: limite,
|
||||
}),
|
||||
prisma.cliente.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{ nombreCompleto: { contains: q } },
|
||||
{ curp: { contains: q } },
|
||||
{ rfc: { contains: q } },
|
||||
{ email: { contains: q } },
|
||||
{ celular: { contains: q } },
|
||||
],
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: limite,
|
||||
}),
|
||||
prisma.cotizacion.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{ clienteNombre: { contains: q } },
|
||||
{ clienteEmail: { contains: q } },
|
||||
{ unidadCodigo: { contains: q } },
|
||||
],
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: limite,
|
||||
}),
|
||||
prisma.unidad.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{ codigo: { contains: q } },
|
||||
{ desarrollo: { contains: q } },
|
||||
],
|
||||
},
|
||||
orderBy: { updatedAt: 'desc' },
|
||||
take: limite,
|
||||
}),
|
||||
])
|
||||
|
||||
const items: BuscadoItem[] = [
|
||||
...leads.map((l) => ({
|
||||
tipo: 'lead' as const,
|
||||
id: l.leadId,
|
||||
nombre: l.nombreLead,
|
||||
detalle: [l.telefono, l.email].filter(Boolean).join(' · ') || l.origen || l.board,
|
||||
url: `/app/#leads`,
|
||||
})),
|
||||
...clientes.map((c) => ({
|
||||
tipo: 'contacto' as const,
|
||||
id: c.id,
|
||||
nombre: c.nombreCompleto,
|
||||
detalle: [c.email, c.celular, c.proyecto].filter(Boolean).join(' · ') || '',
|
||||
url: `/app/#clientes`,
|
||||
})),
|
||||
...cotizaciones.map((ct) => ({
|
||||
tipo: 'oportunidad' as const,
|
||||
id: ct.id,
|
||||
nombre: ct.clienteNombre,
|
||||
detalle: [ct.unidadCodigo, ct.desarrollo].filter(Boolean).join(' · ') || ct.tipoPropuesta,
|
||||
url: `/quote/?cotizacion=${ct.id}`,
|
||||
})),
|
||||
...unidades.map((u) => ({
|
||||
tipo: 'unidad' as const,
|
||||
id: u.id,
|
||||
nombre: u.codigo,
|
||||
detalle: [u.desarrollo, u.modelo, u.status].filter(Boolean).join(' · '),
|
||||
url: `/quote/?unidad=${u.codigo}`,
|
||||
})),
|
||||
]
|
||||
|
||||
// Mezclar y limitar: prioridad por tipo (leads primero, luego contactos,
|
||||
// oportunidades, unidades) y recientes primero (ya ordenados por fecha desc)
|
||||
const porTipo: BuscadoItem['tipo'][] = ['lead', 'contacto', 'oportunidad', 'unidad']
|
||||
items.sort((a, b) => {
|
||||
const ia = porTipo.indexOf(a.tipo)
|
||||
const ib = porTipo.indexOf(b.tipo)
|
||||
if (ia !== ib) return ia - ib
|
||||
return 0
|
||||
})
|
||||
|
||||
return items.slice(0, limite)
|
||||
}
|
||||
Reference in New Issue
Block a user