feat(einstellungen): Betriebswerte im Dashboard einstellbar (#025)
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>
This commit is contained in:
@@ -0,0 +1,341 @@
|
||||
"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 in Prozent"
|
||||
hinweis="Vorgabe für neue Rechnungspositionen."
|
||||
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="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="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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user