A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
518 lines
16 KiB
JavaScript
518 lines
16 KiB
JavaScript
/**
|
|
* BreakPilot Studio - Learning Units Module
|
|
*
|
|
* Lerneinheiten-Verwaltung:
|
|
* - Laden, Erstellen, Löschen von Lerneinheiten
|
|
* - Zuordnung von Arbeitsblättern zu Lerneinheiten
|
|
* - Filter für Lerneinheiten-spezifische Ansicht
|
|
*
|
|
* Refactored: 2026-01-19
|
|
*/
|
|
|
|
import { t } from './i18n.js';
|
|
import { setStatus } from './api-helpers.js';
|
|
|
|
// State
|
|
let units = [];
|
|
let currentUnitId = null;
|
|
let showOnlyUnitFiles = false;
|
|
|
|
// DOM References
|
|
let unitListEl = null;
|
|
let unitHeading1 = null;
|
|
let unitHeading2 = null;
|
|
let btnAddUnit = null;
|
|
let btnToggleFilter = null;
|
|
let btnAttachCurrentToLu = null;
|
|
|
|
// Form Inputs
|
|
let unitStudentInput = null;
|
|
let unitSubjectInput = null;
|
|
let unitGradeInput = null;
|
|
let unitTitleInput = null;
|
|
|
|
// Callbacks für File-Manager Integration
|
|
let getEingangFilesCallback = null;
|
|
let getAllEingangFilesCallback = null;
|
|
let getAllWorksheetPairsCallback = null;
|
|
let setFilteredDataCallback = null;
|
|
let renderListCallback = null;
|
|
let renderPreviewCallback = null;
|
|
let getCurrentWorksheetBasenameCallback = null;
|
|
|
|
/**
|
|
* Initialisiert das Learning Units Modul
|
|
* @param {Object} options - Konfiguration
|
|
*/
|
|
export function initLearningUnitsModule(options = {}) {
|
|
// DOM-Referenzen
|
|
unitListEl = document.getElementById('unit-list') || options.unitListEl;
|
|
unitHeading1 = document.getElementById('unit-heading-1') || options.unitHeading1;
|
|
unitHeading2 = document.getElementById('unit-heading-2') || options.unitHeading2;
|
|
btnAddUnit = document.getElementById('btn-add-unit') || options.btnAddUnit;
|
|
btnToggleFilter = document.getElementById('btn-toggle-filter') || options.btnToggleFilter;
|
|
btnAttachCurrentToLu = document.getElementById('btn-attach-current-to-lu') || options.btnAttachCurrentToLu;
|
|
|
|
// Form Inputs
|
|
unitStudentInput = document.getElementById('unit-student') || options.unitStudentInput;
|
|
unitSubjectInput = document.getElementById('unit-subject') || options.unitSubjectInput;
|
|
unitGradeInput = document.getElementById('unit-grade') || options.unitGradeInput;
|
|
unitTitleInput = document.getElementById('unit-title') || options.unitTitleInput;
|
|
|
|
// Callbacks
|
|
getEingangFilesCallback = options.getEingangFiles || (() => []);
|
|
getAllEingangFilesCallback = options.getAllEingangFiles || (() => []);
|
|
getAllWorksheetPairsCallback = options.getAllWorksheetPairs || (() => ({}));
|
|
setFilteredDataCallback = options.setFilteredData || (() => {});
|
|
renderListCallback = options.renderList || (() => {});
|
|
renderPreviewCallback = options.renderPreview || (() => {});
|
|
getCurrentWorksheetBasenameCallback = options.getCurrentWorksheetBasename || (() => null);
|
|
|
|
// Event-Listener
|
|
if (btnAddUnit) {
|
|
btnAddUnit.addEventListener('click', (ev) => {
|
|
ev.preventDefault();
|
|
addUnitFromForm();
|
|
});
|
|
}
|
|
|
|
if (btnAttachCurrentToLu) {
|
|
btnAttachCurrentToLu.addEventListener('click', (ev) => {
|
|
ev.preventDefault();
|
|
attachCurrentWorksheetToUnit();
|
|
});
|
|
}
|
|
|
|
if (btnToggleFilter) {
|
|
btnToggleFilter.addEventListener('click', () => {
|
|
showOnlyUnitFiles = !showOnlyUnitFiles;
|
|
if (showOnlyUnitFiles) {
|
|
btnToggleFilter.textContent = t('only_unit') || 'Nur Lerneinheit';
|
|
btnToggleFilter.classList.add('btn-primary');
|
|
} else {
|
|
btnToggleFilter.textContent = t('all_files') || 'Alle Dateien';
|
|
btnToggleFilter.classList.remove('btn-primary');
|
|
}
|
|
applyUnitFilter();
|
|
});
|
|
}
|
|
|
|
// Initial laden
|
|
loadLearningUnits();
|
|
}
|
|
|
|
/**
|
|
* Aktualisiert die Überschrift mit dem Namen der aktuellen Lerneinheit
|
|
* @param {Object|null} unit - Lerneinheit oder null
|
|
*/
|
|
function updateUnitHeading(unit = null) {
|
|
if (!unit && currentUnitId && units && units.length) {
|
|
unit = units.find((u) => u.id === currentUnitId) || null;
|
|
}
|
|
|
|
let text = t('no_unit_selected') || 'Keine Lerneinheit ausgewählt';
|
|
if (unit) {
|
|
const name = unit.label || unit.title || t('learning_unit') || 'Lerneinheit';
|
|
text = (t('learning_unit') || 'Lerneinheit') + ': ' + name;
|
|
}
|
|
|
|
if (unitHeading1) unitHeading1.textContent = text;
|
|
if (unitHeading2) unitHeading2.textContent = text;
|
|
}
|
|
|
|
/**
|
|
* Wendet den Lerneinheiten-Filter an
|
|
* Zeigt nur Dateien der aktuellen Lerneinheit oder alle Dateien
|
|
*/
|
|
export function applyUnitFilter() {
|
|
let unit = null;
|
|
if (currentUnitId && units && units.length) {
|
|
unit = units.find((u) => u.id === currentUnitId) || null;
|
|
}
|
|
|
|
const allEingangFiles = getAllEingangFilesCallback();
|
|
const allWorksheetPairs = getAllWorksheetPairsCallback();
|
|
|
|
// Wenn Filter deaktiviert ODER keine Lerneinheit ausgewählt -> alle Dateien anzeigen
|
|
if (!showOnlyUnitFiles || !unit || !Array.isArray(unit.worksheet_files) || unit.worksheet_files.length === 0) {
|
|
setFilteredDataCallback(allEingangFiles.slice(), { ...allWorksheetPairs }, 0);
|
|
renderListCallback();
|
|
renderPreviewCallback();
|
|
updateUnitHeading(unit);
|
|
return;
|
|
}
|
|
|
|
// Filter aktiv: nur Dateien der aktuellen Lerneinheit anzeigen
|
|
const allowed = new Set(unit.worksheet_files || []);
|
|
const filteredFiles = allEingangFiles.filter((f) => allowed.has(f));
|
|
|
|
const filteredPairs = {};
|
|
Object.keys(allWorksheetPairs).forEach((key) => {
|
|
if (allowed.has(key)) {
|
|
filteredPairs[key] = allWorksheetPairs[key];
|
|
}
|
|
});
|
|
|
|
setFilteredDataCallback(filteredFiles, filteredPairs, 0);
|
|
renderListCallback();
|
|
renderPreviewCallback();
|
|
updateUnitHeading(unit);
|
|
}
|
|
|
|
/**
|
|
* Lädt alle Lerneinheiten vom Server
|
|
*/
|
|
export async function loadLearningUnits() {
|
|
try {
|
|
const resp = await fetch('/api/learning-units/');
|
|
if (!resp.ok) {
|
|
console.error('Fehler beim Laden der Lerneinheiten', resp.status);
|
|
return;
|
|
}
|
|
units = await resp.json();
|
|
if (units.length && !currentUnitId) {
|
|
currentUnitId = units[0].id;
|
|
}
|
|
renderUnits();
|
|
applyUnitFilter();
|
|
} catch (e) {
|
|
console.error('Netzwerkfehler beim Laden der Lerneinheiten', e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Rendert die Lerneinheiten-Liste
|
|
*/
|
|
export function renderUnits() {
|
|
if (!unitListEl) return;
|
|
|
|
unitListEl.innerHTML = '';
|
|
|
|
if (!units.length) {
|
|
const li = document.createElement('li');
|
|
li.className = 'unit-item';
|
|
li.textContent = t('no_units_yet') || 'Noch keine Lerneinheiten angelegt.';
|
|
unitListEl.appendChild(li);
|
|
updateUnitHeading(null);
|
|
return;
|
|
}
|
|
|
|
units.forEach((u) => {
|
|
const li = document.createElement('li');
|
|
li.className = 'unit-item';
|
|
if (u.id === currentUnitId) {
|
|
li.classList.add('active');
|
|
}
|
|
|
|
const contentDiv = document.createElement('div');
|
|
contentDiv.style.flex = '1';
|
|
contentDiv.style.minWidth = '0';
|
|
|
|
const titleEl = document.createElement('div');
|
|
titleEl.textContent = u.label || u.title || t('learning_unit') || 'Lerneinheit';
|
|
|
|
const metaEl = document.createElement('div');
|
|
metaEl.className = 'unit-item-meta';
|
|
|
|
const metaParts = [];
|
|
if (u.meta) {
|
|
metaParts.push(u.meta);
|
|
}
|
|
if (Array.isArray(u.worksheet_files)) {
|
|
metaParts.push((t('worksheets') || 'Blätter') + ': ' + u.worksheet_files.length);
|
|
}
|
|
|
|
metaEl.textContent = metaParts.join(' · ');
|
|
|
|
contentDiv.appendChild(titleEl);
|
|
contentDiv.appendChild(metaEl);
|
|
|
|
// Delete-Button
|
|
const deleteBtn = document.createElement('span');
|
|
deleteBtn.textContent = '🗑️';
|
|
deleteBtn.style.cursor = 'pointer';
|
|
deleteBtn.style.fontSize = '12px';
|
|
deleteBtn.style.color = '#ef4444';
|
|
deleteBtn.title = t('delete_unit') || 'Lerneinheit löschen';
|
|
deleteBtn.addEventListener('click', async (ev) => {
|
|
ev.stopPropagation();
|
|
const confirmMsg = t('confirm_delete_unit') || 'Lerneinheit "{name}" wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.';
|
|
const ok = confirm(confirmMsg.replace('{name}', u.label || u.title || ''));
|
|
if (!ok) return;
|
|
await deleteLearningUnit(u.id);
|
|
});
|
|
|
|
li.appendChild(contentDiv);
|
|
li.appendChild(deleteBtn);
|
|
|
|
li.addEventListener('click', () => {
|
|
currentUnitId = u.id;
|
|
renderUnits();
|
|
applyUnitFilter();
|
|
|
|
// Event für andere Module
|
|
window.dispatchEvent(new CustomEvent('unitSelected', { detail: { unitId: u.id, unit: u } }));
|
|
});
|
|
|
|
unitListEl.appendChild(li);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Erstellt eine neue Lerneinheit aus dem Formular
|
|
*/
|
|
export async function addUnitFromForm() {
|
|
const student = (unitStudentInput && unitStudentInput.value || '').trim();
|
|
const subject = (unitSubjectInput && unitSubjectInput.value || '').trim();
|
|
const grade = (unitGradeInput && unitGradeInput.value || '').trim();
|
|
const title = (unitTitleInput && unitTitleInput.value || '').trim();
|
|
|
|
if (!student && !subject && !title) {
|
|
alert(t('unit_form_empty') || 'Bitte mindestens einen Wert (Schüler/in, Fach oder Thema) eintragen.');
|
|
return;
|
|
}
|
|
|
|
const payload = {
|
|
student,
|
|
subject,
|
|
title,
|
|
grade,
|
|
};
|
|
|
|
try {
|
|
const resp = await fetch('/api/learning-units/', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload),
|
|
});
|
|
if (!resp.ok) {
|
|
console.error('Fehler beim Anlegen der Lerneinheit', resp.status);
|
|
alert(t('unit_create_error') || 'Lerneinheit konnte nicht angelegt werden.');
|
|
return;
|
|
}
|
|
|
|
const created = await resp.json();
|
|
units.push(created);
|
|
currentUnitId = created.id;
|
|
|
|
// Formular leeren
|
|
if (unitStudentInput) unitStudentInput.value = '';
|
|
if (unitSubjectInput) unitSubjectInput.value = '';
|
|
if (unitTitleInput) unitTitleInput.value = '';
|
|
if (unitGradeInput) unitGradeInput.value = '';
|
|
|
|
renderUnits();
|
|
applyUnitFilter();
|
|
|
|
// Event für andere Module
|
|
window.dispatchEvent(new CustomEvent('unitCreated', { detail: { unit: created } }));
|
|
} catch (e) {
|
|
console.error('Netzwerkfehler beim Anlegen der Lerneinheit', e);
|
|
alert(t('network_error') || 'Netzwerkfehler beim Anlegen der Lerneinheit.');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Ordnet das aktuelle Arbeitsblatt der aktuellen Lerneinheit zu
|
|
*/
|
|
export async function attachCurrentWorksheetToUnit() {
|
|
if (!currentUnitId) {
|
|
alert(t('select_unit_first') || 'Bitte zuerst eine Lerneinheit auswählen oder anlegen.');
|
|
return;
|
|
}
|
|
|
|
const basename = getCurrentWorksheetBasenameCallback();
|
|
if (!basename) {
|
|
alert(t('select_worksheet_first') || 'Bitte zuerst ein Arbeitsblatt im linken Bereich auswählen.');
|
|
return;
|
|
}
|
|
|
|
const payload = { worksheet_files: [basename] };
|
|
|
|
try {
|
|
const resp = await fetch(`/api/learning-units/${currentUnitId}/attach-worksheets`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload),
|
|
});
|
|
if (!resp.ok) {
|
|
console.error('Fehler beim Zuordnen des Arbeitsblatts', resp.status);
|
|
alert(t('attach_error') || 'Arbeitsblatt konnte nicht zugeordnet werden.');
|
|
return;
|
|
}
|
|
|
|
const updated = await resp.json();
|
|
const idx = units.findIndex((u) => u.id === updated.id);
|
|
if (idx !== -1) {
|
|
units[idx] = updated;
|
|
}
|
|
renderUnits();
|
|
applyUnitFilter();
|
|
|
|
// Event für andere Module
|
|
window.dispatchEvent(new CustomEvent('worksheetAttached', { detail: { unitId: currentUnitId, filename: basename } }));
|
|
} catch (e) {
|
|
console.error('Netzwerkfehler beim Zuordnen des Arbeitsblatts', e);
|
|
alert(t('network_error') || 'Netzwerkfehler beim Zuordnen des Arbeitsblatts.');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Entfernt ein Arbeitsblatt aus der aktuellen Lerneinheit
|
|
* @param {string} filename - Dateiname des Arbeitsblatts
|
|
*/
|
|
export async function removeWorksheetFromCurrentUnit(filename) {
|
|
if (!currentUnitId) {
|
|
alert(t('select_unit_first') || 'Bitte zuerst eine Lerneinheit auswählen.');
|
|
return;
|
|
}
|
|
if (!filename) {
|
|
alert(t('error_no_filename') || 'Fehler: kein Dateiname übergeben.');
|
|
return;
|
|
}
|
|
|
|
const payload = { worksheet_file: filename };
|
|
|
|
try {
|
|
const resp = await fetch(`/api/learning-units/${currentUnitId}/remove-worksheet`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload),
|
|
});
|
|
if (!resp.ok) {
|
|
console.error('Fehler beim Entfernen des Arbeitsblatts', resp.status);
|
|
alert(t('remove_worksheet_error') || 'Arbeitsblatt konnte nicht aus der Lerneinheit entfernt werden.');
|
|
return;
|
|
}
|
|
|
|
const updated = await resp.json();
|
|
const idx = units.findIndex((u) => u.id === updated.id);
|
|
if (idx !== -1) {
|
|
units[idx] = updated;
|
|
}
|
|
renderUnits();
|
|
applyUnitFilter();
|
|
|
|
// Event für andere Module
|
|
window.dispatchEvent(new CustomEvent('worksheetRemoved', { detail: { unitId: currentUnitId, filename } }));
|
|
} catch (e) {
|
|
console.error('Netzwerkfehler beim Entfernen des Arbeitsblatts', e);
|
|
alert(t('network_error') || 'Netzwerkfehler beim Entfernen des Arbeitsblatts.');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Löscht eine Lerneinheit
|
|
* @param {string} unitId - ID der Lerneinheit
|
|
*/
|
|
export async function deleteLearningUnit(unitId) {
|
|
if (!unitId) {
|
|
alert(t('error_no_unit_id') || 'Fehler: keine Lerneinheit-ID übergeben.');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setStatus(t('deleting_unit') || 'Lösche Lerneinheit …', '', 'busy');
|
|
|
|
const resp = await fetch(`/api/learning-units/${unitId}`, {
|
|
method: 'DELETE',
|
|
});
|
|
|
|
if (!resp.ok) {
|
|
console.error('Fehler beim Löschen der Lerneinheit', resp.status);
|
|
setStatus(t('delete_error') || 'Fehler beim Löschen', '', 'error');
|
|
alert(t('unit_delete_error') || 'Lerneinheit konnte nicht gelöscht werden.');
|
|
return;
|
|
}
|
|
|
|
const result = await resp.json();
|
|
if (result.status === 'deleted') {
|
|
setStatus(t('unit_deleted') || 'Lerneinheit gelöscht', '');
|
|
|
|
// Lerneinheit aus der lokalen Liste entfernen
|
|
units = units.filter((u) => u.id !== unitId);
|
|
|
|
// Wenn die gelöschte Einheit ausgewählt war, Auswahl zurücksetzen
|
|
if (currentUnitId === unitId) {
|
|
currentUnitId = units.length > 0 ? units[0].id : null;
|
|
}
|
|
|
|
renderUnits();
|
|
applyUnitFilter();
|
|
|
|
// Event für andere Module
|
|
window.dispatchEvent(new CustomEvent('unitDeleted', { detail: { unitId } }));
|
|
} else {
|
|
setStatus(t('error') || 'Fehler', t('unknown_error') || 'Unbekannter Fehler', 'error');
|
|
alert(t('unit_delete_error') || 'Fehler beim Löschen der Lerneinheit.');
|
|
}
|
|
} catch (e) {
|
|
console.error('Netzwerkfehler beim Löschen der Lerneinheit', e);
|
|
setStatus(t('network_error') || 'Netzwerkfehler', String(e), 'error');
|
|
alert(t('network_error') || 'Netzwerkfehler beim Löschen der Lerneinheit.');
|
|
}
|
|
}
|
|
|
|
// === Getter und Setter ===
|
|
|
|
/**
|
|
* Gibt alle Lerneinheiten zurück
|
|
* @returns {Array}
|
|
*/
|
|
export function getUnits() {
|
|
return units;
|
|
}
|
|
|
|
/**
|
|
* Gibt die aktuelle Lerneinheit-ID zurück
|
|
* @returns {string|null}
|
|
*/
|
|
export function getCurrentUnitId() {
|
|
return currentUnitId;
|
|
}
|
|
|
|
/**
|
|
* Setzt die aktuelle Lerneinheit-ID
|
|
* @param {string|null} unitId
|
|
*/
|
|
export function setCurrentUnitId(unitId) {
|
|
currentUnitId = unitId;
|
|
renderUnits();
|
|
applyUnitFilter();
|
|
}
|
|
|
|
/**
|
|
* Gibt zurück, ob der Filter aktiv ist
|
|
* @returns {boolean}
|
|
*/
|
|
export function getShowOnlyUnitFiles() {
|
|
return showOnlyUnitFiles;
|
|
}
|
|
|
|
/**
|
|
* Setzt den Filter-Status
|
|
* @param {boolean} value
|
|
*/
|
|
export function setShowOnlyUnitFiles(value) {
|
|
showOnlyUnitFiles = value;
|
|
if (btnToggleFilter) {
|
|
if (showOnlyUnitFiles) {
|
|
btnToggleFilter.textContent = t('only_unit') || 'Nur Lerneinheit';
|
|
btnToggleFilter.classList.add('btn-primary');
|
|
} else {
|
|
btnToggleFilter.textContent = t('all_files') || 'Alle Dateien';
|
|
btnToggleFilter.classList.remove('btn-primary');
|
|
}
|
|
}
|
|
applyUnitFilter();
|
|
}
|
|
|
|
/**
|
|
* Gibt die aktuelle Lerneinheit zurück
|
|
* @returns {Object|null}
|
|
*/
|
|
export function getCurrentUnit() {
|
|
if (!currentUnitId || !units.length) return null;
|
|
return units.find((u) => u.id === currentUnitId) || null;
|
|
}
|