feat: web settings restructure + popup drag + calendar management table (v52)

- Remove "Ansicht" tab; move Kalenderansicht to top of Darstellung
- Reorder Darstellung: Kalenderansicht → Sprache → Stundenhöhe → Farben
- Move Standard-Termindauer to Profil tab as contrast-button selector
- Replace per-source account sections in Kalender tab with unified
  calendar management table (Name/Herkunft/Ausgeblendet/Benachrichtigungen/
  Export·Import/Sync/Delete) covering all calendar sources
- Add "Konto hinzufügen" buttons (Lokal/CalDAV/iCal/HA/Google) to Kalender tab
- Popup drag-to-move via pointer capture on .popup-header

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-06-15 20:36:09 +02:00
parent 2c59d873f3
commit d1c5b66230
4 changed files with 380 additions and 92 deletions

View File

@@ -1137,6 +1137,7 @@ a { color: var(--primary); text-decoration: none; }
background: linear-gradient(180deg, background: linear-gradient(180deg,
color-mix(in srgb, var(--ev-color, var(--primary)) 13%, transparent), transparent); color-mix(in srgb, var(--ev-color, var(--primary)) 13%, transparent), transparent);
} }
.ep-dragging { user-select: none; }
/* Slim accent strip in the event's colour. */ /* Slim accent strip in the event's colour. */
.popup-header::before { .popup-header::before {
content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 4px; content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 4px;
@@ -1430,6 +1431,31 @@ a { color: var(--primary); text-decoration: none; }
width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; display: inline-block; width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; display: inline-block;
} }
/* ── Calendar Management Table ──────────────────────────── */
.accounts-add-row { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 4px; }
.cal-manage-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.cal-manage-table th {
text-align: left; font-size: 11px; font-weight: 600; text-transform: uppercase;
letter-spacing: .4px; color: var(--text-3); padding: 0 8px 8px 0;
border-bottom: 1px solid var(--border-light);
}
.cal-manage-table td { padding: 6px 8px 6px 0; border-bottom: 1px solid var(--border-light); vertical-align: middle; }
.cal-manage-table tbody tr:last-child td { border-bottom: none; }
.ct-acc-row td { padding-top: 14px; background: none; }
.ct-acc-row td:first-child { font-size: 13px; }
.ct-cal-row .ct-indent { padding-left: 16px; }
.ct-src { font-size: 11px; color: var(--text-3); white-space: nowrap; }
.ct-badge {
display: inline-block; font-size: 10px; font-weight: 600; padding: 1px 5px;
border-radius: 3px; background: var(--surface-2); color: var(--text-3);
margin-left: 6px; vertical-align: middle;
}
.ct-badge-g { background: #e8f0fe; color: #1a73e8; }
.ct-badge-ha { background: #e8f5e9; color: #2e7d32; }
.ct-dot { width: 9px; height: 9px; border-radius: 50%; display: inline-block; margin-right: 5px; vertical-align: middle; flex-shrink: 0; }
.ct-empty { color: var(--text-3); font-size: 13px; padding: 16px 0; text-align: center; }
.ct-toggle { cursor: pointer; }
/* ── Month View (spanning bars) ─────────────────────────── */ /* ── Month View (spanning bars) ─────────────────────────── */
.month-body { .month-body {
display: flex; flex-direction: column; flex: 1; overflow: hidden; display: flex; flex-direction: column; flex: 1; overflow: hidden;

View File

@@ -692,7 +692,6 @@
<nav class="settings-nav"> <nav class="settings-nav">
<button class="settings-nav-btn active" data-panel="profile" data-i18n="settings_nav_profile">Profil</button> <button class="settings-nav-btn active" data-panel="profile" data-i18n="settings_nav_profile">Profil</button>
<button class="settings-nav-btn" data-panel="general" data-i18n="settings_nav_appearance">Darstellung</button> <button class="settings-nav-btn" data-panel="general" data-i18n="settings_nav_appearance">Darstellung</button>
<button class="settings-nav-btn" data-panel="view" data-i18n="settings_nav_view">Ansicht</button>
<button class="settings-nav-btn" data-panel="accounts" data-i18n="settings_nav_calendars">Kalender</button> <button class="settings-nav-btn" data-panel="accounts" data-i18n="settings_nav_calendars">Kalender</button>
<button class="settings-nav-btn hidden" data-panel="users" id="settings-nav-users" data-i18n="settings_nav_users">Benutzerverwaltung</button> <button class="settings-nav-btn hidden" data-panel="users" id="settings-nav-users" data-i18n="settings_nav_users">Benutzerverwaltung</button>
</nav> </nav>
@@ -727,6 +726,16 @@
</select> </select>
</div> </div>
<h4 class="panel-title" style="margin-top:24px" data-i18n="settings_default_duration">Standard-Termindauer</h4>
<div class="contrast-selector" id="cfg-event-duration">
<button class="contrast-btn" data-val="15">15 min</button>
<button class="contrast-btn" data-val="30">30 min</button>
<button class="contrast-btn" data-val="45">45 min</button>
<button class="contrast-btn" data-val="60">1 h</button>
<button class="contrast-btn" data-val="90">1,5 h</button>
<button class="contrast-btn" data-val="120">2 h</button>
</div>
<h4 class="panel-title" style="margin-top:24px" data-i18n="settings_calendars">Geteilter Kalender</h4> <h4 class="panel-title" style="margin-top:24px" data-i18n="settings_calendars">Geteilter Kalender</h4>
<p class="panel-desc" data-i18n="settings_group_visible_desc">Wähle, welcher deiner Kalender für deine Gruppenmitglieder sichtbar ist</p> <p class="panel-desc" data-i18n="settings_group_visible_desc">Wähle, welcher deiner Kalender für deine Gruppenmitglieder sichtbar ist</p>
<div class="form-group"> <div class="form-group">
@@ -735,10 +744,35 @@
</div> </div>
</div> </div>
<!-- Darstellung: Sprache, Farben, Stundenhöhe --> <!-- Darstellung: Kalenderansicht, Sprache, Stundenhöhe, Farben -->
<div class="settings-panel" id="settings-panel-general"> <div class="settings-panel" id="settings-panel-general">
<h4 class="panel-title" data-i18n="settings_language">Sprache</h4> <h4 class="panel-title" data-i18n="settings_calendar_view">Kalenderansicht</h4>
<div class="form-group">
<label data-i18n="settings_default_view">Standardansicht</label>
<select id="cfg-default-view">
<option value="month" data-i18n="view_month">Monat</option>
<option value="week" data-i18n="view_week">Woche</option>
<option value="day" data-i18n="view_day">Tag</option>
<option value="quarter" data-i18n="view_quarter">Quartal</option>
<option value="agenda" data-i18n="view_agenda">Termine</option>
</select>
</div>
<div class="form-group">
<label data-i18n="settings_week_start">Erster Wochentag</label>
<select id="cfg-week-start">
<option value="monday" data-i18n="week_start_monday">Montag</option>
<option value="sunday" data-i18n="week_start_sunday">Sonntag</option>
</select>
</div>
<div class="form-group">
<label class="toggle-label">
<input type="checkbox" id="cfg-dim-past" />
<span data-i18n="settings_dim_past">Vergangene Termine ausgrauen</span>
</label>
</div>
<h4 class="panel-title" style="margin-top:24px" data-i18n="settings_language">Sprache</h4>
<div class="form-group"> <div class="form-group">
<select id="cfg-language"> <select id="cfg-language">
<option value="de">Deutsch</option> <option value="de">Deutsch</option>
@@ -746,6 +780,15 @@
</select> </select>
</div> </div>
<h4 class="panel-title" style="margin-top:24px" data-i18n="settings_hour_height">Stundenhöhe (Wochen- &amp; Tagesansicht)</h4>
<p class="panel-desc" data-i18n="settings_hour_height_desc">Wie viel Platz eine Stunde in der Zeitrasteransicht einnimmt</p>
<div class="contrast-selector" id="cfg-hour-height" data-setting="hour_height">
<button class="contrast-btn" data-val="28"><span class="hour-preview">━━</span><span class="contrast-lbl" data-i18n="hour_compact">Kompakt</span></button>
<button class="contrast-btn" data-val="44"><span class="hour-preview">━━━</span><span class="contrast-lbl" data-i18n="hour_normal">Normal</span></button>
<button class="contrast-btn" data-val="60"><span class="hour-preview">━━━━</span><span class="contrast-lbl" data-i18n="hour_comfort">Komfort</span></button>
<button class="contrast-btn" data-val="80"><span class="hour-preview">━━━━━</span><span class="contrast-lbl" data-i18n="hour_large">Gross</span></button>
</div>
<h4 class="panel-title" style="margin-top:24px" data-i18n="settings_colors">Farben</h4> <h4 class="panel-title" style="margin-top:24px" data-i18n="settings_colors">Farben</h4>
<div class="form-group"> <div class="form-group">
<label data-i18n="settings_primary_color">Primärfarbe</label> <label data-i18n="settings_primary_color">Primärfarbe</label>
@@ -807,88 +850,19 @@
<button type="button" class="btn btn-ghost btn-sm" id="cfg-bg-color-reset" data-i18n="reset" title="Zurücksetzen">Reset</button> <button type="button" class="btn btn-ghost btn-sm" id="cfg-bg-color-reset" data-i18n="reset" title="Zurücksetzen">Reset</button>
</div> </div>
</div> </div>
<h4 class="panel-title" style="margin-top:24px" data-i18n="settings_hour_height">Stundenhöhe (Wochen- &amp; Tagesansicht)</h4>
<p class="panel-desc" data-i18n="settings_hour_height_desc">Wie viel Platz eine Stunde in der Zeitrasteransicht einnimmt</p>
<div class="contrast-selector" id="cfg-hour-height" data-setting="hour_height">
<button class="contrast-btn" data-val="28"><span class="hour-preview">━━</span><span class="contrast-lbl" data-i18n="hour_compact">Kompakt</span></button>
<button class="contrast-btn" data-val="44"><span class="hour-preview">━━━</span><span class="contrast-lbl" data-i18n="hour_normal">Normal</span></button>
<button class="contrast-btn" data-val="60"><span class="hour-preview">━━━━</span><span class="contrast-lbl" data-i18n="hour_comfort">Komfort</span></button>
<button class="contrast-btn" data-val="80"><span class="hour-preview">━━━━━</span><span class="contrast-lbl" data-i18n="hour_large">Gross</span></button>
</div>
</div> </div>
<!-- Ansicht: Standardansicht, Wochenstart, vergangene Termine, ausgeblendete Kalender --> <!-- Kalender: einheitliche Tabelle aller Kalender aus allen Quellen -->
<div class="settings-panel" id="settings-panel-view">
<h4 class="panel-title" data-i18n="settings_calendar_view">Kalenderansicht</h4>
<div class="form-group">
<label data-i18n="settings_default_view">Standardansicht</label>
<select id="cfg-default-view">
<option value="month" data-i18n="view_month">Monat</option>
<option value="week" data-i18n="view_week">Woche</option>
<option value="day" data-i18n="view_day">Tag</option>
<option value="quarter" data-i18n="view_quarter">Quartal</option>
<option value="agenda" data-i18n="view_agenda">Termine</option>
</select>
</div>
<div class="form-group">
<label data-i18n="settings_week_start">Erster Wochentag</label>
<select id="cfg-week-start">
<option value="monday" data-i18n="week_start_monday">Montag</option>
<option value="sunday" data-i18n="week_start_sunday">Sonntag</option>
</select>
</div>
<div class="form-group">
<label class="toggle-label">
<input type="checkbox" id="cfg-dim-past" />
<span data-i18n="settings_dim_past">Vergangene Termine ausgrauen</span>
</label>
</div>
<div class="form-group">
<label data-i18n="settings_default_duration">Standard-Termindauer</label>
<select id="cfg-default-duration">
<option value="15">15 min</option>
<option value="30">30 min</option>
<option value="45">45 min</option>
<option value="60">1 h</option>
<option value="90">1.5 h</option>
<option value="120">2 h</option>
<option value="240">4 h</option>
</select>
</div>
<h4 class="panel-title" style="margin-top:24px" data-i18n="settings_hidden_cals">Ausgeblendete Kalender</h4>
<div id="hidden-cals-list"><span style="font-size:13px;color:var(--text-3)" data-i18n="settings_no_hidden_cals">Keine ausgeblendeten Kalender</span></div>
</div>
<!-- Konten (CalDAV, Lokal, iCal, Google) -->
<div class="settings-panel" id="settings-panel-accounts"> <div class="settings-panel" id="settings-panel-accounts">
<h4 class="panel-title" data-i18n="settings_nav_calendars">Kalender</h4> <h4 class="panel-title" data-i18n="settings_nav_calendars">Kalender</h4>
<div class="accounts-add-row">
<div class="accounts-section"> <button class="btn btn-secondary btn-sm" id="settings-btn-add-local">+ Lokal</button>
<div class="accounts-section-heading" data-i18n="settings_accounts_caldav">CalDAV-Konten</div> <button class="btn btn-secondary btn-sm" id="settings-btn-add-caldav">+ CalDAV</button>
<div id="accounts-caldav-list"><span class="accounts-section-empty" data-i18n="settings_no_caldav_accounts">Keine CalDAV-Konten</span></div> <button class="btn btn-secondary btn-sm" id="settings-btn-add-ical">+ iCal</button>
</div> <button class="btn btn-secondary btn-sm" id="settings-btn-add-ha">+ Home Assistant</button>
<button class="btn btn-secondary btn-sm" id="settings-btn-add-google">+ Google</button>
<div class="accounts-section">
<div class="accounts-section-heading" data-i18n="settings_accounts_local">Lokale Kalender</div>
<div id="accounts-local-list"><span class="accounts-section-empty" data-i18n="settings_no_local_cals">Keine lokalen Kalender</span></div>
</div>
<div class="accounts-section">
<div class="accounts-section-heading" data-i18n="settings_accounts_ical">iCal-Abonnements</div>
<div id="accounts-ical-list"><span class="accounts-section-empty" data-i18n="settings_no_ical_subs">Keine Abonnements</span></div>
</div>
<div class="accounts-section">
<div class="accounts-section-heading" data-i18n="settings_accounts_google">Google-Konten</div>
<div id="google-accounts-list"><span class="accounts-section-empty" data-i18n="settings_no_google_accounts">Keine Google-Konten</span></div>
</div>
<div class="accounts-section">
<div class="accounts-section-heading">Home Assistant</div>
<div id="accounts-ha-list"><span class="accounts-section-empty">Keine HA-Konten</span></div>
</div> </div>
<div id="cal-settings-table" style="margin-top:16px;overflow-x:auto"></div>
</div> </div>
<!-- Benutzerverwaltung --> <!-- Benutzerverwaltung -->

View File

@@ -1425,6 +1425,33 @@ function showEventPopup(ev, anchor) {
popup.style.left = Math.max(8, left) + 'px'; popup.style.left = Math.max(8, left) + 'px';
popup.style.top = Math.max(8, top) + 'px'; popup.style.top = Math.max(8, top) + 'px';
// Drag-to-move: grab the header and drag the popup anywhere on screen
const dragHandle = popup.querySelector('.popup-header');
let dragOffX = 0, dragOffY = 0;
dragHandle.style.cursor = 'grab';
const onPointerMove = e => {
if (!popup.classList.contains('ep-dragging')) return;
const vw = window.innerWidth, vh = window.innerHeight;
const x = Math.max(0, Math.min(vw - popup.offsetWidth, e.clientX - dragOffX));
const y = Math.max(0, Math.min(vh - popup.offsetHeight, e.clientY - dragOffY));
popup.style.left = x + 'px';
popup.style.top = y + 'px';
};
const onPointerUp = () => {
popup.classList.remove('ep-dragging');
dragHandle.style.cursor = 'grab';
};
dragHandle.onpointerdown = e => {
if (e.button !== 0) return;
dragOffX = e.clientX - popup.getBoundingClientRect().left;
dragOffY = e.clientY - popup.getBoundingClientRect().top;
dragHandle.setPointerCapture(e.pointerId);
popup.classList.add('ep-dragging');
dragHandle.style.cursor = 'grabbing';
};
dragHandle.onpointermove = onPointerMove;
dragHandle.onpointerup = onPointerUp;
// Hide edit/delete for read-only iCal subscription events // Hide edit/delete for read-only iCal subscription events
const isReadOnly = (ev.source === 'ical'); const isReadOnly = (ev.source === 'ical');
document.getElementById('popup-edit').style.display = isReadOnly ? 'none' : ''; document.getElementById('popup-edit').style.display = isReadOnly ? 'none' : '';
@@ -3004,7 +3031,6 @@ function openSettingsModal() {
prev.style.background = val || fallback; prev.style.background = val || fallback;
}); });
document.getElementById('cfg-dim-past').checked = !!s.dim_past_events; document.getElementById('cfg-dim-past').checked = !!s.dim_past_events;
document.getElementById('cfg-default-duration').value = String(s.default_event_duration_minutes || 60);
document.getElementById('cfg-language').value = getLang(); document.getElementById('cfg-language').value = getLang();
document.getElementById('cfg-private-visibility').value = s.private_event_visibility || 'busy'; document.getElementById('cfg-private-visibility').value = s.private_event_visibility || 'busy';
renderGroupVisibleList(s.group_visible_calendar_id); renderGroupVisibleList(s.group_visible_calendar_id);
@@ -3017,11 +3043,12 @@ function openSettingsModal() {
document.getElementById('cfg-email').value = p.email || ''; document.getElementById('cfg-email').value = p.email || '';
}).catch(() => {}); }).catch(() => {});
// Set active contrast/hour-height buttons // Set active contrast/hour-height/duration buttons
[ [
{ id: 'cfg-text-contrast', val: s.text_contrast || 3 }, { id: 'cfg-text-contrast', val: s.text_contrast || 3 },
{ id: 'cfg-line-contrast', val: s.line_contrast || 3 }, { id: 'cfg-line-contrast', val: s.line_contrast || 3 },
{ id: 'cfg-hour-height', val: s.hour_height || 60 }, { id: 'cfg-hour-height', val: s.hour_height || 60 },
{ id: 'cfg-event-duration', val: s.default_event_duration_minutes || 60 },
].forEach(({ id, val }) => { ].forEach(({ id, val }) => {
const sel = document.getElementById(id); const sel = document.getElementById(id);
if (!sel) return; if (!sel) return;
@@ -3040,9 +3067,8 @@ function openSettingsModal() {
const firstBtn = document.querySelector('.settings-nav-btn:not(.hidden)'); const firstBtn = document.querySelector('.settings-nav-btn:not(.hidden)');
if (firstBtn) activateSettingsPanel(firstBtn.dataset.panel); if (firstBtn) activateSettingsPanel(firstBtn.dataset.panel);
// Render all accounts and hidden calendars // Render unified calendar table
renderAllAccounts(); renderAllAccounts();
renderHiddenCalendars();
openModal('modal-settings'); openModal('modal-settings');
} }
@@ -3215,9 +3241,6 @@ function renderAllAccounts() {
} }
} }
// Google accounts section — delegate to existing function
renderGoogleAccounts();
// Home Assistant accounts section // Home Assistant accounts section
const haList = document.getElementById('accounts-ha-list'); const haList = document.getElementById('accounts-ha-list');
if (haList) { if (haList) {
@@ -3262,10 +3285,13 @@ function renderAllAccounts() {
}); });
} }
} }
renderCalendarTable();
} }
function renderHiddenCalendars() { function renderHiddenCalendars() {
const list = document.getElementById('hidden-cals-list'); const list = document.getElementById('hidden-cals-list');
if (!list) return;
const hidden = []; const hidden = [];
for (const acc of state.accounts) { for (const acc of state.accounts) {
for (const cal of acc.calendars) { for (const cal of acc.calendars) {
@@ -3325,6 +3351,252 @@ function renderHiddenCalendars() {
}); });
} }
function renderCalendarTable() {
const container = document.getElementById('cal-settings-table');
if (!container) return;
const TRASH = `<svg viewBox="0 0 24 24" fill="currentColor" width="14" height="14"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>`;
let rowCount = 0;
const hid = (src, id, checked) =>
`<input type="checkbox" class="ct-toggle" ${checked ? 'checked' : ''} data-ct-hid="${src}" data-ct-id="${id}">`;
const rem = (src, id, checked) =>
`<input type="checkbox" class="ct-toggle" ${checked ? 'checked' : ''} data-ct-rem="${src}" data-ct-id="${id}">`;
const dot = (color, fb) =>
`<span class="ct-dot" style="background:${color || fb}"></span>`;
let rows = '';
// Local calendars
for (const cal of state.localCalendars) {
if (cal.group) continue;
const owned = cal.owned !== false;
const canWrite = owned || cal.permission === 'read_write';
rows += `<tr>
<td>${dot(cal.color, '#34a853')}${escHtml(cal.name)}</td>
<td class="ct-src">Lokal</td>
<td>—</td>
<td>${owned ? rem('local', cal.id, cal.reminders_enabled !== false) : '—'}</td>
<td>
<button class="btn btn-ghost btn-sm" data-ct-export="local" data-ct-id="${cal.id}" data-ct-name="${escHtml(cal.name)}">Export</button>
${canWrite ? `<button class="btn btn-ghost btn-sm" data-ct-import="local" data-ct-id="${cal.id}">Import</button>` : ''}
</td>
<td>—</td>
<td>${owned ? `<button class="icon-btn mini-btn" data-ct-del="local" data-ct-id="${cal.id}">${TRASH}</button>` : ''}</td>
</tr>`;
rowCount++;
}
// iCal subscriptions
for (const sub of state.icalSubscriptions) {
rows += `<tr>
<td>${dot(sub.color, '#aa66cc')}${escHtml(sub.name)}</td>
<td class="ct-src">iCal</td>
<td>${hid('ical', sub.id, !sub.sidebar_hidden)}</td>
<td>${rem('ical', sub.id, sub.reminders_enabled !== false)}</td>
<td>—</td>
<td>—</td>
<td><button class="icon-btn mini-btn" data-ct-del="ical" data-ct-id="${sub.id}">${TRASH}</button></td>
</tr>`;
rowCount++;
}
// CalDAV accounts + their calendars
for (const acc of state.accounts) {
rows += `<tr class="ct-acc-row">
<td colspan="2"><strong>${escHtml(acc.name)}</strong><span class="ct-badge">CalDAV</span></td>
<td>—</td><td>—</td><td>—</td>
<td><button class="btn btn-secondary btn-sm" data-ct-sync="caldav" data-ct-id="${acc.id}">${t('sync')}</button></td>
<td><button class="btn btn-ghost btn-sm" data-ct-disc="caldav" data-ct-id="${acc.id}">${t('disconnect')}</button></td>
</tr>`;
rowCount++;
for (const cal of (acc.calendars || [])) {
rows += `<tr class="ct-cal-row">
<td class="ct-indent">${dot(cal.color, '#4285f4')}${escHtml(cal.name)}</td>
<td class="ct-src">${escHtml(acc.name)}</td>
<td>${hid('caldav', cal.id, !cal.sidebar_hidden)}</td>
<td>${rem('caldav', cal.id, cal.reminders_enabled !== false)}</td>
<td>—</td><td>—</td><td>—</td>
</tr>`;
rowCount++;
}
}
// Google accounts + their calendars
for (const acc of state.googleAccounts) {
rows += `<tr class="ct-acc-row">
<td colspan="2"><strong>${escHtml(acc.email)}</strong><span class="ct-badge ct-badge-g">Google</span></td>
<td>—</td><td>—</td><td>—</td>
<td><button class="btn btn-secondary btn-sm" data-ct-sync="google" data-ct-id="${acc.id}">${t('sync')}</button></td>
<td><button class="btn btn-ghost btn-sm" data-ct-disc="google" data-ct-id="${acc.id}">${t('disconnect')}</button></td>
</tr>`;
rowCount++;
for (const cal of (acc.calendars || [])) {
rows += `<tr class="ct-cal-row">
<td class="ct-indent">${dot(cal.color, '#4285f4')}${escHtml(cal.name)}</td>
<td class="ct-src">${escHtml(acc.email)}</td>
<td>${hid('google', cal.id, !cal.sidebar_hidden)}</td>
<td>${rem('google', cal.id, cal.reminders_enabled !== false)}</td>
<td>—</td><td>—</td><td>—</td>
</tr>`;
rowCount++;
}
}
// Home Assistant accounts + their calendars
for (const acc of state.haAccounts) {
rows += `<tr class="ct-acc-row">
<td colspan="2"><strong>${escHtml(acc.name)}</strong><span class="ct-badge ct-badge-ha">Home Assistant</span></td>
<td>—</td><td>—</td><td>—</td>
<td><button class="btn btn-secondary btn-sm" data-ct-sync="ha" data-ct-id="${acc.id}">${t('sync')}</button></td>
<td><button class="btn btn-ghost btn-sm" data-ct-disc="ha" data-ct-id="${acc.id}">${t('disconnect')}</button></td>
</tr>`;
rowCount++;
for (const cal of (acc.calendars || [])) {
rows += `<tr class="ct-cal-row">
<td class="ct-indent">${dot(cal.color, '#03a9f4')}${escHtml(cal.name)}</td>
<td class="ct-src">${escHtml(acc.name)}</td>
<td>${hid('ha', cal.id, !cal.sidebar_hidden)}</td>
<td>${rem('ha', cal.id, cal.reminders_enabled !== false)}</td>
<td>—</td><td>—</td><td>—</td>
</tr>`;
rowCount++;
}
}
if (!rowCount) {
rows = `<tr><td colspan="7" class="ct-empty">Keine Kalender vorhanden</td></tr>`;
}
container.innerHTML = `<table class="cal-manage-table">
<thead><tr>
<th>Name</th><th>Herkunft</th><th>Ausgeblendet</th><th>Benachrichtigungen</th><th>Export / Import</th><th>Sync</th><th></th>
</tr></thead>
<tbody>${rows}</tbody>
</table>`;
// Hidden toggle
container.querySelectorAll('.ct-toggle[data-ct-hid]').forEach(cb => {
cb.addEventListener('change', async () => {
const src = cb.dataset.ctHid, id = parseInt(cb.dataset.ctId), hidden = !cb.checked;
try {
if (src === 'ical') {
await api.put(`/ical/subscriptions/${id}`, { sidebar_hidden: hidden });
const s = state.icalSubscriptions.find(s => s.id === id);
if (s) s.sidebar_hidden = hidden;
} else if (src === 'caldav') {
await api.put(`/caldav/calendars/${id}`, { enabled: !hidden, sidebar_hidden: hidden });
for (const acc of state.accounts) { const c = acc.calendars.find(c => c.id === id); if (c) { c.sidebar_hidden = hidden; c.enabled = !hidden; } }
} else if (src === 'google') {
await api.put(`/google/calendars/${id}`, { enabled: !hidden, sidebar_hidden: hidden });
for (const acc of state.googleAccounts) { const c = acc.calendars.find(c => c.id === id); if (c) { c.sidebar_hidden = hidden; c.enabled = !hidden; } }
} else if (src === 'ha') {
await api.put(`/homeassistant/calendars/${id}`, { enabled: !hidden, sidebar_hidden: hidden });
for (const acc of state.haAccounts) { const c = acc.calendars.find(c => c.id === id); if (c) { c.sidebar_hidden = hidden; c.enabled = !hidden; } }
}
renderCalendarList();
} catch (e) { showToast(e.message, true); cb.checked = !cb.checked; }
});
});
// Reminders toggle
container.querySelectorAll('.ct-toggle[data-ct-rem]').forEach(cb => {
cb.addEventListener('change', async () => {
const src = cb.dataset.ctRem, id = parseInt(cb.dataset.ctId), enabled = cb.checked;
const path = src === 'ical' ? `/ical/subscriptions/${id}` : `/${src === 'ha' ? 'homeassistant' : src}/calendars/${id}`;
try {
await api.put(path, { reminders_enabled: enabled });
if (src === 'local') { const c = state.localCalendars.find(c => c.id === id); if (c) c.reminders_enabled = enabled; }
else if (src === 'ical') { const s = state.icalSubscriptions.find(s => s.id === id); if (s) s.reminders_enabled = enabled; }
else if (src === 'caldav') { for (const acc of state.accounts) { const c = acc.calendars.find(c => c.id === id); if (c) c.reminders_enabled = enabled; } }
else if (src === 'google') { for (const acc of state.googleAccounts) { const c = acc.calendars.find(c => c.id === id); if (c) c.reminders_enabled = enabled; } }
else if (src === 'ha') { for (const acc of state.haAccounts) { const c = acc.calendars.find(c => c.id === id); if (c) c.reminders_enabled = enabled; } }
renderCalendarList();
} catch (e) { showToast(e.message, true); cb.checked = !cb.checked; }
});
});
// Sync buttons
container.querySelectorAll('[data-ct-sync]').forEach(btn => {
btn.addEventListener('click', async () => {
const src = btn.dataset.ctSync, id = parseInt(btn.dataset.ctId);
const label = btn.textContent;
btn.disabled = true; btn.textContent = '…';
try {
if (src === 'caldav') {
await api.post(`/caldav/accounts/${id}/sync`);
} else if (src === 'google') {
const updated = await api.post(`/google/accounts/${id}/sync`);
const idx = state.googleAccounts.findIndex(a => a.id === updated.id);
if (idx !== -1) state.googleAccounts[idx] = updated;
} else if (src === 'ha') {
const updated = await api.post(`/homeassistant/accounts/${id}/sync`);
const idx = state.haAccounts.findIndex(a => a.id === updated.id);
if (idx !== -1) state.haAccounts[idx] = updated;
}
renderCalendarTable(); renderCalendarList(); fetchAndRender(true);
showToast(t('google_synced'));
} catch (e) { showToast(e.message, true); }
finally { btn.disabled = false; btn.textContent = label; }
});
});
// Disconnect buttons
container.querySelectorAll('[data-ct-disc]').forEach(btn => {
btn.addEventListener('click', async () => {
const src = btn.dataset.ctDisc, id = parseInt(btn.dataset.ctId);
const msg = src === 'caldav' ? t('confirm_caldav_disconnect')
: src === 'google' ? t('confirm_google_disconnect')
: 'Konto wirklich trennen?';
if (!confirm(msg)) return;
try {
if (src === 'caldav') {
await api.delete(`/caldav/accounts/${id}`);
state.accounts = state.accounts.filter(a => a.id !== id);
} else if (src === 'google') {
await api.delete(`/google/accounts/${id}`);
state.googleAccounts = state.googleAccounts.filter(a => a.id !== id);
} else if (src === 'ha') {
await api.delete(`/homeassistant/accounts/${id}`);
state.haAccounts = state.haAccounts.filter(a => a.id !== id);
}
renderCalendarTable(); renderCalendarList(); fetchAndRender(true);
showToast(t('caldav_disconnected'));
} catch (e) { showToast(e.message, true); }
});
});
// Export / Import
container.querySelectorAll('[data-ct-export]').forEach(btn => {
btn.addEventListener('click', async () => {
try { await api.download(`/local/calendars/${btn.dataset.ctId}/export`, `${btn.dataset.ctName || 'calendar'}.ics`); }
catch (e) { showToast(e.message, true); }
});
});
container.querySelectorAll('[data-ct-import]').forEach(btn => {
btn.addEventListener('click', () => triggerIcsImport(parseInt(btn.dataset.ctId)));
});
// Delete
container.querySelectorAll('[data-ct-del]').forEach(btn => {
btn.addEventListener('click', async () => {
const src = btn.dataset.ctDel, id = parseInt(btn.dataset.ctId);
const msg = src === 'local' ? t('confirm_delete_local_cal') : t('confirm_remove_ical');
if (!confirm(msg)) return;
try {
if (src === 'local') {
await api.delete(`/local/calendars/${id}`);
state.localCalendars = state.localCalendars.filter(c => c.id !== id);
} else {
await api.delete(`/ical/subscriptions/${id}`);
state.icalSubscriptions = state.icalSubscriptions.filter(s => s.id !== id);
}
renderCalendarTable(); renderCalendarList(); fetchAndRender(true);
} catch (e) { showToast(e.message, true); }
});
});
}
async function loadUsers() { async function loadUsers() {
try { try {
const users = await api.get('/users/'); const users = await api.get('/users/');
@@ -3515,7 +3787,7 @@ function bindSettingsModal() {
line_color: colourOrNull('cfg-line-color-hex'), line_color: colourOrNull('cfg-line-color-hex'),
bg_color: colourOrNull('cfg-bg-color-hex'), bg_color: colourOrNull('cfg-bg-color-hex'),
dim_past_events: document.getElementById('cfg-dim-past').checked, dim_past_events: document.getElementById('cfg-dim-past').checked,
default_event_duration_minutes: parseInt(document.getElementById('cfg-default-duration').value, 10) || 60, default_event_duration_minutes: getActive('cfg-event-duration') || 60,
hour_height: getActive('cfg-hour-height') || 44, hour_height: getActive('cfg-hour-height') || 44,
language: document.getElementById('cfg-language').value, language: document.getElementById('cfg-language').value,
private_event_visibility: document.getElementById('cfg-private-visibility').value, private_event_visibility: document.getElementById('cfg-private-visibility').value,
@@ -3535,6 +3807,22 @@ function bindSettingsModal() {
fetchAndRender(); fetchAndRender();
} catch (e) { showToast(e.message, true); } } catch (e) { showToast(e.message, true); }
}; };
// Add-account buttons in calendar panel
const addBtnMap = {
'settings-btn-add-local': openLocalCalModal,
'settings-btn-add-caldav': openAccountModal,
'settings-btn-add-ical': openICalSubModal,
'settings-btn-add-ha': openHAAccountModal,
'settings-btn-add-google': async () => {
const url = await api.get('/google/auth/url');
if (url && url.auth_url) window.open(url.auth_url, '_blank');
},
};
Object.entries(addBtnMap).forEach(([id, fn]) => {
const btn = document.getElementById(id);
if (btn) btn.addEventListener('click', () => { closeModal('modal-settings'); fn(); });
});
} }
// ── Profile Modal ───────────────────────────────────────── // ── Profile Modal ─────────────────────────────────────────

View File

@@ -1,2 +1,2 @@
// Increment APP_VERSION with every code change // Increment APP_VERSION with every code change
export const APP_VERSION = 'v51'; export const APP_VERSION = 'v52';