// ----------------------------- // 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) => `
${esc(k)}${v}
`; el.innerHTML = row('Benutzername', esc(currentUser.username || '')) + row('Anzeigename', esc(currentUser.displayName || '')) + `
E-Mail
` + 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) : 'nicht gesetzt — „Meine Aufgaben" bleibt dadurch leer') + (currentUser.mustChangePassword ? row('Hinweis', 'Für dieses Konto ist ein Passwortwechsel erforderlich.') : ''); } // 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 = '' + (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; } } }