Google Kalender: individuelle Kalender in Sidebar anzeigen wie bei CalDAV

- GoogleCalendar-Modell hinzugefügt (pro Account, mit enabled/color/name)
- Kalender werden nach OAuth automatisch synchronisiert
- Sidebar zeigt individuelle Google-Kalender mit Checkbox, Farbpunkt und Ausblenden-Button
- Einstellungen: Google-Konten-Bereich mit Sync- und Trennen-Button
- Ausgeblendete Kalender-Liste zeigt auch Google-Kalender
- Event-Erstellung/Bearbeitung/Löschung nutzt GoogleCalendar-ID statt Account-ID
This commit is contained in:
2026-03-27 09:45:10 +01:00
parent 21d8ddfb7c
commit b867554e23
4 changed files with 303 additions and 72 deletions

View File

@@ -154,3 +154,19 @@ class GoogleAccount(Base):
token_expiry = Column(DateTime, nullable=True) token_expiry = Column(DateTime, nullable=True)
user = relationship("User", back_populates="google_accounts") user = relationship("User", back_populates="google_accounts")
calendars = relationship(
"GoogleCalendar", back_populates="account", cascade="all, delete-orphan"
)
class GoogleCalendar(Base):
__tablename__ = "google_calendars"
id = Column(Integer, primary_key=True, index=True)
account_id = Column(Integer, ForeignKey("google_accounts.id"), nullable=False)
cal_id = Column(String(500), nullable=False)
name = Column(String(255), nullable=False)
color = Column(String(7), nullable=True)
enabled = Column(Boolean, default=True)
account = relationship("GoogleAccount", back_populates="calendars")

View File

@@ -5,7 +5,7 @@ from typing import Optional
from urllib.parse import urlencode from urllib.parse import urlencode
import requests as http_requests import requests as http_requests
from fastapi import APIRouter, Depends, HTTPException, Query, Request from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi.responses import RedirectResponse from fastapi.responses import RedirectResponse
from pydantic import BaseModel from pydantic import BaseModel
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
@@ -98,7 +98,7 @@ def _build_event_body(data: dict) -> dict:
return body return body
def _parse_google_event(ev: dict, account_id: int, cal_name: str, cal_color: str) -> dict: def _parse_google_event(ev: dict, gcal_db_id: int, cal_name: str, cal_color: str) -> dict:
"""Convert Google Calendar event to our format.""" """Convert Google Calendar event to our format."""
start = ev.get("start", {}) start = ev.get("start", {})
end = ev.get("end", {}) end = ev.get("end", {})
@@ -106,7 +106,7 @@ def _parse_google_event(ev: dict, account_id: int, cal_name: str, cal_color: str
return { return {
"id": ev["id"], "id": ev["id"],
"url": f"google://{account_id}/{ev['id']}", "url": f"google://{gcal_db_id}/{ev['id']}",
"title": ev.get("summary", "(Kein Titel)"), "title": ev.get("summary", "(Kein Titel)"),
"start": start.get("date") or start.get("dateTime", ""), "start": start.get("date") or start.get("dateTime", ""),
"end": end.get("date") or end.get("dateTime", ""), "end": end.get("date") or end.get("dateTime", ""),
@@ -114,13 +114,57 @@ def _parse_google_event(ev: dict, account_id: int, cal_name: str, cal_color: str
"location": ev.get("location", ""), "location": ev.get("location", ""),
"description": ev.get("description", ""), "description": ev.get("description", ""),
"color": None, "color": None,
"calendar_id": f"google-{account_id}", "calendar_id": f"google-{gcal_db_id}",
"calendar_name": cal_name, "calendar_name": cal_name,
"calendarColor": cal_color, "calendarColor": cal_color,
"source": "google", "source": "google",
} }
def _account_dict(a: models.GoogleAccount) -> dict:
return {
"id": a.id,
"email": a.email,
"calendars": [
{
"id": c.id,
"name": c.name,
"color": c.color or "#4285f4",
"enabled": c.enabled,
}
for c in a.calendars
],
}
def _sync_google_calendars(account: models.GoogleAccount, db: Session):
"""Fetch calendar list from Google and persist/update GoogleCalendar records."""
try:
token = _refresh_access_token(account, db)
cal_list = _google_api(token, "/users/me/calendarList")
existing = {c.cal_id: c for c in account.calendars}
for cal in cal_list.get("items", []):
if cal.get("deleted"):
continue
cal_id = cal["id"]
if cal_id not in existing:
db.add(models.GoogleCalendar(
account_id=account.id,
cal_id=cal_id,
name=cal.get("summary", cal_id),
color=cal.get("backgroundColor", "#4285f4"),
enabled=True,
))
else:
existing[cal_id].name = cal.get("summary", cal_id)
if not existing[cal_id].color:
existing[cal_id].color = cal.get("backgroundColor", "#4285f4")
db.commit()
db.refresh(account)
except Exception as exc:
logger.error("Error syncing Google calendars for %s: %s", account.email, exc)
# ── OAuth2 Flow ────────────────────────────────────────── # ── OAuth2 Flow ──────────────────────────────────────────
@router.get("/configured") @router.get("/configured")
@@ -198,8 +242,10 @@ def oauth_callback(
existing.token_expiry = datetime.fromtimestamp( existing.token_expiry = datetime.fromtimestamp(
datetime.now(timezone.utc).timestamp() + expires_in, tz=timezone.utc datetime.now(timezone.utc).timestamp() + expires_in, tz=timezone.utc
) )
db.commit()
account = existing
else: else:
db.add(models.GoogleAccount( account = models.GoogleAccount(
user_id=user_id, user_id=user_id,
email=email, email=email,
access_token=access_token, access_token=access_token,
@@ -207,10 +253,14 @@ def oauth_callback(
token_expiry=datetime.fromtimestamp( token_expiry=datetime.fromtimestamp(
datetime.now(timezone.utc).timestamp() + expires_in, tz=timezone.utc datetime.now(timezone.utc).timestamp() + expires_in, tz=timezone.utc
), ),
)) )
db.commit() db.add(account)
db.commit()
db.refresh(account)
# Sync calendars after connecting/reconnecting
_sync_google_calendars(account, db)
# Redirect back to app
return RedirectResponse(url="/", status_code=302) return RedirectResponse(url="/", status_code=302)
@@ -226,7 +276,11 @@ def list_accounts(
.filter(models.GoogleAccount.user_id == current_user.id) .filter(models.GoogleAccount.user_id == current_user.id)
.all() .all()
) )
return [{"id": a.id, "email": a.email} for a in accounts] # Auto-sync calendars for accounts that have none yet
for acc in accounts:
if not acc.calendars:
_sync_google_calendars(acc, db)
return [_account_dict(a) for a in accounts]
@router.delete("/accounts/{account_id}") @router.delete("/accounts/{account_id}")
@@ -247,10 +301,64 @@ def delete_account(
return {"ok": True} return {"ok": True}
@router.post("/accounts/{account_id}/sync")
def sync_account(
account_id: int,
db: Session = Depends(get_db),
current_user: models.User = Depends(get_current_user),
):
acc = (
db.query(models.GoogleAccount)
.filter(models.GoogleAccount.id == account_id, models.GoogleAccount.user_id == current_user.id)
.first()
)
if not acc:
raise HTTPException(404, "Account not found")
_sync_google_calendars(acc, db)
db.refresh(acc)
return _account_dict(acc)
# ── Calendar Management ──────────────────────────────────
class GoogleCalendarUpdate(BaseModel):
enabled: Optional[bool] = None
color: Optional[str] = None
name: Optional[str] = None
@router.put("/calendars/{calendar_id}")
def update_calendar(
calendar_id: int,
data: GoogleCalendarUpdate,
db: Session = Depends(get_db),
current_user: models.User = Depends(get_current_user),
):
gcal = (
db.query(models.GoogleCalendar)
.join(models.GoogleAccount)
.filter(
models.GoogleCalendar.id == calendar_id,
models.GoogleAccount.user_id == current_user.id,
)
.first()
)
if not gcal:
raise HTTPException(404, "Calendar not found")
if data.enabled is not None:
gcal.enabled = data.enabled
if data.color is not None:
gcal.color = data.color
if data.name is not None:
gcal.name = data.name
db.commit()
return {"ok": True}
# ── Events ─────────────────────────────────────────────── # ── Events ───────────────────────────────────────────────
def get_google_events(account: models.GoogleAccount, start_dt: datetime, end_dt: datetime, db: Session) -> list: def get_google_events(account: models.GoogleAccount, start_dt: datetime, end_dt: datetime, db: Session) -> list:
"""Fetch events from all calendars in a Google account.""" """Fetch events from all enabled Google calendars for an account."""
try: try:
token = _refresh_access_token(account, db) token = _refresh_access_token(account, db)
except Exception: except Exception:
@@ -258,15 +366,10 @@ def get_google_events(account: models.GoogleAccount, start_dt: datetime, end_dt:
all_events = [] all_events = []
try: try:
cal_list = _google_api(token, "/users/me/calendarList") for gcal in account.calendars:
for cal in cal_list.get("items", []): if not gcal.enabled:
if cal.get("deleted"):
continue continue
cal_id = cal["id"] events_resp = _google_api(token, f"/calendars/{gcal.cal_id}/events", params={
cal_name = cal.get("summary", cal_id)
cal_color = cal.get("backgroundColor", "#4285f4")
events_resp = _google_api(token, f"/calendars/{cal_id}/events", params={
"timeMin": start_dt.isoformat(), "timeMin": start_dt.isoformat(),
"timeMax": end_dt.isoformat(), "timeMax": end_dt.isoformat(),
"singleEvents": "true", "singleEvents": "true",
@@ -275,7 +378,7 @@ def get_google_events(account: models.GoogleAccount, start_dt: datetime, end_dt:
for ev in events_resp.get("items", []): for ev in events_resp.get("items", []):
if ev.get("status") == "cancelled": if ev.get("status") == "cancelled":
continue continue
all_events.append(_parse_google_event(ev, account.id, cal_name, cal_color)) all_events.append(_parse_google_event(ev, gcal.id, gcal.name, gcal.color or "#4285f4"))
except Exception as exc: except Exception as exc:
logger.error("Error fetching Google Calendar for %s: %s", account.email, exc) logger.error("Error fetching Google Calendar for %s: %s", account.email, exc)
@@ -283,7 +386,7 @@ def get_google_events(account: models.GoogleAccount, start_dt: datetime, end_dt:
class GoogleEventCreate(BaseModel): class GoogleEventCreate(BaseModel):
account_id: int calendar_db_id: int
title: str title: str
start: str start: str
end: str end: str
@@ -307,57 +410,69 @@ def create_event(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: models.User = Depends(get_current_user), current_user: models.User = Depends(get_current_user),
): ):
acc = ( gcal = (
db.query(models.GoogleAccount) db.query(models.GoogleCalendar)
.filter(models.GoogleAccount.id == data.account_id, models.GoogleAccount.user_id == current_user.id) .join(models.GoogleAccount)
.filter(
models.GoogleCalendar.id == data.calendar_db_id,
models.GoogleAccount.user_id == current_user.id,
)
.first() .first()
) )
if not acc: if not gcal:
raise HTTPException(404, "Google account not found") raise HTTPException(404, "Google calendar not found")
token = _refresh_access_token(acc, db) token = _refresh_access_token(gcal.account, db)
body = _build_event_body(data.model_dump()) body = _build_event_body(data.model_dump())
result = _google_api(token, "/calendars/primary/events", method="POST", json_body=body) result = _google_api(token, f"/calendars/{gcal.cal_id}/events", method="POST", json_body=body)
return {"id": result.get("id"), "ok": True} return {"id": result.get("id"), "ok": True}
@router.put("/events/{account_id}/{event_id}") @router.put("/events/{gcal_db_id}/{event_id}")
def update_event( def update_event(
account_id: int, gcal_db_id: int,
event_id: str, event_id: str,
data: GoogleEventUpdate, data: GoogleEventUpdate,
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: models.User = Depends(get_current_user), current_user: models.User = Depends(get_current_user),
): ):
acc = ( gcal = (
db.query(models.GoogleAccount) db.query(models.GoogleCalendar)
.filter(models.GoogleAccount.id == account_id, models.GoogleAccount.user_id == current_user.id) .join(models.GoogleAccount)
.filter(
models.GoogleCalendar.id == gcal_db_id,
models.GoogleAccount.user_id == current_user.id,
)
.first() .first()
) )
if not acc: if not gcal:
raise HTTPException(404, "Google account not found") raise HTTPException(404, "Google calendar not found")
token = _refresh_access_token(acc, db) token = _refresh_access_token(gcal.account, db)
body = _build_event_body(data.model_dump(exclude_none=True)) body = _build_event_body(data.model_dump(exclude_none=True))
_google_api(token, f"/calendars/primary/events/{event_id}", method="PATCH", json_body=body) _google_api(token, f"/calendars/{gcal.cal_id}/events/{event_id}", method="PATCH", json_body=body)
return {"ok": True} return {"ok": True}
@router.delete("/events/{account_id}/{event_id}") @router.delete("/events/{gcal_db_id}/{event_id}")
def delete_event( def delete_event(
account_id: int, gcal_db_id: int,
event_id: str, event_id: str,
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: models.User = Depends(get_current_user), current_user: models.User = Depends(get_current_user),
): ):
acc = ( gcal = (
db.query(models.GoogleAccount) db.query(models.GoogleCalendar)
.filter(models.GoogleAccount.id == account_id, models.GoogleAccount.user_id == current_user.id) .join(models.GoogleAccount)
.filter(
models.GoogleCalendar.id == gcal_db_id,
models.GoogleAccount.user_id == current_user.id,
)
.first() .first()
) )
if not acc: if not gcal:
raise HTTPException(404, "Google account not found") raise HTTPException(404, "Google calendar not found")
token = _refresh_access_token(acc, db) token = _refresh_access_token(gcal.account, db)
_google_api(token, f"/calendars/primary/events/{event_id}", method="DELETE") _google_api(token, f"/calendars/{gcal.cal_id}/events/{event_id}", method="DELETE")
return {"ok": True} return {"ok": True}

View File

@@ -428,6 +428,11 @@
</div> </div>
</div> </div>
<div class="settings-section" id="settings-google-section">
<h4>Google Konten</h4>
<div id="google-accounts-list"><span style="font-size:13px;color:var(--text-3)">Keine Google-Konten verbunden</span></div>
</div>
<div class="settings-section" id="settings-hidden-cals-section"> <div class="settings-section" id="settings-hidden-cals-section">
<h4>Ausgeblendete Kalender</h4> <h4>Ausgeblendete Kalender</h4>
<div id="hidden-cals-list"><span style="font-size:13px;color:var(--text-3)">Keine ausgeblendeten Kalender</span></div> <div id="hidden-cals-list"><span style="font-size:13px;color:var(--text-3)">Keine ausgeblendeten Kalender</span></div>

View File

@@ -310,16 +310,21 @@ function renderCalendarList() {
// ── Google accounts ─────────────────────────────────── // ── Google accounts ───────────────────────────────────
if (state.googleAccounts.length) { if (state.googleAccounts.length) {
html += `<div class="cal-account-name">Google Kalender</div>`; html += state.googleAccounts.map(acc => {
html += state.googleAccounts.map(acc => const visibleCals = acc.calendars.filter(c => !c._hidden);
`<div class="cal-item" data-acc-id="${acc.id}" data-source="google"> if (!visibleCals.length) return `<div class="cal-account-name">${escHtml(acc.email)}</div>`;
<div class="cal-item-dot" style="background:#4285f4"></div> return `<div class="cal-account-name">${escHtml(acc.email)}</div>` +
<span class="cal-item-name" data-source="google">${escHtml(acc.email)}</span> visibleCals.map(cal =>
<button class="icon-btn mini-btn cal-item-remove" data-acc-id="${acc.id}" data-source="google" title="Google-Konto entfernen"> `<div class="cal-item" data-cal-id="${cal.id}" data-source="google">
<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> <input type="checkbox" ${cal.enabled ? 'checked' : ''} data-cal-id="${cal.id}" data-source="google" />
</button> <div class="cal-item-dot" style="background:${cal.color || '#4285f4'}" data-cal-id="${cal.id}" data-source="google" title="Farbe ändern"></div>
</div>` <span class="cal-item-name" data-source="google">${escHtml(cal.name)}</span>
).join(''); <button class="icon-btn mini-btn cal-item-remove" data-cal-id="${cal.id}" data-source="google" title="Kalender ausblenden">
<svg viewBox="0 0 24 24" fill="currentColor" width="14" height="14"><path d="M12 7c2.76 0 5 2.24 5 5 0 .65-.13 1.26-.36 1.83l2.92 2.92c1.51-1.26 2.7-2.89 3.43-4.75-1.73-4.39-6-7.5-11-7.5-1.4 0-2.74.25-3.98.7l2.16 2.16C10.74 7.13 11.35 7 12 7zM2 4.27l2.28 2.28.46.46C3.08 8.3 1.78 10.02 1 12c1.73 4.39 6 7.5 11 7.5 1.55 0 3.03-.3 4.38-.84l.42.42L19.73 22 21 20.73 3.27 3 2 4.27zM7.53 9.8l1.55 1.55c-.05.21-.08.43-.08.65 0 1.66 1.34 3 3 3 .22 0 .44-.03.65-.08l1.55 1.55c-.67.33-1.41.53-2.2.53-2.76 0-5-2.24-5-5 0-.79.2-1.53.53-2.2zm4.31-.78l3.15 3.15.02-.16c0-1.66-1.34-3-3-3l-.17.01z"/></svg>
</button>
</div>`
).join('');
}).join('');
} }
if (!html) { if (!html) {
@@ -351,6 +356,13 @@ function renderCalendarList() {
await api.put(`/ical/subscriptions/${subId}`, { enabled: cb.checked }); await api.put(`/ical/subscriptions/${subId}`, { enabled: cb.checked });
const sub = state.icalSubscriptions.find(s => s.id === subId); const sub = state.icalSubscriptions.find(s => s.id === subId);
if (sub) sub.enabled = cb.checked; if (sub) sub.enabled = cb.checked;
} else if (source === 'google') {
const calId = parseInt(cb.dataset.calId);
await api.put(`/google/calendars/${calId}`, { enabled: cb.checked });
for (const acc of state.googleAccounts) {
const cal = acc.calendars.find(c => c.id === calId);
if (cal) cal.enabled = cb.checked;
}
} }
fetchAndRender(); fetchAndRender();
}); });
@@ -383,6 +395,20 @@ function renderCalendarList() {
renderCalendarList(); renderCalendarList();
fetchAndRender(); fetchAndRender();
} }
} else if (source === 'google') {
const calId = parseInt(dot.dataset.calId);
let gcal = null;
for (const acc of state.googleAccounts) {
gcal = acc.calendars.find(c => c.id === calId);
if (gcal) break;
}
const picked = await openColorPicker(dot, gcal?.color || '#4285f4');
if (picked) {
await api.put(`/google/calendars/${calId}`, { color: picked });
if (gcal) gcal.color = picked;
renderCalendarList();
fetchAndRender();
}
} }
}); });
}); });
@@ -458,10 +484,13 @@ function renderCalendarList() {
await api.delete(`/ical/subscriptions/${subId}`); await api.delete(`/ical/subscriptions/${subId}`);
state.icalSubscriptions = state.icalSubscriptions.filter(s => s.id !== subId); state.icalSubscriptions = state.icalSubscriptions.filter(s => s.id !== subId);
} else if (source === 'google') { } else if (source === 'google') {
if (!confirm('Google-Konto wirklich entfernen?')) return; const calId = parseInt(btn.dataset.calId);
const accId = parseInt(btn.dataset.accId); await api.put(`/google/calendars/${calId}`, { enabled: false });
await api.delete(`/google/accounts/${accId}`); for (const acc of state.googleAccounts) {
state.googleAccounts = state.googleAccounts.filter(a => a.id !== accId); for (const cal of acc.calendars) {
if (cal.id === calId) { cal.enabled = false; cal._hidden = true; }
}
}
} }
renderCalendarList(); renderCalendarList();
fetchAndRender(); fetchAndRender();
@@ -650,11 +679,13 @@ function populateCalendarSelect(selectedId) {
// iCal subscriptions are read-only, not shown here // iCal subscriptions are read-only, not shown here
// Google calendars (read/write) // Google calendars (read/write)
state.googleAccounts.forEach(acc => { state.googleAccounts.forEach(acc => {
const opt = document.createElement('option'); acc.calendars.filter(c => c.enabled).forEach(cal => {
opt.value = `google-${acc.id}`; const opt = document.createElement('option');
opt.textContent = `Google / ${acc.email}`; opt.value = `google-${cal.id}`;
if (`google-${acc.id}` === selectedId) opt.selected = true; opt.textContent = `${acc.email} / ${cal.name}`;
sel.appendChild(opt); if (`google-${cal.id}` === selectedId) opt.selected = true;
sel.appendChild(opt);
});
}); });
} }
@@ -798,9 +829,9 @@ function bindEventModal() {
} }
showToast('Termin aktualisiert'); showToast('Termin aktualisiert');
} else if (isGoogle) { } else if (isGoogle) {
const accId = parseInt(calVal.replace('google-', '')); const calDbId = parseInt(calVal.replace('google-', ''));
await api.post('/google/events', { await api.post('/google/events', {
account_id: accId, title, start, end, allDay, calendar_db_id: calDbId, title, start, end, allDay,
location: loc, description: desc, location: loc, description: desc,
}); });
showToast('Termin erstellt'); showToast('Termin erstellt');
@@ -1022,18 +1053,72 @@ function openSettingsModal() {
usersSection.classList.add('hidden'); usersSection.classList.add('hidden');
} }
// Render Google accounts
renderGoogleAccounts();
// Render hidden calendars // Render hidden calendars
renderHiddenCalendars(); renderHiddenCalendars();
openModal('modal-settings'); openModal('modal-settings');
} }
function renderGoogleAccounts() {
const list = document.getElementById('google-accounts-list');
if (!list) return;
if (!state.googleAccounts.length) {
list.innerHTML = '<span style="font-size:13px;color:var(--text-3)">Keine Google-Konten verbunden</span>';
return;
}
list.innerHTML = state.googleAccounts.map(acc =>
`<div style="display:flex;align-items:center;justify-content:space-between;padding:4px 0">
<span style="font-size:13px">${escHtml(acc.email)}</span>
<div style="display:flex;gap:6px">
<button class="btn btn-secondary btn-sm" data-sync-acc="${acc.id}">Sync</button>
<button class="btn btn-ghost btn-sm" data-disconnect-acc="${acc.id}">Trennen</button>
</div>
</div>`
).join('');
list.querySelectorAll('[data-sync-acc]').forEach(btn => {
btn.addEventListener('click', async () => {
btn.disabled = true;
btn.textContent = '…';
try {
const updated = await api.post(`/google/accounts/${btn.dataset.syncAcc}/sync`);
const idx = state.googleAccounts.findIndex(a => a.id === updated.id);
if (idx !== -1) state.googleAccounts[idx] = updated;
renderGoogleAccounts();
renderCalendarList();
fetchAndRender();
showToast('Kalender synchronisiert');
} catch (e) { showToast(e.message, true); }
});
});
list.querySelectorAll('[data-disconnect-acc]').forEach(btn => {
btn.addEventListener('click', async () => {
if (!confirm('Google-Konto wirklich trennen?')) return;
try {
await api.delete(`/google/accounts/${btn.dataset.disconnectAcc}`);
state.googleAccounts = state.googleAccounts.filter(a => a.id !== parseInt(btn.dataset.disconnectAcc));
renderGoogleAccounts();
renderCalendarList();
fetchAndRender();
showToast('Google-Konto getrennt');
} catch (e) { showToast(e.message, true); }
});
});
}
function renderHiddenCalendars() { function renderHiddenCalendars() {
const list = document.getElementById('hidden-cals-list'); const list = document.getElementById('hidden-cals-list');
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) {
if (!cal.enabled || cal._hidden) hidden.push({ id: cal.id, name: cal.name, acc: acc.name }); if (!cal.enabled || cal._hidden) hidden.push({ id: cal.id, name: cal.name, acc: acc.name, source: 'caldav' });
}
}
for (const acc of state.googleAccounts) {
for (const cal of acc.calendars) {
if (!cal.enabled || cal._hidden) hidden.push({ id: cal.id, name: cal.name, acc: acc.email, source: 'google' });
} }
} }
if (!hidden.length) { if (!hidden.length) {
@@ -1043,16 +1128,26 @@ function renderHiddenCalendars() {
list.innerHTML = hidden.map(c => list.innerHTML = hidden.map(c =>
`<div style="display:flex;align-items:center;justify-content:space-between;padding:4px 0"> `<div style="display:flex;align-items:center;justify-content:space-between;padding:4px 0">
<span style="font-size:13px">${escHtml(c.acc)} / ${escHtml(c.name)}</span> <span style="font-size:13px">${escHtml(c.acc)} / ${escHtml(c.name)}</span>
<button class="btn btn-secondary btn-sm" data-restore-cal="${c.id}">Einblenden</button> <button class="btn btn-secondary btn-sm" data-restore-cal="${c.id}" data-restore-source="${c.source}">Einblenden</button>
</div>` </div>`
).join(''); ).join('');
list.querySelectorAll('[data-restore-cal]').forEach(btn => { list.querySelectorAll('[data-restore-cal]').forEach(btn => {
btn.addEventListener('click', async () => { btn.addEventListener('click', async () => {
const calId = parseInt(btn.dataset.restoreCal); const calId = parseInt(btn.dataset.restoreCal);
await api.put(`/caldav/calendars/${calId}`, { enabled: true }); const source = btn.dataset.restoreSource;
for (const acc of state.accounts) { if (source === 'google') {
for (const cal of acc.calendars) { await api.put(`/google/calendars/${calId}`, { enabled: true });
if (cal.id === calId) { cal.enabled = true; delete cal._hidden; } for (const acc of state.googleAccounts) {
for (const cal of acc.calendars) {
if (cal.id === calId) { cal.enabled = true; delete cal._hidden; }
}
}
} else {
await api.put(`/caldav/calendars/${calId}`, { enabled: true });
for (const acc of state.accounts) {
for (const cal of acc.calendars) {
if (cal.id === calId) { cal.enabled = true; delete cal._hidden; }
}
} }
} }
renderHiddenCalendars(); renderHiddenCalendars();