Merge branch 'feature/searchbar-drawer-redesign'
This commit is contained in:
commit
8b0401a6a7
|
|
@ -15,6 +15,8 @@ import {
|
||||||
storesResponseSchema,
|
storesResponseSchema,
|
||||||
customersResponseSchema,
|
customersResponseSchema,
|
||||||
sellersResponseSchema,
|
sellersResponseSchema,
|
||||||
|
partnersResponseSchema,
|
||||||
|
productsResponseSchema,
|
||||||
unwrapApiData,
|
unwrapApiData,
|
||||||
} from '../schemas/order.schema';
|
} from '../schemas/order.schema';
|
||||||
import {
|
import {
|
||||||
|
|
@ -178,5 +180,42 @@ export const orderService = {
|
||||||
return unwrapApiData(response, cuttingItemResponseSchema, []);
|
return unwrapApiData(response, cuttingItemResponseSchema, []);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Busca parceiros por nome/CPF.
|
||||||
|
* Retorna array vazio se o termo de busca tiver menos de 2 caracteres.
|
||||||
|
*
|
||||||
|
* @param {string} filter - O termo de busca (mínimo 2 caracteres)
|
||||||
|
* @returns {Promise<Array<{id: number, cpf: string, nome: string}>>} Array de parceiros correspondentes
|
||||||
|
*/
|
||||||
|
findPartners: async (
|
||||||
|
filter: string
|
||||||
|
): Promise<Array<{ id: number; cpf: string; nome: string }>> => {
|
||||||
|
if (!filter || filter.trim().length < 2) return [];
|
||||||
|
|
||||||
|
const response = await ordersApi.get(
|
||||||
|
`/api/v1/parceiros/${encodeURIComponent(filter)}`
|
||||||
|
);
|
||||||
|
return unwrapApiData(response, partnersResponseSchema, []);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Busca produtos por código ou descrição.
|
||||||
|
* Retorna array vazio se o termo de busca tiver menos de 2 caracteres.
|
||||||
|
*
|
||||||
|
* @param {string} term - O termo de busca (mínimo 2 caracteres)
|
||||||
|
* @returns {Promise<Array<{id: number, description: string}>>} Array de produtos correspondentes
|
||||||
|
*/
|
||||||
|
findProducts: async (
|
||||||
|
term: string
|
||||||
|
): Promise<Array<{ id: number; description: string }>> => {
|
||||||
|
if (!term || term.trim().length < 2) return [];
|
||||||
|
|
||||||
|
const response = await ordersApi.get(
|
||||||
|
`/api/v1/data-consult/products/${encodeURIComponent(term)}`
|
||||||
|
);
|
||||||
|
return unwrapApiData(response, productsResponseSchema, []);
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -12,22 +12,28 @@ import {
|
||||||
Autocomplete,
|
Autocomplete,
|
||||||
Paper,
|
Paper,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Collapse,
|
|
||||||
CircularProgress,
|
CircularProgress,
|
||||||
Badge,
|
Badge,
|
||||||
|
Drawer,
|
||||||
|
IconButton,
|
||||||
|
Typography,
|
||||||
|
Divider,
|
||||||
|
Stack,
|
||||||
type AutocompleteRenderInputParams,
|
type AutocompleteRenderInputParams,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { useStores } from '../store/useStores';
|
import { useStores } from '../store/useStores';
|
||||||
import { useCustomers } from '../hooks/useCustomers';
|
import { useCustomers } from '../hooks/useCustomers';
|
||||||
import { useSellers } from '../hooks/useSellers';
|
import { useSellers } from '../hooks/useSellers';
|
||||||
|
import { usePartners } from '../hooks/usePartners';
|
||||||
|
import { useProducts } from '../hooks/useProducts';
|
||||||
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
||||||
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
|
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
|
||||||
import { AdapterMoment } from '@mui/x-date-pickers/AdapterMoment';
|
import { AdapterMoment } from '@mui/x-date-pickers/AdapterMoment';
|
||||||
import {
|
import {
|
||||||
Search as SearchIcon,
|
Search as SearchIcon,
|
||||||
RestartAlt as ResetIcon,
|
RestartAlt as ResetIcon,
|
||||||
ExpandLess as ExpandLessIcon,
|
Tune as TuneIcon,
|
||||||
ExpandMore as ExpandMoreIcon,
|
Close as CloseIcon,
|
||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import 'moment/locale/pt-br';
|
import 'moment/locale/pt-br';
|
||||||
|
|
@ -39,6 +45,10 @@ interface LocalFilters {
|
||||||
orderId: number | null;
|
orderId: number | null;
|
||||||
customerId: number | null;
|
customerId: number | null;
|
||||||
customerName: string | null;
|
customerName: string | null;
|
||||||
|
partnerId: string | null;
|
||||||
|
partnerName: string | null;
|
||||||
|
productId: number | null;
|
||||||
|
productName: string | null;
|
||||||
createDateIni: string | null;
|
createDateIni: string | null;
|
||||||
createDateEnd: string | null;
|
createDateEnd: string | null;
|
||||||
store: string[] | null;
|
store: string[] | null;
|
||||||
|
|
@ -54,6 +64,10 @@ const getInitialLocalFilters = (
|
||||||
orderId: urlFilters.orderId ?? null,
|
orderId: urlFilters.orderId ?? null,
|
||||||
customerId: urlFilters.customerId ?? null,
|
customerId: urlFilters.customerId ?? null,
|
||||||
customerName: urlFilters.customerName ?? null,
|
customerName: urlFilters.customerName ?? null,
|
||||||
|
partnerId: urlFilters.partnerId ?? null,
|
||||||
|
partnerName: urlFilters.partnerName ?? null,
|
||||||
|
productId: urlFilters.productId ?? null,
|
||||||
|
productName: urlFilters.productName ?? null,
|
||||||
createDateIni: urlFilters.createDateIni ?? null,
|
createDateIni: urlFilters.createDateIni ?? null,
|
||||||
createDateEnd: urlFilters.createDateEnd ?? null,
|
createDateEnd: urlFilters.createDateEnd ?? null,
|
||||||
store: urlFilters.store ?? null,
|
store: urlFilters.store ?? null,
|
||||||
|
|
@ -73,6 +87,10 @@ export const SearchBar = () => {
|
||||||
const sellers = useSellers();
|
const sellers = useSellers();
|
||||||
const [customerSearchTerm, setCustomerSearchTerm] = useState('');
|
const [customerSearchTerm, setCustomerSearchTerm] = useState('');
|
||||||
const customers = useCustomers(customerSearchTerm);
|
const customers = useCustomers(customerSearchTerm);
|
||||||
|
const [partnerSearchTerm, setPartnerSearchTerm] = useState('');
|
||||||
|
const partners = usePartners(partnerSearchTerm);
|
||||||
|
const [productSearchTerm, setProductSearchTerm] = useState('');
|
||||||
|
const products = useProducts(productSearchTerm);
|
||||||
const [showAdvancedFilters, setShowAdvancedFilters] = useState(false);
|
const [showAdvancedFilters, setShowAdvancedFilters] = useState(false);
|
||||||
const { isFetching } = useOrders();
|
const { isFetching } = useOrders();
|
||||||
const [touchedFields, setTouchedFields] = useState<{
|
const [touchedFields, setTouchedFields] = useState<{
|
||||||
|
|
@ -94,6 +112,8 @@ export const SearchBar = () => {
|
||||||
const handleReset = useCallback(() => {
|
const handleReset = useCallback(() => {
|
||||||
setTouchedFields({});
|
setTouchedFields({});
|
||||||
setCustomerSearchTerm('');
|
setCustomerSearchTerm('');
|
||||||
|
setPartnerSearchTerm('');
|
||||||
|
setProductSearchTerm('');
|
||||||
|
|
||||||
const resetState = {
|
const resetState = {
|
||||||
status: null,
|
status: null,
|
||||||
|
|
@ -103,7 +123,6 @@ export const SearchBar = () => {
|
||||||
codusur2: null,
|
codusur2: null,
|
||||||
store: null,
|
store: null,
|
||||||
orderId: null,
|
orderId: null,
|
||||||
productId: null,
|
|
||||||
stockId: null,
|
stockId: null,
|
||||||
hasPreBox: false,
|
hasPreBox: false,
|
||||||
includeCheckout: false,
|
includeCheckout: false,
|
||||||
|
|
@ -112,6 +131,10 @@ export const SearchBar = () => {
|
||||||
searchTriggered: false,
|
searchTriggered: false,
|
||||||
customerId: null,
|
customerId: null,
|
||||||
customerName: null,
|
customerName: null,
|
||||||
|
partnerId: null,
|
||||||
|
partnerName: null,
|
||||||
|
productId: null,
|
||||||
|
productName: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
setLocalFilters(getInitialLocalFilters(resetState));
|
setLocalFilters(getInitialLocalFilters(resetState));
|
||||||
|
|
@ -167,17 +190,32 @@ export const SearchBar = () => {
|
||||||
touchedFields.createDateIni && !localFilters.createDateIni;
|
touchedFields.createDateIni && !localFilters.createDateIni;
|
||||||
const showDateEndError = touchedFields.createDateEnd && dateError;
|
const showDateEndError = touchedFields.createDateEnd && dateError;
|
||||||
|
|
||||||
// Contador de filtros avançados ativos
|
// Contador de filtros ativos no Drawer
|
||||||
const advancedFiltersCount = [
|
const activeDrawerFiltersCount = [
|
||||||
|
localFilters.status,
|
||||||
|
localFilters.customerId,
|
||||||
localFilters.store?.length,
|
localFilters.store?.length,
|
||||||
localFilters.stockId?.length,
|
localFilters.stockId?.length,
|
||||||
localFilters.sellerId,
|
localFilters.sellerId,
|
||||||
|
localFilters.partnerId,
|
||||||
|
localFilters.productId,
|
||||||
].filter(Boolean).length;
|
].filter(Boolean).length;
|
||||||
|
|
||||||
|
const toggleDrawer = (open: boolean) => (event: React.KeyboardEvent | React.MouseEvent) => {
|
||||||
|
if (
|
||||||
|
event.type === 'keydown' &&
|
||||||
|
((event as React.KeyboardEvent).key === 'Tab' ||
|
||||||
|
(event as React.KeyboardEvent).key === 'Shift')
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setShowAdvancedFilters(open);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper
|
<Paper
|
||||||
sx={{
|
sx={{
|
||||||
p: { xs: 2, md: 3 },
|
p: { xs: 2, md: 2 },
|
||||||
mb: 2,
|
mb: 2,
|
||||||
bgcolor: 'background.paper',
|
bgcolor: 'background.paper',
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
|
|
@ -186,10 +224,8 @@ export const SearchBar = () => {
|
||||||
elevation={0}
|
elevation={0}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
>
|
>
|
||||||
<Grid container spacing={{ xs: 1.5, md: 2 }} alignItems="flex-end">
|
<Grid container spacing={2} alignItems="center">
|
||||||
{/* --- Primary Filters (Always Visible) --- */}
|
{/* Nº do Pedido */}
|
||||||
|
|
||||||
{/* Campo de Texto Simples (Nº Pedido) */}
|
|
||||||
<Grid size={{ xs: 12, sm: 6, md: 2 }}>
|
<Grid size={{ xs: 12, sm: 6, md: 2 }}>
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
|
|
@ -207,404 +243,227 @@ export const SearchBar = () => {
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
{/* Select do MUI (Situação) */}
|
{/* Datas */}
|
||||||
<Grid size={{ xs: 12, sm: 6, md: 2 }}>
|
<Grid size={{ xs: 12, sm: 12, md: 5 }}>
|
||||||
<TextField
|
<LocalizationProvider dateAdapter={AdapterMoment} adapterLocale="pt-br">
|
||||||
select
|
<Box display="flex" gap={1.5}>
|
||||||
fullWidth
|
<DatePicker
|
||||||
label="Situação"
|
label="Data Inicial"
|
||||||
size="small"
|
value={localFilters.createDateIni ? moment(localFilters.createDateIni, 'YYYY-MM-DD') : null}
|
||||||
value={localFilters.status ?? ''}
|
onChange={(date) => {
|
||||||
onChange={(e) => {
|
setTouchedFields(prev => ({ ...prev, createDateIni: true }));
|
||||||
const value = e.target.value || null;
|
updateLocalFilter('createDateIni', date ? date.format('YYYY-MM-DD') : null);
|
||||||
updateLocalFilter('status', value);
|
}}
|
||||||
}}
|
format="DD/MM/YYYY"
|
||||||
>
|
slotProps={{
|
||||||
<MenuItem value="">Todos</MenuItem>
|
textField: {
|
||||||
<MenuItem value="P">Pendente</MenuItem>
|
size: 'small',
|
||||||
<MenuItem value="F">Faturado</MenuItem>
|
fullWidth: true,
|
||||||
<MenuItem value="C">Cancelado</MenuItem>
|
required: true,
|
||||||
</TextField>
|
error: showDateIniError,
|
||||||
</Grid>
|
helperText: showDateIniError ? 'Obrigatório' : '',
|
||||||
|
}
|
||||||
{/* Autocomplete do MUI para Cliente */}
|
}}
|
||||||
<Grid size={{ xs: 12, sm: 6, md: 2 }}>
|
|
||||||
<Autocomplete
|
|
||||||
size="small"
|
|
||||||
options={customers.options}
|
|
||||||
getOptionLabel={(option) => option.label}
|
|
||||||
isOptionEqualToValue={(option, value) => option.id === value.id}
|
|
||||||
value={
|
|
||||||
customers.options.find(
|
|
||||||
(option) => localFilters.customerId === option.customer?.id
|
|
||||||
) || null
|
|
||||||
}
|
|
||||||
onChange={(_, newValue) => {
|
|
||||||
if (!newValue) {
|
|
||||||
updateLocalFilter('customerName', null);
|
|
||||||
updateLocalFilter('customerId', null);
|
|
||||||
setCustomerSearchTerm('');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateLocalFilter('customerId', newValue.customer?.id || null);
|
|
||||||
updateLocalFilter(
|
|
||||||
'customerName',
|
|
||||||
newValue.customer?.name || null
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
onInputChange={(_, newInputValue, reason) => {
|
|
||||||
if (reason === 'clear') {
|
|
||||||
updateLocalFilter('customerName', null);
|
|
||||||
updateLocalFilter('customerId', null);
|
|
||||||
setCustomerSearchTerm('');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reason === 'input') {
|
|
||||||
setCustomerSearchTerm(newInputValue);
|
|
||||||
if (!newInputValue) {
|
|
||||||
updateLocalFilter('customerName', null);
|
|
||||||
updateLocalFilter('customerId', null);
|
|
||||||
setCustomerSearchTerm('');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
loading={customers.isLoading}
|
|
||||||
renderInput={(params: AutocompleteRenderInputParams) => (
|
|
||||||
<TextField
|
|
||||||
{...params}
|
|
||||||
label="Cliente"
|
|
||||||
placeholder="Digite para buscar..."
|
|
||||||
/>
|
/>
|
||||||
)}
|
<DatePicker
|
||||||
noOptionsText={
|
label="Data Final"
|
||||||
customerSearchTerm.length < 2
|
value={localFilters.createDateEnd ? moment(localFilters.createDateEnd, 'YYYY-MM-DD') : null}
|
||||||
? 'Digite pelo menos 2 caracteres'
|
onChange={(date) => {
|
||||||
: 'Nenhum cliente encontrado'
|
setTouchedFields(prev => ({ ...prev, createDateEnd: true }));
|
||||||
}
|
updateLocalFilter('createDateEnd', date ? date.format('YYYY-MM-DD') : null);
|
||||||
loadingText="Buscando clientes..."
|
}}
|
||||||
filterOptions={(x) => x}
|
format="DD/MM/YYYY"
|
||||||
clearOnBlur={false}
|
slotProps={{
|
||||||
selectOnFocus
|
textField: {
|
||||||
handleHomeEndKeys
|
size: 'small',
|
||||||
/>
|
fullWidth: true,
|
||||||
</Grid>
|
error: !!showDateEndError,
|
||||||
|
helperText: showDateEndError || '',
|
||||||
{/* Campos de Data */}
|
|
||||||
<Grid size={{ xs: 12, sm: 12, md: 3.5 }}>
|
|
||||||
<LocalizationProvider
|
|
||||||
dateAdapter={AdapterMoment}
|
|
||||||
adapterLocale="pt-br"
|
|
||||||
>
|
|
||||||
<Box display="flex" gap={{ xs: 1.5, md: 2 }} flexDirection={{ xs: 'column', sm: 'row' }}>
|
|
||||||
<Box flex={1}>
|
|
||||||
<DatePicker
|
|
||||||
label="Data Inicial"
|
|
||||||
value={
|
|
||||||
localFilters.createDateIni
|
|
||||||
? moment(localFilters.createDateIni, 'YYYY-MM-DD')
|
|
||||||
: null
|
|
||||||
}
|
}
|
||||||
onChange={(date: moment.Moment | null) => {
|
}}
|
||||||
setTouchedFields((prev) => ({
|
/>
|
||||||
...prev,
|
|
||||||
createDateIni: true,
|
|
||||||
}));
|
|
||||||
updateLocalFilter(
|
|
||||||
'createDateIni',
|
|
||||||
date ? date.format('YYYY-MM-DD') : null
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
format="DD/MM/YYYY"
|
|
||||||
maxDate={
|
|
||||||
localFilters.createDateEnd
|
|
||||||
? moment(localFilters.createDateEnd, 'YYYY-MM-DD')
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
slotProps={{
|
|
||||||
textField: {
|
|
||||||
size: 'small',
|
|
||||||
fullWidth: true,
|
|
||||||
required: true,
|
|
||||||
error: showDateIniError,
|
|
||||||
helperText: showDateIniError
|
|
||||||
? 'Data inicial é obrigatória'
|
|
||||||
: '',
|
|
||||||
onBlur: () =>
|
|
||||||
setTouchedFields((prev) => ({
|
|
||||||
...prev,
|
|
||||||
createDateIni: true,
|
|
||||||
})),
|
|
||||||
inputProps: {
|
|
||||||
'aria-required': true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
<Box flex={1}>
|
|
||||||
<DatePicker
|
|
||||||
label="Data Final (opcional)"
|
|
||||||
value={
|
|
||||||
localFilters.createDateEnd
|
|
||||||
? moment(localFilters.createDateEnd, 'YYYY-MM-DD')
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
onChange={(date: moment.Moment | null) => {
|
|
||||||
setTouchedFields((prev) => ({
|
|
||||||
...prev,
|
|
||||||
createDateEnd: true,
|
|
||||||
}));
|
|
||||||
updateLocalFilter(
|
|
||||||
'createDateEnd',
|
|
||||||
date ? date.format('YYYY-MM-DD') : null
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
format="DD/MM/YYYY"
|
|
||||||
minDate={
|
|
||||||
localFilters.createDateIni
|
|
||||||
? moment(localFilters.createDateIni, 'YYYY-MM-DD')
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
slotProps={{
|
|
||||||
textField: {
|
|
||||||
size: 'small',
|
|
||||||
fullWidth: true,
|
|
||||||
error: !!showDateEndError,
|
|
||||||
helperText: showDateEndError || '',
|
|
||||||
onBlur: () =>
|
|
||||||
setTouchedFields((prev) => ({
|
|
||||||
...prev,
|
|
||||||
createDateEnd: true,
|
|
||||||
})),
|
|
||||||
inputProps: {
|
|
||||||
placeholder: 'Opcional',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
</LocalizationProvider>
|
</LocalizationProvider>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
{/* Botão Mais Filtros - inline com filtros primários */}
|
{/* Ações */}
|
||||||
<Grid
|
<Grid size={{ xs: 12, md: 5 }} sx={{ display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
|
||||||
size={{ xs: 12, sm: 12, md: 2.5 }}
|
<Button
|
||||||
sx={{ display: 'flex', alignItems: 'flex-end', justifyContent: { xs: 'flex-start', md: 'flex-end' } }}
|
variant="outlined"
|
||||||
>
|
color="inherit"
|
||||||
<Badge
|
size="small"
|
||||||
badgeContent={advancedFiltersCount}
|
onClick={handleReset}
|
||||||
color="primary"
|
startIcon={<ResetIcon />}
|
||||||
invisible={advancedFiltersCount === 0}
|
sx={{ textTransform: 'none', minWidth: 90 }}
|
||||||
>
|
>
|
||||||
|
Limpar
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Badge badgeContent={activeDrawerFiltersCount} color="primary">
|
||||||
<Button
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
color="primary"
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() => setShowAdvancedFilters(!showAdvancedFilters)}
|
onClick={toggleDrawer(true)}
|
||||||
endIcon={
|
startIcon={<TuneIcon />}
|
||||||
showAdvancedFilters ? <ExpandLessIcon /> : <ExpandMoreIcon />
|
sx={{ textTransform: 'none', minWidth: 100 }}
|
||||||
}
|
|
||||||
aria-label={showAdvancedFilters ? 'Ocultar filtros avançados' : 'Mostrar filtros avançados'}
|
|
||||||
sx={{ textTransform: 'none', color: 'text.secondary', minHeight: 40 }}
|
|
||||||
>
|
>
|
||||||
{showAdvancedFilters ? 'Menos filtros' : 'Mais filtros'}
|
Filtros
|
||||||
</Button>
|
</Button>
|
||||||
</Badge>
|
</Badge>
|
||||||
</Grid>
|
|
||||||
|
|
||||||
{/* Botões de Ação - nova linha abaixo */}
|
<Button
|
||||||
<Grid
|
variant="contained"
|
||||||
size={{ xs: 12 }}
|
color="primary"
|
||||||
sx={{ display: 'flex', justifyContent: 'flex-end', mt: 1 }}
|
size="small"
|
||||||
>
|
onClick={handleFilter}
|
||||||
<Box sx={{ display: 'flex', gap: 1, width: { xs: '100%', sm: 'auto' }, flexWrap: 'nowrap' }}>
|
disabled={!isDateValid || !!dateError || isFetching}
|
||||||
<Tooltip title="Limpar filtros" arrow>
|
startIcon={isFetching ? <CircularProgress size={16} color="inherit" /> : <SearchIcon />}
|
||||||
<span>
|
sx={{ textTransform: 'none', minWidth: 100 }}
|
||||||
<Button
|
>
|
||||||
variant="outlined"
|
Buscar
|
||||||
color="inherit"
|
</Button>
|
||||||
size="small"
|
|
||||||
onClick={handleReset}
|
|
||||||
aria-label="Limpar filtros"
|
|
||||||
sx={{
|
|
||||||
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 sx={{ mr: 0.5, fontSize: 18 }} />
|
|
||||||
Limpar
|
|
||||||
</Button>
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip
|
|
||||||
title={
|
|
||||||
isDateValid
|
|
||||||
? 'Buscar pedidos'
|
|
||||||
: 'Preencha a data inicial para buscar'
|
|
||||||
}
|
|
||||||
arrow
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
size="small"
|
|
||||||
onClick={handleFilter}
|
|
||||||
disabled={!isDateValid || !!dateError || isFetching}
|
|
||||||
aria-label="Buscar pedidos"
|
|
||||||
sx={{
|
|
||||||
minWidth: { xs: 'auto', sm: 90 },
|
|
||||||
minHeight: 32,
|
|
||||||
px: 1.5,
|
|
||||||
flexShrink: 0,
|
|
||||||
flex: { xs: 1, sm: 'none' },
|
|
||||||
fontSize: '0.8125rem',
|
|
||||||
'&:disabled': {
|
|
||||||
opacity: 0.6,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{isFetching ? (
|
|
||||||
<CircularProgress size={16} color="inherit" />
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<SearchIcon sx={{ mr: 0.5, fontSize: 18 }} />
|
|
||||||
Buscar
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
</Box>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
{/* --- Advanced Filters (Collapsible) --- */}
|
|
||||||
<Grid size={{ xs: 12 }}>
|
|
||||||
<Collapse in={showAdvancedFilters}>
|
|
||||||
<Grid container spacing={2} sx={{ pt: 2 }}>
|
|
||||||
{/* Autocomplete do MUI para Múltiplas Filiais (codfilial) */}
|
|
||||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
|
||||||
<Autocomplete
|
|
||||||
multiple
|
|
||||||
size="small"
|
|
||||||
options={stores.options}
|
|
||||||
getOptionLabel={(option) => option.label}
|
|
||||||
isOptionEqualToValue={(option, value) =>
|
|
||||||
option.id === value.id
|
|
||||||
}
|
|
||||||
value={stores.options.filter((option) =>
|
|
||||||
localFilters.store?.includes(option.value)
|
|
||||||
)}
|
|
||||||
onChange={(_, newValue) => {
|
|
||||||
updateLocalFilter(
|
|
||||||
'store',
|
|
||||||
newValue.map((option) => option.value)
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
loading={stores.isLoading}
|
|
||||||
renderInput={(params: AutocompleteRenderInputParams) => (
|
|
||||||
<TextField
|
|
||||||
{...params}
|
|
||||||
label="Filiais"
|
|
||||||
placeholder={
|
|
||||||
localFilters.store?.length
|
|
||||||
? `${localFilters.store.length} selecionadas`
|
|
||||||
: 'Selecione'
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
{/* Autocomplete do MUI para Filial de Estoque */}
|
|
||||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
|
||||||
<Autocomplete
|
|
||||||
multiple
|
|
||||||
size="small"
|
|
||||||
options={stores.options}
|
|
||||||
getOptionLabel={(option) => option.label}
|
|
||||||
isOptionEqualToValue={(option, value) =>
|
|
||||||
option.id === value.id
|
|
||||||
}
|
|
||||||
value={stores.options.filter((option) =>
|
|
||||||
localFilters.stockId?.includes(option.value)
|
|
||||||
)}
|
|
||||||
onChange={(_, newValue) => {
|
|
||||||
updateLocalFilter(
|
|
||||||
'stockId',
|
|
||||||
newValue.map((option) => option.value)
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
loading={stores.isLoading}
|
|
||||||
renderInput={(params: AutocompleteRenderInputParams) => (
|
|
||||||
<TextField
|
|
||||||
{...params}
|
|
||||||
label="Filial de Estoque"
|
|
||||||
placeholder={
|
|
||||||
localFilters.stockId?.length
|
|
||||||
? `${localFilters.stockId.length} selecionadas`
|
|
||||||
: 'Selecione'
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
{/* Autocomplete do MUI para Vendedor */}
|
|
||||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
|
||||||
<Autocomplete
|
|
||||||
size="small"
|
|
||||||
options={sellers.options}
|
|
||||||
getOptionLabel={(option) => option.label}
|
|
||||||
isOptionEqualToValue={(option, value) =>
|
|
||||||
option.id === value.id
|
|
||||||
}
|
|
||||||
value={
|
|
||||||
sellers.options.find(
|
|
||||||
(option) =>
|
|
||||||
localFilters.sellerId === option.seller.id.toString()
|
|
||||||
) || null
|
|
||||||
}
|
|
||||||
onChange={(_, newValue) => {
|
|
||||||
updateLocalFilter(
|
|
||||||
'sellerId',
|
|
||||||
newValue?.seller.id.toString() || null
|
|
||||||
);
|
|
||||||
updateLocalFilter(
|
|
||||||
'sellerName',
|
|
||||||
newValue?.seller.name || null
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
loading={sellers.isLoading}
|
|
||||||
renderInput={(params) => (
|
|
||||||
<TextField
|
|
||||||
{...params}
|
|
||||||
label="Vendedor"
|
|
||||||
placeholder="Selecione um vendedor"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
renderOption={(props, option) => {
|
|
||||||
const { key, ...otherProps } = props;
|
|
||||||
return (
|
|
||||||
<li key={option.id} {...otherProps}>
|
|
||||||
{option.label}
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</Collapse>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
{/* Sidebar de Filtros */}
|
||||||
|
<Drawer
|
||||||
|
anchor="right"
|
||||||
|
open={showAdvancedFilters}
|
||||||
|
onClose={toggleDrawer(false)}
|
||||||
|
PaperProps={{
|
||||||
|
sx: { width: { xs: '100%', sm: 400 }, p: 0 }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||||
|
{/* Header */}
|
||||||
|
<Box sx={{ p: 2, display: 'flex', alignItems: 'center', justifyContent: 'space-between', borderBottom: '1px solid', borderColor: 'divider' }}>
|
||||||
|
<Typography variant="h6" sx={{ fontWeight: 600 }}>Todos os Filtros</Typography>
|
||||||
|
<IconButton onClick={toggleDrawer(false)} size="small">
|
||||||
|
<CloseIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Filtros Content */}
|
||||||
|
<Box sx={{ p: 3, flex: 1, overflowY: 'auto' }}>
|
||||||
|
<Stack spacing={3}>
|
||||||
|
{/* Situação */}
|
||||||
|
<TextField
|
||||||
|
select
|
||||||
|
fullWidth
|
||||||
|
label="Situação"
|
||||||
|
size="small"
|
||||||
|
value={localFilters.status ?? ''}
|
||||||
|
onChange={(e) => updateLocalFilter('status', e.target.value || null)}
|
||||||
|
>
|
||||||
|
<MenuItem value="">Todos</MenuItem>
|
||||||
|
<MenuItem value="P">Pendente</MenuItem>
|
||||||
|
<MenuItem value="F">Faturado</MenuItem>
|
||||||
|
<MenuItem value="C">Cancelado</MenuItem>
|
||||||
|
</TextField>
|
||||||
|
|
||||||
|
{/* Cliente */}
|
||||||
|
<Autocomplete
|
||||||
|
size="small"
|
||||||
|
options={customers.options}
|
||||||
|
getOptionLabel={(option) => option.label}
|
||||||
|
isOptionEqualToValue={(option, value) => option.id === value.id}
|
||||||
|
value={customers.options.find(opt => localFilters.customerId === opt.customer?.id) || null}
|
||||||
|
onChange={(_, newValue) => {
|
||||||
|
updateLocalFilter('customerId', newValue?.customer?.id || null);
|
||||||
|
updateLocalFilter('customerName', newValue?.customer?.name || null);
|
||||||
|
}}
|
||||||
|
onInputChange={(_, val) => setCustomerSearchTerm(val)}
|
||||||
|
loading={customers.isLoading}
|
||||||
|
renderInput={(params) => <TextField {...params} label="Cliente" placeholder="Buscar cliente..." />}
|
||||||
|
noOptionsText={customerSearchTerm.length < 2 ? 'Digite 2+ caracteres' : 'Nenhum cliente'}
|
||||||
|
filterOptions={(x) => x}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Filiais */}
|
||||||
|
<Autocomplete
|
||||||
|
multiple
|
||||||
|
size="small"
|
||||||
|
options={stores.options}
|
||||||
|
getOptionLabel={(option) => option.label}
|
||||||
|
isOptionEqualToValue={(option, value) => option.id === value.id}
|
||||||
|
value={stores.options.filter((opt) => localFilters.store?.includes(opt.value))}
|
||||||
|
onChange={(_, newValue) => updateLocalFilter('store', newValue.map((opt) => opt.value))}
|
||||||
|
loading={stores.isLoading}
|
||||||
|
renderInput={(params) => <TextField {...params} label="Filiais" placeholder="Selecione filiais" />}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Filial de Estoque */}
|
||||||
|
<Autocomplete
|
||||||
|
multiple
|
||||||
|
size="small"
|
||||||
|
options={stores.options}
|
||||||
|
getOptionLabel={(option) => option.label}
|
||||||
|
isOptionEqualToValue={(option, value) => option.id === value.id}
|
||||||
|
value={stores.options.filter((opt) => localFilters.stockId?.includes(opt.value))}
|
||||||
|
onChange={(_, newValue) => updateLocalFilter('stockId', newValue.map((opt) => opt.value))}
|
||||||
|
loading={stores.isLoading}
|
||||||
|
renderInput={(params) => <TextField {...params} label="Filial de Estoque" placeholder="Selecione filial" />}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Vendedor */}
|
||||||
|
<Autocomplete
|
||||||
|
size="small"
|
||||||
|
options={sellers.options}
|
||||||
|
getOptionLabel={(option) => option.label}
|
||||||
|
value={sellers.options.find(opt => localFilters.sellerId === opt.seller.id.toString()) || null}
|
||||||
|
onChange={(_, newValue) => {
|
||||||
|
updateLocalFilter('sellerId', newValue?.seller.id.toString() || null);
|
||||||
|
updateLocalFilter('sellerName', newValue?.seller.name || null);
|
||||||
|
}}
|
||||||
|
loading={sellers.isLoading}
|
||||||
|
renderInput={(params) => <TextField {...params} label="Vendedor" placeholder="Selecione vendedor" />}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Parceiro */}
|
||||||
|
<Autocomplete
|
||||||
|
size="small"
|
||||||
|
options={partners.options}
|
||||||
|
getOptionLabel={(option) => option.label}
|
||||||
|
value={partners.options.find(opt => localFilters.partnerId === opt.partner?.id?.toString()) || null}
|
||||||
|
onChange={(_, newValue) => {
|
||||||
|
updateLocalFilter('partnerId', newValue?.partner?.id?.toString() || null);
|
||||||
|
updateLocalFilter('partnerName', newValue?.partner?.nome || null);
|
||||||
|
}}
|
||||||
|
onInputChange={(_, val) => setPartnerSearchTerm(val)}
|
||||||
|
loading={partners.isLoading}
|
||||||
|
renderInput={(params) => <TextField {...params} label="Parceiro" placeholder="Buscar parceiro..." />}
|
||||||
|
noOptionsText={partnerSearchTerm.length < 2 ? 'Digite 2+ caracteres' : 'Nenhum parceiro'}
|
||||||
|
filterOptions={(x) => x}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Produto */}
|
||||||
|
<Autocomplete
|
||||||
|
size="small"
|
||||||
|
options={products.options}
|
||||||
|
getOptionLabel={(option) => option.label}
|
||||||
|
value={products.options.find(opt => localFilters.productId === opt.product?.id) || null}
|
||||||
|
onChange={(_, newValue) => {
|
||||||
|
updateLocalFilter('productId', newValue?.product?.id || null);
|
||||||
|
updateLocalFilter('productName', newValue?.product?.description || null);
|
||||||
|
}}
|
||||||
|
onInputChange={(_, val) => setProductSearchTerm(val)}
|
||||||
|
loading={products.isLoading}
|
||||||
|
renderInput={(params) => <TextField {...params} label="Produto" placeholder="Buscar produto..." />}
|
||||||
|
noOptionsText={productSearchTerm.length < 2 ? 'Digite 2+ caracteres' : 'Nenhum produto'}
|
||||||
|
filterOptions={(x) => x}
|
||||||
|
/>
|
||||||
|
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<Box sx={{ p: 2, borderTop: '1px solid', borderColor: 'divider', display: 'flex', gap: 2 }}>
|
||||||
|
<Button fullWidth variant="outlined" color="inherit" onClick={handleReset}>Limpar</Button>
|
||||||
|
<Button fullWidth variant="contained" color="primary" onClick={() => { handleFilter(); setShowAdvancedFilters(false); }}>Aplicar</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Drawer>
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -16,12 +16,15 @@ export const useOrderFilters = () => {
|
||||||
sellerId: parseAsString,
|
sellerId: parseAsString,
|
||||||
customerName: parseAsString,
|
customerName: parseAsString,
|
||||||
customerId: parseAsInteger,
|
customerId: parseAsInteger,
|
||||||
|
partnerName: parseAsString,
|
||||||
|
partnerId: parseAsString,
|
||||||
|
|
||||||
codfilial: parseAsArrayOf(parseAsString, ','),
|
codfilial: parseAsArrayOf(parseAsString, ','),
|
||||||
codusur2: parseAsArrayOf(parseAsString, ','),
|
codusur2: parseAsArrayOf(parseAsString, ','),
|
||||||
store: parseAsArrayOf(parseAsString, ','),
|
store: parseAsArrayOf(parseAsString, ','),
|
||||||
orderId: parseAsInteger,
|
orderId: parseAsInteger,
|
||||||
productId: parseAsInteger,
|
productId: parseAsInteger,
|
||||||
|
productName: parseAsString,
|
||||||
stockId: parseAsArrayOf(parseAsString, ','),
|
stockId: parseAsArrayOf(parseAsString, ','),
|
||||||
|
|
||||||
hasPreBox: parseAsBoolean.withDefault(false),
|
hasPreBox: parseAsBoolean.withDefault(false),
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { orderService } from '../api/order.service';
|
||||||
|
|
||||||
|
export interface Partner {
|
||||||
|
id: number;
|
||||||
|
cpf: string;
|
||||||
|
nome: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePartners(searchTerm: string) {
|
||||||
|
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(searchTerm);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setDebouncedSearchTerm(searchTerm);
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [searchTerm]);
|
||||||
|
|
||||||
|
const isEnabled = debouncedSearchTerm.length >= 2;
|
||||||
|
|
||||||
|
const query = useQuery({
|
||||||
|
queryKey: ['partners', debouncedSearchTerm],
|
||||||
|
queryFn: () => orderService.findPartners(debouncedSearchTerm),
|
||||||
|
enabled: isEnabled,
|
||||||
|
staleTime: 1000 * 60 * 5,
|
||||||
|
retry: 1,
|
||||||
|
retryOnMount: false,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const options =
|
||||||
|
query.data?.map((partner, index) => ({
|
||||||
|
value: partner.id.toString(),
|
||||||
|
label: partner.nome,
|
||||||
|
id: `partner-${partner.id}-${index}`,
|
||||||
|
partner: partner,
|
||||||
|
})) ?? [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
...query,
|
||||||
|
options,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { orderService } from '../api/order.service';
|
||||||
|
|
||||||
|
export interface Product {
|
||||||
|
id: number;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useProducts(searchTerm: string) {
|
||||||
|
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(searchTerm);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setDebouncedSearchTerm(searchTerm);
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [searchTerm]);
|
||||||
|
|
||||||
|
const isEnabled = debouncedSearchTerm.length >= 2;
|
||||||
|
|
||||||
|
const query = useQuery({
|
||||||
|
queryKey: ['products', debouncedSearchTerm],
|
||||||
|
queryFn: () => orderService.findProducts(debouncedSearchTerm),
|
||||||
|
enabled: isEnabled,
|
||||||
|
staleTime: 1000 * 60 * 5,
|
||||||
|
retry: 1,
|
||||||
|
retryOnMount: false,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const options =
|
||||||
|
query.data?.map((product, index) => ({
|
||||||
|
value: product.id.toString(),
|
||||||
|
label: product.description,
|
||||||
|
id: `product-${product.id}-${index}`,
|
||||||
|
product: product,
|
||||||
|
})) ?? [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
...query,
|
||||||
|
options,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,8 @@ import { z } from 'zod';
|
||||||
import { storeSchema } from './store.schema';
|
import { storeSchema } from './store.schema';
|
||||||
import { customerSchema } from './customer.schema';
|
import { customerSchema } from './customer.schema';
|
||||||
import { sellerSchema } from './seller.schema';
|
import { sellerSchema } from './seller.schema';
|
||||||
|
import { partnerSchema } from './partner.schema';
|
||||||
|
import { productSchema } from './product.schema';
|
||||||
import { createApiSchema } from './api-response.schema';
|
import { createApiSchema } from './api-response.schema';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -39,3 +41,15 @@ export const customersResponseSchema = createApiSchema(z.array(customerSchema));
|
||||||
* Formato: { success: boolean, data: Seller[] }
|
* Formato: { success: boolean, data: Seller[] }
|
||||||
*/
|
*/
|
||||||
export const sellersResponseSchema = createApiSchema(z.array(sellerSchema));
|
export const sellersResponseSchema = createApiSchema(z.array(sellerSchema));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schema para validar a resposta da API ao buscar parceiros.
|
||||||
|
* Formato: { success: boolean, data: Partner[] }
|
||||||
|
*/
|
||||||
|
export const partnersResponseSchema = createApiSchema(z.array(partnerSchema));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schema para validar a resposta da API ao buscar produtos.
|
||||||
|
* Formato: { success: boolean, data: Product[] }
|
||||||
|
*/
|
||||||
|
export const productsResponseSchema = createApiSchema(z.array(productSchema));
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ export const findOrdersSchema = z.object({
|
||||||
minute: z.coerce.number().optional(),
|
minute: z.coerce.number().optional(),
|
||||||
|
|
||||||
partnerId: z.string().optional(),
|
partnerId: z.string().optional(),
|
||||||
|
partnerName: z.string().optional(),
|
||||||
codusur2: z.string().optional(),
|
codusur2: z.string().optional(),
|
||||||
customerName: z.string().optional(),
|
customerName: z.string().optional(),
|
||||||
stockId: z.union([z.string(), z.array(z.string())]).optional(),
|
stockId: z.union([z.string(), z.array(z.string())]).optional(),
|
||||||
|
|
@ -27,6 +28,7 @@ export const findOrdersSchema = z.object({
|
||||||
orderId: z.coerce.number().optional(),
|
orderId: z.coerce.number().optional(),
|
||||||
invoiceId: z.coerce.number().optional(),
|
invoiceId: z.coerce.number().optional(),
|
||||||
productId: z.coerce.number().optional(),
|
productId: z.coerce.number().optional(),
|
||||||
|
productName: z.string().optional(),
|
||||||
|
|
||||||
createDateIni: z.string().optional(),
|
createDateIni: z.string().optional(),
|
||||||
createDateEnd: z.string().optional(),
|
createDateEnd: z.string().optional(),
|
||||||
|
|
@ -98,19 +100,31 @@ const formatValueToString = (val: any): string => {
|
||||||
*/
|
*/
|
||||||
export const orderApiParamsSchema = findOrdersSchema
|
export const orderApiParamsSchema = findOrdersSchema
|
||||||
.transform((filters) => {
|
.transform((filters) => {
|
||||||
// Remove customerName quando customerId existe (evita redundância)
|
const {
|
||||||
const { customerName, customerId, ...rest } = filters;
|
productName,
|
||||||
return customerId ? { customerId, ...rest } : filters;
|
partnerName,
|
||||||
|
customerName,
|
||||||
|
customerId,
|
||||||
|
...rest
|
||||||
|
} = filters;
|
||||||
|
|
||||||
|
const queryParams: any = { ...rest };
|
||||||
|
|
||||||
|
if (customerId) queryParams.customerId = customerId;
|
||||||
|
else if (customerName) queryParams.customerName = customerName;
|
||||||
|
|
||||||
|
if (filters.partnerId) queryParams.partnerId = filters.partnerId;
|
||||||
|
if (filters.productId) queryParams.productId = filters.productId;
|
||||||
|
|
||||||
|
return queryParams;
|
||||||
})
|
})
|
||||||
.transform((filters) => {
|
.transform((filters) => {
|
||||||
// Mapeamento de chaves que precisam ser renomeadas
|
|
||||||
const keyMap: Record<string, string> = {
|
const keyMap: Record<string, string> = {
|
||||||
store: 'codfilial',
|
store: 'codfilial',
|
||||||
};
|
};
|
||||||
|
|
||||||
return Object.entries(filters).reduce(
|
return Object.entries(filters).reduce(
|
||||||
(acc, [key, value]) => {
|
(acc, [key, value]) => {
|
||||||
// Early return: ignora valores vazios
|
|
||||||
if (isEmptyValue(value)) return acc;
|
if (isEmptyValue(value)) return acc;
|
||||||
|
|
||||||
const apiKey = keyMap[key] ?? key;
|
const apiKey = keyMap[key] ?? key;
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,16 @@ export { sellerSchema } from './seller.schema';
|
||||||
export type { Seller } from './seller.schema';
|
export type { Seller } from './seller.schema';
|
||||||
export { sellersResponseSchema } from './api-responses.schema';
|
export { sellersResponseSchema } from './api-responses.schema';
|
||||||
|
|
||||||
|
// Schema de parceiros
|
||||||
|
export { partnerSchema } from './partner.schema';
|
||||||
|
export type { Partner } from './partner.schema';
|
||||||
|
export { partnersResponseSchema } from './api-responses.schema';
|
||||||
|
|
||||||
|
// Schema de produtos
|
||||||
|
export { productSchema } from './product.schema';
|
||||||
|
export type { Product } from './product.schema';
|
||||||
|
export { productsResponseSchema } from './api-responses.schema';
|
||||||
|
|
||||||
// Schema de itens de pedido
|
// Schema de itens de pedido
|
||||||
export { orderItemSchema, orderItemsResponseSchema } from './order.item.schema';
|
export { orderItemSchema, orderItemsResponseSchema } from './order.item.schema';
|
||||||
export type { OrderItem, OrderItemsResponse } from './order.item.schema';
|
export type { OrderItem, OrderItemsResponse } from './order.item.schema';
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schema Zod para validar um parceiro.
|
||||||
|
*/
|
||||||
|
export const partnerSchema = z.object({
|
||||||
|
id: z.number(),
|
||||||
|
cpf: z.string(),
|
||||||
|
nome: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type para parceiro inferido do schema.
|
||||||
|
*/
|
||||||
|
export type Partner = z.infer<typeof partnerSchema>;
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schema Zod para validar um produto na busca.
|
||||||
|
*/
|
||||||
|
export const productSchema = z.object({
|
||||||
|
id: z.number(),
|
||||||
|
description: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type para produto inferido do schema.
|
||||||
|
*/
|
||||||
|
export type Product = z.infer<typeof productSchema>;
|
||||||
Loading…
Reference in New Issue