Rechnungsdatum und Belegupload ueberarbeiten
CI / Build (push) Successful in 1m59s
CI / Deploy (push) Successful in 2m2s

This commit is contained in:
jan
2026-05-01 16:51:27 +02:00
parent 549c8f16c6
commit 796e134ea2
14 changed files with 165 additions and 95 deletions
@@ -516,6 +516,7 @@ export async function POST(_: Request, { params }: Context) {
approvalStatus: asString(deleted.approvalStatus, "Freigabestatus") as "PENDING" | "APPROVED",
recurrence: asString(deleted.recurrence, "Wiederholung") as "NONE" | "MONTHLY",
recurrenceStartAt: asDate(deleted.recurrenceStartAt, "Abo-Startdatum"),
invoiceDate: asDate(deleted.invoiceDate, "Rechnungsdatum"),
proofUrl: asNullableString(deleted.proofUrl),
createdAt: asDate(deleted.createdAt, "Ausgabe erstellt am") ?? new Date(),
paidAt: asDate(deleted.paidAt, "Bezahlt am"),
@@ -574,6 +575,7 @@ export async function POST(_: Request, { params }: Context) {
},
data: {
proofUrl: asNullableString(rollback.previousProofUrl),
invoiceDate: asDate(rollback.previousInvoiceDate, "Vorheriges Rechnungsdatum"),
documentedAt: asDate(rollback.previousDocumentedAt, "Vorheriger Dokumentationszeitpunkt")
}
});
+4
View File
@@ -35,6 +35,10 @@ export async function POST(_: Request, { params }: Context) {
return NextResponse.json({ error: "Bezahlt ist erst nach Freigabe moeglich." }, { status: 400 });
}
if (!expense.proofUrl || !expense.invoiceDate || !expense.documentedAt) {
return NextResponse.json({ error: "Bitte zuerst Rechnung mit Rechnungsdatum abgeben." }, { status: 400 });
}
const updatedExpense = await prisma.expense.update({
where: { id },
data: {
+46 -1
View File
@@ -1,5 +1,6 @@
import { NextResponse } from "next/server";
import { createAuditLog } from "@/lib/audit-log";
import { canDocumentExpense } from "@/lib/domain";
import { uploadExpenseProofToDrive } from "@/lib/google-drive";
import prisma from "@/lib/prisma";
@@ -8,6 +9,15 @@ import { getCurrentViewer } from "@/lib/session";
const ACCEPTED_MIME_TYPES = new Set(["application/pdf", "image/jpeg", "image/png", "image/webp", "image/heic", "image/heif"]);
const MAX_FILE_SIZE = 12 * 1024 * 1024;
function parseInvoiceDate(value: FormDataEntryValue | null) {
if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(value)) {
return null;
}
const parsed = new Date(`${value}T12:00:00.000Z`);
return Number.isNaN(parsed.getTime()) ? null : parsed;
}
type Context = {
params: Promise<{
id: string;
@@ -34,8 +44,17 @@ export async function POST(request: Request, { params }: Context) {
return NextResponse.json({ error: "Du darfst fuer diese Ausgabe keinen Beleg hochladen." }, { status: 403 });
}
if (expense.approvalStatus !== "APPROVED") {
return NextResponse.json({ error: "Belegabgabe ist erst nach Freigabe moeglich." }, { status: 400 });
}
const formData = await request.formData().catch(() => null);
const file = formData?.get("file");
const invoiceDate = parseInvoiceDate(formData?.get("invoiceDate") ?? null);
if (!invoiceDate) {
return NextResponse.json({ error: "Bitte ein gueltiges Rechnungsdatum angeben." }, { status: 400 });
}
if (!(file instanceof File)) {
return NextResponse.json({ error: "Bitte einen Beleg als Bild oder PDF auswaehlen." }, { status: 400 });
@@ -51,6 +70,7 @@ export async function POST(request: Request, { params }: Context) {
const proofUrl = await uploadExpenseProofToDrive({
title: expense.title,
invoiceDate: invoiceDate.toISOString().slice(0, 10),
fileName: file.name,
mimeType: file.type,
buffer: Buffer.from(await file.arrayBuffer())
@@ -59,7 +79,32 @@ export async function POST(request: Request, { params }: Context) {
const updatedExpense = await prisma.expense.update({
where: { id: expense.id },
data: {
proofUrl
proofUrl,
invoiceDate,
documentedAt: expense.documentedAt ?? new Date()
}
});
await createAuditLog(prisma, {
actorId: viewer.id,
action: "expense.document",
entityType: "expense",
entityId: updatedExpense.id,
entityLabel: updatedExpense.title,
summary: `Rechnung fuer ${updatedExpense.title} wurde abgegeben.`,
metadata: {
proofUrl: updatedExpense.proofUrl,
invoiceDate: updatedExpense.invoiceDate?.toISOString() ?? null,
rollback: {
kind: "expense.document",
expenseId: updatedExpense.id,
previousProofUrl: expense.proofUrl,
previousInvoiceDate: expense.invoiceDate?.toISOString() ?? null,
previousDocumentedAt: expense.documentedAt?.toISOString() ?? null,
nextProofUrl: updatedExpense.proofUrl,
nextInvoiceDate: updatedExpense.invoiceDate?.toISOString() ?? null,
nextDocumentedAt: updatedExpense.documentedAt?.toISOString() ?? null
}
}
});
+2 -2
View File
@@ -43,7 +43,8 @@ const expenseSchema = z
}),
proofUrl: z
.union([z.string().trim().url(), z.literal(""), z.null(), z.undefined()])
.transform((value) => (typeof value === "string" && value.length > 0 ? value : undefined))
.optional()
.transform(() => undefined)
})
.superRefine((value, ctx) => {
if (value.recurrence === "MONTHLY" && !value.recurrenceStartAt) {
@@ -111,7 +112,6 @@ export async function POST(request: Request) {
budgetId: parsed.data.budgetId,
periodId: budget.periodId,
creatorId: viewer.id,
proofUrl: parsed.data.proofUrl,
recurrence: parsed.data.recurrence,
recurrenceStartAt,
approvalStatus: needsManualApproval ? "PENDING" : "APPROVED"
+13 -5
View File
@@ -39,6 +39,7 @@ const CSV_HEADERS = [
"approvalType",
"recurrence",
"recurrenceStartAt",
"invoiceDate",
"proofUrl",
"createdAt",
"paidAt",
@@ -205,7 +206,8 @@ export async function GET() {
approvalType: "",
recurrence: "",
recurrenceStartAt: "",
proofUrl: "",
invoiceDate: "",
proofUrl: "",
createdAt: user.createdAt.toISOString(),
paidAt: "",
documentedAt: "",
@@ -258,7 +260,8 @@ export async function GET() {
approvalType: "",
recurrence: "",
recurrenceStartAt: "",
proofUrl: "",
invoiceDate: "",
proofUrl: "",
createdAt: period.createdAt.toISOString(),
paidAt: "",
documentedAt: "",
@@ -311,7 +314,8 @@ export async function GET() {
approvalType: "",
recurrence: "",
recurrenceStartAt: "",
proofUrl: "",
invoiceDate: "",
proofUrl: "",
createdAt: group.createdAt.toISOString(),
paidAt: "",
documentedAt: "",
@@ -363,7 +367,8 @@ export async function GET() {
approvalType: "",
recurrence: "",
recurrenceStartAt: "",
proofUrl: "",
invoiceDate: "",
proofUrl: "",
createdAt: budget.createdAt.toISOString(),
paidAt: "",
documentedAt: "",
@@ -415,6 +420,7 @@ export async function GET() {
approvalType: "",
recurrence: expense.recurrence,
recurrenceStartAt: expense.recurrenceStartAt?.toISOString() ?? "",
invoiceDate: expense.invoiceDate?.toISOString() ?? "",
proofUrl: expense.proofUrl ?? "",
createdAt: expense.createdAt.toISOString(),
paidAt: expense.paidAt?.toISOString() ?? "",
@@ -467,6 +473,7 @@ export async function GET() {
approvalType: approval.approvalType,
recurrence: expense.recurrence,
recurrenceStartAt: expense.recurrenceStartAt?.toISOString() ?? "",
invoiceDate: expense.invoiceDate?.toISOString() ?? "",
proofUrl: "",
createdAt: approval.timestamp.toISOString(),
paidAt: "",
@@ -523,7 +530,8 @@ export async function GET() {
approvalType: "",
recurrence: "",
recurrenceStartAt: "",
proofUrl: "",
invoiceDate: "",
proofUrl: "",
createdAt: auditLog.createdAt.toISOString(),
paidAt: "",
documentedAt: "",
+1
View File
@@ -226,6 +226,7 @@ export async function POST(request: Request) {
approvalStatus: row.approvalStatus === "APPROVED" ? "APPROVED" : "PENDING",
recurrence: row.recurrence === "MONTHLY" ? "MONTHLY" : "NONE",
recurrenceStartAt: toDate(row.recurrenceStartAt),
invoiceDate: toDate(row.invoiceDate),
proofUrl: toNullable(row.proofUrl),
createdAt: toDate(row.createdAt) ?? new Date(),
paidAt: toDate(row.paidAt),
+1
View File
@@ -185,6 +185,7 @@ export default async function DashboardPage() {
recurrenceStartAt,
paidAt: expense.paidAt?.toISOString() ?? null,
documentedAt: expense.documentedAt?.toISOString() ?? null,
invoiceDate: expense.invoiceDate?.toISOString() ?? null,
proofUrl: expense.proofUrl,
createdAt: expense.createdAt.toISOString(),
creator: {