Einige kleine verbesserungen #1
@@ -107,6 +107,8 @@ def _parse_ics(raw: str, event_url: str) -> List[Dict]:
|
|||||||
color = str(component.get("X-CALENDARR-COLOR", "") or "")
|
color = str(component.get("X-CALENDARR-COLOR", "") or "")
|
||||||
rrule_prop = component.get("RRULE")
|
rrule_prop = component.get("RRULE")
|
||||||
rrule_str = rrule_prop.to_ical().decode("utf-8") if rrule_prop else None
|
rrule_str = rrule_prop.to_ical().decode("utf-8") if rrule_prop else None
|
||||||
|
recurrence_id = component.get("RECURRENCE-ID")
|
||||||
|
is_recurring = rrule_str is not None or recurrence_id is not None
|
||||||
|
|
||||||
dtstart_prop = component.get("DTSTART")
|
dtstart_prop = component.get("DTSTART")
|
||||||
dtend_prop = component.get("DTEND")
|
dtend_prop = component.get("DTEND")
|
||||||
@@ -157,6 +159,7 @@ def _parse_ics(raw: str, event_url: str) -> List[Dict]:
|
|||||||
"description": description,
|
"description": description,
|
||||||
"color": color or None,
|
"color": color or None,
|
||||||
"rrule": rrule_str,
|
"rrule": rrule_str,
|
||||||
|
"recurring": is_recurring,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
|||||||
@@ -109,6 +109,45 @@ def _ha_get_events(url: str, token: str, entity_id: str, start_dt: datetime, end
|
|||||||
raise http_requests.exceptions.Timeout(f"HA Timeout für {entity_id}")
|
raise http_requests.exceptions.Timeout(f"HA Timeout für {entity_id}")
|
||||||
|
|
||||||
|
|
||||||
|
def _ha_update_event(url: str, token: str, entity_id: str, uid: str, data: dict):
|
||||||
|
"""Update an event via HA REST API."""
|
||||||
|
body = {}
|
||||||
|
if "title" in data:
|
||||||
|
body["summary"] = data["title"]
|
||||||
|
if "description" in data:
|
||||||
|
body["description"] = data["description"]
|
||||||
|
if "location" in data:
|
||||||
|
body["location"] = data["location"]
|
||||||
|
if "start" in data and "end" in data:
|
||||||
|
if data.get("allDay"):
|
||||||
|
body["start"] = {"date": data["start"][:10]}
|
||||||
|
body["end"] = {"date": data["end"][:10]}
|
||||||
|
else:
|
||||||
|
body["start"] = {"dateTime": data["start"]}
|
||||||
|
body["end"] = {"dateTime": data["end"]}
|
||||||
|
resp = http_requests.put(
|
||||||
|
f"{url.rstrip('/')}/api/calendars/{entity_id}/{uid}",
|
||||||
|
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
|
||||||
|
json=body,
|
||||||
|
timeout=15,
|
||||||
|
verify=False,
|
||||||
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
|
return resp
|
||||||
|
|
||||||
|
|
||||||
|
def _ha_delete_event(url: str, token: str, entity_id: str, uid: str):
|
||||||
|
"""Delete an event via HA REST API."""
|
||||||
|
resp = http_requests.delete(
|
||||||
|
f"{url.rstrip('/')}/api/calendars/{entity_id}/{uid}",
|
||||||
|
headers={"Authorization": f"Bearer {token}"},
|
||||||
|
timeout=15,
|
||||||
|
verify=False,
|
||||||
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
|
return resp
|
||||||
|
|
||||||
|
|
||||||
def _parse_ha_event(ev: dict, cal_db_id: int, cal_name: str, cal_color: str) -> dict:
|
def _parse_ha_event(ev: dict, cal_db_id: int, cal_name: str, cal_color: str) -> dict:
|
||||||
start = ev.get("start", {})
|
start = ev.get("start", {})
|
||||||
end = ev.get("end", {})
|
end = ev.get("end", {})
|
||||||
@@ -442,3 +481,72 @@ def update_calendar(
|
|||||||
cal.sidebar_hidden = data.sidebar_hidden
|
cal.sidebar_hidden = data.sidebar_hidden
|
||||||
db.commit()
|
db.commit()
|
||||||
return {"ok": True}
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
# ── Event CRUD ───────────────────────────────────────────
|
||||||
|
|
||||||
|
class HAEventUpdate(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
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/events/{calendar_id}/{uid}")
|
||||||
|
def update_event(
|
||||||
|
calendar_id: int,
|
||||||
|
uid: str,
|
||||||
|
data: HAEventUpdate,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user: models.User = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
cal = (
|
||||||
|
db.query(models.HomeAssistantCalendar)
|
||||||
|
.join(models.HomeAssistantAccount)
|
||||||
|
.filter(
|
||||||
|
models.HomeAssistantCalendar.id == calendar_id,
|
||||||
|
models.HomeAssistantAccount.user_id == current_user.id,
|
||||||
|
)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if not cal:
|
||||||
|
raise HTTPException(404, "Calendar not found")
|
||||||
|
account = cal.account
|
||||||
|
token = _get_valid_token(account, db)
|
||||||
|
try:
|
||||||
|
_ha_update_event(
|
||||||
|
account.url, token, cal.entity_id, uid,
|
||||||
|
data.model_dump(exclude_none=True),
|
||||||
|
)
|
||||||
|
return {"ok": True}
|
||||||
|
except Exception as exc:
|
||||||
|
raise HTTPException(500, f"HA event update failed: {exc}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/events/{calendar_id}/{uid}")
|
||||||
|
def delete_event(
|
||||||
|
calendar_id: int,
|
||||||
|
uid: str,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user: models.User = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
cal = (
|
||||||
|
db.query(models.HomeAssistantCalendar)
|
||||||
|
.join(models.HomeAssistantAccount)
|
||||||
|
.filter(
|
||||||
|
models.HomeAssistantCalendar.id == calendar_id,
|
||||||
|
models.HomeAssistantAccount.user_id == current_user.id,
|
||||||
|
)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if not cal:
|
||||||
|
raise HTTPException(404, "Calendar not found")
|
||||||
|
account = cal.account
|
||||||
|
token = _get_valid_token(account, db)
|
||||||
|
try:
|
||||||
|
_ha_delete_event(account.url, token, cal.entity_id, uid)
|
||||||
|
return {"ok": True}
|
||||||
|
except Exception as exc:
|
||||||
|
raise HTTPException(500, f"HA event delete failed: {exc}")
|
||||||
|
|||||||
@@ -487,7 +487,8 @@ a { color: var(--primary); text-decoration: none; }
|
|||||||
.month-col:hover { background: var(--bg-hover); }
|
.month-col:hover { background: var(--bg-hover); }
|
||||||
.month-col.today { background: rgba(66,133,244,.08); }
|
.month-col.today { background: rgba(66,133,244,.08); }
|
||||||
.month-col.month-selected { background: var(--primary-dim); }
|
.month-col.month-selected { background: var(--primary-dim); }
|
||||||
.month-col.month-selected .cell-day { background: var(--primary); color: #fff; font-weight: 700; }
|
.month-col.month-selected .cell-day { border: 2px solid var(--primary); color: var(--primary); font-weight: 700; }
|
||||||
|
.month-col.month-selected .cell-day.today { background: var(--today-color); color: #fff; border: none; }
|
||||||
.month-col.other-month .cell-day { color: var(--text-3); }
|
.month-col.other-month .cell-day { color: var(--text-3); }
|
||||||
.cell-day {
|
.cell-day {
|
||||||
font-size: 12px; font-weight: 500; color: var(--text-2);
|
font-size: 12px; font-weight: 500; color: var(--text-2);
|
||||||
|
|||||||
@@ -858,7 +858,7 @@ function bindSidebar() {
|
|||||||
// ── Day Context Menu (month view) ────────────────────────
|
// ── Day Context Menu (month view) ────────────────────────
|
||||||
// ── Delete logic ──────────────────────────────────────────
|
// ── Delete logic ──────────────────────────────────────────
|
||||||
async function deleteEventByScope(ev, scope) {
|
async function deleteEventByScope(ev, scope) {
|
||||||
if (scope === 'all' || !ev.rrule) {
|
if (scope === 'all' || !(ev.rrule || ev.recurring)) {
|
||||||
// Delete the entire event (or non-recurring)
|
// Delete the entire event (or non-recurring)
|
||||||
if (ev.source === 'google') {
|
if (ev.source === 'google') {
|
||||||
const accId = ev.calendar_id.replace('google-', '');
|
const accId = ev.calendar_id.replace('google-', '');
|
||||||
@@ -868,6 +868,9 @@ async function deleteEventByScope(ev, scope) {
|
|||||||
} else if (ev.source === 'ical') {
|
} else if (ev.source === 'ical') {
|
||||||
const subId = ev.calendar_id.replace('ical-', '');
|
const subId = ev.calendar_id.replace('ical-', '');
|
||||||
await api.delete(`/ical/events/${subId}/${encodeURIComponent(ev.id)}`);
|
await api.delete(`/ical/events/${subId}/${encodeURIComponent(ev.id)}`);
|
||||||
|
} else if (ev.source === 'homeassistant') {
|
||||||
|
const haCalId = ev.calendar_id.replace('homeassistant-', '');
|
||||||
|
await api.delete(`/homeassistant/events/${haCalId}/${encodeURIComponent(ev.id)}`);
|
||||||
} else {
|
} else {
|
||||||
await api.delete(`/caldav/events/${encodeURIComponent(ev.id)}?event_url=${encodeURIComponent(ev.url)}&calendar_id=${ev.calendar_id}`);
|
await api.delete(`/caldav/events/${encodeURIComponent(ev.id)}?event_url=${encodeURIComponent(ev.url)}&calendar_id=${ev.calendar_id}`);
|
||||||
}
|
}
|
||||||
@@ -894,7 +897,7 @@ async function deleteEventByScope(ev, scope) {
|
|||||||
function showDeleteConfirm(ev) {
|
function showDeleteConfirm(ev) {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
const modal = document.getElementById('modal-delete-confirm');
|
const modal = document.getElementById('modal-delete-confirm');
|
||||||
const isRecurring = !!(ev.rrule);
|
const isRecurring = !!(ev.rrule || ev.recurring);
|
||||||
|
|
||||||
document.getElementById('delete-confirm-title').textContent = t('confirm_delete_title');
|
document.getElementById('delete-confirm-title').textContent = t('confirm_delete_title');
|
||||||
document.getElementById('delete-confirm-text').textContent = t('confirm_delete_event', { title: ev.title });
|
document.getElementById('delete-confirm-text').textContent = t('confirm_delete_event', { title: ev.title });
|
||||||
@@ -994,8 +997,8 @@ 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';
|
||||||
|
|
||||||
// Hide edit/delete for read-only sources (iCal subscriptions, Home Assistant)
|
// Hide edit/delete for read-only iCal subscription events
|
||||||
const isReadOnly = (ev.source === 'ical' || ev.source === 'homeassistant');
|
const isReadOnly = (ev.source === 'ical');
|
||||||
document.getElementById('popup-edit').style.display = isReadOnly ? 'none' : '';
|
document.getElementById('popup-edit').style.display = isReadOnly ? 'none' : '';
|
||||||
document.getElementById('popup-delete').style.display = isReadOnly ? 'none' : '';
|
document.getElementById('popup-delete').style.display = isReadOnly ? 'none' : '';
|
||||||
|
|
||||||
@@ -1134,7 +1137,7 @@ function openNewEventModal(date) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openEditEventModal(ev) {
|
function openEditEventModal(ev) {
|
||||||
if (ev.source === 'ical' || ev.source === 'homeassistant') { showToast(t('event_readonly'), true); return; }
|
if (ev.source === 'ical') { showToast(t('event_readonly'), true); return; }
|
||||||
state.editingEvent = ev;
|
state.editingEvent = ev;
|
||||||
state.selectedEventColor = ev.color || '';
|
state.selectedEventColor = ev.color || '';
|
||||||
|
|
||||||
@@ -1426,9 +1429,14 @@ function bindEventModal() {
|
|||||||
await api.put(`/local/events/${encodeURIComponent(ev.id)}`,
|
await api.put(`/local/events/${encodeURIComponent(ev.id)}`,
|
||||||
{ title, start, end, allDay, location: loc, description: desc, color: color || null, rrule: rrule || '' }
|
{ title, start, end, allDay, location: loc, description: desc, color: color || null, rrule: rrule || '' }
|
||||||
);
|
);
|
||||||
} else if (ev.source === 'ical' || ev.source === 'homeassistant') {
|
} else if (ev.source === 'ical') {
|
||||||
showToast(t('event_readonly'), true);
|
showToast(t('event_readonly'), true);
|
||||||
return;
|
return;
|
||||||
|
} else if (ev.source === 'homeassistant') {
|
||||||
|
const haCalId = ev.calendar_id.replace('homeassistant-', '');
|
||||||
|
await api.put(`/homeassistant/events/${haCalId}/${encodeURIComponent(ev.id)}`,
|
||||||
|
{ title, start, end, allDay, location: loc, description: desc }
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
await api.put(
|
await api.put(
|
||||||
`/caldav/events/${encodeURIComponent(ev.id)}?event_url=${encodeURIComponent(ev.url)}&calendar_id=${ev.calendar_id}`,
|
`/caldav/events/${encodeURIComponent(ev.id)}?event_url=${encodeURIComponent(ev.url)}&calendar_id=${ev.calendar_id}`,
|
||||||
|
|||||||
Reference in New Issue
Block a user