feat(c3): consulta de pedidos — schema, api, web (OrdersModule + ClientDetailPage)

- Prisma: Order, OrderItem, OrderStatusHistory + migration
- Seed: 17 pedidos em 7 clientes com itens, histórico e desnorm de clientes
- @sar/api-interface: contratos Zod (OrderSummary, OrderDetail, OrderListQuery, etc.)
- API: GET /orders, GET /orders/:id, GET /clients/:id/orders (últimos 10)
- Web: OrdersPage (lista + filtro status/número + pending_approval highlighted)
- Web: ClientDetailPage (ficha completa + últimos 10 pedidos)
- Web: /pedidos e /pedidos/$id adicionados ao router; ClientDetailPage substitui placeholder

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-27 23:31:18 +00:00
parent 14c8350216
commit c36451dd33
15 changed files with 1494 additions and 71 deletions

View File

@@ -23,49 +23,137 @@ datasource db {
// ─── Enums ───────────────────────────────────────────────────────────────────
// Situação financeira resumida do cliente — cacheável offline (FR-2.4, FR-2.5).
// Valor numérico de crédito e inadimplência requerem conexão.
enum FinancialStatus {
regular
attention
blocked
}
// Status do pedido (FR-3.2). Transições: budget → pending_approval → approved → invoiced.
// Qualquer status pode ir para cancelled.
enum OrderStatus {
budget // orçamento
pending_approval // aprovação pendente
approved // aprovado
invoiced // faturado
cancelled // cancelado
}
// ─── Client (C2) ─────────────────────────────────────────────────────────────
//
// Cadastro sincronizado do ERP legado (FR-2.6). Rep não cria/edita no MVP.
// creditLimit: gerenciado no SAR — admin/supervisor define (OQ-4 resolvido 2026-05-27).
// lastOrderAt/lastOrderValue: desnormalizados, atualizados ao sincronizar Orders (C3/C4).
// activityStatus: calculado em runtime a partir de lastOrderAt (não persiste — evita drift).
// creditLimit: gerenciado no SAR (OQ-4 resolvido 2026-05-27).
// lastOrderAt/lastOrderValue/openOrdersCount: desnormalizados de Orders.
model Client {
id String @id @default(uuid()) @db.Uuid
name String // razão social / nome completo
tradeName String? // nome fantasia
taxId String @unique // CNPJ (14 dígitos) ou CPF (11 dígitos), sem máscara
email String?
phone String?
address Json? // { street, number, complement?, district, city, state, zip }
id String @id @default(uuid()) @db.Uuid
name String
tradeName String?
taxId String @unique
email String?
phone String?
address Json?
// Situação financeira — resumo cacheável; detalhes numéricos requerem conexão
financialStatus FinancialStatus @default(regular)
creditLimit Decimal? @db.Decimal(15, 2)
// Desnormalizados de Orders (atualizados em C3/C4)
repId String // userId do Rep responsável (JWT sub)
repId String
lastOrderAt DateTime?
lastOrderValue Decimal? @db.Decimal(15, 2)
openOrdersCount Int @default(0)
// Controle de sync com ERP
erpCode String? // código no ERP legado
erpCode String?
syncedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime? // soft delete — não remove fisicamente
deletedAt DateTime?
orders Order[]
@@index([repId])
@@index([taxId])
@@index([name])
@@index([deletedAt]) // filtragem de soft delete eficiente
@@index([deletedAt])
}
// ─── Order (C3) ──────────────────────────────────────────────────────────────
//
// Pedido emitido pelo Rep. Itens desnormalizados (produto sem FK — C4 traz catálogo).
// number: gerado pelo SAR (sequencial por workspace, ex: "PED-00042").
// discountPct: desconto global do pedido (além de descontos por item).
// approvedById: userId de quem aprovou (se status = approved ou invoiced).
model Order {
id String @id @default(uuid()) @db.Uuid
number String @unique // "PED-00001"
clientId String @db.Uuid
repId String // userId do Rep que emitiu
status OrderStatus @default(budget)
discountPct Decimal @default(0) @db.Decimal(5, 2) // % desconto global
subtotal Decimal @db.Decimal(15, 2) // soma dos itens sem desconto global
total Decimal @db.Decimal(15, 2) // subtotal × (1 - discountPct/100)
notes String?
approvedById String? // userId de quem aprovou
approvedAt DateTime?
invoicedAt DateTime?
cancelledAt DateTime?
// Idempotency key para lançamentos offline (C4, FR-4.12)
idempotencyKey String? @unique
issuedAt DateTime @default(now()) // data de emissão pelo Rep
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
client Client @relation(fields: [clientId], references: [id])
items OrderItem[]
history OrderStatusHistory[]
@@index([clientId])
@@index([repId])
@@index([status])
@@index([issuedAt])
@@index([number])
@@index([deletedAt])
}
// ─── OrderItem (C3) ──────────────────────────────────────────────────────────
//
// Item do pedido. Produto desnormalizado (nome/código como string) — catálogo virá em C4.
// discountPct: desconto por linha (além do desconto global do Order).
model OrderItem {
id String @id @default(uuid()) @db.Uuid
orderId String @db.Uuid
productCode String // código no ERP / catálogo
productName String // desnormalizado para exibição offline
quantity Decimal @db.Decimal(10, 3)
unitPrice Decimal @db.Decimal(15, 2)
discountPct Decimal @default(0) @db.Decimal(5, 2)
subtotal Decimal @db.Decimal(15, 2) // qty × unitPrice × (1 - discountPct/100)
order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
@@index([orderId])
}
// ─── OrderStatusHistory (C3) ─────────────────────────────────────────────────
//
// Registro imutável de cada transição de status. changedById = userId do ator.
model OrderStatusHistory {
id String @id @default(uuid()) @db.Uuid
orderId String @db.Uuid
fromStatus OrderStatus?
toStatus OrderStatus
changedById String // userId
note String?
changedAt DateTime @default(now())
order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
@@index([orderId])
@@index([changedAt])
}