diff --git a/README.md b/README.md index 3ba7545..9f3d11a 100644 --- a/README.md +++ b/README.md @@ -163,13 +163,26 @@ Key settings: `YYYY-MM-DD` are listed automatically. If a yearly subfolder exists (for example `.../2026/`), it is preferred. -- **calendar_ics_files** - Local calendar `.ics` files (one absolute file path per line). - Events are parsed by `sabre/vobject` and shown on matching chronological day pages. - Recurrence and exclusions from the ICS are respected. For timed entries, the - page stores the original timestamp and renders the visible time in the - browser's local timezone. - Multi-day events appear on each overlapping day. +- **calendar_ics_files** (REMOVED — replaced by per-slot calendar configuration) + +- **Calendar Slots** (configured via Admin -> luxtools) + The plugin supports 4 calendar slots: `general`, `maintenance`, `slot3`, `slot4`. + Each slot has its own settings: + + - **File**: Local `.ics` file path + - **CalDAV URL**: Remote CalDAV collection URL (optional) + - **Username**: CalDAV authentication username + - **Password**: CalDAV authentication password + - **Color**: CSS color for calendar widget indicators + + A slot is enabled if it has a local file path or a CalDAV URL configured. + The old `calendar_ics_files` setting has been replaced by the `general` slot's file path. + + Default colors: + - general: `#4a90d9` (blue) + - maintenance: `#e67e22` (orange) + - slot3: `#27ae60` (green) + - slot4: `#8e44ad` (purple) - **pagelink_search_depth** Maximum directory depth for `.pagelink` discovery under each configured root. @@ -287,6 +300,10 @@ Notes: - Month switches fetch server-rendered widget HTML via AJAX and replace only the widget node. - Calendar output is marked as non-cacheable to keep missing/existing link styling and current-day highlighting up to date. +- Each day cell shows colored corner indicators for calendar slots that have events on that day. + Indicator positions (clockwise): general = top-left, maintenance = top-right, + slot3 = bottom-right, slot4 = bottom-left. + Indicator colors are taken from the slot's configured color. ### 0.4) Virtual chronological day pages @@ -297,7 +314,8 @@ default "page does not exist" output. The virtual page includes: - a German-formatted heading (for example `Freitag, 13. Februar 2026`) -- matching local calendar events from configured `.ics` files (when available) +- matching calendar events from all enabled calendar slots (grouped by slot) +- maintenance tasks with completion buttons (from the maintenance slot) - matching day photos (via existing `{{images>...}}` rendering) when available The page is only created once you edit and save actual content. @@ -314,6 +332,90 @@ luxtools provides an admin-only **Invalidate Cache** action in the page tools me permission errors). - Also useful when actively adding external photos to the current day page. +### 0.6) Multi-calendar slot system + +The plugin supports 4 calendar slots, each with independent configuration for +a local `.ics` file, CalDAV URL, authentication, and display color. + +- **general**: The default event calendar. Events appear on day pages like before. +- **maintenance**: A task-oriented calendar. Events are treated as tasks with + completion tracking. Tasks can be marked complete/reopened via buttons on day pages. +- **slot3**, **slot4**: Reserved for future use. Events from these slots appear + on day pages with the slot's label as the section heading. + +Calendar data is always read from local `.ics` files for rendering. If a remote +CalDAV source is configured, use the sync feature to populate the local file. + +### 0.7) Maintenance task completion + +Maintenance tasks shown on day pages include a "Complete" button. Clicking it: + +1. Updates the event's `STATUS` property in the local `.ics` file. +2. If the maintenance slot has a CalDAV URL configured, also updates the remote + calendar object. +3. Shows visual feedback and reports any remote write failures. + +Completed tasks can be reopened with a "Reopen" button. + +Write-back rules: +- `VEVENT` components: `STATUS:TODO` for open, `STATUS:COMPLETED` for completed. +- `VTODO` components: Uses native completion semantics (`STATUS:COMPLETED`, + `COMPLETED` timestamp, `PERCENT-COMPLETE:100`). +- Recurring events: Completion writes an occurrence override/exception to preserve + per-occurrence state rather than modifying the master event. + +### 0.8) Event popup + +Clicking any event on a day page opens a popup overlay showing: +- Title +- Date/time (formatted in the browser's locale) +- Location (if available) +- Description (if available) +- Calendar slot name + +Close the popup by clicking outside it or pressing Escape. + +### 0.9) Maintenance task list syntax + +Embed a list of open maintenance tasks anywhere on a wiki page: + +``` +{{maintenance_tasks>}} +``` + +This renders all non-completed maintenance tasks due today or earlier, sorted +with overdue tasks first (then by date, time, and title). + +Each task shows its date, optional time, summary, and a "Complete" button. + +### 0.10) CalDAV sync + +If a slot has a CalDAV URL configured, the admin panel provides a sync button. +Triggering sync downloads all calendar objects from the remote CalDAV collection +and merges them into the slot's local `.ics` file. + +Sync is admin-only and does not run automatically. For scheduled sync, set up +a cron job that triggers the sync via the DokuWiki AJAX endpoint. + +### Known limitations + +- **Recurring event completion write-back**: For recurring events, completing a + single occurrence writes an override/exception component to the `.ics` file. + This works well for simple `RRULE` patterns. Some CalDAV servers may handle + the override differently. If the override is rejected by the remote server, + the local file will still have the correct state, but remote sync may + overwrite it on next sync. + +- **Sync direction**: Sync is currently one-directional (remote → local). Local + changes made via task completion are written back to the remote individually, + but a full remote-to-local sync may overwrite local changes if the remote + still has stale data. The completion write-back updates the remote immediately + to mitigate this. + +- **VTODO recurrence**: sabre/vobject's recurrence expansion has limited support + for `VTODO` components. Recurring `VTODO` items may not expand as expected. + Non-recurring `VTODO` items work correctly. + ### 1) List files by glob pattern The `{{directory>...}}` syntax (or `{{files>...}}` for backwards compatibility) can handle both directory listings and glob patterns. When a glob pattern is used, it renders as a table: diff --git a/action.php b/action.php index d047f77..a0d7c4c 100644 --- a/action.php +++ b/action.php @@ -4,11 +4,15 @@ use dokuwiki\Extension\ActionPlugin; use dokuwiki\Extension\Event; use dokuwiki\Extension\EventHandler; use dokuwiki\plugin\luxtools\CacheInvalidation; +use dokuwiki\plugin\luxtools\CalDavClient; +use dokuwiki\plugin\luxtools\CalendarEvent; +use dokuwiki\plugin\luxtools\CalendarService; +use dokuwiki\plugin\luxtools\CalendarSlot; use dokuwiki\plugin\luxtools\ChronoID; use dokuwiki\plugin\luxtools\ChronologicalCalendarWidget; use dokuwiki\plugin\luxtools\ChronologicalDateAutoLinker; use dokuwiki\plugin\luxtools\ChronologicalDayTemplate; -use dokuwiki\plugin\luxtools\ChronologicalIcsEvents; +use dokuwiki\plugin\luxtools\IcsWriter; use dokuwiki\plugin\luxtools\MenuItem\InvalidateCache; require_once(__DIR__ . '/autoload.php'); @@ -65,6 +69,18 @@ class action_plugin_luxtools extends ActionPlugin $this, "handleCalendarWidgetAjax", ); + $controller->register_hook( + "AJAX_CALL_UNKNOWN", + "BEFORE", + $this, + "handleMaintenanceTaskAction", + ); + $controller->register_hook( + "AJAX_CALL_UNKNOWN", + "BEFORE", + $this, + "handleCalendarSyncAction", + ); $controller->register_hook( "ACTION_ACT_PREPROCESS", "BEFORE", @@ -105,6 +121,7 @@ class action_plugin_luxtools extends ActionPlugin "page-link.js", "linkfavicon.js", "calendar-widget.js", + "event-popup.js", "main.js", ]; @@ -147,7 +164,18 @@ class action_plugin_luxtools extends ActionPlugin $this->sendNoStoreHeaders(); - $html = ChronologicalCalendarWidget::render($year, $month, $baseNs); + // Load slot indicators and colors for the calendar widget + $slots = CalendarSlot::loadEnabled($this); + $indicators = CalendarService::monthIndicators($slots, $year, $month); + $slotColors = []; + foreach ($slots as $slot) { + $color = $slot->getColor(); + if ($color !== '') { + $slotColors[$slot->getKey()] = $color; + } + } + + $html = ChronologicalCalendarWidget::render($year, $month, $baseNs, $indicators, $slotColors); if ($html === '') { http_status(500); echo 'Calendar rendering failed'; @@ -458,50 +486,348 @@ class action_plugin_luxtools extends ActionPlugin /** * Render local calendar events section for a given date. * + * Uses the slot-aware CalendarService to render events from all enabled slots. + * * @param string $dateIso * @return string */ protected function renderChronologicalEventsHtml(string $dateIso): string { - $icsConfig = (string)$this->getConf('calendar_ics_files'); - if (trim($icsConfig) === '') return ''; + $slots = CalendarSlot::loadEnabled($this); + if ($slots === []) return ''; - $events = ChronologicalIcsEvents::eventsForDate($icsConfig, $dateIso); - if ($events === []) return ''; + $grouped = CalendarService::eventsForDateGrouped($slots, $dateIso); + if ($grouped === []) return ''; - $title = (string)$this->getLang('chronological_events_title'); - if ($title === '') $title = 'Events'; + $html = ''; - $items = ''; - foreach ($events as $entry) { - $summary = trim((string)($entry['summary'] ?? '')); - if ($summary === '') $summary = '(ohne Titel)'; + // Render general events + if (isset($grouped['general'])) { + $title = (string)$this->getLang('chronological_events_title'); + if ($title === '') $title = 'Events'; + $html .= $this->renderEventSection($grouped['general'], $title, 'general'); + } - $time = trim((string)($entry['time'] ?? '')); - $startIso = trim((string)($entry['startIso'] ?? '')); - $isAllDay = (bool)($entry['allDay'] ?? false); + // Render maintenance tasks + if (isset($grouped['maintenance'])) { + $title = (string)$this->getLang('chronological_maintenance_title'); + if ($title === '') $title = 'Tasks'; + $html .= $this->renderMaintenanceSection($grouped['maintenance'], $title, $dateIso); + } - if ($isAllDay || $time === '') { - $items .= '