feat(orders): fluxo de aprovação — approve/reject endpoints + UIs (C5)

PATCH /orders/:id/approve e /reject com alçada role-gated; OrderDetailPage
com modais de aprovação e recusa; ApprovalQueuePage para Sandra; badge de
pendências na Sidebar; DevLogin com 4 perfis (rep, supervisor, gerente).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-28 00:01:14 +00:00
parent 6769a0d82a
commit 356c8e3c2c
9 changed files with 731 additions and 33 deletions

View File

@@ -1,19 +1,36 @@
import { Body, Controller, Get, HttpCode, Param, ParseUUIDPipe, Post, Query } from '@nestjs/common';
import {
Body,
Controller,
ForbiddenException,
Get,
HttpCode,
Param,
ParseUUIDPipe,
Patch,
Post,
Query,
} from '@nestjs/common';
import { ClsService } from 'nestjs-cls';
import { createZodDto } from 'nestjs-zod';
import {
ApproveOrderSchema,
CreateOrderSchema,
OrderListQuerySchema,
RejectOrderSchema,
type ApproveOrder,
type CreateOrder,
type OrderDetail,
type OrderListQuery,
type OrderListResponse,
type RejectOrder,
} from '@sar/api-interface';
import type { WorkspaceClsStore } from '../workspace/workspace.types';
import { OrdersService } from './orders.service';
class OrderListQueryDto extends createZodDto(OrderListQuerySchema) {}
class CreateOrderDto extends createZodDto(CreateOrderSchema) {}
class ApproveOrderDto extends createZodDto(ApproveOrderSchema) {}
class RejectOrderDto extends createZodDto(RejectOrderSchema) {}
@Controller({ path: 'orders' })
export class OrdersController {
@@ -35,6 +52,28 @@ export class OrdersController {
return this.orders.create(parsed, this.cls.get('userId') ?? '');
}
@Patch(':id/approve')
approve(
@Param('id', ParseUUIDPipe) id: string,
@Body() body: ApproveOrderDto,
): Promise<OrderDetail> {
const role = this.cls.get('role') ?? 'rep';
if (role === 'rep') throw new ForbiddenException('Apenas supervisores podem aprovar pedidos');
const parsed = ApproveOrderSchema.parse(body) as ApproveOrder;
return this.orders.approve(id, this.cls.get('userId') ?? '', parsed);
}
@Patch(':id/reject')
reject(
@Param('id', ParseUUIDPipe) id: string,
@Body() body: RejectOrderDto,
): Promise<OrderDetail> {
const role = this.cls.get('role') ?? 'rep';
if (role === 'rep') throw new ForbiddenException('Apenas supervisores podem recusar pedidos');
const parsed = RejectOrderSchema.parse(body) as RejectOrder;
return this.orders.reject(id, this.cls.get('userId') ?? '', parsed);
}
@Get(':id')
findOne(@Param('id', ParseUUIDPipe) id: string): Promise<OrderDetail> {
return this.orders.findOne(id, this.cls.get('userId') ?? '', this.cls.get('role') ?? 'rep');