feat(caldav): add Settings UI and Event Modal CalDAV target selection
- Add CalDAV accounts card to Settings page with: * List of configured accounts showing URL and last sync * Expandable calendar list with enable/disable checkboxes * Sync Now, Refresh Calendars, and Delete actions per account * Add Account modal with name, URL, username, password fields - Add CalDAV target selector to event modal: * Dropdown showing local and all enabled CalDAV calendars * Grouped by account using optgroups * Pre-selects current target when editing events * Includes target_caldav_account_id and target_caldav_calendar_url in save - Add CalDAV component styles to settings.css: * Account cards with header, meta, and action sections * Expandable calendar details with checkboxes and color dots * Empty state for no accounts - Add missing i18n keys for calendar enable/disable, refresh, delete confirm - Load CalDAV targets async when modal opens - Admin-only access to account management Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1253,6 +1253,59 @@ function bindTimeInputs(root) {
|
||||
});
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// CalDAV Target Helpers
|
||||
// --------------------------------------------------------
|
||||
|
||||
async function loadCalDAVTargets(selectElement, currentEvent = null) {
|
||||
if (!selectElement) return;
|
||||
|
||||
try {
|
||||
const accountsRes = await api.get('/calendar/caldav/accounts');
|
||||
const accounts = accountsRes.data || [];
|
||||
|
||||
// Keep only the "local" option
|
||||
selectElement.replaceChildren();
|
||||
const localOption = document.createElement('option');
|
||||
localOption.value = '';
|
||||
localOption.textContent = t('calendar.caldavTargetLocal');
|
||||
selectElement.appendChild(localOption);
|
||||
|
||||
// Load calendars for each account and build options
|
||||
for (const account of accounts) {
|
||||
try {
|
||||
const calendarsRes = await api.get(`/calendar/caldav/accounts/${account.id}/calendars`);
|
||||
const calendars = calendarsRes.data || [];
|
||||
const enabledCalendars = calendars.filter((cal) => cal.enabled);
|
||||
|
||||
if (enabledCalendars.length === 0) continue;
|
||||
|
||||
const optgroup = document.createElement('optgroup');
|
||||
optgroup.label = account.name;
|
||||
|
||||
for (const calendar of enabledCalendars) {
|
||||
const option = document.createElement('option');
|
||||
option.value = `${account.id}|${calendar.url}`;
|
||||
option.textContent = calendar.display_name || calendar.url;
|
||||
optgroup.appendChild(option);
|
||||
}
|
||||
|
||||
selectElement.appendChild(optgroup);
|
||||
} catch (err) {
|
||||
console.warn(`Failed to load calendars for account ${account.id}:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
// Pre-select current event's target if editing
|
||||
if (currentEvent?.target_caldav_account_id && currentEvent?.target_caldav_calendar_url) {
|
||||
const targetValue = `${currentEvent.target_caldav_account_id}|${currentEvent.target_caldav_calendar_url}`;
|
||||
selectElement.value = targetValue;
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Failed to load CalDAV targets:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Event-Modal (Erstellen / Bearbeiten)
|
||||
// --------------------------------------------------------
|
||||
@@ -1465,6 +1518,12 @@ function openEventModal({ mode, event = null, date = null, reminder = null }) {
|
||||
if (reminderCustom) reminderCustom.hidden = reminderOffset.value !== 'custom';
|
||||
});
|
||||
|
||||
// Load CalDAV targets
|
||||
const caldavTargetSelect = panel.querySelector('#event-caldav-target');
|
||||
if (caldavTargetSelect) {
|
||||
loadCalDAVTargets(caldavTargetSelect, event);
|
||||
}
|
||||
|
||||
panel.querySelector('#modal-cancel').addEventListener('click', closeModal);
|
||||
|
||||
panel.querySelector('#modal-delete')?.addEventListener('click', async () => {
|
||||
@@ -1611,6 +1670,14 @@ function buildEventModalContent({ mode, event, date, reminder = null }) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="event-caldav-target">${t('calendar.caldavTargetLabel')}</label>
|
||||
<select class="form-input" id="event-caldav-target">
|
||||
<option value="">${t('calendar.caldavTargetLocal')}</option>
|
||||
</select>
|
||||
<small class="form-hint">${t('calendar.caldavTargetHint')}</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="modal-description">${t('calendar.descriptionLabel')}</label>
|
||||
<textarea class="form-input" id="modal-description" rows="2"
|
||||
@@ -1725,6 +1792,19 @@ async function saveEvent(overlay, mode, eventId, existingReminder = null, attach
|
||||
attachmentPayload.data = await readFileAsDataUrl(attachmentFile);
|
||||
}
|
||||
|
||||
// Extract CalDAV target
|
||||
const caldavTargetValue = overlay.querySelector('#event-caldav-target')?.value || '';
|
||||
let target_caldav_account_id = null;
|
||||
let target_caldav_calendar_url = null;
|
||||
|
||||
if (caldavTargetValue) {
|
||||
const [accountId, calendarUrl] = caldavTargetValue.split('|');
|
||||
if (accountId && calendarUrl) {
|
||||
target_caldav_account_id = parseInt(accountId, 10);
|
||||
target_caldav_calendar_url = calendarUrl;
|
||||
}
|
||||
}
|
||||
|
||||
const body = {
|
||||
title, description, start_datetime, end_datetime,
|
||||
all_day: allday ? 1 : 0,
|
||||
@@ -1734,6 +1814,8 @@ async function saveEvent(overlay, mode, eventId, existingReminder = null, attach
|
||||
attachment_mime: attachmentPayload.mime,
|
||||
attachment_size: attachmentPayload.size,
|
||||
attachment_data: attachmentPayload.data,
|
||||
target_caldav_account_id,
|
||||
target_caldav_calendar_url,
|
||||
};
|
||||
|
||||
let savedEventId = eventId;
|
||||
|
||||
Reference in New Issue
Block a user