diff --git a/app/(authenticated)/dashboard/_components/expenses-per-category.tsx b/app/(authenticated)/dashboard/_components/expenses-per-category.tsx
new file mode 100644
index 0000000..001dff6
--- /dev/null
+++ b/app/(authenticated)/dashboard/_components/expenses-per-category.tsx
@@ -0,0 +1,35 @@
+import { CardContent, CardHeader, CardTitle } from "@/app/_components/ui/card";
+import { Progress } from "@/app/_components/ui/progress";
+import { ScrollArea } from "@/app/_components/ui/scroll-area";
+import { TRANSACTION_CATEGORY_MAP } from "@/app/_constants/transaction";
+import type { TotalExpensePerCategory } from "@/app/_data/get-dashboard/types";
+
+interface ExpensesPerCategoryProps {
+ expensesPerCategory: TotalExpensePerCategory[];
+}
+const ExpensesPerCategory = ({
+ expensesPerCategory,
+}: ExpensesPerCategoryProps) => {
+ return (
+
+
+ Gastos por categoría
+
+
+ {expensesPerCategory.map((category) => (
+
+
+
+ {TRANSACTION_CATEGORY_MAP[category.category]}
+
+
{category.percentageOfTotal}%
+
+
+
+ ))}
+
+
+ );
+};
+
+export default ExpensesPerCategory;
diff --git a/app/(authenticated)/dashboard/page.tsx b/app/(authenticated)/dashboard/page.tsx
index 37c6a3e..03c0ac5 100644
--- a/app/(authenticated)/dashboard/page.tsx
+++ b/app/(authenticated)/dashboard/page.tsx
@@ -4,6 +4,7 @@ import SummaryCards from "./_components/summary-cards";
import TimeSelect from "./_components/time-select";
import TransactionsPieChart from "./_components/transactions-pie-chart";
import GetDashboard from "@/app/_data/get-dashboard";
+import ExpensesPerCategory from "./_components/expenses-per-category";
interface HomeProps {
searchParams: {
@@ -40,6 +41,9 @@ const Home = async ({ searchParams: { month = "1" } }: HomeProps) => {
+
diff --git a/app/_components/ui/progress.tsx b/app/_components/ui/progress.tsx
new file mode 100644
index 0000000..fc02b79
--- /dev/null
+++ b/app/_components/ui/progress.tsx
@@ -0,0 +1,28 @@
+"use client";
+
+import * as React from "react";
+import * as ProgressPrimitive from "@radix-ui/react-progress";
+
+import { cn } from "@/app/_lib/utils";
+
+const Progress = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, value, ...props }, ref) => (
+
+
+
+));
+Progress.displayName = ProgressPrimitive.Root.displayName;
+
+export { Progress };
diff --git a/app/_components/ui/scroll-area.tsx b/app/_components/ui/scroll-area.tsx
new file mode 100644
index 0000000..4540a61
--- /dev/null
+++ b/app/_components/ui/scroll-area.tsx
@@ -0,0 +1,48 @@
+"use client";
+
+import * as React from "react";
+import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
+
+import { cn } from "@/app/_lib/utils";
+
+const ScrollArea = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+ {children}
+
+
+
+
+));
+ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
+
+const ScrollBar = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, orientation = "vertical", ...props }, ref) => (
+
+
+
+));
+ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
+
+export { ScrollArea, ScrollBar };
diff --git a/app/_constants/transaction.ts b/app/_constants/transaction.ts
index 2a64953..605245e 100644
--- a/app/_constants/transaction.ts
+++ b/app/_constants/transaction.ts
@@ -31,4 +31,5 @@ export const TRANSACTION_CATEGORY_MAP: Record = {
TRANSPORTATION: "Transporte",
SALARY: "Salário",
UTILITY: "Utilidade",
+ OTHER: "Outro",
};
diff --git a/app/_data/get-dashboard/index.ts b/app/_data/get-dashboard/index.ts
index 2bece88..8ebde4f 100644
--- a/app/_data/get-dashboard/index.ts
+++ b/app/_data/get-dashboard/index.ts
@@ -1,6 +1,9 @@
import { db } from "@/app/_lib/prisma";
import { TransactionType } from "@prisma/client";
-import type { TransactionPercentagesPerType } from "./types";
+import type {
+ TotalExpensePerCategory,
+ TransactionPercentagesPerType,
+} from "./types";
const getDashboard = async (month: number) => {
const where = {
@@ -68,12 +71,32 @@ const getDashboard = async (month: number) => {
),
};
+ const totalExpensePerCategory: TotalExpensePerCategory[] = (
+ await db.transactions.groupBy({
+ by: ["category"],
+ where: {
+ ...where,
+ type: TransactionType.EXPENSE,
+ },
+ _sum: {
+ amount: true,
+ },
+ })
+ ).map((category) => ({
+ category: category.category,
+ totalAmount: Number(category._sum.amount),
+ percentageOfTotal: Math.round(
+ (Number(category._sum.amount) / Number(expensesTotal)) * 100,
+ ),
+ }));
+
return {
depositsTotal: Number(depositsTotal),
investmentsTotal: Number(investmentsTotal),
expensesTotal: Number(expensesTotal),
balance: Number(balance),
typesPercentages,
+ totalExpensePerCategory,
};
};
diff --git a/app/_data/get-dashboard/types.ts b/app/_data/get-dashboard/types.ts
index aa0f52b..c116531 100644
--- a/app/_data/get-dashboard/types.ts
+++ b/app/_data/get-dashboard/types.ts
@@ -1,5 +1,11 @@
-import type { TransactionType } from "@prisma/client";
+import type { TransactionCategory, TransactionType } from "@prisma/client";
export type TransactionPercentagesPerType = {
[key in TransactionType]: number;
};
+
+export interface TotalExpensePerCategory {
+ category: TransactionCategory;
+ totalAmount: number;
+ percentageOfTotal: number;
+}
diff --git a/package.json b/package.json
index 0e3a641..5f4649e 100644
--- a/package.json
+++ b/package.json
@@ -17,6 +17,8 @@
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-popover": "^1.1.2",
+ "@radix-ui/react-progress": "^1.1.0",
+ "@radix-ui/react-scroll-area": "^1.2.0",
"@radix-ui/react-select": "^2.1.2",
"@radix-ui/react-slot": "^1.1.0",
"@tanstack/react-table": "8.20.5",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 33e5b27..3681345 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -29,6 +29,12 @@ importers:
'@radix-ui/react-popover':
specifier: ^1.1.2
version: 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-progress':
+ specifier: ^1.1.0
+ version: 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-scroll-area':
+ specifier: ^1.2.0
+ version: 1.2.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-select':
specifier: ^2.1.2
version: 2.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -564,6 +570,32 @@ packages:
'@types/react-dom':
optional: true
+ '@radix-ui/react-progress@1.1.0':
+ resolution: {integrity: sha512-aSzvnYpP725CROcxAOEBVZZSIQVQdHgBr2QQFKySsaD14u8dNT0batuXI+AAGDdAHfXH8rbnHmjYFqVJ21KkRg==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-scroll-area@1.2.0':
+ resolution: {integrity: sha512-q2jMBdsJ9zB7QG6ngQNzNwlvxLQqONyL58QbEGwuyRZZb/ARQwk3uQVbCF7GvQVOtV6EU/pDxAw3zRzJZI3rpQ==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
'@radix-ui/react-select@2.1.2':
resolution: {integrity: sha512-rZJtWmorC7dFRi0owDmoijm6nSJH1tVw64QGiNIZ9PNLyBDtG+iAq+XGsya052At4BfarzY/Dhv9wrrUr6IMZA==}
peerDependencies:
@@ -2957,6 +2989,33 @@ snapshots:
'@types/react': 18.3.12
'@types/react-dom': 18.3.1
+ '@radix-ui/react-progress@1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@radix-ui/react-context': 1.1.0(@types/react@18.3.12)(react@18.3.1)
+ '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.12
+ '@types/react-dom': 18.3.1
+
+ '@radix-ui/react-scroll-area@1.2.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@radix-ui/number': 1.1.0
+ '@radix-ui/primitive': 1.1.0
+ '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1)
+ '@radix-ui/react-context': 1.1.1(@types/react@18.3.12)(react@18.3.1)
+ '@radix-ui/react-direction': 1.1.0(@types/react@18.3.12)(react@18.3.1)
+ '@radix-ui/react-presence': 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.12)(react@18.3.1)
+ '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.12)(react@18.3.1)
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.12
+ '@types/react-dom': 18.3.1
+
'@radix-ui/react-select@2.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/number': 1.1.0