Lokale Kalender und iCal-URL-Abonnements

Neue Features:
- Lokale Kalender erstellen mit vollem Event-CRUD (in SQLite gespeichert)
- iCal-URLs abonnieren mit Auto-Refresh und lokalem Caching
- iCal-Events sind editierbar/löschbar (Änderungen als lokale Overrides)
- Sidebar zeigt alle 3 Kalendertypen mit Farbe, Umbenennen, Löschen
- Dropdown "Kalender hinzufügen" mit 3 Optionen (Lokal, CalDAV, iCal)
Backend: models.py (4 neue Tabellen), local_router.py, ical_router.py
Frontend: Neue Modals, erweiterte Sidebar, Source-basiertes Event-Routing
This commit is contained in:
2026-03-27 07:39:41 +01:00
parent b2bc107d47
commit cd46b45ec6
8 changed files with 1129 additions and 45 deletions

View File

@@ -0,0 +1,245 @@
import uuid
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from sqlalchemy.orm import Session
import models
from auth import get_current_user
from database import get_db
router = APIRouter()
class CalendarCreate(BaseModel):
name: str
color: str = "#34a853"
class CalendarUpdate(BaseModel):
name: Optional[str] = None
color: Optional[str] = None
enabled: Optional[bool] = None
class EventCreate(BaseModel):
calendar_id: int
title: str
start: str
end: str
allDay: bool = False
location: Optional[str] = None
description: Optional[str] = None
color: Optional[str] = None
class EventUpdate(BaseModel):
title: Optional[str] = None
start: Optional[str] = None
end: Optional[str] = None
allDay: Optional[bool] = None
location: Optional[str] = None
description: Optional[str] = None
color: Optional[str] = None
def _cal_dict(cal: models.LocalCalendar) -> dict:
return {
"id": cal.id,
"name": cal.name,
"color": cal.color,
"enabled": cal.enabled,
}
def _event_dict(ev: models.LocalEvent, cal: models.LocalCalendar) -> dict:
return {
"id": ev.uid,
"url": f"local://{ev.uid}",
"title": ev.title,
"start": ev.start,
"end": ev.end,
"allDay": ev.all_day,
"location": ev.location or "",
"description": ev.description or "",
"color": ev.color,
"calendar_id": f"local-{cal.id}",
"calendar_name": cal.name,
"calendarColor": cal.color,
"source": "local",
}
# ── Calendar CRUD ─────────────────────────────────────────
@router.get("/calendars")
def list_calendars(
db: Session = Depends(get_db),
current_user: models.User = Depends(get_current_user),
):
cals = (
db.query(models.LocalCalendar)
.filter(models.LocalCalendar.user_id == current_user.id)
.all()
)
return [_cal_dict(c) for c in cals]
@router.post("/calendars")
def create_calendar(
data: CalendarCreate,
db: Session = Depends(get_db),
current_user: models.User = Depends(get_current_user),
):
cal = models.LocalCalendar(
user_id=current_user.id,
name=data.name,
color=data.color,
)
db.add(cal)
db.commit()
db.refresh(cal)
return _cal_dict(cal)
@router.put("/calendars/{calendar_id}")
def update_calendar(
calendar_id: int,
data: CalendarUpdate,
db: Session = Depends(get_db),
current_user: models.User = Depends(get_current_user),
):
cal = (
db.query(models.LocalCalendar)
.filter(
models.LocalCalendar.id == calendar_id,
models.LocalCalendar.user_id == current_user.id,
)
.first()
)
if not cal:
raise HTTPException(404, "Calendar not found")
if data.name is not None:
cal.name = data.name
if data.color is not None:
cal.color = data.color
if data.enabled is not None:
cal.enabled = data.enabled
db.commit()
return {"ok": True}
@router.delete("/calendars/{calendar_id}")
def delete_calendar(
calendar_id: int,
db: Session = Depends(get_db),
current_user: models.User = Depends(get_current_user),
):
cal = (
db.query(models.LocalCalendar)
.filter(
models.LocalCalendar.id == calendar_id,
models.LocalCalendar.user_id == current_user.id,
)
.first()
)
if not cal:
raise HTTPException(404, "Calendar not found")
db.delete(cal)
db.commit()
return {"ok": True}
# ── Event CRUD ────────────────────────────────────────────
@router.post("/events")
def create_event(
data: EventCreate,
db: Session = Depends(get_db),
current_user: models.User = Depends(get_current_user),
):
cal = (
db.query(models.LocalCalendar)
.filter(
models.LocalCalendar.id == data.calendar_id,
models.LocalCalendar.user_id == current_user.id,
)
.first()
)
if not cal:
raise HTTPException(404, "Calendar not found")
ev = models.LocalEvent(
calendar_id=cal.id,
uid=str(uuid.uuid4()),
title=data.title,
start=data.start,
end=data.end,
all_day=data.allDay,
location=data.location,
description=data.description,
color=data.color,
)
db.add(ev)
db.commit()
db.refresh(ev)
return _event_dict(ev, cal)
@router.put("/events/{uid}")
def update_event(
uid: str,
data: EventUpdate,
db: Session = Depends(get_db),
current_user: models.User = Depends(get_current_user),
):
ev = (
db.query(models.LocalEvent)
.join(models.LocalCalendar)
.filter(
models.LocalEvent.uid == uid,
models.LocalCalendar.user_id == current_user.id,
)
.first()
)
if not ev:
raise HTTPException(404, "Event not found")
if data.title is not None:
ev.title = data.title
if data.start is not None:
ev.start = data.start
if data.end is not None:
ev.end = data.end
if data.allDay is not None:
ev.all_day = data.allDay
if data.location is not None:
ev.location = data.location
if data.description is not None:
ev.description = data.description
if data.color is not None:
ev.color = data.color
db.commit()
return {"ok": True}
@router.delete("/events/{uid}")
def delete_event(
uid: str,
db: Session = Depends(get_db),
current_user: models.User = Depends(get_current_user),
):
ev = (
db.query(models.LocalEvent)
.join(models.LocalCalendar)
.filter(
models.LocalEvent.uid == uid,
models.LocalCalendar.user_id == current_user.id,
)
.first()
)
if not ev:
raise HTTPException(404, "Event not found")
db.delete(ev)
db.commit()
return {"ok": True}