Termin aus Mail, Rollen & Rechte, Abwesenheiten, Glas-Darstellung und Pi-Betrieb
- Termin aus Mail: Mailtext oder .eml per Drag & Drop/Strg+V auf Terminplan oder Aufgaben, KI schlägt Aufgaben vor, Prüfung im Aufgaben-Panel; neues Notizfeld je Aufgabe (plan_todos.notiz) - Terminplan: Meilensteine ohne Aufgabenbezug je in eigener Zeile, "+ Neu"-Menü klappt neben schmaler Seitenleiste nach rechts, Leistungsphasen - Rollen & Rechte, Abwesenheiten-Seite, Benachrichtigungen, Rundgang - Glas-Darstellung, Logo als Favicon - Pi-Betrieb: PDF-Arbeit im Kindprozess, Wächter, Einrichtungs- und Prüfskript inkl. WLAN-Wächter Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f4fb3c550b
commit
b7e0bd0d92
56 files changed
+12265
-2974
No files matched your search
+168
-90
@@ -42,7 +42,7 @@
|
||||
// Dass der Bereich für Nicht-Administratoren gar nicht erst auftaucht, ist
|
||||
// wie überall in dieser App ein reines UI-Gate; die eigentliche Sperre sitzt
|
||||
// serverseitig in requireRole('admin') auf PUT /app-settings,
|
||||
// PUT /honorar-config, POST/DELETE /lph-templates, POST/DELETE /holidays,
|
||||
// PUT /honorar-config, POST/PATCH/DELETE /lph-templates, POST/DELETE /holidays,
|
||||
// GET /backup, POST /import und POST /delete-week.
|
||||
|
||||
// ADMIN_SECTIONS: die Abschnitte in der Reihenfolge der Seitenleiste. Die
|
||||
@@ -52,19 +52,37 @@
|
||||
// Unterpunkt in der Seitenleiste; ein neuer Abschnitt braucht damit nur einen
|
||||
// Eintrag hier plus die Karte. `wide` hebt die Breitenbegrenzung des Rasters
|
||||
// auf — die Benutzertabelle hat zehn Spalten und scrollte sonst quer.
|
||||
// recht: welches Recht (Rollen & Rechte) den Abschnitt öffnet; bei einer
|
||||
// Liste genügt eines davon.
|
||||
const ADMIN_SECTIONS = [
|
||||
{ key: 'benutzer', title: 'Benutzerverwaltung', wide: true },
|
||||
{ key: 'honorar', title: 'Honorar' },
|
||||
{ key: 'auslastung', title: 'Auslastung' },
|
||||
{ key: 'termine', title: 'Termine & Sitzungen' },
|
||||
{ key: 'feiertage', title: 'Feiertage' },
|
||||
{ key: 'vorlagen', title: 'Aufgaben-Vorlagen' },
|
||||
{ key: 'planchecker', title: 'Plan-Checker-Merkmale' },
|
||||
{ key: 'betrieb', title: 'Betrieb & Sicherheit' },
|
||||
{ key: 'papierkorb', title: 'Papierkorb' },
|
||||
{ key: 'datenbank', title: 'Datenbank & Sitzungen' },
|
||||
{ key: 'benutzer', title: 'Benutzerverwaltung', wide: true, recht: 'a.benutzer' },
|
||||
{ key: 'rollen', title: 'Rollen & Rechte', wide: true, recht: 'a.rollen' },
|
||||
{ key: 'honorar', title: 'Honorar', recht: 'a.honorar' },
|
||||
{ key: 'auslastung', title: 'Auslastung', recht: 'a.auslastung' },
|
||||
{ key: 'termine', title: 'Termine & Sitzungen', recht: 'a.termine' },
|
||||
{ key: 'feiertage', title: 'Feiertage', recht: 'a.feiertage' },
|
||||
{ key: 'vorlagen', title: 'Aufgaben-Vorlagen', recht: 'a.vorlagen' },
|
||||
{ key: 'planchecker', title: 'Plan-Checker-Merkmale', recht: 'a.checker' },
|
||||
{ key: 'betrieb', title: 'Betrieb & Sicherheit', recht: 'a.betrieb' },
|
||||
{ key: 'papierkorb', title: 'Papierkorb', recht: 'a.papierkorb' },
|
||||
{ key: 'datenbank', title: 'Datenbank & Sitzungen', recht: ['a.backup', 'a.import'] },
|
||||
];
|
||||
|
||||
// adminNavAbgleichen: blendet Unterpunkte der Seitenleiste ohne Recht aus
|
||||
// (auch von renderUserBar() nach der Anmeldung gerufen).
|
||||
function adminNavAbgleichen(){
|
||||
ADMIN_SECTIONS.forEach(sec => {
|
||||
const item = document.querySelector('#adminSubnav [data-admin-section="' + sec.key + '"]');
|
||||
if(item) item.hidden = !adminAbschnittErlaubt(sec);
|
||||
});
|
||||
}
|
||||
|
||||
// adminAbschnittErlaubt: darf das Konto diesen Abschnitt sehen?
|
||||
function adminAbschnittErlaubt(sec){
|
||||
const r = Array.isArray(sec.recht) ? sec.recht : [sec.recht];
|
||||
return r.some(x => hatRecht(x));
|
||||
}
|
||||
|
||||
// adminSection: der gerade sichtbare Abschnitt.
|
||||
let adminSection = 'benutzer';
|
||||
|
||||
@@ -95,13 +113,13 @@ function renderAdminArea(){
|
||||
// isAdminUser: Kurzform für die Sichtbarkeit des Bereichs.
|
||||
// Rückgabe: Boolean.
|
||||
function isAdminUser(){
|
||||
return !!(currentUser && currentUser.role === 'admin');
|
||||
return hatAdminRecht();
|
||||
}
|
||||
|
||||
// adminSectionExists: prüft, ob ein Schlüssel ein bekannter Abschnitt ist.
|
||||
// Parameter: key. Rückgabe: Boolean.
|
||||
function adminSectionExists(key){
|
||||
return ADMIN_SECTIONS.some(x => x.key === key);
|
||||
return ADMIN_SECTIONS.some(x => x.key === key && adminAbschnittErlaubt(x));
|
||||
}
|
||||
|
||||
// showAdminSection: schaltet auf einen Abschnitt um — blendet die zugehörige
|
||||
@@ -111,8 +129,9 @@ function adminSectionExists(key){
|
||||
// (Klick aus einem anderen Bereich heraus), wird zuerst dorthin gewechselt.
|
||||
// Parameter: key. Keine Rückgabe.
|
||||
function showAdminSection(key){
|
||||
if(!adminSectionExists(key)) key = ADMIN_SECTIONS[0].key;
|
||||
if(!adminSectionExists(key)) key = (ADMIN_SECTIONS.find(adminAbschnittErlaubt) || ADMIN_SECTIONS[0]).key;
|
||||
adminSection = key;
|
||||
adminNavAbgleichen();
|
||||
|
||||
if(currentTab !== 'admin'){ showTab('admin'); return; }
|
||||
|
||||
@@ -140,6 +159,7 @@ function showAdminSection(key){
|
||||
// Der Papierkorb ändert sich durch jedes Löschen irgendwo im Büro — ebenso
|
||||
// frisch holen.
|
||||
if(key === 'papierkorb') loadAdminPapierkorb();
|
||||
if(key === 'rollen' && typeof renderRollenRechte === 'function') renderRollenRechte();
|
||||
|
||||
try {
|
||||
history.replaceState(null, '', location.pathname + location.search + '#' + key);
|
||||
@@ -186,8 +206,8 @@ const APP_SETTING_FIELDS = {
|
||||
// Kapazität der Person — ein volles Band ist eine volle Woche. Die feste
|
||||
// Vorgabe 8 hatte zur Folge, dass ein roter Balken (ab 1,0) nur zu einem
|
||||
// Achtel gefüllt war; Balken und Ampel widersprachen einander.
|
||||
setReferenzHonorar: 'referenzHonorar',
|
||||
setVerteilWochen: 'verteilzeitraumWochen',
|
||||
// Referenzhonorar und Verteilzeitraum sind entfallen: Die Auslastung
|
||||
// rechnet nur noch mit Aufgaben-Stunden (js/load/load.js).
|
||||
},
|
||||
termine: {
|
||||
setStundenProTag: 'arbeitsstundenProTag',
|
||||
@@ -213,49 +233,6 @@ function renderAppSettingFields(){
|
||||
});
|
||||
const autoLock = document.getElementById('setAutoLock');
|
||||
if(autoLock) autoLock.checked = !!Number(APP_SETTINGS.autoLockAktiv);
|
||||
renderAufwandMatrix();
|
||||
}
|
||||
|
||||
// renderAufwandMatrix: die firmenweite Gewichtung als Tabelle Leistungsphase ×
|
||||
// Rolle. „W+M" fehlt bewusst — das Aufwandsmodell kennt nur die
|
||||
// Leistungsphasen (AUFWAND_LPS in js/load/load.js). Ein leeres Feld bedeutet
|
||||
// „kein Eintrag"; dafür zählt die Auslastung dann den vollen Faktor 1,0.
|
||||
// Keine Parameter, keine Rückgabe.
|
||||
function renderAufwandMatrix(){
|
||||
const wrap = document.getElementById('setAufwandMatrix');
|
||||
if(!wrap) return;
|
||||
const vorlage = APP_SETTINGS.aufwandVorlage || {};
|
||||
const phasen = LPHS.filter(l => l !== 'W+M');
|
||||
|
||||
wrap.innerHTML = `<table class="settings-matrix">
|
||||
<thead><tr><th>Phase</th>${ROLES.map(r => `<th>${esc(r)}</th>`).join('')}</tr></thead>
|
||||
<tbody>${phasen.map(l => `<tr>
|
||||
<th>${esc(l)}</th>
|
||||
${ROLES.map(r => {
|
||||
const v = vorlage[l] ? vorlage[l][r] : undefined;
|
||||
return `<td><input type="number" min="0" max="1" step="0.1"
|
||||
data-lph="${esc(l)}" data-role="${esc(r)}"
|
||||
value="${v === undefined ? '' : esc(v)}" placeholder="1,0"></td>`;
|
||||
}).join('')}
|
||||
</tr>`).join('')}</tbody>
|
||||
</table>`;
|
||||
}
|
||||
|
||||
// collectAufwandMatrix: liest die Matrix aus den Feldern zurück. Leere Felder
|
||||
// werden weggelassen (siehe renderAufwandMatrix), 0 ist ein gültiger Wert und
|
||||
// bleibt erhalten. Rückgabe: Objekt LPH -> { Rolle: Faktor }.
|
||||
function collectAufwandMatrix(){
|
||||
const wrap = document.getElementById('setAufwandMatrix');
|
||||
const out = {};
|
||||
if(!wrap) return out;
|
||||
wrap.querySelectorAll('input[data-lph]').forEach(inp => {
|
||||
if(inp.value.trim() === '') return;
|
||||
const v = parseFloat(inp.value);
|
||||
if(!Number.isFinite(v)) return;
|
||||
const lph = inp.dataset.lph;
|
||||
(out[lph] = out[lph] || {})[inp.dataset.role] = v;
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
// putAppSettings: schickt einen Teil-Satz an PUT /app-settings und übernimmt
|
||||
@@ -294,11 +271,9 @@ function fieldValues(gruppe){
|
||||
return patch;
|
||||
}
|
||||
|
||||
// saveAuslastungSettings: Schwellen, Skala, Vollzeit-Stunden und die
|
||||
// Aufwandsvorlage in einem Zug. Keine Parameter, keine Rückgabe.
|
||||
// saveAuslastungSettings: Schwellen und Vollzeit-Stunden in einem Zug. Keine Parameter, keine Rückgabe.
|
||||
function saveAuslastungSettings(){
|
||||
const patch = fieldValues('auslastung');
|
||||
patch.aufwandVorlage = collectAufwandMatrix();
|
||||
putAppSettings(patch, 'Auslastungs-Einstellungen gespeichert.');
|
||||
}
|
||||
|
||||
@@ -365,7 +340,7 @@ function renderAdminSitzungen(){
|
||||
aktionen = `<button class="secondary small" onclick="adminRelockWeek('${esc(w.id)}')">Fertig – wieder sperren</button>`;
|
||||
} else if(w.locked){
|
||||
chip = '<span class="sitzung-chip ist-gesperrt">Gesperrt</span>';
|
||||
aktionen = `<button class="secondary small" onclick="adminUnlockWeek('${esc(w.id)}')">Entsperren</button>`;
|
||||
aktionen = hatRecht('s.entsperren') ? `<button class="secondary small" onclick="adminUnlockWeek('${esc(w.id)}')">Entsperren</button>` : '';
|
||||
} else {
|
||||
chip = '<span class="sitzung-chip ist-entsperrt">Entsperrt</span>';
|
||||
aktionen = `<button class="secondary small" onclick="adminOpenWeek('${esc(w.id)}')">Öffnen</button>`;
|
||||
@@ -375,7 +350,7 @@ function renderAdminSitzungen(){
|
||||
<span class="template-admin-title">${esc(w.label)}${datum ? ' · ' + esc(datum) : ''}</span>
|
||||
${chip}
|
||||
${aktionen}
|
||||
<button class="danger small" onclick="adminDeleteWeek('${esc(w.id)}')" title="Sitzung löschen">×</button>
|
||||
${hatRecht('s.loeschen') ? `<button class="danger small" onclick="adminDeleteWeek('${esc(w.id)}')" title="Sitzung löschen">×</button>` : ''}
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
@@ -609,17 +584,31 @@ function deleteHoliday(date){
|
||||
}
|
||||
|
||||
// ---------- Abschnitt Aufgaben-Vorlagen ----------
|
||||
//
|
||||
// Vorlagen gibt es je Gewerk und Leistungsphase (Eriks Wunsch). Oben wählt
|
||||
// man das Gewerk (Umschalter) und die Leistungsphase, darunter steht die
|
||||
// Liste der Schritte: Bezeichnung, Start und Dauer direkt bearbeitbar
|
||||
// (gespeichert beim Verlassen des Feldes, PATCH /lph-templates/:id),
|
||||
// Reihenfolge über ↑/↓ (POST …/verschieben), Löschen über ×.
|
||||
// Vorlagen ohne Gewerk (Altbestand) stehen unter jedem Gewerk mit dem
|
||||
// Vermerk „alle Gewerke“.
|
||||
|
||||
let adminTemplates = [];
|
||||
let adminTemplateGewerk = null;
|
||||
|
||||
// loadAdminTemplates: holt alle Vorlagen-Schritte (GET /lph-templates) und
|
||||
// rendert die Liste der gerade gewählten Leistungsphase.
|
||||
// Keine Parameter, keine Rückgabe.
|
||||
// rendert die Liste. Keine Parameter, keine Rückgabe.
|
||||
function loadAdminTemplates(){
|
||||
const select = document.getElementById('adminTemplateLph');
|
||||
if(select && !select.options.length){
|
||||
select.innerHTML = LPHS.map(l => `<option value="${esc(l)}">${esc(l)}</option>`).join('');
|
||||
}
|
||||
if(!adminTemplateGewerk){
|
||||
const eigen = currentUser && Object.keys(DEPARTMENTS).includes(currentUser.department) ? currentUser.department : '';
|
||||
adminTemplateGewerk = eigen || Object.keys(DEPARTMENTS)[0];
|
||||
}
|
||||
const listEl = document.getElementById('templateAdminList');
|
||||
if(listEl) listEl.innerHTML = '<span class="hint">Lade Vorlagen…</span>';
|
||||
if(listEl && !adminTemplates.length) listEl.innerHTML = '<span class="hint">Lade Vorlagen…</span>';
|
||||
|
||||
fetch(baseUrl() + '/lph-templates')
|
||||
.then(r => { if(!r.ok) throw new Error('Server HTTP ' + r.status); return r.json(); })
|
||||
@@ -633,33 +622,74 @@ function loadAdminTemplates(){
|
||||
});
|
||||
}
|
||||
|
||||
// renderAdminTemplates: zeigt die Schritte der gewählten Leistungsphase in
|
||||
// ihrer gepflegten Reihenfolge, je Zeile mit Versatz/Dauer und Lösch-Knopf.
|
||||
// Wird auch vom onchange der Leistungsphasen-Auswahl gerufen.
|
||||
// Keine Parameter, keine Rückgabe.
|
||||
function renderAdminTemplates(){
|
||||
const listEl = document.getElementById('templateAdminList');
|
||||
if(!listEl) return;
|
||||
const select = document.getElementById('adminTemplateLph');
|
||||
const lph = select ? select.value : '';
|
||||
const entries = adminTemplates.filter(t => t.lph === lph);
|
||||
|
||||
listEl.innerHTML = entries.length === 0
|
||||
? '<span class="hint">Für diese Leistungsphase ist noch kein Schritt hinterlegt.</span>'
|
||||
: entries.map(t => `<div class="template-admin-row">
|
||||
<span class="template-admin-title">${esc(t.title)}</span>
|
||||
<span class="hint" style="margin:0">Start +${esc(t.offsetDays)} Tage · Dauer ${esc(t.durationDays)} Tage</span>
|
||||
<button class="danger small" onclick="deleteTemplateEntry('${esc(t.id)}')" title="Schritt aus der Vorlage löschen">×</button>
|
||||
</div>`).join('');
|
||||
// vorlagenAnteilText: Anzeige der Spalte „Anteil %“ — Zahl mit Komma oder
|
||||
// „in a)“ / „in a) + b)“ (Siemon: in anderen Schritten enthalten).
|
||||
function vorlagenAnteilText(t){
|
||||
if(t.enthaltenIn) return 'in ' + t.enthaltenIn.split(',').map(x => x + ')').join(' + ');
|
||||
return t.anteil ? String(t.anteil).replace('.', ',') : '';
|
||||
}
|
||||
|
||||
// addTemplateEntry: legt einen Schritt für die gewählte Leistungsphase an
|
||||
// (POST /lph-templates, Admin-only). Keine Parameter, keine Rückgabe.
|
||||
// waehleAdminTemplateGewerk: Umschalter Gewerk. Parameter: g. Keine Rückgabe.
|
||||
function waehleAdminTemplateGewerk(g){
|
||||
adminTemplateGewerk = g;
|
||||
renderAdminTemplates();
|
||||
}
|
||||
|
||||
// renderAdminTemplates: Gewerk-Umschalter (mit Anzahl je Gewerk), die
|
||||
// Anzahl je Leistungsphase in der Auswahl und die Schritte der gewählten
|
||||
// Kombination. Keine Parameter, keine Rückgabe.
|
||||
function renderAdminTemplates(){
|
||||
const g = adminTemplateGewerk;
|
||||
const gewEl = document.getElementById('adminTemplateGewerk');
|
||||
if(gewEl){
|
||||
gewEl.innerHTML = Object.keys(DEPARTMENTS).map(d => {
|
||||
const n = adminTemplates.filter(t => t.gewerk === d).length;
|
||||
return `<button type="button" class="vorlagen-gewerk${d === g ? ' is-active' : ''}" aria-pressed="${d === g}"
|
||||
title="${esc(DEPARTMENTS[d].label)}" onclick="waehleAdminTemplateGewerk('${esc(d)}')">${esc(d)}<span>${n}</span></button>`;
|
||||
}).join('');
|
||||
}
|
||||
const select = document.getElementById('adminTemplateLph');
|
||||
const lph = select ? select.value : '';
|
||||
if(select){
|
||||
Array.from(select.options).forEach(o => {
|
||||
const n = adminTemplates.filter(t => t.lph === o.value && t.gewerk === g).length;
|
||||
o.textContent = o.value + (n ? ' (' + n + ')' : '');
|
||||
});
|
||||
}
|
||||
const listEl = document.getElementById('templateAdminList');
|
||||
if(!listEl) return;
|
||||
const eigene = adminTemplates.filter(t => t.lph === lph && t.gewerk === g);
|
||||
const alle = adminTemplates.filter(t => t.lph === lph && !t.gewerk);
|
||||
|
||||
const zeile = (t, i, n) => `<div class="template-admin-row vorlagen-zeile">
|
||||
<input type="text" value="${esc(t.title)}" aria-label="Bezeichnung" onchange="aendereTemplateEntry('${esc(t.id)}', 'title', this.value)">
|
||||
<input type="text" value="${esc(vorlagenAnteilText(t))}" placeholder="–" aria-label="Anteil in Prozent oder „in a)“" title="Zahl in Prozent oder „in a)“ = zeitgleich mit Schritt a)" onchange="aendereTemplateEntry('${esc(t.id)}', 'anteil', this.value)">
|
||||
<input type="number" min="0" value="${t.durationDays ? esc(t.offsetDays) : ''}" placeholder="–" aria-label="Start in Tagen" onchange="aendereTemplateEntry('${esc(t.id)}', 'offsetDays', this.value === '' ? 0 : this.value)">
|
||||
<input type="number" min="0" value="${t.durationDays ? esc(t.durationDays) : ''}" placeholder="ohne" aria-label="Dauer in Tagen, leer = ohne Termin" onchange="aendereTemplateEntry('${esc(t.id)}', 'durationDays', this.value === '' ? 0 : this.value)">
|
||||
<span class="vorlagen-aktionen">
|
||||
${n !== null ? `<button class="secondary small" title="nach oben" ${i === 0 ? 'disabled' : ''} onclick="verschiebeTemplateEntry('${esc(t.id)}', -1)">↑</button>
|
||||
<button class="secondary small" title="nach unten" ${i === n - 1 ? 'disabled' : ''} onclick="verschiebeTemplateEntry('${esc(t.id)}', 1)">↓</button>` : '<span class="hint" style="margin:0">alle Gewerke</span>'}
|
||||
<button class="danger small" onclick="deleteTemplateEntry('${esc(t.id)}')" title="Schritt aus der Vorlage löschen">×</button>
|
||||
</span>
|
||||
</div>`;
|
||||
|
||||
// Summe der Anteile: Beim Einfügen zählt das Verhältnis (der Anteil eines
|
||||
// Schritts an der Summe der Phase) — die Summe ist nur zur Kontrolle da.
|
||||
const summe = Math.round([...eigene, ...alle].reduce((a, t) => a + (Number(t.anteil) || 0), 0) * 100) / 100;
|
||||
listEl.innerHTML = (eigene.length || alle.length)
|
||||
? eigene.map((t, i) => zeile(t, i, eigene.length)).join('') + alle.map(t => zeile(t, 0, null)).join('')
|
||||
+ `<div class="vorlagen-summe">Summe der Anteile in ${esc(lph)}: <b>${summe ? String(summe).replace('.', ',') + ' %' : 'keine hinterlegt'}</b>${summe ? '' : ' — Schritte ohne Anteil werden mit „Start/Dauer“ bzw. ohne Termin eingefügt.'}</div>`
|
||||
: `<span class="hint">Für ${esc(g)} in ${esc(lph)} ist noch kein Schritt hinterlegt.</span>`;
|
||||
}
|
||||
|
||||
// addTemplateEntry: legt einen Schritt für das gewählte Gewerk und die
|
||||
// gewählte Leistungsphase an (POST /lph-templates). Keine Parameter.
|
||||
function addTemplateEntry(){
|
||||
const select = document.getElementById('adminTemplateLph');
|
||||
const titleEl = document.getElementById('templateNewTitle');
|
||||
const offsetEl = document.getElementById('templateNewOffset');
|
||||
const durationEl = document.getElementById('templateNewDuration');
|
||||
const anteilEl = document.getElementById('templateNewAnteil');
|
||||
const title = titleEl ? titleEl.value.trim() : '';
|
||||
if(!title){ alert('Bitte eine Bezeichnung für den Schritt eingeben.'); return; }
|
||||
|
||||
@@ -668,15 +698,19 @@ function addTemplateEntry(){
|
||||
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
||||
body: JSON.stringify({
|
||||
lph: select ? select.value : '',
|
||||
gewerk: adminTemplateGewerk,
|
||||
title,
|
||||
offsetDays: offsetEl ? parseInt(offsetEl.value, 10) || 0 : 0,
|
||||
durationDays: durationEl ? parseInt(durationEl.value, 10) || 1 : 1,
|
||||
// leer oder 0 = ohne Termin
|
||||
durationDays: durationEl ? parseInt(durationEl.value, 10) || 0 : 0,
|
||||
anteil: anteilEl ? anteilEl.value : '',
|
||||
})
|
||||
})
|
||||
.then(r => r.json().then(json => ({ ok: r.ok, json })))
|
||||
.then(({ ok, json }) => {
|
||||
if(!ok || !json.ok) throw new Error((json && json.error) || 'Anlegen fehlgeschlagen.');
|
||||
if(titleEl) titleEl.value = '';
|
||||
if(titleEl){ titleEl.value = ''; titleEl.focus(); }
|
||||
if(anteilEl) anteilEl.value = '';
|
||||
loadAdminTemplates();
|
||||
showAdminNote('Schritt angelegt.');
|
||||
})
|
||||
@@ -686,6 +720,50 @@ function addTemplateEntry(){
|
||||
});
|
||||
}
|
||||
|
||||
// aendereTemplateEntry: speichert ein geändertes Feld eines Schritts
|
||||
// (PATCH /lph-templates/:id). Parameter: id, feld, wert. Keine Rückgabe.
|
||||
function aendereTemplateEntry(id, feld, wert){
|
||||
const body = {};
|
||||
body[feld] = (feld === 'title' || feld === 'anteil') ? wert : parseInt(wert, 10);
|
||||
fetch(baseUrl() + '/lph-templates/' + encodeURIComponent(id), {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
.then(r => r.json().then(json => ({ ok: r.ok, json })))
|
||||
.then(({ ok, json }) => {
|
||||
if(!ok || !json.ok) throw new Error((json && json.error) || 'Speichern fehlgeschlagen.');
|
||||
const t = adminTemplates.find(x => x.id === id);
|
||||
if(t) Object.assign(t, json.template);
|
||||
renderAdminTemplates();
|
||||
showAdminNote('Gespeichert.');
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Vorlagen-Schritt ändern fehlgeschlagen:', err);
|
||||
alert('Änderung konnte nicht gespeichert werden: ' + (err.message || err));
|
||||
loadAdminTemplates();
|
||||
});
|
||||
}
|
||||
|
||||
// verschiebeTemplateEntry: Schritt eine Stelle nach oben/unten.
|
||||
// Parameter: id, richtung (-1/1). Keine Rückgabe.
|
||||
function verschiebeTemplateEntry(id, richtung){
|
||||
fetch(baseUrl() + '/lph-templates/' + encodeURIComponent(id) + '/verschieben', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
||||
body: JSON.stringify({ richtung })
|
||||
})
|
||||
.then(r => r.json().then(json => ({ ok: r.ok, json })))
|
||||
.then(({ ok, json }) => {
|
||||
if(!ok || !json.ok) throw new Error((json && json.error) || 'Verschieben fehlgeschlagen.');
|
||||
loadAdminTemplates();
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Vorlagen-Schritt verschieben fehlgeschlagen:', err);
|
||||
alert('Schritt konnte nicht verschoben werden: ' + (err.message || err));
|
||||
});
|
||||
}
|
||||
|
||||
// deleteTemplateEntry: löscht einen Vorlagen-Schritt (DELETE, Admin-only).
|
||||
// Bereits in Projekte eingefügte Aufgaben bleiben unberührt — sie sind nach
|
||||
// dem Einfügen ganz normale plan_todos ohne Verbindung zur Vorlage.
|
||||
@@ -706,7 +784,7 @@ function deleteTemplateEntry(id){
|
||||
|
||||
// ---------- Abschnitt Plan-Checker-Merkmale ----------
|
||||
//
|
||||
// Dieselbe add/delete-only Liste wie die Aufgaben-Vorlagen oben, nur ohne
|
||||
// Eine add/delete-only Liste wie früher die Aufgaben-Vorlagen oben, ohne
|
||||
// Gruppierung (die Merkmale gelten unterschiedslos für jede Prüfung) — siehe
|
||||
// plan_checker_merkmale/GET+POST+DELETE /plan-checker-merkmale, server.js.
|
||||
// Die Liste selbst nutzt auch der Plan Checker (js/planchecker/planchecker.js),
|
||||
|
||||
Reference in new issue
Block a user