Files
RFP_Finanzen/src/app/api/export/csv/route.ts
jan 6dec4b8a10
All checks were successful
CI / Build and Deploy (push) Successful in 2m30s
UI Push Deep Links und Drive Diagnose verbessern
2026-05-06 00:11:33 +02:00

657 lines
18 KiB
TypeScript

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<Record<(typeof CSV_HEADERS)[number], string | number | null | undefined>>;
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"
}
});
}