Files
ProxLedger/src/server/actions/rechnungen.ts
T
florian.wildeandClaude Opus 5 558dfea34c feat(rechnungen): Laufzeiten bestaetigen, Rechnungen erfassen und Status setzen (#008)
Rechnungen mit Positionen: gleiche Nummer ergibt eine Sammelrechnung ueber mehrere
Laufzeiten. PDF-Upload mit Inhaltspruefung und Ablage unter der Pruefsumme,
Haekchen direkt in der Tabelle, Filter nach Status, Kunde und Jahr. 78 Tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 21:54:45 +02:00

169 lines
5.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use server";
import { revalidatePath } from "next/cache";
import { anhangSpeichern, UploadFehler } from "@/lib/uploads";
import {
einkaufBezahltSetzen,
kundenrechnungErfassen,
laufzeitAnlegen,
laufzeitBestaetigen,
laufzeitVerwerfen,
proxmoxRechnungErfassen,
RechnungsFehler,
verkaufBezahltSetzen,
verkaufErstelltSetzen,
} from "@/server/services/rechnungen";
import type { AktionsErgebnis } from "./kunden";
function datum(wert: FormDataEntryValue | null): Date | null {
const text = wert?.toString().trim();
return text ? new Date(`${text}T00:00:00Z`) : null;
}
function betrag(wert: FormDataEntryValue | null): number | null {
const text = wert?.toString().trim().replace(",", ".");
if (!text) return null;
const zahl = Number(text);
return Number.isFinite(zahl) ? zahl : null;
}
function seitenNeuLaden(hostId?: string | null) {
revalidatePath("/rechnungen");
revalidatePath("/");
revalidatePath("/kunden");
if (hostId) revalidatePath(`/hosts/${hostId}`);
}
/** Wandelt Fehler aus der Fachlogik in eine Rückmeldung für das Formular. */
async function ausfuehren(arbeit: () => Promise<string | void>): Promise<AktionsErgebnis> {
try {
const meldung = await arbeit();
return { erfolg: meldung || "Gespeichert." };
} catch (fehler) {
if (fehler instanceof RechnungsFehler || fehler instanceof UploadFehler) {
return { fehler: fehler.message };
}
throw fehler;
}
}
export async function laufzeitBestaetigenAktion(formular: FormData) {
const id = formular.get("laufzeitId")?.toString();
if (!id) return;
await laufzeitBestaetigen(id);
seitenNeuLaden(formular.get("hostId")?.toString());
}
export async function laufzeitVerwerfenAktion(formular: FormData) {
const id = formular.get("laufzeitId")?.toString();
if (!id) return;
await laufzeitVerwerfen(id);
seitenNeuLaden(formular.get("hostId")?.toString());
}
/** Häkchen aus der Rechnungstabelle. */
export async function statusUmschalten(formular: FormData): Promise<void> {
const laufzeitId = formular.get("laufzeitId")?.toString();
const feld = formular.get("feld")?.toString();
const wert = formular.get("wert")?.toString() === "true";
if (!laufzeitId || !feld) return;
try {
if (feld === "einkaufBezahlt") await einkaufBezahltSetzen(laufzeitId, wert);
if (feld === "verkaufErstellt") await verkaufErstelltSetzen(laufzeitId, wert);
if (feld === "verkaufBezahlt") await verkaufBezahltSetzen(laufzeitId, wert);
} catch (fehler) {
if (!(fehler instanceof RechnungsFehler)) throw fehler;
// Fehlt die Rechnung, bleibt der Status unverändert; die Tabelle zeigt weiterhin „Fehlt“.
}
seitenNeuLaden(formular.get("hostId")?.toString());
}
export async function proxmoxRechnungSpeichern(
_vorher: AktionsErgebnis,
formular: FormData,
): Promise<AktionsErgebnis> {
const laufzeitId = formular.get("laufzeitId")?.toString();
if (!laufzeitId) return { fehler: "Laufzeit fehlt." };
return ausfuehren(async () => {
const pdf = formular.get("pdf");
const anhangId = pdf instanceof File && pdf.size > 0 ? await anhangSpeichern(pdf) : null;
const rechnungsdatum = datum(formular.get("datum"));
if (!rechnungsdatum) throw new RechnungsFehler("Bitte ein Rechnungsdatum angeben.");
const netto = betrag(formular.get("nettoBetrag"));
if (netto === null) throw new RechnungsFehler("Bitte einen Nettobetrag angeben.");
const ergebnis = await proxmoxRechnungErfassen(laufzeitId, {
nummer: formular.get("nummer")?.toString() ?? "",
datum: rechnungsdatum,
faelligAm: datum(formular.get("faelligAm")),
nettoBetrag: netto,
ustSatz: betrag(formular.get("ustSatz")) ?? 19,
rabatt: betrag(formular.get("rabatt")),
zahlungsart: formular.get("zahlungsart")?.toString().trim() || null,
bezahlt: formular.get("bezahlt") === "on",
bezahltAm: datum(formular.get("bezahltAm")),
anhangId,
notizen: formular.get("notizen")?.toString().trim() || null,
});
seitenNeuLaden(formular.get("hostId")?.toString());
return ergebnis.sammelrechnung
? "Gespeichert – die Laufzeit hängt jetzt als weitere Position an dieser Sammelrechnung."
: "Proxmox-Rechnung gespeichert.";
});
}
export async function kundenrechnungSpeichern(
_vorher: AktionsErgebnis,
formular: FormData,
): Promise<AktionsErgebnis> {
const laufzeitId = formular.get("laufzeitId")?.toString();
if (!laufzeitId) return { fehler: "Laufzeit fehlt." };
return ausfuehren(async () => {
const pdf = formular.get("pdf");
const anhangId = pdf instanceof File && pdf.size > 0 ? await anhangSpeichern(pdf) : null;
const ergebnis = await kundenrechnungErfassen(laufzeitId, {
erstellt: formular.get("erstellt") === "on",
nummer: formular.get("nummer")?.toString().trim() || null,
datum: datum(formular.get("datum")),
nettoBetrag: betrag(formular.get("nettoBetrag")),
ustSatz: betrag(formular.get("ustSatz")) ?? 19,
bezahlt: formular.get("bezahlt") === "on",
bezahltAm: datum(formular.get("bezahltAm")),
anhangId,
notizen: formular.get("notizen")?.toString().trim() || null,
});
seitenNeuLaden(formular.get("hostId")?.toString());
return ergebnis.sammelrechnung
? "Gespeichert – die Laufzeit hängt jetzt als weitere Position an dieser Sammelrechnung."
: "Kundenrechnung gespeichert.";
});
}
export async function laufzeitAnlegenAktion(
_vorher: AktionsErgebnis,
formular: FormData,
): Promise<AktionsErgebnis> {
const hostId = formular.get("hostId")?.toString();
const key = formular.get("key")?.toString().trim();
const start = datum(formular.get("start"));
const ende = datum(formular.get("ende"));
if (!hostId || !key || !start || !ende) {
return { fehler: "Bitte Host, Key, Beginn und Ende angeben." };
}
return ausfuehren(async () => {
await laufzeitAnlegen({ hostId, key, start, ende, notizen: null });
seitenNeuLaden(hostId);
return "Laufzeit angelegt.";
});
}