287 lines
14 KiB
JavaScript
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; }
|
|
}
|
|
}
|