Compare commits
No commits in common. "5558ca6a0d118c659cf6bc1669a852ec3db8332a" and "2e940404818d14a6d28acb4c5e1ecd972ae0b9ba" have entirely different histories.
5558ca6a0d
...
2e94040481
|
|
@ -0,0 +1,64 @@
|
|||
import { Avatar, Box, Typography, Grid, Stack } from '@mui/material';
|
||||
import * as dropdownData from './data';
|
||||
import Link from 'next/link';
|
||||
import React from 'react';
|
||||
|
||||
const AppLinks = () => {
|
||||
return (
|
||||
<Grid container spacing={3} mb={4}>
|
||||
{dropdownData.appsLink.map((links) => (
|
||||
<Grid size={{ lg: 6 }} key={links.title}>
|
||||
<Link href={links.href} className="hover-text-primary">
|
||||
<Stack direction="row" spacing={2}>
|
||||
<Box
|
||||
minWidth="45px"
|
||||
height="45px"
|
||||
bgcolor="grey.100"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
<Avatar
|
||||
src={links.avatar}
|
||||
alt={links.avatar}
|
||||
sx={{
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: 0,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
fontWeight={600}
|
||||
color="textPrimary"
|
||||
noWrap
|
||||
className="text-hover"
|
||||
sx={{
|
||||
width: '240px',
|
||||
}}
|
||||
>
|
||||
{links.title}
|
||||
</Typography>
|
||||
<Typography
|
||||
color="textSecondary"
|
||||
variant="subtitle2"
|
||||
fontSize="12px"
|
||||
sx={{
|
||||
width: '240px',
|
||||
}}
|
||||
noWrap
|
||||
>
|
||||
{links.subtext}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Link>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppLinks;
|
||||
|
|
@ -12,8 +12,11 @@ import DarkModeIcon from '@mui/icons-material/DarkMode';
|
|||
import LightModeIcon from '@mui/icons-material/LightMode';
|
||||
import { useCustomizerStore } from '../store/useCustomizerStore';
|
||||
|
||||
import Notifications from './Notification';
|
||||
import Profile from './Profile';
|
||||
import Search from './Search';
|
||||
import Navigation from './Navigation';
|
||||
import MobileRightSidebar from './MobileRightSidebar';
|
||||
|
||||
const AppBarStyled = styled(AppBar)(({ theme }) => ({
|
||||
boxShadow: 'none',
|
||||
|
|
@ -30,6 +33,7 @@ const ToolbarStyled = styled(Toolbar)(({ theme }) => ({
|
|||
|
||||
const Header = () => {
|
||||
const lgUp = useMediaQuery((theme: Theme) => theme.breakpoints.up('lg'));
|
||||
const lgDown = useMediaQuery((theme: Theme) => theme.breakpoints.down('lg'));
|
||||
|
||||
const {
|
||||
activeMode,
|
||||
|
|
@ -47,7 +51,7 @@ const Header = () => {
|
|||
<AppBarStyled position="sticky" color="default">
|
||||
<ToolbarStyled>
|
||||
{/* ------------------------------------------- */}
|
||||
{/* Toggle Button Sidebar */}
|
||||
{/* Toggle Button Sidebar (Fluxo 2) */}
|
||||
{/* ------------------------------------------- */}
|
||||
<IconButton
|
||||
color="inherit"
|
||||
|
|
@ -58,9 +62,10 @@ const Header = () => {
|
|||
</IconButton>
|
||||
|
||||
{/* ------------------------------------------- */}
|
||||
{/* Search */}
|
||||
{/* Search & Navigation */}
|
||||
{/* ------------------------------------------- */}
|
||||
<Search />
|
||||
{lgUp && <Navigation />}
|
||||
|
||||
<Box flexGrow={1} />
|
||||
|
||||
|
|
@ -79,9 +84,12 @@ const Header = () => {
|
|||
{activeMode === 'light' ? <DarkModeIcon /> : <LightModeIcon />}
|
||||
</IconButton>
|
||||
|
||||
<Notifications />
|
||||
|
||||
{/* ------------------------------------------- */}
|
||||
{/* Profile */}
|
||||
{/* Mobile Right Sidebar & Profile */}
|
||||
{/* ------------------------------------------- */}
|
||||
{lgDown && <MobileRightSidebar />}
|
||||
<Profile />
|
||||
</Stack>
|
||||
</ToolbarStyled>
|
||||
|
|
@ -90,4 +98,3 @@ const Header = () => {
|
|||
};
|
||||
|
||||
export default Header;
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,128 @@
|
|||
import React, { useState } from 'react';
|
||||
import {
|
||||
IconApps,
|
||||
IconChevronDown,
|
||||
IconChevronUp,
|
||||
IconGridDots,
|
||||
IconMail,
|
||||
IconMessages,
|
||||
} from '@tabler/icons-react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Drawer,
|
||||
IconButton,
|
||||
List,
|
||||
ListItemButton,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Collapse,
|
||||
} from '@mui/material';
|
||||
|
||||
import Link from 'next/link';
|
||||
import AppLinks from './AppLinks';
|
||||
|
||||
const MobileRightSidebar = () => {
|
||||
const [showDrawer, setShowDrawer] = useState(false);
|
||||
|
||||
const [open, setOpen] = React.useState(true);
|
||||
|
||||
const handleClick = () => {
|
||||
setOpen(!open);
|
||||
};
|
||||
|
||||
const cartContent = (
|
||||
<Box>
|
||||
{/* ------------------------------------------- */}
|
||||
{/* Apps Content */}
|
||||
{/* ------------------------------------------- */}
|
||||
<Box px={1}>
|
||||
<List
|
||||
sx={{ width: '100%', maxWidth: 360, bgcolor: 'background.paper' }}
|
||||
component="nav"
|
||||
aria-labelledby="nested-list-subheader"
|
||||
>
|
||||
<ListItemButton component={Link} href="/apps/chats">
|
||||
<ListItemIcon sx={{ minWidth: 35 }}>
|
||||
<IconMessages size="21" stroke="1.5" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
<Typography variant="subtitle2" fontWeight={600}>
|
||||
Chats
|
||||
</Typography>
|
||||
</ListItemText>
|
||||
</ListItemButton>
|
||||
<ListItemButton component={Link} href="/apps/email">
|
||||
<ListItemIcon sx={{ minWidth: 35 }}>
|
||||
<IconMail size="21" stroke="1.5" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
<Typography variant="subtitle2" fontWeight={600}>
|
||||
Email
|
||||
</Typography>
|
||||
</ListItemText>
|
||||
</ListItemButton>
|
||||
<ListItemButton onClick={handleClick}>
|
||||
<ListItemIcon sx={{ minWidth: 35 }}>
|
||||
<IconApps size="21" stroke="1.5" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
<Typography variant="subtitle2" fontWeight={600}>
|
||||
Apps
|
||||
</Typography>
|
||||
</ListItemText>
|
||||
{open ? (
|
||||
<IconChevronDown size="21" stroke="1.5" />
|
||||
) : (
|
||||
<IconChevronUp size="21" stroke="1.5" />
|
||||
)}
|
||||
</ListItemButton>
|
||||
<Collapse in={open} timeout="auto" unmountOnExit>
|
||||
<Box px={4} pt={3} overflow="hidden">
|
||||
<AppLinks />
|
||||
</Box>
|
||||
</Collapse>
|
||||
</List>
|
||||
</Box>
|
||||
|
||||
<Box px={3} mt={3}></Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<IconButton
|
||||
size="large"
|
||||
color="inherit"
|
||||
onClick={() => setShowDrawer(true)}
|
||||
sx={{
|
||||
...(showDrawer && {
|
||||
color: 'primary.main',
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<IconGridDots size="21" stroke="1.5" />
|
||||
</IconButton>
|
||||
{/* ------------------------------------------- */}
|
||||
{/* Cart Sidebar */}
|
||||
{/* ------------------------------------------- */}
|
||||
<Drawer
|
||||
anchor="right"
|
||||
open={showDrawer}
|
||||
onClose={() => setShowDrawer(false)}
|
||||
PaperProps={{ sx: { width: '300px' } }}
|
||||
>
|
||||
<Box p={3} pb={0}>
|
||||
<Typography variant="h5" fontWeight={600}>
|
||||
Navigation
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* component */}
|
||||
{cartContent}
|
||||
</Drawer>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default MobileRightSidebar;
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
import { useState } from 'react';
|
||||
import { Box, Menu, Typography, Button, Divider, Grid } from '@mui/material';
|
||||
import Link from 'next/link';
|
||||
import { IconChevronDown, IconHelp } from '@tabler/icons-react';
|
||||
import AppLinks from './AppLinks';
|
||||
|
||||
const AppDD = () => {
|
||||
const [anchorEl2, setAnchorEl2] = useState(null);
|
||||
|
||||
const handleClick2 = (event: any) => {
|
||||
setAnchorEl2(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleClose2 = () => {
|
||||
setAnchorEl2(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box>
|
||||
<Button
|
||||
aria-label="show 11 new notifications"
|
||||
color="inherit"
|
||||
variant="text"
|
||||
aria-controls="msgs-menu"
|
||||
aria-haspopup="true"
|
||||
sx={{
|
||||
bgcolor: anchorEl2 ? 'primary.light' : '',
|
||||
color: anchorEl2
|
||||
? 'primary.main'
|
||||
: (theme) => theme.palette.text.secondary,
|
||||
fontSize: '13px',
|
||||
}}
|
||||
onClick={handleClick2}
|
||||
endIcon={
|
||||
<IconChevronDown
|
||||
size="15"
|
||||
style={{ marginLeft: '-5px', marginTop: '2px' }}
|
||||
/>
|
||||
}
|
||||
>
|
||||
Apps
|
||||
</Button>
|
||||
{/* ------------------------------------------- */}
|
||||
{/* Message Dropdown */}
|
||||
{/* ------------------------------------------- */}
|
||||
<Menu
|
||||
id="msgs-menu"
|
||||
anchorEl={anchorEl2}
|
||||
keepMounted
|
||||
open={Boolean(anchorEl2)}
|
||||
onClose={handleClose2}
|
||||
anchorOrigin={{ horizontal: 'left', vertical: 'bottom' }}
|
||||
transformOrigin={{ horizontal: 'left', vertical: 'top' }}
|
||||
sx={{
|
||||
'& .MuiMenu-paper': {
|
||||
width: '850px',
|
||||
},
|
||||
'& .MuiMenu-paper ul': {
|
||||
p: 0,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Grid container>
|
||||
<Grid size={{ sm: 8 }} display="flex">
|
||||
<Box p={4} pr={0} pb={3}>
|
||||
<AppLinks />
|
||||
<Divider />
|
||||
<Box
|
||||
sx={{
|
||||
display: {
|
||||
xs: 'none',
|
||||
sm: 'flex',
|
||||
},
|
||||
}}
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
pt={2}
|
||||
pr={4}
|
||||
>
|
||||
<Link href="/faq">
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
fontWeight="600"
|
||||
color="textPrimary"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
gap="4px"
|
||||
>
|
||||
<IconHelp width={24} />
|
||||
Frequently Asked Questions
|
||||
</Typography>
|
||||
</Link>
|
||||
<Button variant="contained" color="primary">
|
||||
Check
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
<Divider orientation="vertical" />
|
||||
</Grid>
|
||||
<Grid size={{ sm: 4 }}>
|
||||
<Box p={4}></Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Menu>
|
||||
</Box>
|
||||
<Button
|
||||
color="inherit"
|
||||
sx={{
|
||||
color: (theme) => theme.palette.text.secondary,
|
||||
fontSize: '13px',
|
||||
}}
|
||||
variant="text"
|
||||
href="/apps/chats"
|
||||
component={Link}
|
||||
>
|
||||
Chat
|
||||
</Button>
|
||||
<Button
|
||||
color="inherit"
|
||||
sx={{
|
||||
color: (theme) => theme.palette.text.secondary,
|
||||
fontSize: '13px',
|
||||
}}
|
||||
variant="text"
|
||||
href="/apps/email"
|
||||
component={Link}
|
||||
>
|
||||
Email
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppDD;
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
import React, { useState } from 'react';
|
||||
import {
|
||||
IconButton,
|
||||
Box,
|
||||
Badge,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Avatar,
|
||||
Typography,
|
||||
Button,
|
||||
Chip,
|
||||
} from '@mui/material';
|
||||
import * as dropdownData from './data';
|
||||
import { Scrollbar } from '@/shared/components';
|
||||
|
||||
import { IconBellRinging } from '@tabler/icons-react';
|
||||
import { Stack } from '@mui/system';
|
||||
import Link from 'next/link';
|
||||
|
||||
const Notifications = () => {
|
||||
const [anchorEl2, setAnchorEl2] = useState(null);
|
||||
|
||||
const handleClick2 = (event: any) => {
|
||||
setAnchorEl2(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleClose2 = () => {
|
||||
setAnchorEl2(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<IconButton
|
||||
size="large"
|
||||
aria-label="show 11 new notifications"
|
||||
color="inherit"
|
||||
aria-controls="msgs-menu"
|
||||
aria-haspopup="true"
|
||||
sx={{
|
||||
color: anchorEl2 ? 'primary.main' : 'text.secondary',
|
||||
}}
|
||||
onClick={handleClick2}
|
||||
>
|
||||
<Badge variant="dot" color="primary">
|
||||
<IconBellRinging size="21" stroke="1.5" />
|
||||
</Badge>
|
||||
</IconButton>
|
||||
{/* ------------------------------------------- */}
|
||||
{/* Message Dropdown */}
|
||||
{/* ------------------------------------------- */}
|
||||
<Menu
|
||||
id="msgs-menu"
|
||||
anchorEl={anchorEl2}
|
||||
keepMounted
|
||||
open={Boolean(anchorEl2)}
|
||||
onClose={handleClose2}
|
||||
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
|
||||
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
|
||||
sx={{
|
||||
'& .MuiMenu-paper': {
|
||||
width: '360px',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
py={2}
|
||||
px={4}
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
<Typography variant="h6">Notifications</Typography>
|
||||
<Chip label="5 new" color="primary" size="small" />
|
||||
</Stack>
|
||||
<Scrollbar sx={{ height: '385px' }}>
|
||||
{dropdownData.notifications.map((notification) => (
|
||||
<Box key={notification.id}>
|
||||
<MenuItem sx={{ py: 2, px: 4 }}>
|
||||
<Stack direction="row" spacing={2}>
|
||||
<Avatar
|
||||
src={notification.avatar}
|
||||
alt={notification.avatar}
|
||||
sx={{
|
||||
width: 48,
|
||||
height: 48,
|
||||
}}
|
||||
/>
|
||||
<Box>
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
color="textPrimary"
|
||||
fontWeight={600}
|
||||
noWrap
|
||||
sx={{
|
||||
width: '240px',
|
||||
}}
|
||||
>
|
||||
{notification.title}
|
||||
</Typography>
|
||||
<Typography
|
||||
color="textSecondary"
|
||||
variant="subtitle2"
|
||||
sx={{
|
||||
width: '240px',
|
||||
}}
|
||||
noWrap
|
||||
>
|
||||
{notification.subtitle}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
</MenuItem>
|
||||
</Box>
|
||||
))}
|
||||
</Scrollbar>
|
||||
<Box p={3} pb={1}>
|
||||
<Button
|
||||
href="/apps/email"
|
||||
variant="outlined"
|
||||
component={Link}
|
||||
color="primary"
|
||||
fullWidth
|
||||
>
|
||||
See all Notifications
|
||||
</Button>
|
||||
</Box>
|
||||
</Menu>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Notifications;
|
||||
|
|
@ -5,9 +5,11 @@ import {
|
|||
Menu,
|
||||
Avatar,
|
||||
Typography,
|
||||
Divider,
|
||||
Button,
|
||||
IconButton,
|
||||
} from '@mui/material';
|
||||
import { IconMail } from '@tabler/icons-react';
|
||||
import { Stack } from '@mui/system';
|
||||
|
||||
import { useAuthStore } from '../../login/store/useAuthStore';
|
||||
|
|
@ -25,18 +27,10 @@ const Profile = () => {
|
|||
setAnchorEl2(null);
|
||||
};
|
||||
|
||||
// Gera as iniciais do nome
|
||||
const getIniciais = () => {
|
||||
if (!user?.nome) return user?.userName?.[0]?.toUpperCase() || 'U';
|
||||
const partes = user.nome.trim().split(' ').filter(Boolean);
|
||||
if (partes.length === 1) return partes[0][0].toUpperCase();
|
||||
return (partes[0][0] + partes[partes.length - 1][0]).toUpperCase();
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<IconButton
|
||||
aria-label="menu do perfil"
|
||||
aria-label="show 11 new notifications"
|
||||
color="inherit"
|
||||
aria-controls="msgs-menu"
|
||||
aria-haspopup="true"
|
||||
|
|
@ -54,11 +48,11 @@ const Profile = () => {
|
|||
bgcolor: 'primary.main',
|
||||
}}
|
||||
>
|
||||
{getIniciais()}
|
||||
{user?.nome?.[0] || user?.userName?.[0] || 'U'}
|
||||
</Avatar>
|
||||
</IconButton>
|
||||
{/* ------------------------------------------- */}
|
||||
{/* Profile Dropdown */}
|
||||
{/* Message Dropdown */}
|
||||
{/* ------------------------------------------- */}
|
||||
<Menu
|
||||
id="msgs-menu"
|
||||
|
|
@ -70,19 +64,17 @@ const Profile = () => {
|
|||
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
|
||||
sx={{
|
||||
'& .MuiMenu-paper': {
|
||||
width: '320px',
|
||||
p: 3,
|
||||
width: '360px',
|
||||
p: 4,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" fontWeight={600}>
|
||||
Meu Perfil
|
||||
</Typography>
|
||||
<Stack direction="row" py={2} spacing={2} alignItems="center">
|
||||
<Typography variant="h5">User Profile</Typography>
|
||||
<Stack direction="row" py={3} spacing={2} alignItems="center">
|
||||
<Avatar
|
||||
sx={{ width: 64, height: 64, bgcolor: 'primary.main', fontSize: '1.5rem' }}
|
||||
sx={{ width: 95, height: 95, bgcolor: 'primary.main' }}
|
||||
>
|
||||
{getIniciais()}
|
||||
{user?.nome?.[0] || user?.userName?.[0] || 'U'}
|
||||
</Avatar>
|
||||
<Box>
|
||||
<Typography
|
||||
|
|
@ -92,36 +84,34 @@ const Profile = () => {
|
|||
>
|
||||
{user?.nome || user?.userName || 'Usuário'}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
{user?.nomeFilial || '-'}
|
||||
<Typography variant="subtitle2" color="textSecondary">
|
||||
{user?.nomeFilial || 'Sem filial'}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
Matrícula: {user?.matricula || '-'}
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
color="textSecondary"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
gap={1}
|
||||
>
|
||||
<IconMail width={15} height={15} />
|
||||
{user?.rca ? `RCA: ${user.rca}` : 'Sem e-mail'}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Box mt={2}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
component={Link}
|
||||
href="/dashboard/profile"
|
||||
fullWidth
|
||||
sx={{ mb: 1 }}
|
||||
>
|
||||
Ver Perfil Completo
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={logout}
|
||||
fullWidth
|
||||
>
|
||||
Sair
|
||||
</Button>
|
||||
</Box>
|
||||
</Menu>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Profile;
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,185 @@
|
|||
// Notifications dropdown
|
||||
|
||||
interface NotificationType {
|
||||
id: string;
|
||||
avatar: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
}
|
||||
|
||||
const notifications: NotificationType[] = [
|
||||
{
|
||||
id: '1',
|
||||
avatar: '/images/profile/user-2.jpg',
|
||||
title: 'Roman Joined the Team!',
|
||||
subtitle: 'Congratulate him',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
avatar: '/images/profile/user-3.jpg',
|
||||
title: 'New message received',
|
||||
subtitle: 'Salma sent you new message',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
avatar: '/images/profile/user-4.jpg',
|
||||
title: 'New Payment received',
|
||||
subtitle: 'Check your earnings',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
avatar: '/images/profile/user-5.jpg',
|
||||
title: 'Jolly completed tasks',
|
||||
subtitle: 'Assign her new tasks',
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
avatar: '/images/profile/user-6.jpg',
|
||||
title: 'Roman Joined the Team!',
|
||||
subtitle: 'Congratulate him',
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
avatar: '/images/profile/user-7.jpg',
|
||||
title: 'New message received',
|
||||
subtitle: 'Salma sent you new message',
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
avatar: '/images/profile/user-8.jpg',
|
||||
title: 'New Payment received',
|
||||
subtitle: 'Check your earnings',
|
||||
},
|
||||
{
|
||||
id: '8',
|
||||
avatar: '/images/profile/user-9.jpg',
|
||||
title: 'Jolly completed tasks',
|
||||
subtitle: 'Assign her new tasks',
|
||||
},
|
||||
];
|
||||
|
||||
//
|
||||
// Profile dropdown
|
||||
//
|
||||
interface ProfileType {
|
||||
href: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
icon: any;
|
||||
}
|
||||
const profile: ProfileType[] = [
|
||||
{
|
||||
href: '/dashboard/profile',
|
||||
title: 'My Profile',
|
||||
subtitle: 'Account Settings',
|
||||
icon: '/images/svgs/icon-account.svg',
|
||||
},
|
||||
{
|
||||
href: '/apps/email',
|
||||
title: 'My Inbox',
|
||||
subtitle: 'Messages & Emails',
|
||||
icon: '/images/svgs/icon-inbox.svg',
|
||||
},
|
||||
{
|
||||
href: '/apps/notes',
|
||||
title: 'My Tasks',
|
||||
subtitle: 'To-do and Daily Tasks',
|
||||
icon: '/images/svgs/icon-tasks.svg',
|
||||
},
|
||||
];
|
||||
|
||||
// apps dropdown
|
||||
|
||||
interface AppsLinkType {
|
||||
href: string;
|
||||
title: string;
|
||||
subtext: string;
|
||||
avatar: string;
|
||||
}
|
||||
|
||||
const appsLink: AppsLinkType[] = [
|
||||
{
|
||||
href: '/apps/chats',
|
||||
title: 'Chat Application',
|
||||
subtext: 'New messages arrived',
|
||||
avatar: '/images/svgs/icon-dd-chat.svg',
|
||||
},
|
||||
{
|
||||
href: '/apps/ecommerce/shop',
|
||||
title: 'eCommerce App',
|
||||
subtext: 'New stock available',
|
||||
avatar: '/images/svgs/icon-dd-cart.svg',
|
||||
},
|
||||
{
|
||||
href: '/apps/notes',
|
||||
title: 'Notes App',
|
||||
subtext: 'To-do and Daily tasks',
|
||||
avatar: '/images/svgs/icon-dd-invoice.svg',
|
||||
},
|
||||
{
|
||||
href: '/apps/contacts',
|
||||
title: 'Contact Application',
|
||||
subtext: '2 Unsaved Contacts',
|
||||
avatar: '/images/svgs/icon-dd-mobile.svg',
|
||||
},
|
||||
{
|
||||
href: '/apps/tickets',
|
||||
title: 'Tickets App',
|
||||
subtext: 'Submit tickets',
|
||||
avatar: '/images/svgs/icon-dd-lifebuoy.svg',
|
||||
},
|
||||
{
|
||||
href: '/apps/email',
|
||||
title: 'Email App',
|
||||
subtext: 'Get new emails',
|
||||
avatar: '/images/svgs/icon-dd-message-box.svg',
|
||||
},
|
||||
{
|
||||
href: '/apps/blog/post',
|
||||
title: 'Blog App',
|
||||
subtext: 'added new blog',
|
||||
avatar: '/images/svgs/icon-dd-application.svg',
|
||||
},
|
||||
];
|
||||
|
||||
interface LinkType {
|
||||
href: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
const pageLinks: LinkType[] = [
|
||||
{
|
||||
href: '/theme-pages/pricing',
|
||||
title: 'Pricing Page',
|
||||
},
|
||||
{
|
||||
href: '/auth/auth1/login',
|
||||
title: 'Authentication Design',
|
||||
},
|
||||
{
|
||||
href: '/auth/auth1/register',
|
||||
title: 'Register Now',
|
||||
},
|
||||
{
|
||||
href: '/404',
|
||||
title: '404 Error Page',
|
||||
},
|
||||
{
|
||||
href: '/apps/note',
|
||||
title: 'Notes App',
|
||||
},
|
||||
{
|
||||
href: '/apps/user-profile/profile',
|
||||
title: 'User Application',
|
||||
},
|
||||
{
|
||||
href: '/apps/blog/post',
|
||||
title: 'Blog Design',
|
||||
},
|
||||
{
|
||||
href: '/apps/ecommerce/checkout',
|
||||
title: 'Shopping Cart',
|
||||
},
|
||||
];
|
||||
|
||||
export { notifications, profile, pageLinks, appsLink };
|
||||
|
|
@ -20,7 +20,7 @@ export function AuthInitializer({ children }: { children: React.ReactNode }) {
|
|||
try {
|
||||
await loginService.refreshToken();
|
||||
|
||||
const profile = await profileService.obterColaboradorAtual();
|
||||
const profile = await profileService.getMe();
|
||||
setUser(mapToSafeProfile(profile));
|
||||
} catch (error) {
|
||||
console.warn('Sessão expirada ou inválida', error);
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useAuthStore } from '../store/useAuthStore';
|
||||
import { loginService } from '../services/login.service';
|
||||
import { profileService } from '../../profile/services/profile.service';
|
||||
import { clearAuthData } from '../utils/tokenRefresh';
|
||||
import { mapToSafeProfile } from '../utils/mappers';
|
||||
import { COLABORADOR_ATUAL_QUERY_KEY } from '../../profile';
|
||||
|
||||
export function useAuth() {
|
||||
const queryClient = useQueryClient();
|
||||
|
|
@ -17,11 +16,11 @@ export function useAuth() {
|
|||
mutationFn: loginService.login,
|
||||
onSuccess: async () => {
|
||||
try {
|
||||
const profile = await profileService.obterColaboradorAtual();
|
||||
const profile = await profileService.getMe();
|
||||
const safeProfile = mapToSafeProfile(profile);
|
||||
|
||||
setUser(safeProfile);
|
||||
queryClient.setQueryData(COLABORADOR_ATUAL_QUERY_KEY, profile);
|
||||
queryClient.setQueryData(['auth-me'], safeProfile);
|
||||
|
||||
router.push('/dashboard');
|
||||
} catch (error) {
|
||||
|
|
@ -31,6 +30,19 @@ export function useAuth() {
|
|||
},
|
||||
});
|
||||
|
||||
const useMe = () =>
|
||||
useQuery({
|
||||
queryKey: ['auth-me'],
|
||||
queryFn: async () => {
|
||||
const data = await profileService.getMe();
|
||||
const safeData = mapToSafeProfile(data);
|
||||
setUser(safeData);
|
||||
return safeData;
|
||||
},
|
||||
retry: false,
|
||||
staleTime: Infinity,
|
||||
});
|
||||
|
||||
const logout = async () => {
|
||||
try {
|
||||
await loginService.logout();
|
||||
|
|
@ -42,5 +54,5 @@ export function useAuth() {
|
|||
}
|
||||
};
|
||||
|
||||
return { loginMutation, logout };
|
||||
return { loginMutation, useMe, logout };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { z } from 'zod';
|
||||
import type { ReactNode } from 'react';
|
||||
import type { UserProfileDto } from '../../profile/types';
|
||||
import type { UserProfile } from '../../profile/types';
|
||||
|
||||
import {
|
||||
loginSchema,
|
||||
|
|
@ -38,9 +38,9 @@ export interface AuthLoginProps {
|
|||
}
|
||||
|
||||
export interface AuthState {
|
||||
user: UserProfileDto | null;
|
||||
user: UserProfile | null;
|
||||
isAuthenticated: boolean;
|
||||
setUser: (user: UserProfileDto | null) => void;
|
||||
setUser: (user: UserProfile | null) => void;
|
||||
logout: () => void;
|
||||
hydrate: () => void;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,16 @@
|
|||
import { UserProfileDto } from '../../profile/types';
|
||||
import { Colaborador } from '../../profile/domain/Colaborador';
|
||||
import { UserProfile } from '../../profile/types';
|
||||
|
||||
/**
|
||||
* Converte Colaborador (entidade) ou objeto raw para UserProfileDto.
|
||||
* Suporta ambos os formatos de propriedade (entidade DDD e DTO da API).
|
||||
*/
|
||||
export const mapToSafeProfile = (data: Colaborador | any): UserProfileDto => {
|
||||
export const mapToSafeProfile = (data: any): UserProfile => {
|
||||
return {
|
||||
matricula: data.matricula,
|
||||
userName: data.userName,
|
||||
nome: data.nome,
|
||||
codigoFilial: data.codigoFilial,
|
||||
nomeFilial: data.nomeFilial,
|
||||
rca: data.codigoRCA ?? data.rca,
|
||||
discountPercent: data.percentualDesconto ?? data._percentualDesconto ?? data.discountPercent,
|
||||
sectorId: data.codigoSetor ?? data.sectorId,
|
||||
sectorManagerId: data.sectorManagerId ?? 0,
|
||||
supervisorId: data.codigoSupervisor ?? data.supervisorId,
|
||||
rca: data.rca,
|
||||
discountPercent: data.discountPercent,
|
||||
sectorId: data.sectorId,
|
||||
sectorManagerId: data.sectorManagerId,
|
||||
supervisorId: data.supervisorId,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -15,8 +15,6 @@ import {
|
|||
storesResponseSchema,
|
||||
customersResponseSchema,
|
||||
sellersResponseSchema,
|
||||
partnersResponseSchema,
|
||||
productsResponseSchema,
|
||||
unwrapApiData,
|
||||
} from '../schemas/order.schema';
|
||||
import {
|
||||
|
|
@ -180,42 +178,5 @@ export const orderService = {
|
|||
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,28 +12,22 @@ import {
|
|||
Autocomplete,
|
||||
Paper,
|
||||
Tooltip,
|
||||
Collapse,
|
||||
CircularProgress,
|
||||
Badge,
|
||||
Drawer,
|
||||
IconButton,
|
||||
Typography,
|
||||
Divider,
|
||||
Stack,
|
||||
type AutocompleteRenderInputParams,
|
||||
} from '@mui/material';
|
||||
import { useStores } from '../store/useStores';
|
||||
import { useCustomers } from '../hooks/useCustomers';
|
||||
import { useSellers } from '../hooks/useSellers';
|
||||
import { usePartners } from '../hooks/usePartners';
|
||||
import { useProducts } from '../hooks/useProducts';
|
||||
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
||||
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
|
||||
import { AdapterMoment } from '@mui/x-date-pickers/AdapterMoment';
|
||||
import {
|
||||
Search as SearchIcon,
|
||||
RestartAlt as ResetIcon,
|
||||
Tune as TuneIcon,
|
||||
Close as CloseIcon,
|
||||
ExpandLess as ExpandLessIcon,
|
||||
ExpandMore as ExpandMoreIcon,
|
||||
} from '@mui/icons-material';
|
||||
import moment from 'moment';
|
||||
import 'moment/locale/pt-br';
|
||||
|
|
@ -45,10 +39,6 @@ interface LocalFilters {
|
|||
orderId: number | null;
|
||||
customerId: number | null;
|
||||
customerName: string | null;
|
||||
partnerId: string | null;
|
||||
partnerName: string | null;
|
||||
productId: number | null;
|
||||
productName: string | null;
|
||||
createDateIni: string | null;
|
||||
createDateEnd: string | null;
|
||||
store: string[] | null;
|
||||
|
|
@ -64,10 +54,6 @@ const getInitialLocalFilters = (
|
|||
orderId: urlFilters.orderId ?? null,
|
||||
customerId: urlFilters.customerId ?? 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,
|
||||
createDateEnd: urlFilters.createDateEnd ?? null,
|
||||
store: urlFilters.store ?? null,
|
||||
|
|
@ -87,10 +73,6 @@ export const SearchBar = () => {
|
|||
const sellers = useSellers();
|
||||
const [customerSearchTerm, setCustomerSearchTerm] = useState('');
|
||||
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 { isFetching } = useOrders();
|
||||
const [touchedFields, setTouchedFields] = useState<{
|
||||
|
|
@ -112,8 +94,6 @@ export const SearchBar = () => {
|
|||
const handleReset = useCallback(() => {
|
||||
setTouchedFields({});
|
||||
setCustomerSearchTerm('');
|
||||
setPartnerSearchTerm('');
|
||||
setProductSearchTerm('');
|
||||
|
||||
const resetState = {
|
||||
status: null,
|
||||
|
|
@ -123,6 +103,7 @@ export const SearchBar = () => {
|
|||
codusur2: null,
|
||||
store: null,
|
||||
orderId: null,
|
||||
productId: null,
|
||||
stockId: null,
|
||||
hasPreBox: false,
|
||||
includeCheckout: false,
|
||||
|
|
@ -131,10 +112,6 @@ export const SearchBar = () => {
|
|||
searchTriggered: false,
|
||||
customerId: null,
|
||||
customerName: null,
|
||||
partnerId: null,
|
||||
partnerName: null,
|
||||
productId: null,
|
||||
productName: null,
|
||||
};
|
||||
|
||||
setLocalFilters(getInitialLocalFilters(resetState));
|
||||
|
|
@ -190,32 +167,17 @@ export const SearchBar = () => {
|
|||
touchedFields.createDateIni && !localFilters.createDateIni;
|
||||
const showDateEndError = touchedFields.createDateEnd && dateError;
|
||||
|
||||
// Contador de filtros ativos no Drawer
|
||||
const activeDrawerFiltersCount = [
|
||||
localFilters.status,
|
||||
localFilters.customerId,
|
||||
// Contador de filtros avançados ativos
|
||||
const advancedFiltersCount = [
|
||||
localFilters.store?.length,
|
||||
localFilters.stockId?.length,
|
||||
localFilters.sellerId,
|
||||
localFilters.partnerId,
|
||||
localFilters.productId,
|
||||
].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 (
|
||||
<Paper
|
||||
sx={{
|
||||
p: { xs: 2, md: 2 },
|
||||
p: { xs: 2, md: 3 },
|
||||
mb: 2,
|
||||
bgcolor: 'background.paper',
|
||||
borderRadius: 2,
|
||||
|
|
@ -224,8 +186,10 @@ export const SearchBar = () => {
|
|||
elevation={0}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<Grid container spacing={2} alignItems="center">
|
||||
{/* Nº do Pedido */}
|
||||
<Grid container spacing={{ xs: 1.5, md: 2 }} alignItems="flex-end">
|
||||
{/* --- Primary Filters (Always Visible) --- */}
|
||||
|
||||
{/* Campo de Texto Simples (Nº Pedido) */}
|
||||
<Grid size={{ xs: 12, sm: 6, md: 2 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
|
|
@ -243,227 +207,404 @@ export const SearchBar = () => {
|
|||
/>
|
||||
</Grid>
|
||||
|
||||
{/* Datas */}
|
||||
<Grid size={{ xs: 12, sm: 12, md: 5 }}>
|
||||
<LocalizationProvider dateAdapter={AdapterMoment} adapterLocale="pt-br">
|
||||
<Box display="flex" gap={1.5}>
|
||||
<DatePicker
|
||||
label="Data Inicial"
|
||||
value={localFilters.createDateIni ? moment(localFilters.createDateIni, 'YYYY-MM-DD') : null}
|
||||
onChange={(date) => {
|
||||
setTouchedFields(prev => ({ ...prev, createDateIni: true }));
|
||||
updateLocalFilter('createDateIni', date ? date.format('YYYY-MM-DD') : null);
|
||||
}}
|
||||
format="DD/MM/YYYY"
|
||||
slotProps={{
|
||||
textField: {
|
||||
size: 'small',
|
||||
fullWidth: true,
|
||||
required: true,
|
||||
error: showDateIniError,
|
||||
helperText: showDateIniError ? 'Obrigatório' : '',
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<DatePicker
|
||||
label="Data Final"
|
||||
value={localFilters.createDateEnd ? moment(localFilters.createDateEnd, 'YYYY-MM-DD') : null}
|
||||
onChange={(date) => {
|
||||
setTouchedFields(prev => ({ ...prev, createDateEnd: true }));
|
||||
updateLocalFilter('createDateEnd', date ? date.format('YYYY-MM-DD') : null);
|
||||
}}
|
||||
format="DD/MM/YYYY"
|
||||
slotProps={{
|
||||
textField: {
|
||||
size: 'small',
|
||||
fullWidth: true,
|
||||
error: !!showDateEndError,
|
||||
helperText: showDateEndError || '',
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</LocalizationProvider>
|
||||
</Grid>
|
||||
|
||||
{/* Ações */}
|
||||
<Grid size={{ xs: 12, md: 5 }} sx={{ display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="inherit"
|
||||
size="small"
|
||||
onClick={handleReset}
|
||||
startIcon={<ResetIcon />}
|
||||
sx={{ textTransform: 'none', minWidth: 90 }}
|
||||
>
|
||||
Limpar
|
||||
</Button>
|
||||
|
||||
<Badge badgeContent={activeDrawerFiltersCount} color="primary">
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
size="small"
|
||||
onClick={toggleDrawer(true)}
|
||||
startIcon={<TuneIcon />}
|
||||
sx={{ textTransform: 'none', minWidth: 100 }}
|
||||
>
|
||||
Filtros
|
||||
</Button>
|
||||
</Badge>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
size="small"
|
||||
onClick={handleFilter}
|
||||
disabled={!isDateValid || !!dateError || isFetching}
|
||||
startIcon={isFetching ? <CircularProgress size={16} color="inherit" /> : <SearchIcon />}
|
||||
sx={{ textTransform: 'none', minWidth: 100 }}
|
||||
>
|
||||
Buscar
|
||||
</Button>
|
||||
</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 */}
|
||||
{/* Select do MUI (Situação) */}
|
||||
<Grid size={{ xs: 12, sm: 6, md: 2 }}>
|
||||
<TextField
|
||||
select
|
||||
fullWidth
|
||||
label="Situação"
|
||||
size="small"
|
||||
value={localFilters.status ?? ''}
|
||||
onChange={(e) => updateLocalFilter('status', e.target.value || null)}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value || null;
|
||||
updateLocalFilter('status', value);
|
||||
}}
|
||||
>
|
||||
<MenuItem value="">Todos</MenuItem>
|
||||
<MenuItem value="P">Pendente</MenuItem>
|
||||
<MenuItem value="F">Faturado</MenuItem>
|
||||
<MenuItem value="C">Cancelado</MenuItem>
|
||||
</TextField>
|
||||
</Grid>
|
||||
|
||||
{/* Cliente */}
|
||||
{/* 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(opt => localFilters.customerId === opt.customer?.id) || null}
|
||||
value={
|
||||
customers.options.find(
|
||||
(option) => localFilters.customerId === option.customer?.id
|
||||
) || null
|
||||
}
|
||||
onChange={(_, newValue) => {
|
||||
updateLocalFilter('customerId', newValue?.customer?.id || null);
|
||||
updateLocalFilter('customerName', newValue?.customer?.name || null);
|
||||
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('');
|
||||
}
|
||||
}
|
||||
}}
|
||||
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'}
|
||||
renderInput={(params: AutocompleteRenderInputParams) => (
|
||||
<TextField
|
||||
{...params}
|
||||
label="Cliente"
|
||||
placeholder="Digite para buscar..."
|
||||
/>
|
||||
)}
|
||||
noOptionsText={
|
||||
customerSearchTerm.length < 2
|
||||
? 'Digite pelo menos 2 caracteres'
|
||||
: 'Nenhum cliente encontrado'
|
||||
}
|
||||
loadingText="Buscando clientes..."
|
||||
filterOptions={(x) => x}
|
||||
clearOnBlur={false}
|
||||
selectOnFocus
|
||||
handleHomeEndKeys
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
{/* Filiais */}
|
||||
{/* 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>
|
||||
</LocalizationProvider>
|
||||
</Grid>
|
||||
|
||||
{/* Botão Mais Filtros - inline com filtros primários */}
|
||||
<Grid
|
||||
size={{ xs: 12, sm: 12, md: 2.5 }}
|
||||
sx={{ display: 'flex', alignItems: 'flex-end', justifyContent: { xs: 'flex-start', md: 'flex-end' } }}
|
||||
>
|
||||
<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: '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((opt) => localFilters.store?.includes(opt.value))}
|
||||
onChange={(_, newValue) => updateLocalFilter('store', newValue.map((opt) => opt.value))}
|
||||
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) => <TextField {...params} label="Filiais" placeholder="Selecione filiais" />}
|
||||
renderInput={(params: AutocompleteRenderInputParams) => (
|
||||
<TextField
|
||||
{...params}
|
||||
label="Filiais"
|
||||
placeholder={
|
||||
localFilters.store?.length
|
||||
? `${localFilters.store.length} selecionadas`
|
||||
: 'Selecione'
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
{/* Filial de Estoque */}
|
||||
{/* 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((opt) => localFilters.stockId?.includes(opt.value))}
|
||||
onChange={(_, newValue) => updateLocalFilter('stockId', newValue.map((opt) => opt.value))}
|
||||
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) => <TextField {...params} label="Filial de Estoque" placeholder="Selecione filial" />}
|
||||
renderInput={(params: AutocompleteRenderInputParams) => (
|
||||
<TextField
|
||||
{...params}
|
||||
label="Filial de Estoque"
|
||||
placeholder={
|
||||
localFilters.stockId?.length
|
||||
? `${localFilters.stockId.length} selecionadas`
|
||||
: 'Selecione'
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
{/* Vendedor */}
|
||||
{/* Autocomplete do MUI para Vendedor */}
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<Autocomplete
|
||||
size="small"
|
||||
options={sellers.options}
|
||||
getOptionLabel={(option) => option.label}
|
||||
value={sellers.options.find(opt => localFilters.sellerId === opt.seller.id.toString()) || null}
|
||||
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);
|
||||
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" />}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
label="Vendedor"
|
||||
placeholder="Selecione um 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);
|
||||
)}
|
||||
renderOption={(props, option) => {
|
||||
const { key, ...otherProps } = props;
|
||||
return (
|
||||
<li key={option.id} {...otherProps}>
|
||||
{option.label}
|
||||
</li>
|
||||
);
|
||||
}}
|
||||
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>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Collapse>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -16,15 +16,12 @@ export const useOrderFilters = () => {
|
|||
sellerId: parseAsString,
|
||||
customerName: parseAsString,
|
||||
customerId: parseAsInteger,
|
||||
partnerName: parseAsString,
|
||||
partnerId: parseAsString,
|
||||
|
||||
codfilial: parseAsArrayOf(parseAsString, ','),
|
||||
codusur2: parseAsArrayOf(parseAsString, ','),
|
||||
store: parseAsArrayOf(parseAsString, ','),
|
||||
orderId: parseAsInteger,
|
||||
productId: parseAsInteger,
|
||||
productName: parseAsString,
|
||||
stockId: parseAsArrayOf(parseAsString, ','),
|
||||
|
||||
hasPreBox: parseAsBoolean.withDefault(false),
|
||||
|
|
|
|||
|
|
@ -1,48 +0,0 @@
|
|||
'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,
|
||||
};
|
||||
}
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
'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,8 +2,6 @@ import { z } from 'zod';
|
|||
import { storeSchema } from './store.schema';
|
||||
import { customerSchema } from './customer.schema';
|
||||
import { sellerSchema } from './seller.schema';
|
||||
import { partnerSchema } from './partner.schema';
|
||||
import { productSchema } from './product.schema';
|
||||
import { createApiSchema } from './api-response.schema';
|
||||
|
||||
/**
|
||||
|
|
@ -41,15 +39,3 @@ export const customersResponseSchema = createApiSchema(z.array(customerSchema));
|
|||
* Formato: { success: boolean, data: Seller[] }
|
||||
*/
|
||||
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,7 +16,6 @@ export const findOrdersSchema = z.object({
|
|||
minute: z.coerce.number().optional(),
|
||||
|
||||
partnerId: z.string().optional(),
|
||||
partnerName: z.string().optional(),
|
||||
codusur2: z.string().optional(),
|
||||
customerName: z.string().optional(),
|
||||
stockId: z.union([z.string(), z.array(z.string())]).optional(),
|
||||
|
|
@ -28,7 +27,6 @@ export const findOrdersSchema = z.object({
|
|||
orderId: z.coerce.number().optional(),
|
||||
invoiceId: z.coerce.number().optional(),
|
||||
productId: z.coerce.number().optional(),
|
||||
productName: z.string().optional(),
|
||||
|
||||
createDateIni: z.string().optional(),
|
||||
createDateEnd: z.string().optional(),
|
||||
|
|
@ -100,31 +98,19 @@ const formatValueToString = (val: any): string => {
|
|||
*/
|
||||
export const orderApiParamsSchema = findOrdersSchema
|
||||
.transform((filters) => {
|
||||
const {
|
||||
productName,
|
||||
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;
|
||||
// Remove customerName quando customerId existe (evita redundância)
|
||||
const { customerName, customerId, ...rest } = filters;
|
||||
return customerId ? { customerId, ...rest } : filters;
|
||||
})
|
||||
.transform((filters) => {
|
||||
// Mapeamento de chaves que precisam ser renomeadas
|
||||
const keyMap: Record<string, string> = {
|
||||
store: 'codfilial',
|
||||
};
|
||||
|
||||
return Object.entries(filters).reduce(
|
||||
(acc, [key, value]) => {
|
||||
// Early return: ignora valores vazios
|
||||
if (isEmptyValue(value)) return acc;
|
||||
|
||||
const apiKey = keyMap[key] ?? key;
|
||||
|
|
|
|||
|
|
@ -26,16 +26,6 @@ export { sellerSchema } from './seller.schema';
|
|||
export type { Seller } from './seller.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
|
||||
export { orderItemSchema, orderItemsResponseSchema } from './order.item.schema';
|
||||
export type { OrderItem, OrderItemsResponse } from './order.item.schema';
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
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>;
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
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>;
|
||||
|
|
@ -7,12 +7,15 @@ import Card from '@mui/material/Card';
|
|||
import CardContent from '@mui/material/CardContent';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
import Chip from '@mui/material/Chip';
|
||||
import { useColaboradorAtual } from '../hooks/useColaboradorAtual';
|
||||
import { useAuthStore } from '../../login/store/useAuthStore';
|
||||
import { useAuth } from '../../login/hooks/useAuth';
|
||||
|
||||
export default function ProfilePage() {
|
||||
const { data: colaborador, isLoading, error } = useColaboradorAtual();
|
||||
const { useMe } = useAuth();
|
||||
const { data: profile, isLoading, error } = useMe();
|
||||
const user = useAuthStore((s) => s.user);
|
||||
|
||||
const displayData = profile || user;
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
|
|
@ -33,53 +36,12 @@ export default function ProfilePage() {
|
|||
return <Alert severity="error">Erro ao carregar dados do perfil</Alert>;
|
||||
}
|
||||
|
||||
if (!colaborador) {
|
||||
if (!displayData) {
|
||||
return <Alert severity="warning">Nenhum dado de perfil disponível</Alert>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Grid container spacing={3}>
|
||||
{/* Header com Avatar e Nome */}
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 3 }}>
|
||||
<Avatar
|
||||
sx={{
|
||||
width: 80,
|
||||
height: 80,
|
||||
bgcolor: 'primary.main',
|
||||
fontSize: '1.5rem',
|
||||
}}
|
||||
>
|
||||
{colaborador.iniciais}
|
||||
</Avatar>
|
||||
<Box>
|
||||
<Typography variant="h5" fontWeight="600">
|
||||
{colaborador.nome}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
{colaborador.nomeComFilial}
|
||||
</Typography>
|
||||
<Box sx={{ mt: 1, display: 'flex', gap: 1 }}>
|
||||
{colaborador.ehRepresentanteComercial && (
|
||||
<Chip label="RCA" color="primary" size="small" />
|
||||
)}
|
||||
{colaborador.podeAplicarDesconto && (
|
||||
<Chip
|
||||
label={`${colaborador.percentualDesconto}% desconto`}
|
||||
color="success"
|
||||
size="small"
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
|
||||
{/* Informações Pessoais */}
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
|
|
@ -92,7 +54,7 @@ export default function ProfilePage() {
|
|||
Nome
|
||||
</Typography>
|
||||
<Typography variant="body1" fontWeight="500">
|
||||
{colaborador.nome || '-'}
|
||||
{displayData.nome || '-'}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
|
|
@ -100,7 +62,7 @@ export default function ProfilePage() {
|
|||
Usuário
|
||||
</Typography>
|
||||
<Typography variant="body1" fontWeight="500">
|
||||
{colaborador.userName || '-'}
|
||||
{displayData.userName || '-'}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
|
|
@ -108,7 +70,7 @@ export default function ProfilePage() {
|
|||
Matrícula
|
||||
</Typography>
|
||||
<Typography variant="body1" fontWeight="500">
|
||||
{colaborador.matricula || '-'}
|
||||
{displayData.matricula || '-'}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
|
@ -116,7 +78,6 @@ export default function ProfilePage() {
|
|||
</Card>
|
||||
</Grid>
|
||||
|
||||
{/* Informações Profissionais */}
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
|
|
@ -129,7 +90,8 @@ export default function ProfilePage() {
|
|||
Filial
|
||||
</Typography>
|
||||
<Typography variant="body1" fontWeight="500">
|
||||
{colaborador.nomeFilial || '-'} ({colaborador.codigoFilial || '-'})
|
||||
{displayData.nomeFilial || '-'} (
|
||||
{displayData.codigoFilial || '-'})
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
|
|
@ -137,16 +99,16 @@ export default function ProfilePage() {
|
|||
RCA
|
||||
</Typography>
|
||||
<Typography variant="body1" fontWeight="500">
|
||||
{colaborador.ehRepresentanteComercial ? colaborador.codigoRCA : 'Não é vendedor'}
|
||||
{displayData.rca || '-'}
|
||||
</Typography>
|
||||
</Box>
|
||||
{colaborador.podeAplicarDesconto && (
|
||||
{displayData.discountPercent !== undefined && (
|
||||
<Box>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
Desconto Disponível
|
||||
Desconto (%)
|
||||
</Typography>
|
||||
<Typography variant="body1" fontWeight="500">
|
||||
{colaborador.percentualDesconto}%
|
||||
{displayData.discountPercent}%
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
|
@ -155,9 +117,9 @@ export default function ProfilePage() {
|
|||
</Card>
|
||||
</Grid>
|
||||
|
||||
{/* Informações Adicionais */}
|
||||
{(colaborador.codigoSetor !== undefined ||
|
||||
colaborador.codigoSupervisor !== undefined) && (
|
||||
{(displayData.sectorId !== undefined ||
|
||||
displayData.sectorManagerId !== undefined ||
|
||||
displayData.supervisorId !== undefined) && (
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
|
|
@ -165,23 +127,23 @@ export default function ProfilePage() {
|
|||
Informações Adicionais
|
||||
</Typography>
|
||||
<Grid container spacing={2}>
|
||||
{colaborador.codigoSetor !== undefined && (
|
||||
{displayData.sectorId !== undefined && (
|
||||
<Grid size={{ xs: 12, sm: 4 }}>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
Setor
|
||||
</Typography>
|
||||
<Typography variant="body1" fontWeight="500">
|
||||
{colaborador.codigoSetor}
|
||||
{displayData.sectorId}
|
||||
</Typography>
|
||||
</Grid>
|
||||
)}
|
||||
{colaborador.codigoSupervisor !== undefined && (
|
||||
{displayData.supervisorId !== undefined && (
|
||||
<Grid size={{ xs: 12, sm: 4 }}>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
Supervisor
|
||||
</Typography>
|
||||
<Typography variant="body1" fontWeight="500">
|
||||
{colaborador.codigoSupervisor}
|
||||
{displayData.supervisorId}
|
||||
</Typography>
|
||||
</Grid>
|
||||
)}
|
||||
|
|
@ -193,4 +155,3 @@ export default function ProfilePage() {
|
|||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,71 +0,0 @@
|
|||
import { UserProfileDto } from '../types';
|
||||
|
||||
/**
|
||||
* Entidade de Domínio: Colaborador
|
||||
*/
|
||||
export class Colaborador {
|
||||
private constructor(
|
||||
public readonly matricula: number,
|
||||
public readonly userName: string,
|
||||
public readonly nome: string,
|
||||
public readonly codigoFilial: string,
|
||||
public readonly nomeFilial: string,
|
||||
public readonly codigoRCA: number,
|
||||
private readonly _percentualDesconto: number,
|
||||
public readonly codigoSetor: number,
|
||||
public readonly codigoSupervisor: number
|
||||
) {}
|
||||
|
||||
// ========== FACTORY METHODS ==========
|
||||
|
||||
static criarAPartirDoDto(dto: UserProfileDto): Colaborador {
|
||||
return new Colaborador(
|
||||
dto.matricula,
|
||||
dto.userName,
|
||||
dto.nome,
|
||||
dto.codigoFilial,
|
||||
dto.nomeFilial,
|
||||
dto.rca,
|
||||
dto.discountPercent,
|
||||
dto.sectorId,
|
||||
dto.supervisorId
|
||||
);
|
||||
}
|
||||
|
||||
// ========== COMPUTED PROPERTIES ==========
|
||||
|
||||
get iniciais(): string {
|
||||
if (!this.nome) return 'U';
|
||||
const partes = this.nome.trim().split(' ').filter(Boolean);
|
||||
if (partes.length === 1) {
|
||||
return partes[0][0].toUpperCase();
|
||||
}
|
||||
return (partes[0][0] + partes[partes.length - 1][0]).toUpperCase();
|
||||
}
|
||||
|
||||
get ehRepresentanteComercial(): boolean {
|
||||
return this.codigoRCA > 0;
|
||||
}
|
||||
|
||||
get podeAplicarDesconto(): boolean {
|
||||
return this._percentualDesconto > 0 && this.ehRepresentanteComercial;
|
||||
}
|
||||
|
||||
get percentualDesconto(): number {
|
||||
return this._percentualDesconto;
|
||||
}
|
||||
|
||||
get nomeComFilial(): string {
|
||||
return `${this.nome} - ${this.nomeFilial}`;
|
||||
}
|
||||
|
||||
// ========== BUSINESS METHODS ==========
|
||||
|
||||
aplicarDescontoAoValor(valorOriginal: number): number {
|
||||
if (!this.podeAplicarDesconto) {
|
||||
return valorOriginal;
|
||||
}
|
||||
return valorOriginal * (1 - this._percentualDesconto / 100);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
import { useQuery, UseQueryResult } from '@tanstack/react-query';
|
||||
import { profileService } from '../services/profile.service';
|
||||
import { Colaborador } from '../domain/Colaborador';
|
||||
|
||||
export const COLABORADOR_ATUAL_QUERY_KEY = ['colaborador-atual'] as const;
|
||||
|
||||
/**
|
||||
* Hook para obter o colaborador atualmente autenticado.
|
||||
* Encapsula a lógica de busca e cache do perfil do usuário,
|
||||
* desacoplando essa responsabilidade do módulo de login.
|
||||
*/
|
||||
export function useColaboradorAtual(): UseQueryResult<Colaborador> {
|
||||
return useQuery({
|
||||
queryKey: COLABORADOR_ATUAL_QUERY_KEY,
|
||||
queryFn: () => profileService.obterColaboradorAtual(),
|
||||
retry: false,
|
||||
staleTime: Infinity,
|
||||
});
|
||||
}
|
||||
|
|
@ -1,5 +1,3 @@
|
|||
// Profile feature barrel export
|
||||
export { default as ProfilePage } from './components/ProfilePage';
|
||||
export type { UserProfileDto } from './types';
|
||||
export { Colaborador } from './domain/Colaborador';
|
||||
export { useColaboradorAtual, COLABORADOR_ATUAL_QUERY_KEY } from './hooks/useColaboradorAtual';
|
||||
export type { UserProfile } from './types';
|
||||
|
|
|
|||
|
|
@ -1,23 +1,9 @@
|
|||
import { profileApi } from '../api';
|
||||
import { UserProfileDto } from '../types';
|
||||
import { Colaborador } from '../domain/Colaborador';
|
||||
import { UserProfile } from '../types';
|
||||
|
||||
export const profileService = {
|
||||
/**
|
||||
* Obtém o colaborador atualmente autenticado como entidade de domínio.
|
||||
* Segue o padrão de Linguagem Ubíqua do DDD.
|
||||
*/
|
||||
obterColaboradorAtual: async (): Promise<Colaborador> => {
|
||||
const response = await profileApi.get<UserProfileDto>('/auth/me');
|
||||
return Colaborador.criarAPartirDoDto(response.data);
|
||||
},
|
||||
|
||||
/**
|
||||
* Obtém os dados brutos (DTO) do colaborador autenticado.
|
||||
* Usar apenas quando a conversão para entidade de domínio não for necessária.
|
||||
*/
|
||||
obterColaboradorAtualDto: async (): Promise<UserProfileDto> => {
|
||||
const response = await profileApi.get<UserProfileDto>('/auth/me');
|
||||
getMe: async (): Promise<UserProfile> => {
|
||||
const response = await profileApi.get<UserProfile>('/auth/me');
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* Tipagem da resposta do User Info (Endpoint 1.3)
|
||||
*/
|
||||
export interface UserProfileDto {
|
||||
export interface UserProfile {
|
||||
matricula: number;
|
||||
userName: string;
|
||||
nome: string;
|
||||
|
|
|
|||
Loading…
Reference in New Issue