Files
ProxLedger/src/components/einstellungen/SystemFormular.tsx
T
florian.wildeandClaude Opus 5 e50fb30ab3 feat(rechnungen): Rabatt je Position, Reverse Charge, Ablauf-Regel (#037)
Eine echte Proxmox-Rechnung zeigt je Subscription-Key eine Zeile mit
Listenpreis und eine mit "Client Discount". Unser Modell hing den Rabatt an
die Rechnung - bei einer Sammelrechnung ueber zwei Hosts liesse er sich damit
keinem Host zuordnen, und die Marge je Host waere falsch. Schlimmer: Er wurde
nirgends abgezogen.

Jetzt haengt der Rabatt an der Position, der Rechnungsbetrag ist die Summe
nach Abzug, und Marge wie Summen rechnen mit dem tatsaechlich gezahlten
Betrag. Die Migration verteilt vorhandene Rabatte anteilig auf die Positionen
und rechnet die Summen neu - nichts geht verloren.

Dazu ein eigener Steuersatz fuer den Einkauf mit Vorgabe 0: Proxmox rechnet
gegenueber Unternehmen mit USt-IdNr. ueber Reverse Charge ab.

Neue zwoelfte Warnregel aus Florians Ablauf: Der Kunde hat bezahlt, die
Proxmox-Rechnung ist noch offen - der Moment, an dem man selbst zahlt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-03 16:42:12 +02:00

377 lines
13 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 client";
import { useActionState } from "react";
import { Card } from "@/components/ui/Card";
import { type AktionsErgebnis, einstellungenSpeichernAktion } from "@/server/actions/einstellungen";
import type { Einstellungen } from "@/server/einstellungen";
const LEER: AktionsErgebnis = {};
function Feld({
name,
beschriftung,
hinweis,
fehler,
children,
}: {
name: string;
beschriftung: string;
hinweis?: string;
fehler?: string;
children: React.ReactNode;
}) {
return (
<div className="mb-3">
<label className="form-label" htmlFor={name}>
{beschriftung}
</label>
{children}
{hinweis ? <div className="form-text">{hinweis}</div> : null}
{fehler ? <div className="invalid-feedback d-block">{fehler}</div> : null}
</div>
);
}
export function SystemFormular({ werte, appUrl }: { werte: Einstellungen; appUrl: string }) {
const [ergebnis, aktion, laeuft] = useActionState(einstellungenSpeichernAktion, LEER);
const fehler = ergebnis.felder ?? {};
return (
<form action={aktion}>
{ergebnis.fehler ? <div className="alert alert-danger">{ergebnis.fehler}</div> : null}
{ergebnis.erfolg ? <div className="alert alert-success">{ergebnis.erfolg}</div> : null}
<div className="row g-4">
<div className="col-12 col-xl-6">
<Card titel="Adresse und Agent" rahmen="primary">
<Feld
name="dashboardUrl"
beschriftung="Adresse des Dashboards"
hinweis={`Steht im Installations-Einzeiler und in Benachrichtigungen. Leer lassen heißt: aus der Umgebung (${appUrl || "nicht gesetzt"}).`}
fehler={fehler.dashboardUrl}
>
<input
type="url"
className="form-control"
id="dashboardUrl"
name="dashboardUrl"
defaultValue={werte.dashboardUrl}
placeholder="https://proxledger.example.de"
/>
</Feld>
<Feld
name="agentRhythmus"
beschriftung="Wie oft meldet ein Host?"
hinweis="Gilt für neu installierte Agents; bestehende übernehmen es bei der nächsten Installation."
>
<select className="form-select" id="agentRhythmus" name="agentRhythmus" defaultValue={werte.agentRhythmus}>
<option value="taeglich">täglich</option>
<option value="zwoelfstuendlich">alle 12 Stunden</option>
<option value="stuendlich">stündlich</option>
</select>
</Feld>
<Feld
name="agentStreuungStunden"
beschriftung="Zufällige Verzögerung (Stunden)"
hinweis="Verteilt die Meldungen, damit nicht alle Hosts gleichzeitig anklopfen."
fehler={fehler.agentStreuungStunden}
>
<input
type="number"
min={0}
max={12}
className="form-control"
id="agentStreuungStunden"
name="agentStreuungStunden"
defaultValue={werte.agentStreuungStunden}
/>
</Feld>
<div className="form-check">
<input
type="checkbox"
className="form-check-input pl-touch-target"
id="agentAutoUpdate"
name="agentAutoUpdate"
value="an"
defaultChecked={werte.agentAutoUpdate}
/>
<label className="form-check-label" htmlFor="agentAutoUpdate">
Agents dürfen sich selbst aktualisieren
</label>
<div className="form-text">
Der Agent lädt bei einer neueren Fassung das Skript vom Dashboard nach.
</div>
</div>
</Card>
</div>
<div className="col-12 col-xl-6">
<Card titel="Kaufmännisches" rahmen="info">
<Feld name="waehrung" beschriftung="Währung">
<select className="form-select" id="waehrung" name="waehrung" defaultValue={werte.waehrung}>
<option value="EUR">Euro (€)</option>
<option value="CHF">Schweizer Franken (CHF)</option>
</select>
</Feld>
<Feld
name="steuersatz"
beschriftung="Steuersatz Verkauf in Prozent"
hinweis="Vorgabe für neue Kundenrechnungen."
fehler={fehler.steuersatz}
>
<input
type="number"
step="0.1"
min={0}
max={100}
className="form-control"
id="steuersatz"
name="steuersatz"
defaultValue={werte.steuersatz}
/>
</Feld>
<Feld
name="steuersatzEinkauf"
beschriftung="Steuersatz Einkauf in Prozent"
hinweis="Für die Proxmox-Rechnungen. 0 bei Reverse Charge – der Normalfall mit USt-IdNr."
fehler={fehler.steuersatzEinkauf}
>
<input
type="number"
step="0.1"
min={0}
max={100}
className="form-control"
id="steuersatzEinkauf"
name="steuersatzEinkauf"
defaultValue={werte.steuersatzEinkauf}
/>
</Feld>
<Feld
name="zahlungszielTage"
beschriftung="Zahlungsziel in Tagen"
hinweis="Setzt das Fälligkeitsdatum neuer Rechnungen vor."
fehler={fehler.zahlungszielTage}
>
<input
type="number"
min={0}
max={120}
className="form-control"
id="zahlungszielTage"
name="zahlungszielTage"
defaultValue={werte.zahlungszielTage}
/>
</Feld>
<Feld
name="margeProzent"
beschriftung="Aufschlag in Prozent"
hinweis="Vorschlag für den Verkaufspreis, ausgehend vom Einkaufspreis bei Proxmox."
fehler={fehler.margeProzent}
>
<input
type="number"
step="0.5"
min={0}
max={500}
className="form-control"
id="margeProzent"
name="margeProzent"
defaultValue={werte.margeProzent}
/>
</Feld>
</Card>
</div>
<div className="col-12 col-xl-6">
<Card titel="Sicherheit" rahmen="danger">
<Feld
name="sitzungsdauerStunden"
beschriftung="Sitzungsdauer in Stunden"
hinweis="Nach dieser Zeit ist eine neue Anmeldung nötig."
fehler={fehler.sitzungsdauerStunden}
>
<input
type="number"
min={1}
max={168}
className="form-control"
id="sitzungsdauerStunden"
name="sitzungsdauerStunden"
defaultValue={werte.sitzungsdauerStunden}
/>
</Feld>
<Feld
name="anmeldeversuche"
beschriftung="Anmeldeversuche je Viertelstunde"
fehler={fehler.anmeldeversuche}
>
<input
type="number"
min={3}
max={50}
className="form-control"
id="anmeldeversuche"
name="anmeldeversuche"
defaultValue={werte.anmeldeversuche}
/>
</Feld>
<Feld
name="oidcDomaenen"
beschriftung="Erlaubte E-Mail-Domänen für OIDC"
hinweis="Mehrere durch Komma trennen, etwa „wilde-server.de, kunde.de“. Leer heißt: alle."
>
<input
type="text"
className="form-control"
id="oidcDomaenen"
name="oidcDomaenen"
defaultValue={werte.oidcDomaenen}
placeholder="firma.de"
/>
</Feld>
<div className="form-check mb-3">
<input
type="checkbox"
className="form-check-input pl-touch-target"
id="oidcKontenAnlegen"
name="oidcKontenAnlegen"
value="an"
defaultChecked={werte.oidcKontenAnlegen}
/>
<label className="form-check-label" htmlFor="oidcKontenAnlegen">
Unbekannte OIDC-Anmeldungen legen ein Konto an
</label>
<div className="form-text">
Aus heißt: Nur wer hier schon ein Konto hat, kommt herein – auch wenn die Anmeldung
beim Identitätsanbieter klappt.
</div>
</div>
<Feld
name="oidcStandardRolle"
beschriftung="Rolle für neue OIDC-Konten"
hinweis="Gilt nur für Konten, die bei der ersten Anmeldung automatisch entstehen."
>
<select
className="form-select"
id="oidcStandardRolle"
name="oidcStandardRolle"
defaultValue={werte.oidcStandardRolle}
>
<option value="TECHNIKER">Techniker</option>
<option value="KUNDE">Kunde</option>
<option value="ADMIN">Administrator</option>
</select>
</Feld>
<Feld
name="aufbewahrungReportsTage"
beschriftung="Reports aufbewahren (Tage)"
hinweis="0 heißt: unbegrenzt. Der aktuelle Stand jedes Hosts bleibt immer erhalten."
fehler={fehler.aufbewahrungReportsTage}
>
<input
type="number"
min={0}
max={3650}
className="form-control"
id="aufbewahrungReportsTage"
name="aufbewahrungReportsTage"
defaultValue={werte.aufbewahrungReportsTage}
/>
</Feld>
<Feld
name="aufbewahrungAuditTage"
beschriftung="Protokoll aufbewahren (Tage)"
hinweis="0 heißt: unbegrenzt."
fehler={fehler.aufbewahrungAuditTage}
>
<input
type="number"
min={0}
max={3650}
className="form-control"
id="aufbewahrungAuditTage"
name="aufbewahrungAuditTage"
defaultValue={werte.aufbewahrungAuditTage}
/>
</Feld>
</Card>
</div>
<div className="col-12 col-xl-6">
<Card titel="Firma und Darstellung" rahmen="keiner">
<Feld
name="firmaName"
beschriftung="Firmenname"
hinweis="Erscheint im Kopf der erzeugten PDF-Rechnungen."
>
<input
type="text"
className="form-control"
id="firmaName"
name="firmaName"
defaultValue={werte.firmaName}
/>
</Feld>
<Feld name="firmaAnschrift" beschriftung="Anschrift">
<textarea
className="form-control"
id="firmaAnschrift"
name="firmaAnschrift"
rows={3}
defaultValue={werte.firmaAnschrift}
/>
</Feld>
<Feld name="firmaEmail" beschriftung="E-Mail-Adresse" fehler={fehler.firmaEmail}>
<input
type="email"
className="form-control"
id="firmaEmail"
name="firmaEmail"
defaultValue={werte.firmaEmail}
/>
</Feld>
<Feld
name="zeitzone"
beschriftung="Zeitzone"
hinweis="Gilt für PDF, CSV-Ausgaben und Benachrichtigungen."
fehler={fehler.zeitzone}
>
<input
type="text"
className="form-control"
id="zeitzone"
name="zeitzone"
defaultValue={werte.zeitzone}
placeholder="Europe/Berlin"
/>
</Feld>
</Card>
</div>
</div>
<div className="mb-4">
<button type="submit" className="btn btn-primary" disabled={laeuft}>
{laeuft ? "Wird gespeichert …" : "Einstellungen speichern"}
</button>
</div>
</form>
);
}