feat(schnellstart): Kacheln je Geraetetyp in der Oberflaeche pflegen (#44)
Prüfung / pruefung (push) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s

This commit was merged in pull request #44.
This commit is contained in:
2026-10-04 15:41:47 +00:00
parent 0060a55387
commit f5936327a0
17 changed files with 910 additions and 24 deletions
+119
View File
@@ -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) },
},
});
});
}