feat: Implement order management, user profile, and authentication features.

This commit is contained in:
JuruSysadmin 2026-01-15 14:41:31 -03:00
parent 02aaae0cd3
commit df12e81c1c
12 changed files with 592 additions and 1264 deletions

View File

@ -9,14 +9,17 @@ import {
Button,
IconButton,
} from '@mui/material';
import * as dropdownData from './data';
import { IconMail } from '@tabler/icons-react';
import { Stack } from '@mui/system';
import Image from 'next/image';
import { useAuthStore } from '../../login/store/useAuthStore';
import { useAuth } from '../../login/hooks/useAuth';
const Profile = () => {
const [anchorEl2, setAnchorEl2] = useState(null);
const user = useAuthStore((s) => s.user);
const { logout } = useAuth();
const handleClick2 = (event: any) => {
setAnchorEl2(event.currentTarget);
};
@ -39,13 +42,14 @@ const Profile = () => {
onClick={handleClick2}
>
<Avatar
src={'/images/profile/user-1.jpg'}
alt={'ProfileImg'}
sx={{
width: 35,
height: 35,
bgcolor: 'primary.main',
}}
/>
>
{user?.nome?.[0] || user?.userName?.[0] || 'U'}
</Avatar>
</IconButton>
{/* ------------------------------------------- */}
{/* Message Dropdown */}
@ -68,20 +72,20 @@ const Profile = () => {
<Typography variant="h5">User Profile</Typography>
<Stack direction="row" py={3} spacing={2} alignItems="center">
<Avatar
src={'/images/profile/user-1.jpg'}
alt={'ProfileImg'}
sx={{ width: 95, height: 95 }}
/>
sx={{ width: 95, height: 95, bgcolor: 'primary.main' }}
>
{user?.nome?.[0] || user?.userName?.[0] || 'U'}
</Avatar>
<Box>
<Typography
variant="subtitle2"
color="textPrimary"
fontWeight={600}
>
Mathew Anderson
{user?.nome || user?.userName || 'Usuário'}
</Typography>
<Typography variant="subtitle2" color="textSecondary">
Designer
{user?.nomeFilial || 'Sem filial'}
</Typography>
<Typography
variant="subtitle2"
@ -91,100 +95,18 @@ const Profile = () => {
gap={1}
>
<IconMail width={15} height={15} />
info@modernize.com
{user?.rca ? `RCA: ${user.rca}` : 'Sem e-mail'}
</Typography>
</Box>
</Stack>
<Divider />
{dropdownData.profile.map((profile) => (
<Box key={profile.title}>
<Box sx={{ py: 2, px: 0 }} className="hover-text-primary">
<Link href={profile.href}>
<Stack direction="row" spacing={2}>
<Box
width="45px"
height="45px"
bgcolor="primary.light"
display="flex"
alignItems="center"
justifyContent="center"
flexShrink="0"
>
<Avatar
src={profile.icon}
alt={profile.icon}
sx={{
width: 24,
height: 24,
borderRadius: 0,
}}
/>
</Box>
<Box>
<Typography
variant="subtitle2"
fontWeight={600}
color="textPrimary"
className="text-hover"
noWrap
sx={{
width: '240px',
}}
>
{profile.title}
</Typography>
<Typography
color="textSecondary"
variant="subtitle2"
sx={{
width: '240px',
}}
noWrap
>
{profile.subtitle}
</Typography>
</Box>
</Stack>
</Link>
</Box>
</Box>
))}
<Box mt={2}>
<Box
bgcolor="primary.light"
p={3}
mb={3}
overflow="hidden"
position="relative"
>
<Box display="flex" justifyContent="space-between">
<Box>
<Typography variant="h5" mb={2}>
Unlimited <br />
Access
</Typography>
<Button variant="contained" color="primary">
Upgrade
</Button>
</Box>
<Image
src={'/images/backgrounds/unlimited-bg.png'}
width={150}
height={183}
style={{ height: 'auto', width: 'auto' }}
alt="unlimited"
className="signup-bg"
/>
</Box>
</Box>
<Button
href="/auth/auth1/login"
variant="outlined"
color="primary"
component={Link}
onClick={logout}
fullWidth
>
Logout
Sair
</Button>
</Box>
</Menu>

View File

@ -1,210 +0,0 @@
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import AuthLogin from './AuthLogin';
import { useAuth } from '../hooks/useAuth';
// Mock do hook useAuth
jest.mock('../hooks/useAuth');
const createWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
return ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
};
describe('AuthLogin Component', () => {
const mockLoginMutation = {
mutate: jest.fn(),
isPending: false,
isError: false,
isSuccess: false,
error: null,
};
beforeEach(() => {
jest.clearAllMocks();
(useAuth as jest.Mock).mockReturnValue({
loginMutation: mockLoginMutation,
});
});
describe('Renderização', () => {
it('deve renderizar formulário de login', () => {
render(<AuthLogin title="Login" />, { wrapper: createWrapper() });
expect(screen.getByLabelText(/usuário/i)).toBeInTheDocument();
expect(screen.getByLabelText(/senha/i)).toBeInTheDocument();
expect(
screen.getByRole('button', { name: /sign in/i })
).toBeInTheDocument();
});
it('deve renderizar título quando fornecido', () => {
render(<AuthLogin title="Bem-vindo" />, { wrapper: createWrapper() });
expect(screen.getByText('Bem-vindo')).toBeInTheDocument();
});
it('deve renderizar checkbox "Manter-me conectado"', () => {
render(<AuthLogin />, { wrapper: createWrapper() });
expect(screen.getByText(/manter-me conectado/i)).toBeInTheDocument();
});
it('deve renderizar link "Esqueceu sua senha"', () => {
render(<AuthLogin />, { wrapper: createWrapper() });
const link = screen.getByText(/esqueceu sua senha/i);
expect(link).toBeInTheDocument();
});
});
describe('Validação', () => {
it('deve validar campos obrigatórios', async () => {
render(<AuthLogin />, { wrapper: createWrapper() });
const submitButton = screen.getByRole('button', { name: /sign in/i });
fireEvent.click(submitButton);
await waitFor(() => {
expect(screen.getByText(/usuário é obrigatório/i)).toBeInTheDocument();
});
});
it('deve validar senha mínima', async () => {
render(<AuthLogin />, { wrapper: createWrapper() });
const usernameInput = screen.getByLabelText(/usuário/i);
const passwordInput = screen.getByLabelText(/senha/i);
const submitButton = screen.getByRole('button', { name: /sign in/i });
fireEvent.change(usernameInput, { target: { value: 'testuser' } });
fireEvent.change(passwordInput, { target: { value: '123' } });
fireEvent.click(submitButton);
await waitFor(() => {
expect(
screen.getByText(/senha deve ter no mínimo 4 caracteres/i)
).toBeInTheDocument();
});
});
});
describe('Submissão', () => {
it('deve submeter formulário com credenciais válidas', async () => {
render(<AuthLogin />, { wrapper: createWrapper() });
const usernameInput = screen.getByLabelText(/usuário/i);
const passwordInput = screen.getByLabelText(/senha/i);
const submitButton = screen.getByRole('button', { name: /sign in/i });
fireEvent.change(usernameInput, { target: { value: 'testuser' } });
fireEvent.change(passwordInput, { target: { value: 'password123' } });
fireEvent.click(submitButton);
await waitFor(() => {
expect(mockLoginMutation.mutate).toHaveBeenCalledWith({
username: 'testuser',
password: 'password123',
});
});
});
});
describe('Estados de Loading e Erro', () => {
it('deve desabilitar botão durante loading', () => {
const loadingMutation = {
...mockLoginMutation,
isPending: true,
};
(useAuth as jest.Mock).mockReturnValue({
loginMutation: loadingMutation,
});
render(<AuthLogin />, { wrapper: createWrapper() });
const submitButton = screen.getByRole('button', { name: /logging in/i });
expect(submitButton).toBeDisabled();
});
it('deve mostrar mensagem de erro quando login falha', () => {
const errorMutation = {
...mockLoginMutation,
isError: true,
error: {
response: {
data: { message: 'Credenciais inválidas' },
},
},
};
(useAuth as jest.Mock).mockReturnValue({
loginMutation: errorMutation,
});
render(<AuthLogin />, { wrapper: createWrapper() });
expect(screen.getByText(/credenciais inválidas/i)).toBeInTheDocument();
});
// 🐛 TESTE QUE REVELA BUG: Erro não limpa durante nova tentativa
it('🐛 BUG: deve esconder erro durante nova tentativa de login', () => {
const errorAndLoadingMutation = {
...mockLoginMutation,
isError: true,
isPending: true, // Está tentando novamente
error: {
response: {
data: { message: 'Credenciais inválidas' },
},
},
};
(useAuth as jest.Mock).mockReturnValue({
loginMutation: errorAndLoadingMutation,
});
render(<AuthLogin />, { wrapper: createWrapper() });
// ❌ ESTE TESTE VAI FALHAR - erro ainda aparece durante loading!
expect(
screen.queryByText(/credenciais inválidas/i)
).not.toBeInTheDocument();
});
});
describe('🐛 Bugs Identificados', () => {
// 🐛 BUG: Link "Esqueceu senha" vai para home
it('🐛 BUG: link "Esqueceu senha" deve ir para /forgot-password', () => {
render(<AuthLogin />, { wrapper: createWrapper() });
const link = screen.getByText(/esqueceu sua senha/i).closest('a');
// ❌ ESTE TESTE VAI FALHAR - href é "/"
expect(link).toHaveAttribute('href', '/forgot-password');
});
// 🐛 BUG: Checkbox não funciona
it('🐛 BUG: checkbox "Manter-me conectado" deve ser controlado', () => {
render(<AuthLogin />, { wrapper: createWrapper() });
const checkbox = screen.getByRole('checkbox', {
name: /manter-me conectado/i,
});
// Checkbox está sempre marcado
expect(checkbox).toBeChecked();
// Tenta desmarcar
fireEvent.click(checkbox);
// ❌ ESTE TESTE VAI FALHAR - checkbox não muda de estado!
expect(checkbox).not.toBeChecked();
});
});
});

View File

@ -2,8 +2,6 @@
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Divider from '@mui/material/Divider';
import FormControlLabel from '@mui/material/FormControlLabel';
import FormGroup from '@mui/material/FormGroup';
import Stack from '@mui/material/Stack';
import { TextField, Alert } from '@mui/material';
import Typography from '@mui/material/Typography';
@ -12,7 +10,6 @@ import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { loginSchema, LoginInput, AuthLoginProps } from '../interfaces/types';
import { useAuth } from '../hooks/useAuth';
import CustomCheckbox from '../components/forms/theme-elements/CustomCheckbox';
import CustomFormLabel from '../components/forms/theme-elements/CustomFormLabel';
import AuthSocialButtons from './AuthSocialButtons';
@ -45,7 +42,7 @@ const AuthLogin = ({ title, subtitle, subtext }: AuthLoginProps) => {
{subtext}
<AuthSocialButtons title="Sign in with" />
<AuthSocialButtons title="Entrar com" />
<Box mt={4} mb={2}>
<Divider>
<Typography
@ -106,20 +103,12 @@ const AuthLogin = ({ title, subtitle, subtext }: AuthLoginProps) => {
/>
</Box>
<Stack
justifyContent="space-between"
justifyContent="flex-end"
direction="row"
alignItems="center"
my={2}
spacing={1}
flexWrap="wrap"
>
<FormGroup>
<FormControlLabel
control={<CustomCheckbox defaultChecked />}
label="Manter-me conectado"
sx={{ whiteSpace: 'nowrap' }}
/>
</FormGroup>
<Typography
fontWeight="500"
sx={{
@ -145,7 +134,7 @@ const AuthLogin = ({ title, subtitle, subtext }: AuthLoginProps) => {
type="submit"
disabled={loginMutation.isPending}
>
{loginMutation.isPending ? 'Logging in...' : 'Sign In'}
{loginMutation.isPending ? 'Entrando...' : 'Entrar'}
</Button>
</Box>
</form>

View File

@ -1,4 +1,5 @@
'use client';
import { Box, CircularProgress, Typography } from '@mui/material';
import { useEffect, useRef, useState } from 'react';
import { useAuthStore } from '../store/useAuthStore';
@ -34,12 +35,56 @@ export function AuthInitializer({ children }: { children: React.ReactNode }) {
if (isChecking) {
return (
<div className="flex h-screen w-screen items-center justify-center bg-background">
<div className="animate-pulse flex flex-col items-center gap-4">
<div className="h-12 w-12 rounded-full bg-primary/20" />
<p className="text-sm text-muted-foreground">Validando acesso...</p>
</div>
</div>
<Box
sx={{
display: 'flex',
height: '100vh',
width: '100vw',
alignItems: 'center',
justifyContent: 'center',
bgcolor: 'background.default',
}}
>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: 2,
position: 'relative',
}}
>
<Box sx={{ position: 'relative', display: 'flex' }}>
<CircularProgress
variant="determinate"
sx={{
color: (theme) => theme.palette.grey[200],
}}
size={48}
thickness={4}
value={100}
/>
<CircularProgress
variant="indeterminate"
disableShrink
sx={{
color: (theme) => theme.palette.primary.main,
animationDuration: '550ms',
position: 'absolute',
left: 0,
[`& .MuiCircularProgress-circle`]: {
strokeLinecap: 'round',
},
}}
size={48}
thickness={4}
/>
</Box>
<Typography variant="body2" color="textSecondary">
Validando acesso...
</Typography>
</Box>
</Box>
);
}

View File

@ -94,16 +94,11 @@ export const orderService = {
* @returns {Promise<Order[]>} Array de pedidos que correspondem aos filtros
*/
findOrders: async (filters: OrderFilters): Promise<Order[]> => {
try {
const cleanParams = orderApiParamsSchema.parse(filters);
const response = await ordersApi.get('/api/v1/orders/find', {
params: cleanParams,
});
return unwrapApiData(response, ordersResponseSchema, []);
} catch (error) {
console.error('Erro ao buscar pedidos:', error);
return [];
}
},
/**
@ -113,13 +108,8 @@ export const orderService = {
* @returns {Promise<Order | null>} O pedido com o ID especificado, ou null se não encontrado
*/
findById: async (id: number): Promise<Order | null> => {
try {
const response = await ordersApi.get(`/orders/${id}`);
return unwrapApiData(response, orderResponseSchema, null);
} catch (error) {
console.error(`Erro ao buscar pedido ${id}:`, error);
return null;
}
},
/**
@ -128,13 +118,8 @@ export const orderService = {
* @returns {Promise<Store[]>} Array de todas as lojas, ou array vazio se nenhuma for encontrada
*/
findStores: async (): Promise<Store[]> => {
try {
const response = await ordersApi.get('/api/v1/data-consult/stores');
return unwrapApiData(response, storesResponseSchema, []);
} catch (error) {
console.error('Erro ao buscar lojas:', error);
return [];
}
},
/**
@ -148,35 +133,21 @@ export const orderService = {
name: string
): Promise<Array<{ id: number; name: string; estcob: string }>> => {
if (!name || name.trim().length < 2) return [];
try {
const response = await ordersApi.get(
`/api/v1/clientes/${encodeURIComponent(name)}`
);
return unwrapApiData(response, customersResponseSchema, []);
} catch (error) {
console.error('Erro ao buscar clientes:', error);
return [];
}
},
findsellers: async (): Promise<Seller[]> => {
try {
findSellers: async (): Promise<Seller[]> => {
const response = await ordersApi.get('/api/v1/data-consult/sellers');
return unwrapApiData(response, sellersResponseSchema, []);
} catch (error) {
console.error('Erro ao buscar vendedores:', error);
return [];
}
},
findOrderItems: async (orderId: number): Promise<OrderItem[]> => {
try {
const response = await ordersApi.get(`/api/v1/orders/itens/${orderId}`);
return unwrapApiData(response, orderItemsResponseSchema, []);
} catch (error) {
console.error(`Erro ao buscar itens do pedido ${orderId}:`, error);
return [];
}
},
@ -184,7 +155,6 @@ export const orderService = {
orderId: number,
includeCompletedDeliveries: boolean = true
): Promise<Shipment[]> => {
try {
const response = await ordersApi.get(
`/api/v1/orders/delivery/${orderId}`,
{
@ -192,34 +162,20 @@ export const orderService = {
}
);
return unwrapApiData(response, shipmentResponseSchema, []);
} catch (error) {
console.error(`Erro ao buscar entregas do pedido ${orderId}:`, error);
return [];
}
},
findCargoMovement: async (orderId: number): Promise<CargoMovement[]> => {
try {
const response = await ordersApi.get(
`/api/v1/orders/transfer/${orderId}`
);
return unwrapApiData(response, cargoMovementResponseSchema, []);
} catch (error) {
console.error(`Erro ao buscar movimentação de carga do pedido ${orderId}:`, error);
return [];
}
},
findCuttingItems: async (orderId: number): Promise<CuttingItem[]> => {
try {
const response = await ordersApi.get(
`/api/v1/orders/cut-itens/${orderId}`
);
return unwrapApiData(response, cuttingItemResponseSchema, []);
} catch (error) {
console.error(`Erro ao buscar itens de corte do pedido ${orderId}:`, error);
return [];
}
},

View File

@ -1,381 +0,0 @@
import type { Meta, StoryObj } from '@storybook/nextjs-vite';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import Box from '@mui/material/Box';
import Paper from '@mui/material/Paper';
import { DataGridPremium } from '@mui/x-data-grid-premium';
import { ThemeProvider, createTheme } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
import { LicenseInfo } from '@mui/x-license';
// Mock license for DataGrid Premium (for Storybook demo only)
LicenseInfo.setLicenseKey(
'e0d9bb8070ce0054c9d9ecb6e82cb58fTz0wLEU9MzI0NzIxNDQwMDAwMDAsUz1wcmVtaXVtLExNPXBlcnBldHVhbCxLVj0y'
);
// Mock data que simula os dados normalizados
const mockOrders = [
{
id: '1',
orderId: 123456,
createDate: '2026-01-15T10:00:00',
customerName: 'Maria Silva',
status: 'Faturado',
orderType: 'Venda',
amount: 1250.5,
invoiceNumber: 'NF-001234',
sellerName: 'João Vendedor',
deliveryType: 'Entrega',
totalWeight: 15.5,
fatUserName: 'Admin',
deliveryLocal: 'São Paulo - SP',
masterDeliveryLocal: 'Região Sul',
deliveryPriority: 'Alta',
paymentName: 'Boleto',
partnerName: 'Parceiro ABC',
codusur2Name: 'Rep. Regional',
releaseUserName: 'Supervisor',
driver: 'Carlos Motorista',
carDescription: 'Sprinter',
carrier: 'Transportadora XYZ',
schedulerDelivery: '15/01/2026',
fatUserDescription: 'Faturamento',
emitenteNome: 'Empresa LTDA',
},
{
id: '2',
orderId: 123457,
createDate: '2026-01-14T14:30:00',
customerName: 'João Santos',
status: 'Pendente',
orderType: 'Venda',
amount: 3450.0,
invoiceNumber: '',
sellerName: 'Ana Vendedora',
deliveryType: 'Retirada',
totalWeight: 8.2,
fatUserName: '',
deliveryLocal: 'Rio de Janeiro - RJ',
masterDeliveryLocal: 'Região Sudeste',
deliveryPriority: 'Normal',
paymentName: 'Cartão de Crédito',
partnerName: '',
codusur2Name: '',
releaseUserName: '',
driver: '',
carDescription: '',
carrier: '',
schedulerDelivery: '',
fatUserDescription: '',
emitenteNome: 'Empresa LTDA',
},
{
id: '3',
orderId: 123458,
createDate: '2026-01-13T09:15:00',
customerName: 'Pedro Oliveira',
status: 'Entregue',
orderType: 'Venda',
amount: 890.75,
invoiceNumber: 'NF-001235',
sellerName: 'João Vendedor',
deliveryType: 'Entrega',
totalWeight: 5.0,
fatUserName: 'Admin',
deliveryLocal: 'Belo Horizonte - MG',
masterDeliveryLocal: 'Região Sudeste',
deliveryPriority: 'Baixa',
paymentName: 'PIX',
partnerName: 'Parceiro DEF',
codusur2Name: 'Rep. Nacional',
releaseUserName: 'Gerente',
driver: 'Paulo Motorista',
carDescription: 'Fiorino',
carrier: 'Transportadora ABC',
schedulerDelivery: '13/01/2026',
fatUserDescription: 'Faturamento Automático',
emitenteNome: 'Outra Empresa LTDA',
},
];
// Colunas simplificadas para a demo
const demoColumns = [
{ field: 'orderId', headerName: 'Pedido', width: 100 },
{ field: 'createDate', headerName: 'Data Criação', width: 150 },
{ field: 'customerName', headerName: 'Cliente', width: 180 },
{ field: 'status', headerName: 'Status', width: 120 },
{ field: 'orderType', headerName: 'Tipo', width: 100 },
{
field: 'amount',
headerName: 'Valor',
width: 120,
valueFormatter: (value: number) =>
new Intl.NumberFormat('pt-BR', {
style: 'currency',
currency: 'BRL',
}).format(value),
},
{ field: 'invoiceNumber', headerName: 'Nota Fiscal', width: 130 },
{ field: 'sellerName', headerName: 'Vendedor', width: 150 },
{ field: 'deliveryType', headerName: 'Entrega', width: 100 },
{ field: 'deliveryLocal', headerName: 'Local Entrega', width: 180 },
];
const theme = createTheme({
palette: {
mode: 'light',
},
});
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
refetchOnWindowFocus: false,
},
},
});
// Componente wrapper para o Storybook
const OrderTableDemo = ({
rows = mockOrders,
loading = false,
emptyState = false,
}: {
rows?: typeof mockOrders;
loading?: boolean;
emptyState?: boolean;
}) => {
const displayRows = emptyState ? [] : rows;
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<QueryClientProvider client={queryClient}>
<Box sx={{ width: '100%', p: 2 }}>
<Paper
sx={{
boxShadow: 'none',
border: 'none',
backgroundColor: 'transparent',
overflow: 'hidden',
}}
>
<DataGridPremium
rows={displayRows}
columns={demoColumns}
loading={loading}
density="compact"
autoHeight={false}
initialState={{
pagination: {
paginationModel: {
pageSize: 10,
page: 0,
},
},
sorting: {
sortModel: [{ field: 'createDate', sort: 'desc' }],
},
}}
pageSizeOptions={[10, 25, 50]}
paginationMode="client"
pagination
sx={{
height: 400,
border: '1px solid',
borderColor: 'divider',
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
backgroundColor: 'background.paper',
'& .MuiDataGrid-columnHeaders': {
backgroundColor: 'grey.50',
fontWeight: 600,
fontSize: '0.75rem',
borderBottom: '2px solid',
borderColor: 'divider',
},
'& .MuiDataGrid-row': {
borderBottom: '1px solid',
borderColor: 'divider',
cursor: 'pointer',
'&:hover': {
backgroundColor: 'action.hover',
},
},
'& .MuiDataGrid-cell': {
fontSize: '0.75rem',
},
'& .MuiDataGrid-footerContainer': {
borderTop: '2px solid',
borderColor: 'divider',
backgroundColor: 'grey.50',
},
}}
localeText={{
noRowsLabel: 'Nenhum pedido encontrado.',
noResultsOverlayLabel: 'Nenhum resultado encontrado.',
}}
/>
</Paper>
</Box>
</QueryClientProvider>
</ThemeProvider>
);
};
const meta: Meta<typeof OrderTableDemo> = {
title: 'Features/Orders/OrderTable',
component: OrderTableDemo,
parameters: {
layout: 'fullscreen',
docs: {
description: {
component: `
## OrderTable
Tabela de pedidos com as seguintes funcionalidades:
- **Paginação**: Suporta 10, 25 ou 50 itens por página
- **Ordenação**: Por qualquer coluna clicando no header
- **Seleção**: Clique em uma linha para ver detalhes
- **Responsividade**: Adapta altura para mobile/desktop
`,
},
},
},
tags: ['autodocs'],
};
export default meta;
type Story = StoryObj<typeof OrderTableDemo>;
/**
* Estado padrão da tabela com dados de pedidos.
*/
export const Default: Story = {
args: {
rows: [
{
id: '1',
orderId: 1232236,
createDate: '2026-01-15T10:00:00',
customerName: 'Maria Silva',
status: 'Faturado',
orderType: 'Venda',
amount: 1250.5,
invoiceNumber: 'NF-001234',
sellerName: 'João Vendedor',
deliveryType: 'Entrega',
totalWeight: 15.5,
fatUserName: 'Admin',
deliveryLocal: 'São Paulo - SP',
masterDeliveryLocal: 'Região Sul',
deliveryPriority: 'Alta',
paymentName: 'Boleto',
partnerName: 'Parceiro ABC',
codusur2Name: 'Rep. Regional',
releaseUserName: 'Supervisor',
driver: 'Carlos Motorista',
carDescription: 'Sprinter',
carrier: 'Transportadora XYZ',
schedulerDelivery: '15/01/2026',
fatUserDescription: 'Faturamento',
emitenteNome: 'Empresa LTDA',
},
{
id: '2',
orderId: 123457,
createDate: '2026-01-14T14:30:00',
customerName: 'João Santos',
status: 'Pendente',
orderType: 'Venda',
amount: 3450,
invoiceNumber: '',
sellerName: 'Ana Vendedora',
deliveryType: 'Retirada',
totalWeight: 8.2,
fatUserName: '',
deliveryLocal: 'Rio de Janeiro - RJ',
masterDeliveryLocal: 'Região Sudeste',
deliveryPriority: 'Normal',
paymentName: 'Cartão de Crédito',
partnerName: '',
codusur2Name: '',
releaseUserName: '',
driver: '',
carDescription: '',
carrier: '',
schedulerDelivery: '',
fatUserDescription: '',
emitenteNome: 'Empresa LTDA',
},
{
id: '3',
orderId: 123458,
createDate: '2026-01-13T09:15:00',
customerName: 'Pedro Oliveira',
status: 'Entregue',
orderType: 'Venda',
amount: 890.75,
invoiceNumber: 'NF-001235',
sellerName: 'João Vendedor',
deliveryType: 'Entrega',
totalWeight: 5,
fatUserName: 'Admin',
deliveryLocal: 'Belo Horizonte - MG',
masterDeliveryLocal: 'Região Sudeste',
deliveryPriority: 'Baixa',
paymentName: 'PIX',
partnerName: 'Parceiro DEF',
codusur2Name: 'Rep. Nacional',
releaseUserName: 'Gerente',
driver: 'Paulo Motorista',
carDescription: 'Fiorino',
carrier: 'Transportadora ABC',
schedulerDelivery: '13/01/2026',
fatUserDescription: 'Faturamento Automático',
emitenteNome: 'Outra Empresa LTDA',
},
],
loading: false,
emptyState: false,
},
};
/**
* Estado de carregamento enquanto busca pedidos da API.
*/
export const Loading: Story = {
args: {
rows: [],
loading: true,
emptyState: false,
},
};
/**
* Estado vazio quando não pedidos para exibir.
*/
export const Empty: Story = {
args: {
rows: [],
loading: false,
emptyState: true,
},
};
/**
* Tabela com muitos pedidos para demonstrar a paginação.
*/
export const ManyOrders: Story = {
args: {
rows: Array.from({ length: 50 }, (_, i) => ({
...mockOrders[i % 3],
id: String(i + 1),
orderId: 123456 + i,
customerName: `Cliente ${i + 1}`,
amount: Math.random() * 10000,
})),
loading: false,
emptyState: false,
},
};

View File

@ -135,7 +135,7 @@ export const OrderTable = () => {
border: 'none',
},
'& .MuiDataGrid-columnHeaders': {
backgroundColor: 'grey.50',
backgroundColor: (theme) => theme.palette.mode === 'dark' ? 'grey.900' : 'grey.50',
fontWeight: 600,
fontSize: '0.75rem',
borderBottom: '2px solid',
@ -180,7 +180,7 @@ export const OrderTable = () => {
},
},
'& .MuiDataGrid-row:nth-of-type(even)': {
backgroundColor: 'grey.50',
backgroundColor: (theme) => theme.palette.mode === 'dark' ? 'grey.800' : 'grey.50',
'&:hover': {
backgroundColor: 'action.hover',
},
@ -226,10 +226,10 @@ export const OrderTable = () => {
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
minHeight: '48px !important',
fontSize: '0.75rem',
backgroundColor: 'grey.50',
backgroundColor: (theme) => theme.palette.mode === 'dark' ? 'grey.900' : 'grey.50',
},
'& .MuiDataGrid-aggregationColumnHeader': {
backgroundColor: 'grey.100',
backgroundColor: (theme) => theme.palette.mode === 'dark' ? 'grey.800' : 'grey.100',
fontWeight: 600,
fontSize: '0.75rem',
borderBottom: '2px solid',
@ -239,7 +239,7 @@ export const OrderTable = () => {
fontWeight: 600,
},
'& .MuiDataGrid-aggregationRow': {
backgroundColor: 'grey.100',
backgroundColor: (theme) => theme.palette.mode === 'dark' ? 'grey.800' : 'grey.100',
borderTop: '2px solid',
borderColor: 'divider',
minHeight: '40px !important',
@ -267,13 +267,13 @@ export const OrderTable = () => {
opacity: 1,
},
'& .MuiDataGrid-pinnedColumnHeaders': {
backgroundColor: 'grey.50',
backgroundColor: (theme) => theme.palette.mode === 'dark' ? 'grey.900' : 'grey.50',
},
'& .MuiDataGrid-pinnedColumns': {
backgroundColor: 'background.paper',
},
'& .MuiDataGrid-pinnedColumnHeader': {
backgroundColor: 'grey.50',
backgroundColor: (theme) => theme.palette.mode === 'dark' ? 'grey.900' : 'grey.50',
fontWeight: 600,
fontSize: '0.75rem',
borderBottom: '2px solid',

View File

@ -14,14 +14,122 @@ import {
getPriorityChipProps,
} from '../utils/tableHelpers';
const CELL_FONT_SIZE = '0.75rem';
const CAPTION_FONT_SIZE = '0.6875rem';
const CHIP_STYLES = {
fontSize: CAPTION_FONT_SIZE,
height: 22,
fontWeight: 300,
} as const;
const CHIP_PRIORITY_STYLES = {
...CHIP_STYLES,
maxWidth: '100%',
'& .MuiChip-label': {
px: 1,
py: 0,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
},
} as const;
interface CellTextProps {
value: unknown;
secondary?: boolean;
fontWeight?: number;
}
const CellText = ({ value, secondary = false, fontWeight }: CellTextProps) => (
<Typography
variant="body2"
color={secondary ? 'text.secondary' : 'text.primary'}
sx={{ fontSize: CELL_FONT_SIZE, fontWeight }}
noWrap
>
{String(value ?? '-')}
</Typography>
);
interface CellNumericProps {
value: unknown;
formatter?: (val: number) => string;
secondary?: boolean;
fontWeight?: number;
}
const CellNumeric = ({
value,
formatter,
secondary = false,
fontWeight,
}: CellNumericProps) => (
<Typography
variant="body2"
color={secondary ? 'text.secondary' : 'text.primary'}
sx={{ fontSize: CELL_FONT_SIZE, fontWeight, textAlign: 'right' }}
>
{formatter ? formatter(value as number) : String(value ?? '-')}
</Typography>
);
interface CellDateProps {
value: unknown;
showTime?: boolean;
time?: string;
}
const CellDate = ({ value, showTime = false, time }: CellDateProps) => {
const dateStr = formatDate(value as string | undefined);
if (!showTime && !time) {
return (
<Typography variant="body2" sx={{ fontSize: CELL_FONT_SIZE }}>
{dateStr}
</Typography>
);
}
const timeStr = time || formatDateTime(value as string | undefined);
return (
<Box>
<Typography variant="body2" sx={{ fontSize: CELL_FONT_SIZE }}>
{dateStr}
</Typography>
{timeStr && (
<Typography
variant="caption"
color="text.secondary"
sx={{ fontSize: CAPTION_FONT_SIZE }}
>
{timeStr}
</Typography>
)}
</Box>
);
};
interface CreateOrderColumnsOptions {
storesMap?: Map<string, string>;
}
export const createOrderColumns = (options?: CreateOrderColumnsOptions): GridColDef[] => {
export const createOrderColumns = (
options?: CreateOrderColumnsOptions
): GridColDef[] => {
const storesMap = options?.storesMap;
return [
{
field: 'orderId',
headerName: 'Pedido',
@ -30,12 +138,7 @@ export const createOrderColumns = (options?: CreateOrderColumnsOptions): GridCol
headerAlign: 'right',
align: 'right',
renderCell: (params: Readonly<GridRenderCellParams>) => (
<Typography
variant="body2"
sx={{ fontSize: '0.75rem', fontWeight: 500, textAlign: 'right' }}
>
{params.value}
</Typography>
<CellNumeric value={params.value} fontWeight={500} />
),
},
{
@ -43,26 +146,12 @@ export const createOrderColumns = (options?: CreateOrderColumnsOptions): GridCol
headerName: 'Data',
width: 160,
minWidth: 140,
renderCell: (params: Readonly<GridRenderCellParams>) => {
const dateTime = formatDateTime(params.value);
return (
<Box>
<Typography variant="body2" sx={{ fontSize: '0.75rem' }}>
{formatDate(params.value)}
</Typography>
{dateTime && (
<Typography
variant="caption"
color="text.secondary"
sx={{ fontSize: '0.6875rem' }}
>
{dateTime}
</Typography>
)}
</Box>
);
},
renderCell: (params: Readonly<GridRenderCellParams>) => (
<CellDate value={params.value} showTime />
),
},
{
field: 'customerName',
headerName: 'Cliente',
@ -70,11 +159,22 @@ export const createOrderColumns = (options?: CreateOrderColumnsOptions): GridCol
minWidth: 250,
flex: 1,
renderCell: (params: Readonly<GridRenderCellParams>) => (
<Typography variant="body2" sx={{ fontSize: '0.75rem' }} noWrap>
{params.value}
</Typography>
<CellText value={params.value} />
),
},
{
field: 'customerId',
headerName: 'Código Cliente',
width: 120,
minWidth: 110,
headerAlign: 'right',
align: 'right',
renderCell: (params: Readonly<GridRenderCellParams>) => (
<CellNumeric value={params.value} secondary />
),
},
{
field: 'storeId',
headerName: 'Filial',
@ -83,11 +183,7 @@ export const createOrderColumns = (options?: CreateOrderColumnsOptions): GridCol
renderCell: (params: Readonly<GridRenderCellParams>) => {
const storeId = String(params.value);
const storeName = storesMap?.get(storeId) || storeId;
return (
<Typography variant="body2" sx={{ fontSize: '0.75rem' }} noWrap>
{storeName}
</Typography>
);
return <CellText value={storeName} />;
},
},
{
@ -96,11 +192,11 @@ export const createOrderColumns = (options?: CreateOrderColumnsOptions): GridCol
width: 200,
minWidth: 180,
renderCell: (params: Readonly<GridRenderCellParams>) => (
<Typography variant="body2" sx={{ fontSize: '0.75rem' }} noWrap>
{params.value}
</Typography>
<CellText value={params.value} />
),
},
{
field: 'status',
headerName: 'Situação',
@ -116,11 +212,7 @@ export const createOrderColumns = (options?: CreateOrderColumnsOptions): GridCol
label={chipProps.label}
color={chipProps.color}
size="small"
sx={{
fontSize: '0.6875rem',
height: 22,
fontWeight: 300,
}}
sx={CHIP_STYLES}
/>
</Tooltip>
);
@ -132,11 +224,11 @@ export const createOrderColumns = (options?: CreateOrderColumnsOptions): GridCol
width: 140,
minWidth: 120,
renderCell: (params: Readonly<GridRenderCellParams>) => (
<Typography variant="body2" sx={{ fontSize: '0.75rem' }} noWrap>
{params.value}
</Typography>
<CellText value={params.value} />
),
},
{
field: 'amount',
headerName: 'Valor Total',
@ -148,61 +240,7 @@ export const createOrderColumns = (options?: CreateOrderColumnsOptions): GridCol
align: 'right',
valueFormatter: (value) => formatCurrency(value as number),
renderCell: (params: Readonly<GridRenderCellParams>) => (
<Typography
variant="body2"
sx={{ fontSize: '0.75rem', fontWeight: 500, textAlign: 'right' }}
>
{formatCurrency(params.value)}
</Typography>
),
},
{
field: 'invoiceNumber',
headerName: 'Nota Fiscal',
width: 120,
minWidth: 110,
headerAlign: 'right',
align: 'right',
renderCell: (params: Readonly<GridRenderCellParams>) => (
<Typography
variant="body2"
sx={{ fontSize: '0.75rem', textAlign: 'right' }}
>
{params.value && params.value !== '-' ? params.value : '-'}
</Typography>
),
},
{
field: 'billingId',
headerName: 'Cobrança',
width: 120,
minWidth: 110,
renderCell: (params: Readonly<GridRenderCellParams>) => (
<Typography variant="body2" sx={{ fontSize: '0.75rem' }} noWrap>
{params.value || '-'}
</Typography>
),
},
{
field: 'sellerName',
headerName: 'Vendedor',
width: 200,
minWidth: 180,
renderCell: (params: Readonly<GridRenderCellParams>) => (
<Typography variant="body2" sx={{ fontSize: '0.75rem' }} noWrap>
{params.value}
</Typography>
),
},
{
field: 'deliveryType',
headerName: 'Tipo de Entrega',
width: 160,
minWidth: 140,
renderCell: (params: Readonly<GridRenderCellParams>) => (
<Typography variant="body2" sx={{ fontSize: '0.75rem' }} noWrap>
{params.value}
</Typography>
<CellNumeric value={params.value} formatter={formatCurrency} fontWeight={500} />
),
},
{
@ -216,12 +254,30 @@ export const createOrderColumns = (options?: CreateOrderColumnsOptions): GridCol
align: 'right',
valueFormatter: (value) => formatNumber(value as number),
renderCell: (params: Readonly<GridRenderCellParams>) => (
<Typography
variant="body2"
sx={{ fontSize: '0.75rem', textAlign: 'right' }}
>
{formatNumber(params.value)}
</Typography>
<CellNumeric value={params.value} formatter={formatNumber} />
),
},
{
field: 'invoiceNumber',
headerName: 'Nota Fiscal',
width: 120,
minWidth: 110,
headerAlign: 'right',
align: 'right',
renderCell: (params: Readonly<GridRenderCellParams>) => {
const value = params.value && params.value !== '-' ? params.value : '-';
return <CellNumeric value={value} />;
},
},
{
field: 'invoiceDate',
headerName: 'Data Faturamento',
width: 150,
minWidth: 140,
renderCell: (params: Readonly<GridRenderCellParams>) => (
<CellDate value={params.value} time={params.row.invoiceTime} />
),
},
{
@ -230,26 +286,69 @@ export const createOrderColumns = (options?: CreateOrderColumnsOptions): GridCol
width: 160,
minWidth: 140,
renderCell: (params: Readonly<GridRenderCellParams>) => (
<Typography variant="body2" sx={{ fontSize: '0.75rem' }} noWrap>
{params.value}
</Typography>
<CellText value={params.value} />
),
},
{
field: 'billingId',
headerName: 'Cobrança',
width: 120,
minWidth: 110,
renderCell: (params: Readonly<GridRenderCellParams>) => (
<CellText value={params.value} />
),
},
{
field: 'customerId',
headerName: 'Código Cliente',
width: 120,
minWidth: 110,
field: 'paymentName',
headerName: 'Pagamento',
width: 140,
minWidth: 130,
renderCell: (params: Readonly<GridRenderCellParams>) => (
<CellText value={params.value} />
),
},
{
field: 'sellerName',
headerName: 'Vendedor',
width: 200,
minWidth: 180,
renderCell: (params: Readonly<GridRenderCellParams>) => (
<CellText value={params.value} />
),
},
{
field: 'sellerId',
headerName: 'RCA',
width: 100,
minWidth: 90,
headerAlign: 'right',
align: 'right',
renderCell: (params: Readonly<GridRenderCellParams>) => (
<Typography
variant="body2"
color="text.secondary"
sx={{ fontSize: '0.75rem', textAlign: 'right' }}
>
{params.value || '-'}
</Typography>
<CellNumeric value={params.value} secondary />
),
},
{
field: 'codusur2Name',
headerName: 'RCA 2',
width: 180,
minWidth: 160,
renderCell: (params: Readonly<GridRenderCellParams>) => (
<CellText value={params.value} />
),
},
{
field: 'deliveryType',
headerName: 'Tipo de Entrega',
width: 160,
minWidth: 140,
renderCell: (params: Readonly<GridRenderCellParams>) => (
<CellText value={params.value} />
),
},
{
@ -258,9 +357,7 @@ export const createOrderColumns = (options?: CreateOrderColumnsOptions): GridCol
width: 130,
minWidth: 120,
renderCell: (params: Readonly<GridRenderCellParams>) => (
<Typography variant="body2" sx={{ fontSize: '0.75rem' }}>
{params.value ? formatDate(params.value) : '-'}
</Typography>
<CellDate value={params.value} />
),
},
{
@ -269,9 +366,7 @@ export const createOrderColumns = (options?: CreateOrderColumnsOptions): GridCol
width: 180,
minWidth: 160,
renderCell: (params: Readonly<GridRenderCellParams>) => (
<Typography variant="body2" sx={{ fontSize: '0.75rem' }} noWrap>
{params.value}
</Typography>
<CellText value={params.value} />
),
},
{
@ -284,15 +379,7 @@ export const createOrderColumns = (options?: CreateOrderColumnsOptions): GridCol
const chipProps = getPriorityChipProps(priority);
if (!chipProps) {
return (
<Typography
variant="body2"
sx={{ fontSize: '0.75rem', color: 'text.secondary' }}
noWrap
>
-
</Typography>
);
return <CellText value="-" secondary />;
}
return (
@ -301,121 +388,19 @@ export const createOrderColumns = (options?: CreateOrderColumnsOptions): GridCol
label={chipProps.label}
color={chipProps.color}
size="small"
sx={{
fontSize: '0.6875rem',
height: 22,
fontWeight: 300,
maxWidth: '100%',
'& .MuiChip-label': {
px: 1,
py: 0,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
},
}}
sx={CHIP_PRIORITY_STYLES}
/>
</Tooltip>
);
},
},
{
field: 'invoiceDate',
headerName: 'Data Faturamento',
width: 150,
minWidth: 140,
renderCell: (params: Readonly<GridRenderCellParams>) => {
const dateStr = formatDate(params.value);
const timeStr = params.row.invoiceTime;
return (
<Typography
variant="body2"
sx={{ fontSize: '0.75rem', whiteSpace: 'nowrap' }}
>
{dateStr}
{timeStr && (
<Box
component="span"
sx={{ color: 'text.secondary', fontSize: '0.6875rem', ml: 0.5 }}
>
{timeStr}
</Box>
)}
</Typography>
);
},
},
{
field: 'invoiceTime',
headerName: 'Hora Faturamento',
width: 120,
minWidth: 110,
renderCell: (params: Readonly<GridRenderCellParams>) => (
<Typography variant="body2" sx={{ fontSize: '0.75rem' }}>
{params.value || '-'}
</Typography>
),
},
{
field: 'confirmDeliveryDate',
headerName: 'Data Confirmação Entrega',
width: 150,
minWidth: 140,
renderCell: (params: Readonly<GridRenderCellParams>) => (
<Typography variant="body2" sx={{ fontSize: '0.75rem' }}>
{params.value ? formatDate(params.value) : '-'}
</Typography>
),
},
{
field: 'paymentName',
headerName: 'Pagamento',
width: 140,
minWidth: 130,
renderCell: (params: Readonly<GridRenderCellParams>) => (
<Typography variant="body2" sx={{ fontSize: '0.75rem' }} noWrap>
{params.value}
</Typography>
),
},
{
field: 'sellerId',
headerName: 'RCA',
width: 100,
minWidth: 90,
headerAlign: 'right',
align: 'right',
renderCell: (params: Readonly<GridRenderCellParams>) => (
<Typography
variant="body2"
color="text.secondary"
sx={{ fontSize: '0.75rem', textAlign: 'right' }}
>
{params.value || '-'}
</Typography>
),
},
{
field: 'partnerName',
headerName: 'Parceiro',
width: 180,
minWidth: 160,
renderCell: (params: Readonly<GridRenderCellParams>) => (
<Typography variant="body2" sx={{ fontSize: '0.75rem' }} noWrap>
{params.value}
</Typography>
),
},
{
field: 'codusur2Name',
headerName: 'RCA 2',
width: 180,
minWidth: 160,
renderCell: (params: Readonly<GridRenderCellParams>) => (
<Typography variant="body2" sx={{ fontSize: '0.75rem' }} noWrap>
{params.value}
</Typography>
<CellDate value={params.value} />
),
},
{
@ -424,9 +409,18 @@ export const createOrderColumns = (options?: CreateOrderColumnsOptions): GridCol
width: 160,
minWidth: 140,
renderCell: (params: Readonly<GridRenderCellParams>) => (
<Typography variant="body2" sx={{ fontSize: '0.75rem' }} noWrap>
{params.value}
</Typography>
<CellText value={params.value} />
),
},
{
field: 'partnerName',
headerName: 'Parceiro',
width: 180,
minWidth: 160,
renderCell: (params: Readonly<GridRenderCellParams>) => (
<CellText value={params.value} />
),
},
{
@ -435,10 +429,8 @@ export const createOrderColumns = (options?: CreateOrderColumnsOptions): GridCol
width: 180,
minWidth: 160,
renderCell: (params: Readonly<GridRenderCellParams>) => (
<Typography variant="body2" sx={{ fontSize: '0.75rem' }} noWrap>
{params.value}
</Typography>
<CellText value={params.value} />
),
},
];
];
};

View File

@ -2,6 +2,7 @@
import { useState, useCallback, useEffect } from 'react';
import { useOrderFilters } from '../hooks/useOrderFilters';
import { useOrders } from '../hooks/useOrders';
import {
Box,
TextField,
@ -73,7 +74,7 @@ export const SearchBar = () => {
const [customerSearchTerm, setCustomerSearchTerm] = useState('');
const customers = useCustomers(customerSearchTerm);
const [showAdvancedFilters, setShowAdvancedFilters] = useState(false);
const [isSearching, setIsSearching] = useState(false);
const { isFetching } = useOrders();
const [touchedFields, setTouchedFields] = useState<{
createDateIni?: boolean;
createDateEnd?: boolean;
@ -141,13 +142,10 @@ export const SearchBar = () => {
return;
}
setIsSearching(true);
setUrlFilters({
...localFilters,
searchTriggered: true,
});
// Reset loading state after a short delay
setTimeout(() => setIsSearching(false), 500);
}, [localFilters, setUrlFilters, validateDates]);
const handleKeyDown = useCallback(
@ -230,7 +228,7 @@ export const SearchBar = () => {
</Grid>
{/* Autocomplete do MUI para Cliente */}
<Grid size={{ xs: 12, sm: 6, md: 2.5 }}>
<Grid size={{ xs: 12, sm: 6, md: 2 }}>
<Autocomplete
size="small"
options={customers.options}
@ -294,7 +292,7 @@ export const SearchBar = () => {
</Grid>
{/* Campos de Data */}
<Grid size={{ xs: 12, sm: 12, md: 4 }}>
<Grid size={{ xs: 12, sm: 12, md: 3.5 }}>
<LocalizationProvider
dateAdapter={AdapterMoment}
adapterLocale="pt-br"
@ -391,33 +389,58 @@ export const SearchBar = () => {
</LocalizationProvider>
</Grid>
{/* Botões de Ação */}
{/* Botão Mais Filtros - inline com filtros primários */}
<Grid
size={{ xs: 12, sm: 12, md: 1.5 }}
sx={{ display: 'flex', justifyContent: { xs: 'stretch', sm: 'flex-end' } }}
size={{ xs: 12, sm: 12, md: 2.5 }}
sx={{ display: 'flex', alignItems: 'flex-end', justifyContent: { xs: 'flex-start', md: 'flex-end' } }}
>
<Box sx={{ display: 'flex', gap: 1, width: { xs: '100%', sm: 'auto' } }}>
<Badge
badgeContent={advancedFiltersCount}
color="primary"
invisible={advancedFiltersCount === 0}
>
<Button
size="small"
onClick={() => setShowAdvancedFilters(!showAdvancedFilters)}
endIcon={
showAdvancedFilters ? <ExpandLessIcon /> : <ExpandMoreIcon />
}
aria-label={showAdvancedFilters ? 'Ocultar filtros avançados' : 'Mostrar filtros avançados'}
sx={{ textTransform: 'none', color: 'text.secondary', minHeight: 40 }}
>
{showAdvancedFilters ? 'Menos filtros' : 'Mais filtros'}
</Button>
</Badge>
</Grid>
{/* Botões de Ação - nova linha abaixo */}
<Grid
size={{ xs: 12 }}
sx={{ display: 'flex', justifyContent: 'flex-end', mt: 1 }}
>
<Box sx={{ display: 'flex', gap: 1, width: { xs: '100%', sm: 'auto' }, flexWrap: 'nowrap' }}>
<Tooltip title="Limpar filtros" arrow>
<span>
<Button
variant="outlined"
color="inherit"
size="small"
onClick={handleReset}
aria-label="Limpar filtros"
sx={{
minWidth: { xs: 48, md: 40 },
minHeight: 44,
minWidth: { xs: 'auto', sm: 90 },
minHeight: 32,
px: 1.5,
flexShrink: 0,
flex: { xs: 1, sm: 'none' },
fontSize: '0.8125rem',
'&:hover': {
bgcolor: 'action.hover',
},
}}
>
<ResetIcon />
<Box component="span" sx={{ display: { xs: 'none', md: 'inline' }, ml: 0.5 }}>
<ResetIcon sx={{ mr: 0.5, fontSize: 18 }} />
Limpar
</Box>
</Button>
</span>
</Tooltip>
@ -433,27 +456,28 @@ export const SearchBar = () => {
<Button
variant="contained"
color="primary"
size="small"
onClick={handleFilter}
disabled={!isDateValid || !!dateError || isSearching}
disabled={!isDateValid || !!dateError || isFetching}
aria-label="Buscar pedidos"
sx={{
minWidth: { xs: 48, md: 40 },
minHeight: 44,
minWidth: { xs: 'auto', sm: 90 },
minHeight: 32,
px: 1.5,
flex: { xs: 2, sm: 'none' },
flexShrink: 0,
flex: { xs: 1, sm: 'none' },
fontSize: '0.8125rem',
'&:disabled': {
opacity: 0.6,
},
}}
>
{isSearching ? (
<CircularProgress size={20} color="inherit" />
{isFetching ? (
<CircularProgress size={16} color="inherit" />
) : (
<>
<SearchIcon />
<Box component="span" sx={{ display: { xs: 'none', md: 'inline' }, ml: 0.5 }}>
<SearchIcon sx={{ mr: 0.5, fontSize: 18 }} />
Buscar
</Box>
</>
)}
</Button>
@ -464,25 +488,6 @@ export const SearchBar = () => {
{/* --- Advanced Filters (Collapsible) --- */}
<Grid size={{ xs: 12 }}>
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mt: 1 }}>
<Badge
badgeContent={advancedFiltersCount}
color="primary"
invisible={advancedFiltersCount === 0}
>
<Button
size="small"
onClick={() => setShowAdvancedFilters(!showAdvancedFilters)}
endIcon={
showAdvancedFilters ? <ExpandLessIcon /> : <ExpandMoreIcon />
}
aria-label={showAdvancedFilters ? 'Ocultar filtros avançados' : 'Mostrar filtros avançados'}
sx={{ textTransform: 'none', color: 'text.secondary' }}
>
{showAdvancedFilters ? 'Menos filtros' : 'Mais filtros'}
</Button>
</Badge>
</Box>
<Collapse in={showAdvancedFilters}>
<Grid container spacing={2} sx={{ pt: 2 }}>
{/* Autocomplete do MUI para Múltiplas Filiais (codfilial) */}

View File

@ -1,5 +1,6 @@
import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid-premium';
import Chip from '@mui/material/Chip';
import Box from '@mui/material/Box';
import {
formatCurrency,
formatDate,
@ -7,6 +8,19 @@ import {
getStatusLabel,
} from '../../utils/orderFormatters';
const TextCell = ({ value }: { value: string | null | undefined }) => (
<Box
sx={{
whiteSpace: 'normal',
wordBreak: 'break-word',
lineHeight: 1.3,
py: 0.5,
}}
>
{value ?? ''}
</Box>
);
export const createInformationPanelColumns = (): GridColDef[] => [
{
field: 'customerName',
@ -14,6 +28,7 @@ export const createInformationPanelColumns = (): GridColDef[] => [
minWidth: 180,
flex: 1.5,
description: 'Nome do cliente do pedido',
renderCell: (params: GridRenderCellParams) => <TextCell value={params.value} />,
},
{
field: 'storeId',
@ -55,6 +70,7 @@ export const createInformationPanelColumns = (): GridColDef[] => [
minWidth: 100,
flex: 1,
description: 'Forma de pagamento utilizada',
renderCell: (params: GridRenderCellParams) => <TextCell value={params.value} />,
},
{
field: 'billingName',
@ -62,6 +78,7 @@ export const createInformationPanelColumns = (): GridColDef[] => [
minWidth: 100,
flex: 1,
description: 'Condição de pagamento',
renderCell: (params: GridRenderCellParams) => <TextCell value={params.value} />,
},
{
field: 'amount',
@ -78,6 +95,7 @@ export const createInformationPanelColumns = (): GridColDef[] => [
minWidth: 100,
flex: 1,
description: 'Tipo de entrega selecionado',
renderCell: (params: GridRenderCellParams) => <TextCell value={params.value} />,
},
{
field: 'deliveryLocal',
@ -85,5 +103,7 @@ export const createInformationPanelColumns = (): GridColDef[] => [
minWidth: 120,
flex: 1.2,
description: 'Local de entrega do pedido',
renderCell: (params: GridRenderCellParams) => <TextCell value={params.value} />,
},
];

View File

@ -4,7 +4,7 @@ import { orderService } from '../api/order.service';
export function useSellers() {
const query = useQuery({
queryKey: ['sellers'],
queryFn: () => orderService.findsellers(),
queryFn: () => orderService.findSellers(),
staleTime: 1000 * 60 * 30, // 30 minutes
retry: 1,
retryOnMount: false,

View File

@ -130,27 +130,17 @@ export default function ProfilePage() {
{displayData.sectorId !== undefined && (
<Grid size={{ xs: 12, sm: 4 }}>
<Typography variant="body2" color="textSecondary">
Setor ID
Setor
</Typography>
<Typography variant="body1" fontWeight="500">
{displayData.sectorId}
</Typography>
</Grid>
)}
{displayData.sectorManagerId !== undefined && (
<Grid size={{ xs: 12, sm: 4 }}>
<Typography variant="body2" color="textSecondary">
Gerente de Setor ID
</Typography>
<Typography variant="body1" fontWeight="500">
{displayData.sectorManagerId}
</Typography>
</Grid>
)}
{displayData.supervisorId !== undefined && (
<Grid size={{ xs: 12, sm: 4 }}>
<Typography variant="body2" color="textSecondary">
Supervisor ID
Supervisor
</Typography>
<Typography variant="body1" fontWeight="500">
{displayData.supervisorId}