From d9fa60afa1dedd16bd7b46a29de052e66f44a69f Mon Sep 17 00:00:00 2001 From: Cleyton Jesus Date: Tue, 4 Nov 2025 20:54:19 -0300 Subject: [PATCH] backup #2 --- .../components/ChartSwitcher/ExpenseChart.jsx | 32 ++- .../src/components/ChartSwitcher/index.jsx | 44 +++- .../src/components/LatestExpenses/index.jsx | 119 +++++++---- .../ExpenseForm/FormFieldsExpenses/index.jsx | 7 +- .../useAllGroupExpenseCategoriesQuery.js | 12 ++ .../src/hooks/ReactQuery/useGroupsQuery.js | 12 +- .../hooks/ReactQuery/useMeExpensesQuery.js | 7 +- frontend/src/libs/ReactQuery/keys.js | 3 + .../Groups/GroupDetails/ExpensesList.jsx | 202 +++++++++++++++--- .../src/pages/Groups/GroupDetails/index.jsx | 2 +- frontend/src/pages/Groups/index.jsx | 14 +- frontend/src/services/groups.js | 16 +- frontend/src/services/me.js | 14 +- 13 files changed, 364 insertions(+), 120 deletions(-) create mode 100644 frontend/src/hooks/ReactQuery/useAllGroupExpenseCategoriesQuery.js diff --git a/frontend/src/components/ChartSwitcher/ExpenseChart.jsx b/frontend/src/components/ChartSwitcher/ExpenseChart.jsx index 0c1eed2..28d868a 100644 --- a/frontend/src/components/ChartSwitcher/ExpenseChart.jsx +++ b/frontend/src/components/ChartSwitcher/ExpenseChart.jsx @@ -7,7 +7,7 @@ export default function ExpenseQuantityDonutChart() { const total = expenseData?.reduce((sum, item) => sum + item.quantity, 0) || 0; return ( -
+
- `${name}: ${expenseData.find(c => c.category === name)?.quantity}` + stroke="var(--color-surface)" + strokeWidth={2} + paddingAngle={1} + labelLine={false} + label={({ name }) => + `${name}: ${ + expenseData.find(c => c.category === name)?.quantity ?? 0 + }` } - labelLine={true} > {expenseData?.map((_, index) => ( ))} - [`${value}`, 'Quantidade de despesas']} - /> -
-
-

Total

-

{total}

-
+ {/* 💬 Centro do gráfico */} +
+

Total de despesas

+

{total}

); diff --git a/frontend/src/components/ChartSwitcher/index.jsx b/frontend/src/components/ChartSwitcher/index.jsx index 9ffcdb9..566b7c9 100644 --- a/frontend/src/components/ChartSwitcher/index.jsx +++ b/frontend/src/components/ChartSwitcher/index.jsx @@ -18,7 +18,7 @@ export default function ChartSwitcher() {
{/* Cabeçalho */}
-

+

{activeChart === 'balance' ? 'Visualização Financeira' : 'Distribuição das Despesas por Categoria'} @@ -34,11 +34,47 @@ export default function ChartSwitcher() { {buttonLabel}

+ +
+ ); +} +function GraphicArea({ activeChart }) { + return ( + <> {/* Área do gráfico */} -
- {activeChart === 'balance' ? : } +
+ {/* ✨ Transição suave entre os gráficos */} +
+ +
+ +
+ +
+ + {/* Fallback caso não haja dados */} + {!activeChart && ( +

Nenhum gráfico selecionado.

+ )}
-
+ ); } diff --git a/frontend/src/components/LatestExpenses/index.jsx b/frontend/src/components/LatestExpenses/index.jsx index 095bd1d..bc98f17 100644 --- a/frontend/src/components/LatestExpenses/index.jsx +++ b/frontend/src/components/LatestExpenses/index.jsx @@ -1,3 +1,4 @@ +import { useState } from 'react'; import { Menu, MenuItem } from '@headlessui/react'; import { formatBRL, @@ -5,9 +6,27 @@ import { formatRelativeDate, } from '../../utils/formatters'; import useMeExpensesQuery from '../../hooks/ReactQuery/useMeExpensesQuery'; +import Pagination from '../PaginationController'; export default function LatestExpenses() { - const { data: myExpenses } = useMeExpensesQuery(); + // 🔢 Estado da paginação + const [page, setPage] = useState(0); + + // 🔗 Consulta das despesas + const { data, isLoading } = useMeExpensesQuery(page); + // 💾 Dados vindos do backend + const expenses = data?.expenses; + const totalPages = data?.totalPages; + const totalExpenses = data?.expensesLength; + // 🕹️ Handlers de navegação + const handleNext = () => { + if (page < totalPages - 1) setPage(prev => prev + 1); + }; + + const handlePrev = () => { + if (page > 0) setPage(prev => prev - 1); + }; + return (
- Últimas Despesas{' '} - ({myExpenses?.length}) + Últimas Despesas ({totalExpenses}) - - {myExpenses?.map(expense => ( -
  • - -
    -
    -

    - {expense?.title} -

    -

    - {formatBRL(expense?.partialValue)} -

    -
    + {isLoading ? ( +

    Carregando despesas...

    + ) : expenses?.length === 0 ? ( +

    Nenhuma despesa encontrada.

    + ) : ( + <> + + {expenses?.map(expense => ( +
  • + +
    +
    +

    + {expense.title} +

    +

    + {formatBRL(expense.partialValue)} +

    +
    -
    - - {expense?.groupName && `#${expense.groupName}`} - +
    + + {expense.groupName && `#${expense.groupName}`} + -
    - - criada {formatRelativeDate(expense?.createdAt)} - - - - vence {formatDateBR(expense?.deadlineDate)} - +
    + + criada {formatRelativeDate(expense.createdAt)} + + + + vence {formatDateBR(expense.deadlineDate)} + +
    +
    -
    -
    -
    -
  • - ))} -
    + +
  • + ))} +
    + + {/* 🔄 Controle de Paginação */} + + + )}
    ); } diff --git a/frontend/src/components/forms/ExpenseForm/FormFieldsExpenses/index.jsx b/frontend/src/components/forms/ExpenseForm/FormFieldsExpenses/index.jsx index 2002618..bb71e76 100644 --- a/frontend/src/components/forms/ExpenseForm/FormFieldsExpenses/index.jsx +++ b/frontend/src/components/forms/ExpenseForm/FormFieldsExpenses/index.jsx @@ -1,12 +1,13 @@ -import categories from '../../../../mockData/categories'; import { getCurrentDate } from '../../../../utils/helpers'; import InputUI from '../../../ui/Input'; 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'; -export default function FormFieldsExpenses({ data: members }) { +export default function FormFieldsExpenses() { + const { data: allCategories } = useAllGroupExpenseCategoriesQuery(); const { setAmount } = useFormExpense(); const handleAmountChange = ({ float: numericValue }) => { @@ -111,7 +112,7 @@ export default function FormFieldsExpenses({ data: members }) { className="w-full p-2 border border-neutral rounded-md text-text placeholder-muted focus:outline-none focus:ring-2 focus:ring-primary focus:border-primary transition cursor-pointer" > - {categories?.map(({ id, name }) => ( + {allCategories?.map(({ id, name }) => ( diff --git a/frontend/src/hooks/ReactQuery/useAllGroupExpenseCategoriesQuery.js b/frontend/src/hooks/ReactQuery/useAllGroupExpenseCategoriesQuery.js new file mode 100644 index 0000000..130ed2e --- /dev/null +++ b/frontend/src/hooks/ReactQuery/useAllGroupExpenseCategoriesQuery.js @@ -0,0 +1,12 @@ +import { useQuery } from '@tanstack/react-query'; +import { REACTQUERY_KEYS } from '../../libs/ReactQuery/keys'; +import { getAllGroupsExpenseCategories } from '../../services/groups'; + +export function useAllGroupExpenseCategoriesQuery() { + return useQuery({ + queryKey: [REACTQUERY_KEYS.EXPENSES.CATEGORIES], + queryFn: getAllGroupsExpenseCategories, + staleTime: Infinity, + placeholderData: [], + }); +} diff --git a/frontend/src/hooks/ReactQuery/useGroupsQuery.js b/frontend/src/hooks/ReactQuery/useGroupsQuery.js index adcecc5..a758b09 100644 --- a/frontend/src/hooks/ReactQuery/useGroupsQuery.js +++ b/frontend/src/hooks/ReactQuery/useGroupsQuery.js @@ -2,12 +2,16 @@ import { useQuery } from '@tanstack/react-query'; import { REACTQUERY_KEYS } from '../../libs/ReactQuery/keys'; import { getGroupById, getGroups } from '../../services/groups'; -export function useGroupsQuery(page, size) { +export function useGroupsQuery(page) { return useQuery({ - queryKey: [REACTQUERY_KEYS.GROUPS.ALL, { page, size }], - queryFn: async () => await getGroups(page, size), + queryKey: [REACTQUERY_KEYS.GROUPS.ALL, { page }], + queryFn: async () => await getGroups(page), staleTime: Infinity, - placeholderData: { content: [] }, + placeholderData: { + groups: [], + totalPages: 0, + groupsLength: 0, + }, }); } export function useGroupByIdQuery(id) { diff --git a/frontend/src/hooks/ReactQuery/useMeExpensesQuery.js b/frontend/src/hooks/ReactQuery/useMeExpensesQuery.js index b9d9e6f..429d347 100644 --- a/frontend/src/hooks/ReactQuery/useMeExpensesQuery.js +++ b/frontend/src/hooks/ReactQuery/useMeExpensesQuery.js @@ -3,12 +3,13 @@ import { REACTQUERY_KEYS } from '../../libs/ReactQuery/keys'; import { getMeExpenses } from '../../services/me'; import useMeQuery from './useMeQuery'; -export default function useMeExpensesQuery() { +export default function useMeExpensesQuery(page) { const { data: user } = useMeQuery(); const userId = user?.id; return useQuery({ - queryKey: [REACTQUERY_KEYS.USER.EXPENSES, userId], - queryFn: getMeExpenses, + queryKey: [REACTQUERY_KEYS.USER.EXPENSES, { userId, page }], + queryFn: async () => await getMeExpenses(page), enabled: !!userId, + placeholderData: { expenses: [], totalPages: 0, expensesLength: 0 }, }); } diff --git a/frontend/src/libs/ReactQuery/keys.js b/frontend/src/libs/ReactQuery/keys.js index 507340c..e60cfdb 100644 --- a/frontend/src/libs/ReactQuery/keys.js +++ b/frontend/src/libs/ReactQuery/keys.js @@ -10,6 +10,9 @@ export const REACTQUERY_KEYS = { DETAILS: 'groupDetails', EXPENSES: 'groupExpenses', }, + EXPENSES: { + CATEGORIES: 'allCategories', + }, MEMBERS: { ALL: 'allMembers', }, diff --git a/frontend/src/pages/Groups/GroupDetails/ExpensesList.jsx b/frontend/src/pages/Groups/GroupDetails/ExpensesList.jsx index 496e946..292a985 100644 --- a/frontend/src/pages/Groups/GroupDetails/ExpensesList.jsx +++ b/frontend/src/pages/Groups/GroupDetails/ExpensesList.jsx @@ -1,43 +1,179 @@ +import { useState, useMemo } from 'react'; +import { FiSearch } from 'react-icons/fi'; import { formatBRL } from '../../../utils/formatters'; +import InputUI from '../../../components/ui/Input'; +import SelectUI from '../../../components/ui/Select'; +import CheckboxUI from '../../../components/ui/Checkbox'; +import { Link } from 'react-router'; + +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'); + 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]); + return (
    -

    - Últimas despesas -

    -
      - -
    -
    - ); -} - -function ListExpensesItens({ group }) { - return ( - <> - {group.recentExpenses.map(expense => ( -
  • +

    - - {expense.description} - - - - {formatBRL(expense.amount)} - -

  • - ))} - {group.recentExpenses.length === 0 && ( -

    Nenhuma despesa recente.

    + 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 }) => ( + + ))} + +
    +
    + + {/* 🎚️ Filtro extra */} +
    + +
    +
    + + {/* 🧾 Lista */} + {isLoading ? ( +

    Carregando despesas...

    + ) : filteredExpenses.length > 0 ? ( + + ) : ( +

    + Nenhuma despesa encontrada. +

    )} - + ); } diff --git a/frontend/src/pages/Groups/GroupDetails/index.jsx b/frontend/src/pages/Groups/GroupDetails/index.jsx index 4a162fb..7e7141f 100644 --- a/frontend/src/pages/Groups/GroupDetails/index.jsx +++ b/frontend/src/pages/Groups/GroupDetails/index.jsx @@ -194,7 +194,7 @@ export default function GroupDetails() { {/* Conteúdo principal */}
    - {/* */} + {/* {showBalances ? ( ) : ( diff --git a/frontend/src/pages/Groups/index.jsx b/frontend/src/pages/Groups/index.jsx index 1947726..36db378 100644 --- a/frontend/src/pages/Groups/index.jsx +++ b/frontend/src/pages/Groups/index.jsx @@ -17,15 +17,15 @@ import Pagination from '../../components/PaginationController'; export default function Groups() { const { data: user } = useMeQuery(); const [page, setPage] = useState(0); - const pageSize = undefined; const membersLengthToShow = 4; - const { data: groupsData, isLoading } = useGroupsQuery(page, pageSize); - const groups = groupsData?.content ?? []; - const totalPages = groupsData?.totalPages ?? 1; - + 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 => ({ + queries: groups?.map(group => ({ queryKey: [REACTQUERY_KEYS.GROUPS.MEMBERS, group.id], queryFn: () => getGroupMembers(group.id), enabled: !!group.id, @@ -53,7 +53,7 @@ export default function Groups() { }); const filteredGroups = useFilteredGroups(groups, 'OWNER', filters); - const totalGroups = groups.length; + const totalGroups = groupsLength; const totalFiltered = filteredGroups.length; const title = useMemo(() => { diff --git a/frontend/src/services/groups.js b/frontend/src/services/groups.js index 9ba5233..9b96bcd 100644 --- a/frontend/src/services/groups.js +++ b/frontend/src/services/groups.js @@ -6,7 +6,11 @@ export const getGroups = async (page = 0, size = 6) => { const response = await apiApplication.get( `/groups?page=${page}&size=${size}` ); - return response.data; + return { + groups: response.data.content, + totalPages: response.data.totalPages, + groupsLength: response.data.totalElements, + }; } catch (error) { throw error; } @@ -69,6 +73,16 @@ export const getGroupById = async id => { } }; +export const getAllGroupsExpenseCategories = async () => { + try { + const response = await apiApplication.get('/groups/expenses/categories'); + return response.data; + } catch (error) { + console.error(error); + throw new Error('Erro ao obter todas as categorias de despesa.'); + } +}; + // Atualiza um grupo existente pelo id export const updateGroup = async (id, group) => { const response = await api.put(`/groups/${id}`, group); diff --git a/frontend/src/services/me.js b/frontend/src/services/me.js index 3e35245..98854a2 100644 --- a/frontend/src/services/me.js +++ b/frontend/src/services/me.js @@ -14,7 +14,7 @@ export const getMe = async () => { // A CHAVE [REACTQUERY_KEYS.USER.ME] deve ser onde a query de usuário está. // Aqui, assumimos que a busca de usuário JÁ FOI FEITA. -export const getMeExpenses = async () => { +export const getMeExpenses = async (page, size = 5) => { // 1. Obter o userId do cache da query de usuário // Assumimos que o array de chaves do usuário é [REACTQUERY_KEYS.USER.ME] const user = queryClient.getQueryData([REACTQUERY_KEYS.USER.ME]); @@ -29,19 +29,25 @@ export const getMeExpenses = async () => { try { // 2. Chamada de API para obter as despesas - const response = await apiApplication.get('user/me/expenses'); + const response = await apiApplication.get( + `user/me/expenses?page=${page}&size=${size}` + ); const expensesList = response.data.content; // 3. NORMALIZAÇÃO: Injetar o dado na chave separada, que agora tem o ID // CHAVE: [REACTQUERY_KEYS.USER.EXPENSES, userId] queryClient.setQueryData( - [REACTQUERY_KEYS.USER.EXPENSES, userId], + [REACTQUERY_KEYS.USER.EXPENSES, { userId, page, size }], expensesList ); // 4. Retornar o dado (para a query principal) // OBS: Você pode retornar apenas a lista aqui, pois o setQueryData já lidou com o cache. - return expensesList || []; + return { + expenses: expensesList, + totalPages: response.data.totalPages, + expensesLength: response.data.totalElements, + }; } catch (error) { // É uma boa prática lançar o erro do próprio objeto de erro para que o TanStack Query o capture throw error;