feat(c6): notificações e push — Web Push VAPID, badge dinâmico, Share API
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>
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { OrdersController } from './orders.controller';
|
||||
import { OrdersService } from './orders.service';
|
||||
import { NotificationsModule } from '../notifications/notifications.module';
|
||||
|
||||
@Module({
|
||||
imports: [NotificationsModule],
|
||||
controllers: [OrdersController],
|
||||
providers: [OrdersService],
|
||||
exports: [OrdersService],
|
||||
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
RejectOrder,
|
||||
} from '@sar/api-interface';
|
||||
import type { WorkspaceClsStore } from '../workspace/workspace.types';
|
||||
import { NotificationsService } from '../notifications/notifications.service';
|
||||
|
||||
function decimalToString(v: Prisma.Decimal | null | undefined): string {
|
||||
return v ? v.toString() : '0';
|
||||
@@ -18,7 +19,10 @@ function decimalToString(v: Prisma.Decimal | null | undefined): string {
|
||||
|
||||
@Injectable()
|
||||
export class OrdersService {
|
||||
constructor(private readonly cls: ClsService<WorkspaceClsStore>) {}
|
||||
constructor(
|
||||
private readonly cls: ClsService<WorkspaceClsStore>,
|
||||
private readonly notifications: NotificationsService,
|
||||
) {}
|
||||
|
||||
async list(query: OrderListQuery, userId: string, role: string): Promise<OrderListResponse> {
|
||||
const prisma = this.cls.get('prisma');
|
||||
@@ -252,7 +256,13 @@ export class OrdersService {
|
||||
});
|
||||
|
||||
if (status === OrderStatus.pending_approval) {
|
||||
// Buscar order com history atualizado
|
||||
// FR-6.1: notifica supervisores que há pedido aguardando aprovação
|
||||
void this.notifications.notifySupervisors({
|
||||
title: 'Pedido aguardando aprovação',
|
||||
body: `${order.client.name} — ${order.number} — R$ ${order.total.toFixed(2).replace('.', ',')}`,
|
||||
url: `/pedidos/${order.id}`,
|
||||
});
|
||||
|
||||
const updated = await prisma.order.findUniqueOrThrow({
|
||||
where: { id: order.id },
|
||||
include: {
|
||||
@@ -322,6 +332,14 @@ export class OrdersService {
|
||||
history: { orderBy: { changedAt: 'asc' } },
|
||||
},
|
||||
});
|
||||
|
||||
// FR-6.1: notifica o rep que o pedido foi aprovado
|
||||
void this.notifications.notifyUser(order.repId, {
|
||||
title: 'Pedido aprovado',
|
||||
body: `${final.number} — ${final.client.name} aprovado${dto.discountPct !== undefined ? ` com ${newDiscountPct}% de desconto` : ''}`,
|
||||
url: `/pedidos/${id}`,
|
||||
});
|
||||
|
||||
return this.mapDetail(final);
|
||||
}
|
||||
|
||||
@@ -370,6 +388,14 @@ export class OrdersService {
|
||||
history: { orderBy: { changedAt: 'asc' } },
|
||||
},
|
||||
});
|
||||
|
||||
// FR-6.1: notifica o rep que o pedido foi recusado
|
||||
void this.notifications.notifyUser(order.repId, {
|
||||
title: 'Pedido recusado',
|
||||
body: `${final.number} — ${final.client.name}: ${dto.reason}`,
|
||||
url: `/pedidos/${id}`,
|
||||
});
|
||||
|
||||
return this.mapDetail(final);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user