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,56 @@
import { useQuery } from '@tanstack/react-query';
import {
OrderListResponseSchema,
OrderDetailSchema,
type OrderListQuery,
type OrderListResponse,
type OrderDetail,
type OrderSummary,
} from '@sar/api-interface';
import { apiFetch } from '../api-client';
export function useOrderList(params: Partial<OrderListQuery> = {}) {
const search = new URLSearchParams();
if (params.clientId) search.set('clientId', params.clientId);
if (params.status) search.set('status', params.status);
if (params.number) search.set('number', params.number);
if (params.from) search.set('from', params.from);
if (params.to) search.set('to', params.to);
if (params.page) search.set('page', String(params.page));
if (params.limit) search.set('limit', String(params.limit));
const qs = search.toString();
return useQuery<OrderListResponse>({
queryKey: ['orders', params],
queryFn: async () => {
const res = await apiFetch(`/orders${qs ? `?${qs}` : ''}`);
if (!res.ok) throw new Error(`orders list error ${res.status}`);
return OrderListResponseSchema.parse(await res.json());
},
});
}
export function useOrderDetail(id: string | undefined) {
return useQuery<OrderDetail>({
queryKey: ['orders', id],
enabled: !!id,
queryFn: async () => {
const res = await apiFetch(`/orders/${id}`);
if (!res.ok) throw new Error(`order detail error ${res.status}`);
return OrderDetailSchema.parse(await res.json());
},
});
}
export function useClientOrders(clientId: string | undefined) {
return useQuery<OrderSummary[]>({
queryKey: ['clients', clientId, 'orders'],
enabled: !!clientId,
queryFn: async () => {
const res = await apiFetch(`/clients/${clientId}/orders`);
if (!res.ok) throw new Error(`client orders error ${res.status}`);
const data = await res.json();
return data as OrderSummary[];
},
});
}

View File

@@ -2,6 +2,8 @@ import { createRouter, createRootRoute, createRoute, Outlet } from '@tanstack/re
import { AppShell } from '../components/layout/AppShell';
import { RafaelPainel } from '../cockpits/rafael/RafaelPainel';
import { ClientsPage } from '../cockpits/rafael/ClientsPage';
import { ClientDetailPage } from '../cockpits/rafael/ClientDetailPage';
import { OrdersPage } from '../cockpits/rafael/OrdersPage';
const rootRoute = createRootRoute({
component: () => (
@@ -29,15 +31,26 @@ const clientesRoute = createRoute({
component: ClientsPage,
});
// Placeholder detail route — ClientDetailPage virá em próxima iteração de C2
const clienteDetailRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/clientes/$id',
component: ClientDetailPage,
});
const pedidosRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/pedidos',
component: OrdersPage,
});
const pedidoDetailRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/pedidos/$id',
component: () => {
const { id } = clienteDetailRoute.useParams();
const { id } = pedidoDetailRoute.useParams();
return (
<div style={{ padding: 24 }}>
<p>Ficha do cliente {id} em construção</p>
<p>Detalhe do pedido {id} em construção</p>
</div>
);
},
@@ -48,6 +61,8 @@ const routeTree = rootRoute.addChildren([
rafaelRoute,
clientesRoute,
clienteDetailRoute,
pedidosRoute,
pedidoDetailRoute,
]);
export const router = createRouter({