feat(c4): lançamento de pedido — catálogo, alçada por linha, POST /orders
- Prisma: Product + RepDiscountLimit + productCategory em OrderItem + migration - Seed: 28 produtos (5 categorias) + alçadas user-001 (default 10%, bebidas 8%, perecíveis 5%) - @sar/api-interface: ProductSummarySchema, ProductDetailSchema, ProductSyncRequestSchema, CreateOrderSchema - API: CatalogModule (GET /catalog, GET /catalog/:id, POST /catalog/sync) - API: POST /orders — valida alçada por linha/produto (OQ-2), idempotency-key (FR-4.3), desnorm cliente - Web: NewOrderPage (3 steps: catálogo → desconto/obs → confirmação) - Web: botão Novo Pedido na ClientDetailPage (desabilitado se financialStatus=blocked) - Web: rota /pedidos/novo com search param clientId Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
26
apps/web/src/lib/queries/catalog.ts
Normal file
26
apps/web/src/lib/queries/catalog.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import {
|
||||
ProductListResponseSchema,
|
||||
type ProductListQuery,
|
||||
type ProductListResponse,
|
||||
} from '@sar/api-interface';
|
||||
import { apiFetch } from '../api-client';
|
||||
|
||||
export function useCatalog(params: Partial<ProductListQuery> = {}) {
|
||||
const search = new URLSearchParams();
|
||||
if (params.q) search.set('q', params.q);
|
||||
if (params.category) search.set('category', params.category);
|
||||
if (params.page) search.set('page', String(params.page));
|
||||
if (params.limit) search.set('limit', String(params.limit));
|
||||
|
||||
const qs = search.toString();
|
||||
return useQuery<ProductListResponse>({
|
||||
queryKey: ['catalog', params],
|
||||
queryFn: async () => {
|
||||
const res = await apiFetch(`/catalog${qs ? `?${qs}` : ''}`);
|
||||
if (!res.ok) throw new Error(`catalog error ${res.status}`);
|
||||
return ProductListResponseSchema.parse(await res.json());
|
||||
},
|
||||
staleTime: 4 * 60 * 60 * 1000, // TTL 4h — FR-4.4
|
||||
});
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { RafaelPainel } from '../cockpits/rafael/RafaelPainel';
|
||||
import { ClientsPage } from '../cockpits/rafael/ClientsPage';
|
||||
import { ClientDetailPage } from '../cockpits/rafael/ClientDetailPage';
|
||||
import { OrdersPage } from '../cockpits/rafael/OrdersPage';
|
||||
import { NewOrderPage } from '../cockpits/rafael/NewOrderPage';
|
||||
|
||||
const rootRoute = createRootRoute({
|
||||
component: () => (
|
||||
@@ -43,6 +44,12 @@ const pedidosRoute = createRoute({
|
||||
component: OrdersPage,
|
||||
});
|
||||
|
||||
const novoOrderRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
path: '/pedidos/novo',
|
||||
component: NewOrderPage,
|
||||
});
|
||||
|
||||
const pedidoDetailRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
path: '/pedidos/$id',
|
||||
@@ -62,6 +69,7 @@ const routeTree = rootRoute.addChildren([
|
||||
clientesRoute,
|
||||
clienteDetailRoute,
|
||||
pedidosRoute,
|
||||
novoOrderRoute,
|
||||
pedidoDetailRoute,
|
||||
]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user