diff --git a/src/features/dashboard/header/AppLinks.tsx b/src/features/dashboard/header/AppLinks.tsx deleted file mode 100644 index 80f8e8f..0000000 --- a/src/features/dashboard/header/AppLinks.tsx +++ /dev/null @@ -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 ( - - {dropdownData.appsLink.map((links) => ( - - - - - - - - - {links.title} - - - {links.subtext} - - - - - - ))} - - ); -}; - -export default AppLinks; diff --git a/src/features/dashboard/header/Header.tsx b/src/features/dashboard/header/Header.tsx index 1857583..16cee51 100644 --- a/src/features/dashboard/header/Header.tsx +++ b/src/features/dashboard/header/Header.tsx @@ -12,11 +12,8 @@ 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', @@ -33,7 +30,6 @@ 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, @@ -51,7 +47,7 @@ const Header = () => { {/* ------------------------------------------- */} - {/* Toggle Button Sidebar (Fluxo 2) */} + {/* Toggle Button Sidebar */} {/* ------------------------------------------- */} { {/* ------------------------------------------- */} - {/* Search & Navigation */} + {/* Search */} {/* ------------------------------------------- */} - {lgUp && } @@ -84,12 +79,9 @@ const Header = () => { {activeMode === 'light' ? : } - - {/* ------------------------------------------- */} - {/* Mobile Right Sidebar & Profile */} + {/* Profile */} {/* ------------------------------------------- */} - {lgDown && } @@ -98,3 +90,4 @@ const Header = () => { }; export default Header; + diff --git a/src/features/dashboard/header/MobileRightSidebar.tsx b/src/features/dashboard/header/MobileRightSidebar.tsx deleted file mode 100644 index b165d4a..0000000 --- a/src/features/dashboard/header/MobileRightSidebar.tsx +++ /dev/null @@ -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 = ( - - {/* ------------------------------------------- */} - {/* Apps Content */} - {/* ------------------------------------------- */} - - - - - - - - - Chats - - - - - - - - - - Email - - - - - - - - - - Apps - - - {open ? ( - - ) : ( - - )} - - - - - - - - - - - - ); - - return ( - - setShowDrawer(true)} - sx={{ - ...(showDrawer && { - color: 'primary.main', - }), - }} - > - - - {/* ------------------------------------------- */} - {/* Cart Sidebar */} - {/* ------------------------------------------- */} - setShowDrawer(false)} - PaperProps={{ sx: { width: '300px' } }} - > - - - Navigation - - - - {/* component */} - {cartContent} - - - ); -}; - -export default MobileRightSidebar; diff --git a/src/features/dashboard/header/Navigation.tsx b/src/features/dashboard/header/Navigation.tsx deleted file mode 100644 index e371d42..0000000 --- a/src/features/dashboard/header/Navigation.tsx +++ /dev/null @@ -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 ( - <> - - - {/* ------------------------------------------- */} - {/* Message Dropdown */} - {/* ------------------------------------------- */} - - - - - - - - - - - Frequently Asked Questions - - - - - - - - - - - - - - - - - ); -}; - -export default AppDD; diff --git a/src/features/dashboard/header/Notification.tsx b/src/features/dashboard/header/Notification.tsx deleted file mode 100644 index 89d49e1..0000000 --- a/src/features/dashboard/header/Notification.tsx +++ /dev/null @@ -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 ( - - - - - - - {/* ------------------------------------------- */} - {/* Message Dropdown */} - {/* ------------------------------------------- */} - - - Notifications - - - - {dropdownData.notifications.map((notification) => ( - - - - - - - {notification.title} - - - {notification.subtitle} - - - - - - ))} - - - - - - - ); -}; - -export default Notifications; diff --git a/src/features/dashboard/header/Profile.tsx b/src/features/dashboard/header/Profile.tsx index b8f4aee..0d8394d 100644 --- a/src/features/dashboard/header/Profile.tsx +++ b/src/features/dashboard/header/Profile.tsx @@ -5,11 +5,9 @@ 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'; @@ -27,10 +25,18 @@ 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 ( { bgcolor: 'primary.main', }} > - {user?.nome?.[0] || user?.userName?.[0] || 'U'} + {getIniciais()} {/* ------------------------------------------- */} - {/* Message Dropdown */} + {/* Profile Dropdown */} {/* ------------------------------------------- */} { transformOrigin={{ horizontal: 'right', vertical: 'top' }} sx={{ '& .MuiMenu-paper': { - width: '360px', - p: 4, + width: '320px', + p: 3, }, }} > - User Profile - + + Meu Perfil + + - {user?.nome?.[0] || user?.userName?.[0] || 'U'} + {getIniciais()} { > {user?.nome || user?.userName || 'Usuário'} - - {user?.nomeFilial || 'Sem filial'} + + {user?.nomeFilial || '-'} - - - {user?.rca ? `RCA: ${user.rca}` : 'Sem e-mail'} + + Matrícula: {user?.matricula || '-'} - - - + + ); }; export default Profile; + diff --git a/src/features/dashboard/header/data.ts b/src/features/dashboard/header/data.ts deleted file mode 100644 index 8383eb9..0000000 --- a/src/features/dashboard/header/data.ts +++ /dev/null @@ -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 }; diff --git a/src/features/login/components/AuthInitializer.tsx b/src/features/login/components/AuthInitializer.tsx index 93dcfd6..4df399f 100644 --- a/src/features/login/components/AuthInitializer.tsx +++ b/src/features/login/components/AuthInitializer.tsx @@ -20,7 +20,7 @@ export function AuthInitializer({ children }: { children: React.ReactNode }) { try { await loginService.refreshToken(); - const profile = await profileService.getMe(); + const profile = await profileService.obterColaboradorAtual(); setUser(mapToSafeProfile(profile)); } catch (error) { console.warn('Sessão expirada ou inválida', error); diff --git a/src/features/login/hooks/useAuth.ts b/src/features/login/hooks/useAuth.ts index 94c9032..6c2a213 100644 --- a/src/features/login/hooks/useAuth.ts +++ b/src/features/login/hooks/useAuth.ts @@ -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 { 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(); @@ -16,11 +17,11 @@ export function useAuth() { mutationFn: loginService.login, onSuccess: async () => { try { - const profile = await profileService.getMe(); + const profile = await profileService.obterColaboradorAtual(); const safeProfile = mapToSafeProfile(profile); setUser(safeProfile); - queryClient.setQueryData(['auth-me'], safeProfile); + queryClient.setQueryData(COLABORADOR_ATUAL_QUERY_KEY, profile); router.push('/dashboard'); } 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 () => { try { await loginService.logout(); @@ -54,5 +42,5 @@ export function useAuth() { } }; - return { loginMutation, useMe, logout }; + return { loginMutation, logout }; } diff --git a/src/features/login/interfaces/types.ts b/src/features/login/interfaces/types.ts index a6178bd..bd6f67e 100644 --- a/src/features/login/interfaces/types.ts +++ b/src/features/login/interfaces/types.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; import type { ReactNode } from 'react'; -import type { UserProfile } from '../../profile/types'; +import type { UserProfileDto } from '../../profile/types'; import { loginSchema, @@ -38,9 +38,9 @@ export interface AuthLoginProps { } export interface AuthState { - user: UserProfile | null; + user: UserProfileDto | null; isAuthenticated: boolean; - setUser: (user: UserProfile | null) => void; + setUser: (user: UserProfileDto | null) => void; logout: () => void; hydrate: () => void; } diff --git a/src/features/login/utils/mappers.ts b/src/features/login/utils/mappers.ts index e130a0d..d1c2013 100644 --- a/src/features/login/utils/mappers.ts +++ b/src/features/login/utils/mappers.ts @@ -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 { matricula: data.matricula, userName: data.userName, nome: data.nome, codigoFilial: data.codigoFilial, nomeFilial: data.nomeFilial, - rca: data.rca, - discountPercent: data.discountPercent, - sectorId: data.sectorId, - sectorManagerId: data.sectorManagerId, - supervisorId: data.supervisorId, + 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, }; }; + diff --git a/src/features/orders/api/order.service.ts b/src/features/orders/api/order.service.ts index d21f0ea..7b1bf58 100644 --- a/src/features/orders/api/order.service.ts +++ b/src/features/orders/api/order.service.ts @@ -15,6 +15,8 @@ import { storesResponseSchema, customersResponseSchema, sellersResponseSchema, + partnersResponseSchema, + productsResponseSchema, unwrapApiData, } from '../schemas/order.schema'; import { @@ -178,5 +180,42 @@ 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 de parceiros correspondentes + */ + findPartners: async ( + filter: string + ): Promise> => { + 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 de produtos correspondentes + */ + findProducts: async ( + term: string + ): Promise> => { + if (!term || term.trim().length < 2) return []; + + const response = await ordersApi.get( + `/api/v1/data-consult/products/${encodeURIComponent(term)}` + ); + return unwrapApiData(response, productsResponseSchema, []); + }, + + }; diff --git a/src/features/orders/components/SearchBar.tsx b/src/features/orders/components/SearchBar.tsx index 626bfe5..940b2f3 100644 --- a/src/features/orders/components/SearchBar.tsx +++ b/src/features/orders/components/SearchBar.tsx @@ -20,6 +20,8 @@ import { 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'; @@ -39,6 +41,10 @@ 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; @@ -54,6 +60,10 @@ 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, @@ -73,6 +83,10 @@ 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<{ @@ -94,6 +108,8 @@ export const SearchBar = () => { const handleReset = useCallback(() => { setTouchedFields({}); setCustomerSearchTerm(''); + setPartnerSearchTerm(''); + setProductSearchTerm(''); const resetState = { status: null, @@ -103,7 +119,6 @@ export const SearchBar = () => { codusur2: null, store: null, orderId: null, - productId: null, stockId: null, hasPreBox: false, includeCheckout: false, @@ -112,6 +127,10 @@ export const SearchBar = () => { searchTriggered: false, customerId: null, customerName: null, + partnerId: null, + partnerName: null, + productId: null, + productName: null, }; setLocalFilters(getInitialLocalFilters(resetState)); @@ -172,6 +191,8 @@ export const SearchBar = () => { localFilters.store?.length, localFilters.stockId?.length, localFilters.sellerId, + localFilters.partnerId, + localFilters.productId, ].filter(Boolean).length; return ( @@ -601,6 +622,139 @@ export const SearchBar = () => { }} /> + + {/* Autocomplete do MUI para Parceiro */} + + option.label} + isOptionEqualToValue={(option, value) => option.id === value.id} + value={ + partners.options.find( + (option) => + localFilters.partnerId === + option.partner?.id?.toString() + ) || null + } + onChange={(_, newValue) => { + if (!newValue) { + updateLocalFilter('partnerName', null); + updateLocalFilter('partnerId', null); + setPartnerSearchTerm(''); + return; + } + + updateLocalFilter( + 'partnerId', + newValue.partner?.id?.toString() || null + ); + updateLocalFilter( + 'partnerName', + newValue.partner?.nome || null + ); + }} + onInputChange={(_, newInputValue, reason) => { + if (reason === 'clear') { + updateLocalFilter('partnerName', null); + updateLocalFilter('partnerId', null); + setPartnerSearchTerm(''); + return; + } + + if (reason === 'input') { + setPartnerSearchTerm(newInputValue); + if (!newInputValue) { + updateLocalFilter('partnerName', null); + updateLocalFilter('partnerId', null); + setPartnerSearchTerm(''); + } + } + }} + loading={partners.isLoading} + renderInput={(params: AutocompleteRenderInputParams) => ( + + )} + noOptionsText={ + partnerSearchTerm.length < 2 + ? 'Digite pelo menos 2 caracteres' + : 'Nenhum parceiro encontrado' + } + loadingText="Buscando parceiros..." + filterOptions={(x) => x} + clearOnBlur={false} + selectOnFocus + handleHomeEndKeys + /> + + + {/* Autocomplete do MUI para Produto */} + + option.label} + isOptionEqualToValue={(option, value) => option.id === value.id} + value={ + products.options.find( + (option) => localFilters.productId === option.product?.id + ) || null + } + onChange={(_, newValue) => { + if (!newValue) { + updateLocalFilter('productName', null); + updateLocalFilter('productId', null); + setProductSearchTerm(''); + return; + } + + updateLocalFilter('productId', newValue.product?.id || null); + updateLocalFilter( + 'productName', + newValue.product?.description || null + ); + }} + onInputChange={(_, newInputValue, reason) => { + if (reason === 'clear') { + updateLocalFilter('productName', null); + updateLocalFilter('productId', null); + setProductSearchTerm(''); + return; + } + + if (reason === 'input') { + setProductSearchTerm(newInputValue); + if (!newInputValue) { + updateLocalFilter('productName', null); + updateLocalFilter('productId', null); + setProductSearchTerm(''); + } + } + }} + loading={products.isLoading} + renderInput={(params: AutocompleteRenderInputParams) => ( + + )} + noOptionsText={ + productSearchTerm.length < 2 + ? 'Digite pelo menos 2 caracteres' + : 'Nenhum produto encontrado' + } + loadingText="Buscando produtos..." + filterOptions={(x) => x} + clearOnBlur={false} + selectOnFocus + handleHomeEndKeys + /> + diff --git a/src/features/orders/hooks/useOrderFilters.ts b/src/features/orders/hooks/useOrderFilters.ts index dabce54..92bee7a 100644 --- a/src/features/orders/hooks/useOrderFilters.ts +++ b/src/features/orders/hooks/useOrderFilters.ts @@ -16,12 +16,15 @@ 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), diff --git a/src/features/orders/hooks/usePartners.ts b/src/features/orders/hooks/usePartners.ts new file mode 100644 index 0000000..08a5691 --- /dev/null +++ b/src/features/orders/hooks/usePartners.ts @@ -0,0 +1,48 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { orderService } from '../api/order.service'; + +export interface Partner { + id: number; + cpf: string; + nome: string; +} + +export function usePartners(searchTerm: string) { + const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(searchTerm); + + useEffect(() => { + const timer = setTimeout(() => { + setDebouncedSearchTerm(searchTerm); + }, 300); + + return () => clearTimeout(timer); + }, [searchTerm]); + + const isEnabled = debouncedSearchTerm.length >= 2; + + const query = useQuery({ + queryKey: ['partners', debouncedSearchTerm], + queryFn: () => orderService.findPartners(debouncedSearchTerm), + enabled: isEnabled, + staleTime: 1000 * 60 * 5, + retry: 1, + retryOnMount: false, + refetchOnWindowFocus: false, + }); + + const options = + query.data?.map((partner, index) => ({ + value: partner.id.toString(), + label: partner.nome, + id: `partner-${partner.id}-${index}`, + partner: partner, + })) ?? []; + + return { + ...query, + options, + }; +} diff --git a/src/features/orders/hooks/useProducts.ts b/src/features/orders/hooks/useProducts.ts new file mode 100644 index 0000000..4c57153 --- /dev/null +++ b/src/features/orders/hooks/useProducts.ts @@ -0,0 +1,47 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { orderService } from '../api/order.service'; + +export interface Product { + id: number; + description: string; +} + +export function useProducts(searchTerm: string) { + const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(searchTerm); + + useEffect(() => { + const timer = setTimeout(() => { + setDebouncedSearchTerm(searchTerm); + }, 300); + + return () => clearTimeout(timer); + }, [searchTerm]); + + const isEnabled = debouncedSearchTerm.length >= 2; + + const query = useQuery({ + queryKey: ['products', debouncedSearchTerm], + queryFn: () => orderService.findProducts(debouncedSearchTerm), + enabled: isEnabled, + staleTime: 1000 * 60 * 5, + retry: 1, + retryOnMount: false, + refetchOnWindowFocus: false, + }); + + const options = + query.data?.map((product, index) => ({ + value: product.id.toString(), + label: product.description, + id: `product-${product.id}-${index}`, + product: product, + })) ?? []; + + return { + ...query, + options, + }; +} diff --git a/src/features/orders/schemas/api-responses.schema.ts b/src/features/orders/schemas/api-responses.schema.ts index c8fe4d4..a6144ee 100644 --- a/src/features/orders/schemas/api-responses.schema.ts +++ b/src/features/orders/schemas/api-responses.schema.ts @@ -2,6 +2,8 @@ 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'; /** @@ -39,3 +41,15 @@ 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)); diff --git a/src/features/orders/schemas/order-filters.schema.ts b/src/features/orders/schemas/order-filters.schema.ts index f8d4580..f400fc5 100644 --- a/src/features/orders/schemas/order-filters.schema.ts +++ b/src/features/orders/schemas/order-filters.schema.ts @@ -16,6 +16,7 @@ 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(), @@ -27,6 +28,7 @@ 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(), @@ -98,19 +100,31 @@ const formatValueToString = (val: any): string => { */ export const orderApiParamsSchema = findOrdersSchema .transform((filters) => { - // Remove customerName quando customerId existe (evita redundância) - const { customerName, customerId, ...rest } = filters; - return customerId ? { customerId, ...rest } : 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; }) .transform((filters) => { - // Mapeamento de chaves que precisam ser renomeadas const keyMap: Record = { 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; diff --git a/src/features/orders/schemas/order.schema.ts b/src/features/orders/schemas/order.schema.ts index 7f87de1..27a16d7 100644 --- a/src/features/orders/schemas/order.schema.ts +++ b/src/features/orders/schemas/order.schema.ts @@ -26,6 +26,16 @@ 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'; diff --git a/src/features/orders/schemas/partner.schema.ts b/src/features/orders/schemas/partner.schema.ts new file mode 100644 index 0000000..92d180c --- /dev/null +++ b/src/features/orders/schemas/partner.schema.ts @@ -0,0 +1,15 @@ +import { z } from 'zod'; + +/** + * Schema Zod para validar um parceiro. + */ +export const partnerSchema = z.object({ + id: z.number(), + cpf: z.string(), + nome: z.string(), +}); + +/** + * Type para parceiro inferido do schema. + */ +export type Partner = z.infer; diff --git a/src/features/orders/schemas/product.schema.ts b/src/features/orders/schemas/product.schema.ts new file mode 100644 index 0000000..3f86a11 --- /dev/null +++ b/src/features/orders/schemas/product.schema.ts @@ -0,0 +1,14 @@ +import { z } from 'zod'; + +/** + * Schema Zod para validar um produto na busca. + */ +export const productSchema = z.object({ + id: z.number(), + description: z.string(), +}); + +/** + * Type para produto inferido do schema. + */ +export type Product = z.infer; diff --git a/src/features/profile/components/ProfilePage.tsx b/src/features/profile/components/ProfilePage.tsx index b8061c5..507cc2c 100644 --- a/src/features/profile/components/ProfilePage.tsx +++ b/src/features/profile/components/ProfilePage.tsx @@ -7,15 +7,12 @@ 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 { useAuthStore } from '../../login/store/useAuthStore'; -import { useAuth } from '../../login/hooks/useAuth'; +import Avatar from '@mui/material/Avatar'; +import Chip from '@mui/material/Chip'; +import { useColaboradorAtual } from '../hooks/useColaboradorAtual'; export default function ProfilePage() { - const { useMe } = useAuth(); - const { data: profile, isLoading, error } = useMe(); - const user = useAuthStore((s) => s.user); - - const displayData = profile || user; + const { data: colaborador, isLoading, error } = useColaboradorAtual(); if (isLoading) { return ( @@ -36,12 +33,53 @@ export default function ProfilePage() { return Erro ao carregar dados do perfil; } - if (!displayData) { + if (!colaborador) { return Nenhum dado de perfil disponível; } return ( + {/* Header com Avatar e Nome */} + + + + + + {colaborador.iniciais} + + + + {colaborador.nome} + + + {colaborador.nomeComFilial} + + + {colaborador.ehRepresentanteComercial && ( + + )} + {colaborador.podeAplicarDesconto && ( + + )} + + + + + + + + {/* Informações Pessoais */} @@ -54,7 +92,7 @@ export default function ProfilePage() { Nome - {displayData.nome || '-'} + {colaborador.nome || '-'} @@ -62,7 +100,7 @@ export default function ProfilePage() { Usuário - {displayData.userName || '-'} + {colaborador.userName || '-'} @@ -70,7 +108,7 @@ export default function ProfilePage() { Matrícula - {displayData.matricula || '-'} + {colaborador.matricula || '-'} @@ -78,6 +116,7 @@ export default function ProfilePage() { + {/* Informações Profissionais */} @@ -90,8 +129,7 @@ export default function ProfilePage() { Filial - {displayData.nomeFilial || '-'} ( - {displayData.codigoFilial || '-'}) + {colaborador.nomeFilial || '-'} ({colaborador.codigoFilial || '-'}) @@ -99,16 +137,16 @@ export default function ProfilePage() { RCA - {displayData.rca || '-'} + {colaborador.ehRepresentanteComercial ? colaborador.codigoRCA : 'Não é vendedor'} - {displayData.discountPercent !== undefined && ( + {colaborador.podeAplicarDesconto && ( - Desconto (%) + Desconto Disponível - {displayData.discountPercent}% + {colaborador.percentualDesconto}% )} @@ -117,9 +155,9 @@ export default function ProfilePage() { - {(displayData.sectorId !== undefined || - displayData.sectorManagerId !== undefined || - displayData.supervisorId !== undefined) && ( + {/* Informações Adicionais */} + {(colaborador.codigoSetor !== undefined || + colaborador.codigoSupervisor !== undefined) && ( @@ -127,23 +165,23 @@ export default function ProfilePage() { Informações Adicionais - {displayData.sectorId !== undefined && ( + {colaborador.codigoSetor !== undefined && ( Setor - {displayData.sectorId} + {colaborador.codigoSetor} )} - {displayData.supervisorId !== undefined && ( + {colaborador.codigoSupervisor !== undefined && ( Supervisor - {displayData.supervisorId} + {colaborador.codigoSupervisor} )} @@ -155,3 +193,4 @@ export default function ProfilePage() { ); } + diff --git a/src/features/profile/domain/Colaborador.ts b/src/features/profile/domain/Colaborador.ts new file mode 100644 index 0000000..0de80b3 --- /dev/null +++ b/src/features/profile/domain/Colaborador.ts @@ -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); + } +} + diff --git a/src/features/profile/hooks/useColaboradorAtual.ts b/src/features/profile/hooks/useColaboradorAtual.ts new file mode 100644 index 0000000..c5d940a --- /dev/null +++ b/src/features/profile/hooks/useColaboradorAtual.ts @@ -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 { + return useQuery({ + queryKey: COLABORADOR_ATUAL_QUERY_KEY, + queryFn: () => profileService.obterColaboradorAtual(), + retry: false, + staleTime: Infinity, + }); +} diff --git a/src/features/profile/index.ts b/src/features/profile/index.ts index d580e7e..59bdeb7 100644 --- a/src/features/profile/index.ts +++ b/src/features/profile/index.ts @@ -1,3 +1,5 @@ // Profile feature barrel export 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'; diff --git a/src/features/profile/services/profile.service.ts b/src/features/profile/services/profile.service.ts index 57f9576..d0924aa 100644 --- a/src/features/profile/services/profile.service.ts +++ b/src/features/profile/services/profile.service.ts @@ -1,9 +1,23 @@ import { profileApi } from '../api'; -import { UserProfile } from '../types'; +import { UserProfileDto } from '../types'; +import { Colaborador } from '../domain/Colaborador'; export const profileService = { - getMe: async (): Promise => { - const response = await profileApi.get('/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 => { + const response = await profileApi.get('/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 => { + const response = await profileApi.get('/auth/me'); return response.data; }, }; diff --git a/src/features/profile/types.ts b/src/features/profile/types.ts index 07a5876..27e17ba 100644 --- a/src/features/profile/types.ts +++ b/src/features/profile/types.ts @@ -1,7 +1,7 @@ /** * Tipagem da resposta do User Info (Endpoint 1.3) */ -export interface UserProfile { +export interface UserProfileDto { matricula: number; userName: string; nome: string;