657 lines
18 KiB
TypeScript
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"
|
|
}
|
|
});
|
|
}
|