import { beforeEach, describe, expect, it } from "vitest"; import { Betriebsmodus, LaufStatus } from "@/generated/prisma/enums"; import { haeufigeVorlagen, uebersicht } from "@/server/queries/uebersicht"; import { datenbankLeeren, datenbankVerfuegbar, testPrisma } from "./helpers/db"; /** * Die Uebersicht (#015). * * Geprueft wird vor allem eines: **was „ueberfaellig“ heisst.** Der Auftrag sagt * es genau — `faelligAm` verstrichen **und** Status `OFFEN` oder `IN_ARBEIT`. * Ein abgeschlossener Lauf mit verstrichenem Datum ist fertig, kein Rueckstand; * ein archivierter erst recht nicht. Waere das falsch, riefe die Seite nach * Arbeit, die niemand mehr tun kann — und der Ruf waere bald nichts mehr wert. */ const GESTERN = new Date("2026-09-24T09:00:00Z"); const HEUTE = new Date("2026-09-25T09:00:00Z"); const MORGEN = new Date("2026-09-26T09:00:00Z"); async function aufbau() { const eigene = await testPrisma.kunde.create({ data: { name: "Muster", kuerzel: "MUS", istEigeneOrganisation: true }, }); await testPrisma.einstellung.create({ data: { id: "global", modus: Betriebsmodus.INTERN } }); const benutzer = await testPrisma.benutzer.create({ data: { email: "t@firma.example", name: "Ali Demir", rolle: "TECHNIKER" }, }); const andere = await testPrisma.benutzer.create({ data: { email: "k@firma.example", name: "Katrin Winter", rolle: "TECHNIKER" }, }); const vorlage = await testPrisma.vorlage.create({ data: { name: "PC-Neuinstallation" } }); const version = await testPrisma.vorlagenVersion.create({ data: { vorlageId: vorlage.id, version: 1, status: "ENTWURF" }, }); const abschnitt = await testPrisma.vorlagenAbschnitt.create({ data: { versionId: version.id, titel: "Vorbereitung" }, }); const schritt = await testPrisma.vorlagenSchritt.create({ data: { abschnittId: abschnitt.id, titel: "Ein Schritt", pflicht: true }, }); await testPrisma.vorlagenVersion.update({ where: { id: version.id }, data: { status: "VEROEFFENTLICHT", veroeffentlichtAm: new Date() }, }); async function lauf(daten: { titel: string; status?: LaufStatus; faelligAm?: Date | null; zugewiesenAnId?: string | null; }) { const zeile = await testPrisma.lauf.create({ data: { vorlagenVersionId: version.id, kundeId: eigene.id, titel: daten.titel, status: daten.status ?? LaufStatus.OFFEN, faelligAm: daten.faelligAm ?? null, zugewiesenAnId: daten.zugewiesenAnId ?? null, /* * Die Zeitstempel gehoeren zum Status, und zwar nicht nur der Form * halber: drei CHECK-Bedingungen in der Datenbank verlangen sie * (`Lauf_begonnen_mit_zeitpunkt` und die beiden daneben, #006). Ein Test * darf an ihnen nicht vorbeischreiben — sonst pruefte er eine Lage, die * es in der Anwendung nie gibt. */ ...(daten.status && daten.status !== LaufStatus.OFFEN ? { gestartetAm: GESTERN } : {}), ...(daten.status === LaufStatus.ABGESCHLOSSEN || daten.status === LaufStatus.ARCHIVIERT ? { abgeschlossenAm: GESTERN } : {}), ...(daten.status === LaufStatus.ARCHIVIERT ? { archiviertAm: GESTERN } : {}), }, }); await testPrisma.schrittergebnis.create({ data: { laufId: zeile.id, vorlagenSchrittId: schritt.id }, }); return zeile; } return { eigene, vorlage, version, benutzer: { id: benutzer.id, rolle: benutzer.rolle }, andere, lauf }; } describe.runIf(datenbankVerfuegbar)("Übersicht", () => { beforeEach(datenbankLeeren); describe("überfällig", () => { it("sind genau die verstrichenen, die noch laufen", async () => { const { benutzer, lauf } = await aufbau(); await lauf({ titel: "Gestern fällig, offen", faelligAm: GESTERN }); await lauf({ titel: "Gestern fällig, in Arbeit", faelligAm: GESTERN, status: LaufStatus.IN_ARBEIT }); await lauf({ titel: "Morgen fällig", faelligAm: MORGEN }); await lauf({ titel: "Ohne Frist" }); await lauf({ titel: "Gestern fällig, aber fertig", faelligAm: GESTERN, status: LaufStatus.ABGESCHLOSSEN, }); await lauf({ titel: "Gestern fällig, aber archiviert", faelligAm: GESTERN, status: LaufStatus.ARCHIVIERT, }); const daten = await uebersicht(benutzer, HEUTE); expect(daten.ueberfaellig.map((zeile) => zeile.titel).sort()).toEqual([ "Gestern fällig, in Arbeit", "Gestern fällig, offen", ]); }); it("zählt einen Lauf, der heute fällig ist, nicht dazu", async () => { /* * Der Blocker aus dem Review: verglichen wurde mit dem Augenblick statt * mit dem Tagesbeginn. Ein Datumsfeld im Browser speichert Mitternacht * UTC — ein heute faelliger Lauf stand damit hierzulande ab zwei Uhr * morgens in Warnfarbe, an genau dem Tag, an dem er erledigt werden soll. */ const { benutzer, lauf } = await aufbau(); await lauf({ titel: "Heute fällig", faelligAm: new Date("2026-09-25T00:00:00Z") }); await lauf({ titel: "Gestern fällig", faelligAm: GESTERN }); const daten = await uebersicht(benutzer, HEUTE); expect(daten.ueberfaellig.map((zeile) => zeile.titel)).toEqual(["Gestern fällig"]); // Auch spät am Abend gilt der heutige Tag noch nicht als versäumt. const spaet = await uebersicht(benutzer, new Date("2026-09-25T23:45:00+02:00")); expect(spaet.ueberfaellig.map((zeile) => zeile.titel)).toEqual(["Gestern fällig"]); }); it("zeigt das längst Fällige zuerst", async () => { const { benutzer, lauf } = await aufbau(); await lauf({ titel: "Seit gestern", faelligAm: GESTERN }); await lauf({ titel: "Seit letzter Woche", faelligAm: new Date("2026-09-18T09:00:00Z") }); const daten = await uebersicht(benutzer, HEUTE); expect(daten.ueberfaellig[0]?.titel).toBe("Seit letzter Woche"); }); }); describe("mir zugewiesen", () => { it("zeigt nur die eigenen laufenden", async () => { const { benutzer, andere, lauf } = await aufbau(); await lauf({ titel: "Meiner", zugewiesenAnId: benutzer.id }); await lauf({ titel: "Meiner, fertig", zugewiesenAnId: benutzer.id, status: LaufStatus.ABGESCHLOSSEN }); await lauf({ titel: "Von Katrin", zugewiesenAnId: andere.id }); await lauf({ titel: "Niemandem" }); const daten = await uebersicht(benutzer, HEUTE); expect(daten.meine.map((zeile) => zeile.titel)).toEqual(["Meiner"]); }); }); describe("zuletzt bearbeitet", () => { it("zeigt auch die Läufe der Kollegen, aber keine archivierten", async () => { /* * Bewusst nicht auf die eigene Person eingeschraenkt: ein Team teilt sich * die Arbeit, und was der Kollege gestern angefasst hat, ist heute oft * genau die Frage. */ const { benutzer, andere, lauf } = await aufbau(); await lauf({ titel: "Von Katrin", zugewiesenAnId: andere.id }); await lauf({ titel: "Archiviert", status: LaufStatus.ARCHIVIERT }); const titel = (await uebersicht(benutzer, HEUTE)).zuletzt.map((zeile) => zeile.titel); expect(titel).toContain("Von Katrin"); expect(titel).not.toContain("Archiviert"); }); }); describe("Mandantensicht", () => { it("zeigt keinen Lauf eines fremden Kunden", async () => { const { benutzer, version } = await aufbau(); await testPrisma.einstellung.update({ where: { id: "global" }, data: { modus: Betriebsmodus.DIENSTLEISTUNG }, }); process.env["OIDC_CUSTOMER_GROUP_PREFIX"] = "lz-kunde-"; const meiner = await testPrisma.kunde.create({ data: { name: "Meiner", kuerzel: "MEIN" } }); const fremder = await testPrisma.kunde.create({ data: { name: "Fremder", kuerzel: "FREMD" } }); await testPrisma.benutzerKunde.create({ data: { benutzerId: benutzer.id, kundeId: meiner.id }, }); /* * Der sichtbare Lauf gehoert **dem gebundenen Kunden**, nicht der eigenen * Organisation: im Modus Dienstleistung sieht die Technikerin genau die * Kunden, an die sie gebunden ist. Ein Lauf der eigenen Organisation waere * hier zu Recht auch weg — beim ersten Anlauf hat dieser Test genau das * uebersehen und damit nur sich selbst geprueft. */ await testPrisma.lauf.create({ data: { vorlagenVersionId: version.id, kundeId: meiner.id, titel: "Eigener", faelligAm: GESTERN, }, }); const fremd = await testPrisma.lauf.create({ data: { vorlagenVersionId: version.id, kundeId: fremder.id, titel: "Fremder Lauf", faelligAm: GESTERN, zugewiesenAnId: benutzer.id, }, }); expect(fremd.id).toBeTruthy(); const daten = await uebersicht(benutzer, HEUTE); const alle = [...daten.ueberfaellig, ...daten.meine, ...daten.zuletzt].map((zeile) => zeile.titel); expect(alle).not.toContain("Fremder Lauf"); // Und der eigene fehlt auch nicht – sonst filterte der Test nur sich selbst. expect(daten.zuletzt.map((zeile) => zeile.titel)).toContain("Eigener"); delete process.env["OIDC_CUSTOMER_GROUP_PREFIX"]; }); }); describe("Schnellstart", () => { it("zählt über alle Versionen einer Vorlage", async () => { /* * „PC-Neuinstallation“ ist dieselbe Arbeit, ob sie in v1 oder v7 steht. * Eine Rangfolge je Version finge nach jedem Veroeffentlichen bei null an. */ const { benutzer, vorlage, version, lauf } = await aufbau(); await lauf({ titel: "Erster" }); await lauf({ titel: "Zweiter" }); const zweite = await testPrisma.vorlagenVersion.create({ data: { vorlageId: vorlage.id, version: 2, status: "VEROEFFENTLICHT", veroeffentlichtAm: new Date(), }, }); expect(zweite.version).toBe(2); const andere = await testPrisma.vorlage.create({ data: { name: "Offboarding" } }); const andereVersion = await testPrisma.vorlagenVersion.create({ data: { vorlageId: andere.id, version: 1, status: "VEROEFFENTLICHT", veroeffentlichtAm: new Date(), }, }); const haeufig = await haeufigeVorlagen(benutzer); expect(haeufig[0]?.name).toBe("PC-Neuinstallation"); expect(haeufig[0]?.anzahlLaeufe).toBe(2); // Gestartet wird die **neueste** Version, gezaehlt werden alle. expect(haeufig[0]?.versionId).toBe(zweite.id); expect(haeufig[0]?.versionId).not.toBe(version.id); expect(haeufig.map((eintrag) => eintrag.versionId)).toContain(andereVersion.id); }); it("zählt keine Läufe fremder Kunden mit", async () => { /* * Der zweite Blocker: der Zaehler trug keine Mandantenbedingung. Eine * Vorlage ohne Kundenbezug gilt fuer alle (#009) — ihr „12× gestartet“ * war damit eine Auskunft ueber fremde Betriebe, und die Reihenfolge des * Schnellstarts richtete sich nach deren Arbeit. */ const { benutzer, version } = await aufbau(); await testPrisma.einstellung.update({ where: { id: "global" }, data: { modus: Betriebsmodus.DIENSTLEISTUNG }, }); process.env["OIDC_CUSTOMER_GROUP_PREFIX"] = "lz-kunde-"; const meiner = await testPrisma.kunde.create({ data: { name: "Meiner", kuerzel: "MEIN" } }); const fremder = await testPrisma.kunde.create({ data: { name: "Fremder", kuerzel: "FREMD" } }); await testPrisma.benutzerKunde.create({ data: { benutzerId: benutzer.id, kundeId: meiner.id } }); for (const [nummer, kundeId] of [meiner.id, fremder.id, fremder.id, fremder.id].entries()) { await testPrisma.lauf.create({ data: { vorlagenVersionId: version.id, kundeId, titel: `Lauf ${nummer}` }, }); } const haeufig = await haeufigeVorlagen(benutzer); expect(haeufig[0]?.anzahlLaeufe).toBe(1); delete process.env["OIDC_CUSTOMER_GROUP_PREFIX"]; }); it("lässt eine Vorlage ohne veröffentlichte Version weg", async () => { // Ein Entwurf ist noch niemandes Arbeitsanweisung (#009). const { benutzer } = await aufbau(); const entwurf = await testPrisma.vorlage.create({ data: { name: "Nur Entwurf" } }); await testPrisma.vorlagenVersion.create({ data: { vorlageId: entwurf.id, version: 1, status: "ENTWURF" }, }); const namen = (await haeufigeVorlagen(benutzer)).map((eintrag) => eintrag.name); expect(namen).not.toContain("Nur Entwurf"); }); }); });