Skip to content

Commit

Permalink
feat: ✨ add expenses per category component
Browse files Browse the repository at this point in the history
  • Loading branch information
neopromic committed Nov 12, 2024
1 parent 6a21917 commit dd32619
Show file tree
Hide file tree
Showing 9 changed files with 208 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -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 (
<ScrollArea className="col-span-2 h-full rounded-md border pb-6">
<CardHeader>
<CardTitle className="font-bold">Gastos por categoría</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
{expensesPerCategory.map((category) => (
<div className="space-y-2" key={category.category}>
<div className="flex w-full justify-between">
<p className="text-sm font-bold">
{TRANSACTION_CATEGORY_MAP[category.category]}
</p>
<p className="text-sm font-bold">{category.percentageOfTotal}%</p>
</div>
<Progress value={category.percentageOfTotal} />
</div>
))}
</CardContent>
</ScrollArea>
);
};

export default ExpensesPerCategory;
4 changes: 4 additions & 0 deletions app/(authenticated)/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -40,6 +41,9 @@ const Home = async ({ searchParams: { month = "1" } }: HomeProps) => {

<div className="grid grid-cols-3 grid-rows-1 gap-6">
<TransactionsPieChart {...dashboard} />
<ExpensesPerCategory
expensesPerCategory={dashboard.totalExpensePerCategory}
/>
</div>
</div>
</div>
Expand Down
28 changes: 28 additions & 0 deletions app/_components/ui/progress.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof ProgressPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
>(({ className, value, ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
className={cn(
"relative h-4 w-full overflow-hidden rounded-full bg-white bg-opacity-[3%]",
className,
)}
{...props}
>
<ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-[#71717A] transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
));
Progress.displayName = ProgressPrimitive.Root.displayName;

export { Progress };
48 changes: 48 additions & 0 deletions app/_components/ui/scroll-area.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn("relative overflow-hidden", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
));
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;

const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
className,
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
));
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;

export { ScrollArea, ScrollBar };
1 change: 1 addition & 0 deletions app/_constants/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,5 @@ export const TRANSACTION_CATEGORY_MAP: Record<string, string> = {
TRANSPORTATION: "Transporte",
SALARY: "Salário",
UTILITY: "Utilidade",
OTHER: "Outro",
};
25 changes: 24 additions & 1 deletion app/_data/get-dashboard/index.ts
Original file line number Diff line number Diff line change
@@ -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 = {
Expand Down Expand Up @@ -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,
};
};

Expand Down
8 changes: 7 additions & 1 deletion app/_data/get-dashboard/types.ts
Original file line number Diff line number Diff line change
@@ -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;
}
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
59 changes: 59 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit dd32619

Please sign in to comment.