From dd326191ac0f83590ba3b5031dadbab0a5461d57 Mon Sep 17 00:00:00 2001 From: Wesley Souza | Neo Date: Tue, 12 Nov 2024 11:51:15 -0300 Subject: [PATCH] feat: :sparkles: add expenses per category component --- .../_components/expenses-per-category.tsx | 35 +++++++++++ app/(authenticated)/dashboard/page.tsx | 4 ++ app/_components/ui/progress.tsx | 28 +++++++++ app/_components/ui/scroll-area.tsx | 48 +++++++++++++++ app/_constants/transaction.ts | 1 + app/_data/get-dashboard/index.ts | 25 +++++++- app/_data/get-dashboard/types.ts | 8 ++- package.json | 2 + pnpm-lock.yaml | 59 +++++++++++++++++++ 9 files changed, 208 insertions(+), 2 deletions(-) create mode 100644 app/(authenticated)/dashboard/_components/expenses-per-category.tsx create mode 100644 app/_components/ui/progress.tsx create mode 100644 app/_components/ui/scroll-area.tsx 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