feat(tags): QR-Aufkleber, Geraeteseite, Etikettendruck und Scanner
Der Einstieg, fuer den die Anwendung gebaut ist: scannen, sehen was zu tun ist,
loslegen. `/t/<tagCode>` kennt drei Faelle — registriertes Geraet, vorgedruckter
Tag, unbekannter Code —, die Geraeteseite zeigt Zuweisungen, Schnellstart und
Historie, und beides ist am Handy bedienbar.
Im QR steht ausschliesslich `{APP_URL}/t/{tagCode}`: kein Name, keine
Seriennummer, kein Besitzer. Ein Aufkleber haengt jahrelang sichtbar am Geraet
und wird von jedem Telefon gelesen; was darauf steht, ist veroeffentlicht. Die
Masse sind gemessen, nicht behauptet: ein echtes PDF liefert 62 x 29 mm und A4,
und ein bei 300 dpi gerastertes Etikett wird mit demselben Decoder
zurueckgelesen, den auch der Scanner benutzt.
Drei Defekte zeigte erst der gebaute Stand, alle an den Naehten zwischen den
parallel gebauten Teilen: „Starten“ setzte IN_ARBEIT und sprang nicht, „Vorlage
zuweisen“ bot Vorlagen an, die der Dienst danach ablehnte, und die Geraeteliste
verlinkte nichts. Der unabhaengige Review fand drei Blocker — darunter, dass der
Scanner nie die native BarcodeDetector-API benutzte und dass ein Unterpfad in
APP_URL aus dem gedruckten QR herausfiel.
Closes #14
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit was merged in pull request #28.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import type { Prisma } from "@/generated/prisma/client";
|
||||
import type { GeraeteTyp } from "@/generated/prisma/enums";
|
||||
import { LaufStatus, SchrittZustand, VersionStatus, type GeraeteTyp } from "@/generated/prisma/enums";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { sichtbareKundenIds, type HandelnderBenutzer } from "@/server/mandant";
|
||||
|
||||
@@ -34,6 +34,8 @@ export type GeraeteZeile = {
|
||||
readonly besitzerId: string | null;
|
||||
readonly besitzerName: string | null;
|
||||
readonly besitzerAktiv: boolean | null;
|
||||
/** Der rohe `department`-Wert der Person – die Geraeteseite nennt ihn im Kopf (#014). */
|
||||
readonly besitzerAbteilung: string | null;
|
||||
readonly anzahlLaeufe: number;
|
||||
};
|
||||
|
||||
@@ -76,7 +78,7 @@ const ZEILEN_AUSWAHL = {
|
||||
registriertAm: true,
|
||||
kundeId: true,
|
||||
kunde: { select: { name: true } },
|
||||
mitarbeiter: { select: { id: true, anzeigename: true, aktiv: true } },
|
||||
mitarbeiter: { select: { id: true, anzeigename: true, aktiv: true, abteilung: true } },
|
||||
_count: { select: { laeufe: true } },
|
||||
} satisfies Prisma.GeraetSelect;
|
||||
|
||||
@@ -101,6 +103,7 @@ function zeileBauen(zeile: Rohzeile): GeraeteZeile {
|
||||
besitzerId: zeile.mitarbeiter?.id ?? null,
|
||||
besitzerName: zeile.mitarbeiter?.anzeigename ?? null,
|
||||
besitzerAktiv: zeile.mitarbeiter?.aktiv ?? null,
|
||||
besitzerAbteilung: zeile.mitarbeiter?.abteilung ?? null,
|
||||
anzahlLaeufe: zeile._count.laeufe,
|
||||
};
|
||||
}
|
||||
@@ -167,6 +170,258 @@ export async function geraetHolen(
|
||||
return zeile ? zeileBauen(zeile) : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Das Geraet zu einem Tag-Code – der Einstieg von `/t/<code>` (#014).
|
||||
*
|
||||
* **Gross- und Kleinschreibung sind unerheblich.** Der Code wird abgetippt, und
|
||||
* ein Handy schreibt von sich aus klein; wer `lz-7k3f9q` eingibt, meint den
|
||||
* Aufkleber, auf dem `LZ-7K3F9Q` steht. Weiter geht die Nachsicht bewusst
|
||||
* nicht: verwechselbare Zeichen bleiben stehen, wie sie eingegeben wurden. Ein
|
||||
* `I` auf ein `L` zu biegen koennte auf ein **anderes, vorhandenes** Geraet
|
||||
* fuehren, und wer vor dem Geraet steht, saehe den Irrtum nie.
|
||||
*
|
||||
* `undefined` heisst „gibt es nicht oder darfst du nicht sehen“ – dieselbe
|
||||
* Antwort, aus demselben Grund wie in `geraetHolen`.
|
||||
*/
|
||||
export async function geraetNachTagCode(
|
||||
tagCode: string,
|
||||
benutzer: HandelnderBenutzer,
|
||||
): Promise<GeraeteZeile | undefined> {
|
||||
const code = tagCode.trim().toUpperCase();
|
||||
if (code === "") return undefined;
|
||||
const mandant = await kundenBedingung(benutzer);
|
||||
const zeile = await prisma.geraet.findFirst({
|
||||
where: { AND: [{ tagCode: code }, ...(mandant ? [mandant] : [])] },
|
||||
select: ZEILEN_AUSWAHL,
|
||||
});
|
||||
return zeile ? zeileBauen(zeile) : undefined;
|
||||
}
|
||||
|
||||
export type GeraetLaufZeile = {
|
||||
readonly id: string;
|
||||
readonly titel: string;
|
||||
readonly status: LaufStatus;
|
||||
readonly vorlagenName: string;
|
||||
readonly version: number;
|
||||
readonly zugewiesenAn: string | null;
|
||||
readonly faelligAm: Date | null;
|
||||
readonly erstelltAm: Date;
|
||||
readonly abgeschlossenAm: Date | null;
|
||||
readonly anzahlSchritte: number;
|
||||
readonly anzahlErledigt: number;
|
||||
};
|
||||
|
||||
export type GeraetLaeufe = {
|
||||
/** `OFFEN` und `IN_ARBEIT` – die Arbeit, die an diesem Geraet wartet. */
|
||||
readonly zugewiesen: readonly GeraetLaufZeile[];
|
||||
/** `ABGESCHLOSSEN` und `ARCHIVIERT`, das Jüngste zuerst. */
|
||||
readonly historie: readonly GeraetLaufZeile[];
|
||||
/** Waren es mehr als `HISTORIE_HOECHSTENS`? Dann sagt die Seite das auch. */
|
||||
readonly historieGekuerzt: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* So viele abgeschlossene Laeufe zeigt die Geraeteseite.
|
||||
*
|
||||
* Die Zuweisungen bleiben ungekappt: was offen ist, muss vollstaendig sichtbar
|
||||
* sein, und mehr als eine Handvoll gleichzeitig hat ein Geraet nie. Die Historie
|
||||
* dagegen waechst ueber Jahre; ungekappt waere die Seite am Handy irgendwann eine
|
||||
* Chronik statt eines Einstiegs.
|
||||
*/
|
||||
export const HISTORIE_HOECHSTENS = 20;
|
||||
|
||||
const LAUF_AUSWAHL = {
|
||||
id: true,
|
||||
titel: true,
|
||||
status: true,
|
||||
faelligAm: true,
|
||||
erstelltAm: true,
|
||||
abgeschlossenAm: true,
|
||||
zugewiesenAn: { select: { name: true } },
|
||||
vorlagenVersion: { select: { version: true, vorlage: { select: { name: true } } } },
|
||||
ergebnisse: { select: { zustand: true } },
|
||||
} satisfies Prisma.LaufSelect;
|
||||
|
||||
type RohLauf = Prisma.LaufGetPayload<{ select: typeof LAUF_AUSWAHL }>;
|
||||
|
||||
function laufZeileBauen(zeile: RohLauf): GeraetLaufZeile {
|
||||
return {
|
||||
id: zeile.id,
|
||||
titel: zeile.titel,
|
||||
status: zeile.status,
|
||||
vorlagenName: zeile.vorlagenVersion.vorlage.name,
|
||||
version: zeile.vorlagenVersion.version,
|
||||
zugewiesenAn: zeile.zugewiesenAn?.name ?? null,
|
||||
faelligAm: zeile.faelligAm,
|
||||
erstelltAm: zeile.erstelltAm,
|
||||
abgeschlossenAm: zeile.abgeschlossenAm,
|
||||
anzahlSchritte: zeile.ergebnisse.length,
|
||||
// Erledigt heisst hier „vom Tisch“: `NICHT_ZUTREFFEND` zaehlt mit,
|
||||
// `FEHLGESCHLAGEN` nicht – genau wie in der Laufliste und beim Abschluss.
|
||||
anzahlErledigt: zeile.ergebnisse.filter(
|
||||
(ergebnis) =>
|
||||
ergebnis.zustand === SchrittZustand.ERLEDIGT || ergebnis.zustand === SchrittZustand.NICHT_ZUTREFFEND,
|
||||
).length,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Die Laeufe eines Geraets, getrennt in Zuweisungen und Historie (#014).
|
||||
*
|
||||
* Zwei Abfragen statt einer, weil beide Haelften verschieden sortiert und
|
||||
* verschieden gekappt sind: das Offene nach Faelligkeit und vollstaendig, das
|
||||
* Erledigte nach Abschluss und begrenzt.
|
||||
*
|
||||
* Die Mandantensicht steht auch hier im Lesemodell selbst – ein Lauf traegt seine
|
||||
* eigene `kundeId`, und die Bedingung greift auf ihr. Die `geraetId` allein
|
||||
* duerfte nichts oeffnen.
|
||||
*/
|
||||
export async function laeufeDesGeraets(
|
||||
geraetId: string,
|
||||
benutzer: HandelnderBenutzer,
|
||||
): Promise<GeraetLaeufe> {
|
||||
const sichtbar = await sichtbareKundenIds(benutzer);
|
||||
const mandant: Prisma.LaufWhereInput[] = sichtbar === undefined ? [] : [{ kundeId: { in: [...sichtbar] } }];
|
||||
|
||||
const [zugewiesen, historie] = await Promise.all([
|
||||
prisma.lauf.findMany({
|
||||
where: {
|
||||
AND: [{ geraetId }, { status: { in: [LaufStatus.OFFEN, LaufStatus.IN_ARBEIT] } }, ...mandant],
|
||||
},
|
||||
orderBy: [{ faelligAm: { sort: "asc", nulls: "last" } }, { erstelltAm: "desc" }],
|
||||
select: LAUF_AUSWAHL,
|
||||
}),
|
||||
prisma.lauf.findMany({
|
||||
where: {
|
||||
AND: [
|
||||
{ geraetId },
|
||||
{ status: { in: [LaufStatus.ABGESCHLOSSEN, LaufStatus.ARCHIVIERT] } },
|
||||
...mandant,
|
||||
],
|
||||
},
|
||||
// Abgeschlossen zuerst, und wo das fehlt, nach Anlage – ein archivierter
|
||||
// Lauf ohne `abgeschlossenAm` faellt sonst ans Ende der Chronik.
|
||||
orderBy: [{ abgeschlossenAm: { sort: "desc", nulls: "last" } }, { erstelltAm: "desc" }],
|
||||
take: HISTORIE_HOECHSTENS + 1,
|
||||
select: LAUF_AUSWAHL,
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
zugewiesen: zugewiesen.map(laufZeileBauen),
|
||||
historie: historie.slice(0, HISTORIE_HOECHSTENS).map(laufZeileBauen),
|
||||
historieGekuerzt: historie.length > HISTORIE_HOECHSTENS,
|
||||
};
|
||||
}
|
||||
|
||||
export type GeraeteVorlage = {
|
||||
readonly versionId: string;
|
||||
readonly vorlageId: string;
|
||||
readonly name: string;
|
||||
readonly version: number;
|
||||
readonly kategorie: string | null;
|
||||
/**
|
||||
* Hat diese Version Pflichtvariablen?
|
||||
*
|
||||
* Der Schnellstart legt einen Lauf mit einem Tipp an und kann dabei nichts
|
||||
* erfragen. Eine Vorlage, die eine Pflichtangabe verlangt, wuerde `laufAnlegen`
|
||||
* deshalb abweisen — sie fuehrt statt dessen in den Start-Dialog, der fragen
|
||||
* kann. Die Regel selbst bleibt im Dienst; dieses Feld entscheidet nur, welcher
|
||||
* der beiden Wege angeboten wird.
|
||||
*/
|
||||
readonly pflichtangaben: boolean;
|
||||
};
|
||||
|
||||
export type VorlagenFuerGeraet = {
|
||||
/** Die Vorgaben aus `GeraetetypVorlage` fuer diesen Typ, in ihrer Reihenfolge. */
|
||||
readonly schnellstart: readonly GeraeteVorlage[];
|
||||
/** Alles, was sich an diesem Geraet starten laesst – fuer „Vorlage zuweisen“. */
|
||||
readonly alle: readonly GeraeteVorlage[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Die Vorlagen, die an diesem Geraet in Frage kommen (#014).
|
||||
*
|
||||
* Je Vorlage genau die **zuletzt veroeffentlichte** Version: wer an einem Geraet
|
||||
* losgeht, will die geltende Fassung, und ein Entwurf ist noch niemandes
|
||||
* Arbeitsanweisung. Eine Vorlage ohne veroeffentlichte Version steht deshalb
|
||||
* nicht dabei, eine archivierte auch nicht.
|
||||
*
|
||||
* Gefiltert wird auf den Kunden **des Geraets** und nicht nur auf die Sicht des
|
||||
* Benutzers: ein Administrator sieht im Modus Dienstleistung alle Kunden, und
|
||||
* eine kundenspezifische Vorlage von Kunde B liesse sich an einem Geraet von
|
||||
* Kunde A nicht starten — `laufAnlegen` weist das ab. Eine Auswahl anzubieten,
|
||||
* die der Dienst danach ablehnt, ist keine Auswahl.
|
||||
*/
|
||||
export async function vorlagenFuerGeraet(
|
||||
geraet: { readonly typ: GeraeteTyp; readonly kundeId: string },
|
||||
benutzer: HandelnderBenutzer,
|
||||
): Promise<VorlagenFuerGeraet> {
|
||||
const sichtbar = await sichtbareKundenIds(benutzer);
|
||||
// Die Mandantensicht auch hier selbst: ohne sie liesse sich mit der `kundeId`
|
||||
// eines fremden Geraets erfragen, welche Vorlagen dieser Kunde hat.
|
||||
if (sichtbar !== undefined && !sichtbar.includes(geraet.kundeId)) {
|
||||
return { schnellstart: [], alle: [] };
|
||||
}
|
||||
|
||||
const vorlagen = await prisma.vorlage.findMany({
|
||||
where: {
|
||||
AND: [
|
||||
{ archiviert: false },
|
||||
// Eine Vorlage ohne Kundenbezug gilt fuer alle (#009).
|
||||
{ OR: [{ kundeId: null }, { kundeId: geraet.kundeId }] },
|
||||
],
|
||||
},
|
||||
orderBy: [{ kategorie: "asc" }, { name: "asc" }],
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
kategorie: true,
|
||||
versionen: {
|
||||
where: { status: VersionStatus.VEROEFFENTLICHT },
|
||||
orderBy: { version: "desc" },
|
||||
take: 1,
|
||||
select: {
|
||||
id: true,
|
||||
version: true,
|
||||
// Nur ob es welche gibt, nicht welche: mehr braucht die Entscheidung
|
||||
// zwischen Schnellstart und Start-Dialog nicht.
|
||||
variablen: { where: { pflicht: true }, select: { schluessel: true }, take: 1 },
|
||||
},
|
||||
},
|
||||
typVorgaben: { where: { geraeteTyp: geraet.typ }, select: { position: true } },
|
||||
},
|
||||
});
|
||||
|
||||
const alle: GeraeteVorlage[] = [];
|
||||
const schnellstart: { eintrag: GeraeteVorlage; position: number }[] = [];
|
||||
|
||||
for (const vorlage of vorlagen) {
|
||||
const version = vorlage.versionen[0];
|
||||
if (!version) continue;
|
||||
const eintrag: GeraeteVorlage = {
|
||||
versionId: version.id,
|
||||
vorlageId: vorlage.id,
|
||||
name: vorlage.name,
|
||||
version: version.version,
|
||||
kategorie: vorlage.kategorie,
|
||||
pflichtangaben: version.variablen.length > 0,
|
||||
};
|
||||
alle.push(eintrag);
|
||||
const vorgabe = vorlage.typVorgaben[0];
|
||||
if (vorgabe) schnellstart.push({ eintrag, position: vorgabe.position });
|
||||
}
|
||||
|
||||
return {
|
||||
schnellstart: schnellstart
|
||||
// Die Position ist die gepflegte Reihenfolge; bei gleicher Position bleibt
|
||||
// die Sortierung der Abfrage (Kategorie, Name) erhalten.
|
||||
.sort((links, rechts) => links.position - rechts.position)
|
||||
.map((eintrag) => eintrag.eintrag),
|
||||
alle,
|
||||
};
|
||||
}
|
||||
|
||||
export type BesitzerKandidat = {
|
||||
readonly id: string;
|
||||
readonly anzeigename: string;
|
||||
@@ -195,11 +450,25 @@ export type BesitzerAuswahl = {
|
||||
export async function besitzerKandidaten(
|
||||
benutzer: HandelnderBenutzer,
|
||||
grenze = 500,
|
||||
/**
|
||||
* Nur Personen dieses Kunden.
|
||||
*
|
||||
* Gehoert in die **Abfrage** und nicht hinter sie: wer erst 500 Personen aller
|
||||
* sichtbaren Kunden holt und danach filtert, hat bei einem grossen
|
||||
* Dienstleister womoeglich keine einzige passende in der Hand — und die
|
||||
* Meldung „nur die ersten 500“ stimmt dann auch nicht mehr (Reviewbefund zu
|
||||
* #014).
|
||||
*/
|
||||
kundeId?: string,
|
||||
): Promise<BesitzerAuswahl> {
|
||||
const sichtbar = await sichtbareKundenIds(benutzer);
|
||||
const zeilen = await prisma.mitarbeiter.findMany({
|
||||
where: {
|
||||
AND: [{ aktiv: true }, ...(sichtbar === undefined ? [] : [{ kundeId: { in: [...sichtbar] } }])],
|
||||
AND: [
|
||||
{ aktiv: true },
|
||||
...(sichtbar === undefined ? [] : [{ kundeId: { in: [...sichtbar] } }]),
|
||||
...(kundeId === undefined ? [] : [{ kundeId }]),
|
||||
],
|
||||
},
|
||||
select: { id: true, anzeigename: true, aktiv: true, kundeId: true },
|
||||
orderBy: { anzeigename: "asc" },
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
import type { Prisma } from "@/generated/prisma/client";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { istTagCode } from "@/lib/tagcode";
|
||||
import { sichtbareKundenIds, type HandelnderBenutzer } from "@/server/mandant";
|
||||
|
||||
/**
|
||||
* Lesemodelle der Tags und Etiketten (#014).
|
||||
*
|
||||
* Ein „Tag“ ist kein eigenes Modell: es ist ein `Geraet` mit
|
||||
* `registriertAm = null`, also ein vorgedruckter Aufkleber, dem noch kein Objekt
|
||||
* zugeordnet ist. Deshalb liest dieses Modul aus `Geraet` — nur mit den Feldern,
|
||||
* die auf ein Etikett passen, und nicht mit denen der Geraeteliste.
|
||||
*
|
||||
* **Die Mandantensicht wendet jede Funktion hier selbst an**, in einem
|
||||
* `AND`-Array, in dem keine Teilbedingung eine andere ueberschreiben kann (der
|
||||
* Befund aus dem Review von #009). Ein Etikett nennt Hostname und Besitzer; ein
|
||||
* Bogen mit fremden Kunden darauf waere ein Datenleck auf Papier.
|
||||
*/
|
||||
|
||||
export type EtikettZeile = {
|
||||
readonly id: string;
|
||||
readonly tagCode: string;
|
||||
readonly hostname: string | null;
|
||||
readonly bezeichnung: string | null;
|
||||
/** Hostname, sonst Bezeichnung, sonst leer — ein Tag ohne Zuordnung hat keinen Namen. */
|
||||
readonly anzeige: string | null;
|
||||
readonly besitzerName: string | null;
|
||||
/** Steht nur im Modus DIENSTLEISTUNG auf dem Etikett (#008). */
|
||||
readonly kundeKuerzel: string;
|
||||
readonly kundeName: string;
|
||||
/** Leer heisst: vorgedruckter Tag, noch keinem Objekt zugeordnet. */
|
||||
readonly registriertAm: Date | null;
|
||||
readonly erstelltAm: Date;
|
||||
};
|
||||
|
||||
/**
|
||||
* Die Mandantenbedingung als eigener Ausdruck. `undefined` heisst „keine
|
||||
* Einschraenkung“ und ist etwas anderes als eine leere Liste (siehe
|
||||
* `sichtbareKundenIds`).
|
||||
*/
|
||||
async function kundenBedingung(benutzer: HandelnderBenutzer): Promise<Prisma.GeraetWhereInput | undefined> {
|
||||
const sichtbar = await sichtbareKundenIds(benutzer);
|
||||
if (sichtbar === undefined) return undefined;
|
||||
return { kundeId: { in: [...sichtbar] } };
|
||||
}
|
||||
|
||||
const ETIKETT_AUSWAHL = {
|
||||
id: true,
|
||||
tagCode: true,
|
||||
hostname: true,
|
||||
bezeichnung: true,
|
||||
registriertAm: true,
|
||||
erstelltAm: true,
|
||||
kunde: { select: { name: true, kuerzel: true } },
|
||||
mitarbeiter: { select: { anzeigename: true } },
|
||||
} satisfies Prisma.GeraetSelect;
|
||||
|
||||
type Rohzeile = Prisma.GeraetGetPayload<{ select: typeof ETIKETT_AUSWAHL }>;
|
||||
|
||||
function zeileBauen(zeile: Rohzeile): EtikettZeile {
|
||||
return {
|
||||
id: zeile.id,
|
||||
tagCode: zeile.tagCode,
|
||||
hostname: zeile.hostname,
|
||||
bezeichnung: zeile.bezeichnung,
|
||||
anzeige: zeile.hostname ?? zeile.bezeichnung,
|
||||
besitzerName: zeile.mitarbeiter?.anzeigename ?? null,
|
||||
kundeKuerzel: zeile.kunde.kuerzel,
|
||||
kundeName: zeile.kunde.name,
|
||||
registriertAm: zeile.registriertAm,
|
||||
erstelltAm: zeile.erstelltAm,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Wie viele Zeilen ein Aufruf hoechstens liefert.
|
||||
*
|
||||
* Ein Bogen fasst 24 Etiketten; 500 sind gut 20 Bogen und damit mehr, als jemand
|
||||
* in einem Zug druckt. Die Grenze steht hier, damit die Seite nach dem dritten
|
||||
* Stapel nicht tausende Zeilen in ein Dokument gieszt.
|
||||
*/
|
||||
export const TAGS_LISTE_GRENZE = 500;
|
||||
|
||||
/**
|
||||
* Die noch nicht zugeordneten Tags.
|
||||
*
|
||||
* Neueste zuerst: gefragt ist immer der Stapel von gerade, nicht der vom letzten
|
||||
* Jahr. Archivierte bleiben aussen vor — ein archivierter Aufkleber soll nicht
|
||||
* noch einmal gedruckt werden.
|
||||
*/
|
||||
export async function unregistrierteTags(
|
||||
benutzer: HandelnderBenutzer,
|
||||
grenze: number = TAGS_LISTE_GRENZE,
|
||||
): Promise<readonly EtikettZeile[]> {
|
||||
const mandant = await kundenBedingung(benutzer);
|
||||
const zeilen = await prisma.geraet.findMany({
|
||||
where: { AND: [{ registriertAm: null }, { archiviert: false }, ...(mandant ? [mandant] : [])] },
|
||||
orderBy: [{ erstelltAm: "desc" }, { tagCode: "asc" }],
|
||||
take: grenze,
|
||||
select: ETIKETT_AUSWAHL,
|
||||
});
|
||||
return zeilen.map(zeileBauen);
|
||||
}
|
||||
|
||||
/**
|
||||
* Die Etikettenzeilen zu bestimmten Tag-Codes, in genau der uebergebenen
|
||||
* Reihenfolge.
|
||||
*
|
||||
* Die Reihenfolge ist keine Kosmetik: ein Bogen wird ausgeschnitten und der
|
||||
* Reihe nach aufgeklebt, und wer zwei Bogen desselben Stapels nacheinander
|
||||
* druckt, erwartet dieselbe Anordnung. Codes, die es nicht gibt oder die zu
|
||||
* einem fremden Kunden gehoeren, fehlen still — die Unterscheidung waere schon
|
||||
* die Auskunft, die wir nicht geben wollen.
|
||||
*/
|
||||
export async function etikettenZuCodes(
|
||||
codes: readonly string[],
|
||||
benutzer: HandelnderBenutzer,
|
||||
): Promise<readonly EtikettZeile[]> {
|
||||
// Ein formal falscher Code kann keine Zeile treffen. Ihn gar nicht erst zu
|
||||
// fragen haelt die Abfrage klein, wenn jemand die Adresse von Hand baut.
|
||||
const gewuenscht = [...new Set(codes.map((code) => code.trim().toUpperCase()))].filter(istTagCode);
|
||||
if (gewuenscht.length === 0) return [];
|
||||
|
||||
const mandant = await kundenBedingung(benutzer);
|
||||
const zeilen = await prisma.geraet.findMany({
|
||||
where: { AND: [{ tagCode: { in: gewuenscht } }, ...(mandant ? [mandant] : [])] },
|
||||
take: TAGS_LISTE_GRENZE,
|
||||
select: ETIKETT_AUSWAHL,
|
||||
});
|
||||
|
||||
const nachCode = new Map(zeilen.map((zeile) => [zeile.tagCode, zeileBauen(zeile)]));
|
||||
return gewuenscht
|
||||
.map((code) => nachCode.get(code))
|
||||
.filter((zeile): zeile is EtikettZeile => zeile !== undefined);
|
||||
}
|
||||
Reference in New Issue
Block a user