Vier Bereiche unter Einstellungen -> System: Adresse und Agent, Kaufmaennisches, Sicherheit sowie Firma und Darstellung. Alles liegt als eine JSON-Zeile in der Tabelle Einstellung; fehlende Felder bekommen ihren Vorgabewert, eine kaputte Zeile legt den Betrieb nicht lahm. Die Werte wirken auch wirklich: Die Dashboard-Adresse und der Melderhythmus landen im ausgelieferten Einzeiler und im systemd-Timer (live geprueft), der Steuersatz in neuen Rechnungspositionen, die Sitzungsdauer im Token, die Firmenangaben im Kopf der PDF-Dateien. Nebenbei geschlossen: die OIDC-Luecke aus #024. Erlaubte Domaenen und ein Schalter gegen automatisch angelegte Konten. Dazu raeumt der Worker nachts nach den eingestellten Fristen auf - der juengste Report je Host bleibt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
114 lines
3.7 KiB
TypeScript
114 lines
3.7 KiB
TypeScript
import { csvAntwort, csvDatei } from "@/lib/csv";
|
||
import { formatDatum, formatEuro } from "@/lib/format";
|
||
import { Bericht, pdfAntwort, Tabelle } from "@/server/pdf/dokument";
|
||
import { rechnungsListe, type RechnungsFilter } from "@/server/queries/rechnungen";
|
||
import { einstellungen } from "@/server/einstellungen";
|
||
|
||
export const dynamic = "force-dynamic";
|
||
|
||
/** Rechnungsliste als CSV (Buchhaltung) oder PDF, mit den Filtern der Ansicht. */
|
||
export async function GET(request: Request) {
|
||
const parameter = new URL(request.url).searchParams;
|
||
const format = parameter.get("format") ?? "csv";
|
||
const filter: RechnungsFilter = {
|
||
status: (parameter.get("status") as RechnungsFilter["status"]) ?? "alle",
|
||
kunde: parameter.get("kunde") ?? undefined,
|
||
jahr: parameter.get("jahr") ?? undefined,
|
||
q: parameter.get("q") ?? undefined,
|
||
};
|
||
|
||
const { zeilen, summen } = await rechnungsListe(filter);
|
||
const datum = new Date().toISOString().slice(0, 10);
|
||
const zeitraum = filter.jahr ? `Jahr ${filter.jahr}` : "alle Jahre";
|
||
|
||
if (format === "pdf") {
|
||
const { firmaName, firmaAnschrift, firmaEmail, zeitzone } = await einstellungen();
|
||
const firmaAngaben = { name: firmaName, anschrift: firmaAnschrift, email: firmaEmail, zeitzone };
|
||
|
||
return pdfAntwort(
|
||
Bericht({
|
||
firma: firmaAngaben,
|
||
titel: "Rechnungsliste",
|
||
untertitel: `${zeilen.length} Laufzeiten · ${zeitraum}`,
|
||
quer: true,
|
||
children: Tabelle({
|
||
spalten: [
|
||
{
|
||
titel: "Kunde",
|
||
breite: 18,
|
||
wert: (z: (typeof zeilen)[number]) => z.kunde?.name ?? "Nicht zugeordnet",
|
||
},
|
||
{ titel: "Host", breite: 13, wert: (z) => z.hostname },
|
||
{
|
||
titel: "Laufzeit",
|
||
breite: 16,
|
||
wert: (z) => `${formatDatum(z.start)} – ${formatDatum(z.ende)}`,
|
||
},
|
||
{ titel: "Proxmox-Rechnung", breite: 13, wert: (z) => z.einkaufNummer ?? "–" },
|
||
{ titel: "Einkauf", breite: 9, wert: (z) => formatEuro(z.einkaufBetrag), rechts: true },
|
||
{ titel: "Bezahlt", breite: 7, wert: (z) => (z.einkaufBezahlt ? "ja" : "nein") },
|
||
{ titel: "Kundenrechnung", breite: 12, wert: (z) => z.verkaufNummer ?? "–" },
|
||
{ titel: "Verkauf", breite: 6, wert: (z) => formatEuro(z.verkaufBetrag), rechts: true },
|
||
{
|
||
titel: "Marge",
|
||
breite: 6,
|
||
wert: (z) => (z.marge === null ? "–" : formatEuro(z.marge)),
|
||
rechts: true,
|
||
},
|
||
],
|
||
zeilen,
|
||
summe: [
|
||
"Summe",
|
||
"",
|
||
"",
|
||
"",
|
||
formatEuro(summen.einkauf),
|
||
"",
|
||
"",
|
||
formatEuro(summen.verkauf),
|
||
formatEuro(summen.marge),
|
||
],
|
||
}),
|
||
}),
|
||
`proxledger-rechnungen-${filter.jahr ?? "alle"}-${datum}.pdf`,
|
||
);
|
||
}
|
||
|
||
const inhalt = csvDatei(
|
||
[
|
||
"Kunde",
|
||
"Host",
|
||
"Key",
|
||
"Laufzeit von",
|
||
"Laufzeit bis",
|
||
"Proxmox-Rechnung",
|
||
"Einkauf netto",
|
||
"Proxmox bezahlt",
|
||
"Fällig am",
|
||
"Kundenrechnung",
|
||
"Verkauf netto",
|
||
"Kundenrechnung erstellt",
|
||
"Vom Kunden bezahlt",
|
||
"Marge netto",
|
||
],
|
||
zeilen.map((z) => [
|
||
z.kunde?.name ?? "Nicht zugeordnet",
|
||
z.hostname,
|
||
z.key,
|
||
z.start,
|
||
z.ende,
|
||
z.einkaufNummer,
|
||
z.einkaufBetrag,
|
||
z.einkaufBezahlt === null ? "" : z.einkaufBezahlt ? "ja" : "nein",
|
||
z.einkaufFaelligAm,
|
||
z.verkaufNummer,
|
||
z.verkaufBetrag,
|
||
z.verkaufErstellt ? "ja" : "nein",
|
||
z.verkaufBezahlt ? "ja" : "nein",
|
||
z.marge,
|
||
]),
|
||
);
|
||
|
||
return csvAntwort(inhalt, `proxledger-rechnungen-${filter.jahr ?? "alle"}-${datum}.csv`);
|
||
}
|