xref: /plugin/calendar/syntax.php (revision e3a9f44ce79ec1754946340aa2b4e60f3e5583ec)
119378907SAtari911<?php
219378907SAtari911/**
319378907SAtari911 * DokuWiki Plugin calendar (Syntax Component)
419378907SAtari911 * Compact design with integrated event list
519378907SAtari911 *
619378907SAtari911 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
719378907SAtari911 * @author  DokuWiki Community
819378907SAtari911 */
919378907SAtari911
1019378907SAtari911if (!defined('DOKU_INC')) die();
1119378907SAtari911
1219378907SAtari911class syntax_plugin_calendar extends DokuWiki_Syntax_Plugin {
1319378907SAtari911
1419378907SAtari911    public function getType() {
1519378907SAtari911        return 'substition';
1619378907SAtari911    }
1719378907SAtari911
1819378907SAtari911    public function getPType() {
1919378907SAtari911        return 'block';
2019378907SAtari911    }
2119378907SAtari911
2219378907SAtari911    public function getSort() {
2319378907SAtari911        return 155;
2419378907SAtari911    }
2519378907SAtari911
2619378907SAtari911    public function connectTo($mode) {
2719378907SAtari911        $this->Lexer->addSpecialPattern('\{\{calendar(?:[^\}]*)\}\}', $mode, 'plugin_calendar');
2819378907SAtari911        $this->Lexer->addSpecialPattern('\{\{eventlist(?:[^\}]*)\}\}', $mode, 'plugin_calendar');
2919378907SAtari911        $this->Lexer->addSpecialPattern('\{\{eventpanel(?:[^\}]*)\}\}', $mode, 'plugin_calendar');
3019378907SAtari911    }
3119378907SAtari911
3219378907SAtari911    public function handle($match, $state, $pos, Doku_Handler $handler) {
3319378907SAtari911        $isEventList = (strpos($match, '{{eventlist') === 0);
3419378907SAtari911        $isEventPanel = (strpos($match, '{{eventpanel') === 0);
3519378907SAtari911
3619378907SAtari911        if ($isEventList) {
3719378907SAtari911            $match = substr($match, 12, -2);
3819378907SAtari911        } elseif ($isEventPanel) {
3919378907SAtari911            $match = substr($match, 13, -2);
4019378907SAtari911        } else {
4119378907SAtari911            $match = substr($match, 10, -2);
4219378907SAtari911        }
4319378907SAtari911
4419378907SAtari911        $params = array(
4519378907SAtari911            'type' => $isEventPanel ? 'eventpanel' : ($isEventList ? 'eventlist' : 'calendar'),
4619378907SAtari911            'year' => date('Y'),
4719378907SAtari911            'month' => date('n'),
4819378907SAtari911            'namespace' => '',
4919378907SAtari911            'daterange' => '',
50*e3a9f44cSAtari911            'date' => '',
51*e3a9f44cSAtari911            'range' => ''
5219378907SAtari911        );
5319378907SAtari911
5419378907SAtari911        if (trim($match)) {
5519378907SAtari911            $pairs = preg_split('/\s+/', trim($match));
5619378907SAtari911            foreach ($pairs as $pair) {
5719378907SAtari911                if (strpos($pair, '=') !== false) {
5819378907SAtari911                    list($key, $value) = explode('=', $pair, 2);
5919378907SAtari911                    $params[trim($key)] = trim($value);
6087ac9bf3SAtari911                } else {
6187ac9bf3SAtari911                    // Handle standalone flags like "today"
6287ac9bf3SAtari911                    $params[trim($pair)] = true;
6319378907SAtari911                }
6419378907SAtari911            }
6519378907SAtari911        }
6619378907SAtari911
6719378907SAtari911        return $params;
6819378907SAtari911    }
6919378907SAtari911
7019378907SAtari911    public function render($mode, Doku_Renderer $renderer, $data) {
7119378907SAtari911        if ($mode !== 'xhtml') return false;
7219378907SAtari911
7319378907SAtari911        if ($data['type'] === 'eventlist') {
7419378907SAtari911            $html = $this->renderStandaloneEventList($data);
7519378907SAtari911        } elseif ($data['type'] === 'eventpanel') {
7619378907SAtari911            $html = $this->renderEventPanelOnly($data);
7719378907SAtari911        } else {
7819378907SAtari911            $html = $this->renderCompactCalendar($data);
7919378907SAtari911        }
8019378907SAtari911
8119378907SAtari911        $renderer->doc .= $html;
8219378907SAtari911        return true;
8319378907SAtari911    }
8419378907SAtari911
8519378907SAtari911    private function renderCompactCalendar($data) {
8619378907SAtari911        $year = (int)$data['year'];
8719378907SAtari911        $month = (int)$data['month'];
8819378907SAtari911        $namespace = $data['namespace'];
8919378907SAtari911
90*e3a9f44cSAtari911        // Check if multiple namespaces or wildcard specified
91*e3a9f44cSAtari911        $isMultiNamespace = !empty($namespace) && (strpos($namespace, ';') !== false || strpos($namespace, '*') !== false);
92*e3a9f44cSAtari911
93*e3a9f44cSAtari911        if ($isMultiNamespace) {
94*e3a9f44cSAtari911            $events = $this->loadEventsMultiNamespace($namespace, $year, $month);
95*e3a9f44cSAtari911        } else {
9619378907SAtari911            $events = $this->loadEvents($namespace, $year, $month);
97*e3a9f44cSAtari911        }
9819378907SAtari911        $calId = 'cal_' . md5(serialize($data) . microtime());
9919378907SAtari911
10019378907SAtari911        $monthName = date('F Y', mktime(0, 0, 0, $month, 1, $year));
10119378907SAtari911
10219378907SAtari911        $prevMonth = $month - 1;
10319378907SAtari911        $prevYear = $year;
10419378907SAtari911        if ($prevMonth < 1) {
10519378907SAtari911            $prevMonth = 12;
10619378907SAtari911            $prevYear--;
10719378907SAtari911        }
10819378907SAtari911
10919378907SAtari911        $nextMonth = $month + 1;
11019378907SAtari911        $nextYear = $year;
11119378907SAtari911        if ($nextMonth > 12) {
11219378907SAtari911            $nextMonth = 1;
11319378907SAtari911            $nextYear++;
11419378907SAtari911        }
11519378907SAtari911
11619378907SAtari911        $html = '<div class="calendar-compact-container" id="' . $calId . '" data-namespace="' . htmlspecialchars($namespace) . '" data-year="' . $year . '" data-month="' . $month . '">';
11719378907SAtari911
11819378907SAtari911        // Embed events data as JSON for JavaScript access
11919378907SAtari911        $html .= '<script type="application/json" id="events-data-' . $calId . '">' . json_encode($events) . '</script>';
12019378907SAtari911
12119378907SAtari911        // Left side: Calendar
12219378907SAtari911        $html .= '<div class="calendar-compact-left">';
12319378907SAtari911
12419378907SAtari911        // Header with navigation
12519378907SAtari911        $html .= '<div class="calendar-compact-header">';
12619378907SAtari911        $html .= '<button class="cal-nav-btn" onclick="navCalendar(\'' . $calId . '\', ' . $prevYear . ', ' . $prevMonth . ', \'' . $namespace . '\')">‹</button>';
12787ac9bf3SAtari911        $html .= '<h3 class="calendar-month-picker" onclick="openMonthPicker(\'' . $calId . '\', ' . $year . ', ' . $month . ', \'' . $namespace . '\')" title="Click to jump to month">' . $monthName . '</h3>';
12819378907SAtari911        $html .= '<button class="cal-nav-btn" onclick="navCalendar(\'' . $calId . '\', ' . $nextYear . ', ' . $nextMonth . ', \'' . $namespace . '\')">›</button>';
12987ac9bf3SAtari911        $html .= '<button class="cal-today-btn" onclick="jumpToToday(\'' . $calId . '\', \'' . $namespace . '\')">Today</button>';
13019378907SAtari911        $html .= '</div>';
13119378907SAtari911
13219378907SAtari911        // Calendar grid
13319378907SAtari911        $html .= '<table class="calendar-compact-grid">';
13419378907SAtari911        $html .= '<thead><tr>';
13519378907SAtari911        $html .= '<th>S</th><th>M</th><th>T</th><th>W</th><th>T</th><th>F</th><th>S</th>';
13619378907SAtari911        $html .= '</tr></thead><tbody>';
13719378907SAtari911
13819378907SAtari911        $firstDay = mktime(0, 0, 0, $month, 1, $year);
13919378907SAtari911        $daysInMonth = date('t', $firstDay);
14019378907SAtari911        $dayOfWeek = date('w', $firstDay);
14119378907SAtari911
142*e3a9f44cSAtari911        // Build a map of all events with their date ranges for the calendar grid
14387ac9bf3SAtari911        $eventRanges = array();
144*e3a9f44cSAtari911        foreach ($events as $dateKey => $dayEvents) {
14587ac9bf3SAtari911            foreach ($dayEvents as $evt) {
14687ac9bf3SAtari911                $eventId = isset($evt['id']) ? $evt['id'] : '';
14787ac9bf3SAtari911                $startDate = $dateKey;
14887ac9bf3SAtari911                $endDate = isset($evt['endDate']) && $evt['endDate'] ? $evt['endDate'] : $dateKey;
14987ac9bf3SAtari911
15087ac9bf3SAtari911                // Only process events that touch this month
15187ac9bf3SAtari911                $eventStart = new DateTime($startDate);
15287ac9bf3SAtari911                $eventEnd = new DateTime($endDate);
15387ac9bf3SAtari911                $monthStart = new DateTime(sprintf('%04d-%02d-01', $year, $month));
15487ac9bf3SAtari911                $monthEnd = new DateTime(sprintf('%04d-%02d-%02d', $year, $month, $daysInMonth));
15587ac9bf3SAtari911
15687ac9bf3SAtari911                // Skip if event doesn't overlap with current month
15787ac9bf3SAtari911                if ($eventEnd < $monthStart || $eventStart > $monthEnd) {
15887ac9bf3SAtari911                    continue;
15987ac9bf3SAtari911                }
16087ac9bf3SAtari911
16187ac9bf3SAtari911                // Create entry for each day the event spans
16287ac9bf3SAtari911                $current = clone $eventStart;
16387ac9bf3SAtari911                while ($current <= $eventEnd) {
16487ac9bf3SAtari911                    $currentKey = $current->format('Y-m-d');
16587ac9bf3SAtari911
16687ac9bf3SAtari911                    // Check if this date is in current month
16787ac9bf3SAtari911                    $currentDate = DateTime::createFromFormat('Y-m-d', $currentKey);
16887ac9bf3SAtari911                    if ($currentDate && $currentDate->format('Y-m') === sprintf('%04d-%02d', $year, $month)) {
16987ac9bf3SAtari911                        if (!isset($eventRanges[$currentKey])) {
17087ac9bf3SAtari911                            $eventRanges[$currentKey] = array();
17187ac9bf3SAtari911                        }
17287ac9bf3SAtari911
17387ac9bf3SAtari911                        // Add event with span information
17487ac9bf3SAtari911                        $evt['_span_start'] = $startDate;
17587ac9bf3SAtari911                        $evt['_span_end'] = $endDate;
17687ac9bf3SAtari911                        $evt['_is_first_day'] = ($currentKey === $startDate);
17787ac9bf3SAtari911                        $evt['_is_last_day'] = ($currentKey === $endDate);
17887ac9bf3SAtari911                        $evt['_original_date'] = $dateKey; // Keep track of original date
17987ac9bf3SAtari911
18087ac9bf3SAtari911                        // Check if event continues from previous month or to next month
18187ac9bf3SAtari911                        $evt['_continues_from_prev'] = ($eventStart < $monthStart);
18287ac9bf3SAtari911                        $evt['_continues_to_next'] = ($eventEnd > $monthEnd);
18387ac9bf3SAtari911
18487ac9bf3SAtari911                        $eventRanges[$currentKey][] = $evt;
18587ac9bf3SAtari911                    }
18687ac9bf3SAtari911
18787ac9bf3SAtari911                    $current->modify('+1 day');
18887ac9bf3SAtari911                }
18987ac9bf3SAtari911            }
19087ac9bf3SAtari911        }
19187ac9bf3SAtari911
19219378907SAtari911        $currentDay = 1;
19319378907SAtari911        $rowCount = ceil(($daysInMonth + $dayOfWeek) / 7);
19419378907SAtari911
19519378907SAtari911        for ($row = 0; $row < $rowCount; $row++) {
19619378907SAtari911            $html .= '<tr>';
19719378907SAtari911            for ($col = 0; $col < 7; $col++) {
19819378907SAtari911                if (($row === 0 && $col < $dayOfWeek) || $currentDay > $daysInMonth) {
19919378907SAtari911                    $html .= '<td class="cal-empty"></td>';
20019378907SAtari911                } else {
20119378907SAtari911                    $dateKey = sprintf('%04d-%02d-%02d', $year, $month, $currentDay);
20219378907SAtari911                    $isToday = ($dateKey === date('Y-m-d'));
20387ac9bf3SAtari911                    $hasEvents = isset($eventRanges[$dateKey]) && !empty($eventRanges[$dateKey]);
20419378907SAtari911
20519378907SAtari911                    $classes = 'cal-day';
20619378907SAtari911                    if ($isToday) $classes .= ' cal-today';
20719378907SAtari911                    if ($hasEvents) $classes .= ' cal-has-events';
20819378907SAtari911
20919378907SAtari911                    $html .= '<td class="' . $classes . '" data-date="' . $dateKey . '" onclick="showDayPopup(\'' . $calId . '\', \'' . $dateKey . '\', \'' . $namespace . '\')">';
21019378907SAtari911                    $html .= '<span class="day-num">' . $currentDay . '</span>';
21119378907SAtari911
21219378907SAtari911                    if ($hasEvents) {
21319378907SAtari911                        // Sort events by time (no time first, then by time)
21487ac9bf3SAtari911                        $sortedEvents = $eventRanges[$dateKey];
21519378907SAtari911                        usort($sortedEvents, function($a, $b) {
21619378907SAtari911                            $timeA = isset($a['time']) ? $a['time'] : '';
21719378907SAtari911                            $timeB = isset($b['time']) ? $b['time'] : '';
21819378907SAtari911
21919378907SAtari911                            // Events without time go first
22019378907SAtari911                            if (empty($timeA) && !empty($timeB)) return -1;
22119378907SAtari911                            if (!empty($timeA) && empty($timeB)) return 1;
22219378907SAtari911                            if (empty($timeA) && empty($timeB)) return 0;
22319378907SAtari911
22419378907SAtari911                            // Sort by time
22519378907SAtari911                            return strcmp($timeA, $timeB);
22619378907SAtari911                        });
22719378907SAtari911
22819378907SAtari911                        // Show colored stacked bars for each event
22919378907SAtari911                        $html .= '<div class="event-indicators">';
23019378907SAtari911                        foreach ($sortedEvents as $evt) {
23119378907SAtari911                            $eventId = isset($evt['id']) ? $evt['id'] : '';
23219378907SAtari911                            $eventColor = isset($evt['color']) ? htmlspecialchars($evt['color']) : '#3498db';
23319378907SAtari911                            $eventTime = isset($evt['time']) ? $evt['time'] : '';
23419378907SAtari911                            $eventTitle = isset($evt['title']) ? htmlspecialchars($evt['title']) : 'Event';
23587ac9bf3SAtari911                            $originalDate = isset($evt['_original_date']) ? $evt['_original_date'] : $dateKey;
23687ac9bf3SAtari911                            $isFirstDay = isset($evt['_is_first_day']) ? $evt['_is_first_day'] : true;
23787ac9bf3SAtari911                            $isLastDay = isset($evt['_is_last_day']) ? $evt['_is_last_day'] : true;
23819378907SAtari911
23919378907SAtari911                            $barClass = empty($eventTime) ? 'event-bar-no-time' : 'event-bar-timed';
24019378907SAtari911
24187ac9bf3SAtari911                            // Add classes for multi-day spanning
24287ac9bf3SAtari911                            if (!$isFirstDay) $barClass .= ' event-bar-continues';
24387ac9bf3SAtari911                            if (!$isLastDay) $barClass .= ' event-bar-continuing';
24487ac9bf3SAtari911
24519378907SAtari911                            $html .= '<span class="event-bar ' . $barClass . '" ';
24619378907SAtari911                            $html .= 'style="background: ' . $eventColor . ';" ';
24719378907SAtari911                            $html .= 'title="' . $eventTitle . ($eventTime ? ' @ ' . $eventTime : '') . '" ';
24887ac9bf3SAtari911                            $html .= 'onclick="event.stopPropagation(); highlightEvent(\'' . $calId . '\', \'' . $eventId . '\', \'' . $originalDate . '\');">';
24919378907SAtari911                            $html .= '</span>';
25019378907SAtari911                        }
25119378907SAtari911                        $html .= '</div>';
25219378907SAtari911                    }
25319378907SAtari911
25419378907SAtari911                    $html .= '</td>';
25519378907SAtari911                    $currentDay++;
25619378907SAtari911                }
25719378907SAtari911            }
25819378907SAtari911            $html .= '</tr>';
25919378907SAtari911        }
26019378907SAtari911
26119378907SAtari911        $html .= '</tbody></table>';
26219378907SAtari911        $html .= '</div>'; // End calendar-left
26319378907SAtari911
26419378907SAtari911        // Right side: Event list
26519378907SAtari911        $html .= '<div class="calendar-compact-right">';
26619378907SAtari911        $html .= '<div class="event-list-header">';
26719378907SAtari911        $html .= '<div class="event-list-header-content">';
26819378907SAtari911        $html .= '<h4 id="eventlist-title-' . $calId . '">Events</h4>';
26919378907SAtari911        if ($namespace) {
27019378907SAtari911            $html .= '<span class="namespace-badge">' . htmlspecialchars($namespace) . '</span>';
27119378907SAtari911        }
27219378907SAtari911        $html .= '</div>';
27319378907SAtari911        $html .= '<button class="add-event-compact" onclick="openAddEvent(\'' . $calId . '\', \'' . $namespace . '\')">+ Add</button>';
27419378907SAtari911        $html .= '</div>';
27519378907SAtari911
27619378907SAtari911        $html .= '<div class="event-list-compact" id="eventlist-' . $calId . '">';
27719378907SAtari911        $html .= $this->renderEventListContent($events, $calId, $namespace);
27819378907SAtari911        $html .= '</div>';
27919378907SAtari911
28019378907SAtari911        $html .= '</div>'; // End calendar-right
28119378907SAtari911
28219378907SAtari911        // Event dialog
28319378907SAtari911        $html .= $this->renderEventDialog($calId, $namespace);
28419378907SAtari911
28587ac9bf3SAtari911        // Month/Year picker dialog (at container level for proper overlay)
28687ac9bf3SAtari911        $html .= $this->renderMonthPicker($calId, $year, $month, $namespace);
28787ac9bf3SAtari911
28819378907SAtari911        $html .= '</div>'; // End container
28919378907SAtari911
29019378907SAtari911        return $html;
29119378907SAtari911    }
29219378907SAtari911
29319378907SAtari911    private function renderEventListContent($events, $calId, $namespace) {
29419378907SAtari911        if (empty($events)) {
29519378907SAtari911            return '<p class="no-events-msg">No events this month</p>';
29619378907SAtari911        }
29719378907SAtari911
298*e3a9f44cSAtari911        // Sort by date ascending (chronological order - oldest first)
29919378907SAtari911        ksort($events);
30019378907SAtari911
301*e3a9f44cSAtari911        // Sort events within each day by time
302*e3a9f44cSAtari911        foreach ($events as $dateKey => &$dayEvents) {
303*e3a9f44cSAtari911            usort($dayEvents, function($a, $b) {
304*e3a9f44cSAtari911                $timeA = isset($a['time']) ? $a['time'] : '00:00';
305*e3a9f44cSAtari911                $timeB = isset($b['time']) ? $b['time'] : '00:00';
306*e3a9f44cSAtari911                return strcmp($timeA, $timeB);
307*e3a9f44cSAtari911            });
308*e3a9f44cSAtari911        }
309*e3a9f44cSAtari911        unset($dayEvents); // Break reference
310*e3a9f44cSAtari911
311*e3a9f44cSAtari911        // Get today's date for comparison
312*e3a9f44cSAtari911        $today = date('Y-m-d');
313*e3a9f44cSAtari911        $firstFutureEventId = null;
314*e3a9f44cSAtari911
315*e3a9f44cSAtari911        // Build HTML for each event
316*e3a9f44cSAtari911        $html = '';
317*e3a9f44cSAtari911
31819378907SAtari911        foreach ($events as $dateKey => $dayEvents) {
319*e3a9f44cSAtari911            $isPast = $dateKey < $today;
320*e3a9f44cSAtari911            $isToday = $dateKey === $today;
321*e3a9f44cSAtari911
32219378907SAtari911            foreach ($dayEvents as $event) {
323*e3a9f44cSAtari911                // Track first future/today event for auto-scroll
324*e3a9f44cSAtari911                if (!$firstFutureEventId && $dateKey >= $today) {
325*e3a9f44cSAtari911                    $firstFutureEventId = isset($event['id']) ? $event['id'] : '';
326*e3a9f44cSAtari911                }
32719378907SAtari911                $eventId = isset($event['id']) ? $event['id'] : '';
32819378907SAtari911                $title = isset($event['title']) ? htmlspecialchars($event['title']) : 'Untitled';
32919378907SAtari911                $time = isset($event['time']) ? htmlspecialchars($event['time']) : '';
33019378907SAtari911                $color = isset($event['color']) ? htmlspecialchars($event['color']) : '#3498db';
33119378907SAtari911                $description = isset($event['description']) ? $event['description'] : '';
33219378907SAtari911                $isTask = isset($event['isTask']) ? $event['isTask'] : false;
33319378907SAtari911                $completed = isset($event['completed']) ? $event['completed'] : false;
33419378907SAtari911                $endDate = isset($event['endDate']) ? $event['endDate'] : '';
33519378907SAtari911
33619378907SAtari911                // Process description for wiki syntax, HTML, images, and links
33719378907SAtari911                $renderedDescription = $this->renderDescription($description);
33819378907SAtari911
33919378907SAtari911                // Convert to 12-hour format
34019378907SAtari911                $displayTime = '';
34119378907SAtari911                if ($time) {
34219378907SAtari911                    $timeObj = DateTime::createFromFormat('H:i', $time);
34319378907SAtari911                    if ($timeObj) {
34419378907SAtari911                        $displayTime = $timeObj->format('g:i A');
34519378907SAtari911                    } else {
34619378907SAtari911                        $displayTime = $time;
34719378907SAtari911                    }
34819378907SAtari911                }
34919378907SAtari911
35087ac9bf3SAtari911                // Format date display with day of week
351*e3a9f44cSAtari911                // Use originalStartDate if this is a multi-month event continuation
352*e3a9f44cSAtari911                $displayDateKey = isset($event['originalStartDate']) ? $event['originalStartDate'] : $dateKey;
353*e3a9f44cSAtari911                $dateObj = new DateTime($displayDateKey);
35487ac9bf3SAtari911                $displayDate = $dateObj->format('D, M j'); // e.g., "Mon, Jan 24"
35519378907SAtari911
35619378907SAtari911                // Multi-day indicator
35719378907SAtari911                $multiDay = '';
358*e3a9f44cSAtari911                if ($endDate && $endDate !== $displayDateKey) {
35919378907SAtari911                    $endObj = new DateTime($endDate);
36087ac9bf3SAtari911                    $multiDay = ' → ' . $endObj->format('D, M j');
36119378907SAtari911                }
36219378907SAtari911
36319378907SAtari911                $completedClass = $completed ? ' event-completed' : '';
364*e3a9f44cSAtari911                $pastClass = $isPast ? ' event-past' : '';
365*e3a9f44cSAtari911                $firstFutureAttr = ($firstFutureEventId === $eventId) ? ' data-first-future="true"' : '';
36619378907SAtari911
367*e3a9f44cSAtari911                $html .= '<div class="event-compact-item' . $completedClass . $pastClass . '" data-event-id="' . $eventId . '" data-date="' . $dateKey . '" style="border-left-color: ' . $color . ';"' . $firstFutureAttr . '>';
36819378907SAtari911
36919378907SAtari911                $html .= '<div class="event-info">';
37019378907SAtari911                $html .= '<div class="event-title-row">';
37119378907SAtari911                $html .= '<span class="event-title-compact">' . $title . '</span>';
37219378907SAtari911                $html .= '</div>';
37319378907SAtari911
374*e3a9f44cSAtari911                // For past events, hide meta and description (collapsed)
375*e3a9f44cSAtari911                if (!$isPast) {
37619378907SAtari911                    $html .= '<div class="event-meta-compact">';
37719378907SAtari911                    $html .= '<span class="event-date-time">' . $displayDate . $multiDay;
37819378907SAtari911                    if ($displayTime) {
37919378907SAtari911                        $html .= ' • ' . $displayTime;
38019378907SAtari911                    }
381*e3a9f44cSAtari911                    // Add TODAY badge for today's events
382*e3a9f44cSAtari911                    if ($isToday) {
383*e3a9f44cSAtari911                        $html .= ' <span class="event-today-badge">TODAY</span>';
384*e3a9f44cSAtari911                    }
385*e3a9f44cSAtari911                    // Add namespace badge (for multi-namespace or stored namespace)
386*e3a9f44cSAtari911                    $eventNamespace = isset($event['namespace']) ? $event['namespace'] : '';
387*e3a9f44cSAtari911                    if (!$eventNamespace && isset($event['_namespace'])) {
388*e3a9f44cSAtari911                        $eventNamespace = $event['_namespace']; // Fallback to _namespace for backward compatibility
389*e3a9f44cSAtari911                    }
390*e3a9f44cSAtari911                    if ($eventNamespace) {
391*e3a9f44cSAtari911                        $html .= ' <span class="event-namespace-badge">' . htmlspecialchars($eventNamespace) . '</span>';
392*e3a9f44cSAtari911                    }
39319378907SAtari911                    $html .= '</span>';
39419378907SAtari911                    $html .= '</div>';
39519378907SAtari911
39619378907SAtari911                    if ($description) {
39719378907SAtari911                        $html .= '<div class="event-desc-compact">' . $renderedDescription . '</div>';
39819378907SAtari911                    }
399*e3a9f44cSAtari911                }
40019378907SAtari911
40119378907SAtari911                $html .= '</div>'; // event-info
40219378907SAtari911
403*e3a9f44cSAtari911                // Use stored namespace from event, fallback to passed namespace
404*e3a9f44cSAtari911                $buttonNamespace = isset($event['namespace']) ? $event['namespace'] : $namespace;
405*e3a9f44cSAtari911
40619378907SAtari911                $html .= '<div class="event-actions-compact">';
407*e3a9f44cSAtari911                $html .= '<button class="event-action-btn" onclick="deleteEvent(\'' . $calId . '\', \'' . $eventId . '\', \'' . $dateKey . '\', \'' . $buttonNamespace . '\')">��️</button>';
408*e3a9f44cSAtari911                $html .= '<button class="event-action-btn" onclick="editEvent(\'' . $calId . '\', \'' . $eventId . '\', \'' . $dateKey . '\', \'' . $buttonNamespace . '\')">✏️</button>';
40919378907SAtari911                $html .= '</div>';
41019378907SAtari911
41119378907SAtari911                // Checkbox for tasks - ON THE FAR RIGHT
41219378907SAtari911                if ($isTask) {
41319378907SAtari911                    $checked = $completed ? 'checked' : '';
414*e3a9f44cSAtari911                    $html .= '<input type="checkbox" class="task-checkbox" ' . $checked . ' onclick="toggleTaskComplete(\'' . $calId . '\', \'' . $eventId . '\', \'' . $dateKey . '\', \'' . $buttonNamespace . '\', this.checked)">';
41519378907SAtari911                }
41619378907SAtari911
41719378907SAtari911                $html .= '</div>';
418*e3a9f44cSAtari911
419*e3a9f44cSAtari911                // Add to HTML output
42019378907SAtari911            }
42119378907SAtari911        }
42219378907SAtari911
42319378907SAtari911        return $html;
42419378907SAtari911    }
42519378907SAtari911
42619378907SAtari911    private function renderEventPanelOnly($data) {
42719378907SAtari911        $year = (int)$data['year'];
42819378907SAtari911        $month = (int)$data['month'];
42919378907SAtari911        $namespace = $data['namespace'];
43087ac9bf3SAtari911        $height = isset($data['height']) ? $data['height'] : '400px';
43187ac9bf3SAtari911
43287ac9bf3SAtari911        // Validate height format (must be px, em, rem, vh, or %)
43387ac9bf3SAtari911        if (!preg_match('/^\d+(\.\d+)?(px|em|rem|vh|%)$/', $height)) {
43487ac9bf3SAtari911            $height = '400px'; // Default fallback
43587ac9bf3SAtari911        }
43619378907SAtari911
437*e3a9f44cSAtari911        // Check if multiple namespaces or wildcard specified
438*e3a9f44cSAtari911        $isMultiNamespace = !empty($namespace) && (strpos($namespace, ';') !== false || strpos($namespace, '*') !== false);
439*e3a9f44cSAtari911
440*e3a9f44cSAtari911        if ($isMultiNamespace) {
441*e3a9f44cSAtari911            $events = $this->loadEventsMultiNamespace($namespace, $year, $month);
442*e3a9f44cSAtari911        } else {
44319378907SAtari911            $events = $this->loadEvents($namespace, $year, $month);
444*e3a9f44cSAtari911        }
44519378907SAtari911        $calId = 'panel_' . md5(serialize($data) . microtime());
44619378907SAtari911
44719378907SAtari911        $monthName = date('F Y', mktime(0, 0, 0, $month, 1, $year));
44819378907SAtari911
44919378907SAtari911        $prevMonth = $month - 1;
45019378907SAtari911        $prevYear = $year;
45119378907SAtari911        if ($prevMonth < 1) {
45219378907SAtari911            $prevMonth = 12;
45319378907SAtari911            $prevYear--;
45419378907SAtari911        }
45519378907SAtari911
45619378907SAtari911        $nextMonth = $month + 1;
45719378907SAtari911        $nextYear = $year;
45819378907SAtari911        if ($nextMonth > 12) {
45919378907SAtari911            $nextMonth = 1;
46019378907SAtari911            $nextYear++;
46119378907SAtari911        }
46219378907SAtari911
46387ac9bf3SAtari911        $html = '<div class="event-panel-standalone" id="' . $calId . '" data-height="' . htmlspecialchars($height) . '" data-namespace="' . htmlspecialchars($namespace) . '">';
46419378907SAtari911
46519378907SAtari911        // Header with navigation
46619378907SAtari911        $html .= '<div class="panel-standalone-header">';
46719378907SAtari911        $html .= '<button class="cal-nav-btn" onclick="navEventPanel(\'' . $calId . '\', ' . $prevYear . ', ' . $prevMonth . ', \'' . $namespace . '\')">‹</button>';
46887ac9bf3SAtari911        $html .= '<div class="panel-header-content">';
46987ac9bf3SAtari911        $html .= '<h3 class="calendar-month-picker" onclick="openMonthPickerPanel(\'' . $calId . '\', ' . $year . ', ' . $month . ', \'' . $namespace . '\')" title="Click to jump to month">' . $monthName . ' Events</h3>';
47087ac9bf3SAtari911        if ($namespace) {
471*e3a9f44cSAtari911            // Show multiple namespace badges if multi-namespace
472*e3a9f44cSAtari911            if ($isMultiNamespace) {
473*e3a9f44cSAtari911                // Handle wildcard
474*e3a9f44cSAtari911                if (strpos($namespace, '*') !== false) {
475*e3a9f44cSAtari911                    $html .= '<span class="namespace-badge">' . htmlspecialchars($namespace) . '</span> ';
476*e3a9f44cSAtari911                } else {
477*e3a9f44cSAtari911                    // Semicolon-separated list
478*e3a9f44cSAtari911                    $namespaceList = array_map('trim', explode(';', $namespace));
479*e3a9f44cSAtari911                    foreach ($namespaceList as $ns) {
480*e3a9f44cSAtari911                        $ns = trim($ns);
481*e3a9f44cSAtari911                        if (empty($ns)) continue;
482*e3a9f44cSAtari911                        $namespaceUrl = DOKU_BASE . 'doku.php?id=' . str_replace(':', ':', $ns);
483*e3a9f44cSAtari911                        $html .= '<a href="' . $namespaceUrl . '" class="namespace-badge" title="Go to namespace page">' . htmlspecialchars($ns) . '</a> ';
484*e3a9f44cSAtari911                    }
485*e3a9f44cSAtari911                }
486*e3a9f44cSAtari911            } else {
48787ac9bf3SAtari911                $namespaceUrl = DOKU_BASE . 'doku.php?id=' . str_replace(':', ':', $namespace);
48887ac9bf3SAtari911                $html .= '<a href="' . $namespaceUrl . '" class="namespace-badge" title="Go to namespace page">' . htmlspecialchars($namespace) . '</a>';
48987ac9bf3SAtari911            }
490*e3a9f44cSAtari911        }
49187ac9bf3SAtari911        $html .= '</div>';
49219378907SAtari911        $html .= '<button class="cal-nav-btn" onclick="navEventPanel(\'' . $calId . '\', ' . $nextYear . ', ' . $nextMonth . ', \'' . $namespace . '\')">›</button>';
49387ac9bf3SAtari911        $html .= '<button class="cal-today-btn" onclick="jumpTodayPanel(\'' . $calId . '\', \'' . $namespace . '\')">Today</button>';
49419378907SAtari911        $html .= '</div>';
49519378907SAtari911
49619378907SAtari911        $html .= '<div class="panel-standalone-actions">';
49719378907SAtari911        $html .= '<button class="add-event-compact" onclick="openAddEventPanel(\'' . $calId . '\', \'' . $namespace . '\')">+ Add Event</button>';
49819378907SAtari911        $html .= '</div>';
49919378907SAtari911
50087ac9bf3SAtari911        $html .= '<div class="event-list-compact" id="eventlist-' . $calId . '" style="max-height: ' . htmlspecialchars($height) . ';">';
50119378907SAtari911        $html .= $this->renderEventListContent($events, $calId, $namespace);
50219378907SAtari911        $html .= '</div>';
50319378907SAtari911
50419378907SAtari911        $html .= $this->renderEventDialog($calId, $namespace);
50519378907SAtari911
50687ac9bf3SAtari911        // Month/Year picker for event panel
50787ac9bf3SAtari911        $html .= $this->renderMonthPicker($calId, $year, $month, $namespace);
50887ac9bf3SAtari911
50919378907SAtari911        $html .= '</div>';
51019378907SAtari911
51119378907SAtari911        return $html;
51219378907SAtari911    }
51319378907SAtari911
51419378907SAtari911    private function renderStandaloneEventList($data) {
51519378907SAtari911        $namespace = $data['namespace'];
51619378907SAtari911        $daterange = $data['daterange'];
51719378907SAtari911        $date = $data['date'];
518*e3a9f44cSAtari911        $range = isset($data['range']) ? strtolower($data['range']) : '';
51987ac9bf3SAtari911        $today = isset($data['today']) ? true : false;
520*e3a9f44cSAtari911        $sidebar = isset($data['sidebar']) ? true : false;
52119378907SAtari911
522*e3a9f44cSAtari911        // Handle "range" parameter - day, week, or month
523*e3a9f44cSAtari911        if ($range === 'day') {
52487ac9bf3SAtari911            $startDate = date('Y-m-d');
52587ac9bf3SAtari911            $endDate = date('Y-m-d');
526*e3a9f44cSAtari911            $headerText = 'Today';
527*e3a9f44cSAtari911        } elseif ($range === 'week') {
528*e3a9f44cSAtari911            $startDate = date('Y-m-d'); // Today
529*e3a9f44cSAtari911            $endDateTime = new DateTime($startDate);
530*e3a9f44cSAtari911            $endDateTime->modify('+7 days');
531*e3a9f44cSAtari911            $endDate = $endDateTime->format('Y-m-d');
532*e3a9f44cSAtari911            $headerText = 'This Week';
533*e3a9f44cSAtari911        } elseif ($range === 'month') {
534*e3a9f44cSAtari911            $startDate = date('Y-m-01'); // First of current month
535*e3a9f44cSAtari911            $endDate = date('Y-m-t'); // Last of current month
536*e3a9f44cSAtari911            $dt = new DateTime($startDate);
537*e3a9f44cSAtari911            $headerText = $dt->format('F Y');
538*e3a9f44cSAtari911        } elseif ($sidebar) {
539*e3a9f44cSAtari911            // Handle "sidebar" parameter - shows today through one month from today
540*e3a9f44cSAtari911            $startDate = date('Y-m-d'); // Today
541*e3a9f44cSAtari911            $endDateTime = new DateTime($startDate);
542*e3a9f44cSAtari911            $endDateTime->modify('+1 month');
543*e3a9f44cSAtari911            $endDate = $endDateTime->format('Y-m-d'); // One month from today
544*e3a9f44cSAtari911            $headerText = 'Upcoming';
545*e3a9f44cSAtari911        } elseif ($today) {
546*e3a9f44cSAtari911            $startDate = date('Y-m-d');
547*e3a9f44cSAtari911            $endDate = date('Y-m-d');
548*e3a9f44cSAtari911            $headerText = 'Today';
54987ac9bf3SAtari911        } elseif ($daterange) {
55019378907SAtari911            list($startDate, $endDate) = explode(':', $daterange);
551*e3a9f44cSAtari911            $start = new DateTime($startDate);
552*e3a9f44cSAtari911            $end = new DateTime($endDate);
553*e3a9f44cSAtari911            $headerText = $start->format('M j') . ' - ' . $end->format('M j, Y');
55419378907SAtari911        } elseif ($date) {
55519378907SAtari911            $startDate = $date;
55619378907SAtari911            $endDate = $date;
557*e3a9f44cSAtari911            $dt = new DateTime($date);
558*e3a9f44cSAtari911            $headerText = $dt->format('l, F j, Y');
55919378907SAtari911        } else {
56019378907SAtari911            $startDate = date('Y-m-01');
56119378907SAtari911            $endDate = date('Y-m-t');
562*e3a9f44cSAtari911            $dt = new DateTime($startDate);
563*e3a9f44cSAtari911            $headerText = $dt->format('F Y');
56419378907SAtari911        }
56519378907SAtari911
566*e3a9f44cSAtari911        // Load all events in date range
56719378907SAtari911        $allEvents = array();
56819378907SAtari911        $start = new DateTime($startDate);
56919378907SAtari911        $end = new DateTime($endDate);
57019378907SAtari911        $end->modify('+1 day');
57119378907SAtari911
57219378907SAtari911        $interval = new DateInterval('P1D');
57319378907SAtari911        $period = new DatePeriod($start, $interval, $end);
57419378907SAtari911
575*e3a9f44cSAtari911        // Check if multiple namespaces or wildcard specified
576*e3a9f44cSAtari911        $isMultiNamespace = !empty($namespace) && (strpos($namespace, ';') !== false || strpos($namespace, '*') !== false);
577*e3a9f44cSAtari911
57819378907SAtari911        static $loadedMonths = array();
57919378907SAtari911
58019378907SAtari911        foreach ($period as $dt) {
58119378907SAtari911            $year = (int)$dt->format('Y');
58219378907SAtari911            $month = (int)$dt->format('n');
58319378907SAtari911            $dateKey = $dt->format('Y-m-d');
58419378907SAtari911
585*e3a9f44cSAtari911            $monthKey = $year . '-' . $month . '-' . $namespace;
58619378907SAtari911
58719378907SAtari911            if (!isset($loadedMonths[$monthKey])) {
588*e3a9f44cSAtari911                if ($isMultiNamespace) {
589*e3a9f44cSAtari911                    $loadedMonths[$monthKey] = $this->loadEventsMultiNamespace($namespace, $year, $month);
590*e3a9f44cSAtari911                } else {
59119378907SAtari911                    $loadedMonths[$monthKey] = $this->loadEvents($namespace, $year, $month);
59219378907SAtari911                }
593*e3a9f44cSAtari911            }
59419378907SAtari911
59519378907SAtari911            $monthEvents = $loadedMonths[$monthKey];
59619378907SAtari911
59719378907SAtari911            if (isset($monthEvents[$dateKey]) && !empty($monthEvents[$dateKey])) {
59819378907SAtari911                $allEvents[$dateKey] = $monthEvents[$dateKey];
59919378907SAtari911            }
60019378907SAtari911        }
60119378907SAtari911
602*e3a9f44cSAtari911        // Simple 2-line display widget
603*e3a9f44cSAtari911        $html = '<div class="eventlist-simple">';
60419378907SAtari911
60519378907SAtari911        if (empty($allEvents)) {
606*e3a9f44cSAtari911            $html .= '<div class="eventlist-simple-empty">';
607*e3a9f44cSAtari911            $html .= '<div class="eventlist-simple-header">' . htmlspecialchars($headerText);
608*e3a9f44cSAtari911            if ($namespace) {
609*e3a9f44cSAtari911                $html .= ' <span class="eventlist-simple-namespace">' . htmlspecialchars($namespace) . '</span>';
61087ac9bf3SAtari911            }
611*e3a9f44cSAtari911            $html .= '</div>';
612*e3a9f44cSAtari911            $html .= '<div class="eventlist-simple-body">No events</div>';
613*e3a9f44cSAtari911            $html .= '</div>';
614*e3a9f44cSAtari911        } else {
615*e3a9f44cSAtari911            // Calculate today and tomorrow's dates for highlighting
616*e3a9f44cSAtari911            $today = date('Y-m-d');
617*e3a9f44cSAtari911            $tomorrow = date('Y-m-d', strtotime('+1 day'));
618*e3a9f44cSAtari911
619*e3a9f44cSAtari911            foreach ($allEvents as $dateKey => $dayEvents) {
620*e3a9f44cSAtari911                $dateObj = new DateTime($dateKey);
621*e3a9f44cSAtari911                $displayDate = $dateObj->format('D, M j');
622*e3a9f44cSAtari911
623*e3a9f44cSAtari911                // Check if this date is today or tomorrow
624*e3a9f44cSAtari911                // Enable highlighting for sidebar mode AND range modes (day, week, month)
625*e3a9f44cSAtari911                $enableHighlighting = $sidebar || !empty($range);
626*e3a9f44cSAtari911                $isToday = $enableHighlighting && ($dateKey === $today);
627*e3a9f44cSAtari911                $isTomorrow = $enableHighlighting && ($dateKey === $tomorrow);
62819378907SAtari911
62919378907SAtari911                foreach ($dayEvents as $event) {
630*e3a9f44cSAtari911                    // Skip completed tasks when in sidebar mode or day/week range
631*e3a9f44cSAtari911                    $skipCompleted = $sidebar || ($range === 'day') || ($range === 'week');
632*e3a9f44cSAtari911                    if ($skipCompleted && !empty($event['isTask']) && !empty($event['completed'])) {
633*e3a9f44cSAtari911                        continue;
634*e3a9f44cSAtari911                    }
63519378907SAtari911
636*e3a9f44cSAtari911                    // Line 1: Header (Title, Time, Date, Namespace)
637*e3a9f44cSAtari911                    $todayClass = $isToday ? ' eventlist-simple-today' : '';
638*e3a9f44cSAtari911                    $tomorrowClass = $isTomorrow ? ' eventlist-simple-tomorrow' : '';
639*e3a9f44cSAtari911                    $html .= '<div class="eventlist-simple-item' . $todayClass . $tomorrowClass . '">';
640*e3a9f44cSAtari911                    $html .= '<div class="eventlist-simple-header">';
641*e3a9f44cSAtari911
642*e3a9f44cSAtari911                    // Title
643*e3a9f44cSAtari911                    $html .= '<span class="eventlist-simple-title">' . htmlspecialchars($event['title']) . '</span>';
644*e3a9f44cSAtari911
645*e3a9f44cSAtari911                    // Time (12-hour format)
646*e3a9f44cSAtari911                    if (!empty($event['time'])) {
647*e3a9f44cSAtari911                        $timeParts = explode(':', $event['time']);
64887ac9bf3SAtari911                        if (count($timeParts) === 2) {
64987ac9bf3SAtari911                            $hour = (int)$timeParts[0];
65087ac9bf3SAtari911                            $minute = $timeParts[1];
65187ac9bf3SAtari911                            $ampm = $hour >= 12 ? 'PM' : 'AM';
652*e3a9f44cSAtari911                            $hour = $hour % 12 ?: 12;
65387ac9bf3SAtari911                            $displayTime = $hour . ':' . $minute . ' ' . $ampm;
654*e3a9f44cSAtari911                            $html .= ' <span class="eventlist-simple-time">' . $displayTime . '</span>';
65519378907SAtari911                        }
65687ac9bf3SAtari911                    }
65787ac9bf3SAtari911
658*e3a9f44cSAtari911                    // Date
659*e3a9f44cSAtari911                    $html .= ' <span class="eventlist-simple-date">' . $displayDate . '</span>';
660*e3a9f44cSAtari911
661*e3a9f44cSAtari911                    // TODAY badge (show for today's events in sidebar)
662*e3a9f44cSAtari911                    if ($isToday) {
663*e3a9f44cSAtari911                        $html .= ' <span class="eventlist-simple-today-badge">TODAY</span>';
66487ac9bf3SAtari911                    }
665*e3a9f44cSAtari911
666*e3a9f44cSAtari911                    // Namespace badge (show individual event's namespace)
667*e3a9f44cSAtari911                    $eventNamespace = isset($event['namespace']) ? $event['namespace'] : '';
668*e3a9f44cSAtari911                    if (!$eventNamespace && isset($event['_namespace'])) {
669*e3a9f44cSAtari911                        $eventNamespace = $event['_namespace']; // Fallback to _namespace for multi-namespace loading
67019378907SAtari911                    }
671*e3a9f44cSAtari911                    if ($eventNamespace) {
672*e3a9f44cSAtari911                        $html .= ' <span class="eventlist-simple-namespace">' . htmlspecialchars($eventNamespace) . '</span>';
673*e3a9f44cSAtari911                    }
674*e3a9f44cSAtari911
675*e3a9f44cSAtari911                    $html .= '</div>'; // header
676*e3a9f44cSAtari911
677*e3a9f44cSAtari911                    // Line 2: Body (Description only) - only show if description exists
678*e3a9f44cSAtari911                    if (!empty($event['description'])) {
679*e3a9f44cSAtari911                        $html .= '<div class="eventlist-simple-body">' . $this->renderDescription($event['description']) . '</div>';
680*e3a9f44cSAtari911                    }
681*e3a9f44cSAtari911
682*e3a9f44cSAtari911                    $html .= '</div>'; // item
68319378907SAtari911                }
68419378907SAtari911            }
68587ac9bf3SAtari911        }
68619378907SAtari911
687*e3a9f44cSAtari911        $html .= '</div>'; // eventlist-simple
68819378907SAtari911
68919378907SAtari911        return $html;
69019378907SAtari911    }
69119378907SAtari911
69219378907SAtari911    private function renderEventDialog($calId, $namespace) {
69319378907SAtari911        $html = '<div class="event-dialog-compact" id="dialog-' . $calId . '" style="display:none;">';
69419378907SAtari911        $html .= '<div class="dialog-overlay" onclick="closeEventDialog(\'' . $calId . '\')"></div>';
69519378907SAtari911
69619378907SAtari911        // Draggable dialog
69719378907SAtari911        $html .= '<div class="dialog-content-sleek" id="dialog-content-' . $calId . '">';
69819378907SAtari911
69919378907SAtari911        // Header with drag handle and close button
70019378907SAtari911        $html .= '<div class="dialog-header-sleek dialog-drag-handle" id="drag-handle-' . $calId . '">';
70119378907SAtari911        $html .= '<h3 id="dialog-title-' . $calId . '">Add Event</h3>';
70219378907SAtari911        $html .= '<button type="button" class="dialog-close-btn" onclick="closeEventDialog(\'' . $calId . '\')">×</button>';
70319378907SAtari911        $html .= '</div>';
70419378907SAtari911
70519378907SAtari911        // Form content
70619378907SAtari911        $html .= '<form id="eventform-' . $calId . '" onsubmit="saveEventCompact(\'' . $calId . '\', \'' . $namespace . '\'); return false;" class="sleek-form">';
70719378907SAtari911
70819378907SAtari911        // Hidden ID field
70919378907SAtari911        $html .= '<input type="hidden" id="event-id-' . $calId . '" name="eventId" value="">';
71019378907SAtari911
71119378907SAtari911        // Task checkbox
71219378907SAtari911        $html .= '<div class="form-field form-field-checkbox">';
71319378907SAtari911        $html .= '<label class="checkbox-label">';
71419378907SAtari911        $html .= '<input type="checkbox" id="event-is-task-' . $calId . '" name="isTask" class="task-toggle">';
71519378907SAtari911        $html .= '<span>�� This is a task (can be checked off)</span>';
71619378907SAtari911        $html .= '</label>';
71719378907SAtari911        $html .= '</div>';
71819378907SAtari911
71919378907SAtari911        // Date and Time in a row
72019378907SAtari911        $html .= '<div class="form-row-group">';
72119378907SAtari911
72219378907SAtari911        // Start Date field
72319378907SAtari911        $html .= '<div class="form-field form-field-date">';
72419378907SAtari911        $html .= '<label class="field-label">�� Start Date</label>';
72519378907SAtari911        $html .= '<input type="date" id="event-date-' . $calId . '" name="date" required class="input-sleek input-date">';
72619378907SAtari911        $html .= '</div>';
72719378907SAtari911
72819378907SAtari911        // End Date field (for multi-day events)
72919378907SAtari911        $html .= '<div class="form-field form-field-date">';
73019378907SAtari911        $html .= '<label class="field-label">�� End Date</label>';
73119378907SAtari911        $html .= '<input type="date" id="event-end-date-' . $calId . '" name="endDate" class="input-sleek input-date">';
73219378907SAtari911        $html .= '</div>';
73319378907SAtari911
73419378907SAtari911        $html .= '</div>';
73519378907SAtari911
73687ac9bf3SAtari911        // Recurring event section
73787ac9bf3SAtari911        $html .= '<div class="form-field form-field-checkbox">';
73887ac9bf3SAtari911        $html .= '<label class="checkbox-label">';
73987ac9bf3SAtari911        $html .= '<input type="checkbox" id="event-recurring-' . $calId . '" name="isRecurring" class="recurring-toggle" onchange="toggleRecurringOptions(\'' . $calId . '\')">';
74087ac9bf3SAtari911        $html .= '<span>�� Repeating Event</span>';
74187ac9bf3SAtari911        $html .= '</label>';
74287ac9bf3SAtari911        $html .= '</div>';
74387ac9bf3SAtari911
74487ac9bf3SAtari911        // Recurring options (hidden by default)
74587ac9bf3SAtari911        $html .= '<div id="recurring-options-' . $calId . '" class="recurring-options" style="display:none;">';
74687ac9bf3SAtari911
74787ac9bf3SAtari911        // Recurrence pattern
74887ac9bf3SAtari911        $html .= '<div class="form-field">';
74987ac9bf3SAtari911        $html .= '<label class="field-label">Repeat Every</label>';
75087ac9bf3SAtari911        $html .= '<select id="event-recurrence-type-' . $calId . '" name="recurrenceType" class="input-sleek">';
75187ac9bf3SAtari911        $html .= '<option value="daily">Daily</option>';
75287ac9bf3SAtari911        $html .= '<option value="weekly">Weekly</option>';
75387ac9bf3SAtari911        $html .= '<option value="monthly">Monthly</option>';
75487ac9bf3SAtari911        $html .= '<option value="yearly">Yearly</option>';
75587ac9bf3SAtari911        $html .= '</select>';
75687ac9bf3SAtari911        $html .= '</div>';
75787ac9bf3SAtari911
75887ac9bf3SAtari911        // Recurrence end date
75987ac9bf3SAtari911        $html .= '<div class="form-field">';
76087ac9bf3SAtari911        $html .= '<label class="field-label">�� Repeat Until (optional)</label>';
76187ac9bf3SAtari911        $html .= '<input type="date" id="event-recurrence-end-' . $calId . '" name="recurrenceEnd" class="input-sleek input-date">';
76287ac9bf3SAtari911        $html .= '</div>';
76387ac9bf3SAtari911
76487ac9bf3SAtari911        $html .= '</div>';
76587ac9bf3SAtari911
766*e3a9f44cSAtari911        // Time field - dropdown with 15-minute intervals
76719378907SAtari911        $html .= '<div class="form-field">';
76819378907SAtari911        $html .= '<label class="field-label">�� Time (optional)</label>';
769*e3a9f44cSAtari911        $html .= '<select id="event-time-' . $calId . '" name="time" class="input-sleek">';
770*e3a9f44cSAtari911        $html .= '<option value="">No specific time</option>';
771*e3a9f44cSAtari911
772*e3a9f44cSAtari911        // Generate time options in 15-minute intervals
773*e3a9f44cSAtari911        for ($hour = 0; $hour < 24; $hour++) {
774*e3a9f44cSAtari911            for ($minute = 0; $minute < 60; $minute += 15) {
775*e3a9f44cSAtari911                $timeValue = sprintf('%02d:%02d', $hour, $minute);
776*e3a9f44cSAtari911                $displayHour = $hour == 0 ? 12 : ($hour > 12 ? $hour - 12 : $hour);
777*e3a9f44cSAtari911                $ampm = $hour < 12 ? 'AM' : 'PM';
778*e3a9f44cSAtari911                $displayTime = sprintf('%d:%02d %s', $displayHour, $minute, $ampm);
779*e3a9f44cSAtari911                $html .= '<option value="' . $timeValue . '">' . $displayTime . '</option>';
780*e3a9f44cSAtari911            }
781*e3a9f44cSAtari911        }
782*e3a9f44cSAtari911
783*e3a9f44cSAtari911        $html .= '</select>';
78419378907SAtari911        $html .= '</div>';
78519378907SAtari911
78619378907SAtari911        // Title field
78719378907SAtari911        $html .= '<div class="form-field">';
78819378907SAtari911        $html .= '<label class="field-label">�� Title</label>';
78919378907SAtari911        $html .= '<input type="text" id="event-title-' . $calId . '" name="title" required class="input-sleek" placeholder="Event or task title...">';
79019378907SAtari911        $html .= '</div>';
79119378907SAtari911
79219378907SAtari911        // Description field
79319378907SAtari911        $html .= '<div class="form-field">';
79419378907SAtari911        $html .= '<label class="field-label">�� Description</label>';
79519378907SAtari911        $html .= '<textarea id="event-desc-' . $calId . '" name="description" rows="3" class="input-sleek textarea-sleek" placeholder="Add details (optional)..."></textarea>';
79619378907SAtari911        $html .= '</div>';
79719378907SAtari911
79819378907SAtari911        // Color picker
79919378907SAtari911        $html .= '<div class="form-field">';
80019378907SAtari911        $html .= '<label class="field-label">�� Color</label>';
80119378907SAtari911        $html .= '<div class="color-picker-container">';
80219378907SAtari911        $html .= '<input type="color" id="event-color-' . $calId . '" name="color" value="#3498db" class="input-color-sleek">';
80319378907SAtari911        $html .= '<span class="color-label">Choose event color</span>';
80419378907SAtari911        $html .= '</div>';
80519378907SAtari911        $html .= '</div>';
80619378907SAtari911
80719378907SAtari911        // Action buttons
80819378907SAtari911        $html .= '<div class="dialog-actions-sleek">';
80919378907SAtari911        $html .= '<button type="button" class="btn-sleek btn-cancel-sleek" onclick="closeEventDialog(\'' . $calId . '\')">Cancel</button>';
81019378907SAtari911        $html .= '<button type="submit" class="btn-sleek btn-save-sleek">�� Save</button>';
81119378907SAtari911        $html .= '</div>';
81219378907SAtari911
81319378907SAtari911        $html .= '</form>';
81419378907SAtari911        $html .= '</div>';
81519378907SAtari911        $html .= '</div>';
81619378907SAtari911
81719378907SAtari911        return $html;
81819378907SAtari911    }
81919378907SAtari911
82087ac9bf3SAtari911    private function renderMonthPicker($calId, $year, $month, $namespace) {
82187ac9bf3SAtari911        $html = '<div class="month-picker-overlay" id="month-picker-overlay-' . $calId . '" style="display:none;" onclick="closeMonthPicker(\'' . $calId . '\')">';
82287ac9bf3SAtari911        $html .= '<div class="month-picker-dialog" onclick="event.stopPropagation();">';
82387ac9bf3SAtari911        $html .= '<h4>Jump to Month</h4>';
82487ac9bf3SAtari911
82587ac9bf3SAtari911        $html .= '<div class="month-picker-selects">';
82687ac9bf3SAtari911        $html .= '<select id="month-picker-month-' . $calId . '" class="month-picker-select">';
82787ac9bf3SAtari911        $monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
82887ac9bf3SAtari911        for ($m = 1; $m <= 12; $m++) {
82987ac9bf3SAtari911            $selected = ($m == $month) ? ' selected' : '';
83087ac9bf3SAtari911            $html .= '<option value="' . $m . '"' . $selected . '>' . $monthNames[$m - 1] . '</option>';
83187ac9bf3SAtari911        }
83287ac9bf3SAtari911        $html .= '</select>';
83387ac9bf3SAtari911
83487ac9bf3SAtari911        $html .= '<select id="month-picker-year-' . $calId . '" class="month-picker-select">';
83587ac9bf3SAtari911        $currentYear = (int)date('Y');
83687ac9bf3SAtari911        for ($y = $currentYear - 5; $y <= $currentYear + 5; $y++) {
83787ac9bf3SAtari911            $selected = ($y == $year) ? ' selected' : '';
83887ac9bf3SAtari911            $html .= '<option value="' . $y . '"' . $selected . '>' . $y . '</option>';
83987ac9bf3SAtari911        }
84087ac9bf3SAtari911        $html .= '</select>';
84187ac9bf3SAtari911        $html .= '</div>';
84287ac9bf3SAtari911
84387ac9bf3SAtari911        $html .= '<div class="month-picker-actions">';
84487ac9bf3SAtari911        $html .= '<button class="btn-sleek btn-cancel-sleek" onclick="closeMonthPicker(\'' . $calId . '\')">Cancel</button>';
84587ac9bf3SAtari911        $html .= '<button class="btn-sleek btn-save-sleek" onclick="jumpToSelectedMonth(\'' . $calId . '\', \'' . $namespace . '\')">Go</button>';
84687ac9bf3SAtari911        $html .= '</div>';
84787ac9bf3SAtari911
84887ac9bf3SAtari911        $html .= '</div>';
84987ac9bf3SAtari911        $html .= '</div>';
85087ac9bf3SAtari911
85187ac9bf3SAtari911        return $html;
85287ac9bf3SAtari911    }
85387ac9bf3SAtari911
85419378907SAtari911    private function renderDescription($description) {
85519378907SAtari911        if (empty($description)) {
85619378907SAtari911            return '';
85719378907SAtari911        }
85819378907SAtari911
859*e3a9f44cSAtari911        // Token-based parsing to avoid escaping issues
860*e3a9f44cSAtari911        $rendered = $description;
861*e3a9f44cSAtari911        $tokens = array();
862*e3a9f44cSAtari911        $tokenIndex = 0;
86319378907SAtari911
864*e3a9f44cSAtari911        // Convert DokuWiki image syntax {{image.jpg}} to tokens
865*e3a9f44cSAtari911        $pattern = '/\{\{([^}|]+?)(?:\|([^}]+))?\}\}/';
866*e3a9f44cSAtari911        preg_match_all($pattern, $rendered, $matches, PREG_SET_ORDER);
867*e3a9f44cSAtari911        foreach ($matches as $match) {
868*e3a9f44cSAtari911            $imagePath = trim($match[1]);
869*e3a9f44cSAtari911            $alt = isset($match[2]) ? trim($match[2]) : '';
87019378907SAtari911
871*e3a9f44cSAtari911            // Handle external URLs
87219378907SAtari911            if (preg_match('/^https?:\/\//', $imagePath)) {
873*e3a9f44cSAtari911                $imageHtml = '<img src="' . htmlspecialchars($imagePath) . '" alt="' . htmlspecialchars($alt) . '" class="event-image" />';
874*e3a9f44cSAtari911            } else {
87519378907SAtari911                // Handle internal DokuWiki images
87619378907SAtari911                $imageUrl = DOKU_BASE . 'lib/exe/fetch.php?media=' . rawurlencode($imagePath);
877*e3a9f44cSAtari911                $imageHtml = '<img src="' . $imageUrl . '" alt="' . htmlspecialchars($alt) . '" class="event-image" />';
878*e3a9f44cSAtari911            }
87919378907SAtari911
880*e3a9f44cSAtari911            $token = "\x00TOKEN" . $tokenIndex . "\x00";
881*e3a9f44cSAtari911            $tokens[$tokenIndex] = $imageHtml;
882*e3a9f44cSAtari911            $tokenIndex++;
883*e3a9f44cSAtari911            $rendered = str_replace($match[0], $token, $rendered);
884*e3a9f44cSAtari911        }
885*e3a9f44cSAtari911
886*e3a9f44cSAtari911        // Convert DokuWiki link syntax [[link|text]] to tokens
887*e3a9f44cSAtari911        $pattern = '/\[\[([^|\]]+?)(?:\|([^\]]+))?\]\]/';
888*e3a9f44cSAtari911        preg_match_all($pattern, $rendered, $matches, PREG_SET_ORDER);
889*e3a9f44cSAtari911        foreach ($matches as $match) {
890*e3a9f44cSAtari911            $link = trim($match[1]);
891*e3a9f44cSAtari911            $text = isset($match[2]) ? trim($match[2]) : $link;
89219378907SAtari911
89319378907SAtari911            // Handle external URLs
89419378907SAtari911            if (preg_match('/^https?:\/\//', $link)) {
895*e3a9f44cSAtari911                $linkHtml = '<a href="' . htmlspecialchars($link) . '" target="_blank" rel="noopener noreferrer">' . htmlspecialchars($text) . '</a>';
896*e3a9f44cSAtari911            } else {
89787ac9bf3SAtari911                // Handle internal DokuWiki links with section anchors
89887ac9bf3SAtari911                $parts = explode('#', $link, 2);
89987ac9bf3SAtari911                $pagePart = $parts[0];
90087ac9bf3SAtari911                $sectionPart = isset($parts[1]) ? '#' . $parts[1] : '';
90187ac9bf3SAtari911
90287ac9bf3SAtari911                $wikiUrl = DOKU_BASE . 'doku.php?id=' . rawurlencode($pagePart) . $sectionPart;
903*e3a9f44cSAtari911                $linkHtml = '<a href="' . $wikiUrl . '">' . htmlspecialchars($text) . '</a>';
90419378907SAtari911            }
90519378907SAtari911
906*e3a9f44cSAtari911            $token = "\x00TOKEN" . $tokenIndex . "\x00";
907*e3a9f44cSAtari911            $tokens[$tokenIndex] = $linkHtml;
908*e3a9f44cSAtari911            $tokenIndex++;
909*e3a9f44cSAtari911            $rendered = str_replace($match[0], $token, $rendered);
910*e3a9f44cSAtari911        }
91119378907SAtari911
912*e3a9f44cSAtari911        // Convert markdown-style links [text](url) to tokens
913*e3a9f44cSAtari911        $pattern = '/\[([^\]]+)\]\(([^)]+)\)/';
914*e3a9f44cSAtari911        preg_match_all($pattern, $rendered, $matches, PREG_SET_ORDER);
915*e3a9f44cSAtari911        foreach ($matches as $match) {
916*e3a9f44cSAtari911            $text = trim($match[1]);
917*e3a9f44cSAtari911            $url = trim($match[2]);
91819378907SAtari911
919*e3a9f44cSAtari911            if (preg_match('/^https?:\/\//', $url)) {
920*e3a9f44cSAtari911                $linkHtml = '<a href="' . htmlspecialchars($url) . '" target="_blank" rel="noopener noreferrer">' . htmlspecialchars($text) . '</a>';
921*e3a9f44cSAtari911            } else {
922*e3a9f44cSAtari911                $linkHtml = '<a href="' . htmlspecialchars($url) . '">' . htmlspecialchars($text) . '</a>';
923*e3a9f44cSAtari911            }
924*e3a9f44cSAtari911
925*e3a9f44cSAtari911            $token = "\x00TOKEN" . $tokenIndex . "\x00";
926*e3a9f44cSAtari911            $tokens[$tokenIndex] = $linkHtml;
927*e3a9f44cSAtari911            $tokenIndex++;
928*e3a9f44cSAtari911            $rendered = str_replace($match[0], $token, $rendered);
929*e3a9f44cSAtari911        }
930*e3a9f44cSAtari911
931*e3a9f44cSAtari911        // Convert plain URLs to tokens
932*e3a9f44cSAtari911        $pattern = '/(https?:\/\/[^\s<]+)/';
933*e3a9f44cSAtari911        preg_match_all($pattern, $rendered, $matches, PREG_SET_ORDER);
934*e3a9f44cSAtari911        foreach ($matches as $match) {
935*e3a9f44cSAtari911            $url = $match[1];
936*e3a9f44cSAtari911            $linkHtml = '<a href="' . htmlspecialchars($url) . '" target="_blank" rel="noopener noreferrer">' . htmlspecialchars($url) . '</a>';
937*e3a9f44cSAtari911
938*e3a9f44cSAtari911            $token = "\x00TOKEN" . $tokenIndex . "\x00";
939*e3a9f44cSAtari911            $tokens[$tokenIndex] = $linkHtml;
940*e3a9f44cSAtari911            $tokenIndex++;
941*e3a9f44cSAtari911            $rendered = str_replace($match[0], $token, $rendered);
942*e3a9f44cSAtari911        }
943*e3a9f44cSAtari911
944*e3a9f44cSAtari911        // NOW escape HTML (tokens are protected)
945*e3a9f44cSAtari911        $rendered = htmlspecialchars($rendered);
946*e3a9f44cSAtari911
947*e3a9f44cSAtari911        // Convert newlines to <br>
948*e3a9f44cSAtari911        $rendered = nl2br($rendered);
949*e3a9f44cSAtari911
950*e3a9f44cSAtari911        // DokuWiki text formatting
951*e3a9f44cSAtari911        // Bold: **text** or __text__
952*e3a9f44cSAtari911        $rendered = preg_replace('/\*\*(.+?)\*\*/', '<strong>$1</strong>', $rendered);
953*e3a9f44cSAtari911        $rendered = preg_replace('/__(.+?)__/', '<strong>$1</strong>', $rendered);
954*e3a9f44cSAtari911
955*e3a9f44cSAtari911        // Italic: //text//
956*e3a9f44cSAtari911        $rendered = preg_replace('/\/\/(.+?)\/\//', '<em>$1</em>', $rendered);
957*e3a9f44cSAtari911
958*e3a9f44cSAtari911        // Strikethrough: <del>text</del>
959*e3a9f44cSAtari911        $rendered = preg_replace('/&lt;del&gt;(.+?)&lt;\/del&gt;/', '<del>$1</del>', $rendered);
960*e3a9f44cSAtari911
961*e3a9f44cSAtari911        // Monospace: ''text''
962*e3a9f44cSAtari911        $rendered = preg_replace('/&#039;&#039;(.+?)&#039;&#039;/', '<code>$1</code>', $rendered);
963*e3a9f44cSAtari911
964*e3a9f44cSAtari911        // Subscript: <sub>text</sub>
965*e3a9f44cSAtari911        $rendered = preg_replace('/&lt;sub&gt;(.+?)&lt;\/sub&gt;/', '<sub>$1</sub>', $rendered);
966*e3a9f44cSAtari911
967*e3a9f44cSAtari911        // Superscript: <sup>text</sup>
968*e3a9f44cSAtari911        $rendered = preg_replace('/&lt;sup&gt;(.+?)&lt;\/sup&gt;/', '<sup>$1</sup>', $rendered);
969*e3a9f44cSAtari911
970*e3a9f44cSAtari911        // Restore tokens
971*e3a9f44cSAtari911        foreach ($tokens as $i => $html) {
972*e3a9f44cSAtari911            $rendered = str_replace("\x00TOKEN" . $i . "\x00", $html, $rendered);
973*e3a9f44cSAtari911        }
97419378907SAtari911
97519378907SAtari911        return $rendered;
97619378907SAtari911    }
97719378907SAtari911
97819378907SAtari911    private function loadEvents($namespace, $year, $month) {
97919378907SAtari911        $dataDir = DOKU_INC . 'data/meta/';
98019378907SAtari911        if ($namespace) {
98119378907SAtari911            $dataDir .= str_replace(':', '/', $namespace) . '/';
98219378907SAtari911        }
98319378907SAtari911        $dataDir .= 'calendar/';
98419378907SAtari911
98519378907SAtari911        $eventFile = $dataDir . sprintf('%04d-%02d.json', $year, $month);
98619378907SAtari911
98719378907SAtari911        if (file_exists($eventFile)) {
98819378907SAtari911            $json = file_get_contents($eventFile);
98919378907SAtari911            return json_decode($json, true);
99019378907SAtari911        }
99119378907SAtari911
99219378907SAtari911        return array();
99319378907SAtari911    }
994*e3a9f44cSAtari911
995*e3a9f44cSAtari911    private function loadEventsMultiNamespace($namespaces, $year, $month) {
996*e3a9f44cSAtari911        // Check for wildcard pattern (namespace:*)
997*e3a9f44cSAtari911        if (preg_match('/^(.+):\*$/', $namespaces, $matches)) {
998*e3a9f44cSAtari911            $baseNamespace = $matches[1];
999*e3a9f44cSAtari911            return $this->loadEventsWildcard($baseNamespace, $year, $month);
1000*e3a9f44cSAtari911        }
1001*e3a9f44cSAtari911
1002*e3a9f44cSAtari911        // Check for root wildcard (just *)
1003*e3a9f44cSAtari911        if ($namespaces === '*') {
1004*e3a9f44cSAtari911            return $this->loadEventsWildcard('', $year, $month);
1005*e3a9f44cSAtari911        }
1006*e3a9f44cSAtari911
1007*e3a9f44cSAtari911        // Parse namespace list (semicolon separated)
1008*e3a9f44cSAtari911        // e.g., "team:projects;personal;work:tasks" = three namespaces
1009*e3a9f44cSAtari911        $namespaceList = array_map('trim', explode(';', $namespaces));
1010*e3a9f44cSAtari911
1011*e3a9f44cSAtari911        // Load events from all namespaces
1012*e3a9f44cSAtari911        $allEvents = array();
1013*e3a9f44cSAtari911        foreach ($namespaceList as $ns) {
1014*e3a9f44cSAtari911            $ns = trim($ns);
1015*e3a9f44cSAtari911            if (empty($ns)) continue;
1016*e3a9f44cSAtari911
1017*e3a9f44cSAtari911            $events = $this->loadEvents($ns, $year, $month);
1018*e3a9f44cSAtari911
1019*e3a9f44cSAtari911            // Add namespace tag to each event
1020*e3a9f44cSAtari911            foreach ($events as $dateKey => $dayEvents) {
1021*e3a9f44cSAtari911                if (!isset($allEvents[$dateKey])) {
1022*e3a9f44cSAtari911                    $allEvents[$dateKey] = array();
1023*e3a9f44cSAtari911                }
1024*e3a9f44cSAtari911                foreach ($dayEvents as $event) {
1025*e3a9f44cSAtari911                    $event['_namespace'] = $ns;
1026*e3a9f44cSAtari911                    $allEvents[$dateKey][] = $event;
1027*e3a9f44cSAtari911                }
1028*e3a9f44cSAtari911            }
1029*e3a9f44cSAtari911        }
1030*e3a9f44cSAtari911
1031*e3a9f44cSAtari911        return $allEvents;
1032*e3a9f44cSAtari911    }
1033*e3a9f44cSAtari911
1034*e3a9f44cSAtari911    private function loadEventsWildcard($baseNamespace, $year, $month) {
1035*e3a9f44cSAtari911        // Find all subdirectories under the base namespace
1036*e3a9f44cSAtari911        $dataDir = DOKU_INC . 'data/meta/';
1037*e3a9f44cSAtari911        if ($baseNamespace) {
1038*e3a9f44cSAtari911            $dataDir .= str_replace(':', '/', $baseNamespace) . '/';
1039*e3a9f44cSAtari911        }
1040*e3a9f44cSAtari911
1041*e3a9f44cSAtari911        $allEvents = array();
1042*e3a9f44cSAtari911
1043*e3a9f44cSAtari911        // First, load events from the base namespace itself
1044*e3a9f44cSAtari911        if (empty($baseNamespace)) {
1045*e3a9f44cSAtari911            // Root wildcard - load from root calendar
1046*e3a9f44cSAtari911            $events = $this->loadEvents('', $year, $month);
1047*e3a9f44cSAtari911            foreach ($events as $dateKey => $dayEvents) {
1048*e3a9f44cSAtari911                if (!isset($allEvents[$dateKey])) {
1049*e3a9f44cSAtari911                    $allEvents[$dateKey] = array();
1050*e3a9f44cSAtari911                }
1051*e3a9f44cSAtari911                foreach ($dayEvents as $event) {
1052*e3a9f44cSAtari911                    $event['_namespace'] = '';
1053*e3a9f44cSAtari911                    $allEvents[$dateKey][] = $event;
1054*e3a9f44cSAtari911                }
1055*e3a9f44cSAtari911            }
1056*e3a9f44cSAtari911        } else {
1057*e3a9f44cSAtari911            $events = $this->loadEvents($baseNamespace, $year, $month);
1058*e3a9f44cSAtari911            foreach ($events as $dateKey => $dayEvents) {
1059*e3a9f44cSAtari911                if (!isset($allEvents[$dateKey])) {
1060*e3a9f44cSAtari911                    $allEvents[$dateKey] = array();
1061*e3a9f44cSAtari911                }
1062*e3a9f44cSAtari911                foreach ($dayEvents as $event) {
1063*e3a9f44cSAtari911                    $event['_namespace'] = $baseNamespace;
1064*e3a9f44cSAtari911                    $allEvents[$dateKey][] = $event;
1065*e3a9f44cSAtari911                }
1066*e3a9f44cSAtari911            }
1067*e3a9f44cSAtari911        }
1068*e3a9f44cSAtari911
1069*e3a9f44cSAtari911        // Recursively find all subdirectories
1070*e3a9f44cSAtari911        $this->findSubNamespaces($dataDir, $baseNamespace, $year, $month, $allEvents);
1071*e3a9f44cSAtari911
1072*e3a9f44cSAtari911        return $allEvents;
1073*e3a9f44cSAtari911    }
1074*e3a9f44cSAtari911
1075*e3a9f44cSAtari911    private function findSubNamespaces($dir, $baseNamespace, $year, $month, &$allEvents) {
1076*e3a9f44cSAtari911        if (!is_dir($dir)) return;
1077*e3a9f44cSAtari911
1078*e3a9f44cSAtari911        $items = scandir($dir);
1079*e3a9f44cSAtari911        foreach ($items as $item) {
1080*e3a9f44cSAtari911            if ($item === '.' || $item === '..') continue;
1081*e3a9f44cSAtari911
1082*e3a9f44cSAtari911            $path = $dir . $item;
1083*e3a9f44cSAtari911            if (is_dir($path) && $item !== 'calendar') {
1084*e3a9f44cSAtari911                // This is a namespace directory
1085*e3a9f44cSAtari911                $namespace = $baseNamespace ? $baseNamespace . ':' . $item : $item;
1086*e3a9f44cSAtari911
1087*e3a9f44cSAtari911                // Load events from this namespace
1088*e3a9f44cSAtari911                $events = $this->loadEvents($namespace, $year, $month);
1089*e3a9f44cSAtari911                foreach ($events as $dateKey => $dayEvents) {
1090*e3a9f44cSAtari911                    if (!isset($allEvents[$dateKey])) {
1091*e3a9f44cSAtari911                        $allEvents[$dateKey] = array();
1092*e3a9f44cSAtari911                    }
1093*e3a9f44cSAtari911                    foreach ($dayEvents as $event) {
1094*e3a9f44cSAtari911                        $event['_namespace'] = $namespace;
1095*e3a9f44cSAtari911                        $allEvents[$dateKey][] = $event;
1096*e3a9f44cSAtari911                    }
1097*e3a9f44cSAtari911                }
1098*e3a9f44cSAtari911
1099*e3a9f44cSAtari911                // Recurse into subdirectories
1100*e3a9f44cSAtari911                $this->findSubNamespaces($path . '/', $namespace, $year, $month, $allEvents);
1101*e3a9f44cSAtari911            }
1102*e3a9f44cSAtari911        }
1103*e3a9f44cSAtari911    }
110419378907SAtari911}
1105