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 ( - <main className="flex items-center justify-center min-h-screen text-3xl"> - Next Finances - </main> + <DashboardPageClient + scope={scope.type} + scopes={scopes} + income={incomeFormat} + expenses={expensesFormat} + balanceDevelopment={balanceDevelopmentFormat} + categoryExpenses={categoryExpensesFormat} + categoryPercentages={categoryPercentages} + entityExpenses={entityExpensesFormat} + entityPercentages={entityPercentages} + /> ); } 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 ( + <div className="flex flex-col space-y-4 p-8"> + + <div className="flex flex-row items-center justify-between"> + + <h1 className="text-2xl font-semibold leading-6 text-gray-900 dark:text-white">Dashboard</h1> + + <Select + onValueChange={(value) => { + router.push(`?scope=${value}`); + router.refresh(); + }} + value={scope} + > + <SelectTrigger className="w-[250px]"> + <SelectValue placeholder="Select a scope"/> + </SelectTrigger> + <SelectContent> + { + scopes.map((scope) => ( + <SelectItem value={scope} key={scope}>{scope}</SelectItem> + )) + } + </SelectContent> + </Select> + + </div> + + <Card> + <div className="grid grid-cols-1 md:grid-cols-3 divide-y md:divide-y-0 md:divide-x overflow-hidden"> + + <div> + <CardHeader> + <CardTitle>Income</CardTitle> + </CardHeader> + <CardContent> + <div className="flex items-baseline text-2xl font-semibold text-orange-600"> + {income} + </div> + </CardContent> + </div> + + <div> + <CardHeader> + <CardTitle>Expanses</CardTitle> + </CardHeader> + <CardContent> + <div className="flex items-baseline text-2xl font-semibold text-orange-600"> + {expenses} + </div> + </CardContent> + </div> + + <div> + <CardHeader> + <CardTitle>Development</CardTitle> + </CardHeader> + <CardContent> + <div className="flex items-baseline text-2xl font-semibold text-orange-600"> + {balanceDevelopment} + </div> + </CardContent> + </div> + + </div> + </Card> + + <Card> + <div className="grid grid-cols-1 md:grid-cols-2 divide-y md:divide-y-0 md:divide-x overflow-hidden"> + + <div> + <CardHeader> + <CardTitle>Expenses</CardTitle> + <CardDescription>by category (%)</CardDescription> + </CardHeader> + <CardContent> + { + categoryPercentages.map(item => ( + <div className="flex items-center justify-between mt-4" key={item.category.id}> + <div className="flex items-center"> + <div className="w-2 h-2 rounded-full mr-2" + style={{backgroundColor: item.category.color}}/> + <span + className="text-sm text-gray-900 dark:text-white">{item.category.name}</span> + </div> + <span className="text-sm text-gray-900 dark:text-white">{item.value}%</span> + </div> + )) + } + </CardContent> + </div> + + <div> + <CardHeader> + <CardTitle>Expenses</CardTitle> + <CardDescription>by category (€)</CardDescription> + </CardHeader> + <CardContent> + { + categoryExpenses.map((item) => ( + <div className="flex items-center justify-between mt-4" key={item.category.id}> + <div className="flex items-center"> + <div className="w-2 h-2 rounded-full mr-2" + style={{backgroundColor: item.category.color}}/> + <span + className="text-sm text-gray-900 dark:text-white">{item.category.name}</span> + </div> + <span className="text-sm text-gray-900 dark:text-white">{item.value}</span> + </div> + )) + } + </CardContent> + </div> + + </div> + </Card> + + <Card> + <div className="grid grid-cols-1 md:grid-cols-2 divide-y md:divide-y-0 md:divide-x overflow-hidden"> + + <div> + <CardHeader> + <CardTitle>Expenses</CardTitle> + <CardDescription>by entity (%)</CardDescription> + </CardHeader> + <CardContent> + + { + entityPercentages.map(item => ( + <div className="flex items-center justify-between mt-4" key={item.entity.id}> + <div className="flex items-center"> + <span + className="text-sm text-gray-900 dark:text-white">{item.entity.name}</span> + </div> + <span className="text-sm text-gray-900 dark:text-white">{item.value}%</span> + </div> + )) + } + </CardContent> + </div> + + <div> + <CardHeader> + <CardTitle>Expenses</CardTitle> + <CardDescription>by entity (€)</CardDescription> + </CardHeader> + <CardContent> + { + entityExpenses.map(item => ( + <div className="flex items-center justify-between mt-4" key={item.entity.id}> + <div className="flex items-center"> + <span + className="text-sm text-gray-900 dark:text-white">{item.entity.name}</span> + </div> + <span className="text-sm text-gray-900 dark:text-white">{item.value}</span> + </div> + )) + } + </CardContent> + </div> + </div> + </Card> + </div> + ); +} \ No newline at end of file