Initial commit
This commit is contained in:
518
src/app/api/export/csv/route.ts
Normal file
518
src/app/api/export/csv/route.ts
Normal file
@@ -0,0 +1,518 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
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",
|
||||
"title",
|
||||
"description",
|
||||
"amount",
|
||||
"totalBudget",
|
||||
"colorCode",
|
||||
"approvalStatus",
|
||||
"approvalType",
|
||||
"recurrence",
|
||||
"proofUrl",
|
||||
"createdAt",
|
||||
"paidAt",
|
||||
"documentedAt",
|
||||
"memberUsernames",
|
||||
"creatorName",
|
||||
"creatorUsername",
|
||||
"approverName",
|
||||
"approverUsername",
|
||||
"auditActorId",
|
||||
"auditAction",
|
||||
"auditEntityType",
|
||||
"auditEntityId",
|
||||
"auditEntityLabel",
|
||||
"auditSummary",
|
||||
"auditMetadata"
|
||||
] as const;
|
||||
|
||||
type CsvRow = 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 oder Finanz-AG duerfen CSV-Backups herunterladen." }, { status: 403 });
|
||||
}
|
||||
|
||||
const [users, accountingPeriods, workingGroups, auditLogs] = await Promise.all([
|
||||
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
|
||||
},
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
prisma.auditLog.findMany({
|
||||
orderBy: {
|
||||
createdAt: "asc"
|
||||
}
|
||||
})
|
||||
]);
|
||||
|
||||
const rows: 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 ?? "",
|
||||
title: "",
|
||||
description: "",
|
||||
amount: "",
|
||||
totalBudget: "",
|
||||
colorCode: "",
|
||||
approvalStatus: "",
|
||||
approvalType: "",
|
||||
recurrence: "",
|
||||
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: "",
|
||||
title: "",
|
||||
description: "",
|
||||
amount: "",
|
||||
totalBudget: "",
|
||||
colorCode: "",
|
||||
approvalStatus: "",
|
||||
approvalType: "",
|
||||
recurrence: "",
|
||||
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: "",
|
||||
title: "",
|
||||
description: "",
|
||||
amount: "",
|
||||
totalBudget: group.budgets.reduce((sum, budget) => sum + Number(budget.totalBudget), 0).toFixed(2),
|
||||
colorCode: "",
|
||||
approvalStatus: "",
|
||||
approvalType: "",
|
||||
recurrence: "",
|
||||
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: "",
|
||||
title: "",
|
||||
description: "",
|
||||
amount: "",
|
||||
totalBudget: Number(budget.totalBudget).toFixed(2),
|
||||
colorCode: budget.colorCode,
|
||||
approvalStatus: "",
|
||||
approvalType: "",
|
||||
recurrence: "",
|
||||
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: "",
|
||||
title: expense.title,
|
||||
description: expense.description ?? "",
|
||||
amount: Number(expense.amount).toFixed(2),
|
||||
totalBudget: "",
|
||||
colorCode: "",
|
||||
approvalStatus: expense.approvalStatus,
|
||||
approvalType: "",
|
||||
recurrence: expense.recurrence,
|
||||
proofUrl: expense.proofUrl ?? "",
|
||||
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 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: "",
|
||||
title: expense.title,
|
||||
description: "",
|
||||
amount: Number(expense.amount).toFixed(2),
|
||||
totalBudget: "",
|
||||
colorCode: "",
|
||||
approvalStatus: expense.approvalStatus,
|
||||
approvalType: approval.approvalType,
|
||||
recurrence: expense.recurrence,
|
||||
proofUrl: "",
|
||||
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: "",
|
||||
title: "",
|
||||
description: "",
|
||||
amount: "",
|
||||
totalBudget: "",
|
||||
colorCode: "",
|
||||
approvalStatus: "",
|
||||
approvalType: "",
|
||||
recurrence: "",
|
||||
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"
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user