feat(catalog): filtro de preço e seletor de pauta

- Catálogo só mostra produtos com preço preenchido (vl_preco1 > 0) por default
- Novo endpoint GET /catalog/pautas — retorna as 6 pautas do representante logado
- GET /catalog?idPauta=N — usa preço da pauta selecionada (vw_pauta_produtos)
- CatalogPage: dropdown "Selecionar pauta de preços" com as pautas do rep
- product.contract: adiciona PautaSchema e idPauta no ProdutoListQuerySchema

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-29 14:55:18 +00:00
parent 1f8a9d872a
commit e7cbadcf7e
5 changed files with 185 additions and 84 deletions

View File

@@ -1,8 +1,8 @@
import { useState } from 'react';
import { Table, Input, Typography, Tag } from 'antd';
import { Table, Input, Select, Space, Typography, Tag } from 'antd';
import type { TableColumnsType } from 'antd';
import type { ProdutoSummary } from '@sar/api-interface';
import { useCatalog } from '../../lib/queries/catalog';
import { useCatalog, usePautas } from '../../lib/queries/catalog';
const { Title } = Typography;
const { Search } = Input;
@@ -45,9 +45,13 @@ const columns: TableColumnsType<ProdutoSummary> = [
{
title: 'Preço',
dataIndex: 'vlPreco1',
width: 110,
width: 120,
align: 'right',
render: (v: string) => fmtPrice(v),
render: (v: string) => (
<span style={{ fontWeight: 600, color: Number(v) > 0 ? '#389e0d' : '#999' }}>
{fmtPrice(v)}
</span>
),
},
{
title: 'Estoque',
@@ -66,10 +70,12 @@ const columns: TableColumnsType<ProdutoSummary> = [
export function CatalogPage() {
const [q, setQ] = useState('');
const [idPauta, setIdPauta] = useState<number | undefined>();
const [page, setPage] = useState(1);
const limit = 50;
const { data, isLoading } = useCatalog({ q: q || undefined, page, limit });
const { data: pautas, isLoading: pautasLoading } = usePautas();
const { data, isLoading } = useCatalog({ q: q || undefined, idPauta, page, limit });
return (
<div style={{ padding: 24 }}>
@@ -77,21 +83,37 @@ export function CatalogPage() {
Catálogo de Produtos
</Title>
<Search
placeholder="Buscar por código ou descrição..."
allowClear
style={{ width: 320, marginBottom: 16 }}
onSearch={(v) => {
setQ(v);
setPage(1);
}}
onChange={(e) => {
if (!e.target.value) {
setQ('');
<Space style={{ marginBottom: 16 }} wrap>
<Search
placeholder="Buscar por código ou descrição..."
allowClear
style={{ width: 300 }}
onSearch={(v) => {
setQ(v);
setPage(1);
}
}}
/>
}}
onChange={(e) => {
if (!e.target.value) {
setQ('');
setPage(1);
}
}}
/>
<Select
placeholder="Selecionar pauta de preços"
allowClear
loading={pautasLoading}
style={{ width: 340 }}
onChange={(v) => {
setIdPauta(v as number | undefined);
setPage(1);
}}
options={pautas?.map((p) => ({
value: p.idPauta,
label: `${p.codigo}${p.descricao}`,
}))}
/>
</Space>
<Table<ProdutoSummary>
rowKey="idErp"

View File

@@ -1,17 +1,32 @@
import { useQuery } from '@tanstack/react-query';
import {
PautaSchema,
ProdutoListResponseSchema,
ProdutoDetailSchema,
type ProdutoListQuery,
type ProdutoListResponse,
type ProdutoDetail,
type Pauta,
} from '@sar/api-interface';
import { z } from 'zod';
import { apiFetch } from '../api-client';
export function usePautas() {
return useQuery<Pauta[]>({
queryKey: ['catalog', 'pautas'],
queryFn: async () => {
const res = await apiFetch('/catalog/pautas');
return z.array(PautaSchema).parse(res);
},
staleTime: 10 * 60 * 1000,
});
}
export function useCatalog(params: Partial<ProdutoListQuery> = {}) {
const search = new URLSearchParams();
if (params.q) search.set('q', params.q);
if (params.codGrupo) search.set('codGrupo', String(params.codGrupo));
if (params.idPauta) search.set('idPauta', String(params.idPauta));
if (params.page) search.set('page', String(params.page));
if (params.limit) search.set('limit', String(params.limit));