Baustein 6: Checklisten mit Durchlaeufen und PDF-Export
- Durchlauf aus einer Vorlage, mit Datum, Ticket und Kunde - Schritt fuer Schritt abhaken, Bemerkung je Schritt - Fortschritt gesamt und je Gruppe (Vorbereiten/Durchfuehren/Pruefen/Abschliessen) - Abschliessen erst, wenn alles abgehakt ist; Meldung nennt die offenen Schritte - Abgeschlossene Durchlaeufe sind gesperrt und bleiben als Nachweis - Titel und Schritttexte werden beim Start kopiert, nicht verwiesen - PDF-Export mit gezeichneten Ankreuzkaestchen und Unterschriftenzeilen - Gemeinsame PDF-Grundlage fuer alle kuenftigen Ausgaben - Ausbilder liest mit und darf das PDF abrufen, hakt aber nicht ab - Abgeschlossene Durchlaeufe zaehlen im Wochenfortschritt mit Behoben beim Durchstich: Die Fusszeile wurde unterhalb des Satzspiegels gesetzt, wodurch PDFKit je Fusszeile eine leere Seite anhaengte - aus zwei Seiten wurden sechs. Ein Test zaehlt jetzt die Seitenobjekte im PDF. 29 neue Tests, insgesamt 186. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
ffbca01bdc
commit
b9d6041f3b
@@ -0,0 +1,163 @@
|
||||
import crypto from 'node:crypto';
|
||||
import { and, desc, eq } from 'drizzle-orm';
|
||||
import { datenbank, schema } from '../db';
|
||||
import { heute, jetzt } from '../zeit';
|
||||
import { laufstand, type Laufstand, type Schrittstand } from '../checklisten/fortschritt';
|
||||
import type { Checkliste } from '../inhalte/schema';
|
||||
import type { ChecklisteLauf, ChecklisteSchritt } from '../db/schema';
|
||||
|
||||
/** Checklisten-Durchläufe. Die Vorlage kommt aus der Datei, der Lauf aus der Datenbank. */
|
||||
|
||||
export function laeufeVon(benutzerId: string): ChecklisteLauf[] {
|
||||
return datenbank()
|
||||
.select()
|
||||
.from(schema.checklisteLauf)
|
||||
.where(eq(schema.checklisteLauf.benutzerId, benutzerId))
|
||||
.orderBy(desc(schema.checklisteLauf.datum), desc(schema.checklisteLauf.gestartetAm))
|
||||
.all();
|
||||
}
|
||||
|
||||
export function laufNachId(id: string): ChecklisteLauf | null {
|
||||
return datenbank().select().from(schema.checklisteLauf).where(eq(schema.checklisteLauf.id, id)).get() ?? null;
|
||||
}
|
||||
|
||||
export function schritteVon(laufId: string): ChecklisteSchritt[] {
|
||||
return datenbank()
|
||||
.select()
|
||||
.from(schema.checklisteSchritt)
|
||||
.where(eq(schema.checklisteSchritt.laufId, laufId))
|
||||
.all();
|
||||
}
|
||||
|
||||
/** Rechnet den Stand eines Durchlaufs aus. */
|
||||
export function standVon(laufId: string, titelJeGruppe?: Record<string, string>): Laufstand {
|
||||
const schritte: Schrittstand[] = schritteVon(laufId).map((z) => ({
|
||||
gruppeId: z.gruppeId,
|
||||
schrittId: z.schrittId,
|
||||
text: z.text,
|
||||
reihenfolge: z.reihenfolge,
|
||||
erledigt: z.erledigt === 1,
|
||||
bemerkung: z.bemerkung,
|
||||
}));
|
||||
return laufstand(schritte, titelJeGruppe);
|
||||
}
|
||||
|
||||
/**
|
||||
* Startet einen Durchlauf aus einer Vorlage.
|
||||
*
|
||||
* Titel und Schritttexte werden kopiert, nicht verwiesen: Ein abgeschlossener
|
||||
* Durchlauf ist ein Nachweis und muss auch dann noch lesbar sein, wenn die
|
||||
* Vorlage im Repository später geändert wird.
|
||||
*/
|
||||
export function laufStarten(werte: {
|
||||
benutzerId: string;
|
||||
vorlage: Checkliste;
|
||||
datum?: string;
|
||||
ticket?: string | null;
|
||||
kunde?: string | null;
|
||||
}): ChecklisteLauf {
|
||||
const db = datenbank();
|
||||
const id = crypto.randomUUID();
|
||||
|
||||
const lauf: ChecklisteLauf = {
|
||||
id,
|
||||
benutzerId: werte.benutzerId,
|
||||
checklisteId: werte.vorlage.id,
|
||||
titel: werte.vorlage.titel,
|
||||
datum: werte.datum || heute(),
|
||||
ticket: werte.ticket?.trim() || null,
|
||||
kunde: werte.kunde?.trim() || null,
|
||||
bemerkung: null,
|
||||
gestartetAm: jetzt(),
|
||||
abgeschlossenAm: null,
|
||||
};
|
||||
|
||||
const schritte: ChecklisteSchritt[] = [];
|
||||
let reihenfolge = 0;
|
||||
for (const gruppe of werte.vorlage.gruppen) {
|
||||
for (const schritt of gruppe.schritte) {
|
||||
schritte.push({
|
||||
id: crypto.randomUUID(),
|
||||
laufId: id,
|
||||
gruppeId: gruppe.id,
|
||||
schrittId: schritt.id,
|
||||
text: schritt.text,
|
||||
reihenfolge: (reihenfolge += 1),
|
||||
erledigt: 0,
|
||||
bemerkung: null,
|
||||
erledigtAm: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Drizzle führt die Transaktion sofort aus – anders als better-sqlite3,
|
||||
// wo `transaction()` erst eine aufrufbare Funktion zurückgibt.
|
||||
db.transaction((tx) => {
|
||||
tx.insert(schema.checklisteLauf).values(lauf).run();
|
||||
if (schritte.length > 0) tx.insert(schema.checklisteSchritt).values(schritte).run();
|
||||
});
|
||||
|
||||
return lauf;
|
||||
}
|
||||
|
||||
export function schrittSetzen(laufId: string, schrittId: string, erledigt: boolean): void {
|
||||
datenbank()
|
||||
.update(schema.checklisteSchritt)
|
||||
.set({ erledigt: erledigt ? 1 : 0, erledigtAm: erledigt ? jetzt() : null })
|
||||
.where(and(eq(schema.checklisteSchritt.laufId, laufId), eq(schema.checklisteSchritt.schrittId, schrittId)))
|
||||
.run();
|
||||
}
|
||||
|
||||
export function bemerkungSetzen(laufId: string, schrittId: string, bemerkung: string): void {
|
||||
datenbank()
|
||||
.update(schema.checklisteSchritt)
|
||||
.set({ bemerkung: bemerkung.trim() || null })
|
||||
.where(and(eq(schema.checklisteSchritt.laufId, laufId), eq(schema.checklisteSchritt.schrittId, schrittId)))
|
||||
.run();
|
||||
}
|
||||
|
||||
export function laufBemerkung(laufId: string, bemerkung: string): void {
|
||||
datenbank()
|
||||
.update(schema.checklisteLauf)
|
||||
.set({ bemerkung: bemerkung.trim() || null })
|
||||
.where(eq(schema.checklisteLauf.id, laufId))
|
||||
.run();
|
||||
}
|
||||
|
||||
export function laufKopfdaten(laufId: string, werte: { datum: string; ticket: string | null; kunde: string | null }): void {
|
||||
datenbank()
|
||||
.update(schema.checklisteLauf)
|
||||
.set({ datum: werte.datum, ticket: werte.ticket?.trim() || null, kunde: werte.kunde?.trim() || null })
|
||||
.where(eq(schema.checklisteLauf.id, laufId))
|
||||
.run();
|
||||
}
|
||||
|
||||
export function laufAbschliessen(laufId: string): void {
|
||||
datenbank()
|
||||
.update(schema.checklisteLauf)
|
||||
.set({ abgeschlossenAm: jetzt() })
|
||||
.where(eq(schema.checklisteLauf.id, laufId))
|
||||
.run();
|
||||
}
|
||||
|
||||
export function laufWiederOeffnen(laufId: string): void {
|
||||
datenbank()
|
||||
.update(schema.checklisteLauf)
|
||||
.set({ abgeschlossenAm: null })
|
||||
.where(eq(schema.checklisteLauf.id, laufId))
|
||||
.run();
|
||||
}
|
||||
|
||||
export function laufLoeschen(laufId: string): void {
|
||||
datenbank().delete(schema.checklisteLauf).where(eq(schema.checklisteLauf.id, laufId)).run();
|
||||
}
|
||||
|
||||
/** Wie viele Durchläufe der genannten Vorlagen hat dieser Benutzer abgeschlossen? */
|
||||
export function anzahlAbgeschlossen(benutzerId: string, checklistenIds: string[]): number {
|
||||
if (checklistenIds.length === 0) return 0;
|
||||
const laeufe = laeufeVon(benutzerId);
|
||||
const erledigte = new Set(
|
||||
laeufe.filter((l) => l.abgeschlossenAm !== null).map((l) => l.checklisteId),
|
||||
);
|
||||
return checklistenIds.filter((id) => erledigte.has(id)).length;
|
||||
}
|
||||
Reference in New Issue
Block a user