FR-6.1/6.2: Sandra recebe push quando pedido entra em pending_approval; Rafael recebe quando pedido é aprovado ou recusado. Service worker registrado em background (PWA-ready via public/sw.js). FR-6.3: Badge na Topbar busca GET /notifications/pending-count (supervisores veem count de pending_approval; reps veem 0). Intervalo de 30s. FR-6.4: Botão Compartilhar no OrderDetailPage para pedidos approved/invoiced (apenas reps). Usa navigator.share() com texto formatado para WhatsApp. Infra: modelo PushSubscription (Prisma), NotificationsModule (subscribe/ unsubscribe/pending-count + PushService VAPID), VAPID keys em .env, integração no OrdersService (create → supervisores, approve/reject → repId). Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
74 lines
2.5 KiB
TypeScript
74 lines
2.5 KiB
TypeScript
import { Injectable } from '@nestjs/common';
|
|
import { ClsService } from 'nestjs-cls';
|
|
import { OrderStatus } from '@prisma/client';
|
|
import type { SubscribePayload, PendingCountResponse } from '@sar/api-interface';
|
|
import type { WorkspaceClsStore } from '../workspace/workspace.types';
|
|
import { PushService, type PushPayload } from './push.service';
|
|
|
|
@Injectable()
|
|
export class NotificationsService {
|
|
constructor(
|
|
private readonly cls: ClsService<WorkspaceClsStore>,
|
|
private readonly push: PushService,
|
|
) {}
|
|
|
|
async subscribe(userId: string, role: string, dto: SubscribePayload): Promise<void> {
|
|
const prisma = this.cls.get('prisma');
|
|
if (!prisma) throw new Error('prisma não disponível no CLS');
|
|
|
|
await prisma.pushSubscription.upsert({
|
|
where: { endpoint: dto.endpoint },
|
|
update: { userId, role, p256dh: dto.keys.p256dh, auth: dto.keys.auth },
|
|
create: {
|
|
userId,
|
|
role,
|
|
endpoint: dto.endpoint,
|
|
p256dh: dto.keys.p256dh,
|
|
auth: dto.keys.auth,
|
|
},
|
|
});
|
|
}
|
|
|
|
async unsubscribe(endpoint: string): Promise<void> {
|
|
const prisma = this.cls.get('prisma');
|
|
if (!prisma) throw new Error('prisma não disponível no CLS');
|
|
|
|
await prisma.pushSubscription.deleteMany({ where: { endpoint } });
|
|
}
|
|
|
|
async pendingCount(userId: string, role: string): Promise<PendingCountResponse> {
|
|
const prisma = this.cls.get('prisma');
|
|
if (!prisma) throw new Error('prisma não disponível no CLS');
|
|
|
|
if (role === 'supervisor' || role === 'manager' || role === 'admin') {
|
|
const count = await prisma.order.count({
|
|
where: { status: OrderStatus.pending_approval, deletedAt: null },
|
|
});
|
|
return { count };
|
|
}
|
|
|
|
return { count: 0 };
|
|
}
|
|
|
|
// Envia push para todos os supervisores/managers/admin do workspace.
|
|
async notifySupervisors(payload: PushPayload): Promise<void> {
|
|
const prisma = this.cls.get('prisma');
|
|
if (!prisma) return;
|
|
|
|
const subs = await prisma.pushSubscription.findMany({
|
|
where: { role: { in: ['supervisor', 'manager', 'admin'] } },
|
|
});
|
|
|
|
await Promise.allSettled(subs.map((s) => this.push.send(s, payload)));
|
|
}
|
|
|
|
// Envia push para um userId específico (todos os dispositivos registrados).
|
|
async notifyUser(userId: string, payload: PushPayload): Promise<void> {
|
|
const prisma = this.cls.get('prisma');
|
|
if (!prisma) return;
|
|
|
|
const subs = await prisma.pushSubscription.findMany({ where: { userId } });
|
|
await Promise.allSettled(subs.map((s) => this.push.send(s, payload)));
|
|
}
|
|
}
|