feat(auth): endpoint /auth/me, cockpits renomeados e menu de logout

- GET /api/v1/auth/me retorna perfil real do ERP (vw_representantes)
- Contrato UserProfile adicionado ao shared api-interface
- Hook useCurrentUser() no frontend consome o endpoint
- Cockpit rafael → rep, sandra → supervisor (pastas e componentes)
- Topbar exibe iniciais do usuário e dropdown com nome, role e "Sair"
- Logout limpa token e recarrega para voltar ao DevLogin

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-29 17:48:24 +00:00
parent 20b0793227
commit a00a5c6a53
16 changed files with 156 additions and 33 deletions

View File

@@ -0,0 +1,142 @@
import { useState } from 'react';
import { Table, Tag, Input, Select, Space, Typography, Badge } from 'antd';
import type { TableColumnsType } from 'antd';
import { Link } from '@tanstack/react-router';
import type { PedidoSummary } from '@sar/api-interface';
import { SITUA_LABEL } from '@sar/api-interface';
import { useOrderList } from '../../lib/queries/orders';
const { Title } = Typography;
const { Search } = Input;
const SITUA_COLOR: Record<number, string> = {
1: 'warning',
2: 'processing',
3: 'error',
4: 'success',
};
const columns: TableColumnsType<PedidoSummary> = [
{
title: 'Nº',
dataIndex: 'numero',
width: 120,
render: (_: number, row: PedidoSummary) => {
const label = row.numero ? String(row.numero) : row.numPedSar || row.id;
return row.fonte === 'erp' ? (
<span style={{ fontVariantNumeric: 'tabular-nums' }}>{label}</span>
) : (
<Link to="/pedidos/$id" params={{ id: row.id }}>
{label}
</Link>
);
},
},
{
title: 'Status',
dataIndex: 'situa',
width: 150,
render: (s: number, row: PedidoSummary) => {
const label = row.statusDescr ?? SITUA_LABEL[s] ?? String(s);
return (
<Badge
status={
(SITUA_COLOR[s] ?? 'default') as
| 'default'
| 'warning'
| 'processing'
| 'success'
| 'error'
}
text={<Tag color={SITUA_COLOR[s] ?? 'default'}>{label}</Tag>}
/>
);
},
},
{
title: 'Total',
dataIndex: 'total',
width: 130,
align: 'right',
render: (v: string) =>
Number(v).toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' }),
},
{
title: 'Data',
dataIndex: 'dtPedido',
width: 130,
render: (v: string) => new Date(v).toLocaleDateString('pt-BR'),
},
];
export function OrdersPage() {
const [numFilter, setNumFilter] = useState('');
const [situaFilter, setSituaFilter] = useState<number | undefined>();
const [page, setPage] = useState(1);
const limit = 50;
const { data, isLoading } = useOrderList({
numPedSar: numFilter || undefined,
situa: situaFilter,
page,
limit,
});
return (
<div style={{ padding: 24 }}>
<Title level={3} style={{ marginBottom: 16 }}>
Pedidos
</Title>
<Space style={{ marginBottom: 16 }} wrap>
<Search
placeholder="Buscar por número (SAR-NNNNN)..."
allowClear
style={{ width: 240 }}
onSearch={(v) => {
setNumFilter(v);
setPage(1);
}}
onChange={(e) => {
if (!e.target.value) {
setNumFilter('');
setPage(1);
}
}}
/>
<Select
placeholder="Status"
allowClear
style={{ width: 160 }}
onChange={(v) => {
setSituaFilter(v as number | undefined);
setPage(1);
}}
options={[
{ value: 1, label: 'Ag. Aprovação' },
{ value: 2, label: 'Aprovado' },
{ value: 3, label: 'Cancelado' },
{ value: 4, label: 'Faturado' },
]}
/>
</Space>
<Table<PedidoSummary>
rowKey="id"
columns={columns}
dataSource={data?.data ?? []}
loading={isLoading}
rowClassName={(row) => (row.situa === 1 ? 'row-pending' : '')}
pagination={{
current: page,
pageSize: limit,
total: data?.total ?? 0,
showSizeChanger: false,
onChange: (p) => setPage(p),
}}
/>
<style>{`.row-pending td { background: #fffbe6 !important; }`}</style>
</div>
);
}