Baustein 7: Berichtsheft mit Vorschlaegen, Pruefung, Abzeichnen und PDF
- Taegliche Stichworte, je Tag ein eigenes Formular - Wochenblatt mit Betrieb, Berufsschule und Sonstiges, je mit Stunden - Vorschlaege aus Tagesnotizen, Aufgaben, Checklisten und Handouts - Pruefung mitlaufend beim Tippen: fehlende Stunden, Floskeln, IP- und MAC-Adressen, E-Mail, Zugangsdaten, Kundennamen - Warnung, keine Sperre - Einreichen ist immer moeglich - Formulierungshilfe mit Taetigkeitsverben und Gegenueberstellungen - Einreichen, zurueckgeben mit Anmerkung, abzeichnen; danach gesperrt - Fehlende Wochen werden benannt und lassen sich nachtragen; die laufende Nummer richtet sich nach dem Zeitraum, nicht nach dem Anlegen - PDF im IHK-Layout, einzeln oder als ganzer Zeitraum Behoben: Im PDF-Kopffeld liefen die Spalten auseinander (dokument.y als Bezugspunkt in der Schleife). Das Abzeichnen-Formular hatte keine action - gefunden ueber die ESLint-Meldung zur ungenutzten Serveraktion. 55 neue Tests, insgesamt 241. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
b9d6041f3b
commit
7c4a051347
@@ -0,0 +1,250 @@
|
||||
import crypto from 'node:crypto';
|
||||
import { and, asc, desc, eq, gte, lte } from 'drizzle-orm';
|
||||
import { datenbank, schema } from '../db';
|
||||
import { freitagDerWoche, heute, jetzt, montagDerWoche } from '../zeit';
|
||||
import type { BerichtStatus, Tagesnotiz, Wochenbericht } from '../db/schema';
|
||||
import type { Eintrag, Schulwoche, Wochenblatt } from '../berichtsheft/pruefung';
|
||||
|
||||
/** Tagesnotizen und Wochenblätter des Ausbildungsnachweises. */
|
||||
|
||||
// --- Tagesnotizen ----------------------------------------------------------
|
||||
|
||||
export function notizenIm(benutzerId: string, von: string, bis: string): Tagesnotiz[] {
|
||||
return datenbank()
|
||||
.select()
|
||||
.from(schema.tagesnotiz)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.tagesnotiz.benutzerId, benutzerId),
|
||||
gte(schema.tagesnotiz.datum, von),
|
||||
lte(schema.tagesnotiz.datum, bis),
|
||||
),
|
||||
)
|
||||
.orderBy(asc(schema.tagesnotiz.datum))
|
||||
.all();
|
||||
}
|
||||
|
||||
export function notizAm(benutzerId: string, datum: string): Tagesnotiz | null {
|
||||
return (
|
||||
datenbank()
|
||||
.select()
|
||||
.from(schema.tagesnotiz)
|
||||
.where(and(eq(schema.tagesnotiz.benutzerId, benutzerId), eq(schema.tagesnotiz.datum, datum)))
|
||||
.get() ?? null
|
||||
);
|
||||
}
|
||||
|
||||
/** Schreibt die Notiz eines Tages. Leerer Text mit 0 Stunden löscht sie wieder. */
|
||||
export function notizSetzen(benutzerId: string, datum: string, text: string, stunden: number): void {
|
||||
const db = datenbank();
|
||||
const sauber = text.trim();
|
||||
|
||||
if (sauber === '' && stunden <= 0) {
|
||||
db.delete(schema.tagesnotiz)
|
||||
.where(and(eq(schema.tagesnotiz.benutzerId, benutzerId), eq(schema.tagesnotiz.datum, datum)))
|
||||
.run();
|
||||
return;
|
||||
}
|
||||
|
||||
db.insert(schema.tagesnotiz)
|
||||
.values({
|
||||
id: crypto.randomUUID(),
|
||||
benutzerId,
|
||||
datum,
|
||||
text: sauber,
|
||||
stunden: Math.max(0, stunden),
|
||||
geaendertAm: jetzt(),
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: [schema.tagesnotiz.benutzerId, schema.tagesnotiz.datum],
|
||||
set: { text: sauber, stunden: Math.max(0, stunden), geaendertAm: jetzt() },
|
||||
})
|
||||
.run();
|
||||
}
|
||||
|
||||
// --- Wochenblätter ---------------------------------------------------------
|
||||
|
||||
export function berichteVon(benutzerId: string): Wochenbericht[] {
|
||||
return datenbank()
|
||||
.select()
|
||||
.from(schema.wochenbericht)
|
||||
.where(eq(schema.wochenbericht.benutzerId, benutzerId))
|
||||
.orderBy(desc(schema.wochenbericht.nr))
|
||||
.all();
|
||||
}
|
||||
|
||||
export function berichtNachId(id: string): Wochenbericht | null {
|
||||
return datenbank().select().from(schema.wochenbericht).where(eq(schema.wochenbericht.id, id)).get() ?? null;
|
||||
}
|
||||
|
||||
export function berichtNachNr(benutzerId: string, nr: number): Wochenbericht | null {
|
||||
return (
|
||||
datenbank()
|
||||
.select()
|
||||
.from(schema.wochenbericht)
|
||||
.where(and(eq(schema.wochenbericht.benutzerId, benutzerId), eq(schema.wochenbericht.nr, nr)))
|
||||
.get() ?? null
|
||||
);
|
||||
}
|
||||
|
||||
export function berichtImZeitraum(benutzerId: string, von: string): Wochenbericht | null {
|
||||
return (
|
||||
datenbank()
|
||||
.select()
|
||||
.from(schema.wochenbericht)
|
||||
.where(and(eq(schema.wochenbericht.benutzerId, benutzerId), eq(schema.wochenbericht.von, von)))
|
||||
.get() ?? null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Legt bei Bedarf das Blatt für die Woche eines Datums an.
|
||||
*
|
||||
* Die laufende Nummer ergibt sich aus der Reihenfolge der Zeiträume, nicht aus
|
||||
* der Reihenfolge des Anlegens: Wer eine Woche nachträgt, soll nicht die
|
||||
* höchste Nummer bekommen.
|
||||
*/
|
||||
export function sorgeFuerBlatt(benutzerId: string, datumInDerWoche: string): Wochenbericht {
|
||||
const von = montagDerWoche(datumInDerWoche);
|
||||
const vorhanden = berichtImZeitraum(benutzerId, von);
|
||||
if (vorhanden) return vorhanden;
|
||||
|
||||
const bis = freitagDerWoche(datumInDerWoche);
|
||||
const alle = berichteVon(benutzerId);
|
||||
const frueher = alle.filter((b) => b.von < von).length;
|
||||
|
||||
const db = datenbank();
|
||||
|
||||
// Alles, was zeitlich dahinter liegt, rückt eine Nummer weiter.
|
||||
for (const bericht of alle.filter((b) => b.von > von)) {
|
||||
db.update(schema.wochenbericht)
|
||||
.set({ nr: bericht.nr + 1 })
|
||||
.where(eq(schema.wochenbericht.id, bericht.id))
|
||||
.run();
|
||||
}
|
||||
|
||||
const datensatz: Wochenbericht = {
|
||||
id: crypto.randomUUID(),
|
||||
benutzerId,
|
||||
nr: frueher + 1,
|
||||
von,
|
||||
bis,
|
||||
betrieb: '[]',
|
||||
schule: '[]',
|
||||
sonstiges: '',
|
||||
status: 'entwurf',
|
||||
anmerkung: null,
|
||||
eingereichtAm: null,
|
||||
abgezeichnetVon: null,
|
||||
abgezeichnetAm: null,
|
||||
geaendertAm: jetzt(),
|
||||
};
|
||||
|
||||
db.insert(schema.wochenbericht).values(datensatz).run();
|
||||
return datensatz;
|
||||
}
|
||||
|
||||
/** Der Inhalt eines Blattes, aus JSON gelesen und gegen Unsinn abgesichert. */
|
||||
export function alsWochenblatt(bericht: Wochenbericht): Wochenblatt {
|
||||
return {
|
||||
betrieb: leseListe<Eintrag>(bericht.betrieb, (roh) => ({
|
||||
text: typeof roh.text === 'string' ? roh.text : '',
|
||||
stunden: typeof roh.stunden === 'number' && Number.isFinite(roh.stunden) ? roh.stunden : 0,
|
||||
})),
|
||||
schule: leseListe<Schulwoche>(bericht.schule, (roh) => ({
|
||||
lernfeld: typeof roh.lernfeld === 'string' ? roh.lernfeld : '',
|
||||
themen: typeof roh.themen === 'string' ? roh.themen : '',
|
||||
stunden: typeof roh.stunden === 'number' && Number.isFinite(roh.stunden) ? roh.stunden : 0,
|
||||
})),
|
||||
sonstiges: bericht.sonstiges,
|
||||
};
|
||||
}
|
||||
|
||||
function leseListe<T>(roh: string, abbilden: (wert: Record<string, unknown>) => T): T[] {
|
||||
try {
|
||||
const gelesen: unknown = JSON.parse(roh);
|
||||
if (!Array.isArray(gelesen)) return [];
|
||||
return gelesen
|
||||
.filter((e): e is Record<string, unknown> => typeof e === 'object' && e !== null)
|
||||
.map(abbilden);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export function inhaltSpeichern(berichtId: string, blatt: Wochenblatt): void {
|
||||
datenbank()
|
||||
.update(schema.wochenbericht)
|
||||
.set({
|
||||
betrieb: JSON.stringify(blatt.betrieb),
|
||||
schule: JSON.stringify(blatt.schule),
|
||||
sonstiges: blatt.sonstiges,
|
||||
geaendertAm: jetzt(),
|
||||
})
|
||||
.where(eq(schema.wochenbericht.id, berichtId))
|
||||
.run();
|
||||
}
|
||||
|
||||
export function statusSetzen(berichtId: string, status: BerichtStatus, zusatz: Partial<Wochenbericht> = {}): void {
|
||||
datenbank()
|
||||
.update(schema.wochenbericht)
|
||||
.set({ status, ...zusatz, geaendertAm: jetzt() })
|
||||
.where(eq(schema.wochenbericht.id, berichtId))
|
||||
.run();
|
||||
}
|
||||
|
||||
export function einreichen(berichtId: string): void {
|
||||
statusSetzen(berichtId, 'eingereicht', { eingereichtAm: jetzt(), abgezeichnetAm: null, abgezeichnetVon: null });
|
||||
}
|
||||
|
||||
export function abzeichnen(berichtId: string, ausbilderId: string, anmerkung: string | null): void {
|
||||
statusSetzen(berichtId, 'abgezeichnet', {
|
||||
abgezeichnetVon: ausbilderId,
|
||||
abgezeichnetAm: jetzt(),
|
||||
anmerkung,
|
||||
});
|
||||
}
|
||||
|
||||
export function zurueckgeben(berichtId: string, anmerkung: string): void {
|
||||
statusSetzen(berichtId, 'zurueckgegeben', {
|
||||
anmerkung,
|
||||
eingereichtAm: null,
|
||||
abgezeichnetAm: null,
|
||||
abgezeichnetVon: null,
|
||||
});
|
||||
}
|
||||
|
||||
/** Blätter, die auf eine Abzeichnung warten – für „Was wartet auf mich". */
|
||||
export function wartetAufAbzeichnung(benutzerIds: string[]): Wochenbericht[] {
|
||||
if (benutzerIds.length === 0) return [];
|
||||
return datenbank()
|
||||
.select()
|
||||
.from(schema.wochenbericht)
|
||||
.where(eq(schema.wochenbericht.status, 'eingereicht'))
|
||||
.orderBy(asc(schema.wochenbericht.von))
|
||||
.all()
|
||||
.filter((b) => benutzerIds.includes(b.benutzerId));
|
||||
}
|
||||
|
||||
/** Wochen seit Ausbildungsbeginn, für die noch gar kein Blatt existiert. */
|
||||
export function fehlendeWochen(benutzerId: string, beginn: string, bisDatum = heute()): string[] {
|
||||
const vorhanden = new Set(berichteVon(benutzerId).map((b) => b.von));
|
||||
const fehlend: string[] = [];
|
||||
|
||||
let montag = montagDerWoche(beginn);
|
||||
const letzterMontag = montagDerWoche(bisDatum);
|
||||
|
||||
// Nur abgelaufene Wochen zählen als fehlend – die laufende noch nicht.
|
||||
while (montag < letzterMontag) {
|
||||
if (!vorhanden.has(montag)) fehlend.push(montag);
|
||||
montag = naechsterMontag(montag);
|
||||
}
|
||||
|
||||
return fehlend;
|
||||
}
|
||||
|
||||
function naechsterMontag(montag: string): string {
|
||||
const d = new Date(`${montag}T12:00:00Z`);
|
||||
d.setUTCDate(d.getUTCDate() + 7);
|
||||
return d.toISOString().slice(0, 10);
|
||||
}
|
||||
Reference in New Issue
Block a user