feat(stammdaten): Mitarbeiter, Abteilungen und Geraete (#25)
This commit was merged in pull request #25.
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
import type { Prisma } from "@/generated/prisma/client";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { sichtbareKundenIds, type HandelnderBenutzer } from "@/server/mandant";
|
||||
|
||||
/**
|
||||
* Lesemodelle der Abteilungen (#011).
|
||||
*
|
||||
* Die Zahl der zugeordneten Personen kommt aus einer zweiten Abfrage statt aus
|
||||
* `_count`: die Zuordnung laeuft ueber den **Namen** im Freitextfeld
|
||||
* `Mitarbeiter.abteilung`, nicht ueber einen Fremdschluessel. So bleibt ein
|
||||
* Abteilungswert aus dem Verzeichnis erhalten, auch wenn es dazu (noch) keine
|
||||
* gepflegte Abteilung gibt — und genau diese Faelle will man sehen.
|
||||
*/
|
||||
|
||||
export type AbteilungsZeile = {
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly zielOu: string | null;
|
||||
readonly standardGruppen: readonly string[];
|
||||
readonly mailDomain: string | null;
|
||||
readonly notizen: string | null;
|
||||
readonly archiviert: boolean;
|
||||
readonly kundeId: string;
|
||||
readonly anzahlMitarbeiter: number;
|
||||
};
|
||||
|
||||
async function kundenBedingung(benutzer: HandelnderBenutzer): Promise<{ kundeId: { in: string[] } }[]> {
|
||||
const sichtbar = await sichtbareKundenIds(benutzer);
|
||||
return sichtbar === undefined ? [] : [{ kundeId: { in: [...sichtbar] } }];
|
||||
}
|
||||
|
||||
const AUSWAHL = {
|
||||
id: true,
|
||||
name: true,
|
||||
zielOu: true,
|
||||
standardGruppen: true,
|
||||
mailDomain: true,
|
||||
notizen: true,
|
||||
archiviert: true,
|
||||
kundeId: true,
|
||||
} satisfies Prisma.AbteilungSelect;
|
||||
|
||||
export async function abteilungsListe(
|
||||
benutzer: HandelnderBenutzer,
|
||||
auchArchivierte = true,
|
||||
): Promise<readonly AbteilungsZeile[]> {
|
||||
const mandant = await kundenBedingung(benutzer);
|
||||
const zeilen = await prisma.abteilung.findMany({
|
||||
where: { AND: [...(auchArchivierte ? [] : [{ archiviert: false }]), ...mandant] },
|
||||
orderBy: [{ archiviert: "asc" }, { name: "asc" }],
|
||||
select: AUSWAHL,
|
||||
});
|
||||
|
||||
// Eine Abfrage fuer alle Zaehlungen statt einer je Abteilung.
|
||||
const zaehlung = await prisma.mitarbeiter.groupBy({
|
||||
by: ["abteilung", "kundeId"],
|
||||
where: { AND: [{ aktiv: true }, { abteilung: { not: null } }, ...mandant] },
|
||||
_count: { _all: true },
|
||||
});
|
||||
const anzahlNach = new Map(
|
||||
zaehlung.map((eintrag) => [`${eintrag.kundeId}\u0000${eintrag.abteilung ?? ""}`, eintrag._count._all]),
|
||||
);
|
||||
|
||||
return zeilen.map((zeile) => ({
|
||||
...zeile,
|
||||
standardGruppen: zeile.standardGruppen,
|
||||
anzahlMitarbeiter: anzahlNach.get(`${zeile.kundeId}\u0000${zeile.name}`) ?? 0,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Abteilungsnamen, die bei Mitarbeitern vorkommen, aber nicht gepflegt sind.
|
||||
*
|
||||
* Das ist der Vorschlag fuer „Abteilung anlegen“ nach einem Verzeichnisabgleich:
|
||||
* die Namen stehen schon in den Daten, es fehlen nur Ziel-OU und Gruppen.
|
||||
*
|
||||
* Gruppiert wird nach **Kunde und Name**, nicht nach Name allein. Sonst gilt
|
||||
* „Vertrieb“ als gepflegt, sobald **irgendein** Kunde eine Abteilung dieses
|
||||
* Namens hat — und beim Kunden ohne sie blieben `{{ou}}` und `{{gruppen}}` im
|
||||
* Lauf leer, ohne dass diese Seite es je sagte. Genau so stand es hier, bis der
|
||||
* Review es gefunden hat; `abteilungsListe` darueber macht es seit jeher
|
||||
* richtig.
|
||||
*/
|
||||
export async function ungepflegteAbteilungen(
|
||||
benutzer: HandelnderBenutzer,
|
||||
): Promise<readonly { name: string; anzahl: number; kundeId: string }[]> {
|
||||
const mandant = await kundenBedingung(benutzer);
|
||||
const [vorhandene, benutzte] = await Promise.all([
|
||||
prisma.abteilung.findMany({ where: { AND: mandant }, select: { name: true, kundeId: true } }),
|
||||
prisma.mitarbeiter.groupBy({
|
||||
by: ["abteilung", "kundeId"],
|
||||
where: { AND: [{ aktiv: true }, { abteilung: { not: null } }, ...mandant] },
|
||||
_count: { _all: true },
|
||||
}),
|
||||
]);
|
||||
const schluessel = (kundeId: string, name: string) => `${kundeId}\u0000${name}`;
|
||||
const gepflegt = new Set(vorhandene.map((zeile) => schluessel(zeile.kundeId, zeile.name)));
|
||||
|
||||
return benutzte
|
||||
.filter(
|
||||
(eintrag) =>
|
||||
eintrag.abteilung !== null && !gepflegt.has(schluessel(eintrag.kundeId, eintrag.abteilung)),
|
||||
)
|
||||
.map((eintrag) => ({
|
||||
name: eintrag.abteilung as string,
|
||||
anzahl: eintrag._count._all,
|
||||
kundeId: eintrag.kundeId,
|
||||
}))
|
||||
.sort((a, b) => a.name.localeCompare(b.name, "de"));
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
import type { Prisma } from "@/generated/prisma/client";
|
||||
import type { GeraeteTyp } from "@/generated/prisma/enums";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { sichtbareKundenIds, type HandelnderBenutzer } from "@/server/mandant";
|
||||
|
||||
/**
|
||||
* Lesemodelle der Geraete (#011).
|
||||
*
|
||||
* Die Mandantensicht wenden diese Funktionen **selbst** an. Im Review von #009
|
||||
* war genau das die Fundstelle fuer drei Blocker: ein Suchfilter brachte ein
|
||||
* zweites `OR` mit und ueberschrieb damit die Kundenbedingung, weil beide in
|
||||
* dasselbe Objektliteral gespreizt wurden. Deshalb liegt hier **jede**
|
||||
* Teilbedingung in einem `AND`-Array, wo keine die andere ueberschreiben kann.
|
||||
* `tests/geraete.test.ts` haelt das mit einem eigenen Test fest.
|
||||
*/
|
||||
|
||||
export type GeraeteZeile = {
|
||||
readonly id: string;
|
||||
readonly tagCode: string;
|
||||
readonly hostname: string | null;
|
||||
readonly bezeichnung: string | null;
|
||||
/** Hostname, sonst Bezeichnung, sonst der Tag-Code. Nie leer. */
|
||||
readonly anzeige: string;
|
||||
readonly typ: GeraeteTyp;
|
||||
readonly modell: string | null;
|
||||
readonly seriennummer: string | null;
|
||||
readonly inventarnummer: string | null;
|
||||
readonly notizen: string | null;
|
||||
readonly archiviert: boolean;
|
||||
/** Leer heisst: vorgedruckter Tag, noch keinem Objekt zugeordnet. */
|
||||
readonly registriertAm: Date | null;
|
||||
readonly kundeId: string;
|
||||
readonly kundeName: string;
|
||||
readonly besitzerId: string | null;
|
||||
readonly besitzerName: string | null;
|
||||
readonly besitzerAktiv: boolean | null;
|
||||
readonly anzahlLaeufe: number;
|
||||
};
|
||||
|
||||
export type GeraeteFilter = {
|
||||
readonly suche?: string;
|
||||
readonly typ?: GeraeteTyp;
|
||||
readonly auchArchivierte?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Die Mandantenbedingung als **eigener** Ausdruck, nicht als Objekt zum
|
||||
* Hineinspreizen. `undefined` heisst „keine Einschraenkung“.
|
||||
*
|
||||
* Jede Geraetezeile traegt eine `kundeId` – auch im Modus INTERN, wo es die eine
|
||||
* eigene Organisation ist. Es gibt hier also kein „gilt fuer alle“ wie bei den
|
||||
* Vorlagen.
|
||||
*/
|
||||
async function kundenBedingung(benutzer: HandelnderBenutzer): Promise<Prisma.GeraetWhereInput | undefined> {
|
||||
const sichtbar = await sichtbareKundenIds(benutzer);
|
||||
if (sichtbar === undefined) return undefined;
|
||||
return { kundeId: { in: [...sichtbar] } };
|
||||
}
|
||||
|
||||
/**
|
||||
* Auswahl und Abbildung stehen getrennt, damit Liste und Einzelabruf garantiert
|
||||
* dieselben Felder liefern – zwei Modelle fuer dieselbe Zeile laufen mit der Zeit
|
||||
* auseinander.
|
||||
*/
|
||||
const ZEILEN_AUSWAHL = {
|
||||
id: true,
|
||||
tagCode: true,
|
||||
hostname: true,
|
||||
bezeichnung: true,
|
||||
typ: true,
|
||||
modell: true,
|
||||
seriennummer: true,
|
||||
inventarnummer: true,
|
||||
notizen: true,
|
||||
archiviert: true,
|
||||
registriertAm: true,
|
||||
kundeId: true,
|
||||
kunde: { select: { name: true } },
|
||||
mitarbeiter: { select: { id: true, anzeigename: true, aktiv: true } },
|
||||
_count: { select: { laeufe: true } },
|
||||
} satisfies Prisma.GeraetSelect;
|
||||
|
||||
type Rohzeile = Prisma.GeraetGetPayload<{ select: typeof ZEILEN_AUSWAHL }>;
|
||||
|
||||
function zeileBauen(zeile: Rohzeile): GeraeteZeile {
|
||||
return {
|
||||
id: zeile.id,
|
||||
tagCode: zeile.tagCode,
|
||||
hostname: zeile.hostname,
|
||||
bezeichnung: zeile.bezeichnung,
|
||||
anzeige: zeile.hostname ?? zeile.bezeichnung ?? zeile.tagCode,
|
||||
typ: zeile.typ,
|
||||
modell: zeile.modell,
|
||||
seriennummer: zeile.seriennummer,
|
||||
inventarnummer: zeile.inventarnummer,
|
||||
notizen: zeile.notizen,
|
||||
archiviert: zeile.archiviert,
|
||||
registriertAm: zeile.registriertAm,
|
||||
kundeId: zeile.kundeId,
|
||||
kundeName: zeile.kunde.name,
|
||||
besitzerId: zeile.mitarbeiter?.id ?? null,
|
||||
besitzerName: zeile.mitarbeiter?.anzeigename ?? null,
|
||||
besitzerAktiv: zeile.mitarbeiter?.aktiv ?? null,
|
||||
anzahlLaeufe: zeile._count.laeufe,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Die Suche geht ueber Hostname, Bezeichnung, Seriennummer und `tagCode`.
|
||||
*
|
||||
* Der `tagCode` ist dabei der Notnagel: wer einen Aufkleber in der Hand hat und
|
||||
* die Kamera nicht benutzen kann, tippt ihn ab. Deshalb `contains` und nicht
|
||||
* `equals` – die sechs Zeichen ohne das Praefix sollen genuegen.
|
||||
*/
|
||||
function sucheBedingung(suche: string): Prisma.GeraetWhereInput {
|
||||
const wie = { contains: suche, mode: "insensitive" as const };
|
||||
return {
|
||||
OR: [
|
||||
{ hostname: wie },
|
||||
{ bezeichnung: wie },
|
||||
{ seriennummer: wie },
|
||||
{ inventarnummer: wie },
|
||||
{ tagCode: wie },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export async function geraeteListe(
|
||||
benutzer: HandelnderBenutzer,
|
||||
filter: GeraeteFilter = {},
|
||||
): Promise<readonly GeraeteZeile[]> {
|
||||
const suche = filter.suche?.trim();
|
||||
|
||||
const bedingungen = [
|
||||
await kundenBedingung(benutzer),
|
||||
filter.auchArchivierte ? undefined : { archiviert: false },
|
||||
filter.typ ? { typ: filter.typ } : undefined,
|
||||
suche ? sucheBedingung(suche) : undefined,
|
||||
].filter((bedingung): bedingung is Prisma.GeraetWhereInput => bedingung !== undefined);
|
||||
|
||||
const zeilen = await prisma.geraet.findMany({
|
||||
where: { AND: bedingungen },
|
||||
// Archivierte nach unten, dann nach Anzeigename. Prisma kann nicht nach
|
||||
// „Hostname, sonst Bezeichnung“ sortieren; die zwei Schluessel danach kommen
|
||||
// dem nahe genug und sind stabil.
|
||||
orderBy: [{ archiviert: "asc" }, { hostname: "asc" }, { bezeichnung: "asc" }],
|
||||
select: ZEILEN_AUSWAHL,
|
||||
});
|
||||
return zeilen.map(zeileBauen);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ein einzelnes Geraet.
|
||||
*
|
||||
* `undefined` heisst „gibt es nicht oder darfst du nicht sehen“ — die
|
||||
* Unterscheidung waere schon die Auskunft, die wir nicht geben wollen.
|
||||
*/
|
||||
export async function geraetHolen(
|
||||
geraetId: string,
|
||||
benutzer: HandelnderBenutzer,
|
||||
): Promise<GeraeteZeile | undefined> {
|
||||
const mandant = await kundenBedingung(benutzer);
|
||||
const zeile = await prisma.geraet.findFirst({
|
||||
where: { AND: [{ id: geraetId }, ...(mandant ? [mandant] : [])] },
|
||||
select: ZEILEN_AUSWAHL,
|
||||
});
|
||||
return zeile ? zeileBauen(zeile) : undefined;
|
||||
}
|
||||
|
||||
export type BesitzerKandidat = {
|
||||
readonly id: string;
|
||||
readonly anzeigename: string;
|
||||
readonly aktiv: boolean;
|
||||
/** Ein Geraet kann nur einer Person desselben Kunden gehoeren. */
|
||||
readonly kundeId: string;
|
||||
};
|
||||
|
||||
export type BesitzerAuswahl = {
|
||||
readonly kandidaten: readonly BesitzerKandidat[];
|
||||
/** Waren es mehr als die Grenze? Dann sagt die Oberflaeche das auch. */
|
||||
readonly gekuerzt: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Die Personen, die als Besitzer in Frage kommen.
|
||||
*
|
||||
* Eigenes, schmales Lesemodell mit genau vier Feldern – die Geraeteseite braucht
|
||||
* nichts weiter, und die Mitarbeitersicht aus #011 wird an anderer Stelle
|
||||
* gebaut. Deaktivierte sind nicht dabei: wer offboardet, aendert den Besitzer
|
||||
* nicht mehr, sondern gibt das Geraet zurueck.
|
||||
*
|
||||
* Die Grenze ist da, damit ein Verzeichnis mit zehntausend Personen nicht in ein
|
||||
* Auswahlfeld gegossen wird. Dass gekuerzt wurde, bleibt nicht verborgen.
|
||||
*/
|
||||
export async function besitzerKandidaten(
|
||||
benutzer: HandelnderBenutzer,
|
||||
grenze = 500,
|
||||
): Promise<BesitzerAuswahl> {
|
||||
const sichtbar = await sichtbareKundenIds(benutzer);
|
||||
const zeilen = await prisma.mitarbeiter.findMany({
|
||||
where: {
|
||||
AND: [{ aktiv: true }, ...(sichtbar === undefined ? [] : [{ kundeId: { in: [...sichtbar] } }])],
|
||||
},
|
||||
select: { id: true, anzeigename: true, aktiv: true, kundeId: true },
|
||||
orderBy: { anzeigename: "asc" },
|
||||
take: grenze + 1,
|
||||
});
|
||||
return {
|
||||
/*
|
||||
* `kundeId` bleibt drin: ein Geraet kann nur einer Person **desselben**
|
||||
* Kunden gehoeren (zusammengesetzter Fremdschluessel, siehe
|
||||
* services/geraete.ts). Ohne das Feld boete die Auswahlliste Personen an,
|
||||
* die der Dienst danach abweist — eine Wahl, die sie nicht einloesen kann.
|
||||
*/
|
||||
kandidaten: zeilen
|
||||
.slice(0, grenze)
|
||||
.map(({ id, anzeigename, aktiv, kundeId }) => ({ id, anzeigename, aktiv, kundeId })),
|
||||
gekuerzt: zeilen.length > grenze,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
import type { Prisma } from "@/generated/prisma/client";
|
||||
import { Herkunft } from "@/generated/prisma/enums";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { sichtbareKundenIds, type HandelnderBenutzer } from "@/server/mandant";
|
||||
|
||||
/**
|
||||
* Lesemodelle der Mitarbeiter (#011).
|
||||
*
|
||||
* Wie ueberall wenden sie die Mandantensicht **selbst** an. Im Review von #009
|
||||
* lag genau hier die Fundstelle fuer drei Blocker: ein Suchfilter brachte ein
|
||||
* zweites `OR` mit und ueberschrieb die Kundenbedingung — ein Buchstabe im
|
||||
* Suchfeld genuegte. Deshalb liegen alle Teilbedingungen in einem `AND`, wo
|
||||
* keine die andere erreichen kann.
|
||||
*/
|
||||
|
||||
export type MitarbeiterZeile = {
|
||||
readonly id: string;
|
||||
readonly anzeigename: string;
|
||||
readonly vorname: string;
|
||||
readonly nachname: string;
|
||||
readonly mail: string | null;
|
||||
readonly upn: string | null;
|
||||
readonly sam: string | null;
|
||||
readonly abteilung: string | null;
|
||||
readonly titel: string | null;
|
||||
readonly aktiv: boolean;
|
||||
readonly herkunft: Herkunft;
|
||||
readonly kundeId: string;
|
||||
readonly kundeName: string;
|
||||
readonly anzahlGeraete: number;
|
||||
readonly anzahlLaeufe: number;
|
||||
};
|
||||
|
||||
export type MitarbeiterFilter = {
|
||||
readonly suche?: string;
|
||||
readonly abteilung?: string;
|
||||
/** Standard ist „nur aktive“ – fuers Offboarding schaltet man es um. */
|
||||
readonly auchDeaktivierte?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Die Mandantenbedingung als **Liste von Teilbedingungen**, nie als Objekt zum
|
||||
* Hineinspreizen: so kann kein zweiter Filter mit demselben Schluessel sie
|
||||
* ueberschreiben. `[]` heisst „keine Einschraenkung“.
|
||||
*
|
||||
* Dieselbe Form passt fuer Mitarbeiter und Geraete, weil beide `kundeId`
|
||||
* tragen — jede kundenbezogene Zeile tut das (#008).
|
||||
*/
|
||||
async function kundenBedingung(benutzer: HandelnderBenutzer): Promise<{ kundeId: { in: string[] } }[]> {
|
||||
const sichtbar = await sichtbareKundenIds(benutzer);
|
||||
return sichtbar === undefined ? [] : [{ kundeId: { in: [...sichtbar] } }];
|
||||
}
|
||||
|
||||
const ZEILEN_AUSWAHL = {
|
||||
id: true,
|
||||
anzeigename: true,
|
||||
vorname: true,
|
||||
nachname: true,
|
||||
mail: true,
|
||||
upn: true,
|
||||
sam: true,
|
||||
abteilung: true,
|
||||
titel: true,
|
||||
aktiv: true,
|
||||
herkunft: true,
|
||||
kundeId: true,
|
||||
kunde: { select: { name: true } },
|
||||
_count: { select: { geraete: true, laeufe: true } },
|
||||
} satisfies Prisma.MitarbeiterSelect;
|
||||
|
||||
type Rohzeile = Prisma.MitarbeiterGetPayload<{ select: typeof ZEILEN_AUSWAHL }>;
|
||||
|
||||
function zeileBauen(zeile: Rohzeile): MitarbeiterZeile {
|
||||
return {
|
||||
id: zeile.id,
|
||||
anzeigename: zeile.anzeigename,
|
||||
vorname: zeile.vorname,
|
||||
nachname: zeile.nachname,
|
||||
mail: zeile.mail,
|
||||
upn: zeile.upn,
|
||||
sam: zeile.sam,
|
||||
abteilung: zeile.abteilung,
|
||||
titel: zeile.titel,
|
||||
aktiv: zeile.aktiv,
|
||||
herkunft: zeile.herkunft,
|
||||
kundeId: zeile.kundeId,
|
||||
/*
|
||||
* Immer der Name, auch bei der eigenen Organisation. Ob er **gezeigt** wird,
|
||||
* entscheidet der Betriebsmodus — und das entscheidet die Seite mit
|
||||
* `kundenEbeneSichtbar()`, nicht das Lesemodell. Vorher haengte es hier an
|
||||
* `istEigeneOrganisation`, und im Modus Dienstleistung fehlte die Pille
|
||||
* ausgerechnet bei einem Kunden: „keine Pille“ hiess dann nicht „kein
|
||||
* Kunde“, sondern „ausgerechnet dieser eine“.
|
||||
*/
|
||||
kundeName: zeile.kunde.name,
|
||||
anzahlGeraete: zeile._count.geraete,
|
||||
anzahlLaeufe: zeile._count.laeufe,
|
||||
};
|
||||
}
|
||||
|
||||
export async function mitarbeiterListe(
|
||||
benutzer: HandelnderBenutzer,
|
||||
filter: MitarbeiterFilter = {},
|
||||
): Promise<readonly MitarbeiterZeile[]> {
|
||||
const suche = filter.suche?.trim();
|
||||
|
||||
const bedingungen = [
|
||||
...(await kundenBedingung(benutzer)),
|
||||
filter.auchDeaktivierte ? undefined : { aktiv: true },
|
||||
filter.abteilung ? { abteilung: filter.abteilung } : undefined,
|
||||
// Gesucht wird ueber die vier Felder, unter denen eine Person im Haus
|
||||
// bekannt ist. Der Anzeigename allein reicht nicht: im Ticket steht oft nur
|
||||
// die Mail-Adresse oder der sAMAccountName.
|
||||
suche
|
||||
? {
|
||||
OR: [
|
||||
{ anzeigename: { contains: suche, mode: "insensitive" as const } },
|
||||
{ mail: { contains: suche, mode: "insensitive" as const } },
|
||||
{ upn: { contains: suche, mode: "insensitive" as const } },
|
||||
{ sam: { contains: suche, mode: "insensitive" as const } },
|
||||
],
|
||||
}
|
||||
: undefined,
|
||||
].filter((bedingung) => bedingung !== undefined);
|
||||
|
||||
const zeilen = await prisma.mitarbeiter.findMany({
|
||||
where: { AND: bedingungen },
|
||||
orderBy: [{ aktiv: "desc" }, { nachname: "asc" }, { vorname: "asc" }],
|
||||
select: ZEILEN_AUSWAHL,
|
||||
});
|
||||
return zeilen.map(zeileBauen);
|
||||
}
|
||||
|
||||
/** `undefined` heisst „gibt es nicht oder darfst du nicht sehen“. */
|
||||
export async function mitarbeiterHolen(
|
||||
mitarbeiterId: string,
|
||||
benutzer: HandelnderBenutzer,
|
||||
): Promise<MitarbeiterZeile | undefined> {
|
||||
const zeile = await prisma.mitarbeiter.findFirst({
|
||||
where: { AND: [{ id: mitarbeiterId }, ...(await kundenBedingung(benutzer))] },
|
||||
select: ZEILEN_AUSWAHL,
|
||||
});
|
||||
return zeile ? zeileBauen(zeile) : undefined;
|
||||
}
|
||||
|
||||
export type MitarbeiterGeraet = {
|
||||
readonly id: string;
|
||||
readonly tagCode: string;
|
||||
readonly name: string;
|
||||
readonly typ: string;
|
||||
readonly archiviert: boolean;
|
||||
};
|
||||
|
||||
/** Die Geraete einer Person – fuer die Mitarbeiterseite. */
|
||||
export async function geraeteDesMitarbeiters(
|
||||
mitarbeiterId: string,
|
||||
benutzer: HandelnderBenutzer,
|
||||
): Promise<readonly MitarbeiterGeraet[]> {
|
||||
const zeilen = await prisma.geraet.findMany({
|
||||
where: { AND: [{ mitarbeiterId }, ...(await kundenBedingung(benutzer))] },
|
||||
orderBy: [{ archiviert: "asc" }, { hostname: "asc" }],
|
||||
select: { id: true, tagCode: true, hostname: true, bezeichnung: true, typ: true, archiviert: true },
|
||||
});
|
||||
return zeilen.map((zeile) => ({
|
||||
id: zeile.id,
|
||||
tagCode: zeile.tagCode,
|
||||
// Hostname zuerst: danach sucht man. Ohne Hostname traegt die Bezeichnung.
|
||||
name: zeile.hostname ?? zeile.bezeichnung ?? zeile.tagCode,
|
||||
typ: zeile.typ,
|
||||
archiviert: zeile.archiviert,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Der Vorgesetzte einer Person – oder `undefined`.
|
||||
*
|
||||
* Die Verknuepfung laeuft ueber `vorgesetzterExterneId` und die `externeId`
|
||||
* **derselben Verzeichnisquelle**: die Kennung ist nur innerhalb einer Quelle
|
||||
* eindeutig, und ohne diese Einschraenkung zoege ein zweites Verzeichnis
|
||||
* fremde Treffer herbei. Gefuellt wird das Feld vom Abgleich (#016); bei einer
|
||||
* von Hand angelegten Person steht hier nichts, und das ist richtig so.
|
||||
*/
|
||||
export async function vorgesetzterVon(
|
||||
mitarbeiterId: string,
|
||||
benutzer: HandelnderBenutzer,
|
||||
): Promise<{ readonly id: string; readonly anzeigename: string } | undefined> {
|
||||
const person = await prisma.mitarbeiter.findFirst({
|
||||
where: { AND: [{ id: mitarbeiterId }, ...(await kundenBedingung(benutzer))] },
|
||||
select: { quelleId: true, vorgesetzterExterneId: true },
|
||||
});
|
||||
if (!person?.vorgesetzterExterneId || !person.quelleId) return undefined;
|
||||
|
||||
const vorgesetzter = await prisma.mitarbeiter.findFirst({
|
||||
where: {
|
||||
AND: [
|
||||
{ quelleId: person.quelleId, externeId: person.vorgesetzterExterneId },
|
||||
...(await kundenBedingung(benutzer)),
|
||||
],
|
||||
},
|
||||
select: { id: true, anzeigename: true },
|
||||
});
|
||||
return vorgesetzter ?? undefined;
|
||||
}
|
||||
|
||||
/** Die vorkommenden Abteilungsnamen – fuer den Filter. */
|
||||
export async function abteilungsNamen(benutzer: HandelnderBenutzer): Promise<readonly string[]> {
|
||||
const zeilen = await prisma.mitarbeiter.findMany({
|
||||
where: { AND: [{ abteilung: { not: null } }, ...(await kundenBedingung(benutzer))] },
|
||||
select: { abteilung: true },
|
||||
distinct: ["abteilung"],
|
||||
});
|
||||
return zeilen
|
||||
.map((zeile) => zeile.abteilung)
|
||||
.filter((name): name is string => name !== null)
|
||||
.sort((a, b) => a.localeCompare(b, "de"));
|
||||
}
|
||||
Reference in New Issue
Block a user