feat(web+api): catálogo com detalhe do produto, preços por pauta e consulta de pedidos ERP

- Catálogo: modal de detalhe centralizado (1100px) com identificação, estoque,
  preços base e lista de preços por pauta do representante (DISTINCT para evitar duplicatas)
- Contrato: loteMulVenda promovido para ProdutoSummarySchema; PautaPrecoSchema adicionado
- Pedido novo: valida lote múltiplo de venda — qtd inicial = lote, step = lote,
  InputNumber em erro quando inválido, alerta e bloqueio do submit
- Consulta ERP: tela OrdersErpPage com listagem de pedidos do ERP (P/E), contrato
  PedidoErpConsulta, endpoint no orders.controller, rota e sidebar

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-24 20:27:19 +00:00
parent 0858e1e941
commit a2bab75bad
15 changed files with 1804 additions and 492 deletions

View File

@@ -4,7 +4,7 @@ import {
Button,
Card,
Col,
Drawer,
Modal,
Dropdown,
Grid,
Row,
@@ -397,9 +397,9 @@ function CustomerExpandedDetail({ summary }: { summary: ClientSummary }) {
);
}
// ─── CustomerDetailsDrawer ────────────────────────────────────────────────────
// ─── CustomerDetailsModal ────────────────────────────────────────────────────
function CustomerDetailsDrawer({
function CustomerDetailsModal({
summary,
onClose,
onAnalyze,
@@ -439,7 +439,7 @@ function CustomerDetailsDrawer({
};
return (
<Drawer
<Modal
title={
<Space>
<Text strong style={{ fontSize: 16 }}>
@@ -448,14 +448,29 @@ function CustomerDetailsDrawer({
<CustomerStatusBadge status={summary.activityStatus} />
</Space>
}
open
onClose={onClose}
width={520}
placement="right"
styles={{ body: { padding: '16px 24px' } }}
open={!!summary}
onCancel={onClose}
centered
width={860}
destroyOnHidden
styles={{
body: { padding: '20px 24px', maxHeight: 'calc(85vh - 110px)', overflowY: 'auto' },
}}
footer={
<Space wrap>
<Button onClick={onClose}>Fechar</Button>
<Button
icon={<WhatsAppOutlined />}
style={{
background: '#25D366',
borderColor: '#25D366',
color: '#fff',
fontWeight: 600,
}}
onClick={() => window.open(`https://wa.me/55${phone.replace(/\D/g, '')}`, '_blank')}
>
WhatsApp
</Button>
<Button icon={<BarChartOutlined />} onClick={onAnalyze}>
Analisar
</Button>
@@ -483,135 +498,141 @@ function CustomerDetailsDrawer({
</Space>
}
>
<Space orientation="vertical" size={20} style={{ width: '100%' }}>
<Space direction="vertical" size={20} style={{ width: '100%' }}>
{/* Identificação */}
<Card
style={{ borderRadius: 8, background: '#F8FAFC', border: '1px solid #EBF0F5' }}
styles={{ body: { padding: '14px 16px' } }}
>
<Text strong style={{ fontSize: 18, color: '#1F2937', display: 'block' }}>
{summary.nome}
</Text>
{summary.razao && (
<Text style={{ fontSize: 13, color: '#64748B', display: 'block', marginTop: 2 }}>
{summary.razao}
</Text>
)}
{summary.cgcpf && (
<Text style={{ fontSize: 13, color: '#94A3B8', fontFamily: 'monospace' }}>
{summary.cgcpf}
</Text>
)}
<Row gutter={[16, 8]}>
<Col span={16}>
<Text strong style={{ fontSize: 18, color: '#1F2937', display: 'block' }}>
{summary.nome}
</Text>
{summary.razao && (
<Text style={{ fontSize: 13, color: '#64748B', display: 'block', marginTop: 2 }}>
{summary.razao}
</Text>
)}
{summary.cgcpf && (
<Text style={{ fontSize: 13, color: '#94A3B8', fontFamily: 'monospace' }}>
{summary.cgcpf}
</Text>
)}
</Col>
<Col span={8} style={{ textAlign: 'right' }}>
<CustomerStatusBadge status={summary.activityStatus} />
</Col>
</Row>
</Card>
{/* Dados cadastrais */}
<div>
<Text
style={{
fontSize: 12,
fontWeight: 700,
letterSpacing: '0.07em',
textTransform: 'uppercase',
color: '#64748B',
display: 'block',
marginBottom: 10,
}}
>
Dados Cadastrais
</Text>
<Row gutter={[12, 12]}>
<Col span={12}>
<span style={label}>Telefone</span>
<Space size={4}>
<PhoneOutlined style={{ color: '#94A3B8', fontSize: 12 }} />
<Text style={{ fontSize: 13 }}>{phone}</Text>
</Space>
</Col>
<Col span={12}>
<span style={label}>E-mail</span>
<Space size={4}>
<MailOutlined style={{ color: '#94A3B8', fontSize: 12 }} />
<Text style={{ fontSize: 13, wordBreak: 'break-all' }}>{summary.email ?? '—'}</Text>
</Space>
</Col>
{loadingDetail ? (
<Col span={24}>
<Spin size="small" />
</Col>
) : (
<>
{endereco !== '—' && (
<Col span={24}>
<span style={label}>Endereço</span>
<Text style={{ fontSize: 13 }}>{endereco}</Text>
</Col>
)}
{d?.inscricaoEstadual && (
<Col span={12}>
<span style={label}>Insc. Estadual</span>
<Text style={{ fontSize: 13 }}>{d.inscricaoEstadual}</Text>
</Col>
)}
</>
)}
</Row>
</div>
<Divider style={{ margin: '4px 0' }} />
{/* Dados comerciais */}
<div>
<Text
style={{
fontSize: 12,
fontWeight: 700,
letterSpacing: '0.07em',
textTransform: 'uppercase',
color: '#64748B',
display: 'block',
marginBottom: 10,
}}
>
Dados Comerciais
</Text>
<Row gutter={[12, 12]}>
{limiteFormatado !== '—' && (
<Col span={12}>
<span style={label}>Limite de Crédito</span>
<Text strong style={{ fontSize: 13, color: '#003B8E' }}>
{limiteFormatado}
</Text>
</Col>
)}
<Col span={12}>
<span style={label}>Representante</span>
<Text style={{ fontSize: 13 }}>
{summary.nomeVendedor ?? `Cód. ${summary.codVendedor}`}
{summary.nomeVendedor && (
<Text type="secondary" style={{ fontSize: 11, marginLeft: 4 }}>
(cód. {summary.codVendedor})
</Text>
)}
</Text>
</Col>
{summary.dtUltimaCompra && (
<Col span={12}>
<span style={label}>Última Compra</span>
{/* Dados cadastrais + comerciais em 2 colunas */}
<Row gutter={[24, 16]}>
<Col span={12}>
<Text
style={{
fontSize: 12,
fontWeight: 700,
letterSpacing: '0.07em',
textTransform: 'uppercase',
color: '#64748B',
display: 'block',
marginBottom: 10,
}}
>
Dados Cadastrais
</Text>
<Space direction="vertical" size={10} style={{ width: '100%' }}>
<div>
<span style={label}>Telefone</span>
<Space size={4}>
<CalendarOutlined style={{ color: '#94A3B8', fontSize: 12 }} />
<Text style={{ fontSize: 13 }}>
{fmtDate(summary.dtUltimaCompra)}
{dias !== null && (
<Text style={{ fontSize: 11, color: '#94A3B8', marginLeft: 4 }}>
({dias} dias)
</Text>
)}
<PhoneOutlined style={{ color: '#94A3B8', fontSize: 12 }} />
<Text style={{ fontSize: 13 }}>{phone}</Text>
</Space>
</div>
<div>
<span style={label}>E-mail</span>
<Space size={4}>
<MailOutlined style={{ color: '#94A3B8', fontSize: 12 }} />
<Text style={{ fontSize: 13, wordBreak: 'break-all' }}>
{summary.email ?? '—'}
</Text>
</Space>
</Col>
)}
</Row>
</div>
</div>
{loadingDetail ? (
<Spin size="small" />
) : (
<>
{endereco !== '—' && (
<div>
<span style={label}>Endereço</span>
<Text style={{ fontSize: 13 }}>{endereco}</Text>
</div>
)}
{d?.inscricaoEstadual && (
<div>
<span style={label}>Insc. Estadual</span>
<Text style={{ fontSize: 13 }}>{d.inscricaoEstadual}</Text>
</div>
)}
</>
)}
</Space>
</Col>
<Col span={12}>
<Text
style={{
fontSize: 12,
fontWeight: 700,
letterSpacing: '0.07em',
textTransform: 'uppercase',
color: '#64748B',
display: 'block',
marginBottom: 10,
}}
>
Dados Comerciais
</Text>
<Space direction="vertical" size={10} style={{ width: '100%' }}>
{limiteFormatado !== '—' && (
<div>
<span style={label}>Limite de Crédito</span>
<Text strong style={{ fontSize: 13, color: '#003B8E' }}>
{limiteFormatado}
</Text>
</div>
)}
<div>
<span style={label}>Representante</span>
<Text style={{ fontSize: 13 }}>
{summary.nomeVendedor ?? `Cód. ${summary.codVendedor}`}
{summary.nomeVendedor && (
<Text type="secondary" style={{ fontSize: 11, marginLeft: 4 }}>
(cód. {summary.codVendedor})
</Text>
)}
</Text>
</div>
{summary.dtUltimaCompra && (
<div>
<span style={label}>Última Compra</span>
<Space size={4}>
<CalendarOutlined style={{ color: '#94A3B8', fontSize: 12 }} />
<Text style={{ fontSize: 13 }}>
{fmtDate(summary.dtUltimaCompra)}
{dias !== null && (
<Text style={{ fontSize: 11, color: '#94A3B8', marginLeft: 4 }}>
({dias} dias)
</Text>
)}
</Text>
</Space>
</div>
)}
</Space>
</Col>
</Row>
{/* Últimos pedidos */}
{(loadingOrders || orders.length > 0) && (
@@ -634,7 +655,7 @@ function CustomerDetailsDrawer({
{loadingOrders ? (
<Spin size="small" />
) : (
<Space orientation="vertical" size={6} style={{ width: '100%' }}>
<Space direction="vertical" size={6} style={{ width: '100%' }}>
{orders.slice(0, 5).map((p) => (
<div
key={p.id}
@@ -706,29 +727,14 @@ function CustomerDetailsDrawer({
</div>
</>
)}
<Button
block
icon={<WhatsAppOutlined />}
style={{
borderRadius: 8,
background: '#25D366',
borderColor: '#25D366',
color: '#fff',
fontWeight: 600,
}}
onClick={() => window.open(`https://wa.me/55${phone.replace(/\D/g, '')}`, '_blank')}
>
Enviar WhatsApp
</Button>
</Space>
</Drawer>
</Modal>
);
}
// ─── CustomerAnalysisDrawer ───────────────────────────────────────────────────
// ─── CustomerAnalysisModal ───────────────────────────────────────────────────
function CustomerAnalysisDrawer({
function CustomerAnalysisModal({
summary,
onClose,
}: {
@@ -755,25 +761,55 @@ function CustomerAnalysisDrawer({
};
return (
<Drawer
<Modal
title={
<Space>
<BarChartOutlined />
<Text strong>Análise Comercial</Text>
<Text strong>Análise Comercial {summary.nome}</Text>
</Space>
}
open={!!summary}
onCancel={onClose}
centered
width={720}
destroyOnHidden
styles={{ body: { padding: '20px 24px' } }}
footer={
<Space>
<Button
icon={<WhatsAppOutlined />}
style={{
background: '#25D366',
borderColor: '#25D366',
color: '#fff',
fontWeight: 600,
borderRadius: 8,
}}
onClick={() =>
window.open(
`https://wa.me/55${(summary.telefone ?? '').replace(/\D/g, '')}`,
'_blank',
)
}
>
WhatsApp
</Button>
<Button
icon={<MailOutlined />}
style={{ borderRadius: 8 }}
onClick={() => window.open(`mailto:${summary.email ?? ''}`, '_blank')}
>
E-mail
</Button>
<Button onClick={onClose} style={{ borderRadius: 8 }}>
Fechar
</Button>
</Space>
}
open
onClose={onClose}
width={480}
placement="right"
styles={{ body: { padding: '16px 24px' } }}
>
<Space orientation="vertical" size={20} style={{ width: '100%' }}>
<Space direction="vertical" size={20} style={{ width: '100%' }}>
<div>
<Text strong style={{ fontSize: 17, color: '#1F2937', display: 'block' }}>
{summary.nome}
</Text>
<Space size={8} style={{ marginTop: 4 }}>
<Space size={8}>
<CustomerStatusBadge status={summary.activityStatus} />
{summary.cgcpf && (
<Text style={{ fontSize: 12, color: '#64748B' }}>{summary.cgcpf}</Text>
@@ -875,39 +911,8 @@ function CustomerAnalysisDrawer({
</div>
</div>
)}
<Space>
<Button
icon={<WhatsAppOutlined />}
style={{
background: '#25D366',
borderColor: '#25D366',
color: '#fff',
fontWeight: 600,
borderRadius: 8,
}}
onClick={() =>
window.open(
`https://wa.me/55${(summary.telefone ?? '').replace(/\D/g, '')}`,
'_blank',
)
}
>
WhatsApp
</Button>
<Button
icon={<MailOutlined />}
style={{ borderRadius: 8 }}
onClick={() => window.open(`mailto:${summary.email ?? ''}`, '_blank')}
>
E-mail
</Button>
<Button onClick={onClose} style={{ borderRadius: 8 }}>
Fechar
</Button>
</Space>
</Space>
</Drawer>
</Modal>
);
}
@@ -1535,9 +1540,9 @@ export function ClientsPage() {
)}
</Row>
{/* ── Drawers ───────────────────────────────────────────────────── */}
{/* ── Modais ───────────────────────────────────────────────────── */}
{detailSummary && (
<CustomerDetailsDrawer
<CustomerDetailsModal
summary={detailSummary}
onClose={() => setDetailSummary(null)}
onAnalyze={() => {
@@ -1547,10 +1552,7 @@ export function ClientsPage() {
/>
)}
{analysisSummary && (
<CustomerAnalysisDrawer
summary={analysisSummary}
onClose={() => setAnalysisSummary(null)}
/>
<CustomerAnalysisModal summary={analysisSummary} onClose={() => setAnalysisSummary(null)} />
)}
{/* FAB mobile */}