Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 13 additions & 19 deletions frontend/src/components/ChartSwitcher/ExpenseChart.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export default function ExpenseQuantityDonutChart() {
const total = expenseData?.reduce((sum, item) => sum + item.quantity, 0) || 0;

return (
<div className="relative w-full h-full">
<div className="relative w-full h-[280px] sm:h-[340px]">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
Expand All @@ -16,10 +16,15 @@ export default function ExpenseQuantityDonutChart() {
nameKey="category"
innerRadius="60%"
outerRadius="80%"
label={({ name, percent }) =>
`${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) => (
<Cell
Expand All @@ -30,24 +35,13 @@ export default function ExpenseQuantityDonutChart() {
/>
))}
</Pie>
<Tooltip
contentStyle={{
backgroundColor: 'var(--color-surface)',
borderColor: 'var(--color-muted)',
color: 'var(--color-text)',
borderRadius: '8px',
}}
labelStyle={{ color: 'var(--color-text)', fontWeight: 600 }}
formatter={value => [`${value}`, 'Quantidade de despesas']}
/>
</PieChart>
</ResponsiveContainer>

<div className="absolute inset-0 flex items-center justify-center">
<div className="text-center">
<p className="text-sm text-muted">Total</p>
<p className="text-lg font-semibold text-text">{total}</p>
</div>
{/* 💬 Centro do gráfico */}
<div className="absolute inset-0 flex flex-col items-center justify-center pointer-events-none">
<p className="text-sm text-muted">Total de despesas</p>
<p className="text-3xl font-bold text-text mt-1">{total}</p>
</div>
</div>
);
Expand Down
44 changes: 40 additions & 4 deletions frontend/src/components/ChartSwitcher/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export default function ChartSwitcher() {
<div className="w-full bg-surface p-4 rounded-2xl shadow-md space-y-4 mb-6 transition-colors">
{/* Cabeçalho */}
<div className="flex justify-between items-center">
<h2 className="text-xl font-semibold text-[var(--color-text)]">
<h2 className="text-xl font-semibold text-text">
{activeChart === 'balance'
? 'Visualização Financeira'
: 'Distribuição das Despesas por Categoria'}
Expand All @@ -34,11 +34,47 @@ export default function ChartSwitcher() {
<span className="text-sm">{buttonLabel}</span>
</button>
</div>
<GraphicArea activeChart={activeChart} />
</div>
);
}

function GraphicArea({ activeChart }) {
return (
<>
{/* Área do gráfico */}
<div className=" w-full h-72">
{activeChart === 'balance' ? <BalanceChart /> : <ExpenseChart />}
<div
className="
relative w-full h-72 sm:h-80
bg-surface
rounded-xl
flex items-center justify-center
transition-all duration-500

"
>
{/* ✨ Transição suave entre os gráficos */}
<div
className={`absolute inset-0 transition-opacity duration-500 ${
activeChart === 'balance' ? 'opacity-100 z-10' : 'opacity-0 z-0'
}`}
>
<BalanceChart />
</div>

<div
className={`absolute inset-0 transition-opacity duration-500 ${
activeChart === 'expense' ? 'opacity-100 z-10' : 'opacity-0 z-0'
}`}
>
<ExpenseChart />
</div>

{/* Fallback caso não haja dados */}
{!activeChart && (
<p className="text-muted italic">Nenhum gráfico selecionado.</p>
)}
</div>
</div>
</>
);
}
119 changes: 78 additions & 41 deletions frontend/src/components/LatestExpenses/index.jsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,32 @@
import { useState } from 'react';
import { Menu, MenuItem } from '@headlessui/react';
import {
formatBRL,
formatDateBR,
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 (
<section
aria-labelledby="ultimas-despesas-heading"
Expand All @@ -17,51 +36,69 @@ export default function LatestExpenses() {
id="ultimas-despesas-heading"
className="text-lg font-bold text-text mb-4"
>
Últimas Despesas{' '}
<span className="text-muted">({myExpenses?.length})</span>
Últimas Despesas <span className="text-muted">({totalExpenses})</span>
</h2>

<Menu as="ul" className="space-y-4">
{myExpenses?.map(expense => (
<li key={expense?.id}>
<MenuItem
as="a"
href={`/groups/${expense?.groupId}`}
aria-label={`Despesa: ${expense?.title}`}
>
<div className="text-text bg-neutral border-surface rounded-lg p-4 border hover:shadow-sm transition-shadow cursor-pointer ">
<div className="flex justify-between items-start">
<p className="font-semibold" aria-label="Título da despesa">
{expense?.title}
</p>
<p
className="font-bold text-error"
aria-label={`Valor da despesa: ${formatBRL(expense?.partialValue)}`}
>
{formatBRL(expense?.partialValue)}
</p>
</div>
{isLoading ? (
<p className="text-muted text-sm">Carregando despesas...</p>
) : expenses?.length === 0 ? (
<p className="text-muted text-sm italic">Nenhuma despesa encontrada.</p>
) : (
<>
<Menu as="ul" className="space-y-4">
{expenses?.map(expense => (
<li key={expense.expenseId}>
<MenuItem
as="a"
href={`/groups/${expense.groupId}`}
aria-label={`Despesa: ${expense.title}`}
>
<div className="text-text bg-neutral border-surface rounded-lg p-4 border hover:shadow-sm transition-shadow cursor-pointer">
<div className="flex justify-between items-start">
<p
className="font-semibold"
aria-label="Título da despesa"
>
{expense.title}
</p>
<p
className="font-bold text-error"
aria-label={`Valor da despesa: ${formatBRL(expense.partialValue)}`}
>
{formatBRL(expense.partialValue)}
</p>
</div>

<div className="flex flex-wrap items-center justify-between mt-1 text-xs text-muted">
<span className="font-medium text-primary/80">
{expense?.groupName && `#${expense.groupName}`}
</span>
<div className="flex flex-wrap items-center justify-between mt-1 text-xs text-muted">
<span className="font-medium text-primary/80">
{expense.groupName && `#${expense.groupName}`}
</span>

<div className="flex items-center gap-1">
<span className="italic">
criada {formatRelativeDate(expense?.createdAt)}
</span>
<span className="text-muted-foreground">•</span>
<span className="font-semibold text-accent">
vence {formatDateBR(expense?.deadlineDate)}
</span>
<div className="flex items-center gap-1">
<span className="italic">
criada {formatRelativeDate(expense.createdAt)}
</span>
<span className="text-muted-foreground">•</span>
<span className="font-semibold text-accent">
vence {formatDateBR(expense.deadlineDate)}
</span>
</div>
</div>
</div>
</div>
</div>
</MenuItem>
</li>
))}
</Menu>
</MenuItem>
</li>
))}
</Menu>

{/* 🔄 Controle de Paginação */}
<Pagination
onNext={handleNext}
onPrev={handlePrev}
page={page}
totalPages={totalPages}
/>
</>
)}
</section>
);
}
Original file line number Diff line number Diff line change
@@ -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 }) => {
Expand Down Expand Up @@ -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 }) => (
<option key={id} value={id.toString()}>
{name}
</option>
Expand Down
Original file line number Diff line number Diff line change
@@ -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: [],
});
}
12 changes: 8 additions & 4 deletions frontend/src/hooks/ReactQuery/useGroupsQuery.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
7 changes: 4 additions & 3 deletions frontend/src/hooks/ReactQuery/useMeExpensesQuery.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
});
}
3 changes: 3 additions & 0 deletions frontend/src/libs/ReactQuery/keys.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ export const REACTQUERY_KEYS = {
DETAILS: 'groupDetails',
EXPENSES: 'groupExpenses',
},
EXPENSES: {
CATEGORIES: 'allCategories',
},
MEMBERS: {
ALL: 'allMembers',
},
Expand Down
Loading
Loading