ADD: added some quiality of life features

This commit is contained in:
hwinkel
2026-03-15 19:53:11 +01:00
parent f5b259cae2
commit 40a2764dd0
30 changed files with 1397 additions and 51 deletions
+68 -2
View File
@@ -1,5 +1,6 @@
import { getApiUser } from "@/session.server";
import prisma from "@/lib/prisma.server";
import { generateInvoiceNumber } from "@/lib/invoice-number.server";
import { InvoiceStatus } from "@prisma/client";
import { z } from "zod";
@@ -22,6 +23,31 @@ export async function loader({ request, params }: { request: Request; params: {
const statusSchema = z.object({ status: z.nativeEnum(InvoiceStatus) });
const itemSchema = z.object({
position: z.number().int(),
description: z.string().min(1),
quantity: z.number().positive(),
unit: z.string().optional(),
unitPrice: z.number(),
taxRate: z.number(),
netAmount: z.number(),
taxAmount: z.number(),
grossAmount: z.number(),
});
const updateSchema = z.object({
customerId: z.string().min(1),
issueDate: z.string(),
deliveryDate: z.string().optional(),
dueDate: z.string(),
notes: z.string().optional(),
kleinunternehmer: z.boolean().optional().default(false),
items: z.array(itemSchema).min(1),
netTotal: z.number(),
taxTotal: z.number(),
grossTotal: z.number(),
});
export async function action({ request, params }: { request: Request; params: { id: string } }) {
const user = await getApiUser(request);
if (!user) return Response.json({ error: "Unauthorized" }, { status: 401 });
@@ -29,9 +55,36 @@ export async function action({ request, params }: { request: Request; params: {
const invoice = await getInvoice(params.id, user.id);
if (!invoice) return Response.json({ error: "Not found" }, { status: 404 });
if (request.method === "PUT") {
const body = await request.json();
const parsed = updateSchema.safeParse(body);
if (!parsed.success) return Response.json({ error: parsed.error.issues }, { status: 400 });
const { items, ...invoiceData } = parsed.data;
const updated = await prisma.$transaction(async (tx) => {
await tx.invoiceItem.deleteMany({ where: { invoiceId: params.id } });
return tx.invoice.update({
where: { id: params.id },
data: {
customerId: invoiceData.customerId,
issueDate: new Date(invoiceData.issueDate),
deliveryDate: invoiceData.deliveryDate ? new Date(invoiceData.deliveryDate) : null,
dueDate: new Date(invoiceData.dueDate),
notes: invoiceData.notes ?? null,
kleinunternehmer: invoiceData.kleinunternehmer,
netTotal: invoiceData.netTotal,
taxTotal: invoiceData.taxTotal,
grossTotal: invoiceData.grossTotal,
items: { create: items },
},
});
});
return Response.json(updated);
}
if (request.method === "DELETE") {
const isAdmin = user.role === "ADMIN";
const deletableStatuses: InvoiceStatus[] = [InvoiceStatus.DRAFT, InvoiceStatus.CANCELLED];
const deletableStatuses: InvoiceStatus[] = [InvoiceStatus.DRAFT, InvoiceStatus.CANCELLED, InvoiceStatus.DELETED];
if (!isAdmin && !deletableStatuses.includes(invoice.status)) {
return Response.json(
{ error: "Nur Entwürfe und stornierte Rechnungen können gelöscht werden." },
@@ -47,9 +100,22 @@ export async function action({ request, params }: { request: Request; params: {
const parsed = statusSchema.safeParse(body);
if (!parsed.success) return Response.json({ error: parsed.error.issues }, { status: 400 });
const newStatus = parsed.data.status;
let numberUpdate: string | null | undefined = undefined;
if (newStatus === "DELETED") {
numberUpdate = null;
} else if (invoice.status === "DELETED") {
numberUpdate = await generateInvoiceNumber(invoice.companyId);
}
const updated = await prisma.invoice.update({
where: { id: params.id },
data: { status: parsed.data.status },
data: {
status: newStatus,
deletedAt: newStatus === "DELETED" ? new Date() : null,
...(numberUpdate !== undefined && { number: numberUpdate }),
},
});
return Response.json(updated);
}