From 1f8a9d872a3104ecbdd4482bd228fc9e3cfe4788 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 29 May 2026 14:46:25 +0000 Subject: [PATCH] =?UTF-8?q?feat(web):=20clientes=20e=20cat=C3=A1logo=20fun?= =?UTF-8?q?cionando=20com=20dados=20do=20ERP?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - clients.ts, catalog.ts: corrige bug res.ok/res.json() — apiFetch retorna JSON direto - catalog.service.ts: corrige nomes de coluna da vw_produtos (descr_det, lista_pauta, remove preco_com_ipi inexistente) - CatalogPage.tsx: nova tela — código, descrição, grupo, marca, preço, estoque - router.tsx: adiciona rota /catalogo Co-Authored-By: Claude Sonnet 4.6 (1M context) --- apps/api/src/app/catalog/catalog.service.ts | 23 ++-- apps/web/src/cockpits/rafael/CatalogPage.tsx | 113 +++++++++++++++++++ apps/web/src/lib/queries/catalog.ts | 18 ++- apps/web/src/lib/queries/clients.ts | 6 +- apps/web/src/lib/router.tsx | 8 ++ 5 files changed, 148 insertions(+), 20 deletions(-) create mode 100644 apps/web/src/cockpits/rafael/CatalogPage.tsx diff --git a/apps/api/src/app/catalog/catalog.service.ts b/apps/api/src/app/catalog/catalog.service.ts index 679e720..291f205 100644 --- a/apps/api/src/app/catalog/catalog.service.ts +++ b/apps/api/src/app/catalog/catalog.service.ts @@ -28,16 +28,15 @@ interface ProdutoRow { marca: string | null; ativo: number; qtd_estoque: string | null; - lista_parauta: number | null; + lista_pauta: number | null; referencia: string | null; - descricao_detalhada: string | null; + descr_det: string | null; vl_preco2: string | null; vl_preco3: string | null; aliq_ipi: string | null; peso_liquido: string | null; qtd_volume: string | null; lote_mul_venda: number | null; - preco_com_ipi: string | null; preco_promocional: string | null; } @@ -72,16 +71,15 @@ export class CatalogService { p.marca, p.ativo, e.qtd_estoque::text, - p.lista_parauta, + p.lista_pauta, p.referencia, - p.descricao_detalhada, + p.descr_det, p.vl_preco2::text, p.vl_preco3::text, p.aliq_ipi::text, p.peso_liquido::text, p.qtd_volume::text, p.lote_mul_venda, - p.preco_com_ipi::text, p.preco_promocional::text FROM vw_produtos p LEFT JOIN vw_estoque e ON e.id_erp = p.id_erp AND e.id_empresa = ${idEmpresa} @@ -114,7 +112,7 @@ export class CatalogService { marca: p.marca, ativo: Number(p.ativo), qtdEstoque: p.qtd_estoque, - listaParauta: p.lista_parauta !== null ? Number(p.lista_parauta) : null, + listaParauta: p.lista_pauta !== null ? Number(p.lista_pauta) : null, })); return { data, total, page, limit }; @@ -139,16 +137,15 @@ export class CatalogService { p.marca, p.ativo, e.qtd_estoque::text, - p.lista_parauta, + p.lista_pauta, p.referencia, - p.descricao_detalhada, + p.descr_det, p.vl_preco2::text, p.vl_preco3::text, p.aliq_ipi::text, p.peso_liquido::text, p.qtd_volume::text, p.lote_mul_venda, - p.preco_com_ipi::text, p.preco_promocional::text FROM vw_produtos p LEFT JOIN vw_estoque e ON e.id_erp = p.id_erp AND e.id_empresa = ${idEmpresa} @@ -172,16 +169,16 @@ export class CatalogService { marca: p.marca, ativo: Number(p.ativo), qtdEstoque: p.qtd_estoque, - listaParauta: p.lista_parauta !== null ? Number(p.lista_parauta) : null, + listaParauta: p.lista_pauta !== null ? Number(p.lista_pauta) : null, referencia: p.referencia, - descricaoDetalhada: p.descricao_detalhada, + descricaoDetalhada: p.descr_det, vlPreco2: p.vl_preco2, vlPreco3: p.vl_preco3, aliqIpi: p.aliq_ipi, pesoLiquido: p.peso_liquido, qtdVolume: p.qtd_volume, loteMulVenda: p.lote_mul_venda !== null ? Number(p.lote_mul_venda) : null, - precoComIpi: p.preco_com_ipi, + precoComIpi: null, precoPromocional: p.preco_promocional, }; } diff --git a/apps/web/src/cockpits/rafael/CatalogPage.tsx b/apps/web/src/cockpits/rafael/CatalogPage.tsx new file mode 100644 index 0000000..c59c0d3 --- /dev/null +++ b/apps/web/src/cockpits/rafael/CatalogPage.tsx @@ -0,0 +1,113 @@ +import { useState } from 'react'; +import { Table, Input, Typography, Tag } from 'antd'; +import type { TableColumnsType } from 'antd'; +import type { ProdutoSummary } from '@sar/api-interface'; +import { useCatalog } from '../../lib/queries/catalog'; + +const { Title } = Typography; +const { Search } = Input; + +function fmtPrice(v: string | null | undefined): string { + const n = Number(v ?? 0); + return n > 0 ? n.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' }) : '—'; +} + +const columns: TableColumnsType = [ + { + title: 'Código', + dataIndex: 'codigo', + width: 110, + render: (v: string) => {v.trim()}, + }, + { + title: 'Descrição', + dataIndex: 'descricao', + render: (v: string, row: ProdutoSummary) => ( +
+
{v.trim()}
+ {row.grupo &&
{row.grupo.trim()}
} +
+ ), + }, + { + title: 'Und', + dataIndex: 'unidade', + width: 60, + align: 'center', + render: (v: string | null) => v ?? '—', + }, + { + title: 'Marca', + dataIndex: 'marca', + width: 130, + render: (v: string | null) => (v ? {v.trim()} : null), + }, + { + title: 'Preço', + dataIndex: 'vlPreco1', + width: 110, + align: 'right', + render: (v: string) => fmtPrice(v), + }, + { + title: 'Estoque', + dataIndex: 'qtdEstoque', + width: 90, + align: 'right', + render: (v: string | null) => { + if (v == null) return '—'; + const n = Number(v); + return ( + 0 ? 'inherit' : '#f5222d' }}>{n.toLocaleString('pt-BR')} + ); + }, + }, +]; + +export function CatalogPage() { + const [q, setQ] = useState(''); + const [page, setPage] = useState(1); + const limit = 50; + + const { data, isLoading } = useCatalog({ q: q || undefined, page, limit }); + + return ( +
+ + Catálogo de Produtos + + + { + setQ(v); + setPage(1); + }} + onChange={(e) => { + if (!e.target.value) { + setQ(''); + setPage(1); + } + }} + /> + + + rowKey="idErp" + columns={columns} + dataSource={data?.data ?? []} + loading={isLoading} + size="small" + pagination={{ + current: page, + pageSize: limit, + total: data?.total ?? 0, + showSizeChanger: false, + showTotal: (t) => `${t.toLocaleString('pt-BR')} produtos`, + onChange: (p) => setPage(p), + }} + /> +
+ ); +} diff --git a/apps/web/src/lib/queries/catalog.ts b/apps/web/src/lib/queries/catalog.ts index 89600ca..8091342 100644 --- a/apps/web/src/lib/queries/catalog.ts +++ b/apps/web/src/lib/queries/catalog.ts @@ -1,8 +1,10 @@ import { useQuery } from '@tanstack/react-query'; import { ProdutoListResponseSchema, + ProdutoDetailSchema, type ProdutoListQuery, type ProdutoListResponse, + type ProdutoDetail, } from '@sar/api-interface'; import { apiFetch } from '../api-client'; @@ -18,9 +20,19 @@ export function useCatalog(params: Partial = {}) { queryKey: ['catalog', params], queryFn: async () => { const res = await apiFetch(`/catalog${qs ? `?${qs}` : ''}`); - if (!res.ok) throw new Error(`catalog error ${res.status}`); - return ProdutoListResponseSchema.parse(await res.json()); + return ProdutoListResponseSchema.parse(res); + }, + staleTime: 4 * 60 * 60 * 1000, + }); +} + +export function useProdutoDetail(id: number | undefined) { + return useQuery({ + queryKey: ['catalog', id], + enabled: id != null, + queryFn: async () => { + const res = await apiFetch(`/catalog/${id}`); + return ProdutoDetailSchema.parse(res); }, - staleTime: 4 * 60 * 60 * 1000, // TTL 4h — FR-4.4 }); } diff --git a/apps/web/src/lib/queries/clients.ts b/apps/web/src/lib/queries/clients.ts index 45ea7fc..635b009 100644 --- a/apps/web/src/lib/queries/clients.ts +++ b/apps/web/src/lib/queries/clients.ts @@ -27,8 +27,7 @@ export function useClientList(params: Partial = {}) { queryKey: CLIENT_KEYS.list(params), queryFn: async () => { const res = await apiFetch(`/clients${query ? `?${query}` : ''}`); - if (!res.ok) throw new Error(`clients list error ${res.status}`); - return ClientListResponseSchema.parse(await res.json()); + return ClientListResponseSchema.parse(res); }, }); } @@ -38,8 +37,7 @@ export function useClientDetail(id: number | string | undefined) { queryKey: CLIENT_KEYS.detail(Number(id)), queryFn: async () => { const res = await apiFetch(`/clients/${id}`); - if (!res.ok) throw new Error(`client detail error ${res.status}`); - return ClientDetailSchema.parse(await res.json()); + return ClientDetailSchema.parse(res); }, enabled: !!id, }); diff --git a/apps/web/src/lib/router.tsx b/apps/web/src/lib/router.tsx index c2f96fc..4157a2e 100644 --- a/apps/web/src/lib/router.tsx +++ b/apps/web/src/lib/router.tsx @@ -6,6 +6,7 @@ import { ClientDetailPage } from '../cockpits/rafael/ClientDetailPage'; import { OrdersPage } from '../cockpits/rafael/OrdersPage'; import { OrderDetailPage } from '../cockpits/rafael/OrderDetailPage'; import { NewOrderPage } from '../cockpits/rafael/NewOrderPage'; +import { CatalogPage } from '../cockpits/rafael/CatalogPage'; import { ApprovalQueuePage } from '../cockpits/sandra/ApprovalQueuePage'; import { SandraPainel } from '../cockpits/sandra/SandraPainel'; import { authStore } from './auth-store'; @@ -76,6 +77,12 @@ const pedidoDetailRoute = createRoute({ component: OrderDetailPage, }); +const catalogoRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/catalogo', + component: CatalogPage, +}); + const aprovacoes = createRoute({ getParentRoute: () => rootRoute, path: '/aprovacoes', @@ -90,6 +97,7 @@ const routeTree = rootRoute.addChildren([ pedidosRoute, novoOrderRoute, pedidoDetailRoute, + catalogoRoute, aprovacoes, ]);