All new
alle new
This commit is contained in:
1 parent
da64472251
commit
5b20adee48
1605 files changed
+396517
-1674
No files matched your search
@@ -0,0 +1,286 @@
|
||||
// -----------------------------
|
||||
// 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; }
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user