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>
155 lines
6.6 KiB
TypeScript
155 lines
6.6 KiB
TypeScript
import type { Metadata } from 'next';
|
||
import Link from 'next/link';
|
||
import { erfordereBenutzer } from '@/lib/auth/rechte';
|
||
import { ausbildungenVonAusbilder, ausbildungNachAzubi } from '@/lib/repos/ausbildung';
|
||
import { inhalte } from '@/lib/inhalte';
|
||
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 =
|
||
benutzer.rolle === 'ausbilder'
|
||
? ausbildungenVonAusbilder(benutzer.id).map(({ azubi, ausbildung }) => ({ azubi, ausbildung }))
|
||
: (() => {
|
||
const ausbildung = ausbildungNachAzubi(benutzer.id);
|
||
return ausbildung ? [{ azubi: benutzer, ausbildung }] : [];
|
||
})();
|
||
|
||
return (
|
||
<div className="stapel">
|
||
<div>
|
||
<h1>Feedbackgespräche</h1>
|
||
<p className="leise">
|
||
Monatlich, mit Leitfaden und Bewertungsbogen. Von beiden Seiten bestätigt und danach unveränderlich –
|
||
für die Ausbildungsakte.
|
||
</p>
|
||
</div>
|
||
|
||
{beteiligte.length === 0 ? (
|
||
<p className="hinweis hinweis-warnung">
|
||
{benutzer.rolle === 'ausbilder' ? (
|
||
<>
|
||
Noch kein Azubikonto. <Link href="/verwaltung">Jetzt eines anlegen</Link>.
|
||
</>
|
||
) : (
|
||
'Zu deinem Konto ist noch keine Ausbildung hinterlegt.'
|
||
)}
|
||
</p>
|
||
) : (
|
||
beteiligte.map(({ azubi, ausbildung }) => {
|
||
const gespraeche = gespraecheVon(ausbildung.id);
|
||
const letztes = gespraeche[0];
|
||
const faellig =
|
||
!letztes || montagDerWoche(letztes.datum) < montagDerWoche(vorMonaten(heute(), 1));
|
||
|
||
return (
|
||
<section key={ausbildung.id} className="stapel" aria-labelledby={`azubi-${azubi.id}`}>
|
||
<div className="reihe" style={{ justifyContent: 'space-between' }}>
|
||
<h2 id={`azubi-${azubi.id}`} style={{ margin: 0 }}>
|
||
{benutzer.rolle === 'ausbilder' ? azubi.name : 'Deine Gespräche'}
|
||
</h2>
|
||
|
||
{benutzer.rolle === 'ausbilder' ? (
|
||
<form action={gespraechAnlegen}>
|
||
<input type="hidden" name="azubiId" value={azubi.id} />
|
||
<input type="hidden" name="datum" value={heute()} />
|
||
<button type="submit" className={`knopf ${faellig ? 'knopf-akzent' : 'knopf-zweit'}`}>
|
||
Neues Gespräch anlegen
|
||
</button>
|
||
</form>
|
||
) : null}
|
||
</div>
|
||
|
||
{faellig && benutzer.rolle === 'ausbilder' ? (
|
||
<p className="hinweis hinweis-warnung" style={{ marginBottom: 0 }}>
|
||
{letztes
|
||
? `Das letzte Gespräch war am ${alsDatumText(letztes.datum)} – das ist über einen Monat her.`
|
||
: 'Es gab noch kein Feedbackgespräch.'}
|
||
</p>
|
||
) : null}
|
||
|
||
{gespraeche.length === 0 ? (
|
||
<p className="karte" style={{ marginBottom: 0 }}>
|
||
Noch kein Gespräch geführt.
|
||
</p>
|
||
) : (
|
||
<ul className="gespraechsliste">
|
||
{gespraeche.map((gespraech) => {
|
||
const inhalt = inhaltVon(gespraech, vorlage);
|
||
const summe = zusammenfassung(inhalt);
|
||
const fertig = istAbgeschlossen({
|
||
bestaetigtAzubi: gespraech.bestaetigtAzubi === 1,
|
||
bestaetigtAusbilder: gespraech.bestaetigtAusbilder === 1,
|
||
});
|
||
|
||
return (
|
||
<li key={gespraech.id}>
|
||
<Link href={`/feedback/${gespraech.id}`} className="gespraechszeile">
|
||
<span className="gespraech-datum">{alsDatumText(gespraech.datum)}</span>
|
||
|
||
<span className="gespraech-inhalt">
|
||
{summe.bewertet} von {summe.gesamt} Kriterien bewertet
|
||
<span className="leise" style={{ display: 'block', fontWeight: 400 }}>
|
||
{summe.schnittAusbilder !== null
|
||
? `Ausbilder ⌀ ${zahl(summe.schnittAusbilder)}`
|
||
: 'noch keine Einschätzung'}
|
||
{summe.schnittAzubi !== null ? ` · Selbsteinschätzung ⌀ ${zahl(summe.schnittAzubi)}` : ''}
|
||
{summe.abweichungen.length > 0
|
||
? ` · ${summe.abweichungen.length} unterschiedliche Sicht${summe.abweichungen.length === 1 ? '' : 'en'}`
|
||
: ''}
|
||
</span>
|
||
</span>
|
||
|
||
<span className="reihe" style={{ gap: '0.35rem' }}>
|
||
<span
|
||
className="marke-status"
|
||
data-ton={gespraech.bestaetigtAusbilder === 1 ? 'gut' : 'neutral'}
|
||
>
|
||
{gespraech.bestaetigtAusbilder === 1 ? '✓' : '○'} Ausbilder
|
||
</span>
|
||
<span
|
||
className="marke-status"
|
||
data-ton={gespraech.bestaetigtAzubi === 1 ? 'gut' : 'neutral'}
|
||
>
|
||
{gespraech.bestaetigtAzubi === 1 ? '✓' : '○'} Azubi
|
||
</span>
|
||
{fertig ? (
|
||
<span className="marke-status" data-ton="gut">
|
||
🔒 abgeschlossen
|
||
</span>
|
||
) : null}
|
||
</span>
|
||
</Link>
|
||
</li>
|
||
);
|
||
})}
|
||
</ul>
|
||
)}
|
||
</section>
|
||
);
|
||
})
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function vorMonaten(datum: string, monate: number): string {
|
||
const d = new Date(`${datum}T12:00:00Z`);
|
||
d.setUTCMonth(d.getUTCMonth() - monate);
|
||
return d.toISOString().slice(0, 10);
|
||
}
|
||
|
||
function zahl(wert: number): string {
|
||
return wert.toString().replace('.', ',');
|
||
}
|