From 404258a57a80d51a3606141352ee6f63de8babc9 Mon Sep 17 00:00:00 2001 From: Markus Thielker Date: Mon, 11 Mar 2024 02:46:08 +0100 Subject: [PATCH] N-FIN-8: add dashboard page UI --- src/app/page.tsx | 204 ++++++++++++++++- .../dashboardPageClientComponents.tsx | 212 ++++++++++++++++++ 2 files changed, 412 insertions(+), 4 deletions(-) create mode 100644 src/components/dashboardPageClientComponents.tsx 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/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