Breite Tabellen (Kunden, Rechnungen, Ablauf-Kalender) werden unterhalb von 768 px zu Karten: jede Angabe mit ihrer Beschriftung, statt rechts abgeschnitten zu sein. Kartenkoepfe brechen um, die Jahresbahn bekommt eine schmalere Namensspalte und einen Hinweis aufs Wischen. Standort am Host (#036): Das Formular sendete nur den Kunden, die Aktion las aber auch standortId - jede Kundenzuweisung hat den Standort still geloescht. Jetzt stehen Kunde und Standort in einem Formular, die Liste richtet sich nach dem Kunden, und die Aktion prueft, dass der Standort zum Kunden gehoert. Nebenbefund: Das Kontrollkaestchen fuer den Rechnungsstatus trug pl-touch-target und wurde dadurch selbst zum 44 Pixel hohen leeren Kasten. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -16,7 +16,7 @@ import { Tabs } from "@/components/ui/Tabs";
|
||||
import { PRODUKT_TEXT } from "@/lib/anzeige";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { formatDatum, formatDatumZeit, formatEuro, formatUptime, formatZahl } from "@/lib/format";
|
||||
import { hostKundeSetzen } from "@/server/actions/host-detail";
|
||||
import { KundeStandortFormular } from "@/components/hosts/KundeStandortFormular";
|
||||
import { hostTokenWiderrufen } from "@/server/actions/tokens";
|
||||
import { hostMitDetails } from "@/server/queries/hostliste";
|
||||
import { meldungsGrenze } from "@/server/queries/hosts";
|
||||
@@ -43,7 +43,11 @@ export default async function HostDetailSeite({ params }: { params: Promise<{ id
|
||||
|
||||
const kunden = await prisma.kunde.findMany({
|
||||
orderBy: { name: "asc" },
|
||||
select: { id: true, name: true },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
standorte: { orderBy: { name: "asc" }, select: { id: true, name: true } },
|
||||
},
|
||||
});
|
||||
|
||||
// Welche Cluster-Mitglieder melden sich selbst? Fehlende Knoten haben keinen Agent (#022).
|
||||
@@ -167,30 +171,12 @@ export default async function HostDetailSeite({ params }: { params: Promise<{ id
|
||||
<Zeile bezeichnung="Erstmals gesehen">{formatDatum(host.erstmalsGesehenAm)}</Zeile>
|
||||
<Zeile bezeichnung="Letzte Meldung">{formatDatumZeit(host.letzterReportAm)}</Zeile>
|
||||
|
||||
<form action={hostKundeSetzen} className="mt-3">
|
||||
<input type="hidden" name="hostId" value={host.id} />
|
||||
<label className="form-label small" htmlFor="kundeId">
|
||||
Kunde ändern
|
||||
</label>
|
||||
<div className="input-group input-group-sm">
|
||||
<select
|
||||
id="kundeId"
|
||||
name="kundeId"
|
||||
className="form-select"
|
||||
defaultValue={host.kundeId ?? ""}
|
||||
>
|
||||
<option value="">– nicht zugeordnet –</option>
|
||||
{kunden.map((kunde) => (
|
||||
<option key={kunde.id} value={kunde.id}>
|
||||
{kunde.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button type="submit" className="btn btn-outline-primary">
|
||||
Setzen
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<KundeStandortFormular
|
||||
hostId={host.id}
|
||||
kunden={kunden}
|
||||
kundeId={host.kundeId}
|
||||
standortId={host.standortId}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -41,8 +41,8 @@ export default async function JahresbahnSeite() {
|
||||
<p className="mb-0 text-body-secondary">Noch kein Host mit Ablaufdatum gemeldet.</p>
|
||||
) : (
|
||||
<div className="table-responsive">
|
||||
<div style={{ minWidth: "820px" }}>
|
||||
<div className="d-flex text-body-secondary small mb-2" style={{ marginLeft: "13rem" }}>
|
||||
<div className="pl-timeline">
|
||||
<div className="d-flex text-body-secondary small mb-2 pl-timeline-kopf">
|
||||
{monate.map((monat) => (
|
||||
<div key={monat} style={{ width: `${100 / 12}%` }}>
|
||||
{monat}
|
||||
@@ -72,7 +72,7 @@ export default async function JahresbahnSeite() {
|
||||
: "var(--pl-info)";
|
||||
return (
|
||||
<div key={eintrag.hostId} className="d-flex align-items-center mb-1">
|
||||
<div className="small text-truncate" style={{ width: "13rem" }}>
|
||||
<div className="small text-truncate pl-timeline-name">
|
||||
<Link href={`/hosts/${eintrag.hostId}`} className="text-decoration-none">
|
||||
{eintrag.hostname}
|
||||
</Link>
|
||||
@@ -112,7 +112,8 @@ export default async function JahresbahnSeite() {
|
||||
)}
|
||||
|
||||
<p className="small text-body-secondary mb-0">
|
||||
Die rote Linie markiert heute ({formatDatum(jetzt)}). Balkenfarbe: rot unter 14 Tagen, gelb
|
||||
<span className="d-md-none">Seitwärts wischen zeigt die späteren Monate. </span>Die rote
|
||||
Linie markiert heute ({formatDatum(jetzt)}). Balkenfarbe: rot unter 14 Tagen, gelb
|
||||
unter 30 Tagen, sonst blau. Ein Klick führt zur Laufzeit mit ihren Rechnungen.
|
||||
</p>
|
||||
</Card>
|
||||
@@ -125,11 +126,11 @@ export default async function JahresbahnSeite() {
|
||||
tabelle
|
||||
>
|
||||
<div className="table-responsive">
|
||||
<table className="table table-striped align-middle">
|
||||
<table className="table table-striped align-middle pl-karten-tabelle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Ablauf</th>
|
||||
<th scope="col">Host</th>
|
||||
<th scope="col">Ablauf</th>
|
||||
<th scope="col">Kunde</th>
|
||||
<th scope="col">Key</th>
|
||||
<th scope="col">Proxmox</th>
|
||||
@@ -139,15 +140,15 @@ export default async function JahresbahnSeite() {
|
||||
<tbody>
|
||||
{naechste90Tage.map((eintrag) => (
|
||||
<tr key={eintrag.hostId}>
|
||||
<td>
|
||||
<AblaufBadge datum={eintrag.ablauf.toISOString()} heute={jetzt} />
|
||||
</td>
|
||||
<td>
|
||||
<Link href={`/hosts/${eintrag.hostId}`} className="text-decoration-none">
|
||||
<td className="pl-karten-kopf">
|
||||
<Link href={`/hosts/${eintrag.hostId}`} className="fw-semibold text-decoration-none">
|
||||
{eintrag.hostname}
|
||||
</Link>
|
||||
</td>
|
||||
<td>
|
||||
<td data-label="Ablauf">
|
||||
<AblaufBadge datum={eintrag.ablauf.toISOString()} heute={jetzt} />
|
||||
</td>
|
||||
<td data-label="Kunde">
|
||||
{eintrag.kunde ? (
|
||||
<Link href={`/kunden/${eintrag.kunde.id}`} className="text-decoration-none">
|
||||
{eintrag.kunde.name}
|
||||
@@ -156,13 +157,13 @@ export default async function JahresbahnSeite() {
|
||||
<span className="badge pl-bg-neutral">Nicht zugeordnet</span>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<td data-label="Key">
|
||||
<CopyableCode wert={eintrag.key} />
|
||||
</td>
|
||||
<td>
|
||||
<td data-label="Proxmox">
|
||||
<EinkaufBadge bezahlt={eintrag.einkaufBezahlt} />
|
||||
</td>
|
||||
<td>
|
||||
<td data-label="Kundenrechnung">
|
||||
<VerkaufBadge erstellt={eintrag.verkaufErstellt ?? false} bezahlt={false} />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -39,7 +39,7 @@ export default async function KundenSeite() {
|
||||
}
|
||||
>
|
||||
<div className="table-responsive">
|
||||
<table className="table table-striped align-middle">
|
||||
<table className="table table-striped align-middle pl-karten-tabelle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Kunde</th>
|
||||
@@ -56,26 +56,28 @@ export default async function KundenSeite() {
|
||||
const punkte = rechnungspunkte.get(kunde.id);
|
||||
return (
|
||||
<tr key={kunde.id}>
|
||||
<td>
|
||||
<td className="pl-karten-kopf">
|
||||
<Link href={`/kunden/${kunde.id}`} className="fw-semibold text-decoration-none">
|
||||
{kunde.name}
|
||||
</Link>
|
||||
<div className="small text-body-secondary">{kunde.ansprechpartner ?? "–"}</div>
|
||||
</td>
|
||||
<td className="pl-mono">{kunde.kundennummer}</td>
|
||||
<td>{kunde.hosts}</td>
|
||||
<td>{kunde.aktiv}</td>
|
||||
<td>
|
||||
<td className="pl-mono" data-label="Kundennummer">
|
||||
{kunde.kundennummer}
|
||||
</td>
|
||||
<td data-label="Hosts">{kunde.hosts}</td>
|
||||
<td data-label="Aktiv lizenziert">{kunde.aktiv}</td>
|
||||
<td data-label="Nächster Ablauf">
|
||||
<AblaufBadge datum={kunde.naechsterAblauf?.toISOString() ?? null} heute={heute} />
|
||||
</td>
|
||||
<td>
|
||||
<td data-label="Warnungen">
|
||||
{kunde.offeneAlerts > 0 ? (
|
||||
<Badge farbe="warning">{kunde.offeneAlerts} offen</Badge>
|
||||
) : (
|
||||
<Badge farbe="success">keine</Badge>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<td data-label="Rechnungen">
|
||||
{punkte && punkte.offen > 0 ? (
|
||||
<Badge farbe="danger">{punkte.offen} offen</Badge>
|
||||
) : (
|
||||
|
||||
@@ -197,7 +197,7 @@ export default async function RechnungenSeite({ searchParams }: { searchParams:
|
||||
</form>
|
||||
|
||||
<div className="table-responsive">
|
||||
<table className="table table-striped align-middle">
|
||||
<table className="table table-striped align-middle pl-karten-tabelle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Kunde</th>
|
||||
@@ -234,7 +234,13 @@ export default async function RechnungenSeite({ searchParams }: { searchParams:
|
||||
const faelligIn = tageBis(zeile.einkaufFaelligAm, heute);
|
||||
return (
|
||||
<tr key={zeile.id}>
|
||||
<td>
|
||||
{/* Auf dem Telefon steht der Host als Überschrift der Karte, sonst als Spalte. */}
|
||||
<td className="d-md-none pl-karten-kopf">
|
||||
<Link href={`/hosts/${zeile.hostId}`} className="fw-semibold text-decoration-none">
|
||||
{zeile.hostname}
|
||||
</Link>
|
||||
</td>
|
||||
<td data-label="Kunde">
|
||||
{zeile.kunde ? (
|
||||
<Link href={`/kunden/${zeile.kunde.id}`} className="text-decoration-none">
|
||||
{zeile.kunde.name}
|
||||
@@ -248,13 +254,13 @@ export default async function RechnungenSeite({ searchParams }: { searchParams:
|
||||
{zeile.hostname}
|
||||
</Link>
|
||||
</td>
|
||||
<td className="d-none d-lg-table-cell">
|
||||
<td className="d-none d-lg-table-cell" data-label="Laufzeit">
|
||||
<Link href={`/rechnungen/${zeile.id}`} className="text-decoration-none">
|
||||
{formatDatum(zeile.start)} – {formatDatum(zeile.ende)}
|
||||
</Link>
|
||||
{zeile.vorschlag ? <Badge farbe="info">Vorschlag</Badge> : null}
|
||||
</td>
|
||||
<td>
|
||||
<td data-label="Proxmox-Rechnung">
|
||||
<CopyableCode wert={zeile.einkaufNummer} />
|
||||
{zeile.einkaufBezahlt === false && faelligIn !== null && faelligIn <= 7 ? (
|
||||
<div className="small pl-text-danger">
|
||||
@@ -262,8 +268,10 @@ export default async function RechnungenSeite({ searchParams }: { searchParams:
|
||||
</div>
|
||||
) : null}
|
||||
</td>
|
||||
<td className="d-none d-xl-table-cell">{formatEuro(zeile.einkaufBetrag)}</td>
|
||||
<td className="text-center">
|
||||
<td className="d-none d-xl-table-cell" data-label="Einkauf">
|
||||
{formatEuro(zeile.einkaufBetrag)}
|
||||
</td>
|
||||
<td className="text-center" data-label="Einkauf bezahlt">
|
||||
<StatusHaken
|
||||
laufzeitId={zeile.id}
|
||||
hostId={zeile.hostId}
|
||||
@@ -273,11 +281,13 @@ export default async function RechnungenSeite({ searchParams }: { searchParams:
|
||||
titel="Proxmox-Rechnung bezahlt"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<td data-label="Kundenrechnung">
|
||||
<CopyableCode wert={zeile.verkaufNummer} />
|
||||
</td>
|
||||
<td className="d-none d-xl-table-cell">{formatEuro(zeile.verkaufBetrag)}</td>
|
||||
<td className="text-center">
|
||||
<td className="d-none d-xl-table-cell" data-label="Verkauf">
|
||||
{formatEuro(zeile.verkaufBetrag)}
|
||||
</td>
|
||||
<td className="text-center" data-label="Rechnung erstellt">
|
||||
<StatusHaken
|
||||
laufzeitId={zeile.id}
|
||||
hostId={zeile.hostId}
|
||||
@@ -286,7 +296,7 @@ export default async function RechnungenSeite({ searchParams }: { searchParams:
|
||||
titel="Kundenrechnung erstellt"
|
||||
/>
|
||||
</td>
|
||||
<td className="text-center">
|
||||
<td className="text-center" data-label="Vom Kunden bezahlt">
|
||||
<StatusHaken
|
||||
laufzeitId={zeile.id}
|
||||
hostId={zeile.hostId}
|
||||
@@ -297,6 +307,7 @@ export default async function RechnungenSeite({ searchParams }: { searchParams:
|
||||
/>
|
||||
</td>
|
||||
<td
|
||||
data-label="Marge"
|
||||
className={`d-none d-xl-table-cell ${(zeile.marge ?? 0) >= 0 ? "pl-text-success" : "pl-text-danger"}`}
|
||||
>
|
||||
{zeile.marge === null ? "–" : formatEuro(zeile.marge)}
|
||||
@@ -306,7 +317,7 @@ export default async function RechnungenSeite({ searchParams }: { searchParams:
|
||||
})}
|
||||
{zeilen.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={11} className="text-center text-body-secondary py-4">
|
||||
<td colSpan={12} className="text-center text-body-secondary py-4">
|
||||
Nichts gefunden. <Link href="/rechnungen?status=alle">Alle anzeigen</Link>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -382,10 +382,128 @@ body {
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
/* Zeitleiste der Jahresbahn: Breiten als Klassen, damit sie auf dem Telefon kleiner ausfallen. */
|
||||
.pl-timeline {
|
||||
min-width: 820px;
|
||||
}
|
||||
|
||||
.pl-timeline-kopf {
|
||||
margin-left: 13rem;
|
||||
}
|
||||
|
||||
.pl-timeline-name {
|
||||
width: 13rem;
|
||||
}
|
||||
|
||||
/* ---------- Mobil ---------- */
|
||||
|
||||
@media (max-width: 575.98px) {
|
||||
.pl-touch-target {
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
/*
|
||||
* Ein Kontrollkästchen darf nicht selbst 44 px hoch werden – sonst steht in der Zeile ein
|
||||
* großer leerer Kasten. Die Fläche zum Treffen bekommt stattdessen sein Umfeld (#035).
|
||||
*/
|
||||
.pl-haken {
|
||||
min-width: 44px;
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
.pl-haken .form-check-input {
|
||||
width: 1.4rem;
|
||||
height: 1.4rem;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Breite Tabellen auf dem Telefon: Jede Zeile wird zur Karte, jede Zelle bekommt ihre
|
||||
* Beschriftung aus `data-label` davor. Das ersetzt das Scrollen nach rechts, bei dem die
|
||||
* Hälfte der Angaben unsichtbar bleibt (#035).
|
||||
*
|
||||
* Zellen ohne `data-label` stehen für sich – etwa eine Zeile mit Knöpfen.
|
||||
*/
|
||||
@media (max-width: 767.98px) {
|
||||
.pl-karten-tabelle,
|
||||
.pl-karten-tabelle tbody,
|
||||
.pl-karten-tabelle tr,
|
||||
.pl-karten-tabelle td {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pl-karten-tabelle thead {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pl-karten-tabelle tr {
|
||||
padding: 0.35rem 0.25rem;
|
||||
border-bottom: 1px solid var(--bs-border-color);
|
||||
}
|
||||
|
||||
.pl-karten-tabelle tr:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.pl-karten-tabelle td {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
padding: 0.3rem 0.75rem;
|
||||
text-align: right;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.pl-karten-tabelle td::before {
|
||||
flex: 0 0 auto;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--bs-secondary-color);
|
||||
text-align: left;
|
||||
content: attr(data-label);
|
||||
}
|
||||
|
||||
/* Die erste Zelle trägt den Namen – sie steht als Überschrift der Karte für sich. */
|
||||
.pl-karten-tabelle td.pl-karten-kopf {
|
||||
display: block;
|
||||
padding-top: 0.6rem;
|
||||
font-size: 1.0625rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.pl-karten-tabelle td.pl-karten-kopf::before {
|
||||
content: none;
|
||||
}
|
||||
|
||||
/* Lange Namen und Schlüssel sollen umbrechen, aber nicht Buchstabe für Buchstabe. */
|
||||
.pl-karten-tabelle td a,
|
||||
.pl-karten-tabelle td .pl-mono {
|
||||
overflow-wrap: anywhere;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
/* Auf dem Telefon bleibt mehr Platz für die Monate, wenn die Namensspalte schrumpft. */
|
||||
.pl-timeline {
|
||||
min-width: 560px;
|
||||
}
|
||||
|
||||
.pl-timeline-kopf {
|
||||
margin-left: 7rem;
|
||||
}
|
||||
|
||||
.pl-timeline-name {
|
||||
width: 7rem;
|
||||
}
|
||||
|
||||
/* Kartenkopf mit Knöpfen: untereinander statt nebeneinander über den Rand hinaus. */
|
||||
.card-header {
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.card-header .card-tools {
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { hostKundeSetzen } from "@/server/actions/host-detail";
|
||||
|
||||
export interface KundeMitStandorten {
|
||||
id: string;
|
||||
name: string;
|
||||
standorte: { id: string; name: string }[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Kunde und Standort eines Hosts ändern (#036).
|
||||
*
|
||||
* Beides gehört in **ein** Formular: Die Standorte hängen am Kunden, und ein Formular, das nur den
|
||||
* Kunden sendet, würde den Standort beim Speichern mitlöschen – genau das passierte vorher.
|
||||
*/
|
||||
export function KundeStandortFormular({
|
||||
hostId,
|
||||
kunden,
|
||||
kundeId,
|
||||
standortId,
|
||||
}: {
|
||||
hostId: string;
|
||||
kunden: KundeMitStandorten[];
|
||||
kundeId: string | null;
|
||||
standortId: string | null;
|
||||
}) {
|
||||
const [gewaehlterKunde, setGewaehlterKunde] = useState(kundeId ?? "");
|
||||
const standorte = kunden.find((kunde) => kunde.id === gewaehlterKunde)?.standorte ?? [];
|
||||
|
||||
return (
|
||||
<form action={hostKundeSetzen} className="mt-3">
|
||||
<input type="hidden" name="hostId" value={hostId} />
|
||||
|
||||
<label className="form-label small" htmlFor="kundeId">
|
||||
Kunde ändern
|
||||
</label>
|
||||
<select
|
||||
id="kundeId"
|
||||
name="kundeId"
|
||||
className="form-select form-select-sm mb-2"
|
||||
value={gewaehlterKunde}
|
||||
onChange={(e) => setGewaehlterKunde(e.target.value)}
|
||||
>
|
||||
<option value="">– nicht zugeordnet –</option>
|
||||
{kunden.map((kunde) => (
|
||||
<option key={kunde.id} value={kunde.id}>
|
||||
{kunde.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<label className="form-label small" htmlFor="standortId">
|
||||
Standort
|
||||
</label>
|
||||
<div className="input-group input-group-sm">
|
||||
<select
|
||||
id="standortId"
|
||||
name="standortId"
|
||||
className="form-select"
|
||||
defaultValue={standortId ?? ""}
|
||||
disabled={standorte.length === 0}
|
||||
// `key` erzwingt ein frisches Feld, wenn der Kunde wechselt – sonst bliebe die alte Auswahl stehen.
|
||||
key={gewaehlterKunde}
|
||||
>
|
||||
<option value="">– keiner –</option>
|
||||
{standorte.map((standort) => (
|
||||
<option key={standort.id} value={standort.id}>
|
||||
{standort.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button type="submit" className="btn btn-outline-primary">
|
||||
Setzen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{gewaehlterKunde && standorte.length === 0 ? (
|
||||
<div className="form-text">
|
||||
Für diesen Kunden gibt es noch keinen Standort. Anlegen lässt er sich auf der Kundenseite.
|
||||
</div>
|
||||
) : null}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -37,7 +37,7 @@ export function StatusHaken({
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="d-inline-flex align-items-center justify-content-center">
|
||||
<span className="pl-haken d-inline-flex align-items-center justify-content-center">
|
||||
{laeuft ? (
|
||||
<span
|
||||
className="spinner-border spinner-border-sm text-body-secondary"
|
||||
@@ -47,7 +47,7 @@ export function StatusHaken({
|
||||
) : (
|
||||
<input
|
||||
type="checkbox"
|
||||
className="form-check-input pl-touch-target"
|
||||
className="form-check-input"
|
||||
checked={wert}
|
||||
disabled={deaktiviert}
|
||||
onChange={(e) => umschalten(e.target.checked)}
|
||||
|
||||
@@ -57,8 +57,24 @@ export async function hostKundeSetzen(formular: FormData) {
|
||||
const standortId = formular.get("standortId")?.toString() || null;
|
||||
if (!hostId) return;
|
||||
|
||||
await prisma.host.update({ where: { id: hostId }, data: { kundeId, standortId } });
|
||||
await protokolliere("host.zugewiesen", "Host", hostId, { kundeId, standortId });
|
||||
// Ein Standort gehört immer zu einem Kunden. Passt beides nicht zusammen – etwa weil der Kunde
|
||||
// gewechselt hat –, wird der Standort verworfen statt eine falsche Zuordnung zu speichern.
|
||||
const gueltigerStandort =
|
||||
standortId && kundeId
|
||||
? ((await prisma.standort.findFirst({
|
||||
where: { id: standortId, kundeId },
|
||||
select: { id: true },
|
||||
})) ?? null)
|
||||
: null;
|
||||
|
||||
await prisma.host.update({
|
||||
where: { id: hostId },
|
||||
data: { kundeId, standortId: gueltigerStandort?.id ?? null },
|
||||
});
|
||||
await protokolliere("host.zugewiesen", "Host", hostId, {
|
||||
kundeId,
|
||||
standortId: gueltigerStandort?.id ?? null,
|
||||
});
|
||||
|
||||
if (kundeId) {
|
||||
await prisma.alert.updateMany({
|
||||
|
||||
Reference in New Issue
Block a user