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:
87
api-gateway/src/services/push.service.ts
Normal file
87
api-gateway/src/services/push.service.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import webPush from 'web-push'
|
||||
import { config } from '../config/index.js'
|
||||
import { prisma } from './prisma.js'
|
||||
|
||||
// Configurar VAPID solo si las claves están presentes
|
||||
if (config.push.vapidPublicKey && config.push.vapidPrivateKey) {
|
||||
webPush.setVapidDetails(
|
||||
config.push.vapidEmail,
|
||||
config.push.vapidPublicKey,
|
||||
config.push.vapidPrivateKey,
|
||||
)
|
||||
}
|
||||
|
||||
export interface PushSubscriptionInput {
|
||||
endpoint: string
|
||||
p256dh: string
|
||||
auth: string
|
||||
}
|
||||
|
||||
/** Registra o actualiza la suscripción push de un usuario */
|
||||
export async function registrarSuscripcion(usuarioId: string, subscription: PushSubscriptionInput): Promise<void> {
|
||||
await prisma.pushSubscription.upsert({
|
||||
where: { usuarioId },
|
||||
update: {
|
||||
endpoint: subscription.endpoint,
|
||||
p256dh: subscription.p256dh,
|
||||
auth: subscription.auth,
|
||||
},
|
||||
create: {
|
||||
usuarioId,
|
||||
endpoint: subscription.endpoint,
|
||||
p256dh: subscription.p256dh,
|
||||
auth: subscription.auth,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** Elimina la suscripción push de un usuario */
|
||||
export async function eliminarSuscripcion(usuarioId: string): Promise<void> {
|
||||
await prisma.pushSubscription.deleteMany({ where: { usuarioId } })
|
||||
}
|
||||
|
||||
/** Devuelve la VAPID public key para el frontend */
|
||||
export function obtenerVapidPublicKey(): string {
|
||||
return config.push.vapidPublicKey
|
||||
}
|
||||
|
||||
/** Busca el usuarioId local a partir de un mondayUserId */
|
||||
export async function usuarioIdPorMondayUserId(mondayUserId: number): Promise<string | null> {
|
||||
const usuario = await prisma.usuario.findFirst({
|
||||
where: { mondayUserId },
|
||||
select: { id: true },
|
||||
})
|
||||
return usuario?.id ?? null
|
||||
}
|
||||
|
||||
/** Envía una notificación push a un usuario. Silencioso si no tiene suscripción o si web-push no está configurado. */
|
||||
export async function enviarPush(
|
||||
usuarioId: string,
|
||||
titulo: string,
|
||||
cuerpo: string,
|
||||
url?: string,
|
||||
): Promise<boolean> {
|
||||
if (!config.push.vapidPublicKey || !config.push.vapidPrivateKey) return false
|
||||
|
||||
const sub = await prisma.pushSubscription.findUnique({ where: { usuarioId } })
|
||||
if (!sub) return false
|
||||
|
||||
const payload = JSON.stringify({ titulo, cuerpo, url: url ?? '/' })
|
||||
|
||||
try {
|
||||
await webPush.sendNotification(
|
||||
{ endpoint: sub.endpoint, keys: { p256dh: sub.p256dh, auth: sub.auth } },
|
||||
payload,
|
||||
)
|
||||
return true
|
||||
} catch (err: unknown) {
|
||||
// Si la suscripción ya no es válida (404 o 410), eliminarla
|
||||
const status = (err as { statusCode?: number }).statusCode
|
||||
if (status === 404 || status === 410) {
|
||||
await prisma.pushSubscription.deleteMany({ where: { usuarioId } })
|
||||
}
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`[push] No se pudo enviar a ${usuarioId}:`, err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user