feat(orders): fluxo de aprovação — approve/reject endpoints + UIs (C5)

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>
This commit is contained in:
2026-05-28 00:01:14 +00:00
parent 6769a0d82a
commit 356c8e3c2c
9 changed files with 731 additions and 33 deletions

View File

@@ -2,22 +2,31 @@
// Em produção o token vem do master-login real (fora do escopo do MVP).
import { useState } from 'react';
import { Alert, Button, Card, Flex, Space, Typography } from 'antd';
import { Alert, Button, Card, Divider, Flex, Space, Typography } from 'antd';
import { apiFetch } from '../../lib/api-client';
import { authStore } from '../../lib/auth-store';
import { AuthTokenResponseSchema } from '@sar/api-interface';
type DevUser = { userId: string; role: string; label: string };
const DEV_USERS: DevUser[] = [
{ userId: 'user-001', role: 'rep', label: 'Rafael — Rep (user-001)' },
{ userId: 'user-002', role: 'rep', label: 'Rep 2 (user-002)' },
{ userId: 'user-sandra-01', role: 'supervisor', label: 'Sandra — Supervisora' },
{ userId: 'user-manager-01', role: 'manager', label: 'Gerente (user-manager-01)' },
];
export function DevLogin({ onLogin }: { onLogin: () => void }) {
const [loading, setLoading] = useState(false);
const [loading, setLoading] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
async function handleLogin() {
setLoading(true);
async function handleLogin(user: DevUser) {
setLoading(user.userId);
setError(null);
try {
const raw = await apiFetch('/api/v1/auth/dev/token', {
method: 'POST',
body: { userId: 'user-001', workspaceId: 'dev-workspace', role: 'rep' },
body: { userId: user.userId, workspaceId: 'dev-workspace', role: user.role },
});
const { accessToken } = AuthTokenResponseSchema.parse(raw);
authStore.set(accessToken);
@@ -25,14 +34,14 @@ export function DevLogin({ onLogin }: { onLogin: () => void }) {
} catch (e) {
setError(e instanceof Error ? e.message : 'Erro ao obter token');
} finally {
setLoading(false);
setLoading(null);
}
}
return (
<Flex justify="center" align="center" style={{ minHeight: '100vh' }}>
<Card style={{ width: 360 }}>
<Space direction="vertical" size={20} style={{ width: '100%' }}>
<Card style={{ width: 380 }}>
<Space direction="vertical" size={16} style={{ width: '100%' }}>
<Typography.Title level={3} style={{ margin: 0 }}>
SAR · Login Dev
</Typography.Title>
@@ -43,9 +52,18 @@ export function DevLogin({ onLogin }: { onLogin: () => void }) {
showIcon
/>
{error && <Alert type="error" message={error} showIcon />}
<Button type="primary" block loading={loading} onClick={() => void handleLogin()}>
Entrar como Rafael (rep · user-001)
</Button>
<Divider style={{ margin: '4px 0' }}>Entrar como</Divider>
{DEV_USERS.map((u) => (
<Button
key={u.userId}
block
type={u.role === 'rep' ? 'primary' : 'default'}
loading={loading === u.userId}
onClick={() => void handleLogin(u)}
>
{u.label}
</Button>
))}
</Space>
</Card>
</Flex>