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
78 changes: 39 additions & 39 deletions backend/finboostplus_server/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,41 +1,41 @@
services:
server:
build:
context: .
ports:
- 8080:8080
env_file:
- .env
depends_on:
db:
condition: service_healthy
volumes:
- .:/build
environment:
- POSTGRES_URL=jdbc:postgresql://db:5432/finboostplus
- SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/finboostplus
- SPRING_DATASOURCE_USERNAME=${POSTGRES_USERNAME}
- SPRING_DATASOURCE_PASSWORD=${POSTGRES_PASSWORD}
- SPRING_MAIL_USERNAME=${MENSAGERIE_EMAIL}
- SPRING_MAIL_PASSWORD=${MENSAGERIE_PASSWORD}
- SPRING_CLIENT_ID=${CLIENT_ID}
- SPRING_CLIENT_SECRET=${CLIENT_SECRET}
- CORS_ORIGINS=${CORS_ORIGINS}
db:
image: postgres
restart: always
volumes:
- db-data:/var/lib/postgresql/data
environment:
- POSTGRES_DB=finboostplus
- POSTGRES_USER=${POSTGRES_USERNAME}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
ports:
- 5432:5432
healthcheck:
test: ['CMD', 'pg_isready', '-U', 'postgres']
interval: 10s
timeout: 5s
retries: 5
server:
build:
context: .
ports:
- 8080:8080
env_file:
- .env
depends_on:
db:
condition: service_healthy
volumes:
- .:/build
environment:
- POSTGRES_URL=jdbc:postgresql://db:5432/finboostplus
- SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/finboostplus
- SPRING_DATASOURCE_USERNAME=${POSTGRES_USERNAME}
- SPRING_DATASOURCE_PASSWORD=${POSTGRES_PASSWORD}
- SPRING_MAIL_USERNAME=${MENSAGERIE_EMAIL}
- SPRING_MAIL_PASSWORD=${MENSAGERIE_PASSWORD}
- SPRING_CLIENT_ID=${CLIENT_ID}
- SPRING_CLIENT_SECRET=${CLIENT_SECRET}
- CORS_ORIGINS=${CORS_ORIGINS}
db:
image: postgres
restart: always
volumes:
- db-data:/var/lib/postgresql
environment:
- POSTGRES_DB=finboostplus
- POSTGRES_USER=${POSTGRES_USERNAME}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
ports:
- 5432:5432
healthcheck:
test: ['CMD', 'pg_isready', '-U', 'postgres']
interval: 10s
timeout: 5s
retries: 5
volumes:
db-data:
db-data:
Original file line number Diff line number Diff line change
Expand Up @@ -16,30 +16,36 @@
@Repository
public interface GroupRepository extends JpaRepository<Group, Long> {
@Query(nativeQuery = true, value = """
SELECT
G.ID AS GROUP_ID,
G.NAME,
G.DESCRIPTION,
GM.AUTH_LEVEL AS AUTHORITY,
G.ICON,
G.CREATED_AT,
COALESCE(SUM(UED.PARTIAL_VALUE), 0) AS PARTIAL_TOTAL,
COALESCE(SUM(E.VALUE), 0) AS TOTAL
FROM
GROUPS G
INNER JOIN GROUP_MEMBERS GM ON GM.GROUP_ID = G.ID
LEFT JOIN EXPENSES E ON E.GROUP_ID = G.ID
AND E.STATUS != 'PAID' -- aplica filtro no join!
LEFT JOIN USER_EXPENSE_DIVISIONS UED ON UED.EXPENSE_ID = E.ID
AND UED.USER_ID = :memberId
AND UED.STATUS != 'PAID'
GROUP BY
G.ID,
G.NAME,
G.DESCRIPTION,
GM.AUTH_LEVEL,
G.ICON,
G.CREATED_AT
SELECT
G.ID AS GROUP_ID,
G.NAME,
G.DESCRIPTION,
GM.AUTH_LEVEL AS AUTHORITY,
G.ICON,
G.CREATED_AT,
COALESCE(SUM(UED.PARTIAL_VALUE), 0) AS PARTIAL_TOTAL,
COALESCE(SUM(E.VALUE), 0) AS TOTAL
FROM
GROUPS G
INNER JOIN GROUP_MEMBERS GM
ON GM.GROUP_ID = G.ID
AND GM.USER_ID = :memberId
LEFT JOIN EXPENSES E
ON E.GROUP_ID = G.ID
AND E.STATUS != 'PAID'
LEFT JOIN USER_EXPENSE_DIVISIONS UED
ON UED.EXPENSE_ID = E.ID
AND UED.USER_ID = :memberId
AND UED.STATUS != 'PAID'
GROUP BY
G.ID,
G.NAME,
G.DESCRIPTION,
GM.AUTH_LEVEL,
G.ICON,
G.CREATED_AT
ORDER BY
G.CREATED_AT DESC
""")
Page<GroupProjection> listUserGroupsPaged(Long memberId, Pageable pageable);

Expand All @@ -60,35 +66,31 @@ public interface GroupRepository extends JpaRepository<Group, Long> {

@Query(nativeQuery = true, value = """
SELECT
G.ID,
G.NAME,
G.DESCRIPTION,
G.ICON,
GM.AUTH_LEVEL AS AUTHORIZATION,
(
SELECT
COALESCE(SUM(UED.PARTIAL_VALUE), 0)
FROM
USER_EXPENSE_DIVISIONS UED
WHERE
UED.USER_ID = :userId
AND UED.STATUS != 'PAID'
) AS PARTIAL_TOTAL,
(
SELECT
COALESCE(SUM(E.VALUE), 0)
FROM
EXPENSES E
WHERE
E.GROUP_ID = :groupId
AND E.STATUS != 'PAID'
) AS TOTAL
FROM
GROUPS AS G
INNER JOIN GROUP_MEMBERS AS GM ON GM.GROUP_ID = G.ID
WHERE
GM.GROUP_ID = :groupId
AND GM.USER_ID = :userId
G.ID,
G.NAME,
G.DESCRIPTION,
G.ICON,
GM.AUTH_LEVEL AS AUTHORIZATION,
COALESCE((
SELECT SUM(UED.PARTIAL_VALUE)
FROM USER_EXPENSE_DIVISIONS UED
INNER JOIN EXPENSES E ON E.ID = UED.EXPENSE_ID
WHERE UED.USER_ID = :userId
AND E.GROUP_ID = :groupId
AND UED.STATUS != 'PAID'
), 0) AS PARTIAL_TOTAL,
COALESCE((
SELECT SUM(E.VALUE)
FROM EXPENSES E
WHERE E.GROUP_ID = :groupId
AND E.STATUS != 'PAID'
), 0) AS TOTAL
FROM GROUPS G
INNER JOIN GROUP_MEMBERS GM
ON GM.GROUP_ID = G.ID
AND GM.USER_ID = :userId
WHERE G.ID = :groupId
""")
GroupDetailsDTO getGroupDetails(Long groupId, Long userId);

}
6 changes: 0 additions & 6 deletions frontend/src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
import { useEffect } from 'react';
import ChartSwitcher from './components/ChartSwitcher';
import LatestExpenses from './components/LatestExpenses';
import SummaryCards from './components/SummaryCards';
import { useAuthStore } from './context/stores/auth';

function App() {
const user = useAuthStore(store => store.user);
useEffect(() => {
console.log({ user });
}, []);
return (
<div>
<SummaryCards />
Expand Down
86 changes: 55 additions & 31 deletions frontend/src/components/ChartSwitcher/BalanceChart.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,62 +5,86 @@ import {
YAxis,
CartesianGrid,
Tooltip,
Legend,
ResponsiveContainer,
} from 'recharts';
import userData from '../../mockData/user/user.data';
import { useQuery } from '@tanstack/react-query';
import { REACTQUERY_KEYS } from '../../libs/ReactQuery/keys';
import { getMeExpensesByMonthly } from '../../services/me';
import { formatBRL } from '../../utils/formatters';

export default function BalanceChart() {
const balanceData = userData.dashboard.chartsData.balanceData;
const { data: expensesResponse } = useQuery({
queryKey: [REACTQUERY_KEYS.USER.DASHBOARD, 'balance'],
queryFn: getMeExpensesByMonthly,
});

const expensesByMonthly =
expensesResponse?.map(item => ({
name: item.month,
despesas: item.total,
})) ?? [];

return (
<ResponsiveContainer width="100%" height="100%">
<AreaChart
data={balanceData}
margin={{ top: 10, right: 30, left: 0, bottom: 0 }}
data={expensesByMonthly}
margin={{ top: 10, right: 30, left: 30, bottom: 10 }}
>
{/* Grid */}
<CartesianGrid stroke="var(--color-muted)" strokeDasharray="3 3" />
{/* Grade */}
<CartesianGrid stroke="var(--color-border)" strokeDasharray="3 3" />

<XAxis
dataKey="name"
stroke="var(--color-text)"
tick={{ fontSize: 12, fill: 'var(--color-text)' }}
axisLine={false}
tickLine={false}
/>

{/* Eixos */}
<XAxis dataKey="name" stroke="var(--color-text)" />
<YAxis stroke="var(--color-text)" />
<YAxis
stroke="var(--color-text)"
tickFormatter={value => formatBRL(value)}
tick={{ fontSize: 12, fill: 'var(--color-text)' }}
axisLine={false}
tickLine={false}
/>

{/* Tooltip customizado */}
{/* Tooltip com tema dinâmico */}
<Tooltip
formatter={value => formatBRL(value)}
contentStyle={{
backgroundColor: 'var(--color-surface)',
borderColor: 'var(--color-muted)',
borderColor: 'var(--color-border)',
color: 'var(--color-text)',
}}
labelStyle={{ color: 'var(--color-text)', fontWeight: 500 }}
itemStyle={{ color: 'var(--color-text)' }}
/>

{/* Legenda */}
<Legend
wrapperStyle={{
color: 'var(--color-text)',
labelStyle={{
color: 'var(--color-text-muted)',
fontWeight: 500,
}}
itemStyle={{ color: 'var(--color-text)' }}
/>

{/* Áreas do gráfico */}
<Area
type="monotone"
dataKey="receitas"
name="Receitas"
stroke="var(--color-success)"
fill="var(--color-success)"
fillOpacity={0.2}
/>
{/* Gradiente suave (segue o tema pelas CSS vars) */}
<defs>
<linearGradient id="colorExpense" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor="var(--color-error)"
stopOpacity={0.4}
/>
<stop offset="95%" stopColor="var(--color-error)" stopOpacity={0} />
</linearGradient>
</defs>

{/* Área principal */}
<Area
type="monotone"
dataKey="despesas"
name="Despesas"
stroke="var(--color-error)"
fill="var(--color-error)"
fillOpacity={0.2}
fill="url(#colorExpense)"
strokeWidth={2}
animationDuration={800}
/>
</AreaChart>
</ResponsiveContainer>
Expand Down
18 changes: 11 additions & 7 deletions frontend/src/components/LatestExpenses/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,17 @@ export default function LatestExpenses() {
))}
</Menu>

{/* 🔄 Controle de Paginação */}
<Pagination
onNext={handleNext}
onPrev={handlePrev}
page={page}
totalPages={totalPages}
/>
{totalPages > 1 && (
<>
{/* 🔄 Controle de Paginação */}
<Pagination
onNext={handleNext}
onPrev={handlePrev}
page={page}
totalPages={totalPages}
/>
</>
)}
</>
)}
</section>
Expand Down
10 changes: 5 additions & 5 deletions frontend/src/components/Layout/DropdownMenu/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@ 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() ?? '?';
const { data: user } = useMeQuery();

return (
<Menu
Expand All @@ -23,15 +21,17 @@ export default function DropdownMenu() {
style={{ backgroundColor: user?.themeColor }}
aria-label={`Abrir menu do usuário ${user?.name}`}
>
<span aria-hidden="true">{firstLetter}</span>
<span className="capitalize" aria-hidden="true">
{user?.name[0] ?? '?'}
</span>
</MenuButton>

{/* Menu dropdown */}
<MenuItems
anchor="bottom end"
className="absolute mt-2 w-56 origin-top-right rounded-xl bg-surface border border-muted shadow-lg ring-1 ring-black/5 z-50
transition-opacity duration-150 ease-out
data-[closed]:opacity-0 data-[enter]:opacity-100"
data-closed:opacity-0 data-enter:opacity-100"
>
<div className="p-2 space-y-1">
{navItems.map(({ icon, label, href, isDanger }, index) => (
Expand Down
Loading
Loading