Files
projektuebersicht/public/js/settings/settings.js
erik 5b20adee48 All new
alle new
2026-10-01 06:46:51 +02:00

287 lines
14 KiB
JavaScript

// -----------------------------
// Projektuebersicht - Bereich "Einstellungen" (Mein Konto)
// -----------------------------
// Dieser Bereich gehoert dem angemeldeten Konto selbst: Stammdaten zur
// Ansicht, die eigene E-Mail-Adresse und der Passwortwechsel. Sonst nichts.
//
// Bis zum Umbau standen hier auch die firmenweiten Masken (Honorar,
// Auslastung, Termine, Feiertage, Vorlagen, Betrieb). Das vermischte zwei
// verschiedene Dinge in einer Liste: die eigenen Kontoangaben, die jeden
// etwas angehen, und Vorgaben, die die Zahlen ALLER verschieben und
// ausschliesslich Administratoren zustehen. Letztere liegen jetzt im Bereich
// "Admin" (js/admin/admin.js) - dort, wo die uebrigen Admin-Aufgaben schon
// waren.
//
// Anzeigename, Rolle und die Personen-Zuordnung sind hier bewusst nur ZU
// SEHEN: Sie pflegt ein Administrator in der Benutzerverwaltung
// (PATCH /admin/users/:id). Die E-Mail-Adresse pflegt dagegen das Konto
// selbst (POST /change-email) - ein Administrator kann sie weiterhin
// setzen/aendern, aber niemand muss dafuer erst einen Administrator bitten,
// nur um erreichbar zu sein. Das Passwort gehoert ebenfalls dem Konto
// selbst; den Dialog dafuer stellt js/auth.js.
// renderSettings: Einstiegspunkt des Bereichs, von showTab('settings')
// gerufen. Keine Parameter, keine Rueckgabe.
function renderSettings(){
renderSettingsAccount();
renderSettingsAiKeys();
}
// ---------- Mein Konto ----------
// renderSettingsAccount: zeigt die Stammdaten des angemeldeten Kontos an.
// Nur die E-Mail-Adresse ist hier bearbeitbar (submitEmailChange()); alles
// andere ausser dem Passwort pflegt ein Administrator in der
// Benutzerverwaltung (PATCH /admin/users/:id). Keine Parameter, keine
// Rückgabe.
function renderSettingsAccount(){
const el = document.getElementById('settingsAccountInfo');
if(!el) return;
if(!currentUser){ el.innerHTML = ''; return; }
const row = (k, v) => `<div class="settings-kv-row"><span class="settings-kv-key">${esc(k)}</span><span>${v}</span></div>`;
el.innerHTML =
row('Benutzername', esc(currentUser.username || ''))
+ row('Anzeigename', esc(currentUser.displayName || ''))
+ `<div class="settings-kv-row">
<span class="settings-kv-key">E-Mail</span>
<span class="settings-email-edit">
<input type="email" id="settingsEmailInput" value="${esc(currentUser.email || '')}" placeholder="name@firma.de">
<button class="secondary" onclick="submitEmailChange()">Speichern</button>
<span id="settingsEmailError" class="settings-warn" hidden></span>
</span>
</div>`
+ row('Rolle', currentUser.role === 'admin' ? 'Administrator' : 'Mitglied')
// Ohne Personen-Zuordnung bleibt „Meine Aufgaben" leer, weil die Aufgaben
// über plan_todo_assignees.person_id zugeordnet werden (siehe
// GET /my-dashboard). Deshalb steht der Hinweis hier und nicht nur dort.
+ row('Personen-Zuordnung', currentUser.personId
? esc(currentUser.personId)
: '<span class="settings-warn">nicht gesetzt — „Meine Aufgaben" bleibt dadurch leer</span>')
+ (currentUser.mustChangePassword
? row('Hinweis', '<span class="settings-warn">Für dieses Konto ist ein Passwortwechsel erforderlich.</span>')
: '');
}
// submitEmailChange: schickt die eigene E-Mail-Adresse an POST /change-email.
// Anders als der Passwortwechsel braucht es kein aktuelles Passwort zur
// Bestätigung — die Route ist ohnehin an die eigene, per Session
// authentifizierte Kontokennung gebunden (req.user.id), nicht an eine vom
// Client mitgeschickte ID. Keine Parameter, keine Rückgabe.
async function submitEmailChange(){
const input = document.getElementById('settingsEmailInput');
const errEl = document.getElementById('settingsEmailError');
const show = msg => { if(errEl){ errEl.textContent = msg; errEl.hidden = false; } };
if(errEl){ errEl.textContent = ''; errEl.hidden = true; }
if(!input) return;
const email = input.value.trim();
if(!email) return show('E-Mail-Adresse erforderlich.');
try {
const r = await fetch(baseUrl() + '/change-email', {
method: 'POST',
headers: { 'Content-Type': 'application/json; charset=utf-8' },
body: JSON.stringify({ email })
});
const d = await r.json();
if(!r.ok || !d.ok) return show(d && d.error ? d.error : 'E-Mail-Adresse konnte nicht gespeichert werden.');
if(currentUser) currentUser.email = d.email;
} catch(err){
console.error('E-Mail-Änderung fehlgeschlagen:', err);
return show('Verbindung zum Server fehlgeschlagen.');
}
alert('Die E-Mail-Adresse wurde gespeichert.');
}
// ---------- KI-Zugangsdaten (persönlich je Konto) ----------
// Für die KI-gestützte Punkt-Erkennung beim Protokoll-Import
// (js/plan/plan-protokoll.js). GET /my-ai-settings liefert je Anbieter nur
// den Konfigurations-STATUS und die (nicht geheime) apiUrl — der Schlüssel
// selbst kommt nie zurück, wie ein Passwort; das Eingabefeld bleibt deshalb
// nach dem Laden immer leer. Ein Block je Anbieter (.ai-provider, siehe
// styles.css) statt eines durchgehenden Feld-Rasters — Design-Entwurf mit
// Erik abgestimmt.
// showSettingsNote: kurze, selbst verblassende Bestätigung am Fuß des
// Bereichs — dasselbe Muster wie showAdminNote() in js/admin/admin.js
// (#adminNote), hier für #settings/#settingsNote. Bewusst kein alert(): ein
// erfolgreicher Speichervorgang soll den Arbeitsfluss nicht anhalten.
// Parameter: text (String). Keine Rückgabe.
function showSettingsNote(text){
const el = document.getElementById('settingsNote');
if(!el || !text) return;
el.textContent = text;
el.classList.add('show');
if(showSettingsNote.timer) clearTimeout(showSettingsNote.timer);
showSettingsNote.timer = setTimeout(() => el.classList.remove('show'), 2500);
}
// setAiMode: wechselt einen Anbieter-Block zwischen "Cloud" (Vorgabe, kein
// URL-Feld sichtbar) und "Eigener Server" (URL-Feld sichtbar) — ersetzt den
// früheren, eingeklappten "API-URL"-Link durch eine sichtbare Wahl, siehe
// styles.css. Reiner Anzeige-Zustand, kein Server-Aufruf; das eigentliche
// Speichern der URL bleibt saveMyAiUrl(). Parameter: provider
// ('chatgpt'|'claude'), modus ('cloud'|'lokal'). Keine Rückgabe.
function setAiMode(provider, modus){
const toggle = document.getElementById(provider === 'chatgpt' ? 'aiOpenaiModeToggle' : 'aiClaudeModeToggle');
const panel = document.getElementById(provider === 'chatgpt' ? 'aiOpenaiUrlPanel' : 'aiClaudeUrlPanel');
if(!toggle || !panel) return;
toggle.querySelectorAll('button').forEach((btn, idx) => btn.classList.toggle('is-active', (idx === 0) === (modus === 'cloud')));
panel.classList.toggle('is-open', modus === 'lokal');
}
// renderSettingsAiKeys: holt den aktuellen Stand und füllt Status-Pill,
// URL-Feld, Cloud/Eigener-Server-Umschalter und den Deaktivierungs-Zustand
// der Entfernen-/Neu-verbinden-Knöpfe. Keine Parameter, keine Rückgabe.
async function renderSettingsAiKeys(){
const statusEl = { chatgpt: document.getElementById('aiOpenaiStatus'), claude: document.getElementById('aiClaudeStatus') };
const urlEl = { chatgpt: document.getElementById('setAiOpenaiUrl'), claude: document.getElementById('setAiClaudeUrl') };
const clearBtn = { chatgpt: document.getElementById('aiOpenaiClearBtn'), claude: document.getElementById('aiClaudeClearBtn') };
const reconnectBtn = { chatgpt: document.getElementById('aiOpenaiReconnectBtn'), claude: document.getElementById('aiClaudeReconnectBtn') };
const testErrorEl = { chatgpt: document.getElementById('aiOpenaiTestError'), claude: document.getElementById('aiClaudeTestError') };
if(!statusEl.chatgpt) return;
try {
const r = await fetch(baseUrl() + '/my-ai-settings');
const d = await r.json();
if(!r.ok || !d.ok) throw new Error((d && d.error) || 'Laden fehlgeschlagen.');
['chatgpt', 'claude'].forEach(provider => {
const info = d[provider] || { configured: false, apiUrl: '' };
const chip = statusEl[provider];
if(chip){
chip.classList.toggle('is-on', !!info.configured);
chip.innerHTML = '<span class="dot"></span>' + (info.configured ? 'Verbunden' : 'Nicht eingerichtet');
}
if(urlEl[provider] && document.activeElement !== urlEl[provider]) urlEl[provider].value = info.apiUrl || '';
if(clearBtn[provider]) clearBtn[provider].disabled = !info.configured;
if(reconnectBtn[provider]) reconnectBtn[provider].disabled = !info.configured;
// Ein neuer Seitenaufruf ist ein neuer Anlauf — ein Fehler aus einem
// früheren Verbindungstest soll nicht stehen bleiben, als gälte er
// noch für den gerade geladenen Stand.
if(testErrorEl[provider]) testErrorEl[provider].textContent = '';
// Eine bereits gesetzte abweichende API-URL wird nicht hinter "Cloud"
// versteckt — wer sie konfiguriert hat, soll beim Öffnen der Seite
// sofort sehen, dass "Eigener Server" aktiv ist.
setAiMode(provider, info.apiUrl ? 'lokal' : 'cloud');
});
} catch(err){
console.error('KI-Einstellungen laden fehlgeschlagen:', err);
}
}
// testMyAiConnection: "Neu verbinden" — prüft die GESPEICHERTE Konfiguration
// wirklich (POST /my-ai-settings/test, echter Testaufruf über
// erkennePunkteMitKI() serverseitig), nicht nur ob "configured" gesetzt
// ist. Ein Fehler bleibt im .ai-test-error-Feld stehen, bis der nächste
// Test läuft oder erfolgreich ist — bewusst kein flüchtiger Toast, siehe
// styles.css. Parameter: provider. Keine Rückgabe.
async function testMyAiConnection(provider){
const btn = document.getElementById(provider === 'chatgpt' ? 'aiOpenaiReconnectBtn' : 'aiClaudeReconnectBtn');
const errEl = document.getElementById(provider === 'chatgpt' ? 'aiOpenaiTestError' : 'aiClaudeTestError');
if(errEl) errEl.textContent = '';
if(btn){ btn.disabled = true; btn.classList.add('is-testing'); }
try {
const r = await fetch(baseUrl() + '/my-ai-settings/test', {
method: 'POST',
headers: { 'Content-Type': 'application/json; charset=utf-8' },
body: JSON.stringify({ provider })
});
const d = await r.json();
if(!r.ok || !d.ok){
if(errEl) errEl.textContent = (d && d.error) || 'Verbindung fehlgeschlagen.';
} else {
showSettingsNote((provider === 'chatgpt' ? 'ChatGPT' : 'Claude') + ': Verbindung erfolgreich.');
}
} catch(err){
console.error('KI-Verbindungstest fehlgeschlagen:', err);
if(errEl) errEl.textContent = 'Verbindung zum Server fehlgeschlagen.';
} finally {
if(btn){ btn.disabled = false; btn.classList.remove('is-testing'); }
}
}
// putMyAiKey: gemeinsamer PUT-Aufruf für saveMyAiKey/saveMyAiUrl/
// clearMyAiKey. patch enthält nur das tatsächlich zu ändernde Feld
// (apiKey ODER apiUrl) — ein fehlendes Feld bleibt serverseitig
// unangetastet, sodass ein Speichern im einen Feld nie das andere
// überschreibt. Parameter: provider ('chatgpt'|'claude'), patch (Objekt).
// Rückgabe: Promise.
function putMyAiKey(provider, patch){
const body = {};
body[provider] = patch;
return fetch(baseUrl() + '/my-ai-settings', {
method: 'PUT',
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.');
return json;
});
}
// saveMyAiKey: übernimmt den eingegebenen Schlüssel. Ein leeres Feld gilt
// als Fehlbedienung (zum Löschen dient der eigene "Entfernen"-Knopf,
// clearMyAiKey()), nicht als "nichts tun". Parameter: provider. Keine
// Rückgabe.
async function saveMyAiKey(provider){
const errEl = document.getElementById('settingsAiError');
const show = msg => { if(errEl){ errEl.textContent = msg; errEl.hidden = false; } };
if(errEl){ errEl.textContent = ''; errEl.hidden = true; }
const keyEl = document.getElementById(provider === 'chatgpt' ? 'setAiOpenaiKey' : 'setAiClaudeKey');
const keyValue = keyEl ? keyEl.value.trim() : '';
if(!keyValue) return show('Bitte einen Schlüssel eingeben.');
try {
await putMyAiKey(provider, { apiKey: keyValue });
if(keyEl) keyEl.value = '';
renderSettingsAiKeys();
showSettingsNote('Schlüssel gespeichert.');
} catch(err){
console.error('KI-Schlüssel speichern fehlgeschlagen:', err);
show(err.message || String(err));
}
}
// saveMyAiUrl: übernimmt die eingegebene API-URL — leer löscht eine zuvor
// gesetzte Abweichung und fällt auf die offizielle Cloud-API zurück.
// Eigenständig von saveMyAiKey(), da beide Felder jetzt getrennte
// Speichern-Knöpfe haben (siehe .ai-url-panel/setAiMode()). Parameter:
// provider. Keine Rückgabe.
async function saveMyAiUrl(provider){
const errEl = document.getElementById('settingsAiError');
const show = msg => { if(errEl){ errEl.textContent = msg; errEl.hidden = false; } };
if(errEl){ errEl.textContent = ''; errEl.hidden = true; }
const urlEl = document.getElementById(provider === 'chatgpt' ? 'setAiOpenaiUrl' : 'setAiClaudeUrl');
const urlValue = urlEl ? urlEl.value.trim() : '';
try {
await putMyAiKey(provider, { apiUrl: urlValue });
renderSettingsAiKeys();
showSettingsNote(urlValue ? 'API-URL gespeichert.' : 'API-URL entfernt — offizielle API wird wieder verwendet.');
} catch(err){
console.error('API-URL speichern fehlgeschlagen:', err);
show(err.message || String(err));
}
}
// clearMyAiKey: löscht nur den Schlüssel, lässt eine hinterlegte API-URL
// unangetastet (patch enthält absichtlich kein apiUrl-Feld). Parameter:
// provider. Keine Rückgabe.
async function clearMyAiKey(provider){
const errEl = document.getElementById('settingsAiError');
try {
await putMyAiKey(provider, { apiKey: '' });
renderSettingsAiKeys();
showSettingsNote('Schlüssel entfernt.');
} catch(err){
console.error('KI-Schlüssel entfernen fehlgeschlagen:', err);
if(errEl){ errEl.textContent = err.message || String(err); errEl.hidden = false; }
}
}