feat(schnellstart): Kacheln je Geraetetyp in der Oberflaeche pflegen #44
@@ -26,6 +26,12 @@ und dieses Projekt folgt [Semantic Versioning](https://semver.org/lang/de/).
|
|||||||
|
|
||||||
### Hinzugefügt
|
### Hinzugefügt
|
||||||
|
|
||||||
|
- **Schnellstart je Gerätetyp pflegen**, unter Verwaltung → Schnellstart. Welche Vorlagen auf der
|
||||||
|
Geräteseite als Kachel erscheinen, stand bisher nur im Seed: wer eine eigene Vorlage dort haben
|
||||||
|
wollte, kam nicht heran, und wer eine mitgelieferte loswerden wollte, auch nicht. Zur Auswahl
|
||||||
|
stehen nur veröffentlichte, nicht archivierte Vorlagen; die Reihenfolge der Liste ist die
|
||||||
|
Reihenfolge der Kacheln. Sobald jemand hier gespeichert hat, legt der Seed keine eigenen Vorgaben
|
||||||
|
mehr nach — eine entfernte Kachel soll nicht beim nächsten Einrichtungslauf zurückkommen (#022)
|
||||||
- **Vorgesetzter auch für von Hand angelegte Personen.** Die Zuordnung lief allein über die Kennung
|
- **Vorgesetzter auch für von Hand angelegte Personen.** Die Zuordnung lief allein über die Kennung
|
||||||
des Verzeichnisses, und die ist nur innerhalb einer Verzeichnisquelle eindeutig: wer eine Person
|
des Verzeichnisses, und die ist nur innerhalb einer Verzeichnisquelle eindeutig: wer eine Person
|
||||||
selbst eingetragen hatte, konnte ihr keinen Vorgesetzten geben, und `{{vorgesetzter_sam}}` blieb im
|
selbst eingetragen hatte, konnte ihr keinen Vorgesetzten geben, und `{{vorgesetzter_sam}}` blieb im
|
||||||
@@ -229,6 +235,10 @@ und dieses Projekt folgt [Semantic Versioning](https://semver.org/lang/de/).
|
|||||||
|
|
||||||
### Behoben
|
### Behoben
|
||||||
|
|
||||||
|
- Die Umsortierung im Schnellstart wurde nicht gespeichert, wenn mehrere Zeilen desselben
|
||||||
|
Gerätetyps dieselbe `position` trugen — bei gleicher Position ist die Reihenfolge in Postgres nicht
|
||||||
|
festgelegt, und Lesemodell und Dienst gaben die Zeilen unterschiedlich zurück. Gefunden beim
|
||||||
|
Durchklicken des gebauten Stands, nicht von der Testsuite (#022)
|
||||||
- Der Compose-Stand las `DATABASE_URL` aus der `.env` — dort steht die Adresse für die Entwicklung
|
- Der Compose-Stand las `DATABASE_URL` aus der `.env` — dort steht die Adresse für die Entwicklung
|
||||||
(`localhost` und ein offener Port), und im Container zeigt sie ins Leere. Wer den dokumentierten
|
(`localhost` und ein offener Port), und im Container zeigt sie ins Leere. Wer den dokumentierten
|
||||||
Weg ging, bekam einen Stand, der nicht startet. Die Adresse baut sich jetzt immer aus den
|
Weg ging, bekam einen Stand, der nicht startet. Die Adresse baut sich jetzt immer aus den
|
||||||
|
|||||||
@@ -136,8 +136,9 @@ sofort anfangen? Was war schon?
|
|||||||
den Fall, dass der Aufkleber abgerissen ist.
|
den Fall, dass der Aufkleber abgerissen ist.
|
||||||
- **Zugewiesen** — was an diesem Gerät wartet. Ein offener Lauf hat den Knopf **Starten**: er setzt
|
- **Zugewiesen** — was an diesem Gerät wartet. Ein offener Lauf hat den Knopf **Starten**: er setzt
|
||||||
ihn auf „in Arbeit“ und führt hinein. Ein laufender hat **Weiter**, ein fertiger **Ansehen**.
|
ihn auf „in Arbeit“ und führt hinein. Ein laufender hat **Weiter**, ein fertiger **Ansehen**.
|
||||||
- **Schnellstart** — eine Kachel je Vorlage, die für diesen Gerätetyp hinterlegt ist. Ein Tipp legt
|
- **Schnellstart** — eine Kachel je Vorlage, die für diesen Gerätetyp hinterlegt ist (gepflegt unter
|
||||||
den Lauf mit diesem Gerät und seinem Besitzer an und beginnt ihn, ohne eine einzige Eingabe.
|
**Verwaltung → Schnellstart**, siehe Kapitel 07). Ein Tipp legt den Lauf mit diesem Gerät und
|
||||||
|
seinem Besitzer an und beginnt ihn, ohne eine einzige Eingabe.
|
||||||
Steht auf der Kachel „fragt noch nach Angaben“, führt sie stattdessen in den Start-Dialog — dort
|
Steht auf der Kachel „fragt noch nach Angaben“, führt sie stattdessen in den Start-Dialog — dort
|
||||||
sind Gerät und Person schon vorbelegt.
|
sind Gerät und Person schon vorbelegt.
|
||||||
- **Vorlage zuweisen** — für später oder für jemand anderen: Vorlage, **Techniker (optional)**,
|
- **Vorlage zuweisen** — für später oder für jemand anderen: Vorlage, **Techniker (optional)**,
|
||||||
|
|||||||
@@ -231,9 +231,9 @@ den Fall, dass jemand eine Vorlage von Hand schreiben oder ein Skript dafür bau
|
|||||||
## Vorlagen einem Gerätetyp zuordnen
|
## Vorlagen einem Gerätetyp zuordnen
|
||||||
|
|
||||||
Damit auf der Geräteseite ein **Schnellstart** erscheint, muss für den Gerätetyp eine Vorlage
|
Damit auf der Geräteseite ein **Schnellstart** erscheint, muss für den Gerätetyp eine Vorlage
|
||||||
hinterlegt sein — etwa „PC Neuinstallation“ für den Typ PC. Diese Zuordnung wird beim Einrichten der
|
hinterlegt sein — etwa „PC Neuinstallation“ für den Typ PC. Gepflegt wird das unter
|
||||||
Anwendung gesetzt und lässt sich in der Oberfläche noch nicht ändern; wende dich dafür an die
|
**Verwaltung → Schnellstart**; das verlangt die Rolle Administrator und ist in Kapitel 07
|
||||||
Person, die Laufzettel betreibt.
|
beschrieben.
|
||||||
|
|
||||||
Ohne Zuordnung bleibt der Schnellstart leer, und das ist kein Mangel: unter **Vorlage zuweisen**
|
Ohne Zuordnung bleibt der Schnellstart leer, und das ist kein Mangel: unter **Vorlage zuweisen**
|
||||||
steht auf der Geräteseite jede Vorlage, die zu diesem Gerät passt, und der Start-Dialog kennt sie
|
steht auf der Geräteseite jede Vorlage, die zu diesem Gerät passt, und der Start-Dialog kennt sie
|
||||||
|
|||||||
@@ -88,6 +88,33 @@ Aufkleber sonst ins Leere zeigen würden. Steht dort „APP_URL ist nicht gesetz
|
|||||||
noch nicht gedruckt werden“, muss das erst beim Betrieb der Anwendung eingetragen werden. **Drucke
|
noch nicht gedruckt werden“, muss das erst beim Betrieb der Anwendung eingetragen werden. **Drucke
|
||||||
vorher keine Etiketten.**
|
vorher keine Etiketten.**
|
||||||
|
|
||||||
|
## Schnellstart je Gerätetyp
|
||||||
|
|
||||||
|
Auf der Seite eines Geräts stehen oben die Vorlagen, die dort am häufigsten gebraucht werden — ein
|
||||||
|
Klick, und der Lauf steht. Welche das sind, wird unter **Verwaltung → Schnellstart** festgelegt, je
|
||||||
|
Gerätetyp getrennt.
|
||||||
|
|
||||||
|
Zu jedem Gerätetyp steht dort die Liste in der Reihenfolge, in der die Kacheln erscheinen. Mit den
|
||||||
|
Pfeilen wird umsortiert, mit dem **×** entfernt, und über **Vorlage hinzufügen** kommt eine dazu.
|
||||||
|
Nach jeder Änderung steht „nicht gespeichert“ daneben, bis **Speichern** gedrückt ist — die
|
||||||
|
Reihenfolge wandert als Ganzes in die Datenbank, nicht Zeile für Zeile.
|
||||||
|
|
||||||
|
Drei Dinge, die hier anders sind als erwartet:
|
||||||
|
|
||||||
|
- **Zur Auswahl stehen nur veröffentlichte, nicht archivierte Vorlagen.** Ein Entwurf taugt nicht:
|
||||||
|
aus ihm lässt sich kein Lauf starten, die Kachel täte also nichts.
|
||||||
|
- **Eine archivierte Vorlage verschwindet von selbst** aus dem Schnellstart. Niemand muss hier
|
||||||
|
aufräumen, nachdem eine Vorlage weggelegt wurde.
|
||||||
|
- **Eine Vorlage, die nur für einen Kunden gilt**, lässt sich zuweisen und wird mit dem Kundennamen
|
||||||
|
gezeigt. Sie erscheint trotzdem nur bei den Geräten dieses Kunden.
|
||||||
|
|
||||||
|
**Ein leerer Schnellstart ist kein Mangel.** Unter **Vorlage zuweisen** steht auf der Geräteseite
|
||||||
|
jede Vorlage, die zu diesem Gerät passt, und der Start-Dialog kennt sie ohnehin alle.
|
||||||
|
|
||||||
|
Sobald hier einmal gespeichert wurde, hält sich der Seed heraus: die mitgelieferten Vorgaben werden
|
||||||
|
danach nicht mehr nachgelegt. Das ist gewollt — eine entfernte Kachel soll nicht beim nächsten
|
||||||
|
Einrichtungslauf zurückkommen.
|
||||||
|
|
||||||
## Betriebsmodus umstellen
|
## Betriebsmodus umstellen
|
||||||
|
|
||||||

|

|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
# #022 Schnellstart je Gerätetyp pflegen
|
# #022 Schnellstart je Gerätetyp pflegen
|
||||||
|
|
||||||
**Status:** offen
|
**Status:** erledigt
|
||||||
**Im Tracker:** [#34](https://git.wilde-server.de/florian.wilde/Laufzettel/issues/34)
|
**Im Tracker:** [#34](https://git.wilde-server.de/florian.wilde/Laufzettel/issues/34)
|
||||||
**Meilenstein:** v0.2.0
|
**Meilenstein:** v0.2.0
|
||||||
**Labels:** phase-2, frontend, backend
|
**Labels:** phase-2, frontend, backend
|
||||||
**Geschätzt:** 3 h **Tatsächlich:** –
|
**Geschätzt:** 3 h **Tatsächlich:** 1 h 10 min
|
||||||
|
|
||||||
## Ziel
|
## Ziel
|
||||||
|
|
||||||
@@ -18,17 +18,44 @@ den Schnellstart, konnte aber nicht sagen, wer ihn einrichtet.
|
|||||||
|
|
||||||
## Aufgaben
|
## Aufgaben
|
||||||
|
|
||||||
- [ ] Admin-Seite (oder ein Abschnitt unter Admin → Einstellungen): je Gerätetyp eine sortierbare
|
- [x] Eigene Seite **Verwaltung → Schnellstart** (kein Abschnitt unter Einstellungen: acht Gerätetypen
|
||||||
Liste der Vorlagen, die dort erscheinen sollen
|
mit je einer Liste sind zu viel für eine Seite, die schon den Moduswechsel trägt)
|
||||||
- [ ] Nur **veröffentlichte**, nicht archivierte Vorlagen zur Auswahl; die Reihenfolge ist die
|
- [x] Nur **veröffentlichte**, nicht archivierte Vorlagen zur Auswahl; die Reihenfolge ist die
|
||||||
Reihenfolge der Kacheln (`position`)
|
Reihenfolge der Kacheln (`position`)
|
||||||
- [ ] Recht `verwaltung`, Änderung ins `Protokoll`
|
- [x] Recht `verwaltung`, Änderung ins `Protokoll`
|
||||||
- [ ] Der Seed legt die Vorgaben weiter an, überschreibt aber nie eine im Betrieb geänderte
|
- [x] Der Seed legt die Vorgaben weiter an, überschreibt aber nie eine im Betrieb geänderte
|
||||||
Zuordnung — dieselbe Regel wie für alles andere im Seed
|
Zuordnung — dieselbe Regel wie für alles andere im Seed
|
||||||
- [ ] Anleitung Kapitel 03 und 07 nachziehen
|
- [x] Anleitung Kapitel 03 und 07 nachziehen
|
||||||
|
|
||||||
## Akzeptanzkriterien
|
## Akzeptanzkriterien
|
||||||
|
|
||||||
- [ ] Test: eine zugewiesene Vorlage erscheint als Kachel auf der Geräteseite des passenden Typs
|
- [x] Test: eine zugewiesene Vorlage erscheint als Kachel auf der Geräteseite des passenden Typs
|
||||||
- [ ] Test: eine archivierte Vorlage verschwindet aus dem Schnellstart, ohne dass jemand aufräumt
|
- [x] Test: eine archivierte Vorlage verschwindet aus dem Schnellstart, ohne dass jemand aufräumt
|
||||||
- [ ] Test: `LESER` und `TECHNIKER` dürfen nicht ändern
|
- [x] Test: `LESER` und `TECHNIKER` dürfen nicht ändern
|
||||||
|
|
||||||
|
## Beim Umsetzen dazugekommen
|
||||||
|
|
||||||
|
- **Die Reihenfolge läuft über Hoch und Runter, nicht über Drag & Drop.** Der Vorlagen-Editor
|
||||||
|
benutzt dnd-kit, weil dort hundert Schritte in Abschnitten liegen; hier sind es zwei bis vier
|
||||||
|
Kacheln, und zwei Knöpfe sind am Handy verlässlicher als eine Ziehgeste neben einer scrollenden
|
||||||
|
Seite.
|
||||||
|
- **`Einstellung.schnellstartGepflegt` als Marke.** Eine fehlende Zeile in `GeraetetypVorlage` sagt
|
||||||
|
nicht, ob sie noch nie da war oder bewusst entfernt wurde — ohne Marke käme eine entfernte Kachel
|
||||||
|
beim nächsten Seed-Lauf zurück. Gesetzt wird sie **auch bei unverändert gespeicherter Liste**: das
|
||||||
|
Speichern selbst ist die Aussage „ich pflege das jetzt“.
|
||||||
|
- **Gespeichert wird die ganze Liste, nicht Zeile für Zeile.** `position` kommt aus der Reihenfolge
|
||||||
|
des Formulars; zwei Wahrheiten über die Reihenfolge gäbe es sonst.
|
||||||
|
- **Das Durchklicken des gebauten Stands hat einen echten Fehler gefunden**, den die Suite nicht
|
||||||
|
gesehen hätte: die Umsortierung wurde nicht gespeichert. Altdaten tragen mehrere Zeilen auf
|
||||||
|
`position` 0 — der Seed zählt je Vorgabe von 0, und ein Beispielskript legte eine zweite dazu. Bei
|
||||||
|
gleicher Position ist die Reihenfolge in Postgres nicht festgelegt, und das Lesemodell gab die
|
||||||
|
Zeilen anders zurück als der Dienst. Der Vergleich „unverändert" hielt die Umsortierung deshalb
|
||||||
|
für einen Leerlauf: die Marke wurde gesetzt, geschrieben wurde nichts. Behoben durch einen
|
||||||
|
eindeutigen Tiebreak (`position`, dann `vorlageId`) in **beiden** Abfragen.
|
||||||
|
- Dabei fiel auf, dass ich die Ursache zuerst falsch benannt hatte: eine zusätzliche Prüfung auf
|
||||||
|
aufeinanderfolgende Positionen behebt den Fehler **nicht** (Gegenprobe gefahren, Tests blieben
|
||||||
|
grün). Sie zieht nur alte Positionen gerade; der Kommentar sagt das jetzt so, und ein eigener Test
|
||||||
|
deckt genau das ab.
|
||||||
|
- Zwei der drei Akzeptanzkriterien waren schon durch `tests/geraeteseite.test.ts` gedeckt — die
|
||||||
|
Kachel am passenden Typ und das Verschwinden einer archivierten Vorlage. Das stand bei
|
||||||
|
[#014](014-qr-tags-geraeteseite.md) schon richtig im Leseweg; gefehlt hat nur die Pflege.
|
||||||
|
|||||||
@@ -26,10 +26,10 @@ Diese Dateien sind die Quelle; `pnpm issues:sync` spiegelt sie in den Gitea-Trac
|
|||||||
| #019 | [M14 – Betriebswege und Release v0.1.0](019-betrieb-release.md) | erledigt | v0.1.0 | 10 h | 5 h 25 min |
|
| #019 | [M14 – Betriebswege und Release v0.1.0](019-betrieb-release.md) | erledigt | v0.1.0 | 10 h | 5 h 25 min |
|
||||||
| #020 | [Vorgesetzter auch für von Hand angelegte Personen](020-vorgesetzter-manuell.md) | erledigt | v0.2.0 | 3 h | 1 h 20 min |
|
| #020 | [Vorgesetzter auch für von Hand angelegte Personen](020-vorgesetzter-manuell.md) | erledigt | v0.2.0 | 3 h | 1 h 20 min |
|
||||||
| #021 | [Stabiler Schlüssel für Seed-Vorlagen](021-stabiler-schluessel-vorlagen.md) | erledigt | v0.2.0 | 2 h | 55 min |
|
| #021 | [Stabiler Schlüssel für Seed-Vorlagen](021-stabiler-schluessel-vorlagen.md) | erledigt | v0.2.0 | 2 h | 55 min |
|
||||||
| #022 | [Schnellstart je Gerätetyp pflegen](022-schnellstart-pflegen.md) | offen | v0.2.0 | 3 h | – |
|
| #022 | [Schnellstart je Gerätetyp pflegen](022-schnellstart-pflegen.md) | erledigt | v0.2.0 | 3 h | 1 h 10 min |
|
||||||
| #023 | [Protokoll ansehen](023-protokoll-ansicht.md) | offen | v0.2.0 | 4 h | – |
|
| #023 | [Protokoll ansehen](023-protokoll-ansicht.md) | offen | v0.2.0 | 4 h | – |
|
||||||
| #024 | [Proxmox-Einzeiler auf einem echten PVE-Host gegenprüfen](024-proxmox-echtlauf.md) | offen | v0.2.0 | 2 h | – |
|
| #024 | [Proxmox-Einzeiler auf einem echten PVE-Host gegenprüfen](024-proxmox-echtlauf.md) | offen | v0.2.0 | 2 h | – |
|
||||||
| #025 | [Healthcheck gibt die rohe Datenbankfehlermeldung heraus](025-gesundheit-fehlermeldung.md) | erledigt | v0.2.0 | 1 h | 25 min |
|
| #025 | [Healthcheck gibt die rohe Datenbankfehlermeldung heraus](025-gesundheit-fehlermeldung.md) | erledigt | v0.2.0 | 1 h | 25 min |
|
||||||
| #026 | [Zwei Konventionen präzisieren, die strenger klingen als sie gemeint sind](026-konventionen-praezisieren.md) | erledigt | v0.2.0 | 1 h | 20 min |
|
| #026 | [Zwei Konventionen präzisieren, die strenger klingen als sie gemeint sind](026-konventionen-praezisieren.md) | erledigt | v0.2.0 | 1 h | 20 min |
|
||||||
| #027 | [Die Editor-Abläufe lassen ihre Vorlagen stehen](027-e2e-reste-vorlagen.md) | offen | v0.2.0 | 1 h | – |
|
| #027 | [Die Editor-Abläufe lassen ihre Vorlagen stehen](027-e2e-reste-vorlagen.md) | offen | v0.2.0 | 1 h | – |
|
||||||
| | **Summe** | | | **153 h** | **38 h 20 min** |
|
| | **Summe** | | | **153 h** | **39 h 30 min** |
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- HANDGESCHRIEBEN. Prisma erzeugt hier zusaetzlich Loeschbefehle fuer die
|
||||||
|
-- zusammengesetzten Fremdschluessel (Mandantengrenze), die Trigramm-Indizes
|
||||||
|
-- (Archivsuche), die partiellen eindeutigen Indizes und die Trigger auf
|
||||||
|
-- veroeffentlichten Versionen: es kennt sie nicht, weil sie sich im Schema nicht
|
||||||
|
-- ausdruecken lassen. Diese Loeschbefehle sind bewusst gestrichen.
|
||||||
|
--
|
||||||
|
-- tests/datenbankregeln.test.ts prueft, dass alle diese Objekte vorhanden sind –
|
||||||
|
-- ein versehentlich stehengelassener Loeschbefehl faellt dort sofort auf.
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- Marke fuer den Schnellstart (#022).
|
||||||
|
--
|
||||||
|
-- Der Seed legt Vorgaben je Geraetetyp an. Ohne diese Marke kaeme eine in der
|
||||||
|
-- Oberflaeche entfernte Zuordnung beim naechsten Lauf zurueck: eine fehlende
|
||||||
|
-- Zeile sagt nicht, ob sie noch nie da war oder bewusst entfernt wurde.
|
||||||
|
--
|
||||||
|
-- Bestehende Installationen bekommen `false`. Das ist richtig: dort hat noch
|
||||||
|
-- niemand den Schnellstart in der Oberflaeche gepflegt, weil es sie nicht gab.
|
||||||
|
ALTER TABLE "Einstellung"
|
||||||
|
ADD COLUMN "schnellstartGepflegt" BOOLEAN NOT NULL DEFAULT false;
|
||||||
+11
-6
@@ -98,15 +98,20 @@ enum Herkunft {
|
|||||||
/// der Datenbank und nicht in der Umgebung, damit der Admin ohne Neustart
|
/// der Datenbank und nicht in der Umgebung, damit der Admin ohne Neustart
|
||||||
/// umschalten kann.
|
/// umschalten kann.
|
||||||
model Einstellung {
|
model Einstellung {
|
||||||
id String @id @default("global")
|
id String @id @default("global")
|
||||||
modus Betriebsmodus @default(INTERN)
|
modus Betriebsmodus @default(INTERN)
|
||||||
/// Globale Namensregeln fuer upn, sam, mail und hostname. Im Modus
|
/// Globale Namensregeln fuer upn, sam, mail und hostname. Im Modus
|
||||||
/// DIENSTLEISTUNG koennen Kunden eigene Regeln haben, die diese ueberlagern.
|
/// DIENSTLEISTUNG koennen Kunden eigene Regeln haben, die diese ueberlagern.
|
||||||
namensregeln Json @default("{}")
|
namensregeln Json @default("{}")
|
||||||
/// Ziel-OU fuer deaktivierte Konten, Vorbelegung im Offboarding.
|
/// Ziel-OU fuer deaktivierte Konten, Vorbelegung im Offboarding.
|
||||||
gesperrteOu String?
|
gesperrteOu String?
|
||||||
erstelltAm DateTime @default(now())
|
/// Sobald jemand den Schnellstart in der Oberflaeche gespeichert hat, laesst
|
||||||
geaendertAm DateTime @default(now()) @updatedAt
|
/// der Seed `GeraetetypVorlage` in Ruhe (#022). Ohne diese Marke kaeme eine
|
||||||
|
/// entfernte Zuordnung beim naechsten Seed-Lauf zurueck: eine fehlende Zeile
|
||||||
|
/// sagt nicht, ob sie noch nie da war oder bewusst entfernt wurde.
|
||||||
|
schnellstartGepflegt Boolean @default(false)
|
||||||
|
erstelltAm DateTime @default(now())
|
||||||
|
geaendertAm DateTime @default(now()) @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
model Kunde {
|
model Kunde {
|
||||||
|
|||||||
@@ -273,6 +273,21 @@ export async function typVorgabenSaeen(
|
|||||||
db: PrismaClient,
|
db: PrismaClient,
|
||||||
vorlagen: readonly GesaeteVorlage[],
|
vorlagen: readonly GesaeteVorlage[],
|
||||||
): Promise<number> {
|
): Promise<number> {
|
||||||
|
/*
|
||||||
|
* Hat jemand den Schnellstart in der Oberflaeche gespeichert, haelt der Seed
|
||||||
|
* sich heraus — auch bei einer Vorgabe, die dort fehlt (#022). Eine fehlende
|
||||||
|
* Zeile sagt nicht, ob sie noch nie da war oder bewusst entfernt wurde; ohne
|
||||||
|
* diese Marke kaeme eine entfernte Kachel beim naechsten Lauf zurueck.
|
||||||
|
*/
|
||||||
|
const einstellung = await db.einstellung.findUnique({
|
||||||
|
where: { id: "global" },
|
||||||
|
select: { schnellstartGepflegt: true },
|
||||||
|
});
|
||||||
|
if (einstellung?.schnellstartGepflegt) {
|
||||||
|
console.log(" Schnellstart je Geraetetyp: in der Oberflaeche gepflegt, bleibt unberührt");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
const nachName = new Map(vorlagen.map((eintrag) => [eintrag.name, eintrag.vorlageId]));
|
const nachName = new Map(vorlagen.map((eintrag) => [eintrag.name, eintrag.vorlageId]));
|
||||||
let angelegt = 0;
|
let angelegt = 0;
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const REITER = [
|
|||||||
{ pfad: "/admin", beschriftung: "Einstellungen" },
|
{ pfad: "/admin", beschriftung: "Einstellungen" },
|
||||||
{ pfad: "/admin/benutzer", beschriftung: "Benutzer" },
|
{ pfad: "/admin/benutzer", beschriftung: "Benutzer" },
|
||||||
{ pfad: "/admin/abteilungen", beschriftung: "Abteilungen" },
|
{ pfad: "/admin/abteilungen", beschriftung: "Abteilungen" },
|
||||||
|
{ pfad: "/admin/schnellstart", beschriftung: "Schnellstart" },
|
||||||
/*
|
/*
|
||||||
* Das Verzeichnis gibt es nur im Modus INTERN: dort hat das Haus **ein** AD,
|
* Das Verzeichnis gibt es nur im Modus INTERN: dort hat das Haus **ein** AD,
|
||||||
* aus dem seine Leute kommen. Im Modus DIENSTLEISTUNG waere es ein LDAP zu
|
* aus dem seine Leute kommen. Im Modus DIENSTLEISTUNG waere es ein LDAP zu
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { Rocket } from "lucide-react";
|
||||||
|
import { SchnellstartPflege } from "@/components/schnellstart-pflege";
|
||||||
|
import { sitzungErforderlich } from "@/server/auth/sitzungspflicht";
|
||||||
|
import { schnellstartPflege } from "@/server/queries/schnellstart";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schnellstart je Gerätetyp (#022).
|
||||||
|
*
|
||||||
|
* Welche Vorlagen auf der Geräteseite als Kachel erscheinen, stand bisher nur im
|
||||||
|
* Seed. Wer eine eigene Vorlage dort haben wollte, kam nicht heran.
|
||||||
|
*
|
||||||
|
* `verwaltung`, wie bei den Abteilungen: die Zuordnung gilt global und bestimmt,
|
||||||
|
* was jeder Techniker an jedem Gerät als erstes sieht.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default async function SchnellstartSeite() {
|
||||||
|
// Jede Seite prüft selbst – das Admin-Layout darüber ist das zweite Netz,
|
||||||
|
// nicht das erste (siehe CLAUDE.md).
|
||||||
|
await sitzungErforderlich("verwaltung", "/admin/schnellstart");
|
||||||
|
const { typen, auswahl } = await schnellstartPflege();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1 className="flex items-center gap-2 text-2xl font-bold">
|
||||||
|
<Rocket className="size-6 text-nebentext" strokeWidth={1.75} aria-hidden="true" />
|
||||||
|
Schnellstart
|
||||||
|
</h1>
|
||||||
|
<p className="mt-2 max-w-prose text-nebentext">
|
||||||
|
Auf der Seite eines Geräts stehen oben die Vorlagen, die dort am häufigsten gebraucht werden — ein
|
||||||
|
Klick, und der Lauf steht. Hier wird festgelegt, welche das je Gerätetyp sind und in welcher
|
||||||
|
Reihenfolge sie erscheinen.
|
||||||
|
</p>
|
||||||
|
<p className="mt-2 max-w-prose text-sm text-nebentext">
|
||||||
|
Zur Auswahl stehen nur veröffentlichte, nicht archivierte Vorlagen. Wird eine Vorlage später
|
||||||
|
archiviert, verschwindet sie von selbst aus dem Schnellstart — niemand muss hier aufräumen. Eine
|
||||||
|
Vorlage, die nur für einen Kunden gilt, erscheint auch nur bei dessen Geräten.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="mt-6 flex flex-col gap-4">
|
||||||
|
{typen.map((eintrag) => (
|
||||||
|
<SchnellstartPflege
|
||||||
|
key={eintrag.typ}
|
||||||
|
typ={eintrag.typ}
|
||||||
|
zugewiesen={eintrag.zugewiesen}
|
||||||
|
auswahl={auswahl}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useActionState, useState } from "react";
|
||||||
|
import { ArrowDown, ArrowUp, Plus, X } from "lucide-react";
|
||||||
|
import type { GeraeteTyp } from "@/generated/prisma/enums";
|
||||||
|
import { GERAETE_TYP_TEXT } from "@/lib/geraete-text";
|
||||||
|
import { schnellstartSpeichernAktion, type AktionsErgebnis } from "@/server/actions/schnellstart";
|
||||||
|
|
||||||
|
export type Eintrag = {
|
||||||
|
readonly vorlageId: string;
|
||||||
|
readonly name: string;
|
||||||
|
readonly kategorie: string | null;
|
||||||
|
readonly kundeName: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schnellstart eines Geraetetyps pflegen (#022).
|
||||||
|
*
|
||||||
|
* Die Reihenfolge laeuft ueber Hoch und Runter, nicht ueber Drag & Drop. Der
|
||||||
|
* Editor benutzt dnd-kit, weil dort hundert Schritte in Abschnitten liegen; hier
|
||||||
|
* sind es zwei bis vier Kacheln, und zwei Knoepfe sind am Handy verlaesslicher
|
||||||
|
* als eine Ziehgeste neben einer scrollenden Seite.
|
||||||
|
*
|
||||||
|
* Gespeichert wird die **ganze** Liste als ein Feld, eine Kennung je Zeile. So
|
||||||
|
* gibt es eine Wahrheit ueber die Reihenfolge: die des Formulars.
|
||||||
|
*/
|
||||||
|
export function SchnellstartPflege({
|
||||||
|
typ,
|
||||||
|
zugewiesen,
|
||||||
|
auswahl,
|
||||||
|
}: {
|
||||||
|
typ: GeraeteTyp;
|
||||||
|
zugewiesen: readonly Eintrag[];
|
||||||
|
auswahl: readonly Eintrag[];
|
||||||
|
}) {
|
||||||
|
const [liste, setListe] = useState<readonly Eintrag[]>(zugewiesen);
|
||||||
|
const [ergebnis, aktion, laeuft] = useActionState<AktionsErgebnis, FormData>(
|
||||||
|
schnellstartSpeichernAktion,
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
const offen = auswahl.filter(
|
||||||
|
(eintrag) => !liste.some((gewaehlt) => gewaehlt.vorlageId === eintrag.vorlageId),
|
||||||
|
);
|
||||||
|
|
||||||
|
function verschieben(stelle: number, richtung: -1 | 1) {
|
||||||
|
const ziel = stelle + richtung;
|
||||||
|
if (ziel < 0 || ziel >= liste.length) return;
|
||||||
|
const kopie = [...liste];
|
||||||
|
const [heraus] = kopie.splice(stelle, 1);
|
||||||
|
if (heraus) kopie.splice(ziel, 0, heraus);
|
||||||
|
setListe(kopie);
|
||||||
|
}
|
||||||
|
|
||||||
|
const geaendert =
|
||||||
|
liste.length !== zugewiesen.length ||
|
||||||
|
liste.some((eintrag, stelle) => eintrag.vorlageId !== zugewiesen[stelle]?.vorlageId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="rounded-karte border border-linie bg-flaeche p-5">
|
||||||
|
<h3 className="text-lg font-semibold">{GERAETE_TYP_TEXT[typ]}</h3>
|
||||||
|
|
||||||
|
{liste.length === 0 ? (
|
||||||
|
<p className="mt-2 text-sm text-nebentext">
|
||||||
|
Kein Schnellstart. Auf der Geräteseite erscheint dann nur „Vorlage zuweisen“.
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<ol className="mt-3 flex flex-col gap-2">
|
||||||
|
{liste.map((eintrag, stelle) => (
|
||||||
|
<li
|
||||||
|
key={eintrag.vorlageId}
|
||||||
|
className="flex flex-wrap items-center gap-2 border-t border-linie py-2 first:border-t-0"
|
||||||
|
>
|
||||||
|
<span className="mono w-7 shrink-0 text-xs text-nebentext">
|
||||||
|
{String(stelle + 1).padStart(2, "0")}
|
||||||
|
</span>
|
||||||
|
<span className="min-w-0 flex-1 break-words">{eintrag.name}</span>
|
||||||
|
{eintrag.kundeName ? (
|
||||||
|
<span className="rounded-pille bg-flaeche-hoch px-2.5 py-1 text-xs text-nebentext">
|
||||||
|
nur {eintrag.kundeName}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
<div className="flex shrink-0 items-center gap-1">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => verschieben(stelle, -1)}
|
||||||
|
disabled={stelle === 0}
|
||||||
|
aria-label={`${eintrag.name} nach oben`}
|
||||||
|
className="flex size-11 items-center justify-center rounded-pille text-nebentext hover:bg-flaeche-hoch hover:text-text disabled:opacity-40 md:size-10"
|
||||||
|
>
|
||||||
|
<ArrowUp className="size-4" strokeWidth={1.75} aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => verschieben(stelle, 1)}
|
||||||
|
disabled={stelle === liste.length - 1}
|
||||||
|
aria-label={`${eintrag.name} nach unten`}
|
||||||
|
className="flex size-11 items-center justify-center rounded-pille text-nebentext hover:bg-flaeche-hoch hover:text-text disabled:opacity-40 md:size-10"
|
||||||
|
>
|
||||||
|
<ArrowDown className="size-4" strokeWidth={1.75} aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setListe(liste.filter((andere) => andere.vorlageId !== eintrag.vorlageId))}
|
||||||
|
aria-label={`${eintrag.name} entfernen`}
|
||||||
|
className="flex size-11 items-center justify-center rounded-pille text-nebentext hover:bg-flaeche-hoch hover:text-fehler md:size-10"
|
||||||
|
>
|
||||||
|
<X className="size-4" strokeWidth={1.75} aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{offen.length > 0 ? (
|
||||||
|
<div className="mt-4 flex flex-wrap items-end gap-2">
|
||||||
|
<label className="flex min-w-0 flex-1 flex-col gap-1 text-sm">
|
||||||
|
<span className="text-nebentext">Vorlage hinzufügen</span>
|
||||||
|
<select
|
||||||
|
aria-label={`Vorlage für ${GERAETE_TYP_TEXT[typ]} hinzufügen`}
|
||||||
|
value=""
|
||||||
|
onChange={(ereignis) => {
|
||||||
|
const gewaehlt = offen.find((eintrag) => eintrag.vorlageId === ereignis.target.value);
|
||||||
|
if (gewaehlt) setListe([...liste, gewaehlt]);
|
||||||
|
}}
|
||||||
|
className="min-h-11 w-full rounded-pille border border-linie bg-flaeche-hoch px-4 text-sm md:min-h-10"
|
||||||
|
>
|
||||||
|
<option value="">auswählen …</option>
|
||||||
|
{offen.map((eintrag) => (
|
||||||
|
<option key={eintrag.vorlageId} value={eintrag.vorlageId}>
|
||||||
|
{eintrag.name}
|
||||||
|
{eintrag.kundeName ? ` (nur ${eintrag.kundeName})` : ""}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<Plus className="mb-3 size-4 text-nebentext" strokeWidth={1.75} aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<form action={aktion} className="mt-4 flex flex-wrap items-center gap-3">
|
||||||
|
<input type="hidden" name="geraeteTyp" value={typ} />
|
||||||
|
<input type="hidden" name="vorlageIds" value={liste.map((eintrag) => eintrag.vorlageId).join("\n")} />
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={laeuft}
|
||||||
|
className="min-h-11 rounded-pille bg-akzent px-4 text-sm font-semibold text-grund md:min-h-10"
|
||||||
|
>
|
||||||
|
Speichern
|
||||||
|
</button>
|
||||||
|
{geaendert ? <span className="text-sm text-faellig">nicht gespeichert</span> : null}
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{ergebnis.fehler ? (
|
||||||
|
<p role="alert" className="mt-2 max-w-prose text-sm text-fehler">
|
||||||
|
{ergebnis.fehler}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
{ergebnis.erfolg && !geaendert ? (
|
||||||
|
<p role="status" className="mt-2 text-sm text-erledigt">
|
||||||
|
{ergebnis.erfolg}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import { revalidatePath } from "next/cache";
|
||||||
|
import { z } from "@/lib/validierung";
|
||||||
|
import { GeraeteTyp } from "@/generated/prisma/enums";
|
||||||
|
import { RechteFehler, rechtErforderlich } from "@/server/auth/sitzungspflicht";
|
||||||
|
import { SchnellstartFehler, schnellstartSetzen } from "@/server/services/schnellstart";
|
||||||
|
|
||||||
|
/** Server Action für den Schnellstart (#022). Recht prüfen, Zod, Service rufen. */
|
||||||
|
|
||||||
|
export type AktionsErgebnis = { readonly fehler?: string; readonly erfolg?: string };
|
||||||
|
|
||||||
|
const Schema = z.object({
|
||||||
|
geraeteTyp: z.enum(GeraeteTyp),
|
||||||
|
/**
|
||||||
|
* Die ganze Liste in Anzeigereihenfolge, als ein Feld mit Zeilenumbrüchen.
|
||||||
|
*
|
||||||
|
* Eine leere Liste ist eine Angabe: „für diesen Typ gibt es keinen
|
||||||
|
* Schnellstart“. Deshalb kein `min(1)`.
|
||||||
|
*/
|
||||||
|
vorlageIds: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function schnellstartSpeichernAktion(
|
||||||
|
_vorher: AktionsErgebnis,
|
||||||
|
formular: FormData,
|
||||||
|
): Promise<AktionsErgebnis> {
|
||||||
|
try {
|
||||||
|
const { benutzer } = await rechtErforderlich("verwaltung");
|
||||||
|
|
||||||
|
const eingabe = Schema.safeParse({
|
||||||
|
geraeteTyp: formular.get("geraeteTyp"),
|
||||||
|
vorlageIds: formular.get("vorlageIds") ?? "",
|
||||||
|
});
|
||||||
|
if (!eingabe.success) return { fehler: "Die Eingabe ist unvollständig." };
|
||||||
|
|
||||||
|
const ids = eingabe.data.vorlageIds
|
||||||
|
.split("\n")
|
||||||
|
.map((zeile) => zeile.trim())
|
||||||
|
.filter((zeile) => zeile.length > 0);
|
||||||
|
|
||||||
|
await schnellstartSetzen(eingabe.data.geraeteTyp, ids, benutzer.id);
|
||||||
|
revalidatePath("/admin/schnellstart");
|
||||||
|
// Die Geräteseiten zeigen die Kacheln – ohne das bliebe dort der alte Stand.
|
||||||
|
revalidatePath("/geraete", "layout");
|
||||||
|
return { erfolg: "Gespeichert." };
|
||||||
|
} catch (fehler) {
|
||||||
|
if (fehler instanceof SchnellstartFehler) return { fehler: fehler.message };
|
||||||
|
if (fehler instanceof RechteFehler) {
|
||||||
|
return { fehler: "Dafür fehlt dir die Berechtigung." };
|
||||||
|
}
|
||||||
|
console.error(
|
||||||
|
"Schnellstart speichern fehlgeschlagen:",
|
||||||
|
fehler instanceof Error ? `${fehler.name}: ${fehler.message}` : "unbekannter Fehler",
|
||||||
|
);
|
||||||
|
return { fehler: "Der Schnellstart konnte nicht gespeichert werden." };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import type { GeraeteTyp } from "@/generated/prisma/enums";
|
||||||
|
import { VersionStatus } from "@/generated/prisma/enums";
|
||||||
|
import { prisma } from "@/lib/db";
|
||||||
|
import { GERAETE_TYP_LISTE } from "@/lib/geraete-text";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lesemodell fuer die Pflege des Schnellstarts (#022).
|
||||||
|
*
|
||||||
|
* `GeraetetypVorlage` ist global: die Zuordnung haengt am Geraetetyp, nicht am
|
||||||
|
* Kunden. Deshalb gibt es hier **keine** Mandantensicht — die Seite verlangt
|
||||||
|
* `verwaltung`, und wer die hat, sieht ohnehin die ganze Installation. Dass eine
|
||||||
|
* kundengebundene Vorlage trotzdem nur bei ihrem Kunden erscheint, entscheidet
|
||||||
|
* `vorlagenFuerGeraet` beim Lesen der Geraeteseite, nicht diese Pflege.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type SchnellstartEintrag = {
|
||||||
|
readonly vorlageId: string;
|
||||||
|
readonly name: string;
|
||||||
|
readonly kategorie: string | null;
|
||||||
|
/** Gesetzt, wenn die Vorlage nur fuer einen Kunden gilt. */
|
||||||
|
readonly kundeName: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SchnellstartTyp = {
|
||||||
|
readonly typ: GeraeteTyp;
|
||||||
|
readonly zugewiesen: readonly SchnellstartEintrag[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SchnellstartPflege = {
|
||||||
|
readonly typen: readonly SchnellstartTyp[];
|
||||||
|
/** Alles, was sich zuweisen laesst – veroeffentlicht und nicht archiviert. */
|
||||||
|
readonly auswahl: readonly SchnellstartEintrag[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function schnellstartPflege(): Promise<SchnellstartPflege> {
|
||||||
|
const [vorlagen, vorgaben] = await Promise.all([
|
||||||
|
prisma.vorlage.findMany({
|
||||||
|
where: {
|
||||||
|
archiviert: false,
|
||||||
|
// Ohne veroeffentlichte Version laesst sich kein Lauf starten; sie
|
||||||
|
// anzubieten waere eine Kachel, die nichts tut.
|
||||||
|
versionen: { some: { status: VersionStatus.VEROEFFENTLICHT } },
|
||||||
|
},
|
||||||
|
orderBy: [{ kategorie: "asc" }, { name: "asc" }],
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
kategorie: true,
|
||||||
|
kunde: { select: { name: true } },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
prisma.geraetetypVorlage.findMany({
|
||||||
|
// `vorlageId` als zweites Kriterium, damit die Reihenfolge **eindeutig**
|
||||||
|
// ist. Zwei Zeilen mit derselben `position` gibt es wirklich — der Seed
|
||||||
|
// zaehlt je Vorgabe von 0, und Altdaten tragen beides auf 0. Ohne diesen
|
||||||
|
// Tiebreak gab Postgres sie hier anders zurueck als im Dienst, und eine
|
||||||
|
// echte Umsortierung galt dort als "unveraendert" (#022, gefunden beim
|
||||||
|
// Durchklicken des gebauten Stands).
|
||||||
|
orderBy: [{ geraeteTyp: "asc" }, { position: "asc" }, { vorlageId: "asc" }],
|
||||||
|
select: { geraeteTyp: true, vorlageId: true },
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const nachId = new Map<string, SchnellstartEintrag>(
|
||||||
|
vorlagen.map((vorlage) => [
|
||||||
|
vorlage.id,
|
||||||
|
{
|
||||||
|
vorlageId: vorlage.id,
|
||||||
|
name: vorlage.name,
|
||||||
|
kategorie: vorlage.kategorie,
|
||||||
|
kundeName: vorlage.kunde?.name ?? null,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
typen: GERAETE_TYP_LISTE.map((typ) => ({
|
||||||
|
typ,
|
||||||
|
zugewiesen: vorgaben
|
||||||
|
.filter((vorgabe) => vorgabe.geraeteTyp === typ)
|
||||||
|
// Eine archivierte oder unveroeffentlichte Vorlage bleibt in der Tabelle
|
||||||
|
// stehen und verschwindet trotzdem aus der Liste — dieselbe Regel, die
|
||||||
|
// `vorlagenFuerGeraet` auf der Geraeteseite anwendet. Niemand muss
|
||||||
|
// aufraeumen, damit der Schnellstart stimmt.
|
||||||
|
.map((vorgabe) => nachId.get(vorgabe.vorlageId))
|
||||||
|
.filter((eintrag): eintrag is SchnellstartEintrag => eintrag !== undefined),
|
||||||
|
})),
|
||||||
|
auswahl: [...nachId.values()],
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import { GeraeteTyp, VersionStatus } from "@/generated/prisma/enums";
|
||||||
|
import { prisma } from "@/lib/db";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pflege des Schnellstarts je Geraetetyp (#022).
|
||||||
|
*
|
||||||
|
* Welche Vorlagen auf der Geraeteseite als Kachel erscheinen, stand bisher nur
|
||||||
|
* im Seed. Wer eine eigene Vorlage dort haben wollte, kam nicht heran; wer eine
|
||||||
|
* mitgelieferte loswerden wollte, auch nicht.
|
||||||
|
*
|
||||||
|
* Die Zuordnung ist global und traegt keine `kundeId` — sie haengt am Typ. Dass
|
||||||
|
* eine kundengebundene Vorlage nur bei ihrem Kunden erscheint, entscheidet
|
||||||
|
* `vorlagenFuerGeraet` beim Lesen.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class SchnellstartFehler extends Error {
|
||||||
|
constructor(meldung: string) {
|
||||||
|
super(meldung);
|
||||||
|
this.name = "SchnellstartFehler";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setzt die Liste eines Typs auf genau diese Vorlagen, in dieser Reihenfolge.
|
||||||
|
*
|
||||||
|
* Ersetzen und nicht ergaenzen: die Oberflaeche schickt die ganze Liste, und
|
||||||
|
* `position` kommt aus ihrer Reihenfolge. Zwei Wahrheiten ueber die Reihenfolge
|
||||||
|
* gaebe es sonst — eine im Formular und eine in der Spalte.
|
||||||
|
*/
|
||||||
|
export async function schnellstartSetzen(
|
||||||
|
geraeteTyp: GeraeteTyp,
|
||||||
|
vorlageIds: readonly string[],
|
||||||
|
durchBenutzerId: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const doppelt = vorlageIds.filter((id, stelle) => vorlageIds.indexOf(id) !== stelle);
|
||||||
|
if (doppelt.length > 0) {
|
||||||
|
throw new SchnellstartFehler("Eine Vorlage kann je Gerätetyp nur einmal im Schnellstart stehen.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vorlageIds.length > 0) {
|
||||||
|
const brauchbar = await prisma.vorlage.findMany({
|
||||||
|
where: {
|
||||||
|
id: { in: [...vorlageIds] },
|
||||||
|
archiviert: false,
|
||||||
|
versionen: { some: { status: VersionStatus.VEROEFFENTLICHT } },
|
||||||
|
},
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
if (brauchbar.length !== vorlageIds.length) {
|
||||||
|
throw new SchnellstartFehler(
|
||||||
|
"Mindestens eine der gewählten Vorlagen gibt es nicht mehr, ist archiviert oder hat keine " +
|
||||||
|
"veröffentlichte Version. Seite neu laden und noch einmal versuchen.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const vorher = await prisma.geraetetypVorlage.findMany({
|
||||||
|
where: { geraeteTyp },
|
||||||
|
// Derselbe Tiebreak wie im Lesemodell, und aus demselben Grund: bei
|
||||||
|
// gleicher `position` ist die Reihenfolge sonst nicht eindeutig, und der
|
||||||
|
// Vergleich unten haelt eine echte Umsortierung fuer einen Leerlauf.
|
||||||
|
orderBy: [{ position: "asc" }, { vorlageId: "asc" }],
|
||||||
|
select: { vorlageId: true, position: true, vorlage: { select: { name: true } } },
|
||||||
|
});
|
||||||
|
const vorherIds = vorher.map((eintrag) => eintrag.vorlageId);
|
||||||
|
/*
|
||||||
|
* Eine Liste mit gleichen Positionen gilt nicht als unveraendert, auch wenn
|
||||||
|
* dieselben Vorlagen in derselben Reihenfolge kommen: Altdaten tragen mehrere
|
||||||
|
* Zeilen auf `position` 0, und einmal Speichern soll sie geradeziehen.
|
||||||
|
*
|
||||||
|
* Das ist Aufraeumen, nicht die Behebung des Fehlers aus #022 — der lag am
|
||||||
|
* fehlenden Tiebreak in beiden Abfragen. Ohne ihn gab Postgres die Zeilen hier
|
||||||
|
* anders zurueck als im Lesemodell, und eine echte Umsortierung galt als
|
||||||
|
* Leerlauf.
|
||||||
|
*/
|
||||||
|
const positionenSauber = vorher.every((eintrag, stelle) => eintrag.position === stelle);
|
||||||
|
const unveraendert =
|
||||||
|
positionenSauber &&
|
||||||
|
vorherIds.length === vorlageIds.length &&
|
||||||
|
vorherIds.every((id, stelle) => id === vorlageIds[stelle]);
|
||||||
|
|
||||||
|
await prisma.$transaction(async (tx) => {
|
||||||
|
// Immer schreiben, auch bei unveraenderter Liste: die Marke ist der Punkt,
|
||||||
|
// an dem der Seed aufhoert, eigene Vorgaben nachzulegen. Wer die Seite
|
||||||
|
// einmal gespeichert hat, pflegt den Schnellstart selbst (#022).
|
||||||
|
await tx.einstellung.update({
|
||||||
|
where: { id: "global" },
|
||||||
|
data: { schnellstartGepflegt: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (unveraendert) return;
|
||||||
|
|
||||||
|
await tx.geraetetypVorlage.deleteMany({ where: { geraeteTyp } });
|
||||||
|
if (vorlageIds.length > 0) {
|
||||||
|
await tx.geraetetypVorlage.createMany({
|
||||||
|
data: vorlageIds.map((vorlageId, position) => ({ geraeteTyp, vorlageId, position })),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const namen = await tx.vorlage.findMany({
|
||||||
|
where: { id: { in: [...vorlageIds] } },
|
||||||
|
select: { id: true, name: true },
|
||||||
|
});
|
||||||
|
const nachId = new Map(namen.map((eintrag) => [eintrag.id, eintrag.name]));
|
||||||
|
|
||||||
|
await tx.protokoll.create({
|
||||||
|
data: {
|
||||||
|
entitaet: "GeraetetypVorlage",
|
||||||
|
entitaetId: geraeteTyp,
|
||||||
|
aktion: `Schnellstart für ${geraeteTyp} geändert`,
|
||||||
|
benutzerId: durchBenutzerId,
|
||||||
|
// Namen und nicht Kennungen: ein Protokolleintrag muss auch dann noch
|
||||||
|
// lesbar sein, wenn die Vorlage spaeter anders heisst.
|
||||||
|
vorher: { vorlagen: vorher.map((eintrag) => eintrag.vorlage.name) },
|
||||||
|
nachher: { vorlagen: vorlageIds.map((id) => nachId.get(id) ?? id) },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,264 @@
|
|||||||
|
import { beforeEach, describe, expect, it } from "vitest";
|
||||||
|
import { Betriebsmodus, GeraeteTyp, VersionStatus } from "@/generated/prisma/enums";
|
||||||
|
import { RECHTE } from "@/server/auth/rollen";
|
||||||
|
import { schnellstartPflege } from "@/server/queries/schnellstart";
|
||||||
|
import { SchnellstartFehler, schnellstartSetzen } from "@/server/services/schnellstart";
|
||||||
|
import { datenbankLeeren, datenbankVerfuegbar, testPrisma } from "./helpers/db";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schnellstart je Geraetetyp pflegen (#022).
|
||||||
|
*
|
||||||
|
* Dass die Kacheln auf der Geraeteseite richtig erscheinen, prueft
|
||||||
|
* `tests/geraeteseite.test.ts` — auch den Fall, dass eine archivierte Vorlage von
|
||||||
|
* selbst verschwindet. Hier geht es um das Pflegen: die Reihenfolge, die
|
||||||
|
* Abweisungen, das Protokoll, und die Marke, die den Seed heraushaelt.
|
||||||
|
*/
|
||||||
|
|
||||||
|
async function grundbestand() {
|
||||||
|
const kunde = await testPrisma.kunde.create({
|
||||||
|
data: { name: "Eigene Organisation", kuerzel: "EIGEN", istEigeneOrganisation: true },
|
||||||
|
});
|
||||||
|
await testPrisma.einstellung.create({ data: { id: "global", modus: Betriebsmodus.INTERN } });
|
||||||
|
const admin = await testPrisma.benutzer.create({
|
||||||
|
data: { email: "admin@firma.example", name: "Admin", rolle: "ADMIN" },
|
||||||
|
});
|
||||||
|
return { kunde, adminId: admin.id };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Eine Vorlage mit veroeffentlichter Version 1 – nur die ist zuweisbar. */
|
||||||
|
async function vorlageAnlegen(
|
||||||
|
name: string,
|
||||||
|
optionen: { archiviert?: boolean; nurEntwurf?: boolean; kundeId?: string } = {},
|
||||||
|
): Promise<string> {
|
||||||
|
const vorlage = await testPrisma.vorlage.create({
|
||||||
|
data: {
|
||||||
|
name,
|
||||||
|
archiviert: optionen.archiviert ?? false,
|
||||||
|
...(optionen.kundeId ? { kundeId: optionen.kundeId } : {}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const version = await testPrisma.vorlagenVersion.create({
|
||||||
|
data: { vorlageId: vorlage.id, version: 1, status: VersionStatus.ENTWURF },
|
||||||
|
});
|
||||||
|
if (!optionen.nurEntwurf) {
|
||||||
|
await testPrisma.vorlagenVersion.update({
|
||||||
|
where: { id: version.id },
|
||||||
|
data: { status: VersionStatus.VEROEFFENTLICHT, veroeffentlichtAm: new Date() },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return vorlage.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function listeVon(typ: GeraeteTyp): Promise<readonly string[]> {
|
||||||
|
const { typen } = await schnellstartPflege();
|
||||||
|
return typen.find((eintrag) => eintrag.typ === typ)?.zugewiesen.map((e) => e.name) ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Recht auf den Schnellstart", () => {
|
||||||
|
it("liegt bei verwaltung, also nur bei ADMIN", () => {
|
||||||
|
// Die Zuordnung gilt global und bestimmt, was jeder Techniker an jedem Gerät
|
||||||
|
// zuerst sieht. TECHNIKER und LESER dürfen das nicht ändern.
|
||||||
|
expect(RECHTE.verwaltung).toEqual(["ADMIN"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe.runIf(datenbankVerfuegbar)("Schnellstart pflegen", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await datenbankLeeren();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("setzt die Liste in der übergebenen Reihenfolge", async () => {
|
||||||
|
const { adminId } = await grundbestand();
|
||||||
|
const erste = await vorlageAnlegen("Erster Griff");
|
||||||
|
const zweite = await vorlageAnlegen("Zweiter Griff");
|
||||||
|
|
||||||
|
await schnellstartSetzen(GeraeteTyp.PC, [zweite, erste], adminId);
|
||||||
|
expect(await listeVon(GeraeteTyp.PC)).toEqual(["Zweiter Griff", "Erster Griff"]);
|
||||||
|
|
||||||
|
// Umsortieren heißt ersetzen, nicht ergänzen.
|
||||||
|
await schnellstartSetzen(GeraeteTyp.PC, [erste, zweite], adminId);
|
||||||
|
expect(await listeVon(GeraeteTyp.PC)).toEqual(["Erster Griff", "Zweiter Griff"]);
|
||||||
|
expect(await testPrisma.geraetetypVorlage.count({ where: { geraeteTyp: GeraeteTyp.PC } })).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sortiert auch um, wenn alte Zeilen dieselbe Position tragen", async () => {
|
||||||
|
/*
|
||||||
|
* Der Fall, den erst das Durchklicken des gebauten Stands zeigte. Altdaten
|
||||||
|
* tragen mehrere Zeilen auf `position` 0 — der Seed zaehlt je Vorgabe von 0,
|
||||||
|
* und ein Beispielskript legte eine zweite dazu. Bei gleicher Position ist
|
||||||
|
* die Reihenfolge in Postgres nicht festgelegt, und der Vergleich
|
||||||
|
* "unveraendert" hielt eine echte Umsortierung deshalb fuer einen Leerlauf:
|
||||||
|
* die Marke wurde gesetzt, geschrieben wurde nichts.
|
||||||
|
*/
|
||||||
|
const { adminId } = await grundbestand();
|
||||||
|
const a = await vorlageAnlegen("Alpha");
|
||||||
|
const b = await vorlageAnlegen("Beta");
|
||||||
|
await testPrisma.geraetetypVorlage.createMany({
|
||||||
|
data: [
|
||||||
|
{ geraeteTyp: GeraeteTyp.PC, vorlageId: a, position: 0 },
|
||||||
|
{ geraeteTyp: GeraeteTyp.PC, vorlageId: b, position: 0 },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Genau die Reihenfolge schicken, die das Lesemodell zeigt – ein Mensch, der
|
||||||
|
// nichts umsortiert, aber auf Speichern drückt.
|
||||||
|
const gezeigt = await listeVon(GeraeteTyp.PC);
|
||||||
|
const nachName = new Map([
|
||||||
|
["Alpha", a],
|
||||||
|
["Beta", b],
|
||||||
|
]);
|
||||||
|
const umgekehrt = [...gezeigt].reverse().map((name) => nachName.get(name) ?? "");
|
||||||
|
await schnellstartSetzen(GeraeteTyp.PC, umgekehrt, adminId);
|
||||||
|
|
||||||
|
expect(await listeVon(GeraeteTyp.PC)).toEqual([...gezeigt].reverse());
|
||||||
|
// Und die Positionen stehen danach aufeinanderfolgend, nicht beide auf 0.
|
||||||
|
const zeilen = await testPrisma.geraetetypVorlage.findMany({
|
||||||
|
where: { geraeteTyp: GeraeteTyp.PC },
|
||||||
|
orderBy: { position: "asc" },
|
||||||
|
select: { position: true },
|
||||||
|
});
|
||||||
|
expect(zeilen.map((zeile) => zeile.position)).toEqual([0, 1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("zieht beim Speichern alte, gleiche Positionen gerade", async () => {
|
||||||
|
// Ohne Aenderung gespeichert: die Liste bleibt, die Positionen werden
|
||||||
|
// aufeinanderfolgend. Aufraeumen, kein Fehlerfall (#022).
|
||||||
|
const { adminId } = await grundbestand();
|
||||||
|
const a = await vorlageAnlegen("Alpha");
|
||||||
|
const b = await vorlageAnlegen("Beta");
|
||||||
|
await testPrisma.geraetetypVorlage.createMany({
|
||||||
|
data: [
|
||||||
|
{ geraeteTyp: GeraeteTyp.PC, vorlageId: a, position: 0 },
|
||||||
|
{ geraeteTyp: GeraeteTyp.PC, vorlageId: b, position: 0 },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const gezeigt = await listeVon(GeraeteTyp.PC);
|
||||||
|
const nachName = new Map([
|
||||||
|
["Alpha", a],
|
||||||
|
["Beta", b],
|
||||||
|
]);
|
||||||
|
await schnellstartSetzen(
|
||||||
|
GeraeteTyp.PC,
|
||||||
|
gezeigt.map((name) => nachName.get(name) ?? ""),
|
||||||
|
adminId,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(await listeVon(GeraeteTyp.PC)).toEqual(gezeigt);
|
||||||
|
const zeilen = await testPrisma.geraetetypVorlage.findMany({
|
||||||
|
where: { geraeteTyp: GeraeteTyp.PC },
|
||||||
|
orderBy: { position: "asc" },
|
||||||
|
select: { position: true },
|
||||||
|
});
|
||||||
|
expect(zeilen.map((zeile) => zeile.position)).toEqual([0, 1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("nimmt eine leere Liste als Angabe und entfernt alles", async () => {
|
||||||
|
const { adminId } = await grundbestand();
|
||||||
|
const vorlage = await vorlageAnlegen("Einziger Griff");
|
||||||
|
await schnellstartSetzen(GeraeteTyp.PC, [vorlage], adminId);
|
||||||
|
|
||||||
|
await schnellstartSetzen(GeraeteTyp.PC, [], adminId);
|
||||||
|
expect(await listeVon(GeraeteTyp.PC)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("lässt andere Gerätetypen unberührt", async () => {
|
||||||
|
const { adminId } = await grundbestand();
|
||||||
|
const vorlage = await vorlageAnlegen("Für beide");
|
||||||
|
await schnellstartSetzen(GeraeteTyp.PC, [vorlage], adminId);
|
||||||
|
await schnellstartSetzen(GeraeteTyp.SERVER, [vorlage], adminId);
|
||||||
|
|
||||||
|
await schnellstartSetzen(GeraeteTyp.PC, [], adminId);
|
||||||
|
expect(await listeVon(GeraeteTyp.SERVER)).toEqual(["Für beide"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("weist dieselbe Vorlage zweimal in einer Liste ab", async () => {
|
||||||
|
const { adminId } = await grundbestand();
|
||||||
|
const vorlage = await vorlageAnlegen("Einmal reicht");
|
||||||
|
await expect(schnellstartSetzen(GeraeteTyp.PC, [vorlage, vorlage], adminId)).rejects.toThrow(
|
||||||
|
SchnellstartFehler,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("weist eine archivierte Vorlage ab", async () => {
|
||||||
|
const { adminId } = await grundbestand();
|
||||||
|
const weggelegt = await vorlageAnlegen("Weggelegt", { archiviert: true });
|
||||||
|
await expect(schnellstartSetzen(GeraeteTyp.PC, [weggelegt], adminId)).rejects.toThrow(/archiviert/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("weist eine Vorlage ohne veröffentlichte Version ab", async () => {
|
||||||
|
// Eine Kachel, die zu einem Entwurf führt, kann keinen Lauf starten.
|
||||||
|
const { adminId } = await grundbestand();
|
||||||
|
const entwurf = await vorlageAnlegen("Noch Entwurf", { nurEntwurf: true });
|
||||||
|
await expect(schnellstartSetzen(GeraeteTyp.PC, [entwurf], adminId)).rejects.toThrow(
|
||||||
|
/veröffentlichte Version/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("bietet zur Auswahl nur veröffentlichte, nicht archivierte Vorlagen", async () => {
|
||||||
|
const { kunde } = await grundbestand();
|
||||||
|
await vorlageAnlegen("Brauchbar");
|
||||||
|
await vorlageAnlegen("Weggelegt", { archiviert: true });
|
||||||
|
await vorlageAnlegen("Noch Entwurf", { nurEntwurf: true });
|
||||||
|
await vorlageAnlegen("Nur für einen Kunden", { kundeId: kunde.id });
|
||||||
|
|
||||||
|
const { auswahl } = await schnellstartPflege();
|
||||||
|
expect(auswahl.map((eintrag) => eintrag.name).sort()).toEqual(["Brauchbar", "Nur für einen Kunden"]);
|
||||||
|
// Eine kundengebundene Vorlage ist zuweisbar und wird als solche gezeigt;
|
||||||
|
// dass sie nur bei ihren Geräten erscheint, entscheidet das Lesen.
|
||||||
|
expect(auswahl.find((eintrag) => eintrag.name === "Nur für einen Kunden")?.kundeName).toBe(
|
||||||
|
"Eigene Organisation",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("nennt jeden Gerätetyp, auch die ohne Zuordnung", async () => {
|
||||||
|
await grundbestand();
|
||||||
|
const { typen } = await schnellstartPflege();
|
||||||
|
expect(typen.map((eintrag) => eintrag.typ)).toEqual(Object.values(GeraeteTyp));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("schreibt die Änderung mit Namen ins Protokoll, nicht mit Kennungen", async () => {
|
||||||
|
const { adminId } = await grundbestand();
|
||||||
|
const erste = await vorlageAnlegen("Erster Griff");
|
||||||
|
const zweite = await vorlageAnlegen("Zweiter Griff");
|
||||||
|
await schnellstartSetzen(GeraeteTyp.PC, [erste], adminId);
|
||||||
|
await schnellstartSetzen(GeraeteTyp.PC, [erste, zweite], adminId);
|
||||||
|
|
||||||
|
const eintraege = await testPrisma.protokoll.findMany({
|
||||||
|
where: { entitaet: "GeraetetypVorlage" },
|
||||||
|
orderBy: { erstelltAm: "asc" },
|
||||||
|
});
|
||||||
|
expect(eintraege).toHaveLength(2);
|
||||||
|
expect(eintraege[1]?.aktion).toBe("Schnellstart für PC geändert");
|
||||||
|
expect(eintraege[1]?.vorher).toEqual({ vorlagen: ["Erster Griff"] });
|
||||||
|
expect(eintraege[1]?.nachher).toEqual({ vorlagen: ["Erster Griff", "Zweiter Griff"] });
|
||||||
|
// Ein Protokolleintrag muss auch lesbar sein, wenn die Vorlage später
|
||||||
|
// anders heißt – deshalb Namen und keine Kennungen.
|
||||||
|
expect(JSON.stringify(eintraege[1]?.nachher)).not.toContain(erste);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("schreibt keinen Protokolleintrag, wenn sich die Liste nicht ändert", async () => {
|
||||||
|
const { adminId } = await grundbestand();
|
||||||
|
const vorlage = await vorlageAnlegen("Unverändert");
|
||||||
|
await schnellstartSetzen(GeraeteTyp.PC, [vorlage], adminId);
|
||||||
|
await schnellstartSetzen(GeraeteTyp.PC, [vorlage], adminId);
|
||||||
|
|
||||||
|
expect(await testPrisma.protokoll.count({ where: { entitaet: "GeraetetypVorlage" } })).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("setzt die Marke, die den Seed heraushält – auch bei unveränderter Liste", async () => {
|
||||||
|
const { adminId } = await grundbestand();
|
||||||
|
const vorlage = await vorlageAnlegen("Einziger Griff");
|
||||||
|
expect(
|
||||||
|
(await testPrisma.einstellung.findUniqueOrThrow({ where: { id: "global" } })).schnellstartGepflegt,
|
||||||
|
).toBe(false);
|
||||||
|
|
||||||
|
// Ein Speichern, das nichts ändert, ist trotzdem die Aussage „ich pflege das
|
||||||
|
// selbst“ — sonst legte der Seed seine Vorgaben weiter nach.
|
||||||
|
await schnellstartSetzen(GeraeteTyp.PC, [], adminId);
|
||||||
|
expect(
|
||||||
|
(await testPrisma.einstellung.findUniqueOrThrow({ where: { id: "global" } })).schnellstartGepflegt,
|
||||||
|
).toBe(true);
|
||||||
|
expect(vorlage).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -414,6 +414,36 @@ describe.runIf(datenbankVerfuegbar)("Seed gegen die Datenbank", () => {
|
|||||||
expect(await typVorgabenSaeen(testPrisma, gesaet)).toBe(0);
|
expect(await typVorgabenSaeen(testPrisma, gesaet)).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("laesst den Schnellstart in Ruhe, sobald er in der Oberflaeche gepflegt wurde", async () => {
|
||||||
|
// Der Fall aus #022: wer eine Kachel entfernt, soll sie nicht beim naechsten
|
||||||
|
// Seed-Lauf zurueckbekommen. Eine fehlende Zeile sagt nicht, ob sie noch nie
|
||||||
|
// da war oder bewusst entfernt wurde — die Marke sagt es.
|
||||||
|
const gesaet = await vorlagenSaeen(testPrisma);
|
||||||
|
expect(await typVorgabenSaeen(testPrisma, gesaet)).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
await testPrisma.geraetetypVorlage.deleteMany({ where: { geraeteTyp: GeraeteTyp.PC } });
|
||||||
|
// upsert, weil dieser Block die Einstellung nicht selbst anlegt - und genau
|
||||||
|
// deshalb belegt der Test auch, dass typVorgabenSaeen ohne sie zurechtkommt.
|
||||||
|
await testPrisma.einstellung.upsert({
|
||||||
|
where: { id: "global" },
|
||||||
|
create: { id: "global", schnellstartGepflegt: true },
|
||||||
|
update: { schnellstartGepflegt: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(await typVorgabenSaeen(testPrisma, gesaet)).toBe(0);
|
||||||
|
expect(await testPrisma.geraetetypVorlage.count({ where: { geraeteTyp: GeraeteTyp.PC } })).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("legt die Vorgaben weiter an, solange niemand sie gepflegt hat", async () => {
|
||||||
|
const gesaet = await vorlagenSaeen(testPrisma);
|
||||||
|
await typVorgabenSaeen(testPrisma, gesaet);
|
||||||
|
const vorher = await testPrisma.geraetetypVorlage.count({ where: { geraeteTyp: GeraeteTyp.PC } });
|
||||||
|
expect(vorher).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
await testPrisma.geraetetypVorlage.deleteMany({ where: { geraeteTyp: GeraeteTyp.PC } });
|
||||||
|
expect(await typVorgabenSaeen(testPrisma, gesaet)).toBe(vorher);
|
||||||
|
});
|
||||||
|
|
||||||
it("legt Abteilungen, Personen, Geraete und freie Tags an", async () => {
|
it("legt Abteilungen, Personen, Geraete und freie Tags an", async () => {
|
||||||
const eigene = await eigeneOrganisation();
|
const eigene = await eigeneOrganisation();
|
||||||
const bestand = await stammdatenSaeen(testPrisma, eigene.id);
|
const bestand = await stammdatenSaeen(testPrisma, eigene.id);
|
||||||
|
|||||||
Reference in New Issue
Block a user