import { redirect } from "next/navigation"; import { DashboardShell } from "@/components/dashboard/dashboard-shell"; import { getCurrentAccountingPeriod } from "@/lib/accounting-periods"; import { getAppSettings, serializeAppSettings } from "@/lib/app-settings"; import { getRollbackMetadata } from "@/lib/audit-log"; import type { DashboardAccountingPeriod, DashboardAuditLog, DashboardDonation, DashboardManagedUser, DashboardSettings, DashboardViewer, DashboardWorkingGroup } from "@/lib/dashboard-types"; import { canManageUsers, normalizeApprovalPermissions } from "@/lib/domain"; import prisma from "@/lib/prisma"; import { buildRecurringOccurrences, getExpensePeriodAmount } from "@/lib/recurring-expenses"; import { getCurrentViewer } from "@/lib/session"; export const dynamic = "force-dynamic"; export default async function DashboardPage() { const viewer = await getCurrentViewer(); if (!viewer) { redirect("/login"); } const [currentPeriod, appSettings] = await Promise.all([getCurrentAccountingPeriod(), getAppSettings()]); if (!currentPeriod) { throw new Error("Kein Abrechnungszeitraum gefunden."); } const accountingPeriods = await prisma.accountingPeriod.findMany({ orderBy: { startsAt: "desc" } }); const workingGroups = await prisma.workingGroup.findMany({ include: { members: { select: { id: true, name: true, username: true, role: true } }, budgets: { where: { periodId: currentPeriod.id }, orderBy: { name: "asc" }, include: { expenses: { orderBy: { createdAt: "desc" }, include: { creator: { select: { id: true, username: true } }, approvals: { orderBy: { timestamp: "asc" }, include: { user: { select: { id: true, username: true } } } }, documents: { orderBy: { createdAt: "asc" }, include: { uploadedBy: { select: { id: true, username: true } } } } } } } } }, orderBy: { name: "asc" } }); const managedUsers = canManageUsers(viewer.role) ? await prisma.user.findMany({ include: { workingGroup: { select: { name: true } }, _count: { select: { approvals: true, createdExpenses: true } } }, orderBy: [{ role: "asc" }, { username: "asc" }] }) : []; const auditLogs = canManageUsers(viewer.role) ? await prisma.auditLog.findMany({ orderBy: { createdAt: "desc" }, take: 120, include: { actor: { select: { id: true, name: true, username: true, role: true } } } }) : []; const periodCutoffs = await prisma.$queryRaw< { id: string; name: string; date: Date | null; period_id: string }[] >` SELECT id, name, date, period_id FROM period_cutoffs ORDER BY date ASC NULLS LAST, created_at ASC `; const expenseCutoffs = await prisma.$queryRaw< { id: string; cutoff_id: string | null; cutoff_phase: "PRE" | "POST" }[] >`SELECT id, cutoff_id, cutoff_phase FROM expenses WHERE period_id = ${currentPeriod.id}`; const donationRows = await prisma.$queryRaw< { id: string; title: string; description: string | null; amount: unknown; donated_at: Date; period_id: string; expense_id: string | null; created_at: Date; creator_id: string; creator_name: string; working_group_id: string | null; working_group_name: string | null; expense_title: string | null; }[] >` SELECT d.id, d.title, d.description, d.amount, d.donated_at, d.period_id, d.expense_id, d.created_at, u.id AS creator_id, u.username AS creator_name, wg.id AS working_group_id, wg.name AS working_group_name, e.title AS expense_title FROM donations d JOIN users u ON u.id = d.creator_id LEFT JOIN expenses e ON e.id = d.expense_id LEFT JOIN working_groups wg ON wg.id = e.ag_id WHERE d.period_id = ${currentPeriod.id} ORDER BY d.donated_at DESC `; const periodCutoffsByPeriodId = new Map(); for (const cutoff of periodCutoffs) { periodCutoffsByPeriodId.set(cutoff.period_id, [...(periodCutoffsByPeriodId.get(cutoff.period_id) ?? []), cutoff]); } const primaryCutoffByPeriodId = new Map( [...periodCutoffsByPeriodId.entries()].map(([periodId, cutoffs]) => [periodId, cutoffs[0]]) ); const expenseCutoffById = new Map(expenseCutoffs.map((expense) => [expense.id, expense])); const donationsByExpenseId = new Map(); const donationRowsByExpenseId = new Map(); for (const donation of donationRows) { if (donation.expense_id) { donationsByExpenseId.set( donation.expense_id, (donationsByExpenseId.get(donation.expense_id) ?? 0) + Number(donation.amount) ); donationRowsByExpenseId.set(donation.expense_id, [ ...(donationRowsByExpenseId.get(donation.expense_id) ?? []), donation ]); } } const serializedViewer: DashboardViewer = { id: viewer.id, name: viewer.username, username: viewer.username, role: viewer.role, workingGroupId: viewer.workingGroupId, approvalPermissions: normalizeApprovalPermissions(viewer.role, viewer.approvalPermissions, viewer.approvalPreference) }; const serializedGroups: DashboardWorkingGroup[] = workingGroups.map((workingGroup) => ({ id: workingGroup.id, name: workingGroup.name, totalBudget: workingGroup.budgets.reduce((sum, budget) => sum + Number(budget.totalBudget), 0), members: workingGroup.members.map((member) => ({ id: member.id, name: member.username, username: member.username, role: member.role })), budgets: workingGroup.budgets.map((budget) => ({ id: budget.id, name: budget.name, totalBudget: Number(budget.totalBudget), releasedAmount: Number(budget.releasedAmount), colorCode: budget.colorCode, periodId: budget.periodId, expenses: budget.expenses.map((expense) => { const amount = Number(expense.amount); const recurrenceStartAt = expense.recurrence === "MONTHLY" ? (expense.recurrenceStartAt ?? expense.createdAt).toISOString() : null; const occurrences = expense.recurrence === "MONTHLY" && recurrenceStartAt ? buildRecurringOccurrences({ expenseId: expense.id, amount, recurrenceStartAt, periodStartsAt: currentPeriod.startsAt, periodEndsAt: currentPeriod.endsAt }) : []; return { id: expense.id, title: expense.title, description: expense.description, amount, periodAmount: getExpensePeriodAmount(amount, expense.recurrence, occurrences.length), occurrenceCount: expense.recurrence === "MONTHLY" ? occurrences.length : 1, occurrences, budgetId: expense.budgetId, periodId: expense.periodId, approvalStatus: expense.approvalStatus, recurrence: expense.recurrence, recurrenceStartAt, cutoffId: expenseCutoffById.get(expense.id)?.cutoff_id ?? primaryCutoffByPeriodId.get(expense.periodId)?.id ?? null, cutoffPhase: expenseCutoffById.get(expense.id)?.cutoff_phase ?? "PRE", donationAmount: donationsByExpenseId.get(expense.id) ?? 0, donations: (donationRowsByExpenseId.get(expense.id) ?? []).map((donation) => ({ id: donation.id, title: donation.title, description: donation.description, amount: Number(donation.amount), donatedAt: donation.donated_at.toISOString() })), netPeriodAmount: Math.max( 0, getExpensePeriodAmount(amount, expense.recurrence, occurrences.length) - (donationsByExpenseId.get(expense.id) ?? 0) ), paidAt: expense.paidAt?.toISOString() ?? null, documentedAt: expense.documentedAt?.toISOString() ?? null, documents: expense.documents.map((document) => ({ id: document.id, invoiceDate: document.invoiceDate.toISOString(), proofUrl: document.proofUrl, storedFileName: document.storedFileName, originalFileName: document.originalFileName, mimeType: document.mimeType, size: document.size, createdAt: document.createdAt.toISOString(), uploadedBy: { id: document.uploadedBy.id, name: document.uploadedBy.username } })), createdAt: expense.createdAt.toISOString(), creator: { id: expense.creator.id, name: expense.creator.username }, approvals: expense.approvals.map((approval) => ({ id: approval.id, approvalType: approval.approvalType, timestamp: approval.timestamp.toISOString(), user: { id: approval.user.id, name: approval.user.username } })) }; }) })) })); const serializedUsers: DashboardManagedUser[] = managedUsers.map((user) => ({ id: user.id, name: user.username, username: user.username, role: user.role, workingGroupId: user.workingGroupId, workingGroupName: user.workingGroup?.name ?? null, approvalPermissions: normalizeApprovalPermissions(user.role, user.approvalPermissions, user.approvalPreference), createdExpensesCount: user._count.createdExpenses, approvalsCount: user._count.approvals })); const serializedPeriods: DashboardAccountingPeriod[] = accountingPeriods.map((period) => ({ cutoffs: (periodCutoffsByPeriodId.get(period.id) ?? []).map((cutoff) => ({ id: cutoff.id, name: cutoff.name, date: cutoff.date?.toISOString() ?? null, periodId: cutoff.period_id })), id: period.id, name: period.name, startsAt: period.startsAt.toISOString(), endsAt: period.endsAt.toISOString(), isCurrent: period.isCurrent, cutoffName: primaryCutoffByPeriodId.get(period.id)?.name ?? "Open Air", cutoffDate: primaryCutoffByPeriodId.get(period.id)?.date?.toISOString() ?? null })); const serializedDonations: DashboardDonation[] = donationRows.map((donation) => ({ id: donation.id, title: donation.title, description: donation.description, amount: Number(donation.amount), donatedAt: donation.donated_at.toISOString(), periodId: donation.period_id, expenseId: donation.expense_id, workingGroupId: donation.working_group_id, workingGroupName: donation.working_group_name, expenseTitle: donation.expense_title, createdAt: donation.created_at.toISOString(), creator: { id: donation.creator_id, name: donation.creator_name } })); const serializedSettings: DashboardSettings = serializeAppSettings(appSettings); const serializedAuditLogs: DashboardAuditLog[] = auditLogs.map((entry) => ({ id: entry.id, action: entry.action, entityType: entry.entityType, entityId: entry.entityId, entityLabel: entry.entityLabel, summary: entry.summary, canRestore: Boolean(getRollbackMetadata(entry.metadata)), createdAt: entry.createdAt.toISOString(), actor: entry.actor ? { id: entry.actor.id, name: entry.actor.username, username: entry.actor.username, role: entry.actor.role } : null })); return ( ); }