Merge remote-tracking branch 'origin/feature/ddd-profile-refactoring'

This commit is contained in:
JuruSysadmin 2026-01-15 16:45:42 -03:00
commit b0c8d6c938
17 changed files with 242 additions and 744 deletions

View File

@ -1,64 +0,0 @@
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;

View File

@ -12,11 +12,8 @@ import DarkModeIcon from '@mui/icons-material/DarkMode';
import LightModeIcon from '@mui/icons-material/LightMode'; import LightModeIcon from '@mui/icons-material/LightMode';
import { useCustomizerStore } from '../store/useCustomizerStore'; import { useCustomizerStore } from '../store/useCustomizerStore';
import Notifications from './Notification';
import Profile from './Profile'; import Profile from './Profile';
import Search from './Search'; import Search from './Search';
import Navigation from './Navigation';
import MobileRightSidebar from './MobileRightSidebar';
const AppBarStyled = styled(AppBar)(({ theme }) => ({ const AppBarStyled = styled(AppBar)(({ theme }) => ({
boxShadow: 'none', boxShadow: 'none',
@ -33,7 +30,6 @@ const ToolbarStyled = styled(Toolbar)(({ theme }) => ({
const Header = () => { const Header = () => {
const lgUp = useMediaQuery((theme: Theme) => theme.breakpoints.up('lg')); const lgUp = useMediaQuery((theme: Theme) => theme.breakpoints.up('lg'));
const lgDown = useMediaQuery((theme: Theme) => theme.breakpoints.down('lg'));
const { const {
activeMode, activeMode,
@ -51,7 +47,7 @@ const Header = () => {
<AppBarStyled position="sticky" color="default"> <AppBarStyled position="sticky" color="default">
<ToolbarStyled> <ToolbarStyled>
{/* ------------------------------------------- */} {/* ------------------------------------------- */}
{/* Toggle Button Sidebar (Fluxo 2) */} {/* Toggle Button Sidebar */}
{/* ------------------------------------------- */} {/* ------------------------------------------- */}
<IconButton <IconButton
color="inherit" color="inherit"
@ -62,10 +58,9 @@ const Header = () => {
</IconButton> </IconButton>
{/* ------------------------------------------- */} {/* ------------------------------------------- */}
{/* Search & Navigation */} {/* Search */}
{/* ------------------------------------------- */} {/* ------------------------------------------- */}
<Search /> <Search />
{lgUp && <Navigation />}
<Box flexGrow={1} /> <Box flexGrow={1} />
@ -84,12 +79,9 @@ const Header = () => {
{activeMode === 'light' ? <DarkModeIcon /> : <LightModeIcon />} {activeMode === 'light' ? <DarkModeIcon /> : <LightModeIcon />}
</IconButton> </IconButton>
<Notifications />
{/* ------------------------------------------- */} {/* ------------------------------------------- */}
{/* Mobile Right Sidebar & Profile */} {/* Profile */}
{/* ------------------------------------------- */} {/* ------------------------------------------- */}
{lgDown && <MobileRightSidebar />}
<Profile /> <Profile />
</Stack> </Stack>
</ToolbarStyled> </ToolbarStyled>
@ -98,3 +90,4 @@ const Header = () => {
}; };
export default Header; export default Header;

View File

@ -1,128 +0,0 @@
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;

View File

@ -1,135 +0,0 @@
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;

View File

@ -1,132 +0,0 @@
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;

View File

@ -5,11 +5,9 @@ import {
Menu, Menu,
Avatar, Avatar,
Typography, Typography,
Divider,
Button, Button,
IconButton, IconButton,
} from '@mui/material'; } from '@mui/material';
import { IconMail } from '@tabler/icons-react';
import { Stack } from '@mui/system'; import { Stack } from '@mui/system';
import { useAuthStore } from '../../login/store/useAuthStore'; import { useAuthStore } from '../../login/store/useAuthStore';
@ -27,10 +25,18 @@ const Profile = () => {
setAnchorEl2(null); 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 ( return (
<Box> <Box>
<IconButton <IconButton
aria-label="show 11 new notifications" aria-label="menu do perfil"
color="inherit" color="inherit"
aria-controls="msgs-menu" aria-controls="msgs-menu"
aria-haspopup="true" aria-haspopup="true"
@ -48,11 +54,11 @@ const Profile = () => {
bgcolor: 'primary.main', bgcolor: 'primary.main',
}} }}
> >
{user?.nome?.[0] || user?.userName?.[0] || 'U'} {getIniciais()}
</Avatar> </Avatar>
</IconButton> </IconButton>
{/* ------------------------------------------- */} {/* ------------------------------------------- */}
{/* Message Dropdown */} {/* Profile Dropdown */}
{/* ------------------------------------------- */} {/* ------------------------------------------- */}
<Menu <Menu
id="msgs-menu" id="msgs-menu"
@ -64,17 +70,19 @@ const Profile = () => {
transformOrigin={{ horizontal: 'right', vertical: 'top' }} transformOrigin={{ horizontal: 'right', vertical: 'top' }}
sx={{ sx={{
'& .MuiMenu-paper': { '& .MuiMenu-paper': {
width: '360px', width: '320px',
p: 4, p: 3,
}, },
}} }}
> >
<Typography variant="h5">User Profile</Typography> <Typography variant="h6" fontWeight={600}>
<Stack direction="row" py={3} spacing={2} alignItems="center"> Meu Perfil
</Typography>
<Stack direction="row" py={2} spacing={2} alignItems="center">
<Avatar <Avatar
sx={{ width: 95, height: 95, bgcolor: 'primary.main' }} sx={{ width: 64, height: 64, bgcolor: 'primary.main', fontSize: '1.5rem' }}
> >
{user?.nome?.[0] || user?.userName?.[0] || 'U'} {getIniciais()}
</Avatar> </Avatar>
<Box> <Box>
<Typography <Typography
@ -84,34 +92,36 @@ const Profile = () => {
> >
{user?.nome || user?.userName || 'Usuário'} {user?.nome || user?.userName || 'Usuário'}
</Typography> </Typography>
<Typography variant="subtitle2" color="textSecondary"> <Typography variant="body2" color="textSecondary">
{user?.nomeFilial || 'Sem filial'} {user?.nomeFilial || '-'}
</Typography> </Typography>
<Typography <Typography variant="body2" color="textSecondary">
variant="subtitle2" Matrícula: {user?.matricula || '-'}
color="textSecondary"
display="flex"
alignItems="center"
gap={1}
>
<IconMail width={15} height={15} />
{user?.rca ? `RCA: ${user.rca}` : 'Sem e-mail'}
</Typography> </Typography>
</Box> </Box>
</Stack> </Stack>
<Box mt={2}>
<Button <Button
variant="outlined" variant="outlined"
color="primary" color="primary"
component={Link}
href="/dashboard/profile"
fullWidth
sx={{ mb: 1 }}
>
Ver Perfil Completo
</Button>
<Button
variant="contained"
color="primary"
onClick={logout} onClick={logout}
fullWidth fullWidth
> >
Sair Sair
</Button> </Button>
</Box>
</Menu> </Menu>
</Box> </Box>
); );
}; };
export default Profile; export default Profile;

View File

@ -1,185 +0,0 @@
// 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 };

View File

@ -20,7 +20,7 @@ export function AuthInitializer({ children }: { children: React.ReactNode }) {
try { try {
await loginService.refreshToken(); await loginService.refreshToken();
const profile = await profileService.getMe(); const profile = await profileService.obterColaboradorAtual();
setUser(mapToSafeProfile(profile)); setUser(mapToSafeProfile(profile));
} catch (error) { } catch (error) {
console.warn('Sessão expirada ou inválida', error); console.warn('Sessão expirada ou inválida', error);

View File

@ -1,10 +1,11 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useAuthStore } from '../store/useAuthStore'; import { useAuthStore } from '../store/useAuthStore';
import { loginService } from '../services/login.service'; import { loginService } from '../services/login.service';
import { profileService } from '../../profile/services/profile.service'; import { profileService } from '../../profile/services/profile.service';
import { clearAuthData } from '../utils/tokenRefresh'; import { clearAuthData } from '../utils/tokenRefresh';
import { mapToSafeProfile } from '../utils/mappers'; import { mapToSafeProfile } from '../utils/mappers';
import { COLABORADOR_ATUAL_QUERY_KEY } from '../../profile';
export function useAuth() { export function useAuth() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@ -16,11 +17,11 @@ export function useAuth() {
mutationFn: loginService.login, mutationFn: loginService.login,
onSuccess: async () => { onSuccess: async () => {
try { try {
const profile = await profileService.getMe(); const profile = await profileService.obterColaboradorAtual();
const safeProfile = mapToSafeProfile(profile); const safeProfile = mapToSafeProfile(profile);
setUser(safeProfile); setUser(safeProfile);
queryClient.setQueryData(['auth-me'], safeProfile); queryClient.setQueryData(COLABORADOR_ATUAL_QUERY_KEY, profile);
router.push('/dashboard'); router.push('/dashboard');
} catch (error) { } catch (error) {
@ -30,19 +31,6 @@ 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 () => { const logout = async () => {
try { try {
await loginService.logout(); await loginService.logout();
@ -54,5 +42,5 @@ export function useAuth() {
} }
}; };
return { loginMutation, useMe, logout }; return { loginMutation, logout };
} }

View File

@ -1,6 +1,6 @@
import { z } from 'zod'; import { z } from 'zod';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import type { UserProfile } from '../../profile/types'; import type { UserProfileDto } from '../../profile/types';
import { import {
loginSchema, loginSchema,
@ -38,9 +38,9 @@ export interface AuthLoginProps {
} }
export interface AuthState { export interface AuthState {
user: UserProfile | null; user: UserProfileDto | null;
isAuthenticated: boolean; isAuthenticated: boolean;
setUser: (user: UserProfile | null) => void; setUser: (user: UserProfileDto | null) => void;
logout: () => void; logout: () => void;
hydrate: () => void; hydrate: () => void;
} }

View File

@ -1,16 +1,22 @@
import { UserProfile } from '../../profile/types'; import { UserProfileDto } from '../../profile/types';
import { Colaborador } from '../../profile/domain/Colaborador';
export const mapToSafeProfile = (data: any): UserProfile => { /**
* 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 => {
return { return {
matricula: data.matricula, matricula: data.matricula,
userName: data.userName, userName: data.userName,
nome: data.nome, nome: data.nome,
codigoFilial: data.codigoFilial, codigoFilial: data.codigoFilial,
nomeFilial: data.nomeFilial, nomeFilial: data.nomeFilial,
rca: data.rca, rca: data.codigoRCA ?? data.rca,
discountPercent: data.discountPercent, discountPercent: data.percentualDesconto ?? data._percentualDesconto ?? data.discountPercent,
sectorId: data.sectorId, sectorId: data.codigoSetor ?? data.sectorId,
sectorManagerId: data.sectorManagerId, sectorManagerId: data.sectorManagerId ?? 0,
supervisorId: data.supervisorId, supervisorId: data.codigoSupervisor ?? data.supervisorId,
}; };
}; };

View File

@ -7,15 +7,12 @@ import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent'; import CardContent from '@mui/material/CardContent';
import CircularProgress from '@mui/material/CircularProgress'; import CircularProgress from '@mui/material/CircularProgress';
import Alert from '@mui/material/Alert'; import Alert from '@mui/material/Alert';
import { useAuthStore } from '../../login/store/useAuthStore'; import Avatar from '@mui/material/Avatar';
import { useAuth } from '../../login/hooks/useAuth'; import Chip from '@mui/material/Chip';
import { useColaboradorAtual } from '../hooks/useColaboradorAtual';
export default function ProfilePage() { export default function ProfilePage() {
const { useMe } = useAuth(); const { data: colaborador, isLoading, error } = useColaboradorAtual();
const { data: profile, isLoading, error } = useMe();
const user = useAuthStore((s) => s.user);
const displayData = profile || user;
if (isLoading) { if (isLoading) {
return ( return (
@ -36,12 +33,53 @@ export default function ProfilePage() {
return <Alert severity="error">Erro ao carregar dados do perfil</Alert>; return <Alert severity="error">Erro ao carregar dados do perfil</Alert>;
} }
if (!displayData) { if (!colaborador) {
return <Alert severity="warning">Nenhum dado de perfil disponível</Alert>; return <Alert severity="warning">Nenhum dado de perfil disponível</Alert>;
} }
return ( return (
<Grid container spacing={3}> <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 }}> <Grid size={{ xs: 12, md: 6 }}>
<Card> <Card>
<CardContent> <CardContent>
@ -54,7 +92,7 @@ export default function ProfilePage() {
Nome Nome
</Typography> </Typography>
<Typography variant="body1" fontWeight="500"> <Typography variant="body1" fontWeight="500">
{displayData.nome || '-'} {colaborador.nome || '-'}
</Typography> </Typography>
</Box> </Box>
<Box> <Box>
@ -62,7 +100,7 @@ export default function ProfilePage() {
Usuário Usuário
</Typography> </Typography>
<Typography variant="body1" fontWeight="500"> <Typography variant="body1" fontWeight="500">
{displayData.userName || '-'} {colaborador.userName || '-'}
</Typography> </Typography>
</Box> </Box>
<Box> <Box>
@ -70,7 +108,7 @@ export default function ProfilePage() {
Matrícula Matrícula
</Typography> </Typography>
<Typography variant="body1" fontWeight="500"> <Typography variant="body1" fontWeight="500">
{displayData.matricula || '-'} {colaborador.matricula || '-'}
</Typography> </Typography>
</Box> </Box>
</Box> </Box>
@ -78,6 +116,7 @@ export default function ProfilePage() {
</Card> </Card>
</Grid> </Grid>
{/* Informações Profissionais */}
<Grid size={{ xs: 12, md: 6 }}> <Grid size={{ xs: 12, md: 6 }}>
<Card> <Card>
<CardContent> <CardContent>
@ -90,8 +129,7 @@ export default function ProfilePage() {
Filial Filial
</Typography> </Typography>
<Typography variant="body1" fontWeight="500"> <Typography variant="body1" fontWeight="500">
{displayData.nomeFilial || '-'} ( {colaborador.nomeFilial || '-'} ({colaborador.codigoFilial || '-'})
{displayData.codigoFilial || '-'})
</Typography> </Typography>
</Box> </Box>
<Box> <Box>
@ -99,16 +137,16 @@ export default function ProfilePage() {
RCA RCA
</Typography> </Typography>
<Typography variant="body1" fontWeight="500"> <Typography variant="body1" fontWeight="500">
{displayData.rca || '-'} {colaborador.ehRepresentanteComercial ? colaborador.codigoRCA : 'Não é vendedor'}
</Typography> </Typography>
</Box> </Box>
{displayData.discountPercent !== undefined && ( {colaborador.podeAplicarDesconto && (
<Box> <Box>
<Typography variant="body2" color="textSecondary"> <Typography variant="body2" color="textSecondary">
Desconto (%) Desconto Disponível
</Typography> </Typography>
<Typography variant="body1" fontWeight="500"> <Typography variant="body1" fontWeight="500">
{displayData.discountPercent}% {colaborador.percentualDesconto}%
</Typography> </Typography>
</Box> </Box>
)} )}
@ -117,9 +155,9 @@ export default function ProfilePage() {
</Card> </Card>
</Grid> </Grid>
{(displayData.sectorId !== undefined || {/* Informações Adicionais */}
displayData.sectorManagerId !== undefined || {(colaborador.codigoSetor !== undefined ||
displayData.supervisorId !== undefined) && ( colaborador.codigoSupervisor !== undefined) && (
<Grid size={{ xs: 12 }}> <Grid size={{ xs: 12 }}>
<Card> <Card>
<CardContent> <CardContent>
@ -127,23 +165,23 @@ export default function ProfilePage() {
Informações Adicionais Informações Adicionais
</Typography> </Typography>
<Grid container spacing={2}> <Grid container spacing={2}>
{displayData.sectorId !== undefined && ( {colaborador.codigoSetor !== undefined && (
<Grid size={{ xs: 12, sm: 4 }}> <Grid size={{ xs: 12, sm: 4 }}>
<Typography variant="body2" color="textSecondary"> <Typography variant="body2" color="textSecondary">
Setor Setor
</Typography> </Typography>
<Typography variant="body1" fontWeight="500"> <Typography variant="body1" fontWeight="500">
{displayData.sectorId} {colaborador.codigoSetor}
</Typography> </Typography>
</Grid> </Grid>
)} )}
{displayData.supervisorId !== undefined && ( {colaborador.codigoSupervisor !== undefined && (
<Grid size={{ xs: 12, sm: 4 }}> <Grid size={{ xs: 12, sm: 4 }}>
<Typography variant="body2" color="textSecondary"> <Typography variant="body2" color="textSecondary">
Supervisor Supervisor
</Typography> </Typography>
<Typography variant="body1" fontWeight="500"> <Typography variant="body1" fontWeight="500">
{displayData.supervisorId} {colaborador.codigoSupervisor}
</Typography> </Typography>
</Grid> </Grid>
)} )}
@ -155,3 +193,4 @@ export default function ProfilePage() {
</Grid> </Grid>
); );
} }

View File

@ -0,0 +1,71 @@
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);
}
}

View File

@ -0,0 +1,19 @@
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,
});
}

View File

@ -1,3 +1,5 @@
// Profile feature barrel export // Profile feature barrel export
export { default as ProfilePage } from './components/ProfilePage'; export { default as ProfilePage } from './components/ProfilePage';
export type { UserProfile } from './types'; export type { UserProfileDto } from './types';
export { Colaborador } from './domain/Colaborador';
export { useColaboradorAtual, COLABORADOR_ATUAL_QUERY_KEY } from './hooks/useColaboradorAtual';

View File

@ -1,9 +1,23 @@
import { profileApi } from '../api'; import { profileApi } from '../api';
import { UserProfile } from '../types'; import { UserProfileDto } from '../types';
import { Colaborador } from '../domain/Colaborador';
export const profileService = { export const profileService = {
getMe: async (): Promise<UserProfile> => { /**
const response = await profileApi.get<UserProfile>('/auth/me'); * 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');
return response.data; return response.data;
}, },
}; };

View File

@ -1,7 +1,7 @@
/** /**
* Tipagem da resposta do User Info (Endpoint 1.3) * Tipagem da resposta do User Info (Endpoint 1.3)
*/ */
export interface UserProfile { export interface UserProfileDto {
matricula: number; matricula: number;
userName: string; userName: string;
nome: string; nome: string;