Module abschaltbar machen
Merkkarten, Praxisaufgaben, Checklisten, Berichtsheft, Lernzielkontrollen und Feedbackgespraech lassen sich unter Verwaltung -> Module fuer die ganze Lern-Bar ab- und einschalten. Wochenplan, Handouts, Glossar, Auswertung und Verwaltung bleiben immer an. Gesperrt wird an drei Stellen, nicht im Layout (Next rendert Layouts beim Seitenwechsel darunter nicht neu): Seiten mit erfordereModul, Routen fuer PDF, Anhaenge und Lernstand mit modulAn, alle 37 Serveraktionen der Module mit pruefeModul. Die Anmeldepruefung kommt jeweils zuerst, damit ein Unangemeldeter nicht am 404 abliest, was abgeschaltet ist. Ein abgeschaltetes Modul traegt nirgends bei: Wochenplan, Fortschritt, Was-wartet, Mein Stand, Ergebnisverlauf, Rahmenplanabdeckung, Vorschlaege fuers Berichtsheft, Karten im Handout, Kurzbefehle im Manifest. Die Lernstand-Schnittstelle antwortet mit 410, das Geraet verwirft daraufhin seine Offline-Warteschlange. Abschalten loescht nichts. Gespeichert in modul_einstellung (Migration 0010); fehlt die Zeile, ist das Modul an, eine bestehende Datenbank verhaelt sich also wie vorher. 12 Tests fuer die Regeln, Abschnitt A8 im Abnahmelauf, Anleitung Kapitel 9 mit Bildschirmfoto, README, E43. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
54ce7d87ae
commit
44e6c8bb79
@@ -155,58 +155,64 @@ export function WochenAnsicht({
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="karte" aria-labelledby="woche-aufgaben">
|
||||
<h2 id="woche-aufgaben" style={{ fontSize: '1rem' }}>
|
||||
Praxisaufgaben
|
||||
</h2>
|
||||
{sicht.aufgaben.length === 0 ? (
|
||||
<p className="leise" style={{ marginBottom: 0 }}>Keine.</p>
|
||||
) : (
|
||||
<ul className="stapel-eng" style={{ margin: 0, paddingLeft: '1.1rem' }}>
|
||||
{sicht.aufgaben.map((aufgabe) => (
|
||||
<li key={aufgabe.id}>
|
||||
<strong>{aufgabe.id}</strong> {aufgabe.titel}
|
||||
<br />
|
||||
<span className="leise">{aufgabe.zeitrahmen}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
{sicht.module.aufgaben ? (
|
||||
<section className="karte" aria-labelledby="woche-aufgaben">
|
||||
<h2 id="woche-aufgaben" style={{ fontSize: '1rem' }}>
|
||||
Praxisaufgaben
|
||||
</h2>
|
||||
{sicht.aufgaben.length === 0 ? (
|
||||
<p className="leise" style={{ marginBottom: 0 }}>Keine.</p>
|
||||
) : (
|
||||
<ul className="stapel-eng" style={{ margin: 0, paddingLeft: '1.1rem' }}>
|
||||
{sicht.aufgaben.map((aufgabe) => (
|
||||
<li key={aufgabe.id}>
|
||||
<strong>{aufgabe.id}</strong> {aufgabe.titel}
|
||||
<br />
|
||||
<span className="leise">{aufgabe.zeitrahmen}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
<section className="karte" aria-labelledby="woche-checklisten">
|
||||
<h2 id="woche-checklisten" style={{ fontSize: '1rem' }}>
|
||||
Checklisten
|
||||
</h2>
|
||||
{sicht.checklisten.length === 0 ? (
|
||||
<p className="leise" style={{ marginBottom: 0 }}>Keine.</p>
|
||||
) : (
|
||||
<ul className="stapel-eng" style={{ margin: 0, paddingLeft: '1.1rem' }}>
|
||||
{sicht.checklisten.map((liste) => (
|
||||
<li key={liste.id}>{liste.titel}</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
{sicht.module.checklisten ? (
|
||||
<section className="karte" aria-labelledby="woche-checklisten">
|
||||
<h2 id="woche-checklisten" style={{ fontSize: '1rem' }}>
|
||||
Checklisten
|
||||
</h2>
|
||||
{sicht.checklisten.length === 0 ? (
|
||||
<p className="leise" style={{ marginBottom: 0 }}>Keine.</p>
|
||||
) : (
|
||||
<ul className="stapel-eng" style={{ margin: 0, paddingLeft: '1.1rem' }}>
|
||||
{sicht.checklisten.map((liste) => (
|
||||
<li key={liste.id}>{liste.titel}</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
<section className="karte" aria-labelledby="woche-kontrolle">
|
||||
<h2 id="woche-kontrolle" style={{ fontSize: '1rem' }}>
|
||||
Lernzielkontrolle
|
||||
</h2>
|
||||
{sicht.kontrolle ? (
|
||||
<p style={{ marginBottom: 0 }}>
|
||||
{sicht.kontrolle.titel}
|
||||
<br />
|
||||
<span className="leise">
|
||||
{sicht.kontrolle.fragen.length} Fragen ·{' '}
|
||||
{sicht.kontrolle.fragen.reduce((summe, f) => summe + f.punkte, 0)} Punkte
|
||||
{sicht.kontrolle.dauer_minuten ? ` · ${sicht.kontrolle.dauer_minuten} Minuten` : ''}
|
||||
</span>
|
||||
</p>
|
||||
) : (
|
||||
<p className="leise" style={{ marginBottom: 0 }}>Diese Woche keine.</p>
|
||||
)}
|
||||
</section>
|
||||
{sicht.module.kontrollen ? (
|
||||
<section className="karte" aria-labelledby="woche-kontrolle">
|
||||
<h2 id="woche-kontrolle" style={{ fontSize: '1rem' }}>
|
||||
Lernzielkontrolle
|
||||
</h2>
|
||||
{sicht.kontrolle ? (
|
||||
<p style={{ marginBottom: 0 }}>
|
||||
{sicht.kontrolle.titel}
|
||||
<br />
|
||||
<span className="leise">
|
||||
{sicht.kontrolle.fragen.length} Fragen ·{' '}
|
||||
{sicht.kontrolle.fragen.reduce((summe, f) => summe + f.punkte, 0)} Punkte
|
||||
{sicht.kontrolle.dauer_minuten ? ` · ${sicht.kontrolle.dauer_minuten} Minuten` : ''}
|
||||
</span>
|
||||
</p>
|
||||
) : (
|
||||
<p className="leise" style={{ marginBottom: 0 }}>Diese Woche keine.</p>
|
||||
)}
|
||||
</section>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<nav className="reihe nicht-drucken" style={{ justifyContent: 'space-between' }} aria-label="Andere Wochen">
|
||||
|
||||
@@ -12,6 +12,7 @@ import { alsZeitpunktText } from '@/lib/zeit';
|
||||
import { StatusMarke } from '../StatusMarke';
|
||||
import { AnhangFormular, BewertungsFormular, NotizFormular, StatusWaehlen } from './Formulare';
|
||||
import { anhangEntfernen, aufgabeAbnehmen, aufgabeZurueckgeben } from '../aktionen';
|
||||
import { erfordereModul } from '@/lib/module/pruefen';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
@@ -29,6 +30,7 @@ export default async function AufgabenSeite({
|
||||
searchParams: Promise<{ azubi?: string }>;
|
||||
}) {
|
||||
const benutzer = await erfordereBenutzer();
|
||||
erfordereModul('aufgaben');
|
||||
const { id } = await params;
|
||||
const { azubi: azubiParameter } = await searchParams;
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
zurueckgeben,
|
||||
} from '@/lib/repos/aufgaben';
|
||||
import { aufgabenStatus, type AufgabenStatus } from '@/lib/db/schema';
|
||||
import { pruefeModul } from '@/lib/module/pruefen';
|
||||
|
||||
export type Rueckmeldung = { fehler?: string; erfolg?: string };
|
||||
|
||||
@@ -30,6 +31,7 @@ function aufgabeGibtEs(aufgabeId: string): boolean {
|
||||
|
||||
export async function eigenenStatusSetzen(formular: FormData): Promise<void> {
|
||||
const benutzer = await erfordereBenutzer();
|
||||
pruefeModul('aufgaben');
|
||||
if (benutzer.rolle !== 'azubi') return;
|
||||
|
||||
const aufgabeId = String(formular.get('aufgabeId') ?? '');
|
||||
@@ -46,6 +48,7 @@ export async function eigenenStatusSetzen(formular: FormData): Promise<void> {
|
||||
|
||||
export async function eigeneNotizSpeichern(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
|
||||
const benutzer = await erfordereBenutzer();
|
||||
pruefeModul('aufgaben');
|
||||
if (benutzer.rolle !== 'azubi') return { fehler: 'Nur der Azubi schreibt seine eigenen Notizen.' };
|
||||
|
||||
const aufgabeId = String(formular.get('aufgabeId') ?? '');
|
||||
@@ -61,6 +64,7 @@ export async function eigeneNotizSpeichern(_vorher: Rueckmeldung, formular: Form
|
||||
|
||||
export async function anhangHochladen(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
|
||||
const benutzer = await erfordereBenutzer();
|
||||
pruefeModul('aufgaben');
|
||||
if (benutzer.rolle !== 'azubi') return { fehler: 'Anhänge lädt der Azubi hoch.' };
|
||||
|
||||
const aufgabeId = String(formular.get('aufgabeId') ?? '');
|
||||
@@ -107,6 +111,7 @@ export async function anhangHochladen(_vorher: Rueckmeldung, formular: FormData)
|
||||
|
||||
export async function anhangEntfernen(formular: FormData): Promise<void> {
|
||||
const benutzer = await erfordereBenutzer();
|
||||
pruefeModul('aufgaben');
|
||||
const anhangId = String(formular.get('anhangId') ?? '');
|
||||
const aufgabeId = String(formular.get('aufgabeId') ?? '');
|
||||
|
||||
@@ -126,6 +131,7 @@ export async function anhangEntfernen(formular: FormData): Promise<void> {
|
||||
|
||||
export async function bewerten(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
|
||||
const ausbilder = await erfordereBenutzer();
|
||||
pruefeModul('aufgaben');
|
||||
const azubiId = String(formular.get('azubiId') ?? '');
|
||||
const aufgabeId = String(formular.get('aufgabeId') ?? '');
|
||||
|
||||
@@ -157,6 +163,7 @@ export async function bewerten(_vorher: Rueckmeldung, formular: FormData): Promi
|
||||
|
||||
export async function aufgabeAbnehmen(formular: FormData): Promise<void> {
|
||||
const ausbilder = await erfordereBenutzer();
|
||||
pruefeModul('aufgaben');
|
||||
const azubiId = String(formular.get('azubiId') ?? '');
|
||||
const aufgabeId = String(formular.get('aufgabeId') ?? '');
|
||||
|
||||
@@ -169,6 +176,7 @@ export async function aufgabeAbnehmen(formular: FormData): Promise<void> {
|
||||
|
||||
export async function aufgabeZurueckgeben(formular: FormData): Promise<void> {
|
||||
const ausbilder = await erfordereBenutzer();
|
||||
pruefeModul('aufgaben');
|
||||
const azubiId = String(formular.get('azubiId') ?? '');
|
||||
const aufgabeId = String(formular.get('aufgabeId') ?? '');
|
||||
|
||||
@@ -182,5 +190,6 @@ export async function aufgabeZurueckgeben(formular: FormData): Promise<void> {
|
||||
/** Wird von der Aufgabenseite gebraucht, um fremde Daten überhaupt lesen zu dürfen. */
|
||||
export async function pruefeLesezugriff(azubiId: string): Promise<void> {
|
||||
const benutzer = await erfordereBenutzer();
|
||||
pruefeModul('aufgaben');
|
||||
erfordereZugriffAufAzubi(benutzer, azubiId);
|
||||
}
|
||||
|
||||
@@ -6,12 +6,14 @@ import { inhalte } from '@/lib/inhalte';
|
||||
import { staendeVon } from '@/lib/repos/aufgaben';
|
||||
import { alsZeitpunktText } from '@/lib/zeit';
|
||||
import { StatusMarke } from './StatusMarke';
|
||||
import { erfordereModul } from '@/lib/module/pruefen';
|
||||
|
||||
export const metadata: Metadata = { title: 'Praxisaufgaben' };
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export default async function AufgabenListe() {
|
||||
const benutzer = await erfordereBenutzer();
|
||||
erfordereModul('aufgaben');
|
||||
const alle = inhalte();
|
||||
|
||||
if (benutzer.rolle === 'ausbilder') {
|
||||
|
||||
@@ -103,38 +103,44 @@ function AzubiAuswertung({ azubiId, name }: { azubiId: string; name: string }) {
|
||||
anteil={anteil(stand.handouts.erledigt, stand.handouts.gesamt)}
|
||||
fuss={`${stand.handouts.gesamt - stand.handouts.erledigt} noch offen`}
|
||||
/>
|
||||
<Kachel
|
||||
titel="Merkkarten, die sitzen"
|
||||
wert={`${stand.karten.erledigt} von ${stand.karten.gesamt}`}
|
||||
anteil={anteil(stand.karten.erledigt, stand.karten.gesamt)}
|
||||
fuss={stand.karten.faellig > 0 ? `${stand.karten.faellig} heute fällig` : 'heute nichts fällig'}
|
||||
/>
|
||||
<Kachel
|
||||
titel="Praxisaufgaben abgenommen"
|
||||
wert={`${stand.aufgaben.erledigt} von ${stand.aufgaben.gesamt}`}
|
||||
anteil={anteil(stand.aufgaben.erledigt, stand.aufgaben.gesamt)}
|
||||
fuss={
|
||||
stand.aufgaben.zurAbnahme > 0
|
||||
? `${stand.aufgaben.zurAbnahme} wartet auf Abnahme`
|
||||
: `${stand.aufgaben.offen} noch offen`
|
||||
}
|
||||
/>
|
||||
<Kachel
|
||||
titel="Berichtsheft abgezeichnet"
|
||||
wert={`${stand.berichtsheft.abgezeichnet} von ${stand.berichtsheft.gesamt}`}
|
||||
anteil={anteil(stand.berichtsheft.abgezeichnet, stand.berichtsheft.gesamt)}
|
||||
fuss={
|
||||
stand.berichtsheft.fehlendeWochen > 0
|
||||
? `${stand.berichtsheft.fehlendeWochen} Wochen ohne Blatt`
|
||||
: stand.berichtsheft.eingereicht > 0
|
||||
? `${stand.berichtsheft.eingereicht} beim Ausbilder`
|
||||
: 'nichts offen'
|
||||
}
|
||||
ton={stand.berichtsheft.fehlendeWochen > 0 ? 'fehler' : undefined}
|
||||
/>
|
||||
{stand.module.karten ? (
|
||||
<Kachel
|
||||
titel="Merkkarten, die sitzen"
|
||||
wert={`${stand.karten.erledigt} von ${stand.karten.gesamt}`}
|
||||
anteil={anteil(stand.karten.erledigt, stand.karten.gesamt)}
|
||||
fuss={stand.karten.faellig > 0 ? `${stand.karten.faellig} heute fällig` : 'heute nichts fällig'}
|
||||
/>
|
||||
) : null}
|
||||
{stand.module.aufgaben ? (
|
||||
<Kachel
|
||||
titel="Praxisaufgaben abgenommen"
|
||||
wert={`${stand.aufgaben.erledigt} von ${stand.aufgaben.gesamt}`}
|
||||
anteil={anteil(stand.aufgaben.erledigt, stand.aufgaben.gesamt)}
|
||||
fuss={
|
||||
stand.aufgaben.zurAbnahme > 0
|
||||
? `${stand.aufgaben.zurAbnahme} wartet auf Abnahme`
|
||||
: `${stand.aufgaben.offen} noch offen`
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
{stand.module.berichtsheft ? (
|
||||
<Kachel
|
||||
titel="Berichtsheft abgezeichnet"
|
||||
wert={`${stand.berichtsheft.abgezeichnet} von ${stand.berichtsheft.gesamt}`}
|
||||
anteil={anteil(stand.berichtsheft.abgezeichnet, stand.berichtsheft.gesamt)}
|
||||
fuss={
|
||||
stand.berichtsheft.fehlendeWochen > 0
|
||||
? `${stand.berichtsheft.fehlendeWochen} Wochen ohne Blatt`
|
||||
: stand.berichtsheft.eingereicht > 0
|
||||
? `${stand.berichtsheft.eingereicht} beim Ausbilder`
|
||||
: 'nichts offen'
|
||||
}
|
||||
ton={stand.berichtsheft.fehlendeWochen > 0 ? 'fehler' : undefined}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{stand.berichtsheft.fehlendeWochen > 0 ? (
|
||||
{stand.module.berichtsheft && stand.berichtsheft.fehlendeWochen > 0 ? (
|
||||
<p className="hinweis hinweis-warnung">
|
||||
<strong>
|
||||
{stand.berichtsheft.fehlendeWochen}{' '}
|
||||
@@ -145,7 +151,7 @@ function AzubiAuswertung({ azubiId, name }: { azubiId: string; name: string }) {
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
<ErgebnisTabelle ergebnisse={stand.kontrollen.ergebnisse} />
|
||||
{stand.module.kontrollen ? <ErgebnisTabelle ergebnisse={stand.kontrollen.ergebnisse} /> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -180,9 +186,14 @@ function Kachel({
|
||||
// --- Rahmenplanabdeckung ---------------------------------------------------
|
||||
|
||||
function Rahmenplan({ azubiId }: { azubiId: string }) {
|
||||
const { positionen, gruppennamen } = rahmenplanabdeckung(azubiId);
|
||||
const { positionen, gruppennamen, module: eingeschaltet } = rahmenplanabdeckung(azubiId);
|
||||
const gruppen = nachGruppen(positionen);
|
||||
const gesamt = bilanz(positionen);
|
||||
// Die Regel steht in lib/auswertung/abdeckung – der Text nennt nur, was an ist.
|
||||
const belastbar = [
|
||||
eingeschaltet.aufgaben ? 'eine abgenommene Praxisaufgabe' : null,
|
||||
eingeschaltet.checklisten ? 'einen abgeschlossenen Checklisten-Durchlauf' : null,
|
||||
].filter((teil) => teil !== null);
|
||||
|
||||
return (
|
||||
<section className="stapel" aria-labelledby="rahmenplan">
|
||||
@@ -192,8 +203,9 @@ function Rahmenplan({ azubiId }: { azubiId: string }) {
|
||||
</h2>
|
||||
<p className="leise" style={{ margin: 0 }}>
|
||||
Die Ansicht, die im Zweifel gegenüber der IHK zählt. Als <strong>vermittelt</strong> gilt eine
|
||||
Position, wenn es dazu eine abgenommene Praxisaufgabe oder einen abgeschlossenen
|
||||
Checklisten-Durchlauf gibt – oder mindestens zwei gelesene Handouts.
|
||||
Position, wenn es dazu{' '}
|
||||
{belastbar.length > 0 ? `${belastbar.join(' oder ')} gibt – oder ` : ''}mindestens zwei gelesene
|
||||
Handouts{belastbar.length > 0 ? '' : ' gibt'}.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -339,6 +351,7 @@ function Lernfelder({ azubiId }: { azubiId: string }) {
|
||||
function Kontrollverlauf({ azubiId }: { azubiId: string }) {
|
||||
const ausbildung = ausbildungNachAzubi(azubiId);
|
||||
const stand = eigenerStand(azubiId, ausbildung?.beginn ?? null);
|
||||
if (!stand.module.kontrollen) return null;
|
||||
return <ErgebnisTabelle ergebnisse={stand.kontrollen.ergebnisse} />;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,9 @@ import { Formulierungshilfe } from '../Formulierungshilfe';
|
||||
import { BerichtsMarke } from '../BerichtsMarke';
|
||||
import { Abzeichnen } from './Abzeichnen';
|
||||
import { blattZurueckziehen, blattZumLesen } from '../aktionen';
|
||||
import { erfordereModul } from '@/lib/module/pruefen';
|
||||
import { erfordereBenutzer } from '@/lib/auth/rechte';
|
||||
import { modulStand } from '@/lib/repos/module';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
@@ -27,6 +30,8 @@ export async function generateMetadata({ params }: { params: Promise<{ berichtId
|
||||
}
|
||||
|
||||
export default async function BlattSeite({ params }: { params: Promise<{ berichtId: string }> }) {
|
||||
await erfordereBenutzer();
|
||||
erfordereModul('berichtsheft');
|
||||
const { berichtId } = await params;
|
||||
|
||||
// Rechteprüfung steckt hier drin: kein Zugriff bedeutet null bedeutet 404.
|
||||
@@ -238,6 +243,8 @@ function AnsichtFuerAusbilder({
|
||||
/** Stellt zusammen, was die Anwendung über diese Woche weiß. */
|
||||
function sammleVorschlaege(azubiId: string, von: string, bis: string) {
|
||||
const alle = inhalte();
|
||||
// Aus abgeschalteten Modulen kommen keine Vorschläge.
|
||||
const eingeschaltet = modulStand();
|
||||
|
||||
const notizen = notizenIm(azubiId, von, bis).map((n) => ({
|
||||
datum: n.datum,
|
||||
@@ -247,7 +254,7 @@ function sammleVorschlaege(azubiId: string, von: string, bis: string) {
|
||||
|
||||
// Praxisaufgaben, die in dieser Woche abgegeben oder abgenommen wurden.
|
||||
const staende = aufgabenStaende(azubiId);
|
||||
const aufgaben = [...staende.values()]
|
||||
const aufgaben = [...(eingeschaltet.aufgaben ? staende.values() : [])]
|
||||
.filter((s) => {
|
||||
const zeitpunkt = (s.abgenommenAm ?? s.abgegebenAm ?? '').slice(0, 10);
|
||||
return zeitpunkt >= von && zeitpunkt <= bis;
|
||||
@@ -262,7 +269,7 @@ function sammleVorschlaege(azubiId: string, von: string, bis: string) {
|
||||
};
|
||||
});
|
||||
|
||||
const checklisten = laeufeVon(azubiId)
|
||||
const checklisten = (eingeschaltet.checklisten ? laeufeVon(azubiId) : [])
|
||||
.filter((l) => l.abgeschlossenAm !== null && l.datum >= von && l.datum <= bis)
|
||||
.map((l) => ({ id: l.id, titel: l.titel, ticket: l.ticket, kunde: l.kunde }));
|
||||
|
||||
|
||||
@@ -5,11 +5,15 @@ import { berichtsheftPdf } from '@/lib/pdf/berichtsheft';
|
||||
import { alsDatumText, alsZeitpunktText } from '@/lib/zeit';
|
||||
import { ausbildungsjahr } from '@/lib/berichtsheft/jahr';
|
||||
import { blattZumLesen } from '../../aktionen';
|
||||
import { erfordereBenutzer } from '@/lib/auth/rechte';
|
||||
import { modulAn, modulGesperrt } from '@/lib/module/pruefen';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
/** Ein einzelnes Wochenblatt als PDF. Rechteprüfung über `blattZumLesen`. */
|
||||
export async function GET(_anfrage: Request, kontext: { params: Promise<{ berichtId: string }> }) {
|
||||
await erfordereBenutzer();
|
||||
if (!modulAn('berichtsheft')) return modulGesperrt();
|
||||
const { berichtId } = await kontext.params;
|
||||
|
||||
const gelesen = await blattZumLesen(berichtId);
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
zurueckgeben,
|
||||
} from '@/lib/repos/berichtsheft';
|
||||
import type { Wochenblatt } from '@/lib/berichtsheft/pruefung';
|
||||
import { pruefeModul } from '@/lib/module/pruefen';
|
||||
|
||||
export type Rueckmeldung = { fehler?: string; erfolg?: string };
|
||||
|
||||
@@ -31,6 +32,7 @@ async function blattZumBearbeiten(berichtId: string) {
|
||||
/** Zum Lesen: das eigene Blatt oder das eines betreuten Azubis. */
|
||||
export async function blattZumLesen(berichtId: string) {
|
||||
const benutzer = await erfordereBenutzer();
|
||||
pruefeModul('berichtsheft');
|
||||
const bericht = berichtNachId(berichtId);
|
||||
if (!bericht) return null;
|
||||
|
||||
@@ -50,6 +52,7 @@ function aktualisiere(berichtId?: string) {
|
||||
|
||||
export async function tagesnotizSpeichern(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
|
||||
const benutzer = await erfordereBenutzer();
|
||||
pruefeModul('berichtsheft');
|
||||
if (benutzer.rolle !== 'azubi') return { fehler: 'Tagesnotizen schreibt der Azubi.' };
|
||||
|
||||
const datum = String(formular.get('datum') ?? '').trim();
|
||||
@@ -70,6 +73,7 @@ export async function tagesnotizSpeichern(_vorher: Rueckmeldung, formular: FormD
|
||||
|
||||
export async function blattAnlegen(formular: FormData): Promise<void> {
|
||||
const benutzer = await erfordereBenutzer();
|
||||
pruefeModul('berichtsheft');
|
||||
if (benutzer.rolle !== 'azubi') return;
|
||||
|
||||
const datum = String(formular.get('datum') ?? '').trim();
|
||||
@@ -104,6 +108,7 @@ function leseBlatt(formular: FormData): Wochenblatt {
|
||||
}
|
||||
|
||||
export async function blattSpeichern(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
|
||||
pruefeModul('berichtsheft');
|
||||
const berichtId = String(formular.get('berichtId') ?? '');
|
||||
const { bericht } = await blattZumBearbeiten(berichtId);
|
||||
|
||||
@@ -128,6 +133,7 @@ export async function blattSpeichern(_vorher: Rueckmeldung, formular: FormData):
|
||||
* auf der Seite gesehen; ob er sie beherzigt, entscheidet er.
|
||||
*/
|
||||
export async function blattEinreichen(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
|
||||
pruefeModul('berichtsheft');
|
||||
const berichtId = String(formular.get('berichtId') ?? '');
|
||||
const { bericht } = await blattZumBearbeiten(berichtId);
|
||||
|
||||
@@ -143,6 +149,7 @@ export async function blattEinreichen(_vorher: Rueckmeldung, formular: FormData)
|
||||
}
|
||||
|
||||
export async function blattZurueckziehen(formular: FormData): Promise<void> {
|
||||
pruefeModul('berichtsheft');
|
||||
const berichtId = String(formular.get('berichtId') ?? '');
|
||||
const { bericht } = await blattZumBearbeiten(berichtId);
|
||||
if (bericht.status !== 'eingereicht') return;
|
||||
@@ -155,6 +162,7 @@ export async function blattZurueckziehen(formular: FormData): Promise<void> {
|
||||
|
||||
export async function blattAbzeichnen(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
|
||||
const ausbilder = await erfordereBenutzer();
|
||||
pruefeModul('berichtsheft');
|
||||
const berichtId = String(formular.get('berichtId') ?? '');
|
||||
|
||||
const bericht = berichtNachId(berichtId);
|
||||
@@ -173,6 +181,7 @@ export async function blattAbzeichnen(_vorher: Rueckmeldung, formular: FormData)
|
||||
|
||||
export async function blattZurueckgeben(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
|
||||
const ausbilder = await erfordereBenutzer();
|
||||
pruefeModul('berichtsheft');
|
||||
const berichtId = String(formular.get('berichtId') ?? '');
|
||||
|
||||
const bericht = berichtNachId(berichtId);
|
||||
|
||||
@@ -9,12 +9,14 @@ import { Tagesnotizen, type Tagesfeld } from './Tagesnotizen';
|
||||
import { Formulierungshilfe } from './Formulierungshilfe';
|
||||
import { blattAnlegen } from './aktionen';
|
||||
import { BerichtsMarke } from './BerichtsMarke';
|
||||
import { erfordereModul } from '@/lib/module/pruefen';
|
||||
|
||||
export const metadata: Metadata = { title: 'Berichtsheft' };
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export default async function BerichtsheftSeite() {
|
||||
const benutzer = await erfordereBenutzer();
|
||||
erfordereModul('berichtsheft');
|
||||
|
||||
return benutzer.rolle === 'ausbilder' ? (
|
||||
<AusbilderSicht ausbilderId={benutzer.id} />
|
||||
|
||||
@@ -5,6 +5,7 @@ import { alsWochenblatt, berichteVon } from '@/lib/repos/berichtsheft';
|
||||
import { berichtsheftPdf, type Blattangaben } from '@/lib/pdf/berichtsheft';
|
||||
import { ausbildungsjahr } from '@/lib/berichtsheft/jahr';
|
||||
import { alsDatumText, alsZeitpunktText } from '@/lib/zeit';
|
||||
import { modulAn, modulGesperrt } from '@/lib/module/pruefen';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
@@ -20,6 +21,7 @@ const datumMuster = /^\d{4}-\d{2}-\d{2}$/;
|
||||
*/
|
||||
export async function GET(anfrage: Request) {
|
||||
const benutzer = await erfordereBenutzer();
|
||||
if (!modulAn('berichtsheft')) return modulGesperrt();
|
||||
const adresse = new URL(anfrage.url);
|
||||
|
||||
const azubiId = benutzer.rolle === 'azubi' ? benutzer.id : (adresse.searchParams.get('azubi') ?? '');
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
standVon,
|
||||
} from '@/lib/repos/checklisten';
|
||||
import { darfAbschliessen } from '@/lib/checklisten/fortschritt';
|
||||
import { pruefeModul } from '@/lib/module/pruefen';
|
||||
|
||||
export type Rueckmeldung = { fehler?: string; erfolg?: string };
|
||||
|
||||
@@ -42,6 +43,7 @@ async function laufZumBearbeiten(laufId: string) {
|
||||
/** Zum Lesen: der eigene Durchlauf oder der eines betreuten Azubis. */
|
||||
export async function laufZumLesen(laufId: string) {
|
||||
const benutzer = await erfordereBenutzer();
|
||||
pruefeModul('checklisten');
|
||||
const lauf = laufNachId(laufId);
|
||||
if (!lauf) return null;
|
||||
|
||||
@@ -59,6 +61,7 @@ function aktualisiere(laufId: string) {
|
||||
|
||||
export async function durchlaufStarten(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
|
||||
const benutzer = await erfordereBenutzer();
|
||||
pruefeModul('checklisten');
|
||||
if (benutzer.rolle !== 'azubi') {
|
||||
return { fehler: 'Durchläufe führt der Azubi – der Ausbilder liest mit.' };
|
||||
}
|
||||
@@ -82,6 +85,7 @@ export async function durchlaufStarten(_vorher: Rueckmeldung, formular: FormData
|
||||
}
|
||||
|
||||
export async function schrittUmschalten(formular: FormData): Promise<void> {
|
||||
pruefeModul('checklisten');
|
||||
const laufId = String(formular.get('laufId') ?? '');
|
||||
const schrittId = String(formular.get('schrittId') ?? '');
|
||||
const erledigt = String(formular.get('erledigt') ?? '') === '1';
|
||||
@@ -94,6 +98,7 @@ export async function schrittUmschalten(formular: FormData): Promise<void> {
|
||||
}
|
||||
|
||||
export async function schrittBemerkung(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
|
||||
pruefeModul('checklisten');
|
||||
const laufId = String(formular.get('laufId') ?? '');
|
||||
const schrittId = String(formular.get('schrittId') ?? '');
|
||||
const bemerkung = String(formular.get('bemerkung') ?? '');
|
||||
@@ -108,6 +113,7 @@ export async function schrittBemerkung(_vorher: Rueckmeldung, formular: FormData
|
||||
}
|
||||
|
||||
export async function kopfdatenSpeichern(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
|
||||
pruefeModul('checklisten');
|
||||
const laufId = String(formular.get('laufId') ?? '');
|
||||
const { lauf } = await laufZumBearbeiten(laufId);
|
||||
if (lauf.abgeschlossenAm) return { fehler: 'Der Durchlauf ist abgeschlossen.' };
|
||||
@@ -130,6 +136,7 @@ export async function kopfdatenSpeichern(_vorher: Rueckmeldung, formular: FormDa
|
||||
}
|
||||
|
||||
export async function durchlaufAbschliessen(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
|
||||
pruefeModul('checklisten');
|
||||
const laufId = String(formular.get('laufId') ?? '');
|
||||
const { lauf } = await laufZumBearbeiten(laufId);
|
||||
if (lauf.abgeschlossenAm) return { fehler: 'Der Durchlauf ist schon abgeschlossen.' };
|
||||
@@ -148,6 +155,7 @@ export async function durchlaufAbschliessen(_vorher: Rueckmeldung, formular: For
|
||||
}
|
||||
|
||||
export async function durchlaufWiederOeffnen(formular: FormData): Promise<void> {
|
||||
pruefeModul('checklisten');
|
||||
const laufId = String(formular.get('laufId') ?? '');
|
||||
await laufZumBearbeiten(laufId);
|
||||
laufWiederOeffnen(laufId);
|
||||
@@ -155,6 +163,7 @@ export async function durchlaufWiederOeffnen(formular: FormData): Promise<void>
|
||||
}
|
||||
|
||||
export async function durchlaufVerwerfen(formular: FormData): Promise<void> {
|
||||
pruefeModul('checklisten');
|
||||
const laufId = String(formular.get('laufId') ?? '');
|
||||
const { lauf } = await laufZumBearbeiten(laufId);
|
||||
// Ein abgeschlossener Durchlauf ist ein Nachweis und wird nicht gelöscht.
|
||||
|
||||
@@ -7,6 +7,8 @@ import { standVon } from '@/lib/repos/checklisten';
|
||||
import { alsDatumText, alsZeitpunktText } from '@/lib/zeit';
|
||||
import { AbschlussFormular, BemerkungFormular, KopfdatenFormular } from '../../Formulare';
|
||||
import { durchlaufVerwerfen, durchlaufWiederOeffnen, laufZumLesen, schrittUmschalten } from '../../aktionen';
|
||||
import { erfordereModul } from '@/lib/module/pruefen';
|
||||
import { erfordereBenutzer } from '@/lib/auth/rechte';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
@@ -17,6 +19,8 @@ export async function generateMetadata({ params }: { params: Promise<{ laufId: s
|
||||
}
|
||||
|
||||
export default async function LaufSeite({ params }: { params: Promise<{ laufId: string }> }) {
|
||||
await erfordereBenutzer();
|
||||
erfordereModul('checklisten');
|
||||
const { laufId } = await params;
|
||||
|
||||
// Enthält die Rechteprüfung: Wer nicht darf, bekommt null – und damit 404.
|
||||
|
||||
@@ -4,6 +4,8 @@ import { standVon } from '@/lib/repos/checklisten';
|
||||
import { checklistenPdf } from '@/lib/pdf/checkliste';
|
||||
import { alsDatumText, alsZeitpunktText } from '@/lib/zeit';
|
||||
import { laufZumLesen } from '../../../aktionen';
|
||||
import { erfordereBenutzer } from '@/lib/auth/rechte';
|
||||
import { modulAn, modulGesperrt } from '@/lib/module/pruefen';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
@@ -15,6 +17,8 @@ export const dynamic = 'force-dynamic';
|
||||
* gibt, geht einen Unbefugten nichts an.
|
||||
*/
|
||||
export async function GET(_anfrage: Request, kontext: { params: Promise<{ laufId: string }> }) {
|
||||
await erfordereBenutzer();
|
||||
if (!modulAn('checklisten')) return modulGesperrt();
|
||||
const { laufId } = await kontext.params;
|
||||
|
||||
const gelesen = await laufZumLesen(laufId);
|
||||
|
||||
@@ -6,12 +6,14 @@ import { inhalte } from '@/lib/inhalte';
|
||||
import { laeufeVon, standVon } from '@/lib/repos/checklisten';
|
||||
import { alsDatumText, alsZeitpunktText, heute } from '@/lib/zeit';
|
||||
import { StartFormular } from './Formulare';
|
||||
import { erfordereModul } from '@/lib/module/pruefen';
|
||||
|
||||
export const metadata: Metadata = { title: 'Checklisten' };
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export default async function ChecklistenSeite() {
|
||||
const benutzer = await erfordereBenutzer();
|
||||
erfordereModul('checklisten');
|
||||
const alle = inhalte();
|
||||
const istAzubi = benutzer.rolle === 'azubi';
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ import { istAbgeschlossen, zusammenfassung } from '@/lib/feedback/inhalt';
|
||||
import { alsDatumText, alsZeitpunktText } from '@/lib/zeit';
|
||||
import { Gespraechsformular } from '../Gespraechsformular';
|
||||
import { bestaetigungZurueck, gespraechVerwerfen, gespraechZumLesen } from '../aktionen';
|
||||
import { erfordereModul } from '@/lib/module/pruefen';
|
||||
import { erfordereBenutzer } from '@/lib/auth/rechte';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
@@ -18,6 +20,8 @@ export async function generateMetadata({ params }: { params: Promise<{ gespraech
|
||||
}
|
||||
|
||||
export default async function GespraechsSeite({ params }: { params: Promise<{ gespraechId: string }> }) {
|
||||
await erfordereBenutzer();
|
||||
erfordereModul('feedback');
|
||||
const { gespraechId } = await params;
|
||||
|
||||
const gelesen = await gespraechZumLesen(gespraechId);
|
||||
|
||||
@@ -4,11 +4,15 @@ import { inhaltVon } from '@/lib/repos/feedback';
|
||||
import { feedbackPdf } from '@/lib/pdf/feedback';
|
||||
import { alsDatumText, alsZeitpunktText } from '@/lib/zeit';
|
||||
import { gespraechZumLesen } from '../../aktionen';
|
||||
import { erfordereBenutzer } from '@/lib/auth/rechte';
|
||||
import { modulAn, modulGesperrt } from '@/lib/module/pruefen';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
/** Das Feedbackgespräch als PDF. Rechteprüfung über `gespraechZumLesen`. */
|
||||
export async function GET(_anfrage: Request, kontext: { params: Promise<{ gespraechId: string }> }) {
|
||||
await erfordereBenutzer();
|
||||
if (!modulAn('feedback')) return modulGesperrt();
|
||||
const { gespraechId } = await kontext.params;
|
||||
|
||||
const gelesen = await gespraechZumLesen(gespraechId);
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
loeschen,
|
||||
} from '@/lib/repos/feedback';
|
||||
import { istAbgeschlossen, type Gespraechsinhalt } from '@/lib/feedback/inhalt';
|
||||
import { pruefeModul } from '@/lib/module/pruefen';
|
||||
|
||||
export type Rueckmeldung = { fehler?: string; erfolg?: string };
|
||||
|
||||
@@ -30,6 +31,7 @@ const datumMuster = /^\d{4}-\d{2}-\d{2}$/;
|
||||
*/
|
||||
export async function gespraechZumLesen(id: string) {
|
||||
const benutzer = await erfordereBenutzer();
|
||||
pruefeModul('feedback');
|
||||
const gespraech = gespraechNachId(id);
|
||||
if (!gespraech) return null;
|
||||
|
||||
@@ -48,6 +50,7 @@ function aktualisiere(id?: string) {
|
||||
|
||||
export async function gespraechAnlegen(formular: FormData): Promise<void> {
|
||||
const ausbilder = await erfordereBenutzer();
|
||||
pruefeModul('feedback');
|
||||
const azubiId = String(formular.get('azubiId') ?? '');
|
||||
erfordereBewertungsrecht(ausbilder, azubiId);
|
||||
|
||||
@@ -67,6 +70,7 @@ export async function gespraechAnlegen(formular: FormData): Promise<void> {
|
||||
}
|
||||
|
||||
export async function gespraechSpeichern(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
|
||||
pruefeModul('feedback');
|
||||
const id = String(formular.get('gespraechId') ?? '');
|
||||
const gelesen = await gespraechZumLesen(id);
|
||||
if (!gelesen) return { fehler: 'Dieses Gespräch gibt es nicht.' };
|
||||
@@ -125,6 +129,7 @@ export async function gespraechSpeichern(_vorher: Rueckmeldung, formular: FormDa
|
||||
}
|
||||
|
||||
export async function gespraechBestaetigen(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
|
||||
pruefeModul('feedback');
|
||||
const id = String(formular.get('gespraechId') ?? '');
|
||||
const gelesen = await gespraechZumLesen(id);
|
||||
if (!gelesen) return { fehler: 'Dieses Gespräch gibt es nicht.' };
|
||||
@@ -148,6 +153,7 @@ export async function gespraechBestaetigen(_vorher: Rueckmeldung, formular: Form
|
||||
}
|
||||
|
||||
export async function bestaetigungZurueck(formular: FormData): Promise<void> {
|
||||
pruefeModul('feedback');
|
||||
const id = String(formular.get('gespraechId') ?? '');
|
||||
const gelesen = await gespraechZumLesen(id);
|
||||
if (!gelesen) return;
|
||||
@@ -164,6 +170,7 @@ export async function bestaetigungZurueck(formular: FormData): Promise<void> {
|
||||
}
|
||||
|
||||
export async function gespraechVerwerfen(formular: FormData): Promise<void> {
|
||||
pruefeModul('feedback');
|
||||
const id = String(formular.get('gespraechId') ?? '');
|
||||
const gelesen = await gespraechZumLesen(id);
|
||||
if (!gelesen) return;
|
||||
|
||||
@@ -7,12 +7,14 @@ import { gespraecheVon, inhaltVon } from '@/lib/repos/feedback';
|
||||
import { istAbgeschlossen, zusammenfassung } from '@/lib/feedback/inhalt';
|
||||
import { alsDatumText, heute, montagDerWoche } from '@/lib/zeit';
|
||||
import { gespraechAnlegen } from './aktionen';
|
||||
import { erfordereModul } from '@/lib/module/pruefen';
|
||||
|
||||
export const metadata: Metadata = { title: 'Feedbackgespräche' };
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export default async function FeedbackSeite() {
|
||||
const benutzer = await erfordereBenutzer();
|
||||
erfordereModul('feedback');
|
||||
const vorlage = inhalte().feedback;
|
||||
|
||||
const beteiligte =
|
||||
|
||||
@@ -8,6 +8,7 @@ import { gelesenAm } from '@/lib/repos/fortschritt';
|
||||
import { alsZeitpunktText } from '@/lib/zeit';
|
||||
import { gelesenUmschalten } from './aktionen';
|
||||
import { Druckknopf } from '@/komponenten/Druckknopf';
|
||||
import { modulStand } from '@/lib/repos/module';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
@@ -131,24 +132,26 @@ export default async function HandoutSeite({ params }: { params: Promise<{ id: s
|
||||
</details>
|
||||
) : null}
|
||||
|
||||
<section aria-labelledby="karten" className="karte stapel-eng">
|
||||
<h2 id="karten" style={{ marginBottom: '0.3rem' }}>
|
||||
Merkkarten zu diesem Handout
|
||||
</h2>
|
||||
<p className="leise" style={{ margin: 0 }}>
|
||||
{handout.karten.length === 1 ? 'Eine Karte' : `${handout.karten.length} Karten`} – im Lernmodus tauchen sie
|
||||
nach Fälligkeit auf.
|
||||
</p>
|
||||
<ol className="stapel-eng" style={{ margin: '0.5rem 0 0', paddingLeft: '1.2rem' }}>
|
||||
{handout.karten.map((karte, index) => (
|
||||
<li key={index}>
|
||||
<strong>{karte.frage}</strong>
|
||||
<br />
|
||||
<span className="leise">{karte.antwort}</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
{modulStand().karten ? (
|
||||
<section aria-labelledby="karten" className="karte stapel-eng">
|
||||
<h2 id="karten" style={{ marginBottom: '0.3rem' }}>
|
||||
Merkkarten zu diesem Handout
|
||||
</h2>
|
||||
<p className="leise" style={{ margin: 0 }}>
|
||||
{handout.karten.length === 1 ? 'Eine Karte' : `${handout.karten.length} Karten`} – im Lernmodus tauchen sie
|
||||
nach Fälligkeit auf.
|
||||
</p>
|
||||
<ol className="stapel-eng" style={{ margin: '0.5rem 0 0', paddingLeft: '1.2rem' }}>
|
||||
{handout.karten.map((karte, index) => (
|
||||
<li key={index}>
|
||||
<strong>{karte.frage}</strong>
|
||||
<br />
|
||||
<span className="leise">{karte.antwort}</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{istAzubi ? (
|
||||
<form action={gelesenUmschalten} className="nicht-drucken">
|
||||
|
||||
@@ -6,6 +6,7 @@ import { inhalte } from '@/lib/inhalte';
|
||||
import { bewerte } from '@/lib/repos/karten';
|
||||
import { bewertungen, fachNamen, type Bewertung } from '@/lib/lernen/sm2';
|
||||
import { alsDatumText, heute } from '@/lib/zeit';
|
||||
import { pruefeModul } from '@/lib/module/pruefen';
|
||||
|
||||
export type Bewertungsergebnis = {
|
||||
fehler?: string;
|
||||
@@ -25,6 +26,7 @@ export type Bewertungsergebnis = {
|
||||
*/
|
||||
export async function karteBewerten(karteId: string, bewertung: string): Promise<Bewertungsergebnis> {
|
||||
const benutzer = await erfordereBenutzer();
|
||||
pruefeModul('karten');
|
||||
if (benutzer.rolle !== 'azubi') {
|
||||
return { fehler: 'Nur der Azubi führt seinen eigenen Lernstand.' };
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { alsKartenstand, staendeVon } from '@/lib/repos/karten';
|
||||
import { fach, fachNamen, heutigerStapel } from '@/lib/lernen/sm2';
|
||||
import { heute } from '@/lib/zeit';
|
||||
import { Lernmodus, type Lernkarte } from './Lernmodus';
|
||||
import { erfordereModul } from '@/lib/module/pruefen';
|
||||
|
||||
export const metadata: Metadata = { title: 'Lernmodus' };
|
||||
export const dynamic = 'force-dynamic';
|
||||
@@ -15,6 +16,7 @@ const STAPELGROESSE = 20;
|
||||
|
||||
export default async function LernSeite({ searchParams }: { searchParams: Promise<{ block?: string }> }) {
|
||||
const benutzer = await erfordereBenutzer();
|
||||
erfordereModul('karten');
|
||||
if (benutzer.rolle !== 'azubi') redirect('/karten');
|
||||
|
||||
const { block } = await searchParams;
|
||||
|
||||
@@ -5,6 +5,7 @@ import { inhalte } from '@/lib/inhalte';
|
||||
import { alsKartenstand, staendeVon, uebersicht } from '@/lib/repos/karten';
|
||||
import { fach, fachNamen, heutigerStapel } from '@/lib/lernen/sm2';
|
||||
import { alsDatumText, heute } from '@/lib/zeit';
|
||||
import { erfordereModul } from '@/lib/module/pruefen';
|
||||
|
||||
export const metadata: Metadata = { title: 'Merkkarten' };
|
||||
export const dynamic = 'force-dynamic';
|
||||
@@ -13,6 +14,7 @@ const STAPELGROESSE = 20;
|
||||
|
||||
export default async function KartenSeite() {
|
||||
const benutzer = await erfordereBenutzer();
|
||||
erfordereModul('karten');
|
||||
const alle = inhalte();
|
||||
const heuteDatum = heute();
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { alsZeitpunktText } from '@/lib/zeit';
|
||||
import { Fragebogen } from '../Fragebogen';
|
||||
import { Korrektur, type Korrekturfrage } from '../Korrektur';
|
||||
import { standZumLesen } from '../aktionen';
|
||||
import { erfordereModul } from '@/lib/module/pruefen';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
@@ -27,6 +28,7 @@ export default async function KontrollSeite({
|
||||
searchParams: Promise<{ azubi?: string }>;
|
||||
}) {
|
||||
const benutzer = await erfordereBenutzer();
|
||||
erfordereModul('kontrollen');
|
||||
const { kontrolleId } = await params;
|
||||
const { azubi: azubiParameter } = await searchParams;
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
standVon,
|
||||
} from '@/lib/repos/kontrollen';
|
||||
import { darfBearbeiten } from '@/lib/kontrollen/sicht';
|
||||
import { pruefeModul } from '@/lib/module/pruefen';
|
||||
|
||||
export type Rueckmeldung = { fehler?: string; erfolg?: string };
|
||||
|
||||
@@ -36,6 +37,7 @@ export async function antwortZwischenspeichern(
|
||||
antwort: string,
|
||||
): Promise<{ gespeichertUm: string } | { fehler: string }> {
|
||||
const benutzer = await erfordereBenutzer();
|
||||
pruefeModul('kontrollen');
|
||||
|
||||
const inhalt = inhalte().kontrolleNach.get(kontrolleId);
|
||||
if (!inhalt) return { fehler: 'Diese Lernzielkontrolle gibt es nicht.' };
|
||||
@@ -57,6 +59,7 @@ export async function antwortZwischenspeichern(
|
||||
|
||||
export async function kontrolleAbgeben(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
|
||||
const benutzer = await erfordereBenutzer();
|
||||
pruefeModul('kontrollen');
|
||||
const kontrolleId = String(formular.get('kontrolleId') ?? '');
|
||||
|
||||
const inhalt = inhalte().kontrolleNach.get(kontrolleId);
|
||||
@@ -84,6 +87,7 @@ export async function kontrolleAbgeben(_vorher: Rueckmeldung, formular: FormData
|
||||
|
||||
export async function kontrolleFreigeben(formular: FormData): Promise<void> {
|
||||
const ausbilder = await erfordereBenutzer();
|
||||
pruefeModul('kontrollen');
|
||||
const azubiId = String(formular.get('azubiId') ?? '');
|
||||
const kontrolleId = String(formular.get('kontrolleId') ?? '');
|
||||
|
||||
@@ -96,6 +100,7 @@ export async function kontrolleFreigeben(formular: FormData): Promise<void> {
|
||||
|
||||
export async function freigabeZurueck(formular: FormData): Promise<void> {
|
||||
const ausbilder = await erfordereBenutzer();
|
||||
pruefeModul('kontrollen');
|
||||
const azubiId = String(formular.get('azubiId') ?? '');
|
||||
const kontrolleId = String(formular.get('kontrolleId') ?? '');
|
||||
|
||||
@@ -111,6 +116,7 @@ export async function freigabeZurueck(formular: FormData): Promise<void> {
|
||||
|
||||
export async function kontrolleKorrigieren(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
|
||||
const ausbilder = await erfordereBenutzer();
|
||||
pruefeModul('kontrollen');
|
||||
const azubiId = String(formular.get('azubiId') ?? '');
|
||||
const kontrolleId = String(formular.get('kontrolleId') ?? '');
|
||||
|
||||
@@ -153,6 +159,7 @@ export async function kontrolleKorrigieren(_vorher: Rueckmeldung, formular: Form
|
||||
/** Prüft den Lesezugriff und liefert den Stand. Wird von Seiten benutzt. */
|
||||
export async function standZumLesen(azubiId: string, kontrolleId: string) {
|
||||
const benutzer = await erfordereBenutzer();
|
||||
pruefeModul('kontrollen');
|
||||
|
||||
const bezug = ausbildungVonAzubi(azubiId);
|
||||
if (!darfAzubiDatenSehen(benutzer, azubiId, bezug)) return null;
|
||||
|
||||
@@ -8,6 +8,7 @@ import { gesamtpunkte, zustandVon, type Zustand } from '@/lib/kontrollen/sicht';
|
||||
import { verlauf } from '@/lib/kontrollen/bewertung';
|
||||
import { alsZeitpunktText } from '@/lib/zeit';
|
||||
import { kontrolleFreigeben, freigabeZurueck } from './aktionen';
|
||||
import { erfordereModul } from '@/lib/module/pruefen';
|
||||
|
||||
export const metadata: Metadata = { title: 'Lernzielkontrollen' };
|
||||
export const dynamic = 'force-dynamic';
|
||||
@@ -21,6 +22,7 @@ const zustandsmarke: Record<Zustand, { text: string; ton: string; zeichen: strin
|
||||
|
||||
export default async function KontrollenSeite() {
|
||||
const benutzer = await erfordereBenutzer();
|
||||
erfordereModul('kontrollen');
|
||||
const alle = inhalte();
|
||||
|
||||
if (benutzer.rolle === 'azubi') {
|
||||
|
||||
@@ -2,6 +2,8 @@ import { erfordereBenutzer } from '@/lib/auth/rechte';
|
||||
import { Kopfzeile, type Navigationseintrag } from '@/komponenten/Kopfzeile';
|
||||
import { Netzzustand } from '@/komponenten/Netzzustand';
|
||||
import { abmelden } from '../anmelden/aktionen';
|
||||
import { modulStand } from '@/lib/repos/module';
|
||||
import { nurOffene } from '@/lib/module/module';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
@@ -37,7 +39,8 @@ const fuerAusbilder: Navigationseintrag[] = [
|
||||
|
||||
export default async function AngemeldetLayout({ children }: { children: React.ReactNode }) {
|
||||
const benutzer = await erfordereBenutzer();
|
||||
const eintraege = benutzer.rolle === 'ausbilder' ? fuerAusbilder : fuerAzubi;
|
||||
// Abgeschaltete Module verschwinden aus der Navigation (lib/module).
|
||||
const eintraege = nurOffene(benutzer.rolle === 'ausbilder' ? fuerAusbilder : fuerAzubi, modulStand());
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -10,6 +10,7 @@ import { wartetAufAbzeichnung } from '@/lib/repos/berichtsheft';
|
||||
import { wartetAufKorrektur } from '@/lib/repos/kontrollen';
|
||||
import { benutzerNachId } from '@/lib/repos/benutzer';
|
||||
import { WochenAnsicht } from './WochenAnsicht';
|
||||
import { modulStand } from '@/lib/repos/module';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
@@ -151,10 +152,18 @@ function WartetAufMich({ ausbilderId }: { ausbilderId: string }) {
|
||||
const meine = ausbildungenVonAusbilder(ausbilderId);
|
||||
const alle = inhalte();
|
||||
const azubiIds = meine.map(({ azubi }) => azubi.id);
|
||||
const abnahmen = wartetAufAbnahme(azubiIds);
|
||||
const berichte = wartetAufAbzeichnung(azubiIds);
|
||||
const kontrollen = wartetAufKorrektur(azubiIds);
|
||||
// Nur was zu eingeschalteten Modulen gehört – eine Abnahme, die man nicht
|
||||
// öffnen kann, wäre ein toter Verweis.
|
||||
const eingeschaltet = modulStand();
|
||||
const abnahmen = eingeschaltet.aufgaben ? wartetAufAbnahme(azubiIds) : [];
|
||||
const berichte = eingeschaltet.berichtsheft ? wartetAufAbzeichnung(azubiIds) : [];
|
||||
const kontrollen = eingeschaltet.kontrollen ? wartetAufKorrektur(azubiIds) : [];
|
||||
const gesamt = abnahmen.length + berichte.length + kontrollen.length;
|
||||
const art = [
|
||||
eingeschaltet.aufgaben ? 'keine Abnahme' : null,
|
||||
eingeschaltet.kontrollen ? 'keine Korrektur' : null,
|
||||
eingeschaltet.berichtsheft ? 'kein Berichtsheft' : null,
|
||||
].filter((teil) => teil !== null);
|
||||
|
||||
return (
|
||||
<section className="karte stapel-eng" aria-labelledby="wartet">
|
||||
@@ -164,7 +173,7 @@ function WartetAufMich({ ausbilderId }: { ausbilderId: string }) {
|
||||
|
||||
{gesamt === 0 ? (
|
||||
<p className="leise" style={{ marginBottom: 0 }}>
|
||||
Nichts offen – keine Abnahme, keine Korrektur, kein Berichtsheft.
|
||||
{art.length > 0 ? `Nichts offen – ${art.join(', ')}.` : 'Nichts offen.'}
|
||||
</p>
|
||||
) : (
|
||||
<ul className="offen-liste">
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
/** Die beiden Bereiche der Verwaltung. */
|
||||
export function Unternavigation({ aktiv }: { aktiv: 'konten' | 'module' }) {
|
||||
return (
|
||||
<nav className="reihe nicht-drucken" aria-label="Bereiche der Verwaltung">
|
||||
<Link
|
||||
href="/verwaltung"
|
||||
className={`knopf ${aktiv === 'konten' ? '' : 'knopf-zweit'}`}
|
||||
aria-current={aktiv === 'konten' ? 'page' : undefined}
|
||||
>
|
||||
Konten
|
||||
</Link>
|
||||
<Link
|
||||
href="/verwaltung/module"
|
||||
className={`knopf ${aktiv === 'module' ? '' : 'knopf-zweit'}`}
|
||||
aria-current={aktiv === 'module' ? 'page' : undefined}
|
||||
>
|
||||
Module
|
||||
</Link>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
'use server';
|
||||
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import { erfordereAusbilder } from '@/lib/auth/rechte';
|
||||
import { istModulId } from '@/lib/module/module';
|
||||
import { modulSetzen } from '@/lib/repos/module';
|
||||
|
||||
/** Schaltet ein Modul für die ganze Lern-Bar ein oder aus. Nur Ausbilder. */
|
||||
export async function modulUmschalten(formular: FormData): Promise<void> {
|
||||
const ausbilder = await erfordereAusbilder();
|
||||
const modul = String(formular.get('modul') ?? '');
|
||||
const aktiv = String(formular.get('aktiv') ?? '') === '1';
|
||||
|
||||
if (!istModulId(modul)) return;
|
||||
modulSetzen(modul, aktiv, ausbilder.id);
|
||||
|
||||
// Navigation, Startseite, Auswertung – alles hängt am Modulstand.
|
||||
revalidatePath('/', 'layout');
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import type { Metadata } from 'next';
|
||||
import { erfordereAusbilder } from '@/lib/auth/rechte';
|
||||
import { modulIds, moduleintraege } from '@/lib/module/module';
|
||||
import { modulAenderungen, modulStand } from '@/lib/repos/module';
|
||||
import { benutzerNachId } from '@/lib/repos/benutzer';
|
||||
import { alsZeitpunktText } from '@/lib/zeit';
|
||||
import { Unternavigation } from '../Unternavigation';
|
||||
import { modulUmschalten } from './aktionen';
|
||||
|
||||
export const metadata: Metadata = { title: 'Module' };
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export default async function ModulSeite() {
|
||||
await erfordereAusbilder();
|
||||
const stand = modulStand();
|
||||
const aenderungen = new Map(modulAenderungen().map((zeile) => [zeile.modul, zeile]));
|
||||
|
||||
return (
|
||||
<div className="stapel">
|
||||
<div>
|
||||
<h1>Verwaltung</h1>
|
||||
<p className="leise">
|
||||
Module, die ihr nicht braucht, lassen sich abschalten – für die ganze Lern-Bar, für alle Azubis und
|
||||
Ausbilder zugleich.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Unternavigation aktiv="module" />
|
||||
|
||||
<p className="hinweis">
|
||||
<strong>Abschalten löscht nichts.</strong> Ein abgeschaltetes Modul verschwindet aus Navigation, Wochenplan
|
||||
und Auswertung, und seine Seiten sind gesperrt. Was darin schon geschrieben wurde, bleibt gespeichert und ist
|
||||
nach dem Einschalten unverändert wieder da. Wochenplan, Handouts, Glossar, Auswertung und Verwaltung sind
|
||||
immer an.
|
||||
</p>
|
||||
|
||||
<div className="stapel">
|
||||
{modulIds.map((id) => {
|
||||
const eintrag = moduleintraege[id];
|
||||
const an = stand[id];
|
||||
const aenderung = aenderungen.get(id);
|
||||
const wer = aenderung?.geaendertVon ? benutzerNachId(aenderung.geaendertVon)?.name : null;
|
||||
|
||||
return (
|
||||
<section key={id} className="karte stapel-eng" aria-labelledby={`modul-${id}`}>
|
||||
<div className="reihe" style={{ justifyContent: 'space-between' }}>
|
||||
<div className="reihe" style={{ gap: '0.6rem' }}>
|
||||
<h2 id={`modul-${id}`} style={{ margin: 0, fontSize: '1.05rem' }}>
|
||||
{eintrag.titel}
|
||||
</h2>
|
||||
<span className="marke-status" data-ton={an ? 'gut' : 'warnung'}>
|
||||
{an ? '● an' : '○ aus'}
|
||||
</span>
|
||||
</div>
|
||||
<form action={modulUmschalten}>
|
||||
<input type="hidden" name="modul" value={id} />
|
||||
<input type="hidden" name="aktiv" value={an ? '0' : '1'} />
|
||||
<button type="submit" className={`knopf ${an ? 'knopf-zweit' : ''}`}>
|
||||
{an ? 'Abschalten' : 'Einschalten'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<p style={{ margin: 0 }}>{eintrag.beschreibung}</p>
|
||||
<p className="leise" style={{ margin: 0 }}>
|
||||
<strong>Wenn aus:</strong> {eintrag.folgen}
|
||||
</p>
|
||||
{aenderung ? (
|
||||
<p className="leise" style={{ margin: 0 }}>
|
||||
Zuletzt {aenderung.aktiv === 1 ? 'eingeschaltet' : 'abgeschaltet'} am{' '}
|
||||
{alsZeitpunktText(aenderung.geaendertAm)}
|
||||
{wer ? ` von ${wer}` : ''}.
|
||||
</p>
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { ausbildungenVonAusbilder } from '@/lib/repos/ausbildung';
|
||||
import { alsDatumText, alsZeitpunktText, heute } from '@/lib/zeit';
|
||||
import { AusbildungFormular, AzubiAnlegenFormular, PasswortFormular } from './Formulare';
|
||||
import { kontoAktivSetzen } from './aktionen';
|
||||
import { Unternavigation } from './Unternavigation';
|
||||
|
||||
export const metadata: Metadata = { title: 'Verwaltung' };
|
||||
export const dynamic = 'force-dynamic';
|
||||
@@ -17,10 +18,13 @@ export default async function VerwaltungsSeite() {
|
||||
<div>
|
||||
<h1>Verwaltung</h1>
|
||||
<p className="leise">
|
||||
Hier legst du Azubikonten an und pflegst die Eckdaten der Ausbildung. Mehr Verwaltung gibt es bewusst nicht.
|
||||
Hier legst du Azubikonten an und pflegst die Eckdaten der Ausbildung. Unter „Module“ schaltest du ab, was
|
||||
ihr nicht braucht.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Unternavigation aktiv="konten" />
|
||||
|
||||
<section className="karte stapel" aria-labelledby="ueberschrift-azubis">
|
||||
<h2 id="ueberschrift-azubis" style={{ marginBottom: 0 }}>
|
||||
Deine Auszubildenden
|
||||
|
||||
@@ -3,6 +3,7 @@ import { darfAzubiDatenSehen } from '@/lib/auth/regeln';
|
||||
import { ausbildungVonAzubi } from '@/lib/auth/rechte';
|
||||
import { anhangLesen, anhangNachId } from '@/lib/repos/anhaenge';
|
||||
import { istBild } from '@/lib/anhaenge/dateien';
|
||||
import { modulAn, modulGesperrt } from '@/lib/module/pruefen';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
@@ -18,6 +19,7 @@ export const dynamic = 'force-dynamic';
|
||||
export async function GET(_anfrage: Request, kontext: { params: Promise<{ id: string }> }) {
|
||||
const benutzer = await angemeldeterBenutzer();
|
||||
if (!benutzer) return new Response('Nicht angemeldet.', { status: 401 });
|
||||
if (!modulAn('aufgaben')) return modulGesperrt();
|
||||
|
||||
const { id } = await kontext.params;
|
||||
const eintrag = anhangNachId(id);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { bewerte } from '@/lib/repos/karten';
|
||||
import { bewertungen, type Bewertung } from '@/lib/lernen/sm2';
|
||||
import { heute } from '@/lib/zeit';
|
||||
import { tagDerBewertung } from '@/lib/offline/nachtrag';
|
||||
import { modulAn } from '@/lib/module/pruefen';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
@@ -28,6 +29,9 @@ type Eingang = { karteId?: unknown; bewertung?: unknown; erfasstAm?: unknown };
|
||||
export async function POST(anfrage: Request): Promise<Response> {
|
||||
const benutzer = await angemeldeterBenutzer();
|
||||
if (!benutzer) return Response.json({ fehler: 'Nicht angemeldet.' }, { status: 401 });
|
||||
// 410 statt 404: Das Gerät soll seine gesammelten Bewertungen verwerfen,
|
||||
// statt sie bei jeder Verbindung erneut zu schicken.
|
||||
if (!modulAn('karten')) return Response.json({ fehler: 'Die Merkkarten sind abgeschaltet.' }, { status: 410 });
|
||||
if (benutzer.rolle !== 'azubi') {
|
||||
return Response.json({ fehler: 'Nur der Azubi führt seinen eigenen Lernstand.' }, { status: 403 });
|
||||
}
|
||||
|
||||
+15
-6
@@ -1,4 +1,16 @@
|
||||
import type { MetadataRoute } from 'next';
|
||||
import { modulStand } from '@/lib/repos/module';
|
||||
import { pfadOffen } from '@/lib/module/module';
|
||||
|
||||
// Die Kurzbefehle richten sich nach den eingeschalteten Modulen – also bei
|
||||
// jedem Abruf neu erzeugen, nicht einmal beim Bau.
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
const kurzbefehle = [
|
||||
{ name: 'Lernmodus', short_name: 'Lernen', url: '/karten/lernen' },
|
||||
{ name: 'Berichtsheft', short_name: 'Bericht', url: '/berichtsheft' },
|
||||
{ name: 'Handouts', short_name: 'Handouts', url: '/handouts' },
|
||||
];
|
||||
|
||||
/**
|
||||
* Die Angaben für die Installation auf dem Startbildschirm.
|
||||
@@ -8,11 +20,12 @@ import type { MetadataRoute } from 'next';
|
||||
* abgelaufen, leitet der Server ohnehin zur Anmeldung.
|
||||
*/
|
||||
export default function manifest(): MetadataRoute.Manifest {
|
||||
const stand = modulStand();
|
||||
return {
|
||||
name: 'Lern-Bar – Betriebliche Ausbildung',
|
||||
short_name: 'Lern-Bar',
|
||||
description:
|
||||
'Wochenplan, Handouts, Merkkarten, Aufgaben, Checklisten und Berichtsheft für die Ausbildung zum Fachinformatiker für Systemintegration.',
|
||||
'Wochenplan, Lehrgang und Nachweise für die Ausbildung zum Fachinformatiker für Systemintegration.',
|
||||
lang: 'de',
|
||||
dir: 'ltr',
|
||||
start_url: '/',
|
||||
@@ -27,10 +40,6 @@ export default function manifest(): MetadataRoute.Manifest {
|
||||
{ src: '/symbol-512.png', sizes: '512x512', type: 'image/png', purpose: 'any' },
|
||||
{ src: '/symbol-maskabel-512.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' },
|
||||
],
|
||||
shortcuts: [
|
||||
{ name: 'Lernmodus', short_name: 'Lernen', url: '/karten/lernen' },
|
||||
{ name: 'Berichtsheft', short_name: 'Bericht', url: '/berichtsheft' },
|
||||
{ name: 'Handouts', short_name: 'Handouts', url: '/handouts' },
|
||||
],
|
||||
shortcuts: kurzbefehle.filter((kurz) => pfadOffen(kurz.url, stand)),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { berichteVon, fehlendeWochen } from '../repos/berichtsheft';
|
||||
import { ergebnisVon, staendeVon as kontrollStaende } from '../repos/kontrollen';
|
||||
import { alsDatumText, alsZeitpunktText } from '../zeit';
|
||||
import { berechneAbdeckung, berechneLernfelder, type Nachweis } from './abdeckung';
|
||||
import { modulStand } from '../repos/module';
|
||||
|
||||
/**
|
||||
* Sammelt aus Inhalten und Fortschritt alles, was die Auswertung braucht.
|
||||
@@ -17,6 +18,9 @@ import { berechneAbdeckung, berechneLernfelder, type Nachweis } from './abdeckun
|
||||
|
||||
export function rahmenplanabdeckung(azubiId: string) {
|
||||
const alle = inhalte();
|
||||
// Abgeschaltete Module liefern keine Nachweise. Sonst stünden ihre Inhalte
|
||||
// für immer als „geplant, noch nicht bearbeitet" da.
|
||||
const eingeschaltet = modulStand();
|
||||
const gelesen = gelesenSet(azubiId);
|
||||
const aufgaben = aufgabenStaende(azubiId);
|
||||
const laeufe = laeufeVon(azubiId);
|
||||
@@ -47,7 +51,7 @@ export function rahmenplanabdeckung(azubiId: string) {
|
||||
}
|
||||
|
||||
// --- Praxisaufgaben ---
|
||||
for (const aufgabe of alle.aufgaben) {
|
||||
for (const aufgabe of eingeschaltet.aufgaben ? alle.aufgaben : []) {
|
||||
const stand = aufgaben.get(aufgabe.id);
|
||||
const nachweis: Nachweis = {
|
||||
art: 'aufgabe',
|
||||
@@ -61,7 +65,7 @@ export function rahmenplanabdeckung(azubiId: string) {
|
||||
}
|
||||
|
||||
// --- Checklisten ---
|
||||
for (const liste of alle.checklisten) {
|
||||
for (const liste of eingeschaltet.checklisten ? alle.checklisten : []) {
|
||||
const abgeschlossenAm = abgeschlosseneListen.get(liste.id);
|
||||
const nachweis: Nachweis = {
|
||||
art: 'checkliste',
|
||||
@@ -75,6 +79,7 @@ export function rahmenplanabdeckung(azubiId: string) {
|
||||
}
|
||||
|
||||
return {
|
||||
module: eingeschaltet,
|
||||
positionen: berechneAbdeckung(alle.positionen, jePosition),
|
||||
lernfelder: berechneLernfelder(alle.lernfelder, jeLernfeld),
|
||||
gruppennamen: alle.rahmenplan.gruppen,
|
||||
@@ -84,6 +89,7 @@ export function rahmenplanabdeckung(azubiId: string) {
|
||||
/** Der persönliche Stand des Azubis – was er selbst sehen will. */
|
||||
export function eigenerStand(azubiId: string, beginn: string | null) {
|
||||
const alle = inhalte();
|
||||
const eingeschaltet = modulStand();
|
||||
const gelesen = gelesenSet(azubiId);
|
||||
const aufgaben = aufgabenStaende(azubiId);
|
||||
const karten = kartenUebersicht(azubiId, alle.karten.map((k) => k.id));
|
||||
@@ -111,6 +117,7 @@ export function eigenerStand(azubiId: string, beginn: string | null) {
|
||||
.sort((a, b) => a.woche - b.woche);
|
||||
|
||||
return {
|
||||
module: eingeschaltet,
|
||||
handouts: { erledigt: gelesen.size, gesamt: alle.handouts.length },
|
||||
karten: { erledigt: karten.gelernt, gesamt: karten.gesamt, faellig: karten.faellig },
|
||||
aufgaben: {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
-- Fassung 1.1: Module ein- und ausschalten.
|
||||
--
|
||||
-- Eine Zeile je Modul, das schon einmal umgeschaltet wurde. Fehlt die Zeile,
|
||||
-- ist das Modul eingeschaltet – eine frische Datenbank hat alles an, und eine
|
||||
-- bestehende verhält sich nach dem Einspielen genau wie vorher.
|
||||
--
|
||||
-- Abschalten löscht nichts. Die Daten eines Moduls bleiben in ihren Tabellen
|
||||
-- und sind nach dem Wiedereinschalten unverändert da.
|
||||
|
||||
CREATE TABLE modul_einstellung (
|
||||
modul TEXT PRIMARY KEY,
|
||||
aktiv INTEGER NOT NULL CHECK (aktiv IN (0, 1)),
|
||||
geaendert_von TEXT REFERENCES benutzer (id) ON DELETE SET NULL,
|
||||
geaendert_am TEXT NOT NULL
|
||||
);
|
||||
@@ -323,6 +323,14 @@ export const feedbackgespraech = sqliteTable(
|
||||
(t) => [index('feedbackgespraech_ausbildung_idx').on(t.ausbildungId, t.datum)],
|
||||
);
|
||||
|
||||
/** Abschaltbare Module – eine Zeile je Modul, das je umgeschaltet wurde. Siehe lib/module. */
|
||||
export const modulEinstellung = sqliteTable('modul_einstellung', {
|
||||
modul: text('modul').primaryKey(),
|
||||
aktiv: integer('aktiv').notNull(),
|
||||
geaendertVon: text('geaendert_von').references(() => benutzer.id, { onDelete: 'set null' }),
|
||||
geaendertAm: text('geaendert_am').notNull(),
|
||||
});
|
||||
|
||||
export type Benutzer = typeof benutzer.$inferSelect;
|
||||
export type Sitzung = typeof sitzung.$inferSelect;
|
||||
export type Ausbildung = typeof ausbildung.$inferSelect;
|
||||
@@ -339,6 +347,7 @@ export type Wochenbericht = typeof wochenbericht.$inferSelect;
|
||||
export type Kontrolle = typeof kontrolle.$inferSelect;
|
||||
export type KontrollAntwort = typeof kontrollAntwort.$inferSelect;
|
||||
export type Feedbackgespraech = typeof feedbackgespraech.$inferSelect;
|
||||
export type ModulEinstellung = typeof modulEinstellung.$inferSelect;
|
||||
|
||||
/** Wird von der Schema-Abgleichprüfung im Test verwendet. */
|
||||
export const alleTabellen = {
|
||||
@@ -358,4 +367,5 @@ export const alleTabellen = {
|
||||
kontrolle,
|
||||
kontrollAntwort,
|
||||
feedbackgespraech,
|
||||
modulEinstellung,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
abgeschaltet,
|
||||
alleAn,
|
||||
istModulId,
|
||||
modulIds,
|
||||
moduleintraege,
|
||||
modulVonPfad,
|
||||
nurOffene,
|
||||
pfadOffen,
|
||||
standAus,
|
||||
} from './module';
|
||||
|
||||
describe('standAus', () => {
|
||||
it('hat in einer frischen Datenbank alles an', () => {
|
||||
expect(standAus([])).toEqual(alleAn);
|
||||
expect(Object.values(standAus([])).every(Boolean)).toBe(true);
|
||||
});
|
||||
|
||||
it('übernimmt gespeicherte Schalter', () => {
|
||||
const stand = standAus([
|
||||
{ modul: 'berichtsheft', aktiv: 0 },
|
||||
{ modul: 'karten', aktiv: 1 },
|
||||
]);
|
||||
expect(stand.berichtsheft).toBe(false);
|
||||
expect(stand.karten).toBe(true);
|
||||
expect(stand.aufgaben).toBe(true);
|
||||
});
|
||||
|
||||
it('übergeht unbekannte Kennungen, etwa aus einer späteren Fassung', () => {
|
||||
const stand = standAus([{ modul: 'gibtsnicht', aktiv: 0 }]);
|
||||
expect(stand).toEqual(alleAn);
|
||||
expect('gibtsnicht' in stand).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('modulVonPfad', () => {
|
||||
it('ordnet Seiten und Unterseiten ihrem Modul zu', () => {
|
||||
expect(modulVonPfad('/berichtsheft')).toBe('berichtsheft');
|
||||
expect(modulVonPfad('/berichtsheft/abc/pdf')).toBe('berichtsheft');
|
||||
expect(modulVonPfad('/karten/lernen')).toBe('karten');
|
||||
expect(modulVonPfad('/checklisten/lauf/123')).toBe('checklisten');
|
||||
});
|
||||
|
||||
it('ordnet Routen außerhalb der Seiten ihrem Modul zu', () => {
|
||||
expect(modulVonPfad('/anhaenge/abc')).toBe('aufgaben');
|
||||
expect(modulVonPfad('/api/lernstand')).toBe('karten');
|
||||
});
|
||||
|
||||
it('lässt das Gerüst unangetastet', () => {
|
||||
for (const pfad of ['/', '/wochenplan', '/woche/3', '/handouts', '/handouts/N3', '/glossar', '/auswertung', '/verwaltung', '/verwaltung/module']) {
|
||||
expect(modulVonPfad(pfad), pfad).toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
it('trifft nur ganze Pfadteile', () => {
|
||||
// „/kartenspiel" gehört nicht zu den Merkkarten.
|
||||
expect(modulVonPfad('/kartenspiel')).toBeNull();
|
||||
expect(modulVonPfad('/feedbackbogen')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('pfadOffen und nurOffene', () => {
|
||||
const navigation = [
|
||||
{ pfad: '/', titel: 'Woche' },
|
||||
{ pfad: '/karten', titel: 'Merkkarten' },
|
||||
{ pfad: '/berichtsheft', titel: 'Berichtsheft' },
|
||||
{ pfad: '/glossar', titel: 'Glossar' },
|
||||
];
|
||||
|
||||
it('lässt bei allem an die Navigation unverändert', () => {
|
||||
expect(nurOffene(navigation, alleAn)).toEqual(navigation);
|
||||
});
|
||||
|
||||
it('entfernt genau die abgeschalteten Module', () => {
|
||||
const stand = { ...alleAn, berichtsheft: false };
|
||||
expect(nurOffene(navigation, stand).map((e) => e.titel)).toEqual(['Woche', 'Merkkarten', 'Glossar']);
|
||||
expect(pfadOffen('/berichtsheft/pdf', stand)).toBe(false);
|
||||
expect(pfadOffen('/karten/lernen', stand)).toBe(true);
|
||||
});
|
||||
|
||||
it('behält das Gerüst auch, wenn alles aus ist', () => {
|
||||
const alleAus = Object.fromEntries(modulIds.map((id) => [id, false])) as typeof alleAn;
|
||||
expect(nurOffene(navigation, alleAus).map((e) => e.titel)).toEqual(['Woche', 'Glossar']);
|
||||
expect(abgeschaltet(alleAus)).toEqual([...modulIds]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Moduleinträge', () => {
|
||||
it('beschreibt jedes Modul vollständig', () => {
|
||||
for (const id of modulIds) {
|
||||
const eintrag = moduleintraege[id];
|
||||
expect(eintrag.id).toBe(id);
|
||||
expect(eintrag.titel.length, id).toBeGreaterThan(0);
|
||||
expect(eintrag.beschreibung.length, id).toBeGreaterThan(10);
|
||||
expect(eintrag.folgen.length, id).toBeGreaterThan(10);
|
||||
expect(eintrag.pfade.length, id).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
it('erkennt gültige Kennungen', () => {
|
||||
expect(istModulId('karten')).toBe(true);
|
||||
expect(istModulId('verwaltung')).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* Die abschaltbaren Module der Lern-Bar.
|
||||
*
|
||||
* Bewusst ohne Datenbank und ohne Next.js – die Liste und die Regeln stehen
|
||||
* hier, der gespeicherte Zustand kommt aus `repos/module`, die Durchsetzung aus
|
||||
* `module/pruefen`. So lassen sich die Regeln vollständig testen.
|
||||
*
|
||||
* Immer an bleiben Wochenplan, Handouts, Glossar, Auswertung und Verwaltung:
|
||||
* Sie sind das Gerüst, an dem alles andere hängt. Die Auswertung passt sich an
|
||||
* und zeigt nur, was eingeschaltet ist.
|
||||
*
|
||||
* Abschalten heißt ausblenden und sperren, **nicht löschen**. Was ein Azubi in
|
||||
* einem Modul geschrieben hat, bleibt in der Datenbank und ist nach dem
|
||||
* Wiedereinschalten unverändert da. Siehe E43.
|
||||
*/
|
||||
|
||||
export const modulIds = ['karten', 'aufgaben', 'checklisten', 'berichtsheft', 'kontrollen', 'feedback'] as const;
|
||||
export type ModulId = (typeof modulIds)[number];
|
||||
|
||||
/** Für jedes Modul: an oder aus. */
|
||||
export type ModulStand = Record<ModulId, boolean>;
|
||||
|
||||
export type Moduleintrag = {
|
||||
id: ModulId;
|
||||
titel: string;
|
||||
/** Was das Modul tut – für die Verwaltung. */
|
||||
beschreibung: string;
|
||||
/** Was sich ändert, wenn es aus ist – damit niemand überrascht wird. */
|
||||
folgen: string;
|
||||
/** Die Pfade, die zu diesem Modul gehören und mit ihm gesperrt werden. */
|
||||
pfade: string[];
|
||||
};
|
||||
|
||||
export const moduleintraege: Record<ModulId, Moduleintrag> = {
|
||||
karten: {
|
||||
id: 'karten',
|
||||
titel: 'Merkkarten',
|
||||
beschreibung: 'Wiederholung nach Zeitabstand, täglicher Stapel, Lernen auch ohne Netz.',
|
||||
folgen:
|
||||
'Merkkarten verschwinden aus Handouts, Wochenfortschritt und „Mein Stand". Bewertungen, die ein Gerät noch ohne Netz gesammelt hat, werden verworfen.',
|
||||
pfade: ['/karten', '/api/lernstand'],
|
||||
},
|
||||
aufgaben: {
|
||||
id: 'aufgaben',
|
||||
titel: 'Praxisaufgaben',
|
||||
beschreibung: 'Aufträge mit Bewertungskriterien, Notizen und Anhängen, Abnahme mit Note.',
|
||||
folgen:
|
||||
'Aufgaben verschwinden aus Wochenplan, „Was auf dich wartet" und Auswertung. In der Rahmenplanabdeckung zählen sie nicht mehr; Anhänge sind nicht mehr abrufbar.',
|
||||
pfade: ['/aufgaben', '/anhaenge'],
|
||||
},
|
||||
checklisten: {
|
||||
id: 'checklisten',
|
||||
titel: 'Checklisten',
|
||||
beschreibung: 'Abläufe Schritt für Schritt abhaken, Durchläufe als Nachweis mit PDF.',
|
||||
folgen:
|
||||
'Checklisten verschwinden aus Wochenplan und Wochenfortschritt. In der Rahmenplanabdeckung und bei den Vorschlägen fürs Berichtsheft zählen sie nicht mehr.',
|
||||
pfade: ['/checklisten'],
|
||||
},
|
||||
berichtsheft: {
|
||||
id: 'berichtsheft',
|
||||
titel: 'Berichtsheft',
|
||||
beschreibung: 'Tagesnotizen, Wochenblätter mit Vorschlägen, Einreichen, Abzeichnen und PDF im IHK-Layout.',
|
||||
folgen:
|
||||
'Das Berichtsheft verschwindet aus Navigation, „Was auf dich wartet" und „Mein Stand". Den Ausbildungsnachweis muss der Azubi dann anderswo führen – auf Papier oder im Portal der Kammer.',
|
||||
pfade: ['/berichtsheft'],
|
||||
},
|
||||
kontrollen: {
|
||||
id: 'kontrollen',
|
||||
titel: 'Lernzielkontrollen',
|
||||
beschreibung: 'Freigabe durch den Ausbilder, Abgabe, Korrektur mit Punkten und IHK-Notenschlüssel.',
|
||||
folgen:
|
||||
'Kontrollen verschwinden aus Wochenplan, Wochenfortschritt, „Was auf dich wartet" und dem Ergebnisverlauf der Auswertung.',
|
||||
pfade: ['/kontrollen'],
|
||||
},
|
||||
feedback: {
|
||||
id: 'feedback',
|
||||
titel: 'Feedbackgespräch',
|
||||
beschreibung: 'Leitfaden und Bewertungsbogen, getrennte Einschätzung beider Seiten, PDF für die Akte.',
|
||||
folgen: 'Der Bereich „Feedback" verschwindet aus der Navigation. Bereits geführte Gespräche bleiben gespeichert.',
|
||||
pfade: ['/feedback'],
|
||||
},
|
||||
};
|
||||
|
||||
/** Werkszustand: alles an. */
|
||||
export const alleAn: ModulStand = Object.fromEntries(modulIds.map((id) => [id, true])) as ModulStand;
|
||||
|
||||
export function istModulId(wert: string): wert is ModulId {
|
||||
return (modulIds as readonly string[]).includes(wert);
|
||||
}
|
||||
|
||||
/**
|
||||
* Baut den Stand aus gespeicherten Zeilen. Fehlt ein Modul, gilt es als an –
|
||||
* eine frische Datenbank hat alles eingeschaltet. Unbekannte Kennungen (etwa
|
||||
* aus einer späteren Fassung) werden übergangen.
|
||||
*/
|
||||
export function standAus(zeilen: { modul: string; aktiv: number }[]): ModulStand {
|
||||
const stand: ModulStand = { ...alleAn };
|
||||
for (const zeile of zeilen) {
|
||||
if (istModulId(zeile.modul)) stand[zeile.modul] = zeile.aktiv === 1;
|
||||
}
|
||||
return stand;
|
||||
}
|
||||
|
||||
/** Zu welchem Modul gehört ein Pfad? `null`, wenn zu keinem abschaltbaren. */
|
||||
export function modulVonPfad(pfad: string): ModulId | null {
|
||||
for (const id of modulIds) {
|
||||
for (const wurzel of moduleintraege[id].pfade) {
|
||||
if (pfad === wurzel || pfad.startsWith(`${wurzel}/`)) return id;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Ist ein Pfad bei diesem Stand erreichbar? */
|
||||
export function pfadOffen(pfad: string, stand: ModulStand): boolean {
|
||||
const modul = modulVonPfad(pfad);
|
||||
return modul === null || stand[modul];
|
||||
}
|
||||
|
||||
/** Lässt aus einer Liste von Zielen nur die erreichbaren übrig – für Navigation und Kurzbefehle. */
|
||||
export function nurOffene<T extends { pfad: string }>(eintraege: T[], stand: ModulStand): T[] {
|
||||
return eintraege.filter((eintrag) => pfadOffen(eintrag.pfad, stand));
|
||||
}
|
||||
|
||||
/** Die abgeschalteten Module, in fester Reihenfolge. */
|
||||
export function abgeschaltet(stand: ModulStand): ModulId[] {
|
||||
return modulIds.filter((id) => !stand[id]);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import 'server-only';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { modulStand } from '../repos/module';
|
||||
import { moduleintraege, type ModulId } from './module';
|
||||
|
||||
/**
|
||||
* Die Durchsetzung der Modulschalter. Ein abgeschaltetes Modul ist nicht nur
|
||||
* unsichtbar, sondern zu – an drei Stellen:
|
||||
*
|
||||
* - **Seiten** rufen `erfordereModul` auf und enden mit „nicht gefunden",
|
||||
* genau wie bei fehlender Berechtigung (E21).
|
||||
* - **Routen** (PDF, Anhänge, Lernstand) fragen `modulAn` und antworten selbst.
|
||||
* - **Serveraktionen** rufen `pruefeModul` auf. Ein Formular, das noch in einem
|
||||
* offenen Fenster steht, darf nach dem Abschalten nichts mehr schreiben.
|
||||
*
|
||||
* Bewusst nicht im Layout des Moduls: Next.js rendert Layouts beim Wechsel
|
||||
* zwischen Seiten darunter nicht neu, eine Prüfung dort würde also nicht bei
|
||||
* jedem Aufruf greifen.
|
||||
*/
|
||||
|
||||
export function modulAn(modul: ModulId): boolean {
|
||||
return modulStand()[modul];
|
||||
}
|
||||
|
||||
/** Für Seiten: abgeschaltet heißt „nicht gefunden". */
|
||||
export function erfordereModul(modul: ModulId): void {
|
||||
if (!modulAn(modul)) notFound();
|
||||
}
|
||||
|
||||
/** Für Serveraktionen: abgeschaltet heißt abbrechen, bevor etwas geschrieben wird. */
|
||||
export function pruefeModul(modul: ModulId): void {
|
||||
if (!modulAn(modul)) {
|
||||
throw new Error(`Das Modul „${moduleintraege[modul].titel}" ist abgeschaltet.`);
|
||||
}
|
||||
}
|
||||
|
||||
/** Für Routen: die Antwort, wenn das Modul aus ist. */
|
||||
export function modulGesperrt(): Response {
|
||||
return new Response('Nicht gefunden.', { status: 404 });
|
||||
}
|
||||
@@ -100,6 +100,12 @@ export async function warteschlangeAbschicken(benutzerId: string): Promise<numbe
|
||||
credentials: 'same-origin',
|
||||
});
|
||||
|
||||
// Das Modul Merkkarten ist abgeschaltet: Die Bewertungen werden nie mehr
|
||||
// angenommen, also nicht ewig weiter mitschleppen.
|
||||
if (antwort.status === 410) {
|
||||
warteschlangeLeeren(benutzerId);
|
||||
return 0;
|
||||
}
|
||||
if (!antwort.ok) return null;
|
||||
|
||||
const ergebnis: unknown = await antwort.json();
|
||||
|
||||
@@ -7,6 +7,8 @@ import { anzahlAbgenommen } from '../repos/aufgaben';
|
||||
import { anzahlAbgeschlossen } from '../repos/checklisten';
|
||||
import { anzahlAbgegeben } from '../repos/kontrollen';
|
||||
import { heute } from '../zeit';
|
||||
import { modulStand } from '../repos/module';
|
||||
import type { ModulStand } from '../module/module';
|
||||
import { aktuelleWoche } from './wochen';
|
||||
import { wochenFortschritt, type Fortschritt } from './fortschritt';
|
||||
import type { Wochenplanung } from '../db/schema';
|
||||
@@ -34,6 +36,8 @@ export type Wochensicht = {
|
||||
aufgaben: Aufgabe[];
|
||||
checklisten: Checkliste[];
|
||||
kontrolle: Kontrolle | null;
|
||||
/** Welche Module an sind – die Ansicht blendet die übrigen aus. */
|
||||
module: ModulStand;
|
||||
vorherige: number | null;
|
||||
naechste: number | null;
|
||||
};
|
||||
@@ -66,14 +70,22 @@ export function wochensicht(ausbildungId: string, azubiId: string, wochenNummer:
|
||||
};
|
||||
});
|
||||
|
||||
const aufgaben = woche.aufgaben.map((id) => alle.aufgabeNach.get(id)).filter((a) => a !== undefined);
|
||||
const checklisten = woche.checklisten.map((id) => alle.checklisteNach.get(id)).filter((c) => c !== undefined);
|
||||
const kontrolle = woche.lernzielkontrolle ? (alle.kontrolleNach.get(woche.lernzielkontrolle) ?? null) : null;
|
||||
// Ein abgeschaltetes Modul trägt nichts zur Woche bei – weder in der Ansicht
|
||||
// noch im Fortschritt. So erreicht eine Woche auch ohne Merkkarten 100 %.
|
||||
const eingeschaltet = modulStand();
|
||||
const aufgaben = eingeschaltet.aufgaben
|
||||
? woche.aufgaben.map((id) => alle.aufgabeNach.get(id)).filter((a) => a !== undefined)
|
||||
: [];
|
||||
const checklisten = eingeschaltet.checklisten
|
||||
? woche.checklisten.map((id) => alle.checklisteNach.get(id)).filter((c) => c !== undefined)
|
||||
: [];
|
||||
const kontrolle =
|
||||
eingeschaltet.kontrollen && woche.lernzielkontrolle ? (alle.kontrolleNach.get(woche.lernzielkontrolle) ?? null) : null;
|
||||
|
||||
// Die Merkkarten dieser Woche sind die Karten ihrer Handouts.
|
||||
const karteIds = handouts.flatMap(({ handout }) =>
|
||||
alle.karten.filter((k) => k.handoutId === handout.id).map((k) => k.id),
|
||||
);
|
||||
const karteIds = eingeschaltet.karten
|
||||
? handouts.flatMap(({ handout }) => alle.karten.filter((k) => k.handoutId === handout.id).map((k) => k.id))
|
||||
: [];
|
||||
const kartenStand = kartenUebersicht(azubiId, karteIds, heuteDatum);
|
||||
|
||||
const fortschritt = wochenFortschritt({
|
||||
@@ -102,6 +114,7 @@ export function wochensicht(ausbildungId: string, azubiId: string, wochenNummer:
|
||||
aufgaben,
|
||||
checklisten,
|
||||
kontrolle,
|
||||
module: eingeschaltet,
|
||||
vorherige: stelle > 0 ? (nummern[stelle - 1] ?? null) : null,
|
||||
naechste: stelle < nummern.length - 1 ? (nummern[stelle + 1] ?? null) : null,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { datenbank, schema } from '../db';
|
||||
import { jetzt } from '../zeit';
|
||||
import { standAus, type ModulId, type ModulStand } from '../module/module';
|
||||
|
||||
/** Sämtlicher Zugriff auf die Tabelle `modul_einstellung` läuft über dieses Modul. */
|
||||
|
||||
/** Der aktuelle Stand aller Module. Ohne gespeicherte Zeile ist ein Modul an. */
|
||||
export function modulStand(): ModulStand {
|
||||
const zeilen = datenbank()
|
||||
.select({ modul: schema.modulEinstellung.modul, aktiv: schema.modulEinstellung.aktiv })
|
||||
.from(schema.modulEinstellung)
|
||||
.all();
|
||||
return standAus(zeilen);
|
||||
}
|
||||
|
||||
/** Wer wann zuletzt umgeschaltet hat – für die Anzeige in der Verwaltung. */
|
||||
export function modulAenderungen() {
|
||||
return datenbank().select().from(schema.modulEinstellung).all();
|
||||
}
|
||||
|
||||
export function modulSetzen(modul: ModulId, aktiv: boolean, benutzerId: string): void {
|
||||
const werte = { aktiv: aktiv ? 1 : 0, geaendertVon: benutzerId, geaendertAm: jetzt() };
|
||||
datenbank()
|
||||
.insert(schema.modulEinstellung)
|
||||
.values({ modul, ...werte })
|
||||
.onConflictDoUpdate({ target: schema.modulEinstellung.modul, set: werte })
|
||||
.run();
|
||||
}
|
||||
Reference in New Issue
Block a user