diff --git a/src/app/page.tsx b/src/app/page.tsx index c527955..94151cd 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,9 +1,205 @@ import React from 'react'; +import { Category, Entity, EntityType } from '@prisma/client'; +import { Scope, ScopeType } from '@/lib/types/scope'; +import { prismaClient } from '@/prisma'; +import { getUser } from '@/auth'; +import DashboardPageClient from '@/components/dashboardPageClientComponents'; + +export type CategoryNumber = { + category: Category, + value: number, +} + +export type EntityNumber = { + entity: Entity, + value: number, +} + +export default async function DashboardPage(props: { searchParams?: { scope: ScopeType } }) { + + const user = await getUser(); + if (!user) { + return; + } + + const scope = Scope.of(props.searchParams?.scope || ScopeType.ThisMonth); + + // get all payments in the current scope + const payments = await prismaClient.payment.findMany({ + where: { + userId: user?.id, + date: { + gte: scope.start, + lte: scope.end, + }, + }, + include: { + payor: true, + payee: true, + category: true, + }, + }); + + let income = 0; + let expenses = 0; + + // sum up income + payments.filter(payment => + payment.payor.type === EntityType.Entity && + payment.payee.type === EntityType.Account, + ).forEach(payment => income += payment.amount); + + // sum up expenses + payments.filter(payment => + payment.payor.type === EntityType.Account && + payment.payee.type === EntityType.Entity, + ).forEach(payment => expenses += payment.amount); + + // ############################ + // Expenses by category + // ############################ + + // init helper variables (category) + const categoryExpenses: CategoryNumber[] = []; + const otherCategory: CategoryNumber = { + category: { + id: 0, + userId: '', + name: 'Other', + color: '#888888', + createdAt: new Date(), + updatedAt: new Date(), + }, + value: 0, + }; + + // sum up expenses per category + payments.filter(payment => + payment.payor.type === EntityType.Account && + payment.payee.type === EntityType.Entity, + ).forEach(payment => { + + if (!payment.category) { + otherCategory.value += payment.amount; + return; + } + + const categoryNumber = categoryExpenses.find(categoryNumber => categoryNumber.category.id === payment.category?.id); + if (categoryNumber) { + categoryNumber.value += payment.amount; + } else { + categoryExpenses.push({category: payment.category, value: payment.amount}); + } + }); + categoryExpenses.sort((a, b) => Number(b.value - a.value)); + if (otherCategory.value > 0) { + categoryExpenses.push(otherCategory); + } + + // ############################ + // Expenses by entity + // ############################ + + // init helper variables (entity) + const entityExpenses: EntityNumber[] = []; + const otherEntity: EntityNumber = { + entity: { + id: 0, + userId: '', + name: 'Other', + type: EntityType.Entity, + createdAt: new Date(), + updatedAt: new Date(), + }, + value: 0, + }; + + // sum up expenses per category + payments.filter(payment => + payment.payor.type === EntityType.Account && + payment.payee.type === EntityType.Entity, + ).forEach(payment => { + + // if (!payment.payee) { + // other.value += payment.amount + // return + // } + + const entityNumber = entityExpenses.find(entityNumber => entityNumber.entity.id === payment.payee?.id); + if (entityNumber) { + entityNumber.value += payment.amount; + } else { + entityExpenses.push({entity: payment.payee, value: payment.amount}); + } + }); + entityExpenses.sort((a, b) => Number(b.value - a.value)); + if (otherEntity.value > 0) { + entityExpenses.push(otherEntity); + } + + // ############################ + // Format data + // ############################ + + const balanceDevelopment = income - expenses; + const scopes = Object.values(ScopeType).map(scopeType => scopeType.toString()); + + const incomeFormat = new Intl.NumberFormat('de-DE', { + style: 'currency', + currency: 'EUR', + }).format(Number(income) / 100); + + const expensesFormat = new Intl.NumberFormat('de-DE', { + style: 'currency', + currency: 'EUR', + }).format(Number(expenses) / 100); + + const balanceDevelopmentFormat = new Intl.NumberFormat('de-DE', { + style: 'currency', + currency: 'EUR', + }).format(Number(balanceDevelopment) / 100); + + const categoryExpensesFormat = categoryExpenses.map(categoryNumber => ({ + category: categoryNumber.category, + value: new Intl.NumberFormat('de-DE', { + style: 'currency', + currency: 'EUR', + }).format(Number(categoryNumber.value) / 100), + })); + + const categoryPercentages = categoryExpenses.map(categoryNumber => ({ + category: categoryNumber.category, + value: amountToPercent(categoryNumber.value, expenses), + })); + + const entityExpensesFormat = entityExpenses.map(entityNumber => ({ + entity: entityNumber.entity, + value: new Intl.NumberFormat('de-DE', { + style: 'currency', + currency: 'EUR', + }).format(Number(entityNumber.value) / 100), + })); + + const entityPercentages = entityExpenses.map(entityNumber => ({ + entity: entityNumber.entity, + value: amountToPercent(entityNumber.value, expenses), + })); + + function amountToPercent(amount: number, total: number): string { + return (Number(amount) / Number(total) * 100).toFixed(2); + } -export default async function Home() { return ( -
- Next Finances -
+ ); } diff --git a/src/components/categoryPageClientComponents.tsx b/src/components/categoryPageClientComponents.tsx index 6de7ec5..ac8b908 100644 --- a/src/components/categoryPageClientComponents.tsx +++ b/src/components/categoryPageClientComponents.tsx @@ -9,7 +9,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from import { DataTable } from '@/components/ui/data-table'; import { columns } from '@/app/categories/columns'; import { z } from 'zod'; -import { ActionResponse } from '@/lib/types/ActionResponse'; +import { ActionResponse } from '@/lib/types/actionResponse'; import { useRouter } from 'next/navigation'; import { toast } from 'sonner'; import { sonnerContent } from '@/components/ui/sonner'; diff --git a/src/components/dashboardPageClientComponents.tsx b/src/components/dashboardPageClientComponents.tsx new file mode 100644 index 0000000..e45aaff --- /dev/null +++ b/src/components/dashboardPageClientComponents.tsx @@ -0,0 +1,212 @@ +'use client'; + +import { useRouter } from 'next/navigation'; +import { Category, Entity } from '@prisma/client'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import React from 'react'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; + +export default function DashboardPageClientContent( + { + scope, + scopes, + income, + expenses, + balanceDevelopment, + categoryExpenses, + categoryPercentages, + entityExpenses, + entityPercentages, + }: { + scope: string, + scopes: string[], + income: string, + expenses: string, + balanceDevelopment: string, + categoryExpenses: { + category: Category, + value: string, + }[], + categoryPercentages: { + category: Category, + value: string, + }[], + entityExpenses: { + entity: Entity, + value: string, + }[], + entityPercentages: { + entity: Entity, + value: string, + }[], + + }, +) { + + const router = useRouter(); + + return ( +
+ +
+ +

Dashboard

+ + + +
+ + +
+ +
+ + Income + + +
+ {income} +
+
+
+ +
+ + Expanses + + +
+ {expenses} +
+
+
+ +
+ + Development + + +
+ {balanceDevelopment} +
+
+
+ +
+
+ + +
+ +
+ + Expenses + by category (%) + + + { + categoryPercentages.map(item => ( +
+
+
+ {item.category.name} +
+ {item.value}% +
+ )) + } + +
+ +
+ + Expenses + by category (€) + + + { + categoryExpenses.map((item) => ( +
+
+
+ {item.category.name} +
+ {item.value} +
+ )) + } + +
+ +
+ + + +
+ +
+ + Expenses + by entity (%) + + + + { + entityPercentages.map(item => ( +
+
+ {item.entity.name} +
+ {item.value}% +
+ )) + } +
+
+ +
+ + Expenses + by entity (€) + + + { + entityExpenses.map(item => ( +
+
+ {item.entity.name} +
+ {item.value} +
+ )) + } +
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/entityPageClientComponents.tsx b/src/components/entityPageClientComponents.tsx index 282dcc4..4f4a8bd 100644 --- a/src/components/entityPageClientComponents.tsx +++ b/src/components/entityPageClientComponents.tsx @@ -11,7 +11,7 @@ import { DataTable } from '@/components/ui/data-table'; import { columns } from '@/app/entities/columns'; import { z } from 'zod'; import { entityFormSchema } from '@/lib/form-schemas/entityFormSchema'; -import { ActionResponse } from '@/lib/types/ActionResponse'; +import { ActionResponse } from '@/lib/types/actionResponse'; import { Input } from '@/components/ui/input'; import { useRouter } from 'next/navigation'; import { toast } from 'sonner'; diff --git a/src/components/form/categoryForm.tsx b/src/components/form/categoryForm.tsx index 89c4bd2..2c2af97 100644 --- a/src/components/form/categoryForm.tsx +++ b/src/components/form/categoryForm.tsx @@ -7,7 +7,7 @@ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from ' import { Input } from '@/components/ui/input'; import React from 'react'; import { Button } from '@/components/ui/button'; -import { ActionResponse } from '@/lib/types/ActionResponse'; +import { ActionResponse } from '@/lib/types/actionResponse'; import { useRouter } from 'next/navigation'; import { toast } from 'sonner'; import { sonnerContent } from '@/components/ui/sonner'; diff --git a/src/components/form/entityForm.tsx b/src/components/form/entityForm.tsx index 85dbd8a..274c290 100644 --- a/src/components/form/entityForm.tsx +++ b/src/components/form/entityForm.tsx @@ -7,7 +7,7 @@ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from ' import { Input } from '@/components/ui/input'; import React from 'react'; import { Button } from '@/components/ui/button'; -import { ActionResponse } from '@/lib/types/ActionResponse'; +import { ActionResponse } from '@/lib/types/actionResponse'; import { useRouter } from 'next/navigation'; import { toast } from 'sonner'; import { sonnerContent } from '@/components/ui/sonner'; diff --git a/src/components/form/paymentForm.tsx b/src/components/form/paymentForm.tsx index ddf66dc..d6323dd 100644 --- a/src/components/form/paymentForm.tsx +++ b/src/components/form/paymentForm.tsx @@ -7,7 +7,7 @@ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from ' import { Input } from '@/components/ui/input'; import React, { useState } from 'react'; import { Button } from '@/components/ui/button'; -import { ActionResponse } from '@/lib/types/ActionResponse'; +import { ActionResponse } from '@/lib/types/actionResponse'; import { useRouter } from 'next/navigation'; import { toast } from 'sonner'; import { sonnerContent } from '@/components/ui/sonner'; diff --git a/src/components/form/signInForm.tsx b/src/components/form/signInForm.tsx index 7d95e82..e4e96fa 100644 --- a/src/components/form/signInForm.tsx +++ b/src/components/form/signInForm.tsx @@ -8,7 +8,7 @@ import { Input } from '@/components/ui/input'; import React from 'react'; import { Button } from '@/components/ui/button'; import { signInFormSchema } from '@/lib/form-schemas/signInFormSchema'; -import { ActionResponse } from '@/lib/types/ActionResponse'; +import { ActionResponse } from '@/lib/types/actionResponse'; import { useRouter } from 'next/navigation'; import { toast } from 'sonner'; import { sonnerContent } from '@/components/ui/sonner'; diff --git a/src/components/form/signOutForm.tsx b/src/components/form/signOutForm.tsx index b47649f..9dd51fe 100644 --- a/src/components/form/signOutForm.tsx +++ b/src/components/form/signOutForm.tsx @@ -1,6 +1,6 @@ 'use client'; -import { ActionResponse } from '@/lib/types/ActionResponse'; +import { ActionResponse } from '@/lib/types/actionResponse'; import { Button } from '@/components/ui/button'; import React from 'react'; import { useRouter } from 'next/navigation'; diff --git a/src/components/form/signUpForm.tsx b/src/components/form/signUpForm.tsx index ed53c50..2898733 100644 --- a/src/components/form/signUpForm.tsx +++ b/src/components/form/signUpForm.tsx @@ -8,7 +8,7 @@ import { Input } from '@/components/ui/input'; import React from 'react'; import { Button } from '@/components/ui/button'; import { signUpFormSchema } from '@/lib/form-schemas/signUpFormSchema'; -import { ActionResponse } from '@/lib/types/ActionResponse'; +import { ActionResponse } from '@/lib/types/actionResponse'; import { useRouter } from 'next/navigation'; import { toast } from 'sonner'; import { sonnerContent } from '@/components/ui/sonner'; diff --git a/src/components/paymentPageClientComponents.tsx b/src/components/paymentPageClientComponents.tsx index 2308ce2..22fa031 100644 --- a/src/components/paymentPageClientComponents.tsx +++ b/src/components/paymentPageClientComponents.tsx @@ -7,7 +7,7 @@ import { Edit, Trash } from 'lucide-react'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; import { DataTable } from '@/components/ui/data-table'; import { z } from 'zod'; -import { ActionResponse } from '@/lib/types/ActionResponse'; +import { ActionResponse } from '@/lib/types/actionResponse'; import { useRouter } from 'next/navigation'; import { toast } from 'sonner'; import { sonnerContent } from '@/components/ui/sonner'; diff --git a/src/components/ui/sonner.tsx b/src/components/ui/sonner.tsx index cd1cc44..6a9d8be 100644 --- a/src/components/ui/sonner.tsx +++ b/src/components/ui/sonner.tsx @@ -4,7 +4,7 @@ import { useTheme } from 'next-themes'; import { Toaster as Sonner } from 'sonner'; import { AlertCircle, CheckCircle, HelpCircle, XCircle } from 'lucide-react'; import React, { JSX } from 'react'; -import { ActionResponse } from '@/lib/types/ActionResponse'; +import { ActionResponse } from '@/lib/types/actionResponse'; type ToasterProps = React.ComponentProps diff --git a/src/lib/actions/categoryCreateUpdate.ts b/src/lib/actions/categoryCreateUpdate.ts index 4248bd3..a7796d5 100644 --- a/src/lib/actions/categoryCreateUpdate.ts +++ b/src/lib/actions/categoryCreateUpdate.ts @@ -1,5 +1,5 @@ import { z } from 'zod'; -import { ActionResponse } from '@/lib/types/ActionResponse'; +import { ActionResponse } from '@/lib/types/actionResponse'; import { prismaClient } from '@/prisma'; import { getUser } from '@/auth'; import { URL_SIGN_IN } from '@/lib/constants'; diff --git a/src/lib/actions/categoryDelete.ts b/src/lib/actions/categoryDelete.ts index 1a7f55b..11cc496 100644 --- a/src/lib/actions/categoryDelete.ts +++ b/src/lib/actions/categoryDelete.ts @@ -1,4 +1,4 @@ -import { ActionResponse } from '@/lib/types/ActionResponse'; +import { ActionResponse } from '@/lib/types/actionResponse'; import { prismaClient } from '@/prisma'; import { getUser } from '@/auth'; import { URL_SIGN_IN } from '@/lib/constants'; diff --git a/src/lib/actions/entityCreateUpdate.ts b/src/lib/actions/entityCreateUpdate.ts index 37027b6..3d3ca38 100644 --- a/src/lib/actions/entityCreateUpdate.ts +++ b/src/lib/actions/entityCreateUpdate.ts @@ -1,5 +1,5 @@ import { z } from 'zod'; -import { ActionResponse } from '@/lib/types/ActionResponse'; +import { ActionResponse } from '@/lib/types/actionResponse'; import { entityFormSchema } from '@/lib/form-schemas/entityFormSchema'; import { prismaClient } from '@/prisma'; import { getUser } from '@/auth'; diff --git a/src/lib/actions/entityDelete.ts b/src/lib/actions/entityDelete.ts index 37ea1bd..cb2dd45 100644 --- a/src/lib/actions/entityDelete.ts +++ b/src/lib/actions/entityDelete.ts @@ -1,4 +1,4 @@ -import { ActionResponse } from '@/lib/types/ActionResponse'; +import { ActionResponse } from '@/lib/types/actionResponse'; import { prismaClient } from '@/prisma'; import { getUser } from '@/auth'; import { URL_SIGN_IN } from '@/lib/constants'; diff --git a/src/lib/actions/paymentCreateUpdate.ts b/src/lib/actions/paymentCreateUpdate.ts index d49646d..6f889ef 100644 --- a/src/lib/actions/paymentCreateUpdate.ts +++ b/src/lib/actions/paymentCreateUpdate.ts @@ -1,5 +1,5 @@ import { z } from 'zod'; -import { ActionResponse } from '@/lib/types/ActionResponse'; +import { ActionResponse } from '@/lib/types/actionResponse'; import { prismaClient } from '@/prisma'; import { getUser } from '@/auth'; import { URL_SIGN_IN } from '@/lib/constants'; diff --git a/src/lib/actions/paymentDelete.ts b/src/lib/actions/paymentDelete.ts index 46ee086..3401e92 100644 --- a/src/lib/actions/paymentDelete.ts +++ b/src/lib/actions/paymentDelete.ts @@ -1,4 +1,4 @@ -import { ActionResponse } from '@/lib/types/ActionResponse'; +import { ActionResponse } from '@/lib/types/actionResponse'; import { prismaClient } from '@/prisma'; import { getUser } from '@/auth'; import { URL_SIGN_IN } from '@/lib/constants'; diff --git a/src/lib/actions/signIn.ts b/src/lib/actions/signIn.ts index 4027f75..5168179 100644 --- a/src/lib/actions/signIn.ts +++ b/src/lib/actions/signIn.ts @@ -3,7 +3,7 @@ import { Argon2id } from 'oslo/password'; import { lucia } from '@/auth'; import { cookies } from 'next/headers'; import { signInFormSchema } from '@/lib/form-schemas/signInFormSchema'; -import { ActionResponse } from '@/lib/types/ActionResponse'; +import { ActionResponse } from '@/lib/types/actionResponse'; import { URL_HOME } from '@/lib/constants'; import { prismaClient } from '@/prisma'; diff --git a/src/lib/actions/signOut.ts b/src/lib/actions/signOut.ts index 4525a8e..de58559 100644 --- a/src/lib/actions/signOut.ts +++ b/src/lib/actions/signOut.ts @@ -1,6 +1,6 @@ import { getSession, lucia } from '@/auth'; import { cookies } from 'next/headers'; -import { ActionResponse } from '@/lib/types/ActionResponse'; +import { ActionResponse } from '@/lib/types/actionResponse'; import { URL_SIGN_IN } from '@/lib/constants'; export default async function signOut(): Promise { diff --git a/src/lib/actions/signUp.ts b/src/lib/actions/signUp.ts index 4f731b1..1b40aff 100644 --- a/src/lib/actions/signUp.ts +++ b/src/lib/actions/signUp.ts @@ -4,7 +4,7 @@ import { generateId } from 'lucia'; import { lucia } from '@/auth'; import { cookies } from 'next/headers'; import { signUpFormSchema } from '@/lib/form-schemas/signUpFormSchema'; -import { ActionResponse } from '@/lib/types/ActionResponse'; +import { ActionResponse } from '@/lib/types/actionResponse'; import { URL_HOME } from '@/lib/constants'; import { prismaClient } from '@/prisma'; diff --git a/src/lib/types/ActionResponse.ts b/src/lib/types/actionResponse.ts similarity index 100% rename from src/lib/types/ActionResponse.ts rename to src/lib/types/actionResponse.ts diff --git a/src/lib/types/scope.ts b/src/lib/types/scope.ts new file mode 100644 index 0000000..a5f589a --- /dev/null +++ b/src/lib/types/scope.ts @@ -0,0 +1,48 @@ +export enum ScopeType { + ThisMonth = 'This month', + LastMonth = 'Last month', + ThisYear = 'This year', + LastYear = 'Last year', +} + +export class Scope { + + public type: ScopeType; + public start: Date; + public end: Date; + + private constructor(type: ScopeType, start: Date, end: Date) { + this.type = type; + this.start = start; + this.end = end; + } + + static of(type: ScopeType): Scope { + + let start: Date; + let end: Date; + + const today = new Date(); + + switch (type) { + case ScopeType.ThisMonth: + start = new Date(today.getFullYear(), today.getMonth(), 1, 0, 0, 0, 0); + end = new Date(today.getFullYear(), today.getMonth() + 1, 0, 24, 0, 0, -1); + break; + case ScopeType.LastMonth: + start = new Date(today.getFullYear(), today.getMonth() - 1, 1, 0, 0, 0, 0); + end = new Date(today.getFullYear(), today.getMonth(), 0, 24, 0, 0, -1); + break; + case ScopeType.ThisYear: + start = new Date(today.getFullYear(), 0, 1, 0, 0, 0, 0); + end = new Date(today.getFullYear(), 11, 31, 24, 0, 0, -1); + break; + case ScopeType.LastYear: + start = new Date(today.getFullYear() - 1, 0, 1, 0, 0, 0, 0); + end = new Date(today.getFullYear() - 1, 11, 31, 24, 0, 0, -1); + break; + } + + return new Scope(type, start, end); + } +}