feat(vorlagen): Abschnitte nur fuer bestimmte Abteilungen (#58)
Prüfung / pruefung (push) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s

This commit was merged in pull request #58.
This commit is contained in:
2026-10-04 18:04:51 +00:00
parent 80870133b8
commit 855164066c
24 changed files with 815 additions and 37 deletions
+70 -5
View File
@@ -1,6 +1,7 @@
import { Prisma, type Lauf } from "@/generated/prisma/client";
import { LaufStatus, SchrittZustand, VariablenTyp } from "@/generated/prisma/enums";
import { prisma } from "@/lib/db";
import { geltendeAbschnitteErmitteln, nurGeltende } from "@/lib/geltende-abschnitte";
import { darf } from "@/server/auth/rollen";
import {
MandantFehler,
@@ -190,6 +191,13 @@ type VersionFuerLauf = {
readonly pflicht: boolean;
}[];
readonly schrittIds: readonly string[];
/** Fuer die Auswahl nach Abteilung beim Start (#031). */
readonly abschnitte: readonly {
readonly id: string;
readonly titel: string;
readonly nurFuerAbteilungen: readonly string[];
readonly schrittIds: readonly string[];
}[];
};
/**
@@ -219,7 +227,12 @@ async function versionFuerLaufHolen(versionId: string, kundeId: string): Promise
},
abschnitte: {
orderBy: { position: "asc" },
select: { schritte: { orderBy: { position: "asc" }, select: { id: true } } },
select: {
id: true,
titel: true,
nurFuerAbteilungen: true,
schritte: { orderBy: { position: "asc" }, select: { id: true } },
},
},
},
});
@@ -238,6 +251,12 @@ async function versionFuerLaufHolen(versionId: string, kundeId: string): Promise
vorlagenName: version.vorlage.name,
variablen: version.variablen,
schrittIds: version.abschnitte.flatMap((abschnitt) => abschnitt.schritte.map((s) => s.id)),
abschnitte: version.abschnitte.map((abschnitt) => ({
id: abschnitt.id,
titel: abschnitt.titel,
nurFuerAbteilungen: abschnitt.nurFuerAbteilungen,
schrittIds: abschnitt.schritte.map((schritt) => schritt.id),
})),
};
}
@@ -246,6 +265,8 @@ type Bezuege = {
readonly mitarbeiterName: string | null;
readonly geraetId: string | null;
readonly abteilungId: string | null;
/** Name der gewaehlten Abteilung – fuer die Abschnittsauswahl (#031). */
readonly abteilungsName: string | null;
/** Nur fuer den Titelvorschlag. */
readonly geraetName: string | null;
readonly personName: string | null;
@@ -310,6 +331,7 @@ async function bezuegePruefen(
}
const abteilungId = text(eingabe.abteilungId);
let abteilungsName: string | null = null;
if (abteilungId) {
const abteilung = await prisma.abteilung.findUnique({
where: { id: abteilungId },
@@ -322,9 +344,18 @@ async function bezuegePruefen(
if (abteilung.archiviert) {
throw new LaufFehler(`Die Abteilung „${abteilung.name}“ ist archiviert.`);
}
abteilungsName = abteilung.name;
}
return { mitarbeiterId, mitarbeiterName, geraetId, abteilungId, geraetName, personName };
return {
mitarbeiterId,
mitarbeiterName,
geraetId,
abteilungId,
abteilungsName,
geraetName,
personName,
};
}
/**
@@ -441,6 +472,28 @@ export async function laufAnlegen(eingabe: NeuerLauf, benutzer: HandelnderBenutz
person: bezuege.personName,
});
/*
* Die Abschnittsauswahl **jetzt** festlegen und am Lauf festhalten (#031).
* Danach aendert sie sich nie wieder: ein spaeterer Abteilungswechsel darf
* keinen Schritt entfernen, an dem schon eine Notiz haengt, und ein
* archivierter Lauf muss genau das zeigen, was damals abgearbeitet wurde.
*/
const geltende = geltendeAbschnitteErmitteln(version.abschnitte, bezuege.abteilungsName);
if (version.abschnitte.length > 0 && geltende.length === 0) {
throw new LaufFehler(
bezuege.abteilungsName === null
? `Von „${version.vorlagenName}“ gilt ohne Abteilung kein Abschnitt — jeder ist auf ` +
"bestimmte Abteilungen eingeschränkt. Wähle eine Abteilung."
: `Von „${version.vorlagenName}“ gilt für die Abteilung „${bezuege.abteilungsName}“ kein ` +
"Abschnitt. Es gäbe nichts abzuarbeiten.",
);
}
const geltendeSchrittIds = new Set(
version.abschnitte
.filter((abschnitt) => geltende.includes(abschnitt.id))
.flatMap((abschnitt) => abschnitt.schrittIds),
);
return prisma.$transaction(async (tx) => {
const lauf = await tx.lauf.create({
data: {
@@ -457,12 +510,17 @@ export async function laufAnlegen(eingabe: NeuerLauf, benutzer: HandelnderBenutz
zugewiesenAnId,
faelligAm: eingabe.faelligAm ?? null,
erstelltVonId: benutzer.id,
geltendeAbschnitte: [...geltende],
},
});
if (version.schrittIds.length > 0) {
// Nur fuer die geltenden Abschnitte. Ein Schrittergebnis zu einem
// ausgefilterten Schritt waere eine Zeile, die niemand je sieht — und sie
// zaehlte in jeder Statistik mit.
const anzulegen = version.schrittIds.filter((id) => geltendeSchrittIds.has(id));
if (anzulegen.length > 0) {
await tx.schrittergebnis.createMany({
data: version.schrittIds.map((vorlagenSchrittId) => ({
data: anzulegen.map((vorlagenSchrittId) => ({
laufId: lauf.id,
vorlagenSchrittId,
zustand: SchrittZustand.OFFEN,
@@ -605,11 +663,13 @@ export async function offenePflichtschritte(
const lauf = await tx.lauf.findUniqueOrThrow({
where: { id: laufId },
select: {
geltendeAbschnitte: true,
vorlagenVersion: {
select: {
abschnitte: {
orderBy: { position: "asc" },
select: {
id: true,
titel: true,
schritte: {
where: { pflicht: true },
@@ -626,7 +686,12 @@ export async function offenePflichtschritte(
const zustaende = new Map(lauf.ergebnisse.map((e) => [e.vorlagenSchrittId, e.zustand]));
const offen: OffenerPflichtschritt[] = [];
for (const abschnitt of lauf.vorlagenVersion.abschnitte) {
/*
* Nur die geltenden Abschnitte (#031). Ein ausgefilterter Pflichtschritt darf
* den Abschluss nicht blockieren — sonst waere er unerreichbar: abhaken laesst
* sich nur, was in der Abarbeitung steht.
*/
for (const abschnitt of nurGeltende(lauf.vorlagenVersion.abschnitte, lauf.geltendeAbschnitte)) {
for (const schritt of abschnitt.schritte) {
// Ohne Ergebniszeile gilt OFFEN – ein Schritt ohne Eintrag ist nicht erledigt.
const zustand = zustaende.get(schritt.id) ?? SchrittZustand.OFFEN;