UI Push Deep Links und Drive Diagnose verbessern
CI / Build and Deploy (push) Successful in 2m30s

This commit is contained in:
jan
2026-05-06 00:11:33 +02:00
parent f87a82e02f
commit 6dec4b8a10
23 changed files with 491 additions and 81 deletions
+6 -6
View File
@@ -21,7 +21,7 @@ const updateBudgetSchema = z
if (value.releasedAmount !== undefined && value.releasedAmount > value.totalBudget) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Der bereits an die AG uebergebene Betrag darf das Budget nicht uebersteigen.",
message: "Der bereits an die AG übergebene Betrag darf das Budget nicht übersteigen.",
path: ["releasedAmount"]
});
}
@@ -42,7 +42,7 @@ export async function PATCH(request: Request, { params }: Context) {
}
if (!canManageBudgets(viewer.role)) {
return NextResponse.json({ error: "Nur Vorstand allgemein, AG Orga oder AG Finanzen duerfen Budgets aendern." }, { status: 403 });
return NextResponse.json({ error: "Nur Vorstand allgemein, AG Orga oder AG Finanzen dürfen Budgets ändern." }, { status: 403 });
}
const budget = await prisma.budget.findUnique({
@@ -65,7 +65,7 @@ export async function PATCH(request: Request, { params }: Context) {
const parsed = updateBudgetSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json({ error: "Budgetname, Betrag, Mitteluebergabe oder Farbe sind ungueltig." }, { status: 400 });
return NextResponse.json({ error: "Budgetname, Betrag, Mittelübergabe oder Farbe sind ungültig." }, { status: 400 });
}
try {
@@ -140,7 +140,7 @@ export async function DELETE(_: Request, { params }: Context) {
}
if (!canManageBudgets(viewer.role)) {
return NextResponse.json({ error: "Nur Vorstand allgemein, AG Orga oder AG Finanzen duerfen Budgets loeschen." }, { status: 403 });
return NextResponse.json({ error: "Nur Vorstand allgemein, AG Orga oder AG Finanzen dürfen Budgets löschen." }, { status: 403 });
}
const budget = await prisma.budget.findUnique({
@@ -160,7 +160,7 @@ export async function DELETE(_: Request, { params }: Context) {
if (budget._count.expenses > 0) {
return NextResponse.json(
{ error: "Dieses Budget enthaelt noch Ausgaben. Bitte loesche oder verschiebe erst die Posten." },
{ error: "Dieses Budget enthält noch Ausgaben. Bitte lösche oder verschiebe erst die Posten." },
{ status: 400 }
);
}
@@ -175,7 +175,7 @@ export async function DELETE(_: Request, { params }: Context) {
entityType: "budget",
entityId: budget.id,
entityLabel: budget.name,
summary: `Budget ${budget.name} wurde geloescht.`,
summary: `Budget ${budget.name} wurde gelöscht.`,
metadata: {
rollback: {
kind: "budget.delete",
+3 -3
View File
@@ -20,7 +20,7 @@ const budgetSchema = z
if (value.releasedAmount !== undefined && value.releasedAmount > value.totalBudget) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Der bereits an die AG uebergebene Betrag darf das Budget nicht uebersteigen.",
message: "Der bereits an die AG übergebene Betrag darf das Budget nicht übersteigen.",
path: ["releasedAmount"]
});
}
@@ -34,7 +34,7 @@ export async function POST(request: Request) {
}
if (!canManageBudgets(viewer.role)) {
return NextResponse.json({ error: "Nur Vorstand allgemein, AG Orga oder AG Finanzen duerfen Budgets verwalten." }, { status: 403 });
return NextResponse.json({ error: "Nur Vorstand allgemein, AG Orga oder AG Finanzen dürfen Budgets verwalten." }, { status: 403 });
}
const body = await request.json().catch(() => null);
@@ -42,7 +42,7 @@ export async function POST(request: Request) {
if (!parsed.success) {
return NextResponse.json(
{ error: "Bitte AG, Budgetname, Betrag, Mitteluebergabe und Farbe korrekt angeben." },
{ error: "Bitte AG, Budgetname, Betrag, Mittelübergabe und Farbe korrekt angeben." },
{ status: 400 }
);
}
+1 -1
View File
@@ -57,7 +57,7 @@ export async function POST(request: Request, { params }: Context) {
const parsed = approvalSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json({ error: "Freigabetyp ungueltig." }, { status: 400 });
return NextResponse.json({ error: "Freigabetyp ungültig." }, { status: 400 });
}
const existingApprovals = expense.approvals.map((approval) => approval.approvalType);
@@ -20,7 +20,7 @@ export async function POST(_: Request, { params }: Context) {
}
if (!canDocumentExpense(viewer.role)) {
return NextResponse.json({ error: "Nur Vorstand allgemein, AG Orga oder AG Finanzen duerfen dokumentieren." }, { status: 403 });
return NextResponse.json({ error: "Nur Vorstand allgemein, AG Orga oder AG Finanzen dürfen dokumentieren." }, { status: 403 });
}
const expense = await prisma.expense.findUnique({
+1 -1
View File
@@ -20,7 +20,7 @@ export async function POST(_: Request, { params }: Context) {
}
if (!canMarkPaid(viewer.role)) {
return NextResponse.json({ error: "Nur Vorstand allgemein, AG Orga oder AG Finanzen duerfen Bezahlt setzen." }, { status: 403 });
return NextResponse.json({ error: "Nur Vorstand allgemein, AG Orga oder AG Finanzen dürfen Bezahlt setzen." }, { status: 403 });
}
const expense = await prisma.expense.findUnique({
+14 -7
View File
@@ -2,7 +2,7 @@ import { NextResponse } from "next/server";
import { createAuditLog } from "@/lib/audit-log";
import { canDocumentExpense } from "@/lib/domain";
import { uploadExpenseProofToDrive } from "@/lib/google-drive";
import { serializeDriveError, uploadExpenseProofToDrive } from "@/lib/google-drive";
import prisma from "@/lib/prisma";
import { getCurrentViewer } from "@/lib/session";
@@ -48,11 +48,11 @@ export async function POST(request: Request, { params }: Context) {
}
if (expense.creatorId !== viewer.id && !canDocumentExpense(viewer.role)) {
return NextResponse.json({ error: "Du darfst fuer diese Ausgabe keinen Beleg hochladen." }, { status: 403 });
return NextResponse.json({ error: "Du darfst für diese Ausgabe keinen Beleg hochladen." }, { status: 403 });
}
if (expense.approvalStatus !== "APPROVED") {
return NextResponse.json({ error: "Belegabgabe ist erst nach Freigabe moeglich." }, { status: 400 });
return NextResponse.json({ error: "Belegabgabe ist erst nach Freigabe möglich." }, { status: 400 });
}
const formData = await request.formData().catch(() => null);
@@ -60,11 +60,11 @@ export async function POST(request: Request, { params }: Context) {
const invoiceDate = parseInvoiceDate(formData?.get("invoiceDate") ?? null);
if (!invoiceDate) {
return NextResponse.json({ error: "Bitte ein gueltiges Rechnungsdatum angeben." }, { status: 400 });
return NextResponse.json({ error: "Bitte ein gültiges Rechnungsdatum angeben." }, { status: 400 });
}
if (!(file instanceof File)) {
return NextResponse.json({ error: "Bitte einen Beleg als Bild oder PDF auswaehlen." }, { status: 400 });
return NextResponse.json({ error: "Bitte einen Beleg als Bild oder PDF auswählen." }, { status: 400 });
}
if (!ACCEPTED_MIME_TYPES.has(file.type)) {
@@ -72,7 +72,7 @@ export async function POST(request: Request, { params }: Context) {
}
if (file.size > MAX_FILE_SIZE) {
return NextResponse.json({ error: "Der Beleg darf maximal 12 MB gross sein." }, { status: 400 });
return NextResponse.json({ error: "Der Beleg darf maximal 12 MB groß sein." }, { status: 400 });
}
const uploadedFile = await uploadExpenseProofToDrive({
@@ -82,8 +82,15 @@ export async function POST(request: Request, { params }: Context) {
fileName: file.name,
mimeType: file.type,
buffer: Buffer.from(await file.arrayBuffer())
}).catch((error) => {
const serialized = serializeDriveError(error);
return NextResponse.json(serialized, { status: serialized.status ?? 500 });
});
if (uploadedFile instanceof NextResponse) {
return uploadedFile;
}
const now = new Date();
const transactionResult = await prisma.$transaction(async (tx) => {
const document = await tx.expenseDocument.create({
@@ -125,7 +132,7 @@ export async function POST(request: Request, { params }: Context) {
entityType: "expense",
entityId: transactionResult.updatedExpense.id,
entityLabel: transactionResult.updatedExpense.title,
summary: `Rechnung fuer ${transactionResult.updatedExpense.title} wurde abgegeben.`,
summary: `Rechnung für ${transactionResult.updatedExpense.title} wurde abgegeben.`,
metadata: {
documentId: transactionResult.document.id,
proofUrl: transactionResult.document.proofUrl,
+1 -1
View File
@@ -55,7 +55,7 @@ export async function DELETE(_: Request, { params }: Context) {
entityType: "expense",
entityId: expense.id,
entityLabel: expense.title,
summary: `Ausgabe ${expense.title} wurde geloescht.`,
summary: `Ausgabe ${expense.title} wurde gelöscht.`,
metadata: {
rollback: {
kind: "expense.delete",
+2 -1
View File
@@ -120,7 +120,8 @@ export async function POST(request: Request) {
{
id: expense.id,
title: expense.title,
amount: Number(expense.amount)
amount: Number(expense.amount),
workingGroupId: expense.agId
},
requiredApprovalTypes
);
+1 -1
View File
@@ -74,7 +74,7 @@ export async function GET() {
}
if (!canManageUsers(viewer.role)) {
return NextResponse.json({ error: "Nur Vorstand allgemein, AG Orga oder AG Finanzen duerfen CSV-Backups herunterladen." }, { status: 403 });
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([
+1 -1
View File
@@ -73,7 +73,7 @@ export async function POST(request: Request) {
}
if (!canManageUsers(viewer.role)) {
return NextResponse.json({ error: "Nur Vorstand allgemein, AG Orga oder AG Finanzen duerfen Backups einspielen." }, { status: 403 });
return NextResponse.json({ error: "Nur Vorstand allgemein, AG Orga oder AG Finanzen dürfen Backups einspielen." }, { status: 403 });
}
const formData = await request.formData().catch(() => null);
+2 -2
View File
@@ -23,7 +23,7 @@ export async function POST(request: Request) {
const parsed = subscriptionSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json({ error: "Push-Subscription ist ungueltig." }, { status: 400 });
return NextResponse.json({ error: "Push-Subscription ist ungültig." }, { status: 400 });
}
await prisma.pushSubscription.upsert({
@@ -57,7 +57,7 @@ export async function DELETE(request: Request) {
const parsed = z.object({ endpoint: z.string().url() }).safeParse(body);
if (!parsed.success) {
return NextResponse.json({ error: "Push-Subscription ist ungueltig." }, { status: 400 });
return NextResponse.json({ error: "Push-Subscription ist ungültig." }, { status: 400 });
}
await prisma.pushSubscription.deleteMany({
@@ -0,0 +1,26 @@
import { NextResponse } from "next/server";
import { canManageSettings } from "@/lib/domain";
import { runDriveDiagnostics, serializeDriveError } from "@/lib/google-drive";
import { getCurrentViewer } from "@/lib/session";
export async function POST() {
const viewer = await getCurrentViewer();
if (!viewer) {
return NextResponse.json({ error: "Nicht angemeldet." }, { status: 401 });
}
if (!canManageSettings(viewer.role)) {
return NextResponse.json({ error: "Nur AG Orga darf die Drive-Verbindung testen." }, { status: 403 });
}
try {
const result = await runDriveDiagnostics();
return NextResponse.json(result);
} catch (error) {
const serialized = serializeDriveError(error);
return NextResponse.json(serialized, { status: serialized.status ?? 500 });
}
}
+2 -2
View File
@@ -22,14 +22,14 @@ export async function PATCH(request: Request) {
}
if (!canManageUsers(viewer.role)) {
return NextResponse.json({ error: "Nur Vorstand allgemein, AG Orga oder AG Finanzen duerfen Einstellungen aendern." }, { status: 403 });
return NextResponse.json({ error: "Nur Vorstand allgemein, AG Orga oder AG Finanzen dürfen Einstellungen ändern." }, { status: 403 });
}
const body = await request.json().catch(() => null);
const parsed = settingsSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json({ error: "Bitte gueltige Einstellungen eingeben." }, { status: 400 });
return NextResponse.json({ error: "Bitte gültige Einstellungen eingeben." }, { status: 400 });
}
const changesOrgaSettings =
+2 -2
View File
@@ -56,7 +56,7 @@ export async function PATCH(request: Request, { params }: Context) {
}
if (!canManageUsers(viewer.role)) {
return NextResponse.json({ error: "Nur Vorstand allgemein, AG Orga oder AG Finanzen duerfen Nutzer bearbeiten." }, { status: 403 });
return NextResponse.json({ error: "Nur Vorstand allgemein, AG Orga oder AG Finanzen dürfen Nutzer bearbeiten." }, { status: 403 });
}
const body = await request.json().catch(() => null);
@@ -215,7 +215,7 @@ export async function DELETE(_: Request, { params }: Context) {
entityType: "user",
entityId: user.id,
entityLabel: user.username,
summary: `Nutzer ${user.username} wurde geloescht.`,
summary: `Nutzer ${user.username} wurde gelöscht.`,
metadata: {
rollback: {
kind: "user.delete",
+1 -1
View File
@@ -52,7 +52,7 @@ export async function POST(request: Request) {
}
if (!canManageUsers(viewer.role)) {
return NextResponse.json({ error: "Nur Vorstand allgemein, AG Orga oder AG Finanzen duerfen Nutzer anlegen." }, { status: 403 });
return NextResponse.json({ error: "Nur Vorstand allgemein, AG Orga oder AG Finanzen dürfen Nutzer anlegen." }, { status: 403 });
}
const body = await request.json().catch(() => null);
+2 -2
View File
@@ -33,7 +33,7 @@ export async function PATCH(request: Request, { params }: Context) {
const parsed = workingGroupSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json({ error: "Bitte einen gueltigen AG-Namen angeben." }, { status: 400 });
return NextResponse.json({ error: "Bitte einen gültigen AG-Namen angeben." }, { status: 400 });
}
const workingGroup = await prisma.workingGroup.findUnique({
@@ -137,7 +137,7 @@ export async function DELETE(_: Request, { params }: Context) {
entityType: "workingGroup",
entityId: workingGroup.id,
entityLabel: workingGroup.name,
summary: `AG ${workingGroup.name} wurde geloescht.`,
summary: `AG ${workingGroup.name} wurde gelöscht.`,
metadata: {
rollback: {
kind: "workingGroup.delete",
+1 -1
View File
@@ -26,7 +26,7 @@ export async function POST(request: Request) {
const parsed = workingGroupSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json({ error: "Bitte einen gueltigen AG-Namen angeben." }, { status: 400 });
return NextResponse.json({ error: "Bitte einen gültigen AG-Namen angeben." }, { status: 400 });
}
const existingWorkingGroup = await prisma.workingGroup.findFirst({