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:
@@ -154,3 +154,19 @@ class GoogleAccount(Base):
|
||||
token_expiry = Column(DateTime, nullable=True)
|
||||
|
||||
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")
|
||||
|
||||
@@ -5,7 +5,7 @@ from typing import Optional
|
||||
from urllib.parse import urlencode
|
||||
|
||||
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 pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session
|
||||
@@ -98,7 +98,7 @@ def _build_event_body(data: dict) -> dict:
|
||||
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."""
|
||||
start = ev.get("start", {})
|
||||
end = ev.get("end", {})
|
||||
@@ -106,7 +106,7 @@ def _parse_google_event(ev: dict, account_id: int, cal_name: str, cal_color: str
|
||||
|
||||
return {
|
||||
"id": ev["id"],
|
||||
"url": f"google://{account_id}/{ev['id']}",
|
||||
"url": f"google://{gcal_db_id}/{ev['id']}",
|
||||
"title": ev.get("summary", "(Kein Titel)"),
|
||||
"start": start.get("date") or start.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", ""),
|
||||
"description": ev.get("description", ""),
|
||||
"color": None,
|
||||
"calendar_id": f"google-{account_id}",
|
||||
"calendar_id": f"google-{gcal_db_id}",
|
||||
"calendar_name": cal_name,
|
||||
"calendarColor": cal_color,
|
||||
"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 ──────────────────────────────────────────
|
||||
|
||||
@router.get("/configured")
|
||||
@@ -198,8 +242,10 @@ def oauth_callback(
|
||||
existing.token_expiry = datetime.fromtimestamp(
|
||||
datetime.now(timezone.utc).timestamp() + expires_in, tz=timezone.utc
|
||||
)
|
||||
db.commit()
|
||||
account = existing
|
||||
else:
|
||||
db.add(models.GoogleAccount(
|
||||
account = models.GoogleAccount(
|
||||
user_id=user_id,
|
||||
email=email,
|
||||
access_token=access_token,
|
||||
@@ -207,10 +253,14 @@ def oauth_callback(
|
||||
token_expiry=datetime.fromtimestamp(
|
||||
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)
|
||||
|
||||
|
||||
@@ -226,7 +276,11 @@ def list_accounts(
|
||||
.filter(models.GoogleAccount.user_id == current_user.id)
|
||||
.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}")
|
||||
@@ -247,10 +301,64 @@ def delete_account(
|
||||
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 ───────────────────────────────────────────────
|
||||
|
||||
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:
|
||||
token = _refresh_access_token(account, db)
|
||||
except Exception:
|
||||
@@ -258,15 +366,10 @@ def get_google_events(account: models.GoogleAccount, start_dt: datetime, end_dt:
|
||||
|
||||
all_events = []
|
||||
try:
|
||||
cal_list = _google_api(token, "/users/me/calendarList")
|
||||
for cal in cal_list.get("items", []):
|
||||
if cal.get("deleted"):
|
||||
for gcal in account.calendars:
|
||||
if not gcal.enabled:
|
||||
continue
|
||||
cal_id = cal["id"]
|
||||
cal_name = cal.get("summary", cal_id)
|
||||
cal_color = cal.get("backgroundColor", "#4285f4")
|
||||
|
||||
events_resp = _google_api(token, f"/calendars/{cal_id}/events", params={
|
||||
events_resp = _google_api(token, f"/calendars/{gcal.cal_id}/events", params={
|
||||
"timeMin": start_dt.isoformat(),
|
||||
"timeMax": end_dt.isoformat(),
|
||||
"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", []):
|
||||
if ev.get("status") == "cancelled":
|
||||
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:
|
||||
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):
|
||||
account_id: int
|
||||
calendar_db_id: int
|
||||
title: str
|
||||
start: str
|
||||
end: str
|
||||
@@ -307,57 +410,69 @@ def create_event(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: models.User = Depends(get_current_user),
|
||||
):
|
||||
acc = (
|
||||
db.query(models.GoogleAccount)
|
||||
.filter(models.GoogleAccount.id == data.account_id, models.GoogleAccount.user_id == current_user.id)
|
||||
gcal = (
|
||||
db.query(models.GoogleCalendar)
|
||||
.join(models.GoogleAccount)
|
||||
.filter(
|
||||
models.GoogleCalendar.id == data.calendar_db_id,
|
||||
models.GoogleAccount.user_id == current_user.id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if not acc:
|
||||
raise HTTPException(404, "Google account not found")
|
||||
if not gcal:
|
||||
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())
|
||||
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}
|
||||
|
||||
|
||||
@router.put("/events/{account_id}/{event_id}")
|
||||
@router.put("/events/{gcal_db_id}/{event_id}")
|
||||
def update_event(
|
||||
account_id: int,
|
||||
gcal_db_id: int,
|
||||
event_id: str,
|
||||
data: GoogleEventUpdate,
|
||||
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)
|
||||
gcal = (
|
||||
db.query(models.GoogleCalendar)
|
||||
.join(models.GoogleAccount)
|
||||
.filter(
|
||||
models.GoogleCalendar.id == gcal_db_id,
|
||||
models.GoogleAccount.user_id == current_user.id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if not acc:
|
||||
raise HTTPException(404, "Google account not found")
|
||||
if not gcal:
|
||||
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))
|
||||
_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}
|
||||
|
||||
|
||||
@router.delete("/events/{account_id}/{event_id}")
|
||||
@router.delete("/events/{gcal_db_id}/{event_id}")
|
||||
def delete_event(
|
||||
account_id: int,
|
||||
gcal_db_id: int,
|
||||
event_id: str,
|
||||
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)
|
||||
gcal = (
|
||||
db.query(models.GoogleCalendar)
|
||||
.join(models.GoogleAccount)
|
||||
.filter(
|
||||
models.GoogleCalendar.id == gcal_db_id,
|
||||
models.GoogleAccount.user_id == current_user.id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if not acc:
|
||||
raise HTTPException(404, "Google account not found")
|
||||
if not gcal:
|
||||
raise HTTPException(404, "Google calendar not found")
|
||||
|
||||
token = _refresh_access_token(acc, db)
|
||||
_google_api(token, f"/calendars/primary/events/{event_id}", method="DELETE")
|
||||
token = _refresh_access_token(gcal.account, db)
|
||||
_google_api(token, f"/calendars/{gcal.cal_id}/events/{event_id}", method="DELETE")
|
||||
return {"ok": True}
|
||||
|
||||
@@ -428,6 +428,11 @@
|
||||
</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">
|
||||
<h4>Ausgeblendete Kalender</h4>
|
||||
<div id="hidden-cals-list"><span style="font-size:13px;color:var(--text-3)">Keine ausgeblendeten Kalender</span></div>
|
||||
|
||||
@@ -310,16 +310,21 @@ function renderCalendarList() {
|
||||
|
||||
// ── Google accounts ───────────────────────────────────
|
||||
if (state.googleAccounts.length) {
|
||||
html += `<div class="cal-account-name">Google Kalender</div>`;
|
||||
html += state.googleAccounts.map(acc =>
|
||||
`<div class="cal-item" data-acc-id="${acc.id}" data-source="google">
|
||||
<div class="cal-item-dot" style="background:#4285f4"></div>
|
||||
<span class="cal-item-name" data-source="google">${escHtml(acc.email)}</span>
|
||||
<button class="icon-btn mini-btn cal-item-remove" data-acc-id="${acc.id}" data-source="google" title="Google-Konto entfernen">
|
||||
<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>
|
||||
</button>
|
||||
</div>`
|
||||
).join('');
|
||||
html += state.googleAccounts.map(acc => {
|
||||
const visibleCals = acc.calendars.filter(c => !c._hidden);
|
||||
if (!visibleCals.length) return `<div class="cal-account-name">${escHtml(acc.email)}</div>`;
|
||||
return `<div class="cal-account-name">${escHtml(acc.email)}</div>` +
|
||||
visibleCals.map(cal =>
|
||||
`<div class="cal-item" data-cal-id="${cal.id}" data-source="google">
|
||||
<input type="checkbox" ${cal.enabled ? 'checked' : ''} data-cal-id="${cal.id}" data-source="google" />
|
||||
<div class="cal-item-dot" style="background:${cal.color || '#4285f4'}" data-cal-id="${cal.id}" data-source="google" title="Farbe ändern"></div>
|
||||
<span class="cal-item-name" data-source="google">${escHtml(cal.name)}</span>
|
||||
<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) {
|
||||
@@ -351,6 +356,13 @@ function renderCalendarList() {
|
||||
await api.put(`/ical/subscriptions/${subId}`, { enabled: cb.checked });
|
||||
const sub = state.icalSubscriptions.find(s => s.id === subId);
|
||||
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();
|
||||
});
|
||||
@@ -383,6 +395,20 @@ function renderCalendarList() {
|
||||
renderCalendarList();
|
||||
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}`);
|
||||
state.icalSubscriptions = state.icalSubscriptions.filter(s => s.id !== subId);
|
||||
} else if (source === 'google') {
|
||||
if (!confirm('Google-Konto wirklich entfernen?')) return;
|
||||
const accId = parseInt(btn.dataset.accId);
|
||||
await api.delete(`/google/accounts/${accId}`);
|
||||
state.googleAccounts = state.googleAccounts.filter(a => a.id !== accId);
|
||||
const calId = parseInt(btn.dataset.calId);
|
||||
await api.put(`/google/calendars/${calId}`, { enabled: false });
|
||||
for (const acc of state.googleAccounts) {
|
||||
for (const cal of acc.calendars) {
|
||||
if (cal.id === calId) { cal.enabled = false; cal._hidden = true; }
|
||||
}
|
||||
}
|
||||
}
|
||||
renderCalendarList();
|
||||
fetchAndRender();
|
||||
@@ -650,11 +679,13 @@ function populateCalendarSelect(selectedId) {
|
||||
// iCal subscriptions are read-only, not shown here
|
||||
// Google calendars (read/write)
|
||||
state.googleAccounts.forEach(acc => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = `google-${acc.id}`;
|
||||
opt.textContent = `Google / ${acc.email}`;
|
||||
if (`google-${acc.id}` === selectedId) opt.selected = true;
|
||||
sel.appendChild(opt);
|
||||
acc.calendars.filter(c => c.enabled).forEach(cal => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = `google-${cal.id}`;
|
||||
opt.textContent = `${acc.email} / ${cal.name}`;
|
||||
if (`google-${cal.id}` === selectedId) opt.selected = true;
|
||||
sel.appendChild(opt);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -798,9 +829,9 @@ function bindEventModal() {
|
||||
}
|
||||
showToast('Termin aktualisiert');
|
||||
} else if (isGoogle) {
|
||||
const accId = parseInt(calVal.replace('google-', ''));
|
||||
const calDbId = parseInt(calVal.replace('google-', ''));
|
||||
await api.post('/google/events', {
|
||||
account_id: accId, title, start, end, allDay,
|
||||
calendar_db_id: calDbId, title, start, end, allDay,
|
||||
location: loc, description: desc,
|
||||
});
|
||||
showToast('Termin erstellt');
|
||||
@@ -1022,18 +1053,72 @@ function openSettingsModal() {
|
||||
usersSection.classList.add('hidden');
|
||||
}
|
||||
|
||||
// Render Google accounts
|
||||
renderGoogleAccounts();
|
||||
|
||||
// Render hidden calendars
|
||||
renderHiddenCalendars();
|
||||
|
||||
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() {
|
||||
const list = document.getElementById('hidden-cals-list');
|
||||
const hidden = [];
|
||||
for (const acc of state.accounts) {
|
||||
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) {
|
||||
@@ -1043,16 +1128,26 @@ function renderHiddenCalendars() {
|
||||
list.innerHTML = hidden.map(c =>
|
||||
`<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>
|
||||
<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>`
|
||||
).join('');
|
||||
list.querySelectorAll('[data-restore-cal]').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
const calId = parseInt(btn.dataset.restoreCal);
|
||||
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; }
|
||||
const source = btn.dataset.restoreSource;
|
||||
if (source === 'google') {
|
||||
await api.put(`/google/calendars/${calId}`, { enabled: true });
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user