PATCH /orders/:id/approve e /reject com alçada role-gated; OrderDetailPage com modais de aprovação e recusa; ApprovalQueuePage para Sandra; badge de pendências na Sidebar; DevLogin com 4 perfis (rep, supervisor, gerente). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
97 lines
2.6 KiB
TypeScript
97 lines
2.6 KiB
TypeScript
import { Table, Tag, Typography, Badge, Space } from 'antd';
|
|
import type { TableColumnsType } from 'antd';
|
|
import { Link } from '@tanstack/react-router';
|
|
import type { OrderSummary } from '@sar/api-interface';
|
|
import { useOrderList } from '../../lib/queries/orders';
|
|
|
|
const { Title } = Typography;
|
|
|
|
function hoursWaiting(issuedAt: string): number {
|
|
return Math.floor((Date.now() - new Date(issuedAt).getTime()) / 3_600_000);
|
|
}
|
|
|
|
const columns: TableColumnsType<OrderSummary> = [
|
|
{
|
|
title: 'Nº',
|
|
dataIndex: 'number',
|
|
width: 120,
|
|
render: (num: string, row: OrderSummary) => (
|
|
<Link to="/pedidos/$id" params={{ id: row.id }}>
|
|
{num}
|
|
</Link>
|
|
),
|
|
},
|
|
{ title: 'Rep', dataIndex: 'repId', width: 130, ellipsis: true },
|
|
{ title: 'Cliente', dataIndex: 'clientName', ellipsis: true },
|
|
{
|
|
title: 'Total',
|
|
dataIndex: 'total',
|
|
width: 130,
|
|
align: 'right',
|
|
render: (v: string) =>
|
|
Number(v).toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' }),
|
|
},
|
|
{
|
|
title: 'Desc. Global',
|
|
dataIndex: 'discountPct',
|
|
width: 110,
|
|
align: 'right',
|
|
render: (v: string) => `${v}%`,
|
|
},
|
|
{
|
|
title: 'Aguardando',
|
|
dataIndex: 'issuedAt',
|
|
width: 130,
|
|
render: (v: string) => {
|
|
const h = hoursWaiting(v);
|
|
return <Tag color={h > 2 ? 'red' : 'orange'}>{h}h</Tag>;
|
|
},
|
|
},
|
|
{
|
|
title: '',
|
|
width: 100,
|
|
render: (_: unknown, row: OrderSummary) => (
|
|
<Link to="/pedidos/$id" params={{ id: row.id }}>
|
|
<Tag color="blue" style={{ cursor: 'pointer' }}>
|
|
Analisar
|
|
</Tag>
|
|
</Link>
|
|
),
|
|
},
|
|
];
|
|
|
|
export function ApprovalQueuePage() {
|
|
const { data, isLoading } = useOrderList({ status: 'pending_approval', limit: 200 });
|
|
|
|
const urgentCount = data?.data.filter((o) => hoursWaiting(o.issuedAt) > 2).length ?? 0;
|
|
|
|
return (
|
|
<div style={{ padding: 24 }}>
|
|
<Space align="center" style={{ marginBottom: 16 }}>
|
|
<Title level={3} style={{ margin: 0 }}>
|
|
Fila de Aprovações
|
|
</Title>
|
|
{urgentCount > 0 && (
|
|
<Badge
|
|
count={urgentCount}
|
|
style={{ backgroundColor: '#cf1322' }}
|
|
title={`${urgentCount} urgente(s) — mais de 2h aguardando`}
|
|
/>
|
|
)}
|
|
</Space>
|
|
|
|
<Table<OrderSummary>
|
|
rowKey="id"
|
|
columns={columns}
|
|
dataSource={data?.data ?? []}
|
|
loading={isLoading}
|
|
rowClassName={(row) => (hoursWaiting(row.issuedAt) > 2 ? 'row-urgent' : '')}
|
|
pagination={false}
|
|
locale={{ emptyText: 'Nenhum pedido aguardando aprovação.' }}
|
|
/>
|
|
|
|
<style>{`.row-urgent td { background: #fff1f0 !important; }`}</style>
|
|
</div>
|
|
);
|
|
}
|