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

@@ -0,0 +1,93 @@
import { z } from 'zod';
// Contratos canônicos de C3 — Consulta de Pedidos.
// Consumidos pela API (output tipado) e pela Web (TanStack Query + parse).
// ─── Enums ────────────────────────────────────────────────────────────────────
export const OrderStatusSchema = z.enum([
'budget',
'pending_approval',
'approved',
'invoiced',
'cancelled',
]);
export type OrderStatus = z.infer<typeof OrderStatusSchema>;
// ─── OrderItem ────────────────────────────────────────────────────────────────
export const OrderItemSchema = z.object({
id: z.string().uuid(),
productCode: z.string(),
productName: z.string(),
quantity: z.string(), // Decimal serializado
unitPrice: z.string(), // Decimal serializado
discountPct: z.string(), // Decimal serializado
subtotal: z.string(), // Decimal serializado
});
export type OrderItem = z.infer<typeof OrderItemSchema>;
// ─── OrderStatusHistory ───────────────────────────────────────────────────────
export const OrderStatusHistorySchema = z.object({
id: z.string().uuid(),
fromStatus: OrderStatusSchema.nullable(),
toStatus: OrderStatusSchema,
changedById: z.string(),
note: z.string().nullable(),
changedAt: z.iso.datetime(),
});
export type OrderStatusHistory = z.infer<typeof OrderStatusHistorySchema>;
// ─── Order Summary (lista) ────────────────────────────────────────────────────
export const OrderSummarySchema = z.object({
id: z.string().uuid(),
number: z.string(),
clientId: z.string().uuid(),
clientName: z.string(),
repId: z.string(),
status: OrderStatusSchema,
discountPct: z.string(),
subtotal: z.string(),
total: z.string(),
issuedAt: z.iso.datetime(),
approvedAt: z.iso.datetime().nullable(),
invoicedAt: z.iso.datetime().nullable(),
cancelledAt: z.iso.datetime().nullable(),
});
export type OrderSummary = z.infer<typeof OrderSummarySchema>;
// ─── Order Detail ─────────────────────────────────────────────────────────────
export const OrderDetailSchema = OrderSummarySchema.extend({
notes: z.string().nullable(),
approvedById: z.string().nullable(),
idempotencyKey: z.string().nullable(),
createdAt: z.iso.datetime(),
updatedAt: z.iso.datetime(),
items: z.array(OrderItemSchema),
history: z.array(OrderStatusHistorySchema),
});
export type OrderDetail = z.infer<typeof OrderDetailSchema>;
// ─── List query + response ────────────────────────────────────────────────────
export const OrderListQuerySchema = z.object({
clientId: z.string().uuid().optional(),
status: OrderStatusSchema.optional(),
number: z.string().optional(), // busca parcial por número
from: z.iso.datetime().optional(), // issuedAt >= from
to: z.iso.datetime().optional(), // issuedAt <= to
page: z.coerce.number().int().positive().default(1),
limit: z.coerce.number().int().min(1).max(200).default(50),
});
export type OrderListQuery = z.infer<typeof OrderListQuerySchema>;
export const OrderListResponseSchema = z.object({
data: z.array(OrderSummarySchema),
total: z.number().int().nonnegative(),
page: z.number().int().positive(),
limit: z.number().int().positive(),
});
export type OrderListResponse = z.infer<typeof OrderListResponseSchema>;