(function(){ // ============================================================ // views.jsx — SettingsPanel, ReviewModal, CalendarWeekView, // SlotAdder, downloadICS // Depends on (window): THEMES, ACCENT_PRESETS, GLASS_PRESETS, applyTheme, applyGlass, // todayISO, localISO, getCatColor, prioCol, hexToRgba // ============================================================ const { useState, useEffect } = React; // ── NotesSendButton ────────────────────────────────────────────────── function NotesSendButton({ notes, onClear, profile }){ const [sendState, setSendState] = useState("idle"); const send = async () => { if(!notes.trim())return; setSendState("sending"); try{ const uq=profile?`?user=${encodeURIComponent(profile)}`:''; const r=await fetch('/api/notes'+uq,{method:'POST',headers:{'Content-Type':'application/json',...authHeaders()},body:JSON.stringify({text:notes,ts:Date.now()})}); setSendState(r.ok?"ok":"err"); if(r.ok&&onClear)onClear(); }catch{setSendState("err");} setTimeout(()=>setSendState("idle"),3000); }; return( ); } // ── SettingsPanel ──────────────────────────────────────────────────── function SettingsPanel({ settings, onSave, onClose }){ const [s, setS] = useState(settings); const [openFeat, setOpenFeat] = useState(null); const [settingsTab, setSettingsTab] = useState("aussehen"); // FIXED: Einstellungen tabs added const notesKey = "planer_notes_" + (settings.profile || ""); const [notes, setNotes] = useState(() => localStorage.getItem("planer_notes_" + (settings.profile || "")) || ""); const [piAuth, setPiAuth] = useState({connected:false,pending:false,userCode:null,verUri:null}); const [pwOpen, setPwOpen] = useState(false); const [pw, setPw] = useState({cur:"",nw:"",status:"",msg:""}); const [unOpen, setUnOpen] = useState(false); const [un, setUn] = useState({nw:"",pw:"",status:"",msg:""}); const [emOpen, setEmOpen] = useState(false); const [em, setEm] = useState({val:"",status:"",msg:""}); const [reportSt, setReportSt] = useState(""); const [usersModal, setUsersModal] = useState(null); const [overviewOpen, setOverviewOpen] = useState(false); const [overview, setOverview] = useState(null); // ?user= for per-profile Outlook tokens — uses SAVED settings.profile, not unsaved local state const uq = () => settings.profile ? `?user=${encodeURIComponent(settings.profile)}` : ''; useEffect(()=>{ fetch('/api/outlook-status'+uq(),{headers:{...authHeaders()}}).then(r=>r.json()).then(d=>{ // FIXED: add authHeaders setPiAuth(p=>({...p,connected:d.connected})); }).catch(()=>{}); },[]); // FIXED: re-register push subscription on mount if permission already granted // (subscriptions can expire/become invalid; this keeps the server-side copy fresh) useEffect(()=>{ if(typeof Notification!=='undefined'&&Notification.permission==='granted')subscribePush(); },[]); async function subscribePush(){ if(typeof Notification==='undefined'||!('serviceWorker' in navigator))return false; try{ const reg = await navigator.serviceWorker.ready; const keyRes = await fetch('/api/push/key').then(r=>r.json()); const raw = keyRes.publicKey; const padding = '='.repeat((4 - raw.length % 4) % 4); const base64 = (raw + padding).replace(/-/g,'+').replace(/_/g,'/'); const rawData = atob(base64); const key = new Uint8Array([...rawData].map(c=>c.charCodeAt(0))); const sub = await reg.pushManager.subscribe({userVisibleOnly:true, applicationServerKey:key}); const r = await fetch('/api/push/subscribe',{method:'POST',headers:{'Content-Type':'application/json',...authHeaders()},body:JSON.stringify(sub)}); return r.ok; }catch(e){console.error('Push subscribe:',e);return false;} } useEffect(()=>{ if(!piAuth.pending)return; const iv=setInterval(async()=>{ try{ const d=await fetch('/api/outlook-status'+uq(),{headers:{...authHeaders()}}).then(r=>r.json()); // FIXED: add authHeaders if(d.connected){setPiAuth({connected:true,pending:false,userCode:null,verUri:null});clearInterval(iv);} else if(!d.pending){setPiAuth(p=>({...p,pending:false}));clearInterval(iv);} }catch{} },3000); return()=>clearInterval(iv); },[piAuth.pending]); async function startPiAuth(){ if(!s.azureClientId){alert(window.t("settings.azureMissing"));return;} try{ const d=await fetch('/api/outlook-auth'+uq(),{method:'POST',headers:{'Content-Type':'application/json',...authHeaders()},body:JSON.stringify({clientId:s.azureClientId})}).then(r=>r.json()); // FIXED: add authHeaders if(d.user_code)setPiAuth({connected:false,pending:true,userCode:d.user_code,verUri:d.verification_uri}); else alert(d.error||"Fehler"); }catch(e){alert("Fehler: "+e.message);} } async function disconnectPiAuth(){ await fetch('/api/outlook-auth'+uq(),{method:'DELETE',headers:{...authHeaders()}}).catch(()=>{}); // FIXED: add authHeaders setPiAuth({connected:false,pending:false,userCode:null,verUri:null}); } async function doChangePw(){ if(!pw.cur||!pw.nw)return; setPw(x=>({...x,status:"saving"})); try{ const r=await fetch('/api/change-password',{method:'POST',headers:{'Content-Type':'application/json',...authHeaders()},body:JSON.stringify({currentPassword:pw.cur,newPassword:pw.nw})}); const d=await r.json(); if(r.ok)setPw({cur:"",nw:"",status:"ok",msg:window.t("settings.passwordChanged")}); else setPw(x=>({...x,status:"err",msg:d.error||window.t("settings.error")})); }catch{setPw(x=>({...x,status:"err",msg:window.t("settings.networkError")}));} } async function doChangeUn(){ if(!un.nw||!un.pw)return; setUn(x=>({...x,status:"saving"})); try{ const r=await fetch('/api/change-username',{method:'POST',headers:{'Content-Type':'application/json',...authHeaders()},body:JSON.stringify({newUsername:un.nw,currentPassword:un.pw})}); const d=await r.json(); if(r.ok){window.setAuthToken(d.token,d.username);window.location.reload();} else setUn(x=>({...x,status:"err",msg:d.error||window.t("settings.error")})); }catch{setUn(x=>({...x,status:"err",msg:window.t("settings.networkError")}));} } async function loadOverview(){ if(overviewOpen){setOverviewOpen(false);return;} setOverviewOpen(true); if(overview)return; try{ const r=await fetch('/api/admin/overview',{headers:{...authHeaders()}}); const d=await r.json(); setOverview(Array.isArray(d)?d:[]); }catch{setOverview([]);} } async function openUsersModal(){ setUsersModal("loading"); try{ const r=await fetch('/api/admin/users',{headers:{...authHeaders()}}); const d=await r.json(); setUsersModal(Array.isArray(d)?d:[]); }catch{setUsersModal([]);} } async function doSendReport(){ setReportSt("sending"); try{ const r=await fetch('/api/admin/send-report',{headers:{...authHeaders()}}); setReportSt(r.ok?"ok":"err"); }catch{setReportSt("err");} } async function doChangeEm(){ setEm(x=>({...x,status:"saving"})); try{ const r=await fetch('/api/change-email',{method:'POST',headers:{'Content-Type':'application/json',...authHeaders()},body:JSON.stringify({email:em.val.trim()})}); const d=await r.json(); if(r.ok)setEm(x=>({...x,status:"ok",msg:d.email?window.t("settings.savedEmail",{email:d.email}):window.t("settings.emailRemoved")})); else setEm(x=>({...x,status:"err",msg:d.error||window.t("settings.error")})); }catch{setEm(x=>({...x,status:"err",msg:window.t("settings.networkError")}));} } const secTitle = (label) => (
{label}
); return (
e.stopPropagation()}> {/* ── Header ── */}
{t("settings.title")}
{/* ── Tab Navigation ── */}
{[["aussehen",t("settings.tab.aussehen")],["verbindungen",t("settings.tab.verbindungen")],["hilfe",t("settings.tab.hilfe")]].map(([id,label])=>( ))}
{/* ── Scrollable content ── */}
{settingsTab==="aussehen"&&<> {/* Darstellung */} {secTitle(t("settings.section.darstellung"))}
{t("settings.colorWorld")}
{Object.entries(THEMES).map(([key,th])=>{ const sel=s.theme===key; const isDark=key==="ink"||key==="marine"; return ( ); })}
{t("settings.accentColor")}
{ACCENT_PRESETS.map(a=>{ const sel=(s.accent||"#A15C38").toLowerCase()===a.hex.toLowerCase(); return (
{t("settings.language")}
{[{k:"de",l:"Deutsch"},{k:"en",l:"English"}].map(({k,l})=>{ const sel=(s.language||"de")===k; return ( ); })}
{t("settings.surface")}
{[{k:"",l:t("settings.surfaceNormal")},{k:"glass",l:t("settings.surfaceGlass")}].map(({k,l})=>{ const sel=(s.surface||"")=== k; return( ); })}
{(s.surface==="glass")&&(
{Object.entries(GLASS_PRESETS).map(([key,gp])=>{ const sel=(s.glassPreset||"frost")===key; return( ); })}
)}
{/* Benachrichtigungen */} {secTitle(t("settings.section.notifications"))}
{s.calendarPush!==false&&(
{t("settings.minutesBefore")} setS(x=>({...x,calendarReminderMin:Math.max(1,Math.min(120,parseInt(e.target.value)||15))}))} style={{width:60,fontSize:13}}/>
)}
{!!s.habitEveningReminder?.enabled&&(
{t("settings.time")} setS(x=>({...x,habitEveningReminder:{...(x.habitEveningReminder||{}),time:e.target.value}}))} style={{fontSize:13}}/>
)}
{t("settings.quietHours")}
{t("settings.quietHoursDesc")}
{t("settings.from")} setS(x=>({...x,quietHoursStart:Math.max(0,Math.min(23,parseInt(e.target.value)||0))}))} style={{width:55,fontSize:13}}/> {t("settings.to")} setS(x=>({...x,quietHoursEnd:Math.max(0,Math.min(23,parseInt(e.target.value)||0))}))} style={{width:55,fontSize:13}}/> {t("settings.oclock")}
{/* Reflektieren anpassen */} {secTitle(t("settings.section.reflect"))}
{t("settings.reflectDesc")}
{[ ["notiz",t("settings.module.notiz")], ["heatmap",t("settings.module.heatmap")], ["muster",t("settings.module.muster")], ["kpi",t("settings.module.kpi")], ["emailReport",t("settings.module.emailReport")], ["highlights",t("settings.module.highlights")], ["verschoben",t("settings.module.verschoben")], ["kategorien",t("settings.module.kategorien")], ["tagesplan",t("settings.module.tagesplan")], ["zeit",t("settings.module.zeit")], ["gewohnheiten",t("settings.module.gewohnheiten")], ["deadlines",t("settings.module.deadlines")], ].map(([key,label])=>{ const checked=(s.statsModules||{})[key]!==false; return( ); })}
} {settingsTab==="verbindungen"&&<> {/* Profil */} {secTitle(t("settings.section.profile"))}
setS(x=>({...x,name:e.target.value}))} placeholder={t("settings.yourNamePlaceholder")} style={{width:"100%",boxSizing:"border-box",fontSize:13}}/>
{/* FIXED A2: profile input replaced with account row */}
{getAuthUsername()||"—"}
{t("settings.loggedIn")}
{/* FIXED B5a: Abmelden button works correctly */}
{/* Passwort ändern */}
{pwOpen&&(
setPw(x=>({...x,cur:e.target.value}))} style={{fontSize:12,width:"100%",boxSizing:"border-box"}}/> setPw(x=>({...x,nw:e.target.value}))} style={{fontSize:12,width:"100%",boxSizing:"border-box"}}/> {pw.status&&
{pw.msg}
}
)}
{/* Benutzername ändern */}
{unOpen&&(
setUn(x=>({...x,nw:e.target.value}))} autoCapitalize="none" style={{fontSize:12,width:"100%",boxSizing:"border-box"}}/> setUn(x=>({...x,pw:e.target.value}))} style={{fontSize:12,width:"100%",boxSizing:"border-box"}}/> {un.status&&
{un.msg}
}
)}
{/* E-Mail ändern */}
{emOpen&&(
setEm(x=>({...x,val:e.target.value,status:"",msg:""}))} style={{fontSize:12,width:"100%",boxSizing:"border-box"}}/> {em.status&&
{em.msg}
}
)}
{/* Admin — nur für oliver */} {window.getAuthUsername()==="oliver"&&<> {secTitle(t("settings.section.admin"))}
{t("settings.weeklyReportInfo")}
{reportSt==="ok"&&
{t("settings.reportSent")}
} {reportSt==="err"&&
{t("settings.reportError")}
}
{overviewOpen&&(
{overview===null ?
{t("settings.loading")}
:overview.length===0 ?
{t("settings.noData")}
:overview.map((u,i)=>(
0?"1px solid var(--col-border)":"none"}}> {u.username}
{u.openTodos} {t("settings.open")} {u.upcomingExams} {t("settings.exams")} {u.lastActivity||"—"}
)) }
)}
} {/* Konten-Modal */} {usersModal!==null&&
setUsersModal(null)}>
e.stopPropagation()}>
{t("settings.registeredAccounts")}
{usersModal==="loading" ?
{t("settings.loading")}
:usersModal.length===0 ?
{t("settings.noAccountsFound")}
:usersModal.map((u,i)=>(
{u.username} {u.created}
{u.email||{t("settings.noEmailOnFile")}}
)) }
} {/* Integrationen */} {secTitle(t("settings.section.integrations"))}
Microsoft Outlook
setS(x=>({...x,azureClientId:e.target.value}))} placeholder={t("settings.azurePlaceholder")} style={{width:"100%",boxSizing:"border-box",fontSize:12,marginBottom:6}}/>{/* FIXED E8: Azure placeholder clarified */}

{t("settings.azureHelp")}

{t("settings.redirectUriLabel")}
{window.location.href.split("?")[0].split("#")[0].replace(/\/[^/]*$/,'/Mein%20Planer.html')}
{/* FIXED B3: redirect URI uses correct filename */}
{t("settings.redirectUriHelp")}
{/localhost|127\.0\.0\.1/.test(window.location.href)&&
{t("settings.localDevWarning")}
}{/* FIXED: Warnung wenn Dev-URL statt Pi-URL */}
{t("settings.piOutlook")}
{piAuth.connected ?
{t("settings.piConnected")}
:piAuth.userCode ?
{t("settings.openOnDevice")}
{piAuth.verUri}
{t("settings.enterCode")}
{piAuth.userCode}
{t("settings.waitingConfirmation")}
: }
} {settingsTab==="aussehen"&&<> {/* App */} {secTitle(t("settings.section.app"))}
{t("settings.workHours")}
setS(x=>({...x,workStart:e.target.value}))} style={{width:"100%"}}/>
setS(x=>({...x,workEnd:e.target.value}))} style={{width:"100%"}}/>
{'Notification' in window ? ( Notification.permission === 'granted' ?
{t("settings.pushActive")}
: ) :
{t("settings.notificationsUnsupported")}
}
{secTitle(t("settings.section.studyTimer"))}
{t("settings.showSeconds")}
{s.timerSeconds!==false?"1:23:45":"1:23 / 23 min"}
setS(x=>({...x,timerSeconds:e.target.checked}))} style={{width:18,height:18,cursor:"pointer"}}/>
{t("settings.trackTimeOnDone")}
{t("settings.studyPopupDesc")}
setS(x=>({...x,learnPopup:e.target.checked}))} style={{width:18,height:18,cursor:"pointer"}}/>
} {settingsTab==="hilfe"&&<> {/* Notizen */} {secTitle(t("settings.section.notes"))}
{t("settings.notesDesc")}