feat(mandant): Betriebsmodus, Kundenverwaltung und Einstellungen (#22)
Zwei Betriebsmodi, ein Codepfad. src/server/mandant.ts ist die eine Stelle, an der die Mandantenregel steht. Der Review fand drei Blocker: die Lesemodelle wandten die Mandantensicht nicht an, der Protokolleintrag des Moduswechsels stand außerhalb der Transaktion, und Prüfung und Vollzug lagen so weit auseinander, dass zwei gleichzeitige Wechsel beide durchliefen. Alle drei geschlossen, der letzte mit einem Advisory Lock. 336 Tests. Closes #8 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit was merged in pull request #22.
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
import { prisma } from "@/lib/db";
|
||||
import { sichtbareKundenIds, type HandelnderBenutzer } from "@/server/mandant";
|
||||
|
||||
/**
|
||||
* Lesemodelle der Kundenverwaltung (#008).
|
||||
*
|
||||
* Sichtbar sind diese Listen nur im Modus `DIENSTLEISTUNG`; im Modus `INTERN`
|
||||
* gibt es genau die eigene Organisation, und die erscheint nirgends. Welcher
|
||||
* Modus gilt, entscheidet der Aufrufer.
|
||||
*
|
||||
* Die Mandantensicht wenden diese Funktionen aber **selbst** an, statt sie dem
|
||||
* Aufrufer zu ueberlassen: ein vergessener Filter waere hier kein Schoenheits-,
|
||||
* sondern ein Datenschutzfehler. Deshalb verlangt jede von ihnen den Benutzer.
|
||||
*/
|
||||
|
||||
/**
|
||||
* `undefined` heisst "keine Einschraenkung", eine leere Liste heisst "sieht
|
||||
* nichts". Wer das verwechselt, zeigt entweder alles oder gar nichts — deshalb
|
||||
* steht die Umsetzung an einer Stelle.
|
||||
*/
|
||||
async function sichtbarkeitsFilter(benutzer: HandelnderBenutzer) {
|
||||
const sichtbar = await sichtbareKundenIds(benutzer);
|
||||
return sichtbar === undefined ? {} : { id: { in: [...sichtbar] } };
|
||||
}
|
||||
|
||||
export type KundenZeile = {
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly kuerzel: string;
|
||||
readonly istEigeneOrganisation: boolean;
|
||||
readonly archiviert: boolean;
|
||||
readonly notizen: string | null;
|
||||
/** Fuer die Kundenliste: woran haengt der Kunde? */
|
||||
readonly anzahlGeraete: number;
|
||||
readonly anzahlMitarbeiter: number;
|
||||
readonly anzahlLaeufe: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Die Zaehlungen kommen als `_count` aus derselben Abfrage.
|
||||
*
|
||||
* In einer Schleife nachzuzaehlen waere eine Abfrage je Kunde – bei einem
|
||||
* Dienstleister mit dreissig Kunden also einundneunzig statt einer.
|
||||
*/
|
||||
const AUSWAHL = {
|
||||
id: true,
|
||||
name: true,
|
||||
kuerzel: true,
|
||||
istEigeneOrganisation: true,
|
||||
archiviert: true,
|
||||
notizen: true,
|
||||
_count: { select: { geraete: true, mitarbeiter: true, laeufe: true } },
|
||||
} as const;
|
||||
|
||||
type RohZeile = {
|
||||
id: string;
|
||||
name: string;
|
||||
kuerzel: string;
|
||||
istEigeneOrganisation: boolean;
|
||||
archiviert: boolean;
|
||||
notizen: string | null;
|
||||
_count: { geraete: number; mitarbeiter: number; laeufe: number };
|
||||
};
|
||||
|
||||
function zeileBauen(roh: RohZeile): KundenZeile {
|
||||
return {
|
||||
id: roh.id,
|
||||
name: roh.name,
|
||||
kuerzel: roh.kuerzel,
|
||||
istEigeneOrganisation: roh.istEigeneOrganisation,
|
||||
archiviert: roh.archiviert,
|
||||
notizen: roh.notizen,
|
||||
anzahlGeraete: roh._count.geraete,
|
||||
anzahlMitarbeiter: roh._count.mitarbeiter,
|
||||
anzahlLaeufe: roh._count.laeufe,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Alle Kunden, nicht archivierte zuerst, dann nach Namen.
|
||||
*
|
||||
* Archivierte bleiben standardmaessig aussen vor: die Liste ist der
|
||||
* Alltagsbestand, das Archiv ist ein ausdruecklicher Griff.
|
||||
*/
|
||||
export async function kundenListe(
|
||||
benutzer: HandelnderBenutzer,
|
||||
auchArchivierte = false,
|
||||
): Promise<readonly KundenZeile[]> {
|
||||
const zeilen = await prisma.kunde.findMany({
|
||||
where: {
|
||||
...(await sichtbarkeitsFilter(benutzer)),
|
||||
...(auchArchivierte ? {} : { archiviert: false }),
|
||||
},
|
||||
orderBy: [{ archiviert: "asc" }, { name: "asc" }],
|
||||
select: AUSWAHL,
|
||||
});
|
||||
return zeilen.map(zeileBauen);
|
||||
}
|
||||
|
||||
export async function kundeHolen(id: string, benutzer: HandelnderBenutzer): Promise<KundenZeile | undefined> {
|
||||
const zeile = await prisma.kunde.findFirst({
|
||||
where: { id, ...(await sichtbarkeitsFilter(benutzer)) },
|
||||
select: AUSWAHL,
|
||||
});
|
||||
return zeile ? zeileBauen(zeile) : undefined;
|
||||
}
|
||||
|
||||
/** Fuer Auswahlfelder: id, name, kuerzel nicht archivierter Kunden, nach Namen. */
|
||||
export async function kundenZurAuswahl(
|
||||
benutzer: HandelnderBenutzer,
|
||||
): Promise<readonly { id: string; name: string; kuerzel: string }[]> {
|
||||
return prisma.kunde.findMany({
|
||||
where: { archiviert: false, ...(await sichtbarkeitsFilter(benutzer)) },
|
||||
orderBy: { name: "asc" },
|
||||
select: { id: true, name: true, kuerzel: true },
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user