import { NextResponse } from "next/server"; import { getAppSettings, toApprovalThresholdNumber } from "@/lib/app-settings"; import { toCsvCell } from "@/lib/backup-csv"; import { canManageUsers } from "@/lib/domain"; import prisma from "@/lib/prisma"; import { getCurrentViewer } from "@/lib/session"; const CSV_HEADERS = [ "recordType", "id", "parentId", "parentType", "workingGroupId", "workingGroupName", "periodId", "periodName", "periodStartsAt", "periodEndsAt", "periodIsCurrent", "budgetId", "budgetName", "userId", "userName", "username", "passwordHash", "email", "role", "approvalPreference", "approvalPermissions", "approvalThreshold", "requiredApprovalTypes", "budgetReleaseNotifyTarget", "title", "description", "amount", "totalBudget", "releasedAmount", "colorCode", "approvalStatus", "approvalType", "recurrence", "recurrenceStartAt", "invoiceDate", "proofUrl", "storedFileName", "originalFileName", "mimeType", "fileSize", "createdAt", "paidAt", "documentedAt", "memberUsernames", "creatorName", "creatorUsername", "approverName", "approverUsername", "auditActorId", "auditAction", "auditEntityType", "auditEntityId", "auditEntityLabel", "auditSummary", "auditMetadata" ] as const; type CsvRow = Partial>; export async function GET() { const viewer = await getCurrentViewer(); if (!viewer) { return NextResponse.json({ error: "Nicht angemeldet." }, { status: 401 }); } if (!canManageUsers(viewer.role)) { return NextResponse.json({ error: "Nur Vorstand allgemein, AG Orga oder AG Finanzen dürfen CSV-Backups herunterladen." }, { status: 403 }); } const [appSettings, users, accountingPeriods, workingGroups, auditLogs] = await Promise.all([ getAppSettings(), prisma.user.findMany({ include: { workingGroup: { select: { name: true } } }, orderBy: [ { role: "asc" }, { username: "asc" } ] }), prisma.accountingPeriod.findMany({ orderBy: { startsAt: "asc" } }), prisma.workingGroup.findMany({ orderBy: { name: "asc" }, include: { members: { select: { id: true, name: true, username: true, email: true, role: true, approvalPreference: true, approvalPermissions: true }, orderBy: { username: "asc" } }, budgets: { orderBy: { name: "asc" }, include: { period: { select: { id: true, name: true, startsAt: true, endsAt: true, isCurrent: true } }, expenses: { orderBy: { createdAt: "asc" }, include: { creator: { select: { id: true, name: true, username: true } }, approvals: { orderBy: { timestamp: "asc" }, include: { user: { select: { id: true, name: true, username: true } } } }, documents: { orderBy: { createdAt: "asc" }, include: { uploadedBy: { select: { id: true, name: true, username: true } } } } } } } } } }), prisma.auditLog.findMany({ orderBy: { createdAt: "asc" } }) ]); const rows: CsvRow[] = []; rows.push({ recordType: "settings", id: appSettings.id, approvalThreshold: toApprovalThresholdNumber(appSettings.approvalThreshold).toFixed(2), requiredApprovalTypes: appSettings.requiredApprovalTypes.join("|"), budgetReleaseNotifyTarget: appSettings.budgetReleaseNotifyTarget, createdAt: appSettings.createdAt.toISOString() } as CsvRow); for (const user of users) { rows.push({ recordType: "user", id: user.id, parentId: user.workingGroupId, parentType: user.workingGroupId ? "workingGroup" : "", workingGroupId: user.workingGroupId, workingGroupName: user.workingGroup?.name ?? "", periodId: "", periodName: "", periodStartsAt: "", periodEndsAt: "", periodIsCurrent: "", budgetId: "", budgetName: "", userId: user.id, userName: user.name, username: user.username, passwordHash: user.passwordHash, email: user.email, role: user.role, approvalPreference: user.approvalPreference ?? "", approvalPermissions: user.approvalPermissions.join("|"), approvalThreshold: "", title: "", description: "", amount: "", totalBudget: "", releasedAmount: "", colorCode: "", approvalStatus: "", approvalType: "", recurrence: "", recurrenceStartAt: "", invoiceDate: "", proofUrl: "", createdAt: user.createdAt.toISOString(), paidAt: "", documentedAt: "", memberUsernames: "", creatorName: "", creatorUsername: "", approverName: "", approverUsername: "", auditActorId: "", auditAction: "", auditEntityType: "", auditEntityId: "", auditEntityLabel: "", auditSummary: "", auditMetadata: "" }); } for (const period of accountingPeriods) { rows.push({ recordType: "period", id: period.id, parentId: "", parentType: "", workingGroupId: "", workingGroupName: "", periodId: period.id, periodName: period.name, periodStartsAt: period.startsAt.toISOString(), periodEndsAt: period.endsAt.toISOString(), periodIsCurrent: period.isCurrent ? "true" : "false", budgetId: "", budgetName: "", userId: "", userName: "", username: "", passwordHash: "", email: "", role: "", approvalPreference: "", approvalPermissions: "", approvalThreshold: "", title: "", description: "", amount: "", totalBudget: "", releasedAmount: "", colorCode: "", approvalStatus: "", approvalType: "", recurrence: "", recurrenceStartAt: "", invoiceDate: "", proofUrl: "", createdAt: period.createdAt.toISOString(), paidAt: "", documentedAt: "", memberUsernames: "", creatorName: "", creatorUsername: "", approverName: "", approverUsername: "", auditActorId: "", auditAction: "", auditEntityType: "", auditEntityId: "", auditEntityLabel: "", auditSummary: "", auditMetadata: "" }); } for (const group of workingGroups) { rows.push({ recordType: "workingGroup", id: group.id, parentId: "", parentType: "", workingGroupId: group.id, workingGroupName: group.name, periodId: "", periodName: "", periodStartsAt: "", periodEndsAt: "", periodIsCurrent: "", budgetId: "", budgetName: "", userId: "", userName: "", username: "", passwordHash: "", email: "", role: "", approvalPreference: "", approvalPermissions: "", approvalThreshold: "", title: "", description: "", amount: "", totalBudget: group.budgets.reduce((sum, budget) => sum + Number(budget.totalBudget), 0).toFixed(2), releasedAmount: group.budgets.reduce((sum, budget) => sum + Number(budget.releasedAmount), 0).toFixed(2), colorCode: "", approvalStatus: "", approvalType: "", recurrence: "", recurrenceStartAt: "", invoiceDate: "", proofUrl: "", createdAt: group.createdAt.toISOString(), paidAt: "", documentedAt: "", memberUsernames: group.members.map((member) => member.username).join(" | "), creatorName: "", creatorUsername: "", approverName: "", approverUsername: "", auditActorId: "", auditAction: "", auditEntityType: "", auditEntityId: "", auditEntityLabel: "", auditSummary: "", auditMetadata: "" }); for (const budget of group.budgets) { rows.push({ recordType: "budget", id: budget.id, parentId: group.id, parentType: "workingGroup", workingGroupId: group.id, workingGroupName: group.name, periodId: budget.period.id, periodName: budget.period.name, periodStartsAt: budget.period.startsAt.toISOString(), periodEndsAt: budget.period.endsAt.toISOString(), periodIsCurrent: budget.period.isCurrent ? "true" : "false", budgetId: budget.id, budgetName: budget.name, userId: "", userName: "", username: "", passwordHash: "", email: "", role: "", approvalPreference: "", approvalPermissions: "", approvalThreshold: "", title: "", description: "", amount: "", totalBudget: Number(budget.totalBudget).toFixed(2), releasedAmount: Number(budget.releasedAmount).toFixed(2), colorCode: budget.colorCode, approvalStatus: "", approvalType: "", recurrence: "", recurrenceStartAt: "", invoiceDate: "", proofUrl: "", createdAt: budget.createdAt.toISOString(), paidAt: "", documentedAt: "", memberUsernames: "", creatorName: "", creatorUsername: "", approverName: "", approverUsername: "", auditActorId: "", auditAction: "", auditEntityType: "", auditEntityId: "", auditEntityLabel: "", auditSummary: "", auditMetadata: "" }); for (const expense of budget.expenses) { rows.push({ recordType: "expense", id: expense.id, parentId: budget.id, parentType: "budget", workingGroupId: group.id, workingGroupName: group.name, periodId: budget.period.id, periodName: budget.period.name, periodStartsAt: budget.period.startsAt.toISOString(), periodEndsAt: budget.period.endsAt.toISOString(), periodIsCurrent: budget.period.isCurrent ? "true" : "false", budgetId: budget.id, budgetName: budget.name, userId: expense.creator.id, userName: expense.creator.name, username: expense.creator.username, passwordHash: "", email: "", role: "", approvalPreference: "", approvalPermissions: "", approvalThreshold: "", title: expense.title, description: expense.description ?? "", amount: Number(expense.amount).toFixed(2), totalBudget: "", releasedAmount: "", colorCode: "", approvalStatus: expense.approvalStatus, approvalType: "", recurrence: expense.recurrence, recurrenceStartAt: expense.recurrenceStartAt?.toISOString() ?? "", invoiceDate: "", proofUrl: "", storedFileName: "", originalFileName: "", mimeType: "", fileSize: "", createdAt: expense.createdAt.toISOString(), paidAt: expense.paidAt?.toISOString() ?? "", documentedAt: expense.documentedAt?.toISOString() ?? "", memberUsernames: "", creatorName: expense.creator.name, creatorUsername: expense.creator.username, approverName: "", approverUsername: "", auditActorId: "", auditAction: "", auditEntityType: "", auditEntityId: "", auditEntityLabel: "", auditSummary: "", auditMetadata: "" }); for (const document of expense.documents) { rows.push({ recordType: "expenseDocument", id: document.id, parentId: expense.id, parentType: "expense", workingGroupId: group.id, workingGroupName: group.name, periodId: budget.period.id, periodName: budget.period.name, periodStartsAt: budget.period.startsAt.toISOString(), periodEndsAt: budget.period.endsAt.toISOString(), periodIsCurrent: budget.period.isCurrent ? "true" : "false", budgetId: budget.id, budgetName: budget.name, userId: document.uploadedBy.id, userName: document.uploadedBy.name, username: document.uploadedBy.username, passwordHash: "", email: "", role: "", approvalPreference: "", approvalPermissions: "", approvalThreshold: "", title: expense.title, description: "", amount: Number(expense.amount).toFixed(2), totalBudget: "", releasedAmount: "", colorCode: "", approvalStatus: expense.approvalStatus, approvalType: "", recurrence: expense.recurrence, recurrenceStartAt: expense.recurrenceStartAt?.toISOString() ?? "", invoiceDate: document.invoiceDate.toISOString(), proofUrl: document.proofUrl, storedFileName: document.storedFileName, originalFileName: document.originalFileName, mimeType: document.mimeType, fileSize: document.size, createdAt: document.createdAt.toISOString(), paidAt: "", documentedAt: "", memberUsernames: "", creatorName: expense.creator.name, creatorUsername: expense.creator.username, approverName: "", approverUsername: "", auditActorId: "", auditAction: "", auditEntityType: "", auditEntityId: "", auditEntityLabel: "", auditSummary: "", auditMetadata: "" }); } for (const approval of expense.approvals) { rows.push({ recordType: "approval", id: approval.id, parentId: expense.id, parentType: "expense", workingGroupId: group.id, workingGroupName: group.name, periodId: budget.period.id, periodName: budget.period.name, periodStartsAt: budget.period.startsAt.toISOString(), periodEndsAt: budget.period.endsAt.toISOString(), periodIsCurrent: budget.period.isCurrent ? "true" : "false", budgetId: budget.id, budgetName: budget.name, userId: approval.user.id, userName: approval.user.name, username: approval.user.username, passwordHash: "", email: "", role: "", approvalPreference: "", approvalPermissions: "", approvalThreshold: "", title: expense.title, description: "", amount: Number(expense.amount).toFixed(2), totalBudget: "", releasedAmount: "", colorCode: "", approvalStatus: expense.approvalStatus, approvalType: approval.approvalType, recurrence: expense.recurrence, recurrenceStartAt: expense.recurrenceStartAt?.toISOString() ?? "", invoiceDate: "", proofUrl: "", storedFileName: "", originalFileName: "", mimeType: "", fileSize: "", createdAt: approval.timestamp.toISOString(), paidAt: "", documentedAt: "", memberUsernames: "", creatorName: expense.creator.name, creatorUsername: expense.creator.username, approverName: approval.user.name, approverUsername: approval.user.username, auditActorId: "", auditAction: "", auditEntityType: "", auditEntityId: "", auditEntityLabel: "", auditSummary: "", auditMetadata: "" }); } } } } for (const auditLog of auditLogs) { rows.push({ recordType: "auditLog", id: auditLog.id, parentId: "", parentType: "", workingGroupId: "", workingGroupName: "", periodId: "", periodName: "", periodStartsAt: "", periodEndsAt: "", periodIsCurrent: "", budgetId: "", budgetName: "", userId: "", userName: "", username: "", passwordHash: "", email: "", role: "", approvalPreference: "", approvalPermissions: "", approvalThreshold: "", title: "", description: "", amount: "", totalBudget: "", releasedAmount: "", colorCode: "", approvalStatus: "", approvalType: "", recurrence: "", recurrenceStartAt: "", invoiceDate: "", proofUrl: "", createdAt: auditLog.createdAt.toISOString(), paidAt: "", documentedAt: "", memberUsernames: "", creatorName: "", creatorUsername: "", approverName: "", approverUsername: "", auditActorId: auditLog.actorId ?? "", auditAction: auditLog.action, auditEntityType: auditLog.entityType, auditEntityId: auditLog.entityId ?? "", auditEntityLabel: auditLog.entityLabel ?? "", auditSummary: auditLog.summary, auditMetadata: auditLog.metadata ? JSON.stringify(auditLog.metadata) : "" }); } const csvLines = [ CSV_HEADERS.join(","), ...rows.map((row) => CSV_HEADERS.map((header) => toCsvCell(row[header])).join(",")) ]; const timestamp = new Date().toISOString().slice(0, 10); const csv = `\uFEFF${csvLines.join("\n")}`; return new NextResponse(csv, { headers: { "Content-Type": "text/csv; charset=utf-8", "Content-Disposition": `attachment; filename="rfp-finanzuebersicht-backup-${timestamp}.csv"`, "Cache-Control": "no-store" } }); }