feat(orders): implement manual search and information panel
- Refactor SearchBar to use local state (search only triggers on button click) - Add InformationPanel with DataGrid displaying order details - Create shared dataGridStyles for consistent table styling - Simplify OrderItemsTable using shared styles - Add useOrderDetails hook for fetching order by ID - Translate status codes to readable text (F->Faturado, C->Cancelado, P->Pendente)
This commit is contained in:
parent
cf7e56e2b7
commit
544d723f1b
|
|
@ -1,6 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback, useEffect } from 'react';
|
||||||
import { useOrderFilters } from '../hooks/useOrderFilters';
|
import { useOrderFilters } from '../hooks/useOrderFilters';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
|
|
@ -30,8 +30,40 @@ import 'moment/locale/pt-br';
|
||||||
|
|
||||||
moment.locale('pt-br');
|
moment.locale('pt-br');
|
||||||
|
|
||||||
|
// Tipo para os filtros locais (não precisam da flag searchTriggered)
|
||||||
|
interface LocalFilters {
|
||||||
|
status: string | null;
|
||||||
|
orderId: number | null;
|
||||||
|
customerId: number | null;
|
||||||
|
customerName: string | null;
|
||||||
|
createDateIni: string | null;
|
||||||
|
createDateEnd: string | null;
|
||||||
|
store: string[] | null;
|
||||||
|
stockId: string[] | null;
|
||||||
|
sellerId: string | null;
|
||||||
|
sellerName: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getInitialLocalFilters = (urlFilters: any): LocalFilters => ({
|
||||||
|
status: urlFilters.status ?? null,
|
||||||
|
orderId: urlFilters.orderId ?? null,
|
||||||
|
customerId: urlFilters.customerId ?? null,
|
||||||
|
customerName: urlFilters.customerName ?? null,
|
||||||
|
createDateIni: urlFilters.createDateIni ?? null,
|
||||||
|
createDateEnd: urlFilters.createDateEnd ?? null,
|
||||||
|
store: urlFilters.store ?? null,
|
||||||
|
stockId: urlFilters.stockId ?? null,
|
||||||
|
sellerId: urlFilters.sellerId ?? null,
|
||||||
|
sellerName: urlFilters.sellerName ?? null,
|
||||||
|
});
|
||||||
|
|
||||||
export const SearchBar = () => {
|
export const SearchBar = () => {
|
||||||
const [filters, setFilters] = useOrderFilters();
|
const [urlFilters, setUrlFilters] = useOrderFilters();
|
||||||
|
|
||||||
|
// Estado local para inputs (não dispara busca ao mudar)
|
||||||
|
const [localFilters, setLocalFilters] = useState<LocalFilters>(() =>
|
||||||
|
getInitialLocalFilters(urlFilters)
|
||||||
|
);
|
||||||
|
|
||||||
const stores = useStores();
|
const stores = useStores();
|
||||||
const sellers = useSellers();
|
const sellers = useSellers();
|
||||||
|
|
@ -43,6 +75,18 @@ export const SearchBar = () => {
|
||||||
createDateEnd?: boolean;
|
createDateEnd?: boolean;
|
||||||
}>({});
|
}>({});
|
||||||
|
|
||||||
|
// Sync local state com URL (para navegação Back/Forward)
|
||||||
|
useEffect(() => {
|
||||||
|
setLocalFilters(getInitialLocalFilters(urlFilters));
|
||||||
|
}, [urlFilters]);
|
||||||
|
|
||||||
|
const updateLocalFilter = useCallback(<K extends keyof LocalFilters>(
|
||||||
|
key: K,
|
||||||
|
value: LocalFilters[K]
|
||||||
|
) => {
|
||||||
|
setLocalFilters(prev => ({ ...prev, [key]: value }));
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleReset = useCallback(() => {
|
const handleReset = useCallback(() => {
|
||||||
setTouchedFields({});
|
setTouchedFields({});
|
||||||
setCustomerSearchTerm('');
|
setCustomerSearchTerm('');
|
||||||
|
|
@ -66,23 +110,25 @@ export const SearchBar = () => {
|
||||||
customerName: null,
|
customerName: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
setFilters(resetState);
|
// Reset local + URL
|
||||||
}, [setFilters]);
|
setLocalFilters(getInitialLocalFilters(resetState));
|
||||||
|
setUrlFilters(resetState);
|
||||||
|
}, [setUrlFilters]);
|
||||||
|
|
||||||
const validateDates = useCallback(() => {
|
const validateDates = useCallback(() => {
|
||||||
if (!filters.createDateIni || !filters.createDateEnd) {
|
if (!localFilters.createDateIni || !localFilters.createDateEnd) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const dateIni = moment(filters.createDateIni, 'YYYY-MM-DD');
|
const dateIni = moment(localFilters.createDateIni, 'YYYY-MM-DD');
|
||||||
const dateEnd = moment(filters.createDateEnd, 'YYYY-MM-DD');
|
const dateEnd = moment(localFilters.createDateEnd, 'YYYY-MM-DD');
|
||||||
if (dateEnd.isBefore(dateIni)) {
|
if (dateEnd.isBefore(dateIni)) {
|
||||||
return 'Data final não pode ser anterior à data inicial';
|
return 'Data final não pode ser anterior à data inicial';
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}, [filters.createDateIni, filters.createDateEnd]);
|
}, [localFilters.createDateIni, localFilters.createDateEnd]);
|
||||||
|
|
||||||
const handleFilter = useCallback(() => {
|
const handleFilter = useCallback(() => {
|
||||||
if (!filters.createDateIni) {
|
if (!localFilters.createDateIni) {
|
||||||
setTouchedFields(prev => ({ ...prev, createDateIni: true }));
|
setTouchedFields(prev => ({ ...prev, createDateIni: true }));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -93,25 +139,26 @@ export const SearchBar = () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setFilters({
|
// Commit local filters to URL (triggers search)
|
||||||
...filters,
|
setUrlFilters({
|
||||||
|
...localFilters,
|
||||||
searchTriggered: true,
|
searchTriggered: true,
|
||||||
});
|
});
|
||||||
}, [filters, setFilters, validateDates]);
|
}, [localFilters, setUrlFilters, validateDates]);
|
||||||
|
|
||||||
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
const isValid = !!filters.createDateIni;
|
const isValid = !!localFilters.createDateIni;
|
||||||
const dateErr = validateDates();
|
const dateErr = validateDates();
|
||||||
if (isValid && !dateErr) {
|
if (isValid && !dateErr) {
|
||||||
handleFilter();
|
handleFilter();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [filters.createDateIni, validateDates, handleFilter]);
|
}, [localFilters.createDateIni, validateDates, handleFilter]);
|
||||||
|
|
||||||
const isDateValid = !!filters.createDateIni;
|
const isDateValid = !!localFilters.createDateIni;
|
||||||
const dateError = validateDates();
|
const dateError = validateDates();
|
||||||
const showDateIniError = touchedFields.createDateIni && !filters.createDateIni;
|
const showDateIniError = touchedFields.createDateIni && !localFilters.createDateIni;
|
||||||
const showDateEndError = touchedFields.createDateEnd && dateError;
|
const showDateEndError = touchedFields.createDateEnd && dateError;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -138,10 +185,10 @@ export const SearchBar = () => {
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
size="small"
|
size="small"
|
||||||
type="number"
|
type="number"
|
||||||
value={filters.orderId ?? ''}
|
value={localFilters.orderId ?? ''}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const value = e.target.value ? Number(e.target.value) : null;
|
const value = e.target.value ? Number(e.target.value) : null;
|
||||||
setFilters({ orderId: value });
|
updateLocalFilter('orderId', value);
|
||||||
}}
|
}}
|
||||||
slotProps={{ htmlInput: { min: 0 } }}
|
slotProps={{ htmlInput: { min: 0 } }}
|
||||||
placeholder="Ex: 12345"
|
placeholder="Ex: 12345"
|
||||||
|
|
@ -155,10 +202,10 @@ export const SearchBar = () => {
|
||||||
fullWidth
|
fullWidth
|
||||||
label="Situação"
|
label="Situação"
|
||||||
size="small"
|
size="small"
|
||||||
value={filters.status ?? ''}
|
value={localFilters.status ?? ''}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const value = e.target.value || null;
|
const value = e.target.value || null;
|
||||||
setFilters({ status: value });
|
updateLocalFilter('status', value);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MenuItem value="">Todos</MenuItem>
|
<MenuItem value="">Todos</MenuItem>
|
||||||
|
|
@ -176,29 +223,23 @@ export const SearchBar = () => {
|
||||||
getOptionLabel={(option) => option.label}
|
getOptionLabel={(option) => option.label}
|
||||||
isOptionEqualToValue={(option, value) => option.id === value.id}
|
isOptionEqualToValue={(option, value) => option.id === value.id}
|
||||||
value={customers.options.find(option =>
|
value={customers.options.find(option =>
|
||||||
filters.customerId === option.customer?.id
|
localFilters.customerId === option.customer?.id
|
||||||
) || null}
|
) || null}
|
||||||
onChange={(_, newValue) => {
|
onChange={(_, newValue) => {
|
||||||
if (!newValue) {
|
if (!newValue) {
|
||||||
setFilters({
|
updateLocalFilter('customerName', null);
|
||||||
customerName: null,
|
updateLocalFilter('customerId', null);
|
||||||
customerId: null,
|
|
||||||
});
|
|
||||||
setCustomerSearchTerm('');
|
setCustomerSearchTerm('');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setFilters({
|
updateLocalFilter('customerId', newValue.customer?.id || null);
|
||||||
customerId: newValue.customer?.id || null,
|
updateLocalFilter('customerName', newValue.customer?.name || null);
|
||||||
customerName: newValue.customer?.name || null,
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
onInputChange={(_, newInputValue, reason) => {
|
onInputChange={(_, newInputValue, reason) => {
|
||||||
if (reason === 'clear') {
|
if (reason === 'clear') {
|
||||||
setFilters({
|
updateLocalFilter('customerName', null);
|
||||||
customerName: null,
|
updateLocalFilter('customerId', null);
|
||||||
customerId: null,
|
|
||||||
});
|
|
||||||
setCustomerSearchTerm('');
|
setCustomerSearchTerm('');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -206,10 +247,8 @@ export const SearchBar = () => {
|
||||||
if (reason === 'input') {
|
if (reason === 'input') {
|
||||||
setCustomerSearchTerm(newInputValue);
|
setCustomerSearchTerm(newInputValue);
|
||||||
if (!newInputValue) {
|
if (!newInputValue) {
|
||||||
setFilters({
|
updateLocalFilter('customerName', null);
|
||||||
customerName: null,
|
updateLocalFilter('customerId', null);
|
||||||
customerId: null,
|
|
||||||
});
|
|
||||||
setCustomerSearchTerm('');
|
setCustomerSearchTerm('');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -238,15 +277,13 @@ export const SearchBar = () => {
|
||||||
<Box flex={1}>
|
<Box flex={1}>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
label="Data Inicial"
|
label="Data Inicial"
|
||||||
value={filters.createDateIni ? moment(filters.createDateIni, 'YYYY-MM-DD') : null}
|
value={localFilters.createDateIni ? moment(localFilters.createDateIni, 'YYYY-MM-DD') : null}
|
||||||
onChange={(date: moment.Moment | null) => {
|
onChange={(date: moment.Moment | null) => {
|
||||||
setTouchedFields(prev => ({ ...prev, createDateIni: true }));
|
setTouchedFields(prev => ({ ...prev, createDateIni: true }));
|
||||||
setFilters({
|
updateLocalFilter('createDateIni', date ? date.format('YYYY-MM-DD') : null);
|
||||||
createDateIni: date ? date.format('YYYY-MM-DD') : null,
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
format="DD/MM/YYYY"
|
format="DD/MM/YYYY"
|
||||||
maxDate={filters.createDateEnd ? moment(filters.createDateEnd, 'YYYY-MM-DD') : undefined}
|
maxDate={localFilters.createDateEnd ? moment(localFilters.createDateEnd, 'YYYY-MM-DD') : undefined}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
textField: {
|
textField: {
|
||||||
size: 'small',
|
size: 'small',
|
||||||
|
|
@ -265,15 +302,13 @@ export const SearchBar = () => {
|
||||||
<Box flex={1}>
|
<Box flex={1}>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
label="Data Final"
|
label="Data Final"
|
||||||
value={filters.createDateEnd ? moment(filters.createDateEnd, 'YYYY-MM-DD') : null}
|
value={localFilters.createDateEnd ? moment(localFilters.createDateEnd, 'YYYY-MM-DD') : null}
|
||||||
onChange={(date: moment.Moment | null) => {
|
onChange={(date: moment.Moment | null) => {
|
||||||
setTouchedFields(prev => ({ ...prev, createDateEnd: true }));
|
setTouchedFields(prev => ({ ...prev, createDateEnd: true }));
|
||||||
setFilters({
|
updateLocalFilter('createDateEnd', date ? date.format('YYYY-MM-DD') : null);
|
||||||
createDateEnd: date ? date.format('YYYY-MM-DD') : null,
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
format="DD/MM/YYYY"
|
format="DD/MM/YYYY"
|
||||||
minDate={filters.createDateIni ? moment(filters.createDateIni, 'YYYY-MM-DD') : undefined}
|
minDate={localFilters.createDateIni ? moment(localFilters.createDateIni, 'YYYY-MM-DD') : undefined}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
textField: {
|
textField: {
|
||||||
size: 'small',
|
size: 'small',
|
||||||
|
|
@ -361,19 +396,17 @@ export const SearchBar = () => {
|
||||||
getOptionLabel={(option) => option.label}
|
getOptionLabel={(option) => option.label}
|
||||||
isOptionEqualToValue={(option, value) => option.id === value.id}
|
isOptionEqualToValue={(option, value) => option.id === value.id}
|
||||||
value={stores.options.filter(option =>
|
value={stores.options.filter(option =>
|
||||||
filters.store?.includes(option.value)
|
localFilters.store?.includes(option.value)
|
||||||
)}
|
)}
|
||||||
onChange={(_, newValue) => {
|
onChange={(_, newValue) => {
|
||||||
setFilters({
|
updateLocalFilter('store', newValue.map(option => option.value));
|
||||||
store: newValue.map(option => option.value),
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
loading={stores.isLoading}
|
loading={stores.isLoading}
|
||||||
renderInput={(params: Readonly<any>) => (
|
renderInput={(params: Readonly<any>) => (
|
||||||
<TextField
|
<TextField
|
||||||
{...params}
|
{...params}
|
||||||
label="Filiais"
|
label="Filiais"
|
||||||
placeholder={filters.store?.length ? `${filters.store.length} selecionadas` : 'Selecione'}
|
placeholder={localFilters.store?.length ? `${localFilters.store.length} selecionadas` : 'Selecione'}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
@ -388,19 +421,17 @@ export const SearchBar = () => {
|
||||||
getOptionLabel={(option) => option.label}
|
getOptionLabel={(option) => option.label}
|
||||||
isOptionEqualToValue={(option, value) => option.id === value.id}
|
isOptionEqualToValue={(option, value) => option.id === value.id}
|
||||||
value={stores.options.filter(option =>
|
value={stores.options.filter(option =>
|
||||||
filters.stockId?.includes(option.value)
|
localFilters.stockId?.includes(option.value)
|
||||||
)}
|
)}
|
||||||
onChange={(_, newValue) => {
|
onChange={(_, newValue) => {
|
||||||
setFilters({
|
updateLocalFilter('stockId', newValue.map(option => option.value));
|
||||||
stockId: newValue.map(option => option.value),
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
loading={stores.isLoading}
|
loading={stores.isLoading}
|
||||||
renderInput={(params: Readonly<any>) => (
|
renderInput={(params: Readonly<any>) => (
|
||||||
<TextField
|
<TextField
|
||||||
{...params}
|
{...params}
|
||||||
label="Filial de Estoque"
|
label="Filial de Estoque"
|
||||||
placeholder={filters.stockId?.length ? `${filters.stockId.length} selecionadas` : 'Selecione'}
|
placeholder={localFilters.stockId?.length ? `${localFilters.stockId.length} selecionadas` : 'Selecione'}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
@ -414,13 +445,11 @@ export const SearchBar = () => {
|
||||||
getOptionLabel={(option) => option.label}
|
getOptionLabel={(option) => option.label}
|
||||||
isOptionEqualToValue={(option, value) => option.id === value.id}
|
isOptionEqualToValue={(option, value) => option.id === value.id}
|
||||||
value={sellers.options.find(option =>
|
value={sellers.options.find(option =>
|
||||||
filters.sellerId === option.seller.id.toString()
|
localFilters.sellerId === option.seller.id.toString()
|
||||||
) || null}
|
) || null}
|
||||||
onChange={(_, newValue) => {
|
onChange={(_, newValue) => {
|
||||||
setFilters({
|
updateLocalFilter('sellerId', newValue?.seller.id.toString() || null);
|
||||||
sellerId: newValue?.seller.id.toString() || null,
|
updateLocalFilter('sellerName', newValue?.seller.name || null);
|
||||||
sellerName: newValue?.seller.name || null,
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
loading={sellers.isLoading}
|
loading={sellers.isLoading}
|
||||||
renderInput={(params) => (
|
renderInput={(params) => (
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,63 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useMemo } from 'react';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
import Alert from '@mui/material/Alert';
|
import Alert from '@mui/material/Alert';
|
||||||
import Typography from '@mui/material/Typography';
|
import { DataGridPremium } from '@mui/x-data-grid-premium';
|
||||||
|
import { useOrderDetails } from '../../hooks/useOrderDetails';
|
||||||
|
import { createInformationPanelColumns } from './InformationPanelColumns';
|
||||||
|
import { dataGridStylesSimple } from '../../utils/dataGridStyles';
|
||||||
|
|
||||||
interface InformationPanelProps {
|
interface InformationPanelProps {
|
||||||
orderId: number;
|
orderId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const InformationPanel = ({ orderId }: InformationPanelProps) => {
|
export const InformationPanel = ({ orderId }: InformationPanelProps) => {
|
||||||
|
const { data: order, isLoading, error } = useOrderDetails(orderId);
|
||||||
|
|
||||||
|
const columns = useMemo(() => createInformationPanelColumns(), []);
|
||||||
|
|
||||||
|
const rows = useMemo(() => {
|
||||||
|
if (!order) return [];
|
||||||
|
return [{
|
||||||
|
id: order.orderId || orderId,
|
||||||
|
...order
|
||||||
|
}];
|
||||||
|
}, [order, orderId]);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
|
||||||
|
<CircularProgress size={30} />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<Box sx={{ p: 2 }}>
|
||||||
|
<Alert severity="error">Erro ao carregar detalhes do pedido.</Alert>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!order) {
|
||||||
|
return (
|
||||||
|
<Box sx={{ p: 2 }}>
|
||||||
|
<Alert severity="info">Informações do pedido não encontradas.</Alert>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<DataGridPremium
|
||||||
<Alert severity="info" sx={{ mb: 2 }}>
|
rows={rows}
|
||||||
<Typography variant="body2">
|
columns={columns}
|
||||||
Funcionalidade em desenvolvimento para o pedido {orderId}
|
density="compact"
|
||||||
</Typography>
|
autoHeight
|
||||||
</Alert>
|
hideFooter
|
||||||
</Box>
|
sx={dataGridStylesSimple}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid-premium';
|
||||||
|
import Chip from '@mui/material/Chip';
|
||||||
|
import { formatCurrency, formatDate, getStatusColor } from '../../utils/orderFormatters';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mapeia códigos de status para texto legível.
|
||||||
|
*/
|
||||||
|
const STATUS_LABELS: Record<string, string> = {
|
||||||
|
'F': 'Faturado',
|
||||||
|
'C': 'Cancelado',
|
||||||
|
'P': 'Pendente',
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusLabel = (status: string): string => STATUS_LABELS[status] || status;
|
||||||
|
|
||||||
|
export const createInformationPanelColumns = (): GridColDef[] => [
|
||||||
|
{
|
||||||
|
field: 'customerName',
|
||||||
|
headerName: 'Cliente',
|
||||||
|
width: 250,
|
||||||
|
description: 'Nome do cliente do pedido',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'storeId',
|
||||||
|
headerName: 'Filial',
|
||||||
|
width: 80,
|
||||||
|
align: 'center',
|
||||||
|
headerAlign: 'center',
|
||||||
|
description: 'Código da filial',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'createDate',
|
||||||
|
headerName: 'Data Criação',
|
||||||
|
width: 110,
|
||||||
|
align: 'center',
|
||||||
|
headerAlign: 'center',
|
||||||
|
description: 'Data de criação do pedido',
|
||||||
|
valueFormatter: (value) => formatDate(value as string),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'status',
|
||||||
|
headerName: 'Situação',
|
||||||
|
width: 120,
|
||||||
|
align: 'center',
|
||||||
|
headerAlign: 'center',
|
||||||
|
description: 'Situação atual do pedido',
|
||||||
|
renderCell: (params: Readonly<GridRenderCellParams>) => (
|
||||||
|
<Chip
|
||||||
|
label={getStatusLabel(params.value as string)}
|
||||||
|
size="small"
|
||||||
|
color={getStatusColor(params.value as string)}
|
||||||
|
variant="outlined"
|
||||||
|
sx={{ height: 24 }}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'paymentName',
|
||||||
|
headerName: 'Forma Pagamento',
|
||||||
|
width: 150,
|
||||||
|
description: 'Forma de pagamento utilizada',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'billingName',
|
||||||
|
headerName: 'Cond. Pagamento',
|
||||||
|
width: 150,
|
||||||
|
description: 'Condição de pagamento',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'amount',
|
||||||
|
headerName: 'Valor Total',
|
||||||
|
width: 120,
|
||||||
|
align: 'right',
|
||||||
|
headerAlign: 'right',
|
||||||
|
description: 'Valor total do pedido',
|
||||||
|
valueFormatter: (value) => formatCurrency(value as number),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'deliveryType',
|
||||||
|
headerName: 'Tipo Entrega',
|
||||||
|
width: 150,
|
||||||
|
description: 'Tipo de entrega selecionado',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'deliveryLocal',
|
||||||
|
headerName: 'Local Entrega',
|
||||||
|
width: 200,
|
||||||
|
flex: 1,
|
||||||
|
description: 'Local de entrega do pedido',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
|
import Alert from '@mui/material/Alert';
|
||||||
|
import { DataGridPremium } from '@mui/x-data-grid-premium';
|
||||||
|
import { useOrderItems } from '../../hooks/useOrderItems';
|
||||||
|
import { createOrderItemsColumns } from '../OrderItemsTableColumns';
|
||||||
|
import { OrderItem } from '../../schemas/order.item.schema';
|
||||||
|
import { dataGridStyles } from '../../utils/dataGridStyles';
|
||||||
|
|
||||||
|
interface OrderItemsTableProps {
|
||||||
|
orderId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OrderItemsTable = ({ orderId }: OrderItemsTableProps) => {
|
||||||
|
const { data: items, isLoading, error } = useOrderItems(orderId);
|
||||||
|
|
||||||
|
const columns = useMemo(() => createOrderItemsColumns(), []);
|
||||||
|
|
||||||
|
const rows = useMemo(() => {
|
||||||
|
if (!Array.isArray(items) || items.length === 0) return [];
|
||||||
|
return items.map((item: OrderItem, index: number) => ({
|
||||||
|
id: `${orderId}-${item.productId}-${index}`,
|
||||||
|
...item,
|
||||||
|
}));
|
||||||
|
}, [items, orderId]);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', py: 4 }}>
|
||||||
|
<CircularProgress size={40} />
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ ml: 2 }}>
|
||||||
|
Carregando itens do pedido...
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<Box sx={{ mt: 2 }}>
|
||||||
|
<Alert severity="error">
|
||||||
|
{error instanceof Error
|
||||||
|
? `Erro ao carregar itens: ${error.message}`
|
||||||
|
: 'Erro ao carregar itens do pedido.'}
|
||||||
|
</Alert>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!items || items.length === 0) {
|
||||||
|
return (
|
||||||
|
<Box sx={{ mt: 2 }}>
|
||||||
|
<Alert severity="info">Nenhum item encontrado para este pedido.</Alert>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DataGridPremium
|
||||||
|
rows={rows}
|
||||||
|
columns={columns}
|
||||||
|
density="compact"
|
||||||
|
autoHeight
|
||||||
|
hideFooter={rows.length <= 10}
|
||||||
|
initialState={{
|
||||||
|
pagination: {
|
||||||
|
paginationModel: {
|
||||||
|
pageSize: 10,
|
||||||
|
page: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
pageSizeOptions={[10, 25, 50]}
|
||||||
|
sx={dataGridStyles}
|
||||||
|
localeText={{
|
||||||
|
noRowsLabel: 'Nenhum item encontrado.',
|
||||||
|
noResultsOverlayLabel: 'Nenhum resultado encontrado.',
|
||||||
|
footerTotalRows: 'Total de itens:',
|
||||||
|
footerTotalVisibleRows: (visibleCount, totalCount) =>
|
||||||
|
`${visibleCount.toLocaleString()} de ${totalCount.toLocaleString()}`,
|
||||||
|
}}
|
||||||
|
slotProps={{
|
||||||
|
pagination: {
|
||||||
|
labelRowsPerPage: 'Itens por página:',
|
||||||
|
labelDisplayedRows: ({ from, to, count }: { from: number; to: number; count: number }) => {
|
||||||
|
const pageSize = to >= from ? to - from + 1 : 10;
|
||||||
|
const currentPage = Math.floor((from - 1) / pageSize) + 1;
|
||||||
|
const totalPages = Math.ceil(count / pageSize);
|
||||||
|
return `${from}–${to} de ${count} | Página ${currentPage} de ${totalPages}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { orderService } from '../api/order.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to fetch details for a specific order.
|
||||||
|
* Uses the general search endpoint filtering by ID as requested.
|
||||||
|
*/
|
||||||
|
export function useOrderDetails(orderId: number) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['orderDetails', orderId],
|
||||||
|
enabled: !!orderId,
|
||||||
|
queryFn: async () => {
|
||||||
|
// The findOrders method returns an array. We search by orderId and take the first result.
|
||||||
|
const orders = await orderService.findOrders({ orderId: orderId });
|
||||||
|
return orders.length > 0 ? orders[0] : null;
|
||||||
|
},
|
||||||
|
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
import { SxProps, Theme } from '@mui/material/styles';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Estilos compartilhados para DataGridPremium utilizado em tabelas de pedidos.
|
||||||
|
* Garante consistência visual entre OrderItemsTable, InformationPanel, etc.
|
||||||
|
*/
|
||||||
|
export const dataGridStyles: SxProps<Theme> = {
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: 'divider',
|
||||||
|
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
|
||||||
|
backgroundColor: 'background.paper',
|
||||||
|
|
||||||
|
'& .MuiDataGrid-root': {
|
||||||
|
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
|
||||||
|
border: 'none',
|
||||||
|
},
|
||||||
|
|
||||||
|
'& .MuiDataGrid-columnHeaders': {
|
||||||
|
backgroundColor: 'grey.50',
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: '0.75rem',
|
||||||
|
borderBottom: '2px solid',
|
||||||
|
borderColor: 'divider',
|
||||||
|
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
|
||||||
|
minHeight: '40px !important',
|
||||||
|
maxHeight: '40px !important',
|
||||||
|
},
|
||||||
|
|
||||||
|
'& .MuiDataGrid-columnHeader': {
|
||||||
|
borderRight: '1px solid',
|
||||||
|
borderColor: 'divider',
|
||||||
|
paddingLeft: '12px',
|
||||||
|
paddingRight: '12px',
|
||||||
|
'&:focus': { outline: 'none' },
|
||||||
|
'&:focus-within': { outline: 'none' },
|
||||||
|
'&:last-of-type': { borderRight: 'none' },
|
||||||
|
},
|
||||||
|
|
||||||
|
'& .MuiDataGrid-row': {
|
||||||
|
borderBottom: '1px solid',
|
||||||
|
borderColor: 'divider',
|
||||||
|
backgroundColor: 'background.paper',
|
||||||
|
minHeight: '36px !important',
|
||||||
|
maxHeight: '36px !important',
|
||||||
|
'&:hover': { backgroundColor: 'action.hover' },
|
||||||
|
'&:last-child': { borderBottom: 'none' },
|
||||||
|
},
|
||||||
|
|
||||||
|
'& .MuiDataGrid-row:nth-of-type(even)': {
|
||||||
|
backgroundColor: 'grey.50',
|
||||||
|
'&:hover': { backgroundColor: 'action.hover' },
|
||||||
|
},
|
||||||
|
|
||||||
|
'& .MuiDataGrid-cell': {
|
||||||
|
borderRight: '1px solid',
|
||||||
|
borderColor: 'divider',
|
||||||
|
borderBottom: 'none',
|
||||||
|
paddingLeft: '12px',
|
||||||
|
paddingRight: '12px',
|
||||||
|
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
|
||||||
|
fontSize: '0.75rem',
|
||||||
|
lineHeight: 1.2,
|
||||||
|
'&:focus': { outline: 'none' },
|
||||||
|
'&:focus-within': { outline: 'none' },
|
||||||
|
'&:last-of-type': { borderRight: 'none' },
|
||||||
|
},
|
||||||
|
|
||||||
|
'& .MuiDataGrid-footerContainer': {
|
||||||
|
borderTop: '2px solid',
|
||||||
|
borderColor: 'divider',
|
||||||
|
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
|
||||||
|
minHeight: '48px !important',
|
||||||
|
fontSize: '0.75rem',
|
||||||
|
backgroundColor: 'grey.50',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Versão simplificada sem zebra-striping (para tabelas de linha única).
|
||||||
|
*/
|
||||||
|
export const dataGridStylesSimple: SxProps<Theme> = {
|
||||||
|
...dataGridStyles,
|
||||||
|
'& .MuiDataGrid-row': {
|
||||||
|
borderBottom: 'none',
|
||||||
|
backgroundColor: 'background.paper',
|
||||||
|
minHeight: '36px !important',
|
||||||
|
maxHeight: '36px !important',
|
||||||
|
'&:hover': { backgroundColor: 'action.hover' },
|
||||||
|
},
|
||||||
|
'& .MuiDataGrid-row:nth-of-type(even)': {
|
||||||
|
backgroundColor: 'background.paper',
|
||||||
|
},
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue