feat(web): clientes e catálogo funcionando com dados do ERP
- 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) <noreply@anthropic.com>
This commit is contained in:
@@ -28,16 +28,15 @@ interface ProdutoRow {
|
|||||||
marca: string | null;
|
marca: string | null;
|
||||||
ativo: number;
|
ativo: number;
|
||||||
qtd_estoque: string | null;
|
qtd_estoque: string | null;
|
||||||
lista_parauta: number | null;
|
lista_pauta: number | null;
|
||||||
referencia: string | null;
|
referencia: string | null;
|
||||||
descricao_detalhada: string | null;
|
descr_det: string | null;
|
||||||
vl_preco2: string | null;
|
vl_preco2: string | null;
|
||||||
vl_preco3: string | null;
|
vl_preco3: string | null;
|
||||||
aliq_ipi: string | null;
|
aliq_ipi: string | null;
|
||||||
peso_liquido: string | null;
|
peso_liquido: string | null;
|
||||||
qtd_volume: string | null;
|
qtd_volume: string | null;
|
||||||
lote_mul_venda: number | null;
|
lote_mul_venda: number | null;
|
||||||
preco_com_ipi: string | null;
|
|
||||||
preco_promocional: string | null;
|
preco_promocional: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,16 +71,15 @@ export class CatalogService {
|
|||||||
p.marca,
|
p.marca,
|
||||||
p.ativo,
|
p.ativo,
|
||||||
e.qtd_estoque::text,
|
e.qtd_estoque::text,
|
||||||
p.lista_parauta,
|
p.lista_pauta,
|
||||||
p.referencia,
|
p.referencia,
|
||||||
p.descricao_detalhada,
|
p.descr_det,
|
||||||
p.vl_preco2::text,
|
p.vl_preco2::text,
|
||||||
p.vl_preco3::text,
|
p.vl_preco3::text,
|
||||||
p.aliq_ipi::text,
|
p.aliq_ipi::text,
|
||||||
p.peso_liquido::text,
|
p.peso_liquido::text,
|
||||||
p.qtd_volume::text,
|
p.qtd_volume::text,
|
||||||
p.lote_mul_venda,
|
p.lote_mul_venda,
|
||||||
p.preco_com_ipi::text,
|
|
||||||
p.preco_promocional::text
|
p.preco_promocional::text
|
||||||
FROM vw_produtos p
|
FROM vw_produtos p
|
||||||
LEFT JOIN vw_estoque e ON e.id_erp = p.id_erp AND e.id_empresa = ${idEmpresa}
|
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,
|
marca: p.marca,
|
||||||
ativo: Number(p.ativo),
|
ativo: Number(p.ativo),
|
||||||
qtdEstoque: p.qtd_estoque,
|
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 };
|
return { data, total, page, limit };
|
||||||
@@ -139,16 +137,15 @@ export class CatalogService {
|
|||||||
p.marca,
|
p.marca,
|
||||||
p.ativo,
|
p.ativo,
|
||||||
e.qtd_estoque::text,
|
e.qtd_estoque::text,
|
||||||
p.lista_parauta,
|
p.lista_pauta,
|
||||||
p.referencia,
|
p.referencia,
|
||||||
p.descricao_detalhada,
|
p.descr_det,
|
||||||
p.vl_preco2::text,
|
p.vl_preco2::text,
|
||||||
p.vl_preco3::text,
|
p.vl_preco3::text,
|
||||||
p.aliq_ipi::text,
|
p.aliq_ipi::text,
|
||||||
p.peso_liquido::text,
|
p.peso_liquido::text,
|
||||||
p.qtd_volume::text,
|
p.qtd_volume::text,
|
||||||
p.lote_mul_venda,
|
p.lote_mul_venda,
|
||||||
p.preco_com_ipi::text,
|
|
||||||
p.preco_promocional::text
|
p.preco_promocional::text
|
||||||
FROM vw_produtos p
|
FROM vw_produtos p
|
||||||
LEFT JOIN vw_estoque e ON e.id_erp = p.id_erp AND e.id_empresa = ${idEmpresa}
|
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,
|
marca: p.marca,
|
||||||
ativo: Number(p.ativo),
|
ativo: Number(p.ativo),
|
||||||
qtdEstoque: p.qtd_estoque,
|
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,
|
referencia: p.referencia,
|
||||||
descricaoDetalhada: p.descricao_detalhada,
|
descricaoDetalhada: p.descr_det,
|
||||||
vlPreco2: p.vl_preco2,
|
vlPreco2: p.vl_preco2,
|
||||||
vlPreco3: p.vl_preco3,
|
vlPreco3: p.vl_preco3,
|
||||||
aliqIpi: p.aliq_ipi,
|
aliqIpi: p.aliq_ipi,
|
||||||
pesoLiquido: p.peso_liquido,
|
pesoLiquido: p.peso_liquido,
|
||||||
qtdVolume: p.qtd_volume,
|
qtdVolume: p.qtd_volume,
|
||||||
loteMulVenda: p.lote_mul_venda !== null ? Number(p.lote_mul_venda) : null,
|
loteMulVenda: p.lote_mul_venda !== null ? Number(p.lote_mul_venda) : null,
|
||||||
precoComIpi: p.preco_com_ipi,
|
precoComIpi: null,
|
||||||
precoPromocional: p.preco_promocional,
|
precoPromocional: p.preco_promocional,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
113
apps/web/src/cockpits/rafael/CatalogPage.tsx
Normal file
113
apps/web/src/cockpits/rafael/CatalogPage.tsx
Normal file
@@ -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<ProdutoSummary> = [
|
||||||
|
{
|
||||||
|
title: 'Código',
|
||||||
|
dataIndex: 'codigo',
|
||||||
|
width: 110,
|
||||||
|
render: (v: string) => <span style={{ fontVariantNumeric: 'tabular-nums' }}>{v.trim()}</span>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Descrição',
|
||||||
|
dataIndex: 'descricao',
|
||||||
|
render: (v: string, row: ProdutoSummary) => (
|
||||||
|
<div>
|
||||||
|
<div style={{ fontWeight: 500 }}>{v.trim()}</div>
|
||||||
|
{row.grupo && <div style={{ fontSize: 12, color: '#888' }}>{row.grupo.trim()}</div>}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Und',
|
||||||
|
dataIndex: 'unidade',
|
||||||
|
width: 60,
|
||||||
|
align: 'center',
|
||||||
|
render: (v: string | null) => v ?? '—',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Marca',
|
||||||
|
dataIndex: 'marca',
|
||||||
|
width: 130,
|
||||||
|
render: (v: string | null) => (v ? <Tag>{v.trim()}</Tag> : 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 (
|
||||||
|
<span style={{ color: n > 0 ? 'inherit' : '#f5222d' }}>{n.toLocaleString('pt-BR')}</span>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div style={{ padding: 24 }}>
|
||||||
|
<Title level={3} style={{ marginBottom: 16 }}>
|
||||||
|
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('');
|
||||||
|
setPage(1);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Table<ProdutoSummary>
|
||||||
|
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),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import {
|
import {
|
||||||
ProdutoListResponseSchema,
|
ProdutoListResponseSchema,
|
||||||
|
ProdutoDetailSchema,
|
||||||
type ProdutoListQuery,
|
type ProdutoListQuery,
|
||||||
type ProdutoListResponse,
|
type ProdutoListResponse,
|
||||||
|
type ProdutoDetail,
|
||||||
} from '@sar/api-interface';
|
} from '@sar/api-interface';
|
||||||
import { apiFetch } from '../api-client';
|
import { apiFetch } from '../api-client';
|
||||||
|
|
||||||
@@ -18,9 +20,19 @@ export function useCatalog(params: Partial<ProdutoListQuery> = {}) {
|
|||||||
queryKey: ['catalog', params],
|
queryKey: ['catalog', params],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const res = await apiFetch(`/catalog${qs ? `?${qs}` : ''}`);
|
const res = await apiFetch(`/catalog${qs ? `?${qs}` : ''}`);
|
||||||
if (!res.ok) throw new Error(`catalog error ${res.status}`);
|
return ProdutoListResponseSchema.parse(res);
|
||||||
return ProdutoListResponseSchema.parse(await res.json());
|
},
|
||||||
|
staleTime: 4 * 60 * 60 * 1000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useProdutoDetail(id: number | undefined) {
|
||||||
|
return useQuery<ProdutoDetail>({
|
||||||
|
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
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,8 +27,7 @@ export function useClientList(params: Partial<ClientListQuery> = {}) {
|
|||||||
queryKey: CLIENT_KEYS.list(params),
|
queryKey: CLIENT_KEYS.list(params),
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const res = await apiFetch(`/clients${query ? `?${query}` : ''}`);
|
const res = await apiFetch(`/clients${query ? `?${query}` : ''}`);
|
||||||
if (!res.ok) throw new Error(`clients list error ${res.status}`);
|
return ClientListResponseSchema.parse(res);
|
||||||
return ClientListResponseSchema.parse(await res.json());
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -38,8 +37,7 @@ export function useClientDetail(id: number | string | undefined) {
|
|||||||
queryKey: CLIENT_KEYS.detail(Number(id)),
|
queryKey: CLIENT_KEYS.detail(Number(id)),
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const res = await apiFetch(`/clients/${id}`);
|
const res = await apiFetch(`/clients/${id}`);
|
||||||
if (!res.ok) throw new Error(`client detail error ${res.status}`);
|
return ClientDetailSchema.parse(res);
|
||||||
return ClientDetailSchema.parse(await res.json());
|
|
||||||
},
|
},
|
||||||
enabled: !!id,
|
enabled: !!id,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { ClientDetailPage } from '../cockpits/rafael/ClientDetailPage';
|
|||||||
import { OrdersPage } from '../cockpits/rafael/OrdersPage';
|
import { OrdersPage } from '../cockpits/rafael/OrdersPage';
|
||||||
import { OrderDetailPage } from '../cockpits/rafael/OrderDetailPage';
|
import { OrderDetailPage } from '../cockpits/rafael/OrderDetailPage';
|
||||||
import { NewOrderPage } from '../cockpits/rafael/NewOrderPage';
|
import { NewOrderPage } from '../cockpits/rafael/NewOrderPage';
|
||||||
|
import { CatalogPage } from '../cockpits/rafael/CatalogPage';
|
||||||
import { ApprovalQueuePage } from '../cockpits/sandra/ApprovalQueuePage';
|
import { ApprovalQueuePage } from '../cockpits/sandra/ApprovalQueuePage';
|
||||||
import { SandraPainel } from '../cockpits/sandra/SandraPainel';
|
import { SandraPainel } from '../cockpits/sandra/SandraPainel';
|
||||||
import { authStore } from './auth-store';
|
import { authStore } from './auth-store';
|
||||||
@@ -76,6 +77,12 @@ const pedidoDetailRoute = createRoute({
|
|||||||
component: OrderDetailPage,
|
component: OrderDetailPage,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const catalogoRoute = createRoute({
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
path: '/catalogo',
|
||||||
|
component: CatalogPage,
|
||||||
|
});
|
||||||
|
|
||||||
const aprovacoes = createRoute({
|
const aprovacoes = createRoute({
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
path: '/aprovacoes',
|
path: '/aprovacoes',
|
||||||
@@ -90,6 +97,7 @@ const routeTree = rootRoute.addChildren([
|
|||||||
pedidosRoute,
|
pedidosRoute,
|
||||||
novoOrderRoute,
|
novoOrderRoute,
|
||||||
pedidoDetailRoute,
|
pedidoDetailRoute,
|
||||||
|
catalogoRoute,
|
||||||
aprovacoes,
|
aprovacoes,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user