- Menu Catalogo no cockpit do gerente (rota /catalogo existente) - /catalog/pautas: gestor recebe TODAS as pautas ativas com contagem de reps que usam cada uma (rep segue vendo so as suas cod_pauta1..6) - Detalhe do produto: gestor ve o preco em todas as pautas (rep segue restrito as dele) - Select de pauta mostra "N reps" quando disponivel Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
196 lines
6.0 KiB
TypeScript
196 lines
6.0 KiB
TypeScript
import { useState } from 'react';
|
|
import { Grid, Table, Input, Select, Space, Typography, Tag, Button, Tooltip } from 'antd';
|
|
import { EyeOutlined } from '@ant-design/icons';
|
|
import type { TableColumnsType } from 'antd';
|
|
import type { ProdutoSummary } from '@sar/api-interface';
|
|
import { useCatalog, usePautas } from '../../lib/queries/catalog';
|
|
import { useCondicoesComerciais, type CondicaoComercial } from '../../lib/condicoes-comerciais';
|
|
import { CondicaoTag } from '../../components/CondicaoTag';
|
|
import { ProductDetailDrawer } from './ProductDetailDrawer';
|
|
|
|
const { useBreakpoint } = Grid;
|
|
|
|
const { Title, Text } = 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' }) : '—';
|
|
}
|
|
|
|
function buildColumns(
|
|
onDetail: (id: number) => void,
|
|
condicoesDe: (p: ProdutoSummary) => CondicaoComercial[],
|
|
): TableColumnsType<ProdutoSummary> {
|
|
return [
|
|
{
|
|
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,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: 6,
|
|
flexWrap: 'wrap',
|
|
}}
|
|
>
|
|
{v.trim()}
|
|
<CondicaoTag conds={condicoesDe(row)} compact />
|
|
</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: 120,
|
|
align: 'right',
|
|
render: (v: string) => (
|
|
<span style={{ fontWeight: 600, color: Number(v) > 0 ? '#389e0d' : '#999' }}>
|
|
{fmtPrice(v)}
|
|
</span>
|
|
),
|
|
},
|
|
{
|
|
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>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
title: '',
|
|
key: 'actions',
|
|
width: 48,
|
|
align: 'center',
|
|
render: (_: unknown, row: ProdutoSummary) => (
|
|
<Tooltip title="Ver detalhes">
|
|
<Button
|
|
type="text"
|
|
size="small"
|
|
icon={<EyeOutlined />}
|
|
onClick={() => onDetail(row.idErp)}
|
|
/>
|
|
</Tooltip>
|
|
),
|
|
},
|
|
];
|
|
}
|
|
|
|
export function CatalogPage() {
|
|
const [q, setQ] = useState('');
|
|
const [idPauta, setIdPauta] = useState<number | undefined>();
|
|
const [page, setPage] = useState(1);
|
|
const [selectedIdErp, setSelectedIdErp] = useState<number | null>(null);
|
|
const limit = 50;
|
|
const screens = useBreakpoint();
|
|
const isMobile = !screens.md;
|
|
|
|
const { data: pautas, isLoading: pautasLoading } = usePautas();
|
|
const { data, isLoading } = useCatalog({ q: q || undefined, idPauta, page, limit });
|
|
const condicoesDe = useCondicoesComerciais();
|
|
|
|
const columns = buildColumns((id) => setSelectedIdErp(id), condicoesDe);
|
|
|
|
return (
|
|
<div style={{ maxWidth: 1400, margin: '0 auto' }}>
|
|
{/* ── Cabeçalho ────────────────────────────────────────────────── */}
|
|
<div style={{ marginBottom: 20 }}>
|
|
<Title level={3} style={{ margin: 0, color: '#003B8E' }}>
|
|
Catálogo de Produtos
|
|
</Title>
|
|
<Text style={{ color: '#64748B', fontSize: 14 }}>
|
|
Consulte produtos, preços por pauta e disponibilidade de estoque.
|
|
</Text>
|
|
</div>
|
|
|
|
{/* ── Filtros ──────────────────────────────────────────────────── */}
|
|
<Space style={{ marginBottom: 16, width: '100%' }} wrap>
|
|
<Search
|
|
placeholder="Buscar por código ou descrição..."
|
|
allowClear
|
|
style={{ width: isMobile ? '100%' : 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: isMobile ? '100%' : 340 }}
|
|
onChange={(v) => {
|
|
setIdPauta(v as number | undefined);
|
|
setPage(1);
|
|
}}
|
|
options={pautas?.map((p) => ({
|
|
value: p.idPauta,
|
|
label:
|
|
p.qtdReps != null
|
|
? `${p.codigo} — ${p.descricao} · ${p.qtdReps} rep${p.qtdReps !== 1 ? 's' : ''}`
|
|
: `${p.codigo} — ${p.descricao}`,
|
|
}))}
|
|
/>
|
|
</Space>
|
|
|
|
<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),
|
|
}}
|
|
onRow={(row) => ({
|
|
style: { cursor: 'pointer' },
|
|
onDoubleClick: () => setSelectedIdErp(row.idErp),
|
|
})}
|
|
/>
|
|
|
|
<ProductDetailDrawer idErp={selectedIdErp} onClose={() => setSelectedIdErp(null)} />
|
|
</div>
|
|
);
|
|
}
|