diff --git a/frontend/src/components/ChartSwitcher/ExpenseChart.jsx b/frontend/src/components/ChartSwitcher/ExpenseChart.jsx index 28d868a..db2411b 100644 --- a/frontend/src/components/ChartSwitcher/ExpenseChart.jsx +++ b/frontend/src/components/ChartSwitcher/ExpenseChart.jsx @@ -1,6 +1,6 @@ import { PieChart, Pie, Cell, Tooltip, ResponsiveContainer } from 'recharts'; import { avatarBackgroundColors } from '../../mockData/colorsPallete/colors'; -import { useMeDashboardQuery } from '../../hooks/ReactQuery/useMeDashboardStatsQuery'; +import { useMeDashboardQuery } from '../../hooks/ReactQuery/Queries/useMeDashboardStatsQuery'; export default function ExpenseQuantityDonutChart() { const { data: expenseData } = useMeDashboardQuery(); diff --git a/frontend/src/components/LatestExpenses/index.jsx b/frontend/src/components/LatestExpenses/index.jsx index bc98f17..78e1221 100644 --- a/frontend/src/components/LatestExpenses/index.jsx +++ b/frontend/src/components/LatestExpenses/index.jsx @@ -5,8 +5,9 @@ import { formatDateBR, formatRelativeDate, } from '../../utils/formatters'; -import useMeExpensesQuery from '../../hooks/ReactQuery/useMeExpensesQuery'; + import Pagination from '../PaginationController'; +import useMeExpensesQuery from '../../hooks/ReactQuery/Queries/useMeExpensesQuery'; export default function LatestExpenses() { // 🔢 Estado da paginação diff --git a/frontend/src/components/Layout/DropdownMenu/index.jsx b/frontend/src/components/Layout/DropdownMenu/index.jsx index b1fd545..184758b 100644 --- a/frontend/src/components/Layout/DropdownMenu/index.jsx +++ b/frontend/src/components/Layout/DropdownMenu/index.jsx @@ -3,11 +3,12 @@ import { navItems } from './navItems'; import SwitchTheme from './SwitchTheme'; import { FaMoon, FaSun } from 'react-icons/fa'; import { useLogout } from '../../../hooks/useLogout'; -import useMeQuery from '../../../hooks/ReactQuery/useMeQuery'; +import useMeQuery from '../../../hooks/ReactQuery/Queries/useMeQuery'; export default function DropdownMenu() { const logout = useLogout(); const { data: user, isLoading, error } = useMeQuery(); + const firstLetter = user?.name[0]?.toUpperCase() ?? '?'; return ( diff --git a/frontend/src/components/Modal/ModalButton.jsx b/frontend/src/components/Modal/ModalButton.jsx index 16728cc..968bb1e 100644 --- a/frontend/src/components/Modal/ModalButton.jsx +++ b/frontend/src/components/Modal/ModalButton.jsx @@ -2,8 +2,8 @@ import { useState } from 'react'; import ButtonUI from '../ui/Button'; import Modal from '.'; -export default function ModalButton({ modalChildren }) { - const [isOpenModal, setIsOpenModal] = useState(false); +export default function ModalButton({ modalChildren, openState = false }) { + const [isOpenModal, setIsOpenModal] = useState(openState); return ( <> diff --git a/frontend/src/components/forms/ExpenseForm/FormFieldsExpenses/index.jsx b/frontend/src/components/forms/ExpenseForm/FormFieldsExpenses/index.jsx index bb71e76..609c70a 100644 --- a/frontend/src/components/forms/ExpenseForm/FormFieldsExpenses/index.jsx +++ b/frontend/src/components/forms/ExpenseForm/FormFieldsExpenses/index.jsx @@ -4,7 +4,7 @@ import SelectUI from '../../../ui/Select'; import TextareaUI from '../../../ui/Textarea'; import CurrencyInputUI from '../../../ui/CurrencyInput'; import { useFormExpense } from '../useForm'; -import { useAllGroupExpenseCategoriesQuery } from '../../../../hooks/ReactQuery/useAllGroupExpenseCategoriesQuery'; +import { useAllGroupExpenseCategoriesQuery } from '../../../../hooks/ReactQuery/Queries/useAllGroupExpenseCategoriesQuery'; export default function FormFieldsExpenses() { const { data: allCategories } = useAllGroupExpenseCategoriesQuery(); diff --git a/frontend/src/components/forms/ExpenseForm/ListMembers/index.jsx b/frontend/src/components/forms/ExpenseForm/ListMembers/index.jsx index a83ac0c..5cd2afe 100644 --- a/frontend/src/components/forms/ExpenseForm/ListMembers/index.jsx +++ b/frontend/src/components/forms/ExpenseForm/ListMembers/index.jsx @@ -1,32 +1,19 @@ -import { useState, useEffect, useMemo } from 'react'; +import { useState, useEffect } from 'react'; import { FiSearch } from 'react-icons/fi'; import ButtonUI from '../../../ui/Button'; - -import useMeQuery from '../../../../hooks/ReactQuery/useMeQuery'; -import { useMembersQuery } from '../../../../hooks/ReactQuery/useMembersQuery'; import InputUI from '../../../ui/Input'; import Pagination from '../../../PaginationController'; -export default function ListMembers({ groupID, onClose, onConfirm }) { - const [search, setSearch] = useState(''); // termo final para API +export default function ListMembers({ + isLoading, + members: { filteredMembers, totalPages }, + onConfirm, + search: { search, setSearch }, +}) { const [inputValue, setInputValue] = useState(''); // valor do input enquanto digita const [page, setPage] = useState(0); - const { data: user } = useMeQuery(); - const { - data: { members, totalPages } = { members: [], totalPages: 0 }, - isLoading, - } = useMembersQuery(groupID, page, search); - const [checkedMembers, setCheckedMembers] = useState([]); - - const excludeSelf = false; // se excluir da lista de membros - - const filteredMembers = useMemo(() => { - if (!members) return []; - return excludeSelf ? members.filter(m => m.id !== user?.id) : members; - }, [members, user, excludeSelf]); - // Alterna seleção de membros const toggleMember = id => { setCheckedMembers(prev => diff --git a/frontend/src/components/forms/ExpenseForm/index.jsx b/frontend/src/components/forms/ExpenseForm/index.jsx index e927913..4c85fcd 100644 --- a/frontend/src/components/forms/ExpenseForm/index.jsx +++ b/frontend/src/components/forms/ExpenseForm/index.jsx @@ -3,21 +3,31 @@ import FormFieldsExpenses from './FormFieldsExpenses'; import ButtonUI from '../../ui/Button'; import CustomSplitAmount from './CustomSplitAmount'; import { useFormExpense } from './useForm'; -import { useEffect, useState, useCallback } from 'react'; +import { useEffect, useState, useCallback, useMemo } from 'react'; import { customToast } from '../../CustomToast'; import Modal from '../../Modal'; import ListMembers from './ListMembers'; import { FiUserPlus, FiX } from 'react-icons/fi'; -/* import useMeQuery from '../../../hooks/ReactQuery/useMeQuery'; */ -import { useMembersQuery } from '../../../hooks/ReactQuery/useMembersQuery'; -import { useCreateExpenseMutation } from '../../../hooks/ReactQuery/useCreateExpenseMutation'; +import useMeQuery from '../../../hooks/ReactQuery/Queries/useMeQuery'; +import { useCreateExpenseMutation } from '../../../hooks/ReactQuery/Mutations/useCreateExpenseMutation'; +import { useMembersQuery } from '../../../hooks/ReactQuery/Queries/useMembersQuery'; export default function ExpenseForm({ groupData: group }) { - /* const { data: user } = useMeQuery(); */ + const [search, setSearch] = useState(''); // termo final para API + const { data: user } = useMeQuery(); const useExpenseMutation = useCreateExpenseMutation(group?.id); - const { - data: { members: groupMembers }, - } = useMembersQuery(group?.id, 0, ''); + const { data: { members: groupMembers = [], totalPages } = {}, isLoading } = + useMembersQuery(group?.id, 0, search); + + const excludeSelf = true; // 🔧 se quiser incluir o próprio usuário, basta trocar para false + + // 🔍 Aplica filtro de exclusão do próprio usuário + const filteredMembers = useMemo(() => { + if (!groupMembers?.length) return []; + return excludeSelf + ? groupMembers.filter(m => m.id !== user?.id) + : groupMembers; + }, [groupMembers, user, excludeSelf]); const { setMembers, amount, divisionAmount, reset, getRemainingDifference } = useFormExpense(); @@ -123,11 +133,11 @@ export default function ExpenseForm({ groupData: group }) { aria-label="Formulário para adicionar nova despesa" > {/* Campos principais */} - + {/* Seleção de membros */}
- {selectedMembers.length === 0 && groupMembers?.length > 0 && ( + {selectedMembers.length === 0 && filteredMembers.length > 0 && ( setShowMemberModal(true)} @@ -178,10 +188,11 @@ export default function ExpenseForm({ groupData: group }) { type="submit" disabled={isSubmitting || !distributionOK || amount <= 0} aria-disabled={isSubmitting || !distributionOK || amount <= 0} - className={` - bg-primary hover:bg-secondary text-white py-3 px-6 rounded w-full sm:w-auto font-semibold transition - ${isSubmitting || !distributionOK || amount <= 0 ? 'opacity-50 bg-gray-400! cursor-not-allowed!' : 'cursor-pointer'} - `} + className={`bg-primary hover:bg-secondary text-white py-3 px-6 rounded w-full sm:w-auto font-semibold transition ${ + isSubmitting || !distributionOK || amount <= 0 + ? 'opacity-50 bg-gray-400! cursor-not-allowed!' + : 'cursor-pointer' + }`} > {isSubmitting ? 'Enviando...' : 'Adicionar Despesa'} @@ -191,12 +202,14 @@ export default function ExpenseForm({ groupData: group }) { {/* Modal de seleção */} setShowMemberModal(false)}> { setSelectedMembers(selected); setMembers(selected); setShowMemberModal(false); }} + isLoading={isLoading} onClose={() => setShowMemberModal(false)} /> diff --git a/frontend/src/hooks/ReactQuery/Mutations/useCreateExpenseMutation.js b/frontend/src/hooks/ReactQuery/Mutations/useCreateExpenseMutation.js new file mode 100644 index 0000000..cb0437e --- /dev/null +++ b/frontend/src/hooks/ReactQuery/Mutations/useCreateExpenseMutation.js @@ -0,0 +1,31 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { createExpense } from '../../../services/expenses'; +import { REACTQUERY_KEYS } from '../../../libs/ReactQuery/keys'; + +export function useCreateExpenseMutation(groupID) { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: expenseData => createExpense(groupID, expenseData), + + onSuccess: () => { + // 🔄 Invalida TODAS as queries de despesas desse grupo + queryClient.invalidateQueries({ + predicate: query => { + const [key, params] = query.queryKey; + + return ( + key === REACTQUERY_KEYS.GROUPS.EXPENSES && + typeof params === 'object' && + params?.groupID === groupID + ); + }, + }); + + // 🔄 Atualiza listagem geral de grupos + queryClient.invalidateQueries({ + queryKey: [REACTQUERY_KEYS.GROUPS.ALL], + }); + }, + }); +} diff --git a/frontend/src/hooks/ReactQuery/Mutations/useDeleteGroupMutation.js b/frontend/src/hooks/ReactQuery/Mutations/useDeleteGroupMutation.js new file mode 100644 index 0000000..2bf8573 --- /dev/null +++ b/frontend/src/hooks/ReactQuery/Mutations/useDeleteGroupMutation.js @@ -0,0 +1,48 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { deleteGroup } from '../../../services/groups'; +import { REACTQUERY_KEYS } from '../../../libs/ReactQuery/keys'; +import { useNavigate } from 'react-router'; +import { customToast } from '../../../components/CustomToast'; + +export function useDeleteGroupMutation(id) { + const navigate = useNavigate(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: () => deleteGroup(id), + onSuccess: () => { + // Invalida a lista de grupos para forçar um refetch + queryClient.invalidateQueries({ + predicate: query => + query.queryKey[0] === REACTQUERY_KEYS.GROUPS.ALL && + query.queryKey[1]?.groupID === id, // só o id importa + }); + queryClient.invalidateQueries({ + predicate: query => + query.queryKey[0] === REACTQUERY_KEYS.GROUPS.EXPENSES && + query.queryKey[1]?.groupID === id, // só o id importa + }); + queryClient.invalidateQueries([REACTQUERY_KEYS.GROUPS.MEMBERS, id]); + + // Opcional: remover manualmente do cache local + queryClient.setQueryData([REACTQUERY_KEYS.GROUPS.ALL], oldData => { + if (!oldData) return oldData; + return { + ...oldData, + content: oldData.content.filter(group => group.id !== id), + totalElements: oldData.totalElements - 1, + }; + }); + + customToast('Exclusão', 'Grupo excluído com sucesso', 'success'); + navigate('/groups'); + }, + onError: error => { + customToast( + 'Erro', + error.response.data.message || 'Erro ao excluir grupo.', + 'error' + ); + }, + }); +} diff --git a/frontend/src/hooks/ReactQuery/Mutations/useUpdateGroupExpenseByIdMutation.js b/frontend/src/hooks/ReactQuery/Mutations/useUpdateGroupExpenseByIdMutation.js new file mode 100644 index 0000000..e347379 --- /dev/null +++ b/frontend/src/hooks/ReactQuery/Mutations/useUpdateGroupExpenseByIdMutation.js @@ -0,0 +1,3 @@ +export default function useUpdateGroupExpenseByIdMutation() { + return; +} diff --git a/frontend/src/hooks/ReactQuery/useUpdateMeMutation.js b/frontend/src/hooks/ReactQuery/Mutations/useUpdateMeMutation.js similarity index 76% rename from frontend/src/hooks/ReactQuery/useUpdateMeMutation.js rename to frontend/src/hooks/ReactQuery/Mutations/useUpdateMeMutation.js index 8c425f4..c5fcf2b 100644 --- a/frontend/src/hooks/ReactQuery/useUpdateMeMutation.js +++ b/frontend/src/hooks/ReactQuery/Mutations/useUpdateMeMutation.js @@ -1,6 +1,6 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { updateMe } from '../../services/me'; -import { REACTQUERY_KEYS } from '../../libs/ReactQuery/keys'; +import { REACTQUERY_KEYS } from '../../../libs/ReactQuery/keys'; +import { updateMe } from '../../../services/me'; export function useUpdateMeMutation() { const queryClient = useQueryClient(); diff --git a/frontend/src/hooks/ReactQuery/useAllGroupExpenseCategoriesQuery.js b/frontend/src/hooks/ReactQuery/Queries/useAllGroupExpenseCategoriesQuery.js similarity index 66% rename from frontend/src/hooks/ReactQuery/useAllGroupExpenseCategoriesQuery.js rename to frontend/src/hooks/ReactQuery/Queries/useAllGroupExpenseCategoriesQuery.js index 130ed2e..c6ada9e 100644 --- a/frontend/src/hooks/ReactQuery/useAllGroupExpenseCategoriesQuery.js +++ b/frontend/src/hooks/ReactQuery/Queries/useAllGroupExpenseCategoriesQuery.js @@ -1,6 +1,6 @@ import { useQuery } from '@tanstack/react-query'; -import { REACTQUERY_KEYS } from '../../libs/ReactQuery/keys'; -import { getAllGroupsExpenseCategories } from '../../services/groups'; +import { REACTQUERY_KEYS } from '../../../libs/ReactQuery/keys'; +import { getAllGroupsExpenseCategories } from '../../../services/groups'; export function useAllGroupExpenseCategoriesQuery() { return useQuery({ diff --git a/frontend/src/hooks/ReactQuery/Queries/useGroupExpenseByIdQuery.js b/frontend/src/hooks/ReactQuery/Queries/useGroupExpenseByIdQuery.js new file mode 100644 index 0000000..89bc767 --- /dev/null +++ b/frontend/src/hooks/ReactQuery/Queries/useGroupExpenseByIdQuery.js @@ -0,0 +1,25 @@ +import { useQuery } from '@tanstack/react-query'; +import { REACTQUERY_KEYS } from '../../../libs/ReactQuery/keys'; +import { getGroupExpenseById } from '../../../services/groups'; + +export function useGroupExpenseByIdQuery(group_id, expense_id) { + return useQuery({ + queryKey: [REACTQUERY_KEYS.GROUPS.EXPENSES, { group_id, expense_id }], + enabled: !!group_id && !!expense_id, + queryFn: async () => await getGroupExpenseById(group_id, expense_id), + staleTime: Infinity, + placeholderData: { + expenseId: null, + title: '', + description: '', + groupId: null, + groupName: '', + status: 'PENDING', + total: 0, + categoryId: null, + categoryName: '', + createdAt: new Date().toISOString(), + memberList: [], + }, + }); +} diff --git a/frontend/src/hooks/ReactQuery/Queries/useGroupExpensesQuery.js b/frontend/src/hooks/ReactQuery/Queries/useGroupExpensesQuery.js new file mode 100644 index 0000000..8a2ad91 --- /dev/null +++ b/frontend/src/hooks/ReactQuery/Queries/useGroupExpensesQuery.js @@ -0,0 +1,36 @@ +import { useQuery } from '@tanstack/react-query'; +import { REACTQUERY_KEYS } from '../../../libs/ReactQuery/keys'; +import { getGroupExpenses } from '../../../services/groups'; + +export function useGroupExpensesQuery(groupID, page, filter) { + // 1. Desestrutura as propriedades de filtro para uso no queryKey + // Assumimos que 'status' e 'allGroupMembersExpenses' são as chaves + // que podem vir do objeto 'filter'. + const { status, allGroupMembersExpenses } = filter; + + return useQuery({ + // 2. O queryKey agora inclui explicitamente o 'status' e 'allGroupMembersExpenses'. + // Uma mudança em qualquer um desses valores fará com que o array do queryKey mude, + // forçando o TanStack Query a fazer uma nova busca (refetch). + queryKey: [ + REACTQUERY_KEYS.GROUPS.EXPENSES, + { + groupID, + page, + status, // Chave do filtro 1 + allGroupMembersExpenses, // Chave do filtro 2 (se existir) + }, + ], + // 3. A queryFn continua usando o objeto 'filter' completo para a chamada de API + queryFn: async () => await getGroupExpenses({ groupID, page, filter }), + + // Configurações mantidas + enabled: !!groupID, + staleTime: Infinity, + placeholderData: { + expenses: [], + totalPages: 0, + expensesLength: 0, + }, + }); +} diff --git a/frontend/src/hooks/ReactQuery/useGroupsQuery.js b/frontend/src/hooks/ReactQuery/Queries/useGroupsQuery.js similarity index 76% rename from frontend/src/hooks/ReactQuery/useGroupsQuery.js rename to frontend/src/hooks/ReactQuery/Queries/useGroupsQuery.js index a758b09..2993e5d 100644 --- a/frontend/src/hooks/ReactQuery/useGroupsQuery.js +++ b/frontend/src/hooks/ReactQuery/Queries/useGroupsQuery.js @@ -1,6 +1,6 @@ import { useQuery } from '@tanstack/react-query'; -import { REACTQUERY_KEYS } from '../../libs/ReactQuery/keys'; -import { getGroupById, getGroups } from '../../services/groups'; +import { REACTQUERY_KEYS } from '../../../libs/ReactQuery/keys'; +import { getGroupById, getGroups } from '../../../services/groups'; export function useGroupsQuery(page) { return useQuery({ @@ -14,12 +14,13 @@ export function useGroupsQuery(page) { }, }); } + export function useGroupByIdQuery(id) { return useQuery({ queryKey: [REACTQUERY_KEYS.GROUPS.ALL, id], enabled: !!id, queryFn: async () => await getGroupById(id), staleTime: Infinity, - placeholderData: { content: {} }, + placeholderData: {}, }); } diff --git a/frontend/src/hooks/ReactQuery/useMeDashboardStatsQuery.js b/frontend/src/hooks/ReactQuery/Queries/useMeDashboardStatsQuery.js similarity index 64% rename from frontend/src/hooks/ReactQuery/useMeDashboardStatsQuery.js rename to frontend/src/hooks/ReactQuery/Queries/useMeDashboardStatsQuery.js index 3432c72..57e0c2f 100644 --- a/frontend/src/hooks/ReactQuery/useMeDashboardStatsQuery.js +++ b/frontend/src/hooks/ReactQuery/Queries/useMeDashboardStatsQuery.js @@ -1,6 +1,7 @@ import { useQuery } from '@tanstack/react-query'; -import { REACTQUERY_KEYS } from '../../libs/ReactQuery/keys'; -import { getMeDashboardStats } from '../../services/me'; + +import { REACTQUERY_KEYS } from '../../../libs/ReactQuery/keys'; +import { getMeDashboardStats } from '../../../services/me'; export function useMeDashboardQuery() { return useQuery({ diff --git a/frontend/src/hooks/ReactQuery/useMeExpensesQuery.js b/frontend/src/hooks/ReactQuery/Queries/useMeExpensesQuery.js similarity index 79% rename from frontend/src/hooks/ReactQuery/useMeExpensesQuery.js rename to frontend/src/hooks/ReactQuery/Queries/useMeExpensesQuery.js index 429d347..012cb6a 100644 --- a/frontend/src/hooks/ReactQuery/useMeExpensesQuery.js +++ b/frontend/src/hooks/ReactQuery/Queries/useMeExpensesQuery.js @@ -1,7 +1,7 @@ import { useQuery } from '@tanstack/react-query'; -import { REACTQUERY_KEYS } from '../../libs/ReactQuery/keys'; -import { getMeExpenses } from '../../services/me'; import useMeQuery from './useMeQuery'; +import { REACTQUERY_KEYS } from '../../../libs/ReactQuery/keys'; +import { getMeExpenses } from '../../../services/me'; export default function useMeExpensesQuery(page) { const { data: user } = useMeQuery(); diff --git a/frontend/src/hooks/ReactQuery/useMeQuery.js b/frontend/src/hooks/ReactQuery/Queries/useMeQuery.js similarity index 58% rename from frontend/src/hooks/ReactQuery/useMeQuery.js rename to frontend/src/hooks/ReactQuery/Queries/useMeQuery.js index 19c1d03..5f7379f 100644 --- a/frontend/src/hooks/ReactQuery/useMeQuery.js +++ b/frontend/src/hooks/ReactQuery/Queries/useMeQuery.js @@ -1,6 +1,6 @@ import { useQuery } from '@tanstack/react-query'; -import { REACTQUERY_KEYS } from '../../libs/ReactQuery/keys'; -import { getMe } from '../../services/me'; +import { REACTQUERY_KEYS } from '../../../libs/ReactQuery/keys'; +import { getMe } from '../../../services/me'; export default function useMeQuery() { return useQuery({ @@ -8,5 +8,6 @@ export default function useMeQuery() { queryFn: getMe, staleTime: Infinity, refetchInterval: false, + placeholderData: { name: '', themeColor: '' }, }); } diff --git a/frontend/src/hooks/ReactQuery/useMembersQuery.js b/frontend/src/hooks/ReactQuery/Queries/useMembersQuery.js similarity index 57% rename from frontend/src/hooks/ReactQuery/useMembersQuery.js rename to frontend/src/hooks/ReactQuery/Queries/useMembersQuery.js index 8c730aa..1ce4603 100644 --- a/frontend/src/hooks/ReactQuery/useMembersQuery.js +++ b/frontend/src/hooks/ReactQuery/Queries/useMembersQuery.js @@ -1,14 +1,15 @@ import { useQuery } from '@tanstack/react-query'; -import { REACTQUERY_KEYS } from '../../libs/ReactQuery/keys'; -import { getMembers } from '../../services/groups'; + +import { REACTQUERY_KEYS } from '../../../libs/ReactQuery/keys'; +import { getGroupMembers } from '../../../services/groups'; export const useMembersQuery = (groupID, page, search) => { return useQuery({ queryKey: search ? [REACTQUERY_KEYS.MEMBERS.ALL, groupID, page, search] : [REACTQUERY_KEYS.MEMBERS.ALL, groupID, page], - queryFn: () => getMembers(groupID, page, search), + queryFn: () => getGroupMembers({ groupId: groupID, page, search }), placeholderData: [], - enabled: !!groupID && page >= 0, + enabled: !!groupID, }); }; diff --git a/frontend/src/hooks/ReactQuery/useCreateExpenseMutation.js b/frontend/src/hooks/ReactQuery/useCreateExpenseMutation.js deleted file mode 100644 index ffa6f76..0000000 --- a/frontend/src/hooks/ReactQuery/useCreateExpenseMutation.js +++ /dev/null @@ -1,15 +0,0 @@ -import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { REACTQUERY_KEYS } from '../../libs/ReactQuery/keys'; -import { createExpense } from '../../services/expenses'; - -export function useCreateExpenseMutation(groupID) { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: expenseData => createExpense(groupID, expenseData), - onSuccess: () => { - // Atualiza apenas as despesas do grupo específico - queryClient.invalidateQueries([REACTQUERY_KEYS.GROUPS.EXPENSES, groupID]); - }, - }); -} diff --git a/frontend/src/hooks/ReactQuery/useDeleteGroupMutation.js b/frontend/src/hooks/ReactQuery/useDeleteGroupMutation.js deleted file mode 100644 index 7fa3ea9..0000000 --- a/frontend/src/hooks/ReactQuery/useDeleteGroupMutation.js +++ /dev/null @@ -1,25 +0,0 @@ -import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { REACTQUERY_KEYS } from '../../libs/ReactQuery/keys'; -import { deleteGroup } from '../../services/groups'; - -export function useDeleteGroupMutation(id) { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: () => deleteGroup(id), - onSuccess: () => { - // Invalida a lista de grupos para forçar um refetch - queryClient.invalidateQueries([REACTQUERY_KEYS.GROUPS.ALL]); - - // Opcional: remover manualmente do cache local - queryClient.setQueryData([REACTQUERY_KEYS.GROUPS.ALL], oldData => { - if (!oldData) return oldData; - return { - ...oldData, - content: oldData.content.filter(group => group.id !== id), - totalElements: oldData.totalElements - 1, - }; - }); - }, - }); -} diff --git a/frontend/src/hooks/ReactQuery/useGroupMembersQuery.js b/frontend/src/hooks/ReactQuery/useGroupMembersQuery.js deleted file mode 100644 index 76e8fc9..0000000 --- a/frontend/src/hooks/ReactQuery/useGroupMembersQuery.js +++ /dev/null @@ -1,13 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; -import { REACTQUERY_KEYS } from '../../libs/ReactQuery/keys'; -import { getGroupMembers } from '../../services/groups'; - -export function useGroupMembersQuery(group_id) { - return useQuery({ - queryKey: [REACTQUERY_KEYS.GROUPS.MEMBERS, group_id], - queryFn: async () => await getGroupMembers(group_id), - staleTime: Infinity, - enabled: !!group_id, - placeholderData: { content: [] }, - }); -} diff --git a/frontend/src/libs/ReactQuery/keys.js b/frontend/src/libs/ReactQuery/keys.js index e60cfdb..db5d289 100644 --- a/frontend/src/libs/ReactQuery/keys.js +++ b/frontend/src/libs/ReactQuery/keys.js @@ -7,7 +7,6 @@ export const REACTQUERY_KEYS = { GROUPS: { ALL: 'allGroups', MEMBERS: 'members', - DETAILS: 'groupDetails', EXPENSES: 'groupExpenses', }, EXPENSES: { diff --git a/frontend/src/libs/ReactQuery/queryClient.js b/frontend/src/libs/ReactQuery/queryClient.js index 01ae1fe..0ca175c 100644 --- a/frontend/src/libs/ReactQuery/queryClient.js +++ b/frontend/src/libs/ReactQuery/queryClient.js @@ -7,8 +7,13 @@ const refetchInterval = 180000; // 3 minutos export const queryClient = new QueryClient({ defaultOptions: { queries: { + retry: 1, refetchInterval, refetchOnWindowFocus: true, + refetchOnReconnect: true, + }, + mutations: { + retry: false, }, }, }); diff --git a/frontend/src/pages/Expenses/index.jsx b/frontend/src/pages/Expenses/index.jsx index 2b0dd3b..96d7cb9 100644 --- a/frontend/src/pages/Expenses/index.jsx +++ b/frontend/src/pages/Expenses/index.jsx @@ -2,7 +2,7 @@ import ExpenseForm from '../../components/forms/ExpenseForm'; export default function Expenses({ groupData }) { return ( -
+
); diff --git a/frontend/src/pages/Groups/GroupDetails/ExpensesList.jsx b/frontend/src/pages/Groups/GroupDetails/ExpensesList.jsx index 292a985..03a7d73 100644 --- a/frontend/src/pages/Groups/GroupDetails/ExpensesList.jsx +++ b/frontend/src/pages/Groups/GroupDetails/ExpensesList.jsx @@ -1,179 +1,189 @@ import { useState, useMemo } from 'react'; -import { FiSearch } from 'react-icons/fi'; +import { TbPigMoney } from 'react-icons/tb'; import { formatBRL } from '../../../utils/formatters'; -import InputUI from '../../../components/ui/Input'; +import { Link } from 'react-router'; + import SelectUI from '../../../components/ui/Select'; import CheckboxUI from '../../../components/ui/Checkbox'; -import { Link } from 'react-router'; +import ButtonUI from '../../../components/ui/Button'; +import Pagination from '../../../components/PaginationController'; +import Modal from '../../../components/Modal'; + +import { usePermissions } from './usePermissions'; +import { useGroupExpensesQuery } from '../../../hooks/ReactQuery/Queries/useGroupExpensesQuery'; +import { EXPENSE_STATUS } from './statusExpense'; -export const expenses = [ - { - id: 1, - description: 'Pizza da sexta-feira', - amount: 128.5, - status: 'PENDING', - payerName: 'LunaStar', - createdAt: '2025-10-29T20:15:00Z', - }, - { - id: 2, - description: 'Supermercado do mês', - amount: 450.0, - status: 'PAID', - payerName: 'Clark Kent', - createdAt: '2025-10-25T14:45:00Z', - }, - { - id: 3, - description: 'Assinatura Netflix', - amount: 55.9, - status: 'PAID', - payerName: 'LunaStar', - createdAt: '2025-10-20T12:00:00Z', - }, - { - id: 4, - description: 'Taxi para o evento', - amount: 79.75, - status: 'PENDING', - payerName: 'Clark Kent', - createdAt: '2025-10-30T08:30:00Z', - }, - { - id: 5, - description: 'Jantar no restaurante', - amount: 230.0, - status: 'PENDING', - payerName: 'LunaStar', - createdAt: '2025-10-31T22:10:00Z', - }, - { - id: 6, - description: 'Conta de energia', - amount: 189.9, - status: 'PAID', - payerName: 'Clark Kent', - createdAt: '2025-10-28T10:45:00Z', - }, -]; - -export default function ExpensesList({ group }) { - const [search, setSearch] = useState(''); - const [status, setStatus] = useState('ALL'); +export default function ExpensesList({ groupID, authorization }) { + const [page, setPage] = useState(0); + const [status, setStatus] = useState('PENDING'); const [allMembers, setAllMembers] = useState(false); - const isLoading = false; - - // 💡 Filtro local (mock sem API) - const filteredExpenses = useMemo(() => { - return expenses.filter(expense => { - const matchSearch = expense.description - .toLowerCase() - .includes(search.toLowerCase()); - const matchStatus = status === 'ALL' ? true : expense.status === status; - const matchMember = allMembers ? true : expense.payerName === 'LunaStar'; - return matchSearch && matchStatus && matchMember; - }); - }, [search, status, allMembers]); + const [isOpenModal, setIsOpenModal] = useState(false); + + const { canViewAllExpenses } = usePermissions(authorization); + + // 🧩 Filtros estabilizados para o React Query + const queryFilters = useMemo(() => { + const filters = { status }; + if (canViewAllExpenses && allMembers) + filters.allGroupMembersExpenses = true; + return filters; + }, [status, allMembers, canViewAllExpenses]); + + // 🔄 Query de despesas + const { data, isLoading } = useGroupExpensesQuery( + groupID, + page, + queryFilters + ); + + const expenses = data?.expenses ?? []; + const totalPages = data?.totalPages ?? 0; + const expensesLength = data?.expensesLength ?? 0; + + // 🪄 Handlers + const handleStatusChange = e => { + setStatus(e.target.value); + setPage(0); + }; + + const handleAllMembersChange = checked => { + setAllMembers(checked); + setPage(0); + }; + // 🧾 UI return (
-
+ {/* 🧭 Cabeçalho */} +

- Despesas do grupo + + Despesas do Grupo

-
+ {/* 🔍 Filtros */} -
- {/* 🔎 Busca + Status */} -
- {/* Campo de busca */} -
- - setSearch(e.target.value)} - className="pl-9 w-full h-10 rounded-lg border border-border/50 bg-surface-light focus:ring-2 focus:ring-primary/40 transition-all" - /> -
- - {/* Select de status */} -
- setStatus(e.target.value)} - className="w-full sm:w-[160px] h-10 rounded-lg border border-border/50 bg-surface-light text-sm focus:ring-2 focus:ring-primary/40 transition-all cursor-pointer" - > - {[ - { label: 'Todas', value: 'ALL' }, - { label: 'Pendentes', value: 'PENDING' }, - { label: 'Pagas', value: 'PAID' }, - ].map(({ label, value }) => ( - - ))} - -
+
+
+ + {!allMembers ? ( + <> + + + + + ) : ( + + )} +
- {/* 🎚️ Filtro extra */} -
- -
+ {canViewAllExpenses && ( +
+ +
+ )}
- {/* 🧾 Lista */} + {/* 📜 Lista */} {isLoading ? ( -

Carregando despesas...

- ) : filteredExpenses.length > 0 ? ( +

+ Carregando despesas... +

+ ) : expensesLength > 0 ? (
    - {filteredExpenses.map(expense => ( -
  • - -
    - - {expense.description} - - - por {expense.payerName} - -
    - -
    - - - {formatBRL(expense.amount)} - - - {new Date(expense.createdAt).toLocaleDateString('pt-BR')} - -
    - -
  • - ))} + {expenses.map(expense => { + const COLOR_STATUS = + expense.status === 'PAID' + ? 'bg-success text-green-900' + : expense.status === 'PENDING' + ? 'bg-warning text-yellow-900' + : expense.status === 'UNPAID' + ? 'bg-error text-red-900' + : 'text-text'; + return ( +
  • + setIsOpenModal(true)} + className="relative flex w-full justify-between items-center px-4 py-3 rounded-xl bg-muted/20 hover:bg-muted/40 transition-colors border border-border/10 cursor-pointer" + > +
    + + {expense.title} + {expense.status !== null && ( + + {EXPENSE_STATUS[expense.status]} + + )} + +
    + +
    + + {formatBRL(expense.partialValue || expense.total)} + + + Prazo final:{' '} + {expense.deadlineDate + ? new Date(expense.deadlineDate).toLocaleDateString( + 'pt-BR' + ) + : 'Sem prazo'} + +
    +
    +
  • + ); + })}
) : (

- Nenhuma despesa encontrada. + Nenhuma despesa encontrada para os filtros selecionados.

)} + + {/* 📄 Paginação */} + {totalPages > 1 && ( +
+ setPage(p => (p + 1 < totalPages ? p + 1 : p))} + onPrev={() => setPage(p => Math.max(p - 1, 0))} + /> +
+ )} + + {/* 🪟 Modal */} + setIsOpenModal(false)} + setIsOpen={setIsOpenModal} + > +
+ Detalhes da despesa selecionada. +
+
); } diff --git a/frontend/src/pages/Groups/GroupDetails/groupDetailsLoader.js b/frontend/src/pages/Groups/GroupDetails/groupDetailsLoader.js deleted file mode 100644 index 9da71a6..0000000 --- a/frontend/src/pages/Groups/GroupDetails/groupDetailsLoader.js +++ /dev/null @@ -1,44 +0,0 @@ -import { REACTQUERY_KEYS } from '../../../libs/ReactQuery/keys'; -import { queryClient } from '../../../libs/ReactQuery/queryClient'; -import { getGroupById, getGroupMembers } from '../../../services/groups'; - -export async function groupDetailsLoader({ params }) { - const { group_id } = params; - - // 🔹 1. Tenta pegar o grupo do cache - let group = queryClient.getQueryData([ - REACTQUERY_KEYS.GROUPS.DETAILS, - group_id, - ]); - - // 🔹 2. Se não tiver, busca e cacheia - if (!group) { - group = await queryClient.fetchQuery({ - queryKey: [REACTQUERY_KEYS.GROUPS.DETAILS, group_id], - queryFn: () => getGroupById(group_id), - }); - } - - // 🔹 3. Tenta pegar os membros do cache - let members = queryClient.getQueryData([ - REACTQUERY_KEYS.GROUPS.MEMBERS, - group_id, - ]); - - // 🔹 4. Se não tiver no cache, busca e adiciona - if (!members) { - members = await queryClient.fetchQuery({ - queryKey: [REACTQUERY_KEYS.GROUPS.MEMBERS, group_id], - queryFn: () => getGroupMembers(group_id), - staleTime: Infinity, - }); - } - - // 🔹 5. Combina grupo + membros antes de retornar - const groupWithMembers = { - ...group, - members: members?.members || members || [], - }; - console.log({ groupWithMembers }); - return groupWithMembers; -} diff --git a/frontend/src/pages/Groups/GroupDetails/index.jsx b/frontend/src/pages/Groups/GroupDetails/index.jsx index 7e7141f..1e51a24 100644 --- a/frontend/src/pages/Groups/GroupDetails/index.jsx +++ b/frontend/src/pages/Groups/GroupDetails/index.jsx @@ -1,9 +1,4 @@ -import { - useLoaderData, - useLocation, - useNavigate, - useParams, -} from 'react-router'; +import { useNavigate, useParams } from 'react-router'; import { useState } from 'react'; import { FiSettings, FiEdit2, FiTrash2 } from 'react-icons/fi'; import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/react'; @@ -17,34 +12,21 @@ import ModalButton from '../../../components/Modal/ModalButton'; import { CategoryIcon } from '../../../mockData/groupIcons/icons'; import { ConfirmModal } from '../../../components/Modal'; -import { useDeleteGroupMutation } from '../../../hooks/ReactQuery/useDeleteGroupMutation'; import { customToast } from '../../../components/CustomToast'; -import { ROLES } from '../../../utils/constants'; -function useQueryParams() { - const { search } = useLocation(); // ex: "?page=0&size=10" - return new URLSearchParams(search); -} +import { usePermissions } from './usePermissions'; +import { useGroupByIdQuery } from '../../../hooks/ReactQuery/Queries/useGroupsQuery'; +import { useDeleteGroupMutation } from '../../../hooks/ReactQuery/Mutations/useDeleteGroupMutation'; export default function GroupDetails() { - const [isOpenModal, setIsOpenModal] = useState(false); - const group = useLoaderData(); - const navigate = useNavigate(); const { group_id } = useParams(); + const [isOpenModal, setIsOpenModal] = useState(false); + const { data: group } = useGroupByIdQuery(group_id); + const { authorization } = group; + const { canEditGroupInfo, canDeleteGroup, canCreateExpenses } = + usePermissions(authorization); const deleteGroup = useDeleteGroupMutation(group_id); - /* const [showBalances, setShowBalances] = useState(true); */ - - // classes reutilizáveis - /* const baseBtn = - 'cursor-pointer font-medium py-2 px-4 sm:px-6 text-sm sm:text-base rounded-full transition duration-200 focus:outline-none focus:ring-2 focus:ring-opacity-50'; - const activeBtn = - 'bg-primary text-white hover:bg-primary/90 focus:ring-primary'; - const inactiveBtn = - 'bg-neutral text-text hover:bg-neutral/80 focus:ring-muted'; */ - /* if (isLoading) return

Carregando grupo...

; - if (isError) return

Erro ao carregar grupo.

; - if (!group) return

Grupo não encontrado.

; */ return (
setIsOpenModal(false)} onConfirm={() => { - deleteGroup.mutate(undefined, { - onSuccess: () => { - navigate('/groups'); - customToast( - 'Exclusão', - 'Grupo excluído com sucesso', - 'success' - ); - }, - onError: () => { - customToast('Erro', 'Erro ao tentar excluir o grupo', 'error'); - }, - }); + deleteGroup.mutate(); }} confirmLabel="Excluir" message={`Tem certeza de que deseja excluir o grupo "${group?.name}"? Essa ação não pode ser desfeita.`} @@ -90,7 +60,7 @@ export default function GroupDetails() {
{/* Menu de ações */} - {group?.authorization !== ROLES.user && ( + {canEditGroupInfo && canDeleteGroup && (

- {formatBRL(group?.totalExpenses)} + {formatBRL(group?.total)}

acumulado do mês

{group?.description && ( @@ -194,7 +164,7 @@ export default function GroupDetails() { {/* Conteúdo principal */}
- + {/* {showBalances ? ( ) : ( @@ -204,7 +174,7 @@ export default function GroupDetails() { {/* Modal de adicionar despesa */} - {group?.authorization !== ROLES.user && ( + {canCreateExpenses && ( } /> )}
diff --git a/frontend/src/pages/Groups/GroupDetails/statusExpense.js b/frontend/src/pages/Groups/GroupDetails/statusExpense.js new file mode 100644 index 0000000..d16ebb1 --- /dev/null +++ b/frontend/src/pages/Groups/GroupDetails/statusExpense.js @@ -0,0 +1,5 @@ +export const EXPENSE_STATUS = { + PENDING: 'PENDENTE', + UNPAID: 'NÃO PAGO', + PAID: 'PAGO', +}; diff --git a/frontend/src/pages/Groups/GroupDetails/usePermissions.js b/frontend/src/pages/Groups/GroupDetails/usePermissions.js new file mode 100644 index 0000000..690ce08 --- /dev/null +++ b/frontend/src/pages/Groups/GroupDetails/usePermissions.js @@ -0,0 +1,31 @@ +import { hasPermission } from '../../../utils/constants'; + +export function usePermissions(role) { + if (!role) { + return { + canViewOwnExpenses: false, + canViewAllExpenses: false, + canCreateExpenses: false, + canEditExpenses: false, + canEditExpenseValue: false, + canDeleteExpenses: false, + canManageMembers: false, + canEditGroupInfo: false, + canDeleteGroup: false, + canGenerateReports: false, + }; + } + + return { + canViewOwnExpenses: hasPermission(role, 'VIEW_OWN_EXPENSES'), + canViewAllExpenses: hasPermission(role, 'VIEW_ALL_EXPENSES'), + canCreateExpenses: hasPermission(role, 'CREATE_EXPENSES'), + canEditExpenses: hasPermission(role, 'EDIT_EXPENSES'), + canEditExpenseValue: hasPermission(role, 'EDIT_EXPENSE_VALUE'), + canDeleteExpenses: hasPermission(role, 'DELETE_EXPENSES'), + canManageMembers: hasPermission(role, 'MANAGE_MEMBERS'), + canEditGroupInfo: hasPermission(role, 'EDIT_GROUP_INFO'), + canDeleteGroup: hasPermission(role, 'DELETE_GROUP'), + canGenerateReports: hasPermission(role, 'GENERATE_REPORTS'), + }; +} diff --git a/frontend/src/pages/Groups/index.jsx b/frontend/src/pages/Groups/index.jsx index 36db378..c8a1ebb 100644 --- a/frontend/src/pages/Groups/index.jsx +++ b/frontend/src/pages/Groups/index.jsx @@ -1,49 +1,28 @@ import { useState, useMemo } from 'react'; -import { Link } from 'react-router'; +import { Link, useSearchParams } from 'react-router'; import GroupFilters from '../../components/Filters/Groups'; import { useFilteredGroups } from '../../components/Filters/Groups/useFilteredGroups'; import GroupForm from '../../components/forms/GroupForm'; import CardUI from '../../components/ui/Card'; import { formatBRL } from '../../utils/formatters'; import ModalButton from '../../components/Modal/ModalButton'; -import { useGroupsQuery } from '../../hooks/ReactQuery/useGroupsQuery'; -import useMeQuery from '../../hooks/ReactQuery/useMeQuery'; import { CategoryIcon } from '../../mockData/groupIcons/icons'; -import { useQueries } from '@tanstack/react-query'; -import { REACTQUERY_KEYS } from '../../libs/ReactQuery/keys'; -import { getGroupMembers } from '../../services/groups'; import Pagination from '../../components/PaginationController'; +import useMeQuery from '../../hooks/ReactQuery/Queries/useMeQuery'; +import { useGroupsQuery } from '../../hooks/ReactQuery/Queries/useGroupsQuery'; +import useGroupsWithMembers from './useGroupsWithMembers'; export default function Groups() { + const [searchParams] = useSearchParams(); + const createModal = searchParams.has('create'); const { data: user } = useMeQuery(); const [page, setPage] = useState(0); const membersLengthToShow = 4; - const { data, isLoading } = useGroupsQuery(page); - const groups = data?.groups; - const totalPages = data?.totalPages; - const groupsLength = data?.groupsLength; - console.log({ data }); - // Cria queries dinâmicas de membros (cacheadas) - const membersQueries = useQueries({ - queries: groups?.map(group => ({ - queryKey: [REACTQUERY_KEYS.GROUPS.MEMBERS, group.id], - queryFn: () => getGroupMembers(group.id), - enabled: !!group.id, - staleTime: Infinity, // mantém o cache fresco indefinidamente - })), - }); - - // Cria um map: { [groupId]: members } - const membersMap = {}; - membersQueries.forEach((q, i) => { - if (q.data) - membersMap[groups[i].id] = { - totalElements: q.data.totalElements, - members: q.data.members, - }; - }); - - console.log({ membersMap }); + const { + data: { groups, totalPages, groupsLength }, + isLoading, + } = useGroupsQuery(page); + const membersMap = useGroupsWithMembers(groups); // Filtros const [filters, setFilters] = useState({ @@ -73,6 +52,7 @@ export default function Groups() { {title} } className="bg-primary text-white px-4 py-2 rounded-lg shadow hover:bg-primary/90 transition" /> diff --git a/frontend/src/pages/Groups/useGroupsWithMembers.jsx b/frontend/src/pages/Groups/useGroupsWithMembers.jsx new file mode 100644 index 0000000..a20a851 --- /dev/null +++ b/frontend/src/pages/Groups/useGroupsWithMembers.jsx @@ -0,0 +1,26 @@ +import { useQueries } from '@tanstack/react-query'; +import { REACTQUERY_KEYS } from '../../libs/ReactQuery/keys'; +import { getGroupMembers } from '../../services/groups'; + +export default function useGroupsWithMembers(groups) { + // Cria queries dinâmicas de membros (cacheadas) + const membersQueries = useQueries({ + queries: groups.map(group => ({ + queryKey: [REACTQUERY_KEYS.GROUPS.MEMBERS, group.id], + queryFn: () => getGroupMembers({ groupId: group.id, size: 4 }), + enabled: !!group.id, + staleTime: Infinity, // mantém o cache fresco indefinidamente + })), + }); + // Cria um map: { [groupId]: members } + const membersMap = {}; + membersQueries.forEach((q, i) => { + if (q.data) + membersMap[groups[i].id] = { + totalElements: q.data.totalElements, + members: q.data.members, + }; + }); + + return membersMap; +} diff --git a/frontend/src/pages/Profile/ActiveGroups/index.jsx b/frontend/src/pages/Profile/ActiveGroups/index.jsx index eac4a61..ec6f63b 100644 --- a/frontend/src/pages/Profile/ActiveGroups/index.jsx +++ b/frontend/src/pages/Profile/ActiveGroups/index.jsx @@ -1,36 +1,76 @@ -import { useGroupsQuery } from '../../../hooks/ReactQuery/useGroupsQuery'; import { CategoryIcon } from '../../../mockData/groupIcons/icons'; +import { Link } from 'react-router'; +import { FiUsers } from 'react-icons/fi'; +import { useGroupsQuery } from '../../../hooks/ReactQuery/Queries/useGroupsQuery'; export default function ActiveGroups() { - const { data: groups } = useGroupsQuery(); + const { data, isLoading } = useGroupsQuery(); const groupLimit = 4; - const groupsToShow = groups?.content.slice(0, groupLimit); - const hasMoreGroups = groups?.totalElements > groupLimit; + + if (isLoading) { + return ( +
+ + Carregando grupos... +
+ ); + } + + const groups = data?.groups ?? []; + const groupsToShow = groups.slice(0, groupLimit); + const hasMoreGroups = groups.length > groupLimit; + + if (groups.length === 0) { + return ( +
+ +

+ Você ainda não faz parte de nenhum grupo. +

+ + Criar grupo + +
+ ); + } return ( -
- {groupsToShow?.map(group => ( - + {groupsToShow.map(group => ( + - + - {group.name} - + {group.name} + ))} {hasMoreGroups && ( - - +{groups.totalElements - groupLimit} mais - + +{groups.length - groupLimit} mais + )} -
+ ); } diff --git a/frontend/src/pages/Profile/ProfileContent/index.jsx b/frontend/src/pages/Profile/ProfileContent/index.jsx index c57f9ee..9085978 100644 --- a/frontend/src/pages/Profile/ProfileContent/index.jsx +++ b/frontend/src/pages/Profile/ProfileContent/index.jsx @@ -6,10 +6,11 @@ import { avatarBackgroundColors } from '../../../mockData/colorsPallete/colors'; import { Form } from 'react-router'; import { customToast } from '../../../components/CustomToast'; import { useLogout } from '../../../hooks/useLogout'; -import useMeQuery from '../../../hooks/ReactQuery/useMeQuery'; -import { useUpdateMeMutation } from '../../../hooks/ReactQuery/useUpdateMeMutation'; + import { meDataSchema } from '../../../schemas/me/request'; import { FaLock, FaUnlock } from 'react-icons/fa'; +import useMeQuery from '../../../hooks/ReactQuery/Queries/useMeQuery'; +import { useUpdateMeMutation } from '../../../hooks/ReactQuery/Mutations/useUpdateMeMutation'; export default function ProfileContent({ setIsModalOpen }) { const { data: user, isLoading } = useMeQuery(); diff --git a/frontend/src/pages/Profile/index.jsx b/frontend/src/pages/Profile/index.jsx index 1bb9de9..fac5cb2 100644 --- a/frontend/src/pages/Profile/index.jsx +++ b/frontend/src/pages/Profile/index.jsx @@ -16,15 +16,15 @@ import ProfileContent from './ProfileContent'; import { Link } from 'react-router'; import Modal from '../../components/Modal'; -import useMeQuery from '../../hooks/ReactQuery/useMeQuery'; -import { useGroupsQuery } from '../../hooks/ReactQuery/useGroupsQuery'; -import { useMeDashboardQuery } from '../../hooks/ReactQuery/useMeDashboardStatsQuery'; +import useMeQuery from '../../hooks/ReactQuery/Queries/useMeQuery'; +import { useMeDashboardQuery } from '../../hooks/ReactQuery/Queries/useMeDashboardStatsQuery'; +import { useGroupsQuery } from '../../hooks/ReactQuery/Queries/useGroupsQuery'; export default function Profile() { const { data: user } = useMeQuery(); const { data: dashboard } = useMeDashboardQuery(); - const { data: groups } = useGroupsQuery(); - const groupsLength = groups?.content.length; + const { data } = useGroupsQuery(); + const groupsLength = data?.groupsLength; const [isModalOpen, setIsModalOpen] = useState(false); const current_user = userData; diff --git a/frontend/src/routes/routes.jsx b/frontend/src/routes/routes.jsx index d5d5c7c..47fcce4 100644 --- a/frontend/src/routes/routes.jsx +++ b/frontend/src/routes/routes.jsx @@ -7,7 +7,6 @@ import { groupSettingsLoader } from '../pages/Groups/GroupDetails/GroupSettings/ import { loginLoader } from '../loaders/loginLoader'; import { registerLoader } from '../loaders/registerLoader'; import { protectRoutersLoader } from '../loaders/protectRoutersLoader'; -import { groupDetailsLoader } from '../pages/Groups/GroupDetails/groupDetailsLoader'; // Layout e App carregados normalmente import Layout from '../components/Layout'; @@ -55,7 +54,6 @@ export const appRouter = createBrowserRouter([ { path: ':group_id', element: , - loader: groupDetailsLoader, }, { path: ':group_id/settings', @@ -64,6 +62,7 @@ export const appRouter = createBrowserRouter([ }, ], }, + { path: 'profile', element: }, ], }, diff --git a/frontend/src/services/groups.js b/frontend/src/services/groups.js index 9b96bcd..1efeb62 100644 --- a/frontend/src/services/groups.js +++ b/frontend/src/services/groups.js @@ -16,32 +16,22 @@ export const getGroups = async (page = 0, size = 6) => { } }; -export const getGroupMembers = async (groupId, size = 4) => { - try { - const { - data: { content: members, totalElements }, - } = await apiApplication.get(`groups/${groupId}/members?size=${size}`); - console.log({ members }); - return { members, totalElements }; - } catch (error) { - throw error; - } -}; +//Obtém os membros de um grupo +export const getGroupMembers = async ({ groupId, page, size, search } = {}) => { + const query = new URLSearchParams(); + if (page) query.append('page', page); + if (size) query.append('size', size); + if (search) query.append('search', search); -export const getMembers = async (groupId, page, search) => { - try { - const query = new URLSearchParams({ page: page.toString() }); - if (search) query.append('search', search); - const response = await apiApplication.get( - `/groups/${groupId}/members?${query.toString()}` - ); - return { - totalPages: response.data.totalPages, - members: response.data.content, - }; - } catch (error) { - throw error; - } + const { data } = await apiApplication.get( + `/groups/${groupId}/members?${query.toString()}` + ); + + return { + members: data.content, + totalPages: data.totalPages, + totalElements: data.totalElements, + }; }; // Cria um novo grupo @@ -55,6 +45,7 @@ export const createGroup = async group => { } }; +//Obter um grupo por id export const getGroupById = async id => { try { const response = await apiApplication @@ -72,7 +63,7 @@ export const getGroupById = async id => { throw new Error('Erro ao obter grupo - id:', id); } }; - +//Obter todas as categorias de despesa de um grupo export const getAllGroupsExpenseCategories = async () => { try { const response = await apiApplication.get('/groups/expenses/categories'); @@ -92,6 +83,52 @@ export const updateGroup = async (id, group) => { // Remove um grupo pelo id export const deleteGroup = async id => { - const response = await apiApplication.delete(`/groups/${id}`); - return response.data; + try { + const response = await apiApplication.delete(`/groups/${id}`); + return response.data; + } catch (error) { + throw error; + } +}; + +export const getGroupExpenses = async ({ groupID, page, filter = null }) => { + try { + const query = new URLSearchParams({ size: 6, page: page.toString() }); + if (filter !== null) { + if (filter) { + Object.entries(filter).forEach(([key, value]) => { + if (value !== null && value !== undefined && value !== '') { + query.append(key, value.toString()); + } + }); + } + } + + const response = await apiApplication.get( + `/groups/${groupID}/expenses?${query.toString()}` + ); + return { + expenses: response.data.content, + totalPages: response.data.totalPages, + expensesLength: response.data.totalElements, + }; + } catch (error) { + throw error; + } +}; + +export const getGroupExpenseById = async (group_id, expense_id) => { + try { + const response = await apiApplication.get( + `/groups/${group_id}/expenses/${expense_id}` + ); + return response.data; + } catch (error) { + console.error(error); + throw new Error( + 'Erro ao obter uma despesa específica de um grupo - id da despesa e grupo:', + expense_id, + group_id + ); + } }; diff --git a/frontend/src/utils/constants.js b/frontend/src/utils/constants.js index 27ced65..da7db19 100644 --- a/frontend/src/utils/constants.js +++ b/frontend/src/utils/constants.js @@ -1,5 +1,29 @@ -export const ROLES = { +const ROLES = { user: 'USER', admin: 'ADMIN', owner: 'OWNER', }; + +const PERMISSIONS = { + // 🧾 Despesas + VIEW_OWN_EXPENSES: [ROLES.user, ROLES.admin, ROLES.owner], + VIEW_ALL_EXPENSES: [ROLES.admin, ROLES.owner], + EDIT_EXPENSES: [ROLES.admin, ROLES.owner], + EDIT_EXPENSE_VALUE: [ROLES.owner], + DELETE_EXPENSES: [ROLES.admin, ROLES.owner], + CREATE_EXPENSES: [ROLES.admin, ROLES.owner], + + // 👥 Membros + MANAGE_MEMBERS: [ROLES.admin, ROLES.owner], + + // 🏠 Grupo + EDIT_GROUP_INFO: [ROLES.owner], + DELETE_GROUP: [ROLES.owner], + + // 📊 Extras + GENERATE_REPORTS: [ROLES.owner], +}; + +export function hasPermission(role, action) { + return PERMISSIONS[action].includes(role); +}