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' => '', 50e3a9f44cSAtari911 'date' => '', 51e3a9f44cSAtari911 '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 90e3a9f44cSAtari911 // Check if multiple namespaces or wildcard specified 91e3a9f44cSAtari911 $isMultiNamespace = !empty($namespace) && (strpos($namespace, ';') !== false || strpos($namespace, '*') !== false); 92e3a9f44cSAtari911 93e3a9f44cSAtari911 if ($isMultiNamespace) { 94e3a9f44cSAtari911 $events = $this->loadEventsMultiNamespace($namespace, $year, $month); 95e3a9f44cSAtari911 } else { 9619378907SAtari911 $events = $this->loadEvents($namespace, $year, $month); 97e3a9f44cSAtari911 } 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 1161d05cddcSAtari911 $html = '<div class="calendar-compact-container" id="' . $calId . '" data-namespace="' . htmlspecialchars($namespace) . '" data-original-namespace="' . htmlspecialchars($namespace) . '" data-year="' . $year . '" data-month="' . $month . '">'; 1171d05cddcSAtari911 1181d05cddcSAtari911 // Load calendar JavaScript manually (not through DokuWiki concatenation) 1191d05cddcSAtari911 $html .= '<script src="' . DOKU_BASE . 'lib/plugins/calendar/calendar-main.js"></script>'; 1201d05cddcSAtari911 1211d05cddcSAtari911 // Initialize DOKU_BASE for JavaScript 1221d05cddcSAtari911 $html .= '<script>if(typeof DOKU_BASE==="undefined"){window.DOKU_BASE="' . DOKU_BASE . '";}</script>'; 12319378907SAtari911 12419378907SAtari911 // Embed events data as JSON for JavaScript access 12519378907SAtari911 $html .= '<script type="application/json" id="events-data-' . $calId . '">' . json_encode($events) . '</script>'; 12619378907SAtari911 12719378907SAtari911 // Left side: Calendar 12819378907SAtari911 $html .= '<div class="calendar-compact-left">'; 12919378907SAtari911 13019378907SAtari911 // Header with navigation 13119378907SAtari911 $html .= '<div class="calendar-compact-header">'; 13219378907SAtari911 $html .= '<button class="cal-nav-btn" onclick="navCalendar(\'' . $calId . '\', ' . $prevYear . ', ' . $prevMonth . ', \'' . $namespace . '\')">‹</button>'; 13387ac9bf3SAtari911 $html .= '<h3 class="calendar-month-picker" onclick="openMonthPicker(\'' . $calId . '\', ' . $year . ', ' . $month . ', \'' . $namespace . '\')" title="Click to jump to month">' . $monthName . '</h3>'; 13419378907SAtari911 $html .= '<button class="cal-nav-btn" onclick="navCalendar(\'' . $calId . '\', ' . $nextYear . ', ' . $nextMonth . ', \'' . $namespace . '\')">›</button>'; 13587ac9bf3SAtari911 $html .= '<button class="cal-today-btn" onclick="jumpToToday(\'' . $calId . '\', \'' . $namespace . '\')">Today</button>'; 13619378907SAtari911 $html .= '</div>'; 13719378907SAtari911 1381d05cddcSAtari911 // Namespace filter indicator - only show if actively filtering a specific namespace 1391d05cddcSAtari911 if ($namespace && $namespace !== '*' && strpos($namespace, '*') === false && strpos($namespace, ';') === false) { 1401d05cddcSAtari911 $html .= '<div class="calendar-namespace-filter" id="namespace-filter-' . $calId . '">'; 1411d05cddcSAtari911 $html .= '<span class="namespace-filter-label">Filtering:</span>'; 1421d05cddcSAtari911 $html .= '<span class="namespace-filter-name">' . htmlspecialchars($namespace) . '</span>'; 1431d05cddcSAtari911 $html .= '<button class="namespace-filter-clear" onclick="clearNamespaceFilter(\'' . $calId . '\')" title="Clear filter and show all namespaces">✕</button>'; 1441d05cddcSAtari911 $html .= '</div>'; 1451d05cddcSAtari911 } 1461d05cddcSAtari911 14719378907SAtari911 // Calendar grid 14819378907SAtari911 $html .= '<table class="calendar-compact-grid">'; 14919378907SAtari911 $html .= '<thead><tr>'; 15019378907SAtari911 $html .= '<th>S</th><th>M</th><th>T</th><th>W</th><th>T</th><th>F</th><th>S</th>'; 15119378907SAtari911 $html .= '</tr></thead><tbody>'; 15219378907SAtari911 15319378907SAtari911 $firstDay = mktime(0, 0, 0, $month, 1, $year); 15419378907SAtari911 $daysInMonth = date('t', $firstDay); 15519378907SAtari911 $dayOfWeek = date('w', $firstDay); 15619378907SAtari911 157e3a9f44cSAtari911 // Build a map of all events with their date ranges for the calendar grid 15887ac9bf3SAtari911 $eventRanges = array(); 159e3a9f44cSAtari911 foreach ($events as $dateKey => $dayEvents) { 16087ac9bf3SAtari911 foreach ($dayEvents as $evt) { 16187ac9bf3SAtari911 $eventId = isset($evt['id']) ? $evt['id'] : ''; 16287ac9bf3SAtari911 $startDate = $dateKey; 16387ac9bf3SAtari911 $endDate = isset($evt['endDate']) && $evt['endDate'] ? $evt['endDate'] : $dateKey; 16487ac9bf3SAtari911 16587ac9bf3SAtari911 // Only process events that touch this month 16687ac9bf3SAtari911 $eventStart = new DateTime($startDate); 16787ac9bf3SAtari911 $eventEnd = new DateTime($endDate); 16887ac9bf3SAtari911 $monthStart = new DateTime(sprintf('%04d-%02d-01', $year, $month)); 16987ac9bf3SAtari911 $monthEnd = new DateTime(sprintf('%04d-%02d-%02d', $year, $month, $daysInMonth)); 17087ac9bf3SAtari911 17187ac9bf3SAtari911 // Skip if event doesn't overlap with current month 17287ac9bf3SAtari911 if ($eventEnd < $monthStart || $eventStart > $monthEnd) { 17387ac9bf3SAtari911 continue; 17487ac9bf3SAtari911 } 17587ac9bf3SAtari911 17687ac9bf3SAtari911 // Create entry for each day the event spans 17787ac9bf3SAtari911 $current = clone $eventStart; 17887ac9bf3SAtari911 while ($current <= $eventEnd) { 17987ac9bf3SAtari911 $currentKey = $current->format('Y-m-d'); 18087ac9bf3SAtari911 18187ac9bf3SAtari911 // Check if this date is in current month 18287ac9bf3SAtari911 $currentDate = DateTime::createFromFormat('Y-m-d', $currentKey); 18387ac9bf3SAtari911 if ($currentDate && $currentDate->format('Y-m') === sprintf('%04d-%02d', $year, $month)) { 18487ac9bf3SAtari911 if (!isset($eventRanges[$currentKey])) { 18587ac9bf3SAtari911 $eventRanges[$currentKey] = array(); 18687ac9bf3SAtari911 } 18787ac9bf3SAtari911 18887ac9bf3SAtari911 // Add event with span information 18987ac9bf3SAtari911 $evt['_span_start'] = $startDate; 19087ac9bf3SAtari911 $evt['_span_end'] = $endDate; 19187ac9bf3SAtari911 $evt['_is_first_day'] = ($currentKey === $startDate); 19287ac9bf3SAtari911 $evt['_is_last_day'] = ($currentKey === $endDate); 19387ac9bf3SAtari911 $evt['_original_date'] = $dateKey; // Keep track of original date 19487ac9bf3SAtari911 19587ac9bf3SAtari911 // Check if event continues from previous month or to next month 19687ac9bf3SAtari911 $evt['_continues_from_prev'] = ($eventStart < $monthStart); 19787ac9bf3SAtari911 $evt['_continues_to_next'] = ($eventEnd > $monthEnd); 19887ac9bf3SAtari911 19987ac9bf3SAtari911 $eventRanges[$currentKey][] = $evt; 20087ac9bf3SAtari911 } 20187ac9bf3SAtari911 20287ac9bf3SAtari911 $current->modify('+1 day'); 20387ac9bf3SAtari911 } 20487ac9bf3SAtari911 } 20587ac9bf3SAtari911 } 20687ac9bf3SAtari911 20719378907SAtari911 $currentDay = 1; 20819378907SAtari911 $rowCount = ceil(($daysInMonth + $dayOfWeek) / 7); 20919378907SAtari911 21019378907SAtari911 for ($row = 0; $row < $rowCount; $row++) { 21119378907SAtari911 $html .= '<tr>'; 21219378907SAtari911 for ($col = 0; $col < 7; $col++) { 21319378907SAtari911 if (($row === 0 && $col < $dayOfWeek) || $currentDay > $daysInMonth) { 21419378907SAtari911 $html .= '<td class="cal-empty"></td>'; 21519378907SAtari911 } else { 21619378907SAtari911 $dateKey = sprintf('%04d-%02d-%02d', $year, $month, $currentDay); 21719378907SAtari911 $isToday = ($dateKey === date('Y-m-d')); 21887ac9bf3SAtari911 $hasEvents = isset($eventRanges[$dateKey]) && !empty($eventRanges[$dateKey]); 21919378907SAtari911 22019378907SAtari911 $classes = 'cal-day'; 22119378907SAtari911 if ($isToday) $classes .= ' cal-today'; 22219378907SAtari911 if ($hasEvents) $classes .= ' cal-has-events'; 22319378907SAtari911 22419378907SAtari911 $html .= '<td class="' . $classes . '" data-date="' . $dateKey . '" onclick="showDayPopup(\'' . $calId . '\', \'' . $dateKey . '\', \'' . $namespace . '\')">'; 22519378907SAtari911 $html .= '<span class="day-num">' . $currentDay . '</span>'; 22619378907SAtari911 22719378907SAtari911 if ($hasEvents) { 22819378907SAtari911 // Sort events by time (no time first, then by time) 22987ac9bf3SAtari911 $sortedEvents = $eventRanges[$dateKey]; 23019378907SAtari911 usort($sortedEvents, function($a, $b) { 23119378907SAtari911 $timeA = isset($a['time']) ? $a['time'] : ''; 23219378907SAtari911 $timeB = isset($b['time']) ? $b['time'] : ''; 23319378907SAtari911 23419378907SAtari911 // Events without time go first 23519378907SAtari911 if (empty($timeA) && !empty($timeB)) return -1; 23619378907SAtari911 if (!empty($timeA) && empty($timeB)) return 1; 23719378907SAtari911 if (empty($timeA) && empty($timeB)) return 0; 23819378907SAtari911 23919378907SAtari911 // Sort by time 24019378907SAtari911 return strcmp($timeA, $timeB); 24119378907SAtari911 }); 24219378907SAtari911 24319378907SAtari911 // Show colored stacked bars for each event 24419378907SAtari911 $html .= '<div class="event-indicators">'; 24519378907SAtari911 foreach ($sortedEvents as $evt) { 24619378907SAtari911 $eventId = isset($evt['id']) ? $evt['id'] : ''; 24719378907SAtari911 $eventColor = isset($evt['color']) ? htmlspecialchars($evt['color']) : '#3498db'; 24819378907SAtari911 $eventTime = isset($evt['time']) ? $evt['time'] : ''; 24919378907SAtari911 $eventTitle = isset($evt['title']) ? htmlspecialchars($evt['title']) : 'Event'; 25087ac9bf3SAtari911 $originalDate = isset($evt['_original_date']) ? $evt['_original_date'] : $dateKey; 25187ac9bf3SAtari911 $isFirstDay = isset($evt['_is_first_day']) ? $evt['_is_first_day'] : true; 25287ac9bf3SAtari911 $isLastDay = isset($evt['_is_last_day']) ? $evt['_is_last_day'] : true; 25319378907SAtari911 25419378907SAtari911 $barClass = empty($eventTime) ? 'event-bar-no-time' : 'event-bar-timed'; 25519378907SAtari911 25687ac9bf3SAtari911 // Add classes for multi-day spanning 25787ac9bf3SAtari911 if (!$isFirstDay) $barClass .= ' event-bar-continues'; 25887ac9bf3SAtari911 if (!$isLastDay) $barClass .= ' event-bar-continuing'; 25987ac9bf3SAtari911 26019378907SAtari911 $html .= '<span class="event-bar ' . $barClass . '" '; 26119378907SAtari911 $html .= 'style="background: ' . $eventColor . ';" '; 26219378907SAtari911 $html .= 'title="' . $eventTitle . ($eventTime ? ' @ ' . $eventTime : '') . '" '; 26387ac9bf3SAtari911 $html .= 'onclick="event.stopPropagation(); highlightEvent(\'' . $calId . '\', \'' . $eventId . '\', \'' . $originalDate . '\');">'; 26419378907SAtari911 $html .= '</span>'; 26519378907SAtari911 } 26619378907SAtari911 $html .= '</div>'; 26719378907SAtari911 } 26819378907SAtari911 26919378907SAtari911 $html .= '</td>'; 27019378907SAtari911 $currentDay++; 27119378907SAtari911 } 27219378907SAtari911 } 27319378907SAtari911 $html .= '</tr>'; 27419378907SAtari911 } 27519378907SAtari911 27619378907SAtari911 $html .= '</tbody></table>'; 27719378907SAtari911 $html .= '</div>'; // End calendar-left 27819378907SAtari911 27919378907SAtari911 // Right side: Event list 28019378907SAtari911 $html .= '<div class="calendar-compact-right">'; 28119378907SAtari911 $html .= '<div class="event-list-header">'; 28219378907SAtari911 $html .= '<div class="event-list-header-content">'; 28319378907SAtari911 $html .= '<h4 id="eventlist-title-' . $calId . '">Events</h4>'; 28419378907SAtari911 if ($namespace) { 28519378907SAtari911 $html .= '<span class="namespace-badge">' . htmlspecialchars($namespace) . '</span>'; 28619378907SAtari911 } 28719378907SAtari911 $html .= '</div>'; 2881d05cddcSAtari911 2891d05cddcSAtari911 // Search bar in header 2901d05cddcSAtari911 $html .= '<div class="event-search-container-inline">'; 2911d05cddcSAtari911 $html .= '<input type="text" class="event-search-input-inline" id="event-search-' . $calId . '" placeholder=" Search..." oninput="filterEvents(\'' . $calId . '\', this.value)">'; 2921d05cddcSAtari911 $html .= '<button class="event-search-clear-inline" id="search-clear-' . $calId . '" onclick="clearEventSearch(\'' . $calId . '\')" style="display:none;">✕</button>'; 2931d05cddcSAtari911 $html .= '</div>'; 2941d05cddcSAtari911 29519378907SAtari911 $html .= '<button class="add-event-compact" onclick="openAddEvent(\'' . $calId . '\', \'' . $namespace . '\')">+ Add</button>'; 29619378907SAtari911 $html .= '</div>'; 29719378907SAtari911 29819378907SAtari911 $html .= '<div class="event-list-compact" id="eventlist-' . $calId . '">'; 29919378907SAtari911 $html .= $this->renderEventListContent($events, $calId, $namespace); 30019378907SAtari911 $html .= '</div>'; 30119378907SAtari911 30219378907SAtari911 $html .= '</div>'; // End calendar-right 30319378907SAtari911 30419378907SAtari911 // Event dialog 30519378907SAtari911 $html .= $this->renderEventDialog($calId, $namespace); 30619378907SAtari911 30787ac9bf3SAtari911 // Month/Year picker dialog (at container level for proper overlay) 30887ac9bf3SAtari911 $html .= $this->renderMonthPicker($calId, $year, $month, $namespace); 30987ac9bf3SAtari911 31019378907SAtari911 $html .= '</div>'; // End container 31119378907SAtari911 31219378907SAtari911 return $html; 31319378907SAtari911 } 31419378907SAtari911 31519378907SAtari911 private function renderEventListContent($events, $calId, $namespace) { 31619378907SAtari911 if (empty($events)) { 31719378907SAtari911 return '<p class="no-events-msg">No events this month</p>'; 31819378907SAtari911 } 31919378907SAtari911 3201d05cddcSAtari911 // Check for time conflicts 3211d05cddcSAtari911 $events = $this->checkTimeConflicts($events); 3221d05cddcSAtari911 323e3a9f44cSAtari911 // Sort by date ascending (chronological order - oldest first) 32419378907SAtari911 ksort($events); 32519378907SAtari911 326e3a9f44cSAtari911 // Sort events within each day by time 327e3a9f44cSAtari911 foreach ($events as $dateKey => &$dayEvents) { 328e3a9f44cSAtari911 usort($dayEvents, function($a, $b) { 3291d05cddcSAtari911 $timeA = isset($a['time']) && !empty($a['time']) ? $a['time'] : null; 3301d05cddcSAtari911 $timeB = isset($b['time']) && !empty($b['time']) ? $b['time'] : null; 3311d05cddcSAtari911 3321d05cddcSAtari911 // All-day events (no time) go to the TOP 3331d05cddcSAtari911 if ($timeA === null && $timeB !== null) return -1; // A before B 3341d05cddcSAtari911 if ($timeA !== null && $timeB === null) return 1; // A after B 3351d05cddcSAtari911 if ($timeA === null && $timeB === null) return 0; // Both all-day, equal 3361d05cddcSAtari911 3371d05cddcSAtari911 // Both have times, sort chronologically 338e3a9f44cSAtari911 return strcmp($timeA, $timeB); 339e3a9f44cSAtari911 }); 340e3a9f44cSAtari911 } 341e3a9f44cSAtari911 unset($dayEvents); // Break reference 342e3a9f44cSAtari911 343e3a9f44cSAtari911 // Get today's date for comparison 344e3a9f44cSAtari911 $today = date('Y-m-d'); 345e3a9f44cSAtari911 $firstFutureEventId = null; 346e3a9f44cSAtari911 3471d05cddcSAtari911 // Helper function to check if event is past (with 15-minute grace period for timed events) 3481d05cddcSAtari911 $isEventPast = function($dateKey, $time) use ($today) { 3491d05cddcSAtari911 // If event is on a past date, it's definitely past 3501d05cddcSAtari911 if ($dateKey < $today) { 3511d05cddcSAtari911 return true; 3521d05cddcSAtari911 } 3531d05cddcSAtari911 3541d05cddcSAtari911 // If event is on a future date, it's definitely not past 3551d05cddcSAtari911 if ($dateKey > $today) { 3561d05cddcSAtari911 return false; 3571d05cddcSAtari911 } 3581d05cddcSAtari911 3591d05cddcSAtari911 // Event is today - check time with grace period 3601d05cddcSAtari911 if ($time && $time !== '') { 3611d05cddcSAtari911 try { 3621d05cddcSAtari911 $currentDateTime = new DateTime(); 3631d05cddcSAtari911 $eventDateTime = new DateTime($dateKey . ' ' . $time); 3641d05cddcSAtari911 3651d05cddcSAtari911 // Add 15-minute grace period 3661d05cddcSAtari911 $eventDateTime->modify('+15 minutes'); 3671d05cddcSAtari911 3681d05cddcSAtari911 // Event is past if current time > event time + 15 minutes 3691d05cddcSAtari911 return $currentDateTime > $eventDateTime; 3701d05cddcSAtari911 } catch (Exception $e) { 3711d05cddcSAtari911 // If time parsing fails, fall back to date-only comparison 3721d05cddcSAtari911 return false; 3731d05cddcSAtari911 } 3741d05cddcSAtari911 } 3751d05cddcSAtari911 3761d05cddcSAtari911 // No time specified for today's event, treat as future 3771d05cddcSAtari911 return false; 3781d05cddcSAtari911 }; 3791d05cddcSAtari911 3801d05cddcSAtari911 // Build HTML for each event - separate past/completed from future 3811d05cddcSAtari911 $pastHtml = ''; 3821d05cddcSAtari911 $futureHtml = ''; 3831d05cddcSAtari911 $pastCount = 0; 384e3a9f44cSAtari911 38519378907SAtari911 foreach ($events as $dateKey => $dayEvents) { 386e3a9f44cSAtari911 38719378907SAtari911 foreach ($dayEvents as $event) { 388e3a9f44cSAtari911 // Track first future/today event for auto-scroll 389e3a9f44cSAtari911 if (!$firstFutureEventId && $dateKey >= $today) { 390e3a9f44cSAtari911 $firstFutureEventId = isset($event['id']) ? $event['id'] : ''; 391e3a9f44cSAtari911 } 39219378907SAtari911 $eventId = isset($event['id']) ? $event['id'] : ''; 39319378907SAtari911 $title = isset($event['title']) ? htmlspecialchars($event['title']) : 'Untitled'; 3941d05cddcSAtari911 $timeRaw = isset($event['time']) ? $event['time'] : ''; 3951d05cddcSAtari911 $time = htmlspecialchars($timeRaw); 3961d05cddcSAtari911 $endTime = isset($event['endTime']) ? htmlspecialchars($event['endTime']) : ''; 39719378907SAtari911 $color = isset($event['color']) ? htmlspecialchars($event['color']) : '#3498db'; 39819378907SAtari911 $description = isset($event['description']) ? $event['description'] : ''; 39919378907SAtari911 $isTask = isset($event['isTask']) ? $event['isTask'] : false; 40019378907SAtari911 $completed = isset($event['completed']) ? $event['completed'] : false; 40119378907SAtari911 $endDate = isset($event['endDate']) ? $event['endDate'] : ''; 40219378907SAtari911 4031d05cddcSAtari911 // Use helper function to determine if event is past (with grace period) 4041d05cddcSAtari911 $isPast = $isEventPast($dateKey, $timeRaw); 4051d05cddcSAtari911 $isToday = $dateKey === $today; 4061d05cddcSAtari911 4071d05cddcSAtari911 // Check if event should be in past section 4081d05cddcSAtari911 // EXCEPTION: Uncompleted tasks (isTask && !completed) should stay visible even if past 4091d05cddcSAtari911 $isPastOrCompleted = ($isPast && (!$isTask || $completed)) || $completed; 4101d05cddcSAtari911 if ($isPastOrCompleted) { 4111d05cddcSAtari911 $pastCount++; 4121d05cddcSAtari911 } 4131d05cddcSAtari911 4141d05cddcSAtari911 // Determine if task is past due (past date, is task, not completed) 4151d05cddcSAtari911 $isPastDue = $isPast && $isTask && !$completed; 4161d05cddcSAtari911 41719378907SAtari911 // Process description for wiki syntax, HTML, images, and links 41819378907SAtari911 $renderedDescription = $this->renderDescription($description); 41919378907SAtari911 4201d05cddcSAtari911 // Convert to 12-hour format and handle time ranges 42119378907SAtari911 $displayTime = ''; 42219378907SAtari911 if ($time) { 42319378907SAtari911 $timeObj = DateTime::createFromFormat('H:i', $time); 42419378907SAtari911 if ($timeObj) { 42519378907SAtari911 $displayTime = $timeObj->format('g:i A'); 4261d05cddcSAtari911 4271d05cddcSAtari911 // Add end time if present and different from start time 4281d05cddcSAtari911 if ($endTime && $endTime !== $time) { 4291d05cddcSAtari911 $endTimeObj = DateTime::createFromFormat('H:i', $endTime); 4301d05cddcSAtari911 if ($endTimeObj) { 4311d05cddcSAtari911 $displayTime .= ' - ' . $endTimeObj->format('g:i A'); 4321d05cddcSAtari911 } 4331d05cddcSAtari911 } 43419378907SAtari911 } else { 43519378907SAtari911 $displayTime = $time; 43619378907SAtari911 } 43719378907SAtari911 } 43819378907SAtari911 43987ac9bf3SAtari911 // Format date display with day of week 440e3a9f44cSAtari911 // Use originalStartDate if this is a multi-month event continuation 441e3a9f44cSAtari911 $displayDateKey = isset($event['originalStartDate']) ? $event['originalStartDate'] : $dateKey; 442e3a9f44cSAtari911 $dateObj = new DateTime($displayDateKey); 44387ac9bf3SAtari911 $displayDate = $dateObj->format('D, M j'); // e.g., "Mon, Jan 24" 44419378907SAtari911 44519378907SAtari911 // Multi-day indicator 44619378907SAtari911 $multiDay = ''; 447e3a9f44cSAtari911 if ($endDate && $endDate !== $displayDateKey) { 44819378907SAtari911 $endObj = new DateTime($endDate); 44987ac9bf3SAtari911 $multiDay = ' → ' . $endObj->format('D, M j'); 45019378907SAtari911 } 45119378907SAtari911 45219378907SAtari911 $completedClass = $completed ? ' event-completed' : ''; 4531d05cddcSAtari911 // Don't grey out past due tasks - they need attention! 4541d05cddcSAtari911 $pastClass = ($isPast && !$isPastDue) ? ' event-past' : ''; 4551d05cddcSAtari911 $pastDueClass = $isPastDue ? ' event-pastdue' : ''; 456e3a9f44cSAtari911 $firstFutureAttr = ($firstFutureEventId === $eventId) ? ' data-first-future="true"' : ''; 45719378907SAtari911 4581d05cddcSAtari911 $eventHtml = '<div class="event-compact-item' . $completedClass . $pastClass . $pastDueClass . '" data-event-id="' . $eventId . '" data-date="' . $dateKey . '" style="border-left-color: ' . $color . ';"' . $firstFutureAttr . '>'; 45919378907SAtari911 4601d05cddcSAtari911 $eventHtml .= '<div class="event-info">'; 4611d05cddcSAtari911 $eventHtml .= '<div class="event-title-row">'; 4621d05cddcSAtari911 $eventHtml .= '<span class="event-title-compact">' . $title . '</span>'; 4631d05cddcSAtari911 $eventHtml .= '</div>'; 46419378907SAtari911 465e3a9f44cSAtari911 // For past events, hide meta and description (collapsed) 4661d05cddcSAtari911 // EXCEPTION: Past due tasks should show their details 4671d05cddcSAtari911 if (!$isPast || $isPastDue) { 4681d05cddcSAtari911 $eventHtml .= '<div class="event-meta-compact">'; 4691d05cddcSAtari911 $eventHtml .= '<span class="event-date-time">' . $displayDate . $multiDay; 47019378907SAtari911 if ($displayTime) { 4711d05cddcSAtari911 $eventHtml .= ' • ' . $displayTime; 47219378907SAtari911 } 4731d05cddcSAtari911 // Add TODAY badge for today's events OR PAST DUE for uncompleted past tasks 4741d05cddcSAtari911 if ($isPastDue) { 4751d05cddcSAtari911 $eventHtml .= ' <span class="event-pastdue-badge">PAST DUE</span>'; 4761d05cddcSAtari911 } elseif ($isToday) { 4771d05cddcSAtari911 $eventHtml .= ' <span class="event-today-badge">TODAY</span>'; 478e3a9f44cSAtari911 } 4791d05cddcSAtari911 // Add namespace badge - ALWAYS show if event has a namespace 480e3a9f44cSAtari911 $eventNamespace = isset($event['namespace']) ? $event['namespace'] : ''; 481e3a9f44cSAtari911 if (!$eventNamespace && isset($event['_namespace'])) { 482e3a9f44cSAtari911 $eventNamespace = $event['_namespace']; // Fallback to _namespace for backward compatibility 483e3a9f44cSAtari911 } 4841d05cddcSAtari911 // Show badge if namespace exists and is not empty 4851d05cddcSAtari911 if ($eventNamespace && $eventNamespace !== '') { 4861d05cddcSAtari911 $eventHtml .= ' <span class="event-namespace-badge" onclick="filterCalendarByNamespace(\'' . $calId . '\', \'' . htmlspecialchars($eventNamespace) . '\')" style="cursor:pointer;" title="Click to filter by this namespace">' . htmlspecialchars($eventNamespace) . '</span>'; 487e3a9f44cSAtari911 } 4881d05cddcSAtari911 4891d05cddcSAtari911 // Add conflict warning if event has time conflicts 4901d05cddcSAtari911 if (isset($event['hasConflict']) && $event['hasConflict'] && isset($event['conflictsWith'])) { 4911d05cddcSAtari911 $conflictList = []; 4921d05cddcSAtari911 foreach ($event['conflictsWith'] as $conflict) { 4931d05cddcSAtari911 $conflictText = htmlspecialchars($conflict['title']); 4941d05cddcSAtari911 if (!empty($conflict['time'])) { 4951d05cddcSAtari911 // Format time range 4961d05cddcSAtari911 $startTimeObj = DateTime::createFromFormat('H:i', $conflict['time']); 4971d05cddcSAtari911 $startTimeFormatted = $startTimeObj ? $startTimeObj->format('g:i A') : $conflict['time']; 4981d05cddcSAtari911 4991d05cddcSAtari911 if (!empty($conflict['endTime']) && $conflict['endTime'] !== $conflict['time']) { 5001d05cddcSAtari911 $endTimeObj = DateTime::createFromFormat('H:i', $conflict['endTime']); 5011d05cddcSAtari911 $endTimeFormatted = $endTimeObj ? $endTimeObj->format('g:i A') : $conflict['endTime']; 5021d05cddcSAtari911 $conflictText .= ' (' . $startTimeFormatted . ' - ' . $endTimeFormatted . ')'; 5031d05cddcSAtari911 } else { 5041d05cddcSAtari911 $conflictText .= ' (' . $startTimeFormatted . ')'; 5051d05cddcSAtari911 } 5061d05cddcSAtari911 } 5071d05cddcSAtari911 $conflictList[] = $conflictText; 5081d05cddcSAtari911 } 5091d05cddcSAtari911 $conflictCount = count($event['conflictsWith']); 5101d05cddcSAtari911 $conflictJson = htmlspecialchars(json_encode($conflictList), ENT_QUOTES, 'UTF-8'); 5111d05cddcSAtari911 $eventHtml .= ' <span class="event-conflict-badge" data-conflicts="' . $conflictJson . '" onmouseenter="showConflictTooltip(this)" onmouseleave="hideConflictTooltip()">⚠️ ' . $conflictCount . '</span>'; 5121d05cddcSAtari911 } 5131d05cddcSAtari911 5141d05cddcSAtari911 $eventHtml .= '</span>'; 5151d05cddcSAtari911 $eventHtml .= '</div>'; 51619378907SAtari911 51719378907SAtari911 if ($description) { 5181d05cddcSAtari911 $eventHtml .= '<div class="event-desc-compact">' . $renderedDescription . '</div>'; 5191d05cddcSAtari911 } 5201d05cddcSAtari911 } else { 5211d05cddcSAtari911 // Past events: render with display:none for click-to-expand 5221d05cddcSAtari911 $eventHtml .= '<div class="event-meta-compact" style="display:none;">'; 5231d05cddcSAtari911 $eventHtml .= '<span class="event-date-time">' . $displayDate . $multiDay; 5241d05cddcSAtari911 if ($displayTime) { 5251d05cddcSAtari911 $eventHtml .= ' • ' . $displayTime; 5261d05cddcSAtari911 } 5271d05cddcSAtari911 $eventNamespace = isset($event['namespace']) ? $event['namespace'] : ''; 5281d05cddcSAtari911 if (!$eventNamespace && isset($event['_namespace'])) { 5291d05cddcSAtari911 $eventNamespace = $event['_namespace']; 5301d05cddcSAtari911 } 5311d05cddcSAtari911 if ($eventNamespace && $eventNamespace !== '') { 5321d05cddcSAtari911 $eventHtml .= ' <span class="event-namespace-badge" onclick="filterCalendarByNamespace(\'' . $calId . '\', \'' . htmlspecialchars($eventNamespace) . '\')" style="cursor:pointer;" title="Click to filter by this namespace">' . htmlspecialchars($eventNamespace) . '</span>'; 5331d05cddcSAtari911 } 5341d05cddcSAtari911 5351d05cddcSAtari911 // Add conflict warning if event has time conflicts 5361d05cddcSAtari911 if (isset($event['hasConflict']) && $event['hasConflict'] && isset($event['conflictsWith'])) { 5371d05cddcSAtari911 $conflictList = []; 5381d05cddcSAtari911 foreach ($event['conflictsWith'] as $conflict) { 5391d05cddcSAtari911 $conflictText = htmlspecialchars($conflict['title']); 5401d05cddcSAtari911 if (!empty($conflict['time'])) { 5411d05cddcSAtari911 $startTimeObj = DateTime::createFromFormat('H:i', $conflict['time']); 5421d05cddcSAtari911 $startTimeFormatted = $startTimeObj ? $startTimeObj->format('g:i A') : $conflict['time']; 5431d05cddcSAtari911 5441d05cddcSAtari911 if (!empty($conflict['endTime']) && $conflict['endTime'] !== $conflict['time']) { 5451d05cddcSAtari911 $endTimeObj = DateTime::createFromFormat('H:i', $conflict['endTime']); 5461d05cddcSAtari911 $endTimeFormatted = $endTimeObj ? $endTimeObj->format('g:i A') : $conflict['endTime']; 5471d05cddcSAtari911 $conflictText .= ' (' . $startTimeFormatted . ' - ' . $endTimeFormatted . ')'; 5481d05cddcSAtari911 } else { 5491d05cddcSAtari911 $conflictText .= ' (' . $startTimeFormatted . ')'; 5501d05cddcSAtari911 } 5511d05cddcSAtari911 } 5521d05cddcSAtari911 $conflictList[] = $conflictText; 5531d05cddcSAtari911 } 5541d05cddcSAtari911 $conflictCount = count($event['conflictsWith']); 5551d05cddcSAtari911 $conflictJson = htmlspecialchars(json_encode($conflictList), ENT_QUOTES, 'UTF-8'); 5561d05cddcSAtari911 $eventHtml .= ' <span class="event-conflict-badge" data-conflicts="' . $conflictJson . '" onmouseenter="showConflictTooltip(this)" onmouseleave="hideConflictTooltip()">⚠️ ' . $conflictCount . '</span>'; 5571d05cddcSAtari911 } 5581d05cddcSAtari911 5591d05cddcSAtari911 $eventHtml .= '</span>'; 5601d05cddcSAtari911 $eventHtml .= '</div>'; 5611d05cddcSAtari911 5621d05cddcSAtari911 if ($description) { 5631d05cddcSAtari911 $eventHtml .= '<div class="event-desc-compact" style="display:none;">' . $renderedDescription . '</div>'; 56419378907SAtari911 } 565e3a9f44cSAtari911 } 56619378907SAtari911 5671d05cddcSAtari911 $eventHtml .= '</div>'; // event-info 56819378907SAtari911 569e3a9f44cSAtari911 // Use stored namespace from event, fallback to passed namespace 570e3a9f44cSAtari911 $buttonNamespace = isset($event['namespace']) ? $event['namespace'] : $namespace; 571e3a9f44cSAtari911 5721d05cddcSAtari911 $eventHtml .= '<div class="event-actions-compact">'; 5731d05cddcSAtari911 $eventHtml .= '<button class="event-action-btn" onclick="deleteEvent(\'' . $calId . '\', \'' . $eventId . '\', \'' . $dateKey . '\', \'' . $buttonNamespace . '\')">️</button>'; 5741d05cddcSAtari911 $eventHtml .= '<button class="event-action-btn" onclick="editEvent(\'' . $calId . '\', \'' . $eventId . '\', \'' . $dateKey . '\', \'' . $buttonNamespace . '\')">✏️</button>'; 5751d05cddcSAtari911 $eventHtml .= '</div>'; 57619378907SAtari911 57719378907SAtari911 // Checkbox for tasks - ON THE FAR RIGHT 57819378907SAtari911 if ($isTask) { 57919378907SAtari911 $checked = $completed ? 'checked' : ''; 5801d05cddcSAtari911 $eventHtml .= '<input type="checkbox" class="task-checkbox" ' . $checked . ' onclick="toggleTaskComplete(\'' . $calId . '\', \'' . $eventId . '\', \'' . $dateKey . '\', \'' . $buttonNamespace . '\', this.checked)">'; 58119378907SAtari911 } 58219378907SAtari911 5831d05cddcSAtari911 $eventHtml .= '</div>'; 5841d05cddcSAtari911 5851d05cddcSAtari911 // Add to appropriate section 5861d05cddcSAtari911 if ($isPastOrCompleted) { 5871d05cddcSAtari911 $pastHtml .= $eventHtml; 5881d05cddcSAtari911 } else { 5891d05cddcSAtari911 $futureHtml .= $eventHtml; 5901d05cddcSAtari911 } 5911d05cddcSAtari911 } 5921d05cddcSAtari911 } 5931d05cddcSAtari911 5941d05cddcSAtari911 // Build final HTML with collapsible past events section 5951d05cddcSAtari911 $html = ''; 5961d05cddcSAtari911 5971d05cddcSAtari911 // Add collapsible past events section if any exist 5981d05cddcSAtari911 if ($pastCount > 0) { 5991d05cddcSAtari911 $html .= '<div class="past-events-section">'; 6001d05cddcSAtari911 $html .= '<div class="past-events-toggle" onclick="togglePastEvents(\'' . $calId . '\')">'; 6011d05cddcSAtari911 $html .= '<span class="past-events-arrow" id="past-arrow-' . $calId . '">▶</span> '; 6021d05cddcSAtari911 $html .= '<span class="past-events-label">Past Events (' . $pastCount . ')</span>'; 60319378907SAtari911 $html .= '</div>'; 6041d05cddcSAtari911 $html .= '<div class="past-events-content" id="past-events-' . $calId . '" style="display:none;">'; 6051d05cddcSAtari911 $html .= $pastHtml; 6061d05cddcSAtari911 $html .= '</div>'; 6071d05cddcSAtari911 $html .= '</div>'; 6081d05cddcSAtari911 } 609e3a9f44cSAtari911 6101d05cddcSAtari911 // Add future events 6111d05cddcSAtari911 $html .= $futureHtml; 61219378907SAtari911 61319378907SAtari911 return $html; 61419378907SAtari911 } 61519378907SAtari911 6161d05cddcSAtari911 /** 6171d05cddcSAtari911 * Check for time conflicts between events 6181d05cddcSAtari911 */ 6191d05cddcSAtari911 private function checkTimeConflicts($events) { 6201d05cddcSAtari911 // Group events by date 6211d05cddcSAtari911 $eventsByDate = []; 6221d05cddcSAtari911 foreach ($events as $date => $dateEvents) { 6231d05cddcSAtari911 if (!is_array($dateEvents)) continue; 6241d05cddcSAtari911 6251d05cddcSAtari911 foreach ($dateEvents as $evt) { 6261d05cddcSAtari911 if (empty($evt['time'])) continue; // Skip all-day events 6271d05cddcSAtari911 6281d05cddcSAtari911 if (!isset($eventsByDate[$date])) { 6291d05cddcSAtari911 $eventsByDate[$date] = []; 6301d05cddcSAtari911 } 6311d05cddcSAtari911 $eventsByDate[$date][] = $evt; 6321d05cddcSAtari911 } 6331d05cddcSAtari911 } 6341d05cddcSAtari911 6351d05cddcSAtari911 // Check for overlaps on each date 6361d05cddcSAtari911 foreach ($eventsByDate as $date => $dateEvents) { 6371d05cddcSAtari911 for ($i = 0; $i < count($dateEvents); $i++) { 6381d05cddcSAtari911 for ($j = $i + 1; $j < count($dateEvents); $j++) { 6391d05cddcSAtari911 if ($this->eventsOverlap($dateEvents[$i], $dateEvents[$j])) { 6401d05cddcSAtari911 // Mark both events as conflicting 6411d05cddcSAtari911 $dateEvents[$i]['hasConflict'] = true; 6421d05cddcSAtari911 $dateEvents[$j]['hasConflict'] = true; 6431d05cddcSAtari911 6441d05cddcSAtari911 // Store conflict info 6451d05cddcSAtari911 if (!isset($dateEvents[$i]['conflictsWith'])) { 6461d05cddcSAtari911 $dateEvents[$i]['conflictsWith'] = []; 6471d05cddcSAtari911 } 6481d05cddcSAtari911 if (!isset($dateEvents[$j]['conflictsWith'])) { 6491d05cddcSAtari911 $dateEvents[$j]['conflictsWith'] = []; 6501d05cddcSAtari911 } 6511d05cddcSAtari911 6521d05cddcSAtari911 $dateEvents[$i]['conflictsWith'][] = [ 6531d05cddcSAtari911 'id' => $dateEvents[$j]['id'], 6541d05cddcSAtari911 'title' => $dateEvents[$j]['title'], 6551d05cddcSAtari911 'time' => $dateEvents[$j]['time'], 6561d05cddcSAtari911 'endTime' => isset($dateEvents[$j]['endTime']) ? $dateEvents[$j]['endTime'] : '' 6571d05cddcSAtari911 ]; 6581d05cddcSAtari911 6591d05cddcSAtari911 $dateEvents[$j]['conflictsWith'][] = [ 6601d05cddcSAtari911 'id' => $dateEvents[$i]['id'], 6611d05cddcSAtari911 'title' => $dateEvents[$i]['title'], 6621d05cddcSAtari911 'time' => $dateEvents[$i]['time'], 6631d05cddcSAtari911 'endTime' => isset($dateEvents[$i]['endTime']) ? $dateEvents[$i]['endTime'] : '' 6641d05cddcSAtari911 ]; 6651d05cddcSAtari911 } 6661d05cddcSAtari911 } 6671d05cddcSAtari911 } 6681d05cddcSAtari911 6691d05cddcSAtari911 // Update the events array with conflict information 6701d05cddcSAtari911 foreach ($events[$date] as &$evt) { 6711d05cddcSAtari911 foreach ($dateEvents as $checkedEvt) { 6721d05cddcSAtari911 if ($evt['id'] === $checkedEvt['id']) { 6731d05cddcSAtari911 if (isset($checkedEvt['hasConflict'])) { 6741d05cddcSAtari911 $evt['hasConflict'] = $checkedEvt['hasConflict']; 6751d05cddcSAtari911 } 6761d05cddcSAtari911 if (isset($checkedEvt['conflictsWith'])) { 6771d05cddcSAtari911 $evt['conflictsWith'] = $checkedEvt['conflictsWith']; 6781d05cddcSAtari911 } 6791d05cddcSAtari911 break; 6801d05cddcSAtari911 } 6811d05cddcSAtari911 } 6821d05cddcSAtari911 } 6831d05cddcSAtari911 } 6841d05cddcSAtari911 6851d05cddcSAtari911 return $events; 6861d05cddcSAtari911 } 6871d05cddcSAtari911 6881d05cddcSAtari911 /** 6891d05cddcSAtari911 * Check if two events overlap in time 6901d05cddcSAtari911 */ 6911d05cddcSAtari911 private function eventsOverlap($evt1, $evt2) { 6921d05cddcSAtari911 if (empty($evt1['time']) || empty($evt2['time'])) { 6931d05cddcSAtari911 return false; // All-day events don't conflict 6941d05cddcSAtari911 } 6951d05cddcSAtari911 6961d05cddcSAtari911 $start1 = $evt1['time']; 6971d05cddcSAtari911 $end1 = isset($evt1['endTime']) && !empty($evt1['endTime']) ? $evt1['endTime'] : $evt1['time']; 6981d05cddcSAtari911 6991d05cddcSAtari911 $start2 = $evt2['time']; 7001d05cddcSAtari911 $end2 = isset($evt2['endTime']) && !empty($evt2['endTime']) ? $evt2['endTime'] : $evt2['time']; 7011d05cddcSAtari911 7021d05cddcSAtari911 // Convert to minutes for easier comparison 7031d05cddcSAtari911 $start1Mins = $this->timeToMinutes($start1); 7041d05cddcSAtari911 $end1Mins = $this->timeToMinutes($end1); 7051d05cddcSAtari911 $start2Mins = $this->timeToMinutes($start2); 7061d05cddcSAtari911 $end2Mins = $this->timeToMinutes($end2); 7071d05cddcSAtari911 7081d05cddcSAtari911 // Check for overlap: start1 < end2 AND start2 < end1 7091d05cddcSAtari911 return $start1Mins < $end2Mins && $start2Mins < $end1Mins; 7101d05cddcSAtari911 } 7111d05cddcSAtari911 7121d05cddcSAtari911 /** 7131d05cddcSAtari911 * Convert HH:MM time to minutes since midnight 7141d05cddcSAtari911 */ 7151d05cddcSAtari911 private function timeToMinutes($timeStr) { 7161d05cddcSAtari911 $parts = explode(':', $timeStr); 7171d05cddcSAtari911 if (count($parts) !== 2) return 0; 7181d05cddcSAtari911 7191d05cddcSAtari911 return (int)$parts[0] * 60 + (int)$parts[1]; 7201d05cddcSAtari911 } 7211d05cddcSAtari911 72219378907SAtari911 private function renderEventPanelOnly($data) { 72319378907SAtari911 $year = (int)$data['year']; 72419378907SAtari911 $month = (int)$data['month']; 72519378907SAtari911 $namespace = $data['namespace']; 72687ac9bf3SAtari911 $height = isset($data['height']) ? $data['height'] : '400px'; 72787ac9bf3SAtari911 72887ac9bf3SAtari911 // Validate height format (must be px, em, rem, vh, or %) 72987ac9bf3SAtari911 if (!preg_match('/^\d+(\.\d+)?(px|em|rem|vh|%)$/', $height)) { 73087ac9bf3SAtari911 $height = '400px'; // Default fallback 73187ac9bf3SAtari911 } 73219378907SAtari911 733e3a9f44cSAtari911 // Check if multiple namespaces or wildcard specified 734e3a9f44cSAtari911 $isMultiNamespace = !empty($namespace) && (strpos($namespace, ';') !== false || strpos($namespace, '*') !== false); 735e3a9f44cSAtari911 736e3a9f44cSAtari911 if ($isMultiNamespace) { 737e3a9f44cSAtari911 $events = $this->loadEventsMultiNamespace($namespace, $year, $month); 738e3a9f44cSAtari911 } else { 73919378907SAtari911 $events = $this->loadEvents($namespace, $year, $month); 740e3a9f44cSAtari911 } 74119378907SAtari911 $calId = 'panel_' . md5(serialize($data) . microtime()); 74219378907SAtari911 74319378907SAtari911 $monthName = date('F Y', mktime(0, 0, 0, $month, 1, $year)); 74419378907SAtari911 74519378907SAtari911 $prevMonth = $month - 1; 74619378907SAtari911 $prevYear = $year; 74719378907SAtari911 if ($prevMonth < 1) { 74819378907SAtari911 $prevMonth = 12; 74919378907SAtari911 $prevYear--; 75019378907SAtari911 } 75119378907SAtari911 75219378907SAtari911 $nextMonth = $month + 1; 75319378907SAtari911 $nextYear = $year; 75419378907SAtari911 if ($nextMonth > 12) { 75519378907SAtari911 $nextMonth = 1; 75619378907SAtari911 $nextYear++; 75719378907SAtari911 } 75819378907SAtari911 7591d05cddcSAtari911 $html = '<div class="event-panel-standalone" id="' . $calId . '" data-height="' . htmlspecialchars($height) . '" data-namespace="' . htmlspecialchars($namespace) . '" data-original-namespace="' . htmlspecialchars($namespace) . '">'; 76019378907SAtari911 7611d05cddcSAtari911 // Load calendar JavaScript manually (not through DokuWiki concatenation) 7621d05cddcSAtari911 $html .= '<script src="' . DOKU_BASE . 'lib/plugins/calendar/calendar-main.js"></script>'; 7631d05cddcSAtari911 7641d05cddcSAtari911 // Initialize DOKU_BASE for JavaScript 7651d05cddcSAtari911 $html .= '<script>if(typeof DOKU_BASE==="undefined"){window.DOKU_BASE="' . DOKU_BASE . '";}</script>'; 7661d05cddcSAtari911 7671d05cddcSAtari911 // Compact two-row header designed for ~500px width 7681d05cddcSAtari911 $html .= '<div class="panel-header-compact">'; 7691d05cddcSAtari911 7701d05cddcSAtari911 // Row 1: Navigation and title 7711d05cddcSAtari911 $html .= '<div class="panel-header-row-1">'; 7721d05cddcSAtari911 $html .= '<button class="panel-nav-btn" onclick="navEventPanel(\'' . $calId . '\', ' . $prevYear . ', ' . $prevMonth . ', \'' . $namespace . '\')">‹</button>'; 7731d05cddcSAtari911 7741d05cddcSAtari911 // Compact month name (e.g. "Feb 2026" instead of "February 2026 Events") 7751d05cddcSAtari911 $shortMonthName = date('M Y', mktime(0, 0, 0, $month, 1, $year)); 7761d05cddcSAtari911 $html .= '<h3 class="panel-month-title" onclick="openMonthPickerPanel(\'' . $calId . '\', ' . $year . ', ' . $month . ', \'' . $namespace . '\')" title="Click to jump to month">' . $shortMonthName . '</h3>'; 7771d05cddcSAtari911 7781d05cddcSAtari911 $html .= '<button class="panel-nav-btn" onclick="navEventPanel(\'' . $calId . '\', ' . $nextYear . ', ' . $nextMonth . ', \'' . $namespace . '\')">›</button>'; 7791d05cddcSAtari911 7801d05cddcSAtari911 // Namespace badge (if applicable) 78187ac9bf3SAtari911 if ($namespace) { 782e3a9f44cSAtari911 if ($isMultiNamespace) { 783e3a9f44cSAtari911 if (strpos($namespace, '*') !== false) { 7841d05cddcSAtari911 $html .= '<span class="panel-ns-badge" title="' . htmlspecialchars($namespace) . '">' . htmlspecialchars($namespace) . '</span>'; 785e3a9f44cSAtari911 } else { 786e3a9f44cSAtari911 $namespaceList = array_map('trim', explode(';', $namespace)); 7871d05cddcSAtari911 $nsCount = count($namespaceList); 7881d05cddcSAtari911 $html .= '<span class="panel-ns-badge" title="' . htmlspecialchars(implode(', ', $namespaceList)) . '">' . $nsCount . ' NS</span>'; 789e3a9f44cSAtari911 } 790e3a9f44cSAtari911 } else { 7911d05cddcSAtari911 $isFiltering = ($namespace !== '*' && strpos($namespace, '*') === false && strpos($namespace, ';') === false); 7921d05cddcSAtari911 if ($isFiltering) { 7931d05cddcSAtari911 $html .= '<span class="panel-ns-badge filter-on" title="Filtering by ' . htmlspecialchars($namespace) . ' - click to clear" onclick="clearNamespaceFilterPanel(\'' . $calId . '\')">' . htmlspecialchars($namespace) . ' ✕</span>'; 7941d05cddcSAtari911 } else { 7951d05cddcSAtari911 $html .= '<span class="panel-ns-badge" title="' . htmlspecialchars($namespace) . '">' . htmlspecialchars($namespace) . '</span>'; 79687ac9bf3SAtari911 } 797e3a9f44cSAtari911 } 7981d05cddcSAtari911 } 7991d05cddcSAtari911 8001d05cddcSAtari911 $html .= '<button class="panel-today-btn" onclick="jumpTodayPanel(\'' . $calId . '\', \'' . $namespace . '\')">Today</button>'; 80119378907SAtari911 $html .= '</div>'; 80219378907SAtari911 8031d05cddcSAtari911 // Row 2: Search and add button 8041d05cddcSAtari911 $html .= '<div class="panel-header-row-2">'; 8051d05cddcSAtari911 $html .= '<div class="panel-search-box">'; 8061d05cddcSAtari911 $html .= '<input type="text" class="panel-search-input" id="event-search-' . $calId . '" placeholder="Search events..." oninput="filterEvents(\'' . $calId . '\', this.value)">'; 8071d05cddcSAtari911 $html .= '<button class="panel-search-clear" id="search-clear-' . $calId . '" onclick="clearEventSearch(\'' . $calId . '\')" style="display:none;">✕</button>'; 8081d05cddcSAtari911 $html .= '</div>'; 8091d05cddcSAtari911 $html .= '<button class="panel-add-btn" onclick="openAddEventPanel(\'' . $calId . '\', \'' . $namespace . '\')">+ Add</button>'; 8101d05cddcSAtari911 $html .= '</div>'; 8111d05cddcSAtari911 81219378907SAtari911 $html .= '</div>'; 81319378907SAtari911 81487ac9bf3SAtari911 $html .= '<div class="event-list-compact" id="eventlist-' . $calId . '" style="max-height: ' . htmlspecialchars($height) . ';">'; 81519378907SAtari911 $html .= $this->renderEventListContent($events, $calId, $namespace); 81619378907SAtari911 $html .= '</div>'; 81719378907SAtari911 81819378907SAtari911 $html .= $this->renderEventDialog($calId, $namespace); 81919378907SAtari911 82087ac9bf3SAtari911 // Month/Year picker for event panel 82187ac9bf3SAtari911 $html .= $this->renderMonthPicker($calId, $year, $month, $namespace); 82287ac9bf3SAtari911 82319378907SAtari911 $html .= '</div>'; 82419378907SAtari911 82519378907SAtari911 return $html; 82619378907SAtari911 } 82719378907SAtari911 82819378907SAtari911 private function renderStandaloneEventList($data) { 82919378907SAtari911 $namespace = $data['namespace']; 8301d05cddcSAtari911 // If no namespace specified, show all namespaces 8311d05cddcSAtari911 if (empty($namespace)) { 8321d05cddcSAtari911 $namespace = '*'; 8331d05cddcSAtari911 } 83419378907SAtari911 $daterange = $data['daterange']; 83519378907SAtari911 $date = $data['date']; 836e3a9f44cSAtari911 $range = isset($data['range']) ? strtolower($data['range']) : ''; 83787ac9bf3SAtari911 $today = isset($data['today']) ? true : false; 838e3a9f44cSAtari911 $sidebar = isset($data['sidebar']) ? true : false; 8391d05cddcSAtari911 $showchecked = isset($data['showchecked']) ? true : false; // New parameter 8401d05cddcSAtari911 $noheader = isset($data['noheader']) ? true : false; // New parameter to hide header 84119378907SAtari911 842e3a9f44cSAtari911 // Handle "range" parameter - day, week, or month 843e3a9f44cSAtari911 if ($range === 'day') { 8441d05cddcSAtari911 $startDate = date('Y-m-d', strtotime('-30 days')); // Include past 30 days for past due tasks 84587ac9bf3SAtari911 $endDate = date('Y-m-d'); 846e3a9f44cSAtari911 $headerText = 'Today'; 847e3a9f44cSAtari911 } elseif ($range === 'week') { 8481d05cddcSAtari911 $startDate = date('Y-m-d', strtotime('-30 days')); // Include past 30 days for past due tasks 8491d05cddcSAtari911 $endDateTime = new DateTime(); 850e3a9f44cSAtari911 $endDateTime->modify('+7 days'); 851e3a9f44cSAtari911 $endDate = $endDateTime->format('Y-m-d'); 852e3a9f44cSAtari911 $headerText = 'This Week'; 853e3a9f44cSAtari911 } elseif ($range === 'month') { 8541d05cddcSAtari911 $startDate = date('Y-m-01', strtotime('-1 month')); // Include previous month for past due tasks 855e3a9f44cSAtari911 $endDate = date('Y-m-t'); // Last of current month 8561d05cddcSAtari911 $dt = new DateTime(); 857e3a9f44cSAtari911 $headerText = $dt->format('F Y'); 858e3a9f44cSAtari911 } elseif ($sidebar) { 8591d05cddcSAtari911 // NEW: Sidebar widget - load current week's events 8601d05cddcSAtari911 $weekStart = date('Y-m-d', strtotime('monday this week')); 8611d05cddcSAtari911 $weekEnd = date('Y-m-d', strtotime('sunday this week')); 8621d05cddcSAtari911 8631d05cddcSAtari911 // Load events for the entire week 8641d05cddcSAtari911 $start = new DateTime($weekStart); 8651d05cddcSAtari911 $end = new DateTime($weekEnd); 8661d05cddcSAtari911 $end->modify('+1 day'); // DatePeriod excludes end date 8671d05cddcSAtari911 $interval = new DateInterval('P1D'); 8681d05cddcSAtari911 $period = new DatePeriod($start, $interval, $end); 8691d05cddcSAtari911 8701d05cddcSAtari911 $isMultiNamespace = !empty($namespace) && (strpos($namespace, ';') !== false || strpos($namespace, '*') !== false); 8711d05cddcSAtari911 $allEvents = []; 8721d05cddcSAtari911 $loadedMonths = []; 8731d05cddcSAtari911 8741d05cddcSAtari911 foreach ($period as $dt) { 8751d05cddcSAtari911 $year = (int)$dt->format('Y'); 8761d05cddcSAtari911 $month = (int)$dt->format('n'); 8771d05cddcSAtari911 $dateKey = $dt->format('Y-m-d'); 8781d05cddcSAtari911 8791d05cddcSAtari911 $monthKey = $year . '-' . $month . '-' . $namespace; 8801d05cddcSAtari911 8811d05cddcSAtari911 if (!isset($loadedMonths[$monthKey])) { 8821d05cddcSAtari911 if ($isMultiNamespace) { 8831d05cddcSAtari911 $loadedMonths[$monthKey] = $this->loadEventsMultiNamespace($namespace, $year, $month); 8841d05cddcSAtari911 } else { 8851d05cddcSAtari911 $loadedMonths[$monthKey] = $this->loadEvents($namespace, $year, $month); 8861d05cddcSAtari911 } 8871d05cddcSAtari911 } 8881d05cddcSAtari911 8891d05cddcSAtari911 $monthEvents = $loadedMonths[$monthKey]; 8901d05cddcSAtari911 8911d05cddcSAtari911 if (isset($monthEvents[$dateKey]) && !empty($monthEvents[$dateKey])) { 8921d05cddcSAtari911 $allEvents[$dateKey] = $monthEvents[$dateKey]; 8931d05cddcSAtari911 } 8941d05cddcSAtari911 } 8951d05cddcSAtari911 8961d05cddcSAtari911 // Apply time conflict detection 8971d05cddcSAtari911 $allEvents = $this->checkTimeConflicts($allEvents); 8981d05cddcSAtari911 8991d05cddcSAtari911 $calId = 'sidebar-' . substr(md5($namespace . $weekStart), 0, 8); 9001d05cddcSAtari911 9011d05cddcSAtari911 // Render sidebar widget and return immediately 9021d05cddcSAtari911 return $this->renderSidebarWidget($allEvents, $namespace, $calId); 903e3a9f44cSAtari911 } elseif ($today) { 904e3a9f44cSAtari911 $startDate = date('Y-m-d'); 905e3a9f44cSAtari911 $endDate = date('Y-m-d'); 906e3a9f44cSAtari911 $headerText = 'Today'; 90787ac9bf3SAtari911 } elseif ($daterange) { 90819378907SAtari911 list($startDate, $endDate) = explode(':', $daterange); 909e3a9f44cSAtari911 $start = new DateTime($startDate); 910e3a9f44cSAtari911 $end = new DateTime($endDate); 911e3a9f44cSAtari911 $headerText = $start->format('M j') . ' - ' . $end->format('M j, Y'); 91219378907SAtari911 } elseif ($date) { 91319378907SAtari911 $startDate = $date; 91419378907SAtari911 $endDate = $date; 915e3a9f44cSAtari911 $dt = new DateTime($date); 916e3a9f44cSAtari911 $headerText = $dt->format('l, F j, Y'); 91719378907SAtari911 } else { 91819378907SAtari911 $startDate = date('Y-m-01'); 91919378907SAtari911 $endDate = date('Y-m-t'); 920e3a9f44cSAtari911 $dt = new DateTime($startDate); 921e3a9f44cSAtari911 $headerText = $dt->format('F Y'); 92219378907SAtari911 } 92319378907SAtari911 924e3a9f44cSAtari911 // Load all events in date range 92519378907SAtari911 $allEvents = array(); 92619378907SAtari911 $start = new DateTime($startDate); 92719378907SAtari911 $end = new DateTime($endDate); 92819378907SAtari911 $end->modify('+1 day'); 92919378907SAtari911 93019378907SAtari911 $interval = new DateInterval('P1D'); 93119378907SAtari911 $period = new DatePeriod($start, $interval, $end); 93219378907SAtari911 933e3a9f44cSAtari911 // Check if multiple namespaces or wildcard specified 934e3a9f44cSAtari911 $isMultiNamespace = !empty($namespace) && (strpos($namespace, ';') !== false || strpos($namespace, '*') !== false); 935e3a9f44cSAtari911 93619378907SAtari911 static $loadedMonths = array(); 93719378907SAtari911 93819378907SAtari911 foreach ($period as $dt) { 93919378907SAtari911 $year = (int)$dt->format('Y'); 94019378907SAtari911 $month = (int)$dt->format('n'); 94119378907SAtari911 $dateKey = $dt->format('Y-m-d'); 94219378907SAtari911 943e3a9f44cSAtari911 $monthKey = $year . '-' . $month . '-' . $namespace; 94419378907SAtari911 94519378907SAtari911 if (!isset($loadedMonths[$monthKey])) { 946e3a9f44cSAtari911 if ($isMultiNamespace) { 947e3a9f44cSAtari911 $loadedMonths[$monthKey] = $this->loadEventsMultiNamespace($namespace, $year, $month); 948e3a9f44cSAtari911 } else { 94919378907SAtari911 $loadedMonths[$monthKey] = $this->loadEvents($namespace, $year, $month); 95019378907SAtari911 } 951e3a9f44cSAtari911 } 95219378907SAtari911 95319378907SAtari911 $monthEvents = $loadedMonths[$monthKey]; 95419378907SAtari911 95519378907SAtari911 if (isset($monthEvents[$dateKey]) && !empty($monthEvents[$dateKey])) { 95619378907SAtari911 $allEvents[$dateKey] = $monthEvents[$dateKey]; 95719378907SAtari911 } 95819378907SAtari911 } 95919378907SAtari911 9601d05cddcSAtari911 // Sort events by date (already sorted by dateKey), then by time within each day 9611d05cddcSAtari911 foreach ($allEvents as $dateKey => &$dayEvents) { 9621d05cddcSAtari911 usort($dayEvents, function($a, $b) { 9631d05cddcSAtari911 $timeA = isset($a['time']) && !empty($a['time']) ? $a['time'] : null; 9641d05cddcSAtari911 $timeB = isset($b['time']) && !empty($b['time']) ? $b['time'] : null; 9651d05cddcSAtari911 9661d05cddcSAtari911 // All-day events (no time) go to the TOP 9671d05cddcSAtari911 if ($timeA === null && $timeB !== null) return -1; // A before B 9681d05cddcSAtari911 if ($timeA !== null && $timeB === null) return 1; // A after B 9691d05cddcSAtari911 if ($timeA === null && $timeB === null) return 0; // Both all-day, equal 9701d05cddcSAtari911 9711d05cddcSAtari911 // Both have times, sort chronologically 9721d05cddcSAtari911 return strcmp($timeA, $timeB); 9731d05cddcSAtari911 }); 9741d05cddcSAtari911 } 9751d05cddcSAtari911 unset($dayEvents); // Break reference 9761d05cddcSAtari911 977e3a9f44cSAtari911 // Simple 2-line display widget 9781d05cddcSAtari911 $calId = 'eventlist_' . uniqid(); 9791d05cddcSAtari911 $html = '<div class="eventlist-simple" id="' . $calId . '">'; 9801d05cddcSAtari911 9811d05cddcSAtari911 // Load calendar JavaScript manually (not through DokuWiki concatenation) 9821d05cddcSAtari911 $html .= '<script src="' . DOKU_BASE . 'lib/plugins/calendar/calendar-main.js"></script>'; 9831d05cddcSAtari911 9841d05cddcSAtari911 // Initialize DOKU_BASE for JavaScript 9851d05cddcSAtari911 $html .= '<script>if(typeof DOKU_BASE==="undefined"){window.DOKU_BASE="' . DOKU_BASE . '";}</script>'; 9861d05cddcSAtari911 9871d05cddcSAtari911 // Add compact header with date and clock for "today" mode (unless noheader is set) 9881d05cddcSAtari911 if ($today && !empty($allEvents) && !$noheader) { 9891d05cddcSAtari911 $todayDate = new DateTime(); 9901d05cddcSAtari911 $displayDate = $todayDate->format('D, M j, Y'); // "Fri, Jan 30, 2026" 9911d05cddcSAtari911 $currentTime = $todayDate->format('g:i:s A'); // "2:45:30 PM" 9921d05cddcSAtari911 9931d05cddcSAtari911 $html .= '<div class="eventlist-today-header">'; 9941d05cddcSAtari911 $html .= '<span class="eventlist-today-clock" id="clock-' . $calId . '">' . $currentTime . '</span>'; 9951d05cddcSAtari911 $html .= '<div class="eventlist-bottom-info">'; 9961d05cddcSAtari911 $html .= '<span class="eventlist-weather"><span id="weather-icon-' . $calId . '">️</span> <span id="weather-temp-' . $calId . '">--°</span></span>'; 9971d05cddcSAtari911 $html .= '<span class="eventlist-today-date">' . $displayDate . '</span>'; 9981d05cddcSAtari911 $html .= '</div>'; 9991d05cddcSAtari911 10001d05cddcSAtari911 // Three CPU/Memory bars (all update live) 10011d05cddcSAtari911 $html .= '<div class="eventlist-stats-container">'; 10021d05cddcSAtari911 10031d05cddcSAtari911 // 5-minute load average (green, updates every 2 seconds) 10041d05cddcSAtari911 $html .= '<div class="eventlist-cpu-bar" onmouseover="showTooltip_' . $calId . '(\'green\')" onmouseout="hideTooltip_' . $calId . '(\'green\')">'; 10051d05cddcSAtari911 $html .= '<div class="eventlist-cpu-fill" id="cpu-5min-' . $calId . '" style="width: 0%;"></div>'; 10061d05cddcSAtari911 $html .= '<div class="system-tooltip" id="tooltip-green-' . $calId . '" style="display:none;"></div>'; 10071d05cddcSAtari911 $html .= '</div>'; 10081d05cddcSAtari911 10091d05cddcSAtari911 // Real-time CPU (purple, updates with 5-sec average) 10101d05cddcSAtari911 $html .= '<div class="eventlist-cpu-bar eventlist-cpu-realtime" onmouseover="showTooltip_' . $calId . '(\'purple\')" onmouseout="hideTooltip_' . $calId . '(\'purple\')">'; 10111d05cddcSAtari911 $html .= '<div class="eventlist-cpu-fill eventlist-cpu-fill-purple" id="cpu-realtime-' . $calId . '" style="width: 0%;"></div>'; 10121d05cddcSAtari911 $html .= '<div class="system-tooltip" id="tooltip-purple-' . $calId . '" style="display:none;"></div>'; 10131d05cddcSAtari911 $html .= '</div>'; 10141d05cddcSAtari911 10151d05cddcSAtari911 // Real-time Memory (orange, updates) 10161d05cddcSAtari911 $html .= '<div class="eventlist-cpu-bar eventlist-mem-realtime" onmouseover="showTooltip_' . $calId . '(\'orange\')" onmouseout="hideTooltip_' . $calId . '(\'orange\')">'; 10171d05cddcSAtari911 $html .= '<div class="eventlist-cpu-fill eventlist-cpu-fill-orange" id="mem-realtime-' . $calId . '" style="width: 0%;"></div>'; 10181d05cddcSAtari911 $html .= '<div class="system-tooltip" id="tooltip-orange-' . $calId . '" style="display:none;"></div>'; 10191d05cddcSAtari911 $html .= '</div>'; 10201d05cddcSAtari911 10211d05cddcSAtari911 $html .= '</div>'; 10221d05cddcSAtari911 $html .= '</div>'; 10231d05cddcSAtari911 10241d05cddcSAtari911 // Add JavaScript to update clock and weather 10251d05cddcSAtari911 $html .= '<script> 10261d05cddcSAtari911(function() { 10271d05cddcSAtari911 // Update clock every second 10281d05cddcSAtari911 function updateClock() { 10291d05cddcSAtari911 const now = new Date(); 10301d05cddcSAtari911 let hours = now.getHours(); 10311d05cddcSAtari911 const minutes = String(now.getMinutes()).padStart(2, "0"); 10321d05cddcSAtari911 const seconds = String(now.getSeconds()).padStart(2, "0"); 10331d05cddcSAtari911 const ampm = hours >= 12 ? "PM" : "AM"; 10341d05cddcSAtari911 hours = hours % 12 || 12; 10351d05cddcSAtari911 const timeStr = hours + ":" + minutes + ":" + seconds + " " + ampm; 10361d05cddcSAtari911 const clockEl = document.getElementById("clock-' . $calId . '"); 10371d05cddcSAtari911 if (clockEl) clockEl.textContent = timeStr; 10381d05cddcSAtari911 } 10391d05cddcSAtari911 setInterval(updateClock, 1000); 10401d05cddcSAtari911 10411d05cddcSAtari911 // Fetch weather (geolocation-based) 10421d05cddcSAtari911 function updateWeather() { 10431d05cddcSAtari911 if ("geolocation" in navigator) { 10441d05cddcSAtari911 navigator.geolocation.getCurrentPosition(function(position) { 10451d05cddcSAtari911 const lat = position.coords.latitude; 10461d05cddcSAtari911 const lon = position.coords.longitude; 10471d05cddcSAtari911 10481d05cddcSAtari911 // Use Open-Meteo API (free, no key required) 10491d05cddcSAtari911 fetch(`https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}¤t_weather=true&temperature_unit=fahrenheit`) 10501d05cddcSAtari911 .then(response => response.json()) 10511d05cddcSAtari911 .then(data => { 10521d05cddcSAtari911 if (data.current_weather) { 10531d05cddcSAtari911 const temp = Math.round(data.current_weather.temperature); 10541d05cddcSAtari911 const weatherCode = data.current_weather.weathercode; 10551d05cddcSAtari911 const icon = getWeatherIcon(weatherCode); 10561d05cddcSAtari911 const iconEl = document.getElementById("weather-icon-' . $calId . '"); 10571d05cddcSAtari911 const tempEl = document.getElementById("weather-temp-' . $calId . '"); 10581d05cddcSAtari911 if (iconEl) iconEl.textContent = icon; 10591d05cddcSAtari911 if (tempEl) tempEl.innerHTML = temp + "°"; 10601d05cddcSAtari911 } 10611d05cddcSAtari911 }) 10621d05cddcSAtari911 .catch(error => { 10631d05cddcSAtari911 console.log("Weather fetch error:", error); 10641d05cddcSAtari911 }); 10651d05cddcSAtari911 }, function(error) { 10661d05cddcSAtari911 // If geolocation fails, use Sacramento as default 10671d05cddcSAtari911 fetch("https://api.open-meteo.com/v1/forecast?latitude=38.5816&longitude=-121.4944¤t_weather=true&temperature_unit=fahrenheit") 10681d05cddcSAtari911 .then(response => response.json()) 10691d05cddcSAtari911 .then(data => { 10701d05cddcSAtari911 if (data.current_weather) { 10711d05cddcSAtari911 const temp = Math.round(data.current_weather.temperature); 10721d05cddcSAtari911 const weatherCode = data.current_weather.weathercode; 10731d05cddcSAtari911 const icon = getWeatherIcon(weatherCode); 10741d05cddcSAtari911 const iconEl = document.getElementById("weather-icon-' . $calId . '"); 10751d05cddcSAtari911 const tempEl = document.getElementById("weather-temp-' . $calId . '"); 10761d05cddcSAtari911 if (iconEl) iconEl.textContent = icon; 10771d05cddcSAtari911 if (tempEl) tempEl.innerHTML = temp + "°"; 10781d05cddcSAtari911 } 10791d05cddcSAtari911 }) 10801d05cddcSAtari911 .catch(err => console.log("Weather error:", err)); 10811d05cddcSAtari911 }); 10821d05cddcSAtari911 } else { 10831d05cddcSAtari911 // No geolocation, use Sacramento 10841d05cddcSAtari911 fetch("https://api.open-meteo.com/v1/forecast?latitude=38.5816&longitude=-121.4944¤t_weather=true&temperature_unit=fahrenheit") 10851d05cddcSAtari911 .then(response => response.json()) 10861d05cddcSAtari911 .then(data => { 10871d05cddcSAtari911 if (data.current_weather) { 10881d05cddcSAtari911 const temp = Math.round(data.current_weather.temperature); 10891d05cddcSAtari911 const weatherCode = data.current_weather.weathercode; 10901d05cddcSAtari911 const icon = getWeatherIcon(weatherCode); 10911d05cddcSAtari911 const iconEl = document.getElementById("weather-icon-' . $calId . '"); 10921d05cddcSAtari911 const tempEl = document.getElementById("weather-temp-' . $calId . '"); 10931d05cddcSAtari911 if (iconEl) iconEl.textContent = icon; 10941d05cddcSAtari911 if (tempEl) tempEl.innerHTML = temp + "°"; 10951d05cddcSAtari911 } 10961d05cddcSAtari911 }) 10971d05cddcSAtari911 .catch(err => console.log("Weather error:", err)); 10981d05cddcSAtari911 } 10991d05cddcSAtari911 } 11001d05cddcSAtari911 11011d05cddcSAtari911 // WMO Weather interpretation codes 11021d05cddcSAtari911 function getWeatherIcon(code) { 11031d05cddcSAtari911 const icons = { 11041d05cddcSAtari911 0: "☀️", // Clear sky 11051d05cddcSAtari911 1: "️", // Mainly clear 11061d05cddcSAtari911 2: "⛅", // Partly cloudy 11071d05cddcSAtari911 3: "☁️", // Overcast 11081d05cddcSAtari911 45: "️", // Fog 11091d05cddcSAtari911 48: "️", // Depositing rime fog 11101d05cddcSAtari911 51: "️", // Light drizzle 11111d05cddcSAtari911 53: "️", // Moderate drizzle 11121d05cddcSAtari911 55: "️", // Dense drizzle 11131d05cddcSAtari911 61: "️", // Slight rain 11141d05cddcSAtari911 63: "️", // Moderate rain 11151d05cddcSAtari911 65: "⛈️", // Heavy rain 11161d05cddcSAtari911 71: "️", // Slight snow 11171d05cddcSAtari911 73: "️", // Moderate snow 11181d05cddcSAtari911 75: "❄️", // Heavy snow 11191d05cddcSAtari911 77: "️", // Snow grains 11201d05cddcSAtari911 80: "️", // Slight rain showers 11211d05cddcSAtari911 81: "️", // Moderate rain showers 11221d05cddcSAtari911 82: "⛈️", // Violent rain showers 11231d05cddcSAtari911 85: "️", // Slight snow showers 11241d05cddcSAtari911 86: "❄️", // Heavy snow showers 11251d05cddcSAtari911 95: "⛈️", // Thunderstorm 11261d05cddcSAtari911 96: "⛈️", // Thunderstorm with slight hail 11271d05cddcSAtari911 99: "⛈️" // Thunderstorm with heavy hail 11281d05cddcSAtari911 }; 11291d05cddcSAtari911 return icons[code] || "️"; 11301d05cddcSAtari911 } 11311d05cddcSAtari911 11321d05cddcSAtari911 // Update weather immediately and every 10 minutes 11331d05cddcSAtari911 updateWeather(); 11341d05cddcSAtari911 setInterval(updateWeather, 600000); 11351d05cddcSAtari911 11361d05cddcSAtari911 // CPU load history for 4-second rolling average 11371d05cddcSAtari911 const cpuHistory = []; 11381d05cddcSAtari911 const CPU_HISTORY_SIZE = 2; // 2 samples × 2 seconds = 4 seconds 11391d05cddcSAtari911 11401d05cddcSAtari911 // Store latest system stats for tooltips 11411d05cddcSAtari911 let latestStats = { 11421d05cddcSAtari911 load: {"1min": 0, "5min": 0, "15min": 0}, 11431d05cddcSAtari911 uptime: "", 11441d05cddcSAtari911 memory_details: {}, 11451d05cddcSAtari911 top_processes: [] 11461d05cddcSAtari911 }; 11471d05cddcSAtari911 11481d05cddcSAtari911 // Tooltip functions 11491d05cddcSAtari911 window["showTooltip_' . $calId . '"] = function(color) { 11501d05cddcSAtari911 const tooltip = document.getElementById("tooltip-" + color + "-' . $calId . '"); 11511d05cddcSAtari911 if (!tooltip) { 11521d05cddcSAtari911 console.log("Tooltip element not found for color:", color); 11531d05cddcSAtari911 return; 11541d05cddcSAtari911 } 11551d05cddcSAtari911 11561d05cddcSAtari911 console.log("Showing tooltip for:", color, "latestStats:", latestStats); 11571d05cddcSAtari911 11581d05cddcSAtari911 let content = ""; 11591d05cddcSAtari911 11601d05cddcSAtari911 if (color === "green") { 11611d05cddcSAtari911 // Green bar: Load averages and uptime 11621d05cddcSAtari911 content = "<div class=\"tooltip-title\">CPU Load Average</div>"; 11631d05cddcSAtari911 content += "<div>1 min: " + (latestStats.load["1min"] || "N/A") + "</div>"; 11641d05cddcSAtari911 content += "<div>5 min: " + (latestStats.load["5min"] || "N/A") + "</div>"; 11651d05cddcSAtari911 content += "<div>15 min: " + (latestStats.load["15min"] || "N/A") + "</div>"; 11661d05cddcSAtari911 if (latestStats.uptime) { 11671d05cddcSAtari911 content += "<div style=\"margin-top:3px; padding-top:2px; border-top:1px solid rgba(0,204,7,0.3);\">Uptime: " + latestStats.uptime + "</div>"; 11681d05cddcSAtari911 } 11691d05cddcSAtari911 tooltip.style.borderColor = "#00cc07"; 11701d05cddcSAtari911 tooltip.style.color = "#00cc07"; 11711d05cddcSAtari911 } else if (color === "purple") { 11721d05cddcSAtari911 // Purple bar: Load averages (short-term) and top processes 11731d05cddcSAtari911 content = "<div class=\"tooltip-title\">CPU Load (Short-term)</div>"; 11741d05cddcSAtari911 content += "<div>1 min: " + (latestStats.load["1min"] || "N/A") + "</div>"; 11751d05cddcSAtari911 content += "<div>5 min: " + (latestStats.load["5min"] || "N/A") + "</div>"; 11761d05cddcSAtari911 if (latestStats.top_processes && latestStats.top_processes.length > 0) { 11771d05cddcSAtari911 content += "<div style=\"margin-top:3px; padding-top:2px; border-top:1px solid rgba(155,89,182,0.3);\" class=\"tooltip-title\">Top Processes</div>"; 11781d05cddcSAtari911 latestStats.top_processes.slice(0, 5).forEach(proc => { 11791d05cddcSAtari911 content += "<div>" + proc.cpu + " " + proc.command + "</div>"; 11801d05cddcSAtari911 }); 11811d05cddcSAtari911 } 11821d05cddcSAtari911 tooltip.style.borderColor = "#9b59b6"; 11831d05cddcSAtari911 tooltip.style.color = "#9b59b6"; 11841d05cddcSAtari911 } else if (color === "orange") { 11851d05cddcSAtari911 // Orange bar: Memory details and top processes 11861d05cddcSAtari911 content = "<div class=\"tooltip-title\">Memory Usage</div>"; 11871d05cddcSAtari911 if (latestStats.memory_details && latestStats.memory_details.total) { 11881d05cddcSAtari911 content += "<div>Total: " + latestStats.memory_details.total + "</div>"; 11891d05cddcSAtari911 content += "<div>Used: " + latestStats.memory_details.used + "</div>"; 11901d05cddcSAtari911 content += "<div>Available: " + latestStats.memory_details.available + "</div>"; 11911d05cddcSAtari911 if (latestStats.memory_details.cached) { 11921d05cddcSAtari911 content += "<div>Cached: " + latestStats.memory_details.cached + "</div>"; 11931d05cddcSAtari911 } 11941d05cddcSAtari911 } else { 11951d05cddcSAtari911 content += "<div>Loading...</div>"; 11961d05cddcSAtari911 } 11971d05cddcSAtari911 if (latestStats.top_processes && latestStats.top_processes.length > 0) { 11981d05cddcSAtari911 content += "<div style=\"margin-top:3px; padding-top:2px; border-top:1px solid rgba(255,140,0,0.3);\" class=\"tooltip-title\">Top Processes</div>"; 11991d05cddcSAtari911 latestStats.top_processes.slice(0, 5).forEach(proc => { 12001d05cddcSAtari911 content += "<div>" + proc.cpu + " " + proc.command + "</div>"; 12011d05cddcSAtari911 }); 12021d05cddcSAtari911 } 12031d05cddcSAtari911 tooltip.style.borderColor = "#ff9800"; 12041d05cddcSAtari911 tooltip.style.color = "#ff9800"; 12051d05cddcSAtari911 } 12061d05cddcSAtari911 12071d05cddcSAtari911 console.log("Tooltip content:", content); 12081d05cddcSAtari911 tooltip.innerHTML = content; 12091d05cddcSAtari911 tooltip.style.display = "block"; 12101d05cddcSAtari911 12111d05cddcSAtari911 // Position tooltip using fixed positioning above the bar 12121d05cddcSAtari911 const bar = tooltip.parentElement; 12131d05cddcSAtari911 const barRect = bar.getBoundingClientRect(); 12141d05cddcSAtari911 const tooltipRect = tooltip.getBoundingClientRect(); 12151d05cddcSAtari911 12161d05cddcSAtari911 // Center horizontally on the bar 12171d05cddcSAtari911 const left = barRect.left + (barRect.width / 2) - (tooltipRect.width / 2); 12181d05cddcSAtari911 // Position above the bar with 8px gap 12191d05cddcSAtari911 const top = barRect.top - tooltipRect.height - 8; 12201d05cddcSAtari911 12211d05cddcSAtari911 tooltip.style.left = left + "px"; 12221d05cddcSAtari911 tooltip.style.top = top + "px"; 12231d05cddcSAtari911 }; 12241d05cddcSAtari911 12251d05cddcSAtari911 window["hideTooltip_' . $calId . '"] = function(color) { 12261d05cddcSAtari911 const tooltip = document.getElementById("tooltip-" + color + "-' . $calId . '"); 12271d05cddcSAtari911 if (tooltip) { 12281d05cddcSAtari911 tooltip.style.display = "none"; 12291d05cddcSAtari911 } 12301d05cddcSAtari911 }; 12311d05cddcSAtari911 12321d05cddcSAtari911 // Update CPU and memory bars every 2 seconds 12331d05cddcSAtari911 function updateSystemStats() { 12341d05cddcSAtari911 // Fetch real system stats from server 12351d05cddcSAtari911 fetch("' . DOKU_BASE . 'lib/plugins/calendar/get_system_stats.php") 12361d05cddcSAtari911 .then(response => response.json()) 12371d05cddcSAtari911 .then(data => { 12381d05cddcSAtari911 console.log("System stats received:", data); 12391d05cddcSAtari911 12401d05cddcSAtari911 // Store data for tooltips 12411d05cddcSAtari911 latestStats = { 12421d05cddcSAtari911 load: data.load || {"1min": 0, "5min": 0, "15min": 0}, 12431d05cddcSAtari911 uptime: data.uptime || "", 12441d05cddcSAtari911 memory_details: data.memory_details || {}, 12451d05cddcSAtari911 top_processes: data.top_processes || [] 12461d05cddcSAtari911 }; 12471d05cddcSAtari911 12481d05cddcSAtari911 console.log("latestStats updated to:", latestStats); 12491d05cddcSAtari911 12501d05cddcSAtari911 // Update green bar (5-minute average) - updates live now! 12511d05cddcSAtari911 const greenBar = document.getElementById("cpu-5min-' . $calId . '"); 12521d05cddcSAtari911 if (greenBar) { 12531d05cddcSAtari911 greenBar.style.width = Math.min(100, data.cpu_5min) + "%"; 12541d05cddcSAtari911 } 12551d05cddcSAtari911 12561d05cddcSAtari911 // Add current CPU to history for purple bar 12571d05cddcSAtari911 cpuHistory.push(data.cpu); 12581d05cddcSAtari911 if (cpuHistory.length > CPU_HISTORY_SIZE) { 12591d05cddcSAtari911 cpuHistory.shift(); // Remove oldest 12601d05cddcSAtari911 } 12611d05cddcSAtari911 12621d05cddcSAtari911 // Calculate 5-second average for CPU 12631d05cddcSAtari911 const cpuAverage = cpuHistory.reduce((sum, val) => sum + val, 0) / cpuHistory.length; 12641d05cddcSAtari911 12651d05cddcSAtari911 // Update CPU bar (purple) with 5-second average 12661d05cddcSAtari911 const cpuBar = document.getElementById("cpu-realtime-' . $calId . '"); 12671d05cddcSAtari911 if (cpuBar) { 12681d05cddcSAtari911 cpuBar.style.width = Math.min(100, cpuAverage) + "%"; 12691d05cddcSAtari911 } 12701d05cddcSAtari911 12711d05cddcSAtari911 // Update memory bar (orange) with real data 12721d05cddcSAtari911 const memBar = document.getElementById("mem-realtime-' . $calId . '"); 12731d05cddcSAtari911 if (memBar) { 12741d05cddcSAtari911 memBar.style.width = Math.min(100, data.memory) + "%"; 12751d05cddcSAtari911 } 12761d05cddcSAtari911 }) 12771d05cddcSAtari911 .catch(error => { 12781d05cddcSAtari911 console.log("System stats error:", error); 12791d05cddcSAtari911 // Fallback to client-side estimates on error 12801d05cddcSAtari911 const cpuFallback = Math.random() * 100; 12811d05cddcSAtari911 cpuHistory.push(cpuFallback); 12821d05cddcSAtari911 if (cpuHistory.length > CPU_HISTORY_SIZE) { 12831d05cddcSAtari911 cpuHistory.shift(); 12841d05cddcSAtari911 } 12851d05cddcSAtari911 const cpuAverage = cpuHistory.reduce((sum, val) => sum + val, 0) / cpuHistory.length; 12861d05cddcSAtari911 12871d05cddcSAtari911 const greenBar = document.getElementById("cpu-5min-' . $calId . '"); 12881d05cddcSAtari911 if (greenBar) greenBar.style.width = Math.min(100, cpuFallback) + "%"; 12891d05cddcSAtari911 12901d05cddcSAtari911 const cpuBar = document.getElementById("cpu-realtime-' . $calId . '"); 12911d05cddcSAtari911 if (cpuBar) cpuBar.style.width = Math.min(100, cpuAverage) + "%"; 12921d05cddcSAtari911 12931d05cddcSAtari911 let memoryUsage = 0; 12941d05cddcSAtari911 if (performance.memory) { 12951d05cddcSAtari911 memoryUsage = (performance.memory.usedJSHeapSize / performance.memory.jsHeapSizeLimit) * 100; 12961d05cddcSAtari911 } else { 12971d05cddcSAtari911 memoryUsage = Math.random() * 100; 12981d05cddcSAtari911 } 12991d05cddcSAtari911 const memBar = document.getElementById("mem-realtime-' . $calId . '"); 13001d05cddcSAtari911 if (memBar) memBar.style.width = Math.min(100, memoryUsage) + "%"; 13011d05cddcSAtari911 }); 13021d05cddcSAtari911 } 13031d05cddcSAtari911 13041d05cddcSAtari911 // Update immediately and then every 2 seconds 13051d05cddcSAtari911 updateSystemStats(); 13061d05cddcSAtari911 setInterval(updateSystemStats, 2000); 13071d05cddcSAtari911})(); 13081d05cddcSAtari911</script>'; 13091d05cddcSAtari911 } 131019378907SAtari911 131119378907SAtari911 if (empty($allEvents)) { 1312e3a9f44cSAtari911 $html .= '<div class="eventlist-simple-empty">'; 1313e3a9f44cSAtari911 $html .= '<div class="eventlist-simple-header">' . htmlspecialchars($headerText); 1314e3a9f44cSAtari911 if ($namespace) { 1315e3a9f44cSAtari911 $html .= ' <span class="eventlist-simple-namespace">' . htmlspecialchars($namespace) . '</span>'; 131687ac9bf3SAtari911 } 1317e3a9f44cSAtari911 $html .= '</div>'; 1318e3a9f44cSAtari911 $html .= '<div class="eventlist-simple-body">No events</div>'; 1319e3a9f44cSAtari911 $html .= '</div>'; 1320e3a9f44cSAtari911 } else { 1321e3a9f44cSAtari911 // Calculate today and tomorrow's dates for highlighting 13221d05cddcSAtari911 $todayStr = date('Y-m-d'); 1323e3a9f44cSAtari911 $tomorrow = date('Y-m-d', strtotime('+1 day')); 1324e3a9f44cSAtari911 1325e3a9f44cSAtari911 foreach ($allEvents as $dateKey => $dayEvents) { 1326e3a9f44cSAtari911 $dateObj = new DateTime($dateKey); 1327e3a9f44cSAtari911 $displayDate = $dateObj->format('D, M j'); 1328e3a9f44cSAtari911 13291d05cddcSAtari911 // Check if this date is today or tomorrow or past 1330e3a9f44cSAtari911 // Enable highlighting for sidebar mode AND range modes (day, week, month) 1331e3a9f44cSAtari911 $enableHighlighting = $sidebar || !empty($range); 13321d05cddcSAtari911 $isToday = $enableHighlighting && ($dateKey === $todayStr); 1333e3a9f44cSAtari911 $isTomorrow = $enableHighlighting && ($dateKey === $tomorrow); 13341d05cddcSAtari911 $isPast = $dateKey < $todayStr; 133519378907SAtari911 133619378907SAtari911 foreach ($dayEvents as $event) { 13371d05cddcSAtari911 // Check if this is a task and if it's completed 13381d05cddcSAtari911 $isTask = !empty($event['isTask']); 13391d05cddcSAtari911 $completed = !empty($event['completed']); 13401d05cddcSAtari911 13411d05cddcSAtari911 // ALWAYS skip completed tasks UNLESS showchecked is explicitly set 13421d05cddcSAtari911 if (!$showchecked && $isTask && $completed) { 1343e3a9f44cSAtari911 continue; 1344e3a9f44cSAtari911 } 134519378907SAtari911 13461d05cddcSAtari911 // Skip past events that are NOT tasks (only show past due tasks from the past) 13471d05cddcSAtari911 if ($isPast && !$isTask) { 13481d05cddcSAtari911 continue; 13491d05cddcSAtari911 } 13501d05cddcSAtari911 13511d05cddcSAtari911 // Determine if task is past due (past date, is task, not completed) 13521d05cddcSAtari911 $isPastDue = $isPast && $isTask && !$completed; 13531d05cddcSAtari911 1354e3a9f44cSAtari911 // Line 1: Header (Title, Time, Date, Namespace) 1355e3a9f44cSAtari911 $todayClass = $isToday ? ' eventlist-simple-today' : ''; 1356e3a9f44cSAtari911 $tomorrowClass = $isTomorrow ? ' eventlist-simple-tomorrow' : ''; 13571d05cddcSAtari911 $pastDueClass = $isPastDue ? ' eventlist-simple-pastdue' : ''; 13581d05cddcSAtari911 $html .= '<div class="eventlist-simple-item' . $todayClass . $tomorrowClass . $pastDueClass . '">'; 1359e3a9f44cSAtari911 $html .= '<div class="eventlist-simple-header">'; 1360e3a9f44cSAtari911 1361e3a9f44cSAtari911 // Title 1362e3a9f44cSAtari911 $html .= '<span class="eventlist-simple-title">' . htmlspecialchars($event['title']) . '</span>'; 1363e3a9f44cSAtari911 1364e3a9f44cSAtari911 // Time (12-hour format) 1365e3a9f44cSAtari911 if (!empty($event['time'])) { 1366e3a9f44cSAtari911 $timeParts = explode(':', $event['time']); 136787ac9bf3SAtari911 if (count($timeParts) === 2) { 136887ac9bf3SAtari911 $hour = (int)$timeParts[0]; 136987ac9bf3SAtari911 $minute = $timeParts[1]; 137087ac9bf3SAtari911 $ampm = $hour >= 12 ? 'PM' : 'AM'; 1371e3a9f44cSAtari911 $hour = $hour % 12 ?: 12; 137287ac9bf3SAtari911 $displayTime = $hour . ':' . $minute . ' ' . $ampm; 1373e3a9f44cSAtari911 $html .= ' <span class="eventlist-simple-time">' . $displayTime . '</span>'; 137419378907SAtari911 } 137587ac9bf3SAtari911 } 137687ac9bf3SAtari911 1377e3a9f44cSAtari911 // Date 1378e3a9f44cSAtari911 $html .= ' <span class="eventlist-simple-date">' . $displayDate . '</span>'; 1379e3a9f44cSAtari911 13801d05cddcSAtari911 // Badge: PAST DUE, TODAY, or nothing 13811d05cddcSAtari911 if ($isPastDue) { 13821d05cddcSAtari911 $html .= ' <span class="eventlist-simple-pastdue-badge">PAST DUE</span>'; 13831d05cddcSAtari911 } elseif ($isToday) { 1384e3a9f44cSAtari911 $html .= ' <span class="eventlist-simple-today-badge">TODAY</span>'; 138587ac9bf3SAtari911 } 1386e3a9f44cSAtari911 1387e3a9f44cSAtari911 // Namespace badge (show individual event's namespace) 1388e3a9f44cSAtari911 $eventNamespace = isset($event['namespace']) ? $event['namespace'] : ''; 1389e3a9f44cSAtari911 if (!$eventNamespace && isset($event['_namespace'])) { 1390e3a9f44cSAtari911 $eventNamespace = $event['_namespace']; // Fallback to _namespace for multi-namespace loading 139119378907SAtari911 } 1392e3a9f44cSAtari911 if ($eventNamespace) { 1393e3a9f44cSAtari911 $html .= ' <span class="eventlist-simple-namespace">' . htmlspecialchars($eventNamespace) . '</span>'; 1394e3a9f44cSAtari911 } 1395e3a9f44cSAtari911 1396e3a9f44cSAtari911 $html .= '</div>'; // header 1397e3a9f44cSAtari911 1398e3a9f44cSAtari911 // Line 2: Body (Description only) - only show if description exists 1399e3a9f44cSAtari911 if (!empty($event['description'])) { 1400e3a9f44cSAtari911 $html .= '<div class="eventlist-simple-body">' . $this->renderDescription($event['description']) . '</div>'; 1401e3a9f44cSAtari911 } 1402e3a9f44cSAtari911 1403e3a9f44cSAtari911 $html .= '</div>'; // item 140419378907SAtari911 } 140519378907SAtari911 } 140687ac9bf3SAtari911 } 140719378907SAtari911 1408e3a9f44cSAtari911 $html .= '</div>'; // eventlist-simple 140919378907SAtari911 141019378907SAtari911 return $html; 141119378907SAtari911 } 141219378907SAtari911 141319378907SAtari911 private function renderEventDialog($calId, $namespace) { 141419378907SAtari911 $html = '<div class="event-dialog-compact" id="dialog-' . $calId . '" style="display:none;">'; 141519378907SAtari911 $html .= '<div class="dialog-overlay" onclick="closeEventDialog(\'' . $calId . '\')"></div>'; 141619378907SAtari911 141719378907SAtari911 // Draggable dialog 141819378907SAtari911 $html .= '<div class="dialog-content-sleek" id="dialog-content-' . $calId . '">'; 141919378907SAtari911 142019378907SAtari911 // Header with drag handle and close button 142119378907SAtari911 $html .= '<div class="dialog-header-sleek dialog-drag-handle" id="drag-handle-' . $calId . '">'; 142219378907SAtari911 $html .= '<h3 id="dialog-title-' . $calId . '">Add Event</h3>'; 142319378907SAtari911 $html .= '<button type="button" class="dialog-close-btn" onclick="closeEventDialog(\'' . $calId . '\')">×</button>'; 142419378907SAtari911 $html .= '</div>'; 142519378907SAtari911 142619378907SAtari911 // Form content 142719378907SAtari911 $html .= '<form id="eventform-' . $calId . '" onsubmit="saveEventCompact(\'' . $calId . '\', \'' . $namespace . '\'); return false;" class="sleek-form">'; 142819378907SAtari911 142919378907SAtari911 // Hidden ID field 143019378907SAtari911 $html .= '<input type="hidden" id="event-id-' . $calId . '" name="eventId" value="">'; 143119378907SAtari911 14321d05cddcSAtari911 // 1. TITLE 14331d05cddcSAtari911 $html .= '<div class="form-field">'; 14341d05cddcSAtari911 $html .= '<label class="field-label"> Title</label>'; 14351d05cddcSAtari911 $html .= '<input type="text" id="event-title-' . $calId . '" name="title" required class="input-sleek input-compact" placeholder="Event or task title...">'; 143619378907SAtari911 $html .= '</div>'; 143719378907SAtari911 14381d05cddcSAtari911 // 1.5 NAMESPACE SELECTOR (Searchable with fuzzy matching) 14391d05cddcSAtari911 $html .= '<div class="form-field">'; 14401d05cddcSAtari911 $html .= '<label class="field-label"> Namespace</label>'; 14411d05cddcSAtari911 14421d05cddcSAtari911 // Hidden field to store actual selected namespace 14431d05cddcSAtari911 $html .= '<input type="hidden" id="event-namespace-' . $calId . '" name="namespace" value="">'; 14441d05cddcSAtari911 14451d05cddcSAtari911 // Searchable input 14461d05cddcSAtari911 $html .= '<div class="namespace-search-wrapper">'; 14471d05cddcSAtari911 $html .= '<input type="text" id="event-namespace-search-' . $calId . '" class="input-sleek input-compact namespace-search-input" placeholder="Type to search or leave empty for default..." autocomplete="off">'; 14481d05cddcSAtari911 $html .= '<div class="namespace-dropdown" id="event-namespace-dropdown-' . $calId . '" style="display:none;"></div>'; 14491d05cddcSAtari911 $html .= '</div>'; 14501d05cddcSAtari911 14511d05cddcSAtari911 // Store namespaces as JSON for JavaScript 14521d05cddcSAtari911 $allNamespaces = $this->getAllNamespaces(); 14531d05cddcSAtari911 $html .= '<script type="application/json" id="namespaces-data-' . $calId . '">' . json_encode($allNamespaces) . '</script>'; 14541d05cddcSAtari911 14551d05cddcSAtari911 $html .= '</div>'; 14561d05cddcSAtari911 14571d05cddcSAtari911 // 2. DESCRIPTION 14581d05cddcSAtari911 $html .= '<div class="form-field">'; 14591d05cddcSAtari911 $html .= '<label class="field-label"> Description</label>'; 14601d05cddcSAtari911 $html .= '<textarea id="event-desc-' . $calId . '" name="description" rows="1" class="input-sleek textarea-sleek textarea-compact" placeholder="Optional details..."></textarea>'; 14611d05cddcSAtari911 $html .= '</div>'; 14621d05cddcSAtari911 14631d05cddcSAtari911 // 3. START DATE - END DATE (inline) 146419378907SAtari911 $html .= '<div class="form-row-group">'; 146519378907SAtari911 14661d05cddcSAtari911 $html .= '<div class="form-field form-field-half">'; 14671d05cddcSAtari911 $html .= '<label class="field-label-compact"> Start Date</label>'; 14681d05cddcSAtari911 $html .= '<input type="date" id="event-date-' . $calId . '" name="date" required class="input-sleek input-date input-compact">'; 146919378907SAtari911 $html .= '</div>'; 147019378907SAtari911 14711d05cddcSAtari911 $html .= '<div class="form-field form-field-half">'; 14721d05cddcSAtari911 $html .= '<label class="field-label-compact"> End Date</label>'; 14731d05cddcSAtari911 $html .= '<input type="date" id="event-end-date-' . $calId . '" name="endDate" class="input-sleek input-date input-compact" placeholder="Optional">'; 147419378907SAtari911 $html .= '</div>'; 147519378907SAtari911 14761d05cddcSAtari911 $html .= '</div>'; // End row 147719378907SAtari911 14781d05cddcSAtari911 // 4. IS REPEATING CHECKBOX 14791d05cddcSAtari911 $html .= '<div class="form-field form-field-checkbox form-field-checkbox-compact">'; 14801d05cddcSAtari911 $html .= '<label class="checkbox-label checkbox-label-compact">'; 148187ac9bf3SAtari911 $html .= '<input type="checkbox" id="event-recurring-' . $calId . '" name="isRecurring" class="recurring-toggle" onchange="toggleRecurringOptions(\'' . $calId . '\')">'; 148287ac9bf3SAtari911 $html .= '<span> Repeating Event</span>'; 148387ac9bf3SAtari911 $html .= '</label>'; 148487ac9bf3SAtari911 $html .= '</div>'; 148587ac9bf3SAtari911 14861d05cddcSAtari911 // Recurring options (shown when checkbox is checked) 148787ac9bf3SAtari911 $html .= '<div id="recurring-options-' . $calId . '" class="recurring-options" style="display:none;">'; 148887ac9bf3SAtari911 14891d05cddcSAtari911 $html .= '<div class="form-row-group">'; 14901d05cddcSAtari911 14911d05cddcSAtari911 $html .= '<div class="form-field form-field-half">'; 14921d05cddcSAtari911 $html .= '<label class="field-label-compact">Repeat Every</label>'; 14931d05cddcSAtari911 $html .= '<select id="event-recurrence-type-' . $calId . '" name="recurrenceType" class="input-sleek input-compact">'; 149487ac9bf3SAtari911 $html .= '<option value="daily">Daily</option>'; 149587ac9bf3SAtari911 $html .= '<option value="weekly">Weekly</option>'; 149687ac9bf3SAtari911 $html .= '<option value="monthly">Monthly</option>'; 149787ac9bf3SAtari911 $html .= '<option value="yearly">Yearly</option>'; 149887ac9bf3SAtari911 $html .= '</select>'; 149987ac9bf3SAtari911 $html .= '</div>'; 150087ac9bf3SAtari911 15011d05cddcSAtari911 $html .= '<div class="form-field form-field-half">'; 15021d05cddcSAtari911 $html .= '<label class="field-label-compact">Repeat Until</label>'; 15031d05cddcSAtari911 $html .= '<input type="date" id="event-recurrence-end-' . $calId . '" name="recurrenceEnd" class="input-sleek input-date input-compact" placeholder="Optional">'; 150487ac9bf3SAtari911 $html .= '</div>'; 150587ac9bf3SAtari911 15061d05cddcSAtari911 $html .= '</div>'; // End row 15071d05cddcSAtari911 $html .= '</div>'; // End recurring options 150887ac9bf3SAtari911 15091d05cddcSAtari911 // 5. TIME (Start & End) - COLOR (inline) 15101d05cddcSAtari911 $html .= '<div class="form-row-group">'; 15111d05cddcSAtari911 15121d05cddcSAtari911 $html .= '<div class="form-field form-field-half">'; 15131d05cddcSAtari911 $html .= '<label class="field-label-compact"> Start Time</label>'; 15141d05cddcSAtari911 $html .= '<select id="event-time-' . $calId . '" name="time" class="input-sleek input-compact" onchange="updateEndTimeOptions(\'' . $calId . '\')">'; 15151d05cddcSAtari911 $html .= '<option value="">All day</option>'; 1516e3a9f44cSAtari911 1517e3a9f44cSAtari911 // Generate time options in 15-minute intervals 1518e3a9f44cSAtari911 for ($hour = 0; $hour < 24; $hour++) { 1519e3a9f44cSAtari911 for ($minute = 0; $minute < 60; $minute += 15) { 1520e3a9f44cSAtari911 $timeValue = sprintf('%02d:%02d', $hour, $minute); 1521e3a9f44cSAtari911 $displayHour = $hour == 0 ? 12 : ($hour > 12 ? $hour - 12 : $hour); 1522e3a9f44cSAtari911 $ampm = $hour < 12 ? 'AM' : 'PM'; 1523e3a9f44cSAtari911 $displayTime = sprintf('%d:%02d %s', $displayHour, $minute, $ampm); 1524e3a9f44cSAtari911 $html .= '<option value="' . $timeValue . '">' . $displayTime . '</option>'; 1525e3a9f44cSAtari911 } 1526e3a9f44cSAtari911 } 1527e3a9f44cSAtari911 1528e3a9f44cSAtari911 $html .= '</select>'; 152919378907SAtari911 $html .= '</div>'; 153019378907SAtari911 15311d05cddcSAtari911 $html .= '<div class="form-field form-field-half">'; 15321d05cddcSAtari911 $html .= '<label class="field-label-compact"> End Time</label>'; 15331d05cddcSAtari911 $html .= '<select id="event-end-time-' . $calId . '" name="endTime" class="input-sleek input-compact">'; 15341d05cddcSAtari911 $html .= '<option value="">Same as start</option>'; 15351d05cddcSAtari911 15361d05cddcSAtari911 // Generate time options in 15-minute intervals 15371d05cddcSAtari911 for ($hour = 0; $hour < 24; $hour++) { 15381d05cddcSAtari911 for ($minute = 0; $minute < 60; $minute += 15) { 15391d05cddcSAtari911 $timeValue = sprintf('%02d:%02d', $hour, $minute); 15401d05cddcSAtari911 $displayHour = $hour == 0 ? 12 : ($hour > 12 ? $hour - 12 : $hour); 15411d05cddcSAtari911 $ampm = $hour < 12 ? 'AM' : 'PM'; 15421d05cddcSAtari911 $displayTime = sprintf('%d:%02d %s', $displayHour, $minute, $ampm); 15431d05cddcSAtari911 $html .= '<option value="' . $timeValue . '">' . $displayTime . '</option>'; 15441d05cddcSAtari911 } 15451d05cddcSAtari911 } 15461d05cddcSAtari911 15471d05cddcSAtari911 $html .= '</select>'; 154819378907SAtari911 $html .= '</div>'; 154919378907SAtari911 15501d05cddcSAtari911 $html .= '</div>'; // End row 15511d05cddcSAtari911 15521d05cddcSAtari911 // Color field (new row) 15531d05cddcSAtari911 $html .= '<div class="form-row-group">'; 15541d05cddcSAtari911 15551d05cddcSAtari911 $html .= '<div class="form-field form-field-full">'; 15561d05cddcSAtari911 $html .= '<label class="field-label-compact"> Color</label>'; 15571d05cddcSAtari911 $html .= '<div class="color-picker-wrapper">'; 15581d05cddcSAtari911 $html .= '<select id="event-color-' . $calId . '" name="color" class="input-sleek input-compact color-select" onchange="updateCustomColorPicker(\'' . $calId . '\')">'; 15591d05cddcSAtari911 $html .= '<option value="#3498db" style="background:#3498db;color:white"> Blue</option>'; 15601d05cddcSAtari911 $html .= '<option value="#2ecc71" style="background:#2ecc71;color:white"> Green</option>'; 15611d05cddcSAtari911 $html .= '<option value="#e74c3c" style="background:#e74c3c;color:white"> Red</option>'; 15621d05cddcSAtari911 $html .= '<option value="#f39c12" style="background:#f39c12;color:white"> Orange</option>'; 15631d05cddcSAtari911 $html .= '<option value="#9b59b6" style="background:#9b59b6;color:white"> Purple</option>'; 15641d05cddcSAtari911 $html .= '<option value="#e91e63" style="background:#e91e63;color:white"> Pink</option>'; 15651d05cddcSAtari911 $html .= '<option value="#1abc9c" style="background:#1abc9c;color:white"> Teal</option>'; 15661d05cddcSAtari911 $html .= '<option value="custom"> Custom...</option>'; 15671d05cddcSAtari911 $html .= '</select>'; 15681d05cddcSAtari911 $html .= '<input type="color" id="event-color-custom-' . $calId . '" class="color-picker-input color-picker-compact" value="#3498db" onchange="updateColorFromPicker(\'' . $calId . '\')">'; 15691d05cddcSAtari911 $html .= '</div>'; 157019378907SAtari911 $html .= '</div>'; 157119378907SAtari911 15721d05cddcSAtari911 $html .= '</div>'; // End row 15731d05cddcSAtari911 15741d05cddcSAtari911 // Task checkbox 15751d05cddcSAtari911 $html .= '<div class="form-field form-field-checkbox form-field-checkbox-compact">'; 15761d05cddcSAtari911 $html .= '<label class="checkbox-label checkbox-label-compact">'; 15771d05cddcSAtari911 $html .= '<input type="checkbox" id="event-is-task-' . $calId . '" name="isTask" class="task-toggle">'; 15781d05cddcSAtari911 $html .= '<span> This is a task (can be checked off)</span>'; 15791d05cddcSAtari911 $html .= '</label>'; 158019378907SAtari911 $html .= '</div>'; 158119378907SAtari911 158219378907SAtari911 // Action buttons 158319378907SAtari911 $html .= '<div class="dialog-actions-sleek">'; 158419378907SAtari911 $html .= '<button type="button" class="btn-sleek btn-cancel-sleek" onclick="closeEventDialog(\'' . $calId . '\')">Cancel</button>'; 158519378907SAtari911 $html .= '<button type="submit" class="btn-sleek btn-save-sleek"> Save</button>'; 158619378907SAtari911 $html .= '</div>'; 158719378907SAtari911 158819378907SAtari911 $html .= '</form>'; 158919378907SAtari911 $html .= '</div>'; 159019378907SAtari911 $html .= '</div>'; 159119378907SAtari911 159219378907SAtari911 return $html; 159319378907SAtari911 } 159419378907SAtari911 159587ac9bf3SAtari911 private function renderMonthPicker($calId, $year, $month, $namespace) { 159687ac9bf3SAtari911 $html = '<div class="month-picker-overlay" id="month-picker-overlay-' . $calId . '" style="display:none;" onclick="closeMonthPicker(\'' . $calId . '\')">'; 159787ac9bf3SAtari911 $html .= '<div class="month-picker-dialog" onclick="event.stopPropagation();">'; 159887ac9bf3SAtari911 $html .= '<h4>Jump to Month</h4>'; 159987ac9bf3SAtari911 160087ac9bf3SAtari911 $html .= '<div class="month-picker-selects">'; 160187ac9bf3SAtari911 $html .= '<select id="month-picker-month-' . $calId . '" class="month-picker-select">'; 160287ac9bf3SAtari911 $monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; 160387ac9bf3SAtari911 for ($m = 1; $m <= 12; $m++) { 160487ac9bf3SAtari911 $selected = ($m == $month) ? ' selected' : ''; 160587ac9bf3SAtari911 $html .= '<option value="' . $m . '"' . $selected . '>' . $monthNames[$m - 1] . '</option>'; 160687ac9bf3SAtari911 } 160787ac9bf3SAtari911 $html .= '</select>'; 160887ac9bf3SAtari911 160987ac9bf3SAtari911 $html .= '<select id="month-picker-year-' . $calId . '" class="month-picker-select">'; 161087ac9bf3SAtari911 $currentYear = (int)date('Y'); 161187ac9bf3SAtari911 for ($y = $currentYear - 5; $y <= $currentYear + 5; $y++) { 161287ac9bf3SAtari911 $selected = ($y == $year) ? ' selected' : ''; 161387ac9bf3SAtari911 $html .= '<option value="' . $y . '"' . $selected . '>' . $y . '</option>'; 161487ac9bf3SAtari911 } 161587ac9bf3SAtari911 $html .= '</select>'; 161687ac9bf3SAtari911 $html .= '</div>'; 161787ac9bf3SAtari911 161887ac9bf3SAtari911 $html .= '<div class="month-picker-actions">'; 161987ac9bf3SAtari911 $html .= '<button class="btn-sleek btn-cancel-sleek" onclick="closeMonthPicker(\'' . $calId . '\')">Cancel</button>'; 162087ac9bf3SAtari911 $html .= '<button class="btn-sleek btn-save-sleek" onclick="jumpToSelectedMonth(\'' . $calId . '\', \'' . $namespace . '\')">Go</button>'; 162187ac9bf3SAtari911 $html .= '</div>'; 162287ac9bf3SAtari911 162387ac9bf3SAtari911 $html .= '</div>'; 162487ac9bf3SAtari911 $html .= '</div>'; 162587ac9bf3SAtari911 162687ac9bf3SAtari911 return $html; 162787ac9bf3SAtari911 } 162887ac9bf3SAtari911 162919378907SAtari911 private function renderDescription($description) { 163019378907SAtari911 if (empty($description)) { 163119378907SAtari911 return ''; 163219378907SAtari911 } 163319378907SAtari911 1634e3a9f44cSAtari911 // Token-based parsing to avoid escaping issues 1635e3a9f44cSAtari911 $rendered = $description; 1636e3a9f44cSAtari911 $tokens = array(); 1637e3a9f44cSAtari911 $tokenIndex = 0; 163819378907SAtari911 1639e3a9f44cSAtari911 // Convert DokuWiki image syntax {{image.jpg}} to tokens 1640e3a9f44cSAtari911 $pattern = '/\{\{([^}|]+?)(?:\|([^}]+))?\}\}/'; 1641e3a9f44cSAtari911 preg_match_all($pattern, $rendered, $matches, PREG_SET_ORDER); 1642e3a9f44cSAtari911 foreach ($matches as $match) { 1643e3a9f44cSAtari911 $imagePath = trim($match[1]); 1644e3a9f44cSAtari911 $alt = isset($match[2]) ? trim($match[2]) : ''; 164519378907SAtari911 1646e3a9f44cSAtari911 // Handle external URLs 164719378907SAtari911 if (preg_match('/^https?:\/\//', $imagePath)) { 1648e3a9f44cSAtari911 $imageHtml = '<img src="' . htmlspecialchars($imagePath) . '" alt="' . htmlspecialchars($alt) . '" class="event-image" />'; 1649e3a9f44cSAtari911 } else { 165019378907SAtari911 // Handle internal DokuWiki images 165119378907SAtari911 $imageUrl = DOKU_BASE . 'lib/exe/fetch.php?media=' . rawurlencode($imagePath); 1652e3a9f44cSAtari911 $imageHtml = '<img src="' . $imageUrl . '" alt="' . htmlspecialchars($alt) . '" class="event-image" />'; 1653e3a9f44cSAtari911 } 165419378907SAtari911 1655e3a9f44cSAtari911 $token = "\x00TOKEN" . $tokenIndex . "\x00"; 1656e3a9f44cSAtari911 $tokens[$tokenIndex] = $imageHtml; 1657e3a9f44cSAtari911 $tokenIndex++; 1658e3a9f44cSAtari911 $rendered = str_replace($match[0], $token, $rendered); 1659e3a9f44cSAtari911 } 1660e3a9f44cSAtari911 1661e3a9f44cSAtari911 // Convert DokuWiki link syntax [[link|text]] to tokens 1662e3a9f44cSAtari911 $pattern = '/\[\[([^|\]]+?)(?:\|([^\]]+))?\]\]/'; 1663e3a9f44cSAtari911 preg_match_all($pattern, $rendered, $matches, PREG_SET_ORDER); 1664e3a9f44cSAtari911 foreach ($matches as $match) { 1665e3a9f44cSAtari911 $link = trim($match[1]); 1666e3a9f44cSAtari911 $text = isset($match[2]) ? trim($match[2]) : $link; 166719378907SAtari911 166819378907SAtari911 // Handle external URLs 166919378907SAtari911 if (preg_match('/^https?:\/\//', $link)) { 1670e3a9f44cSAtari911 $linkHtml = '<a href="' . htmlspecialchars($link) . '" target="_blank" rel="noopener noreferrer">' . htmlspecialchars($text) . '</a>'; 1671e3a9f44cSAtari911 } else { 167287ac9bf3SAtari911 // Handle internal DokuWiki links with section anchors 167387ac9bf3SAtari911 $parts = explode('#', $link, 2); 167487ac9bf3SAtari911 $pagePart = $parts[0]; 167587ac9bf3SAtari911 $sectionPart = isset($parts[1]) ? '#' . $parts[1] : ''; 167687ac9bf3SAtari911 167787ac9bf3SAtari911 $wikiUrl = DOKU_BASE . 'doku.php?id=' . rawurlencode($pagePart) . $sectionPart; 1678e3a9f44cSAtari911 $linkHtml = '<a href="' . $wikiUrl . '">' . htmlspecialchars($text) . '</a>'; 167919378907SAtari911 } 168019378907SAtari911 1681e3a9f44cSAtari911 $token = "\x00TOKEN" . $tokenIndex . "\x00"; 1682e3a9f44cSAtari911 $tokens[$tokenIndex] = $linkHtml; 1683e3a9f44cSAtari911 $tokenIndex++; 1684e3a9f44cSAtari911 $rendered = str_replace($match[0], $token, $rendered); 1685e3a9f44cSAtari911 } 168619378907SAtari911 1687e3a9f44cSAtari911 // Convert markdown-style links [text](url) to tokens 1688e3a9f44cSAtari911 $pattern = '/\[([^\]]+)\]\(([^)]+)\)/'; 1689e3a9f44cSAtari911 preg_match_all($pattern, $rendered, $matches, PREG_SET_ORDER); 1690e3a9f44cSAtari911 foreach ($matches as $match) { 1691e3a9f44cSAtari911 $text = trim($match[1]); 1692e3a9f44cSAtari911 $url = trim($match[2]); 169319378907SAtari911 1694e3a9f44cSAtari911 if (preg_match('/^https?:\/\//', $url)) { 1695e3a9f44cSAtari911 $linkHtml = '<a href="' . htmlspecialchars($url) . '" target="_blank" rel="noopener noreferrer">' . htmlspecialchars($text) . '</a>'; 1696e3a9f44cSAtari911 } else { 1697e3a9f44cSAtari911 $linkHtml = '<a href="' . htmlspecialchars($url) . '">' . htmlspecialchars($text) . '</a>'; 1698e3a9f44cSAtari911 } 1699e3a9f44cSAtari911 1700e3a9f44cSAtari911 $token = "\x00TOKEN" . $tokenIndex . "\x00"; 1701e3a9f44cSAtari911 $tokens[$tokenIndex] = $linkHtml; 1702e3a9f44cSAtari911 $tokenIndex++; 1703e3a9f44cSAtari911 $rendered = str_replace($match[0], $token, $rendered); 1704e3a9f44cSAtari911 } 1705e3a9f44cSAtari911 1706e3a9f44cSAtari911 // Convert plain URLs to tokens 1707e3a9f44cSAtari911 $pattern = '/(https?:\/\/[^\s<]+)/'; 1708e3a9f44cSAtari911 preg_match_all($pattern, $rendered, $matches, PREG_SET_ORDER); 1709e3a9f44cSAtari911 foreach ($matches as $match) { 1710e3a9f44cSAtari911 $url = $match[1]; 1711e3a9f44cSAtari911 $linkHtml = '<a href="' . htmlspecialchars($url) . '" target="_blank" rel="noopener noreferrer">' . htmlspecialchars($url) . '</a>'; 1712e3a9f44cSAtari911 1713e3a9f44cSAtari911 $token = "\x00TOKEN" . $tokenIndex . "\x00"; 1714e3a9f44cSAtari911 $tokens[$tokenIndex] = $linkHtml; 1715e3a9f44cSAtari911 $tokenIndex++; 1716e3a9f44cSAtari911 $rendered = str_replace($match[0], $token, $rendered); 1717e3a9f44cSAtari911 } 1718e3a9f44cSAtari911 1719e3a9f44cSAtari911 // NOW escape HTML (tokens are protected) 1720e3a9f44cSAtari911 $rendered = htmlspecialchars($rendered); 1721e3a9f44cSAtari911 1722e3a9f44cSAtari911 // Convert newlines to <br> 1723e3a9f44cSAtari911 $rendered = nl2br($rendered); 1724e3a9f44cSAtari911 1725e3a9f44cSAtari911 // DokuWiki text formatting 1726e3a9f44cSAtari911 // Bold: **text** or __text__ 1727e3a9f44cSAtari911 $rendered = preg_replace('/\*\*(.+?)\*\*/', '<strong>$1</strong>', $rendered); 1728e3a9f44cSAtari911 $rendered = preg_replace('/__(.+?)__/', '<strong>$1</strong>', $rendered); 1729e3a9f44cSAtari911 1730e3a9f44cSAtari911 // Italic: //text// 1731e3a9f44cSAtari911 $rendered = preg_replace('/\/\/(.+?)\/\//', '<em>$1</em>', $rendered); 1732e3a9f44cSAtari911 1733e3a9f44cSAtari911 // Strikethrough: <del>text</del> 1734e3a9f44cSAtari911 $rendered = preg_replace('/<del>(.+?)<\/del>/', '<del>$1</del>', $rendered); 1735e3a9f44cSAtari911 1736e3a9f44cSAtari911 // Monospace: ''text'' 1737e3a9f44cSAtari911 $rendered = preg_replace('/''(.+?)''/', '<code>$1</code>', $rendered); 1738e3a9f44cSAtari911 1739e3a9f44cSAtari911 // Subscript: <sub>text</sub> 1740e3a9f44cSAtari911 $rendered = preg_replace('/<sub>(.+?)<\/sub>/', '<sub>$1</sub>', $rendered); 1741e3a9f44cSAtari911 1742e3a9f44cSAtari911 // Superscript: <sup>text</sup> 1743e3a9f44cSAtari911 $rendered = preg_replace('/<sup>(.+?)<\/sup>/', '<sup>$1</sup>', $rendered); 1744e3a9f44cSAtari911 1745e3a9f44cSAtari911 // Restore tokens 1746e3a9f44cSAtari911 foreach ($tokens as $i => $html) { 1747e3a9f44cSAtari911 $rendered = str_replace("\x00TOKEN" . $i . "\x00", $html, $rendered); 1748e3a9f44cSAtari911 } 174919378907SAtari911 175019378907SAtari911 return $rendered; 175119378907SAtari911 } 175219378907SAtari911 175319378907SAtari911 private function loadEvents($namespace, $year, $month) { 175419378907SAtari911 $dataDir = DOKU_INC . 'data/meta/'; 175519378907SAtari911 if ($namespace) { 175619378907SAtari911 $dataDir .= str_replace(':', '/', $namespace) . '/'; 175719378907SAtari911 } 175819378907SAtari911 $dataDir .= 'calendar/'; 175919378907SAtari911 176019378907SAtari911 $eventFile = $dataDir . sprintf('%04d-%02d.json', $year, $month); 176119378907SAtari911 176219378907SAtari911 if (file_exists($eventFile)) { 176319378907SAtari911 $json = file_get_contents($eventFile); 176419378907SAtari911 return json_decode($json, true); 176519378907SAtari911 } 176619378907SAtari911 176719378907SAtari911 return array(); 176819378907SAtari911 } 1769e3a9f44cSAtari911 1770e3a9f44cSAtari911 private function loadEventsMultiNamespace($namespaces, $year, $month) { 1771e3a9f44cSAtari911 // Check for wildcard pattern (namespace:*) 1772e3a9f44cSAtari911 if (preg_match('/^(.+):\*$/', $namespaces, $matches)) { 1773e3a9f44cSAtari911 $baseNamespace = $matches[1]; 1774e3a9f44cSAtari911 return $this->loadEventsWildcard($baseNamespace, $year, $month); 1775e3a9f44cSAtari911 } 1776e3a9f44cSAtari911 1777e3a9f44cSAtari911 // Check for root wildcard (just *) 1778e3a9f44cSAtari911 if ($namespaces === '*') { 1779e3a9f44cSAtari911 return $this->loadEventsWildcard('', $year, $month); 1780e3a9f44cSAtari911 } 1781e3a9f44cSAtari911 1782e3a9f44cSAtari911 // Parse namespace list (semicolon separated) 1783e3a9f44cSAtari911 // e.g., "team:projects;personal;work:tasks" = three namespaces 1784e3a9f44cSAtari911 $namespaceList = array_map('trim', explode(';', $namespaces)); 1785e3a9f44cSAtari911 1786e3a9f44cSAtari911 // Load events from all namespaces 1787e3a9f44cSAtari911 $allEvents = array(); 1788e3a9f44cSAtari911 foreach ($namespaceList as $ns) { 1789e3a9f44cSAtari911 $ns = trim($ns); 1790e3a9f44cSAtari911 if (empty($ns)) continue; 1791e3a9f44cSAtari911 1792e3a9f44cSAtari911 $events = $this->loadEvents($ns, $year, $month); 1793e3a9f44cSAtari911 1794e3a9f44cSAtari911 // Add namespace tag to each event 1795e3a9f44cSAtari911 foreach ($events as $dateKey => $dayEvents) { 1796e3a9f44cSAtari911 if (!isset($allEvents[$dateKey])) { 1797e3a9f44cSAtari911 $allEvents[$dateKey] = array(); 1798e3a9f44cSAtari911 } 1799e3a9f44cSAtari911 foreach ($dayEvents as $event) { 1800e3a9f44cSAtari911 $event['_namespace'] = $ns; 1801e3a9f44cSAtari911 $allEvents[$dateKey][] = $event; 1802e3a9f44cSAtari911 } 1803e3a9f44cSAtari911 } 1804e3a9f44cSAtari911 } 1805e3a9f44cSAtari911 1806e3a9f44cSAtari911 return $allEvents; 1807e3a9f44cSAtari911 } 1808e3a9f44cSAtari911 1809e3a9f44cSAtari911 private function loadEventsWildcard($baseNamespace, $year, $month) { 1810e3a9f44cSAtari911 // Find all subdirectories under the base namespace 1811e3a9f44cSAtari911 $dataDir = DOKU_INC . 'data/meta/'; 1812e3a9f44cSAtari911 if ($baseNamespace) { 1813e3a9f44cSAtari911 $dataDir .= str_replace(':', '/', $baseNamespace) . '/'; 1814e3a9f44cSAtari911 } 1815e3a9f44cSAtari911 1816e3a9f44cSAtari911 $allEvents = array(); 1817e3a9f44cSAtari911 1818e3a9f44cSAtari911 // First, load events from the base namespace itself 1819e3a9f44cSAtari911 if (empty($baseNamespace)) { 1820e3a9f44cSAtari911 // Root wildcard - load from root calendar 1821e3a9f44cSAtari911 $events = $this->loadEvents('', $year, $month); 1822e3a9f44cSAtari911 foreach ($events as $dateKey => $dayEvents) { 1823e3a9f44cSAtari911 if (!isset($allEvents[$dateKey])) { 1824e3a9f44cSAtari911 $allEvents[$dateKey] = array(); 1825e3a9f44cSAtari911 } 1826e3a9f44cSAtari911 foreach ($dayEvents as $event) { 1827e3a9f44cSAtari911 $event['_namespace'] = ''; 1828e3a9f44cSAtari911 $allEvents[$dateKey][] = $event; 1829e3a9f44cSAtari911 } 1830e3a9f44cSAtari911 } 1831e3a9f44cSAtari911 } else { 1832e3a9f44cSAtari911 $events = $this->loadEvents($baseNamespace, $year, $month); 1833e3a9f44cSAtari911 foreach ($events as $dateKey => $dayEvents) { 1834e3a9f44cSAtari911 if (!isset($allEvents[$dateKey])) { 1835e3a9f44cSAtari911 $allEvents[$dateKey] = array(); 1836e3a9f44cSAtari911 } 1837e3a9f44cSAtari911 foreach ($dayEvents as $event) { 1838e3a9f44cSAtari911 $event['_namespace'] = $baseNamespace; 1839e3a9f44cSAtari911 $allEvents[$dateKey][] = $event; 1840e3a9f44cSAtari911 } 1841e3a9f44cSAtari911 } 1842e3a9f44cSAtari911 } 1843e3a9f44cSAtari911 1844e3a9f44cSAtari911 // Recursively find all subdirectories 1845e3a9f44cSAtari911 $this->findSubNamespaces($dataDir, $baseNamespace, $year, $month, $allEvents); 1846e3a9f44cSAtari911 1847e3a9f44cSAtari911 return $allEvents; 1848e3a9f44cSAtari911 } 1849e3a9f44cSAtari911 1850e3a9f44cSAtari911 private function findSubNamespaces($dir, $baseNamespace, $year, $month, &$allEvents) { 1851e3a9f44cSAtari911 if (!is_dir($dir)) return; 1852e3a9f44cSAtari911 1853e3a9f44cSAtari911 $items = scandir($dir); 1854e3a9f44cSAtari911 foreach ($items as $item) { 1855e3a9f44cSAtari911 if ($item === '.' || $item === '..') continue; 1856e3a9f44cSAtari911 1857e3a9f44cSAtari911 $path = $dir . $item; 1858e3a9f44cSAtari911 if (is_dir($path) && $item !== 'calendar') { 1859e3a9f44cSAtari911 // This is a namespace directory 1860e3a9f44cSAtari911 $namespace = $baseNamespace ? $baseNamespace . ':' . $item : $item; 1861e3a9f44cSAtari911 1862e3a9f44cSAtari911 // Load events from this namespace 1863e3a9f44cSAtari911 $events = $this->loadEvents($namespace, $year, $month); 1864e3a9f44cSAtari911 foreach ($events as $dateKey => $dayEvents) { 1865e3a9f44cSAtari911 if (!isset($allEvents[$dateKey])) { 1866e3a9f44cSAtari911 $allEvents[$dateKey] = array(); 1867e3a9f44cSAtari911 } 1868e3a9f44cSAtari911 foreach ($dayEvents as $event) { 1869e3a9f44cSAtari911 $event['_namespace'] = $namespace; 1870e3a9f44cSAtari911 $allEvents[$dateKey][] = $event; 1871e3a9f44cSAtari911 } 1872e3a9f44cSAtari911 } 1873e3a9f44cSAtari911 1874e3a9f44cSAtari911 // Recurse into subdirectories 1875e3a9f44cSAtari911 $this->findSubNamespaces($path . '/', $namespace, $year, $month, $allEvents); 1876e3a9f44cSAtari911 } 1877e3a9f44cSAtari911 } 1878e3a9f44cSAtari911 } 18791d05cddcSAtari911 18801d05cddcSAtari911 private function getAllNamespaces() { 18811d05cddcSAtari911 $dataDir = DOKU_INC . 'data/meta/'; 18821d05cddcSAtari911 $namespaces = []; 18831d05cddcSAtari911 18841d05cddcSAtari911 // Scan for namespaces that have calendar data 18851d05cddcSAtari911 $this->scanForCalendarNamespaces($dataDir, '', $namespaces); 18861d05cddcSAtari911 18871d05cddcSAtari911 // Sort alphabetically 18881d05cddcSAtari911 sort($namespaces); 18891d05cddcSAtari911 18901d05cddcSAtari911 return $namespaces; 18911d05cddcSAtari911 } 18921d05cddcSAtari911 18931d05cddcSAtari911 private function scanForCalendarNamespaces($dir, $baseNamespace, &$namespaces) { 18941d05cddcSAtari911 if (!is_dir($dir)) return; 18951d05cddcSAtari911 18961d05cddcSAtari911 $items = scandir($dir); 18971d05cddcSAtari911 foreach ($items as $item) { 18981d05cddcSAtari911 if ($item === '.' || $item === '..') continue; 18991d05cddcSAtari911 19001d05cddcSAtari911 $path = $dir . $item; 19011d05cddcSAtari911 if (is_dir($path)) { 19021d05cddcSAtari911 // Check if this directory has a calendar subdirectory with data 19031d05cddcSAtari911 $calendarDir = $path . '/calendar/'; 19041d05cddcSAtari911 if (is_dir($calendarDir)) { 19051d05cddcSAtari911 // Check if there are any JSON files in the calendar directory 19061d05cddcSAtari911 $jsonFiles = glob($calendarDir . '*.json'); 19071d05cddcSAtari911 if (!empty($jsonFiles)) { 19081d05cddcSAtari911 // This namespace has calendar data 19091d05cddcSAtari911 $namespace = $baseNamespace ? $baseNamespace . ':' . $item : $item; 19101d05cddcSAtari911 $namespaces[] = $namespace; 19111d05cddcSAtari911 } 19121d05cddcSAtari911 } 19131d05cddcSAtari911 19141d05cddcSAtari911 // Recurse into subdirectories 19151d05cddcSAtari911 $namespace = $baseNamespace ? $baseNamespace . ':' . $item : $item; 19161d05cddcSAtari911 $this->scanForCalendarNamespaces($path . '/', $namespace, $namespaces); 19171d05cddcSAtari911 } 19181d05cddcSAtari911 } 19191d05cddcSAtari911 } 19201d05cddcSAtari911 19211d05cddcSAtari911 /** 19221d05cddcSAtari911 * Render new sidebar widget - Week at a glance itinerary (200px wide) 19231d05cddcSAtari911 */ 19241d05cddcSAtari911 private function renderSidebarWidget($events, $namespace, $calId) { 19251d05cddcSAtari911 if (empty($events)) { 19261d05cddcSAtari911 return '<div style="width:200px; padding:12px; text-align:center; color:#999; font-size:11px;">No events this week</div>'; 19271d05cddcSAtari911 } 19281d05cddcSAtari911 19291d05cddcSAtari911 // Get important namespaces from config 19301d05cddcSAtari911 $configFile = DOKU_PLUGIN . 'calendar/sync_config.php'; 19311d05cddcSAtari911 $importantNsList = ['important']; // default 19321d05cddcSAtari911 if (file_exists($configFile)) { 19331d05cddcSAtari911 $config = include $configFile; 19341d05cddcSAtari911 if (isset($config['important_namespaces']) && !empty($config['important_namespaces'])) { 19351d05cddcSAtari911 $importantNsList = array_map('trim', explode(',', $config['important_namespaces'])); 19361d05cddcSAtari911 } 19371d05cddcSAtari911 } 19381d05cddcSAtari911 19391d05cddcSAtari911 // Calculate date ranges 19401d05cddcSAtari911 $todayStr = date('Y-m-d'); 19411d05cddcSAtari911 $tomorrowStr = date('Y-m-d', strtotime('+1 day')); 19421d05cddcSAtari911 $weekStart = date('Y-m-d', strtotime('monday this week')); 19431d05cddcSAtari911 $weekEnd = date('Y-m-d', strtotime('sunday this week')); 19441d05cddcSAtari911 19451d05cddcSAtari911 // Group events by category 19461d05cddcSAtari911 $todayEvents = []; 19471d05cddcSAtari911 $tomorrowEvents = []; 19481d05cddcSAtari911 $importantEvents = []; 19491d05cddcSAtari911 $weekEvents = []; // For week grid 19501d05cddcSAtari911 19511d05cddcSAtari911 // Process all events 19521d05cddcSAtari911 foreach ($events as $dateKey => $dayEvents) { 19531d05cddcSAtari911 // Skip events before this week 19541d05cddcSAtari911 if ($dateKey < $weekStart) continue; 19551d05cddcSAtari911 19561d05cddcSAtari911 // Initialize week grid day if in current week 19571d05cddcSAtari911 if ($dateKey >= $weekStart && $dateKey <= $weekEnd) { 19581d05cddcSAtari911 if (!isset($weekEvents[$dateKey])) { 19591d05cddcSAtari911 $weekEvents[$dateKey] = []; 19601d05cddcSAtari911 } 19611d05cddcSAtari911 } 19621d05cddcSAtari911 19631d05cddcSAtari911 foreach ($dayEvents as $event) { 19641d05cddcSAtari911 // Add to week grid if in week range 19651d05cddcSAtari911 if ($dateKey >= $weekStart && $dateKey <= $weekEnd) { 19661d05cddcSAtari911 // Pre-render DokuWiki syntax to HTML for JavaScript display 19671d05cddcSAtari911 $eventWithHtml = $event; 19681d05cddcSAtari911 if (isset($event['title'])) { 19691d05cddcSAtari911 $eventWithHtml['title_html'] = $this->renderDokuWikiToHtml($event['title']); 19701d05cddcSAtari911 } 19711d05cddcSAtari911 if (isset($event['description'])) { 19721d05cddcSAtari911 $eventWithHtml['description_html'] = $this->renderDokuWikiToHtml($event['description']); 19731d05cddcSAtari911 } 19741d05cddcSAtari911 $weekEvents[$dateKey][] = $eventWithHtml; 19751d05cddcSAtari911 } 19761d05cddcSAtari911 19771d05cddcSAtari911 // Categorize for detailed sections 19781d05cddcSAtari911 if ($dateKey === $todayStr) { 19791d05cddcSAtari911 $todayEvents[] = array_merge($event, ['date' => $dateKey]); 19801d05cddcSAtari911 } elseif ($dateKey === $tomorrowStr) { 19811d05cddcSAtari911 $tomorrowEvents[] = array_merge($event, ['date' => $dateKey]); 19821d05cddcSAtari911 } else { 19831d05cddcSAtari911 // Check if this is an important namespace 19841d05cddcSAtari911 $eventNs = isset($event['namespace']) ? $event['namespace'] : ''; 19851d05cddcSAtari911 $isImportant = false; 19861d05cddcSAtari911 foreach ($importantNsList as $impNs) { 19871d05cddcSAtari911 if ($eventNs === $impNs || strpos($eventNs, $impNs . ':') === 0) { 19881d05cddcSAtari911 $isImportant = true; 19891d05cddcSAtari911 break; 19901d05cddcSAtari911 } 19911d05cddcSAtari911 } 19921d05cddcSAtari911 19931d05cddcSAtari911 // Important events: this week but not today/tomorrow 19941d05cddcSAtari911 if ($isImportant && $dateKey >= $weekStart && $dateKey <= $weekEnd) { 19951d05cddcSAtari911 $importantEvents[] = array_merge($event, ['date' => $dateKey]); 19961d05cddcSAtari911 } 19971d05cddcSAtari911 } 19981d05cddcSAtari911 } 19991d05cddcSAtari911 } 20001d05cddcSAtari911 20011d05cddcSAtari911 // Start building HTML - Dynamic width with default font 20021d05cddcSAtari911 $html = '<div class="sidebar-widget sidebar-matrix" style="width:100%; max-width:100%; box-sizing:border-box; font-family:system-ui, sans-serif; background:#242424; border:2px solid #00cc07; border-radius:4px; overflow:hidden; box-shadow:0 0 10px rgba(0, 204, 7, 0.3);">'; 20031d05cddcSAtari911 20041d05cddcSAtari911 // Sanitize calId for use in JavaScript variable names (remove dashes) 20051d05cddcSAtari911 $jsCalId = str_replace('-', '_', $calId); 20061d05cddcSAtari911 20071d05cddcSAtari911 // CRITICAL: Add ALL JavaScript FIRST before any HTML that uses it 20081d05cddcSAtari911 $html .= '<script> 20091d05cddcSAtari911(function() { 20101d05cddcSAtari911 // Shared state for system stats and tooltips 20111d05cddcSAtari911 const sharedState_' . $jsCalId . ' = { 20121d05cddcSAtari911 latestStats: { 20131d05cddcSAtari911 load: {"1min": 0, "5min": 0, "15min": 0}, 20141d05cddcSAtari911 uptime: "", 20151d05cddcSAtari911 memory_details: {}, 20161d05cddcSAtari911 top_processes: [] 20171d05cddcSAtari911 }, 20181d05cddcSAtari911 cpuHistory: [], 20191d05cddcSAtari911 CPU_HISTORY_SIZE: 2 20201d05cddcSAtari911 }; 20211d05cddcSAtari911 20221d05cddcSAtari911 // Tooltip functions - MUST be defined before HTML uses them 20231d05cddcSAtari911 window["showTooltip_' . $jsCalId . '"] = function(color) { 20241d05cddcSAtari911 const tooltip = document.getElementById("tooltip-" + color + "-' . $calId . '"); 20251d05cddcSAtari911 if (!tooltip) { 20261d05cddcSAtari911 console.log("Tooltip element not found for color:", color); 20271d05cddcSAtari911 return; 20281d05cddcSAtari911 } 20291d05cddcSAtari911 20301d05cddcSAtari911 const latestStats = sharedState_' . $jsCalId . '.latestStats; 20311d05cddcSAtari911 let content = ""; 20321d05cddcSAtari911 20331d05cddcSAtari911 if (color === "green") { 20341d05cddcSAtari911 content = "<div class=\\"tooltip-title\\">CPU Load Average</div>"; 20351d05cddcSAtari911 content += "<div>1 min: " + (latestStats.load["1min"] || "N/A") + "</div>"; 20361d05cddcSAtari911 content += "<div>5 min: " + (latestStats.load["5min"] || "N/A") + "</div>"; 20371d05cddcSAtari911 content += "<div>15 min: " + (latestStats.load["15min"] || "N/A") + "</div>"; 20381d05cddcSAtari911 if (latestStats.uptime) { 20391d05cddcSAtari911 content += "<div style=\\"margin-top:3px; padding-top:2px; border-top:1px solid rgba(0,204,7,0.3);\\">Uptime: " + latestStats.uptime + "</div>"; 20401d05cddcSAtari911 } 20411d05cddcSAtari911 tooltip.style.borderColor = "#00cc07"; 20421d05cddcSAtari911 tooltip.style.color = "#00cc07"; 20431d05cddcSAtari911 } else if (color === "purple") { 20441d05cddcSAtari911 content = "<div class=\\"tooltip-title\\">CPU Load (Short-term)</div>"; 20451d05cddcSAtari911 content += "<div>1 min: " + (latestStats.load["1min"] || "N/A") + "</div>"; 20461d05cddcSAtari911 content += "<div>5 min: " + (latestStats.load["5min"] || "N/A") + "</div>"; 20471d05cddcSAtari911 if (latestStats.top_processes && latestStats.top_processes.length > 0) { 20481d05cddcSAtari911 content += "<div style=\\"margin-top:3px; padding-top:2px; border-top:1px solid rgba(155,89,182,0.3);\\" class=\\"tooltip-title\\">Top Processes</div>"; 20491d05cddcSAtari911 latestStats.top_processes.slice(0, 5).forEach(proc => { 20501d05cddcSAtari911 content += "<div>" + proc.cpu + " " + proc.command + "</div>"; 20511d05cddcSAtari911 }); 20521d05cddcSAtari911 } 20531d05cddcSAtari911 tooltip.style.borderColor = "#9b59b6"; 20541d05cddcSAtari911 tooltip.style.color = "#9b59b6"; 20551d05cddcSAtari911 } else if (color === "orange") { 20561d05cddcSAtari911 content = "<div class=\\"tooltip-title\\">Memory Usage</div>"; 20571d05cddcSAtari911 if (latestStats.memory_details && latestStats.memory_details.total) { 20581d05cddcSAtari911 content += "<div>Total: " + latestStats.memory_details.total + "</div>"; 20591d05cddcSAtari911 content += "<div>Used: " + latestStats.memory_details.used + "</div>"; 20601d05cddcSAtari911 content += "<div>Available: " + latestStats.memory_details.available + "</div>"; 20611d05cddcSAtari911 if (latestStats.memory_details.cached) { 20621d05cddcSAtari911 content += "<div>Cached: " + latestStats.memory_details.cached + "</div>"; 20631d05cddcSAtari911 } 20641d05cddcSAtari911 } else { 20651d05cddcSAtari911 content += "<div>Loading...</div>"; 20661d05cddcSAtari911 } 20671d05cddcSAtari911 if (latestStats.top_processes && latestStats.top_processes.length > 0) { 20681d05cddcSAtari911 content += "<div style=\\"margin-top:3px; padding-top:2px; border-top:1px solid rgba(255,140,0,0.3);\\" class=\\"tooltip-title\\">Top Processes</div>"; 20691d05cddcSAtari911 latestStats.top_processes.slice(0, 5).forEach(proc => { 20701d05cddcSAtari911 content += "<div>" + proc.cpu + " " + proc.command + "</div>"; 20711d05cddcSAtari911 }); 20721d05cddcSAtari911 } 20731d05cddcSAtari911 tooltip.style.borderColor = "#ff9800"; 20741d05cddcSAtari911 tooltip.style.color = "#ff9800"; 20751d05cddcSAtari911 } 20761d05cddcSAtari911 20771d05cddcSAtari911 tooltip.innerHTML = content; 20781d05cddcSAtari911 tooltip.style.display = "block"; 20791d05cddcSAtari911 20801d05cddcSAtari911 const bar = tooltip.parentElement; 20811d05cddcSAtari911 const barRect = bar.getBoundingClientRect(); 20821d05cddcSAtari911 const tooltipRect = tooltip.getBoundingClientRect(); 20831d05cddcSAtari911 20841d05cddcSAtari911 const left = barRect.left + (barRect.width / 2) - (tooltipRect.width / 2); 20851d05cddcSAtari911 const top = barRect.top - tooltipRect.height - 8; 20861d05cddcSAtari911 20871d05cddcSAtari911 tooltip.style.left = left + "px"; 20881d05cddcSAtari911 tooltip.style.top = top + "px"; 20891d05cddcSAtari911 }; 20901d05cddcSAtari911 20911d05cddcSAtari911 window["hideTooltip_' . $jsCalId . '"] = function(color) { 20921d05cddcSAtari911 const tooltip = document.getElementById("tooltip-" + color + "-' . $calId . '"); 20931d05cddcSAtari911 if (tooltip) { 20941d05cddcSAtari911 tooltip.style.display = "none"; 20951d05cddcSAtari911 } 20961d05cddcSAtari911 }; 20971d05cddcSAtari911 20981d05cddcSAtari911 // Update clock every second 20991d05cddcSAtari911 function updateClock() { 21001d05cddcSAtari911 const now = new Date(); 21011d05cddcSAtari911 let hours = now.getHours(); 21021d05cddcSAtari911 const minutes = String(now.getMinutes()).padStart(2, "0"); 21031d05cddcSAtari911 const seconds = String(now.getSeconds()).padStart(2, "0"); 21041d05cddcSAtari911 const ampm = hours >= 12 ? "PM" : "AM"; 21051d05cddcSAtari911 hours = hours % 12 || 12; 21061d05cddcSAtari911 const timeStr = hours + ":" + minutes + ":" + seconds + " " + ampm; 21071d05cddcSAtari911 const clockEl = document.getElementById("clock-' . $calId . '"); 21081d05cddcSAtari911 if (clockEl) clockEl.textContent = timeStr; 21091d05cddcSAtari911 } 21101d05cddcSAtari911 setInterval(updateClock, 1000); 21111d05cddcSAtari911 21121d05cddcSAtari911 // Weather update function 21131d05cddcSAtari911 function updateWeather() { 21141d05cddcSAtari911 if ("geolocation" in navigator) { 21151d05cddcSAtari911 navigator.geolocation.getCurrentPosition(function(position) { 21161d05cddcSAtari911 const lat = position.coords.latitude; 21171d05cddcSAtari911 const lon = position.coords.longitude; 21181d05cddcSAtari911 21191d05cddcSAtari911 fetch(`https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}¤t_weather=true&temperature_unit=fahrenheit`) 21201d05cddcSAtari911 .then(response => response.json()) 21211d05cddcSAtari911 .then(data => { 21221d05cddcSAtari911 if (data.current_weather) { 21231d05cddcSAtari911 const temp = Math.round(data.current_weather.temperature); 21241d05cddcSAtari911 const weatherCode = data.current_weather.weathercode; 21251d05cddcSAtari911 const icon = getWeatherIcon(weatherCode); 21261d05cddcSAtari911 const iconEl = document.getElementById("weather-icon-' . $calId . '"); 21271d05cddcSAtari911 const tempEl = document.getElementById("weather-temp-' . $calId . '"); 21281d05cddcSAtari911 if (iconEl) iconEl.textContent = icon; 21291d05cddcSAtari911 if (tempEl) tempEl.innerHTML = temp + "°"; 21301d05cddcSAtari911 } 21311d05cddcSAtari911 }) 21321d05cddcSAtari911 .catch(error => console.log("Weather fetch error:", error)); 21331d05cddcSAtari911 }, function(error) { 21341d05cddcSAtari911 // If geolocation fails, use default location (Irvine, CA) 21351d05cddcSAtari911 fetch("https://api.open-meteo.com/v1/forecast?latitude=33.6846&longitude=-117.8265¤t_weather=true&temperature_unit=fahrenheit") 21361d05cddcSAtari911 .then(response => response.json()) 21371d05cddcSAtari911 .then(data => { 21381d05cddcSAtari911 if (data.current_weather) { 21391d05cddcSAtari911 const temp = Math.round(data.current_weather.temperature); 21401d05cddcSAtari911 const weatherCode = data.current_weather.weathercode; 21411d05cddcSAtari911 const icon = getWeatherIcon(weatherCode); 21421d05cddcSAtari911 const iconEl = document.getElementById("weather-icon-' . $calId . '"); 21431d05cddcSAtari911 const tempEl = document.getElementById("weather-temp-' . $calId . '"); 21441d05cddcSAtari911 if (iconEl) iconEl.textContent = icon; 21451d05cddcSAtari911 if (tempEl) tempEl.innerHTML = temp + "°"; 21461d05cddcSAtari911 } 21471d05cddcSAtari911 }) 21481d05cddcSAtari911 .catch(err => console.log("Weather error:", err)); 21491d05cddcSAtari911 }); 21501d05cddcSAtari911 } else { 21511d05cddcSAtari911 // No geolocation, use default (Irvine, CA) 21521d05cddcSAtari911 fetch("https://api.open-meteo.com/v1/forecast?latitude=33.6846&longitude=-117.8265¤t_weather=true&temperature_unit=fahrenheit") 21531d05cddcSAtari911 .then(response => response.json()) 21541d05cddcSAtari911 .then(data => { 21551d05cddcSAtari911 if (data.current_weather) { 21561d05cddcSAtari911 const temp = Math.round(data.current_weather.temperature); 21571d05cddcSAtari911 const weatherCode = data.current_weather.weathercode; 21581d05cddcSAtari911 const icon = getWeatherIcon(weatherCode); 21591d05cddcSAtari911 const iconEl = document.getElementById("weather-icon-' . $calId . '"); 21601d05cddcSAtari911 const tempEl = document.getElementById("weather-temp-' . $calId . '"); 21611d05cddcSAtari911 if (iconEl) iconEl.textContent = icon; 21621d05cddcSAtari911 if (tempEl) tempEl.innerHTML = temp + "°"; 21631d05cddcSAtari911 } 21641d05cddcSAtari911 }) 21651d05cddcSAtari911 .catch(err => console.log("Weather error:", err)); 21661d05cddcSAtari911 } 21671d05cddcSAtari911 } 21681d05cddcSAtari911 21691d05cddcSAtari911 function getWeatherIcon(code) { 21701d05cddcSAtari911 const icons = { 21711d05cddcSAtari911 0: "☀️", 1: "️", 2: "⛅", 3: "☁️", 21721d05cddcSAtari911 45: "️", 48: "️", 51: "️", 53: "️", 55: "️", 21731d05cddcSAtari911 61: "️", 63: "️", 65: "⛈️", 71: "️", 73: "️", 21741d05cddcSAtari911 75: "❄️", 77: "️", 80: "️", 81: "️", 82: "⛈️", 21751d05cddcSAtari911 85: "️", 86: "❄️", 95: "⛈️", 96: "⛈️", 99: "⛈️" 21761d05cddcSAtari911 }; 21771d05cddcSAtari911 return icons[code] || "️"; 21781d05cddcSAtari911 } 21791d05cddcSAtari911 21801d05cddcSAtari911 // Update weather immediately and every 10 minutes 21811d05cddcSAtari911 updateWeather(); 21821d05cddcSAtari911 setInterval(updateWeather, 600000); 21831d05cddcSAtari911 21841d05cddcSAtari911 // Update system stats and tooltips data 21851d05cddcSAtari911 function updateSystemStats() { 21861d05cddcSAtari911 fetch("' . DOKU_BASE . 'lib/plugins/calendar/get_system_stats.php") 21871d05cddcSAtari911 .then(response => response.json()) 21881d05cddcSAtari911 .then(data => { 21891d05cddcSAtari911 sharedState_' . $jsCalId . '.latestStats = { 21901d05cddcSAtari911 load: data.load || {"1min": 0, "5min": 0, "15min": 0}, 21911d05cddcSAtari911 uptime: data.uptime || "", 21921d05cddcSAtari911 memory_details: data.memory_details || {}, 21931d05cddcSAtari911 top_processes: data.top_processes || [] 21941d05cddcSAtari911 }; 21951d05cddcSAtari911 21961d05cddcSAtari911 const greenBar = document.getElementById("cpu-5min-' . $calId . '"); 21971d05cddcSAtari911 if (greenBar) { 21981d05cddcSAtari911 greenBar.style.width = Math.min(100, data.cpu_5min) + "%"; 21991d05cddcSAtari911 } 22001d05cddcSAtari911 22011d05cddcSAtari911 sharedState_' . $jsCalId . '.cpuHistory.push(data.cpu); 22021d05cddcSAtari911 if (sharedState_' . $jsCalId . '.cpuHistory.length > sharedState_' . $jsCalId . '.CPU_HISTORY_SIZE) { 22031d05cddcSAtari911 sharedState_' . $jsCalId . '.cpuHistory.shift(); 22041d05cddcSAtari911 } 22051d05cddcSAtari911 22061d05cddcSAtari911 const cpuAverage = sharedState_' . $jsCalId . '.cpuHistory.reduce((sum, val) => sum + val, 0) / sharedState_' . $jsCalId . '.cpuHistory.length; 22071d05cddcSAtari911 22081d05cddcSAtari911 const cpuBar = document.getElementById("cpu-realtime-' . $calId . '"); 22091d05cddcSAtari911 if (cpuBar) { 22101d05cddcSAtari911 cpuBar.style.width = Math.min(100, cpuAverage) + "%"; 22111d05cddcSAtari911 } 22121d05cddcSAtari911 22131d05cddcSAtari911 const memBar = document.getElementById("mem-realtime-' . $calId . '"); 22141d05cddcSAtari911 if (memBar) { 22151d05cddcSAtari911 memBar.style.width = Math.min(100, data.memory) + "%"; 22161d05cddcSAtari911 } 22171d05cddcSAtari911 }) 22181d05cddcSAtari911 .catch(error => { 22191d05cddcSAtari911 console.log("System stats error:", error); 22201d05cddcSAtari911 }); 22211d05cddcSAtari911 } 22221d05cddcSAtari911 22231d05cddcSAtari911 updateSystemStats(); 22241d05cddcSAtari911 setInterval(updateSystemStats, 2000); 22251d05cddcSAtari911})(); 22261d05cddcSAtari911</script>'; 22271d05cddcSAtari911 22281d05cddcSAtari911 // NOW add the header HTML (after JavaScript is defined) 22291d05cddcSAtari911 $todayDate = new DateTime(); 22301d05cddcSAtari911 $displayDate = $todayDate->format('D, M j, Y'); 22311d05cddcSAtari911 $currentTime = $todayDate->format('g:i:s A'); 22321d05cddcSAtari911 22331d05cddcSAtari911 $html .= '<div class="eventlist-today-header">'; 22341d05cddcSAtari911 $html .= '<span class="eventlist-today-clock" id="clock-' . $calId . '">' . $currentTime . '</span>'; 22351d05cddcSAtari911 $html .= '<div class="eventlist-bottom-info">'; 22361d05cddcSAtari911 $html .= '<span class="eventlist-weather"><span id="weather-icon-' . $calId . '">️</span> <span id="weather-temp-' . $calId . '">--°</span></span>'; 22371d05cddcSAtari911 $html .= '<span class="eventlist-today-date">' . $displayDate . '</span>'; 22381d05cddcSAtari911 $html .= '</div>'; 22391d05cddcSAtari911 22401d05cddcSAtari911 // Three CPU/Memory bars (all update live) 22411d05cddcSAtari911 $html .= '<div class="eventlist-stats-container">'; 22421d05cddcSAtari911 22431d05cddcSAtari911 // 5-minute load average (green, updates every 2 seconds) 22441d05cddcSAtari911 $html .= '<div class="eventlist-cpu-bar" onmouseover="showTooltip_' . $jsCalId . '(\'green\')" onmouseout="hideTooltip_' . $jsCalId . '(\'green\')">'; 22451d05cddcSAtari911 $html .= '<div class="eventlist-cpu-fill" id="cpu-5min-' . $calId . '" style="width: 0%;"></div>'; 22461d05cddcSAtari911 $html .= '<div class="system-tooltip" id="tooltip-green-' . $calId . '" style="display:none;"></div>'; 22471d05cddcSAtari911 $html .= '</div>'; 22481d05cddcSAtari911 22491d05cddcSAtari911 // Real-time CPU (purple, updates with 5-sec average) 22501d05cddcSAtari911 $html .= '<div class="eventlist-cpu-bar eventlist-cpu-realtime" onmouseover="showTooltip_' . $jsCalId . '(\'purple\')" onmouseout="hideTooltip_' . $jsCalId . '(\'purple\')">'; 22511d05cddcSAtari911 $html .= '<div class="eventlist-cpu-fill eventlist-cpu-fill-purple" id="cpu-realtime-' . $calId . '" style="width: 0%;"></div>'; 22521d05cddcSAtari911 $html .= '<div class="system-tooltip" id="tooltip-purple-' . $calId . '" style="display:none;"></div>'; 22531d05cddcSAtari911 $html .= '</div>'; 22541d05cddcSAtari911 22551d05cddcSAtari911 // Real-time Memory (orange, updates) 22561d05cddcSAtari911 $html .= '<div class="eventlist-cpu-bar eventlist-mem-realtime" onmouseover="showTooltip_' . $jsCalId . '(\'orange\')" onmouseout="hideTooltip_' . $jsCalId . '(\'orange\')">'; 22571d05cddcSAtari911 $html .= '<div class="eventlist-cpu-fill eventlist-cpu-fill-orange" id="mem-realtime-' . $calId . '" style="width: 0%;"></div>'; 22581d05cddcSAtari911 $html .= '<div class="system-tooltip" id="tooltip-orange-' . $calId . '" style="display:none;"></div>'; 22591d05cddcSAtari911 $html .= '</div>'; 22601d05cddcSAtari911 22611d05cddcSAtari911 $html .= '</div>'; 22621d05cddcSAtari911 $html .= '</div>'; 22631d05cddcSAtari911 2264*231d0edbSAtari911 // Get today's date for default event date 2265*231d0edbSAtari911 $todayStr = date('Y-m-d'); 2266*231d0edbSAtari911 2267*231d0edbSAtari911 // Thin dark green "Add Event" bar between header and week grid (zero margin, smaller text, text positioned higher) 2268*231d0edbSAtari911 $html .= '<div style="background:#006400; padding:0; margin:0; height:12px; line-height:10px; text-align:center; cursor:pointer; border-top:1px solid rgba(0, 100, 0, 0.3); border-bottom:1px solid rgba(0, 100, 0, 0.3); box-shadow:0 0 8px rgba(0, 100, 0, 0.4); transition:all 0.2s;" onclick="openAddEvent(\'' . $calId . '\', \'' . $namespace . '\', \'' . $todayStr . '\');" onmouseover="this.style.background=\'#004d00\'; this.style.boxShadow=\'0 0 12px rgba(0, 100, 0, 0.6)\';" onmouseout="this.style.background=\'#006400\'; this.style.boxShadow=\'0 0 8px rgba(0, 100, 0, 0.4)\';">'; 2269*231d0edbSAtari911 $html .= '<span style="color:#00ff00; font-size:8px; font-weight:700; letter-spacing:0.4px; font-family:system-ui, sans-serif; text-shadow:0 0 3px rgba(0, 255, 0, 0.5); position:relative; top:-1px;">+ ADD EVENT</span>'; 22701d05cddcSAtari911 $html .= '</div>'; 22711d05cddcSAtari911 22721d05cddcSAtari911 // Week grid (7 cells) 22731d05cddcSAtari911 $html .= $this->renderWeekGrid($weekEvents, $weekStart); 22741d05cddcSAtari911 22751d05cddcSAtari911 // Today section (orange) 22761d05cddcSAtari911 if (!empty($todayEvents)) { 22771d05cddcSAtari911 $html .= $this->renderSidebarSection('Today', $todayEvents, '#ff9800', $calId); 22781d05cddcSAtari911 } 22791d05cddcSAtari911 22801d05cddcSAtari911 // Tomorrow section (green) 22811d05cddcSAtari911 if (!empty($tomorrowEvents)) { 22821d05cddcSAtari911 $html .= $this->renderSidebarSection('Tomorrow', $tomorrowEvents, '#4caf50', $calId); 22831d05cddcSAtari911 } 22841d05cddcSAtari911 22851d05cddcSAtari911 // Important events section (purple) 22861d05cddcSAtari911 if (!empty($importantEvents)) { 22871d05cddcSAtari911 $html .= $this->renderSidebarSection('Important Events', $importantEvents, '#9b59b6', $calId); 22881d05cddcSAtari911 } 22891d05cddcSAtari911 22901d05cddcSAtari911 $html .= '</div>'; 22911d05cddcSAtari911 2292*231d0edbSAtari911 // Add event dialog for sidebar widget 2293*231d0edbSAtari911 $html .= $this->renderEventDialog($calId, $namespace); 2294*231d0edbSAtari911 22951d05cddcSAtari911 return $html; 22961d05cddcSAtari911 } 22971d05cddcSAtari911 22981d05cddcSAtari911 /** 22991d05cddcSAtari911 * Render compact week grid (7 cells with event bars) - Matrix themed with clickable days 23001d05cddcSAtari911 */ 23011d05cddcSAtari911 private function renderWeekGrid($weekEvents, $weekStart) { 23021d05cddcSAtari911 // Generate unique ID for this calendar instance - sanitize for JavaScript 23031d05cddcSAtari911 $calId = 'cal_' . substr(md5($weekStart . microtime()), 0, 8); 23041d05cddcSAtari911 $jsCalId = str_replace('-', '_', $calId); // Sanitize for JS variable names 23051d05cddcSAtari911 23061d05cddcSAtari911 $html = '<div style="display:grid; grid-template-columns:repeat(7, 1fr); gap:1px; background:#1a3d1a; border-bottom:2px solid #00cc07;">'; 23071d05cddcSAtari911 23081d05cddcSAtari911 $dayNames = ['M', 'T', 'W', 'T', 'F', 'S', 'S']; 23091d05cddcSAtari911 $today = date('Y-m-d'); 23101d05cddcSAtari911 23111d05cddcSAtari911 for ($i = 0; $i < 7; $i++) { 23121d05cddcSAtari911 $date = date('Y-m-d', strtotime($weekStart . ' +' . $i . ' days')); 23131d05cddcSAtari911 $dayNum = date('j', strtotime($date)); 23141d05cddcSAtari911 $isToday = $date === $today; 23151d05cddcSAtari911 23161d05cddcSAtari911 $events = isset($weekEvents[$date]) ? $weekEvents[$date] : []; 23171d05cddcSAtari911 $eventCount = count($events); 23181d05cddcSAtari911 23191d05cddcSAtari911 $bgColor = $isToday ? '#2a4d2a' : '#242424'; 23201d05cddcSAtari911 $textColor = $isToday ? '#00ff00' : '#00cc07'; 23211d05cddcSAtari911 $fontWeight = $isToday ? '700' : '500'; 23221d05cddcSAtari911 $textShadow = $isToday ? 'text-shadow:0 0 6px rgba(0, 255, 0, 0.6);' : 'text-shadow:0 0 4px rgba(0, 204, 7, 0.4);'; 23231d05cddcSAtari911 23241d05cddcSAtari911 $hasEvents = $eventCount > 0; 23251d05cddcSAtari911 $clickableStyle = $hasEvents ? 'cursor:pointer;' : ''; 23261d05cddcSAtari911 $clickHandler = $hasEvents ? ' onclick="showDayEvents_' . $jsCalId . '(\'' . $date . '\')"' : ''; 23271d05cddcSAtari911 23281d05cddcSAtari911 $html .= '<div style="background:' . $bgColor . '; padding:4px 2px; text-align:center; min-height:45px; position:relative; border:1px solid rgba(0, 204, 7, 0.2); ' . $clickableStyle . '" ' . $clickHandler . '>'; 23291d05cddcSAtari911 23301d05cddcSAtari911 // Day letter 23311d05cddcSAtari911 $html .= '<div style="font-size:9px; color:#00cc07; font-weight:500; font-family:system-ui, sans-serif; ' . $textShadow . '">' . $dayNames[$i] . '</div>'; 23321d05cddcSAtari911 23331d05cddcSAtari911 // Day number 23341d05cddcSAtari911 $html .= '<div style="font-size:12px; color:' . $textColor . '; font-weight:' . $fontWeight . '; margin:2px 0; font-family:system-ui, sans-serif; ' . $textShadow . '">' . $dayNum . '</div>'; 23351d05cddcSAtari911 23361d05cddcSAtari911 // Event bars (max 3 visible) with glow effect 23371d05cddcSAtari911 if ($eventCount > 0) { 23381d05cddcSAtari911 $showCount = min($eventCount, 3); 23391d05cddcSAtari911 for ($j = 0; $j < $showCount; $j++) { 23401d05cddcSAtari911 $event = $events[$j]; 23411d05cddcSAtari911 $color = isset($event['color']) ? $event['color'] : '#00cc07'; 23421d05cddcSAtari911 $html .= '<div style="height:2px; background:' . htmlspecialchars($color) . '; margin:1px 0; border-radius:1px; box-shadow:0 0 3px ' . htmlspecialchars($color) . ';"></div>'; 23431d05cddcSAtari911 } 23441d05cddcSAtari911 23451d05cddcSAtari911 // Show "+N more" if more than 3 23461d05cddcSAtari911 if ($eventCount > 3) { 23471d05cddcSAtari911 $html .= '<div style="font-size:7px; color:#00cc07; margin-top:1px; font-family:system-ui, sans-serif;">+' . ($eventCount - 3) . '</div>'; 23481d05cddcSAtari911 } 23491d05cddcSAtari911 } 23501d05cddcSAtari911 23511d05cddcSAtari911 $html .= '</div>'; 23521d05cddcSAtari911 } 23531d05cddcSAtari911 23541d05cddcSAtari911 $html .= '</div>'; 23551d05cddcSAtari911 23561d05cddcSAtari911 // Add container for selected day events display (with unique ID) 23571d05cddcSAtari911 $html .= '<div id="selected-day-events-' . $calId . '" style="display:none; margin:8px 4px; border-left:3px solid #3498db; box-shadow:0 0 5px rgba(0, 204, 7, 0.2);">'; 23581d05cddcSAtari911 $html .= '<div style="background:#3498db; color:#000; padding:4px 6px; font-size:9px; font-weight:700; letter-spacing:0.3px; font-family:system-ui, sans-serif; box-shadow:0 0 8px #3498db; display:flex; justify-content:space-between; align-items:center;">'; 23591d05cddcSAtari911 $html .= '<span id="selected-day-title-' . $calId . '"></span>'; 23601d05cddcSAtari911 $html .= '<span onclick="document.getElementById(\'selected-day-events-' . $calId . '\').style.display=\'none\';" style="cursor:pointer; font-size:12px; padding:0 4px; font-weight:700;">✕</span>'; 23611d05cddcSAtari911 $html .= '</div>'; 23621d05cddcSAtari911 $html .= '<div id="selected-day-content-' . $calId . '" style="padding:4px 0; background:rgba(36, 36, 36, 0.5);"></div>'; 23631d05cddcSAtari911 $html .= '</div>'; 23641d05cddcSAtari911 23651d05cddcSAtari911 // Add JavaScript for day selection with event data 23661d05cddcSAtari911 $html .= '<script>'; 23671d05cddcSAtari911 // Sanitize calId for JavaScript variable names 23681d05cddcSAtari911 $jsCalId = str_replace('-', '_', $calId); 23691d05cddcSAtari911 $html .= 'window.weekEventsData_' . $jsCalId . ' = ' . json_encode($weekEvents) . ';'; 23701d05cddcSAtari911 $html .= ' 23711d05cddcSAtari911 window.showDayEvents_' . $jsCalId . ' = function(dateKey) { 23721d05cddcSAtari911 const eventsData = window.weekEventsData_' . $jsCalId . '; 23731d05cddcSAtari911 const container = document.getElementById("selected-day-events-' . $calId . '"); 23741d05cddcSAtari911 const title = document.getElementById("selected-day-title-' . $calId . '"); 23751d05cddcSAtari911 const content = document.getElementById("selected-day-content-' . $calId . '"); 23761d05cddcSAtari911 23771d05cddcSAtari911 if (!eventsData[dateKey] || eventsData[dateKey].length === 0) return; 23781d05cddcSAtari911 23791d05cddcSAtari911 // Format date for display 23801d05cddcSAtari911 const dateObj = new Date(dateKey + "T00:00:00"); 23811d05cddcSAtari911 const dayName = dateObj.toLocaleDateString("en-US", { weekday: "long" }); 23821d05cddcSAtari911 const monthDay = dateObj.toLocaleDateString("en-US", { month: "short", day: "numeric" }); 23831d05cddcSAtari911 title.textContent = dayName + ", " + monthDay; 23841d05cddcSAtari911 23851d05cddcSAtari911 // Clear content 23861d05cddcSAtari911 content.innerHTML = ""; 23871d05cddcSAtari911 2388*231d0edbSAtari911 // Sort events by time (all-day events first, then timed events chronologically) 23891d05cddcSAtari911 const sortedEvents = [...eventsData[dateKey]].sort((a, b) => { 2390*231d0edbSAtari911 // All-day events (no time) go to the beginning 23911d05cddcSAtari911 if (!a.time && !b.time) return 0; 2392*231d0edbSAtari911 if (!a.time) return -1; // a is all-day, comes first 2393*231d0edbSAtari911 if (!b.time) return 1; // b is all-day, comes first 23941d05cddcSAtari911 23951d05cddcSAtari911 // Compare times (format: "HH:MM") 23961d05cddcSAtari911 const timeA = a.time.split(":").map(Number); 23971d05cddcSAtari911 const timeB = b.time.split(":").map(Number); 23981d05cddcSAtari911 const minutesA = timeA[0] * 60 + timeA[1]; 23991d05cddcSAtari911 const minutesB = timeB[0] * 60 + timeB[1]; 24001d05cddcSAtari911 24011d05cddcSAtari911 return minutesA - minutesB; 24021d05cddcSAtari911 }); 24031d05cddcSAtari911 2404*231d0edbSAtari911 // Build events HTML with single color bar (event color only) 24051d05cddcSAtari911 sortedEvents.forEach(event => { 24061d05cddcSAtari911 const eventColor = event.color || "#00cc07"; 24071d05cddcSAtari911 24081d05cddcSAtari911 const eventDiv = document.createElement("div"); 2409*231d0edbSAtari911 eventDiv.style.cssText = "padding:4px 6px; border-bottom:1px solid rgba(0, 204, 7, 0.2); font-size:10px; display:flex; align-items:stretch; gap:6px; background:rgba(36, 36, 36, 0.3); min-height:20px;"; 24101d05cddcSAtari911 24111d05cddcSAtari911 let eventHTML = ""; 24121d05cddcSAtari911 2413*231d0edbSAtari911 // Event assigned color bar (single bar on left) 2414*231d0edbSAtari911 eventHTML += "<div style=\\"width:3px; align-self:stretch; background:" + eventColor + "; border-radius:1px; flex-shrink:0; box-shadow:0 0 3px " + eventColor + ";\\"></div>"; 24151d05cddcSAtari911 2416*231d0edbSAtari911 // Content wrapper 2417*231d0edbSAtari911 eventHTML += "<div style=\\"flex:1; min-width:0; display:flex; justify-content:space-between; align-items:start; gap:4px;\\">"; 24181d05cddcSAtari911 2419*231d0edbSAtari911 // Left side: event details 24201d05cddcSAtari911 eventHTML += "<div style=\\"flex:1; min-width:0;\\">"; 24211d05cddcSAtari911 eventHTML += "<div style=\\"font-weight:600; color:#00cc07; word-wrap:break-word; font-family:system-ui, sans-serif; text-shadow:0 0 3px rgba(0, 204, 7, 0.4);\\">"; 24221d05cddcSAtari911 24231d05cddcSAtari911 // Time 24241d05cddcSAtari911 if (event.time) { 24251d05cddcSAtari911 const timeParts = event.time.split(":"); 24261d05cddcSAtari911 let hours = parseInt(timeParts[0]); 24271d05cddcSAtari911 const minutes = timeParts[1]; 24281d05cddcSAtari911 const ampm = hours >= 12 ? "PM" : "AM"; 24291d05cddcSAtari911 hours = hours % 12 || 12; 24301d05cddcSAtari911 eventHTML += "<span style=\\"color:#00dd00; font-weight:500; font-size:9px;\\">" + hours + ":" + minutes + " " + ampm + "</span> "; 24311d05cddcSAtari911 } 24321d05cddcSAtari911 24331d05cddcSAtari911 // Title - use HTML version if available 24341d05cddcSAtari911 const titleHTML = event.title_html || event.title || "Untitled"; 24351d05cddcSAtari911 eventHTML += titleHTML; 24361d05cddcSAtari911 eventHTML += "</div>"; 24371d05cddcSAtari911 24381d05cddcSAtari911 // Description if present - use HTML version 24391d05cddcSAtari911 if (event.description_html || event.description) { 24401d05cddcSAtari911 const descHTML = event.description_html || event.description; 24411d05cddcSAtari911 eventHTML += "<div style=\\"font-size:9px; color:#00aa00; margin-top:2px;\\">" + descHTML + "</div>"; 24421d05cddcSAtari911 } 24431d05cddcSAtari911 2444*231d0edbSAtari911 eventHTML += "</div>"; // Close event details 2445*231d0edbSAtari911 2446*231d0edbSAtari911 // Right side: conflict badge (if present) 2447*231d0edbSAtari911 if (event.conflict) { 2448*231d0edbSAtari911 eventHTML += "<div style=\\"flex-shrink:0; color:#ff9800; font-size:10px; margin-top:2px; opacity:0.8;\\" title=\\"Time conflict detected\\">⚠</div>"; 2449*231d0edbSAtari911 } 2450*231d0edbSAtari911 2451*231d0edbSAtari911 eventHTML += "</div>"; // Close content wrapper 24521d05cddcSAtari911 24531d05cddcSAtari911 eventDiv.innerHTML = eventHTML; 24541d05cddcSAtari911 content.appendChild(eventDiv); 24551d05cddcSAtari911 }); 24561d05cddcSAtari911 24571d05cddcSAtari911 container.style.display = "block"; 24581d05cddcSAtari911 }; 24591d05cddcSAtari911 '; 24601d05cddcSAtari911 $html .= '</script>'; 24611d05cddcSAtari911 24621d05cddcSAtari911 return $html; 24631d05cddcSAtari911 } 24641d05cddcSAtari911 24651d05cddcSAtari911 /** 24661d05cddcSAtari911 * Render a sidebar section (Today/Tomorrow/Important) - Matrix themed with colored borders 24671d05cddcSAtari911 */ 24681d05cddcSAtari911 private function renderSidebarSection($title, $events, $accentColor, $calId) { 24691d05cddcSAtari911 // Keep the original accent colors for borders 24701d05cddcSAtari911 $borderColor = $accentColor; 24711d05cddcSAtari911 24721d05cddcSAtari911 // Show date for Important Events section 24731d05cddcSAtari911 $showDate = ($title === 'Important Events'); 24741d05cddcSAtari911 24751d05cddcSAtari911 $html = '<div style="border-left:3px solid ' . $borderColor . '; margin:8px 4px; box-shadow:0 0 5px rgba(0, 204, 7, 0.2);">'; 24761d05cddcSAtari911 24771d05cddcSAtari911 // Section header with accent color background - smaller, not all caps 24781d05cddcSAtari911 $html .= '<div style="background:' . $accentColor . '; color:#000; padding:4px 6px; font-size:9px; font-weight:700; letter-spacing:0.3px; font-family:system-ui, sans-serif; box-shadow:0 0 8px ' . $accentColor . ';">'; 24791d05cddcSAtari911 $html .= htmlspecialchars($title); 24801d05cddcSAtari911 $html .= '</div>'; 24811d05cddcSAtari911 24821d05cddcSAtari911 // Events 24831d05cddcSAtari911 $html .= '<div style="padding:4px 0; background:rgba(36, 36, 36, 0.5);">'; 24841d05cddcSAtari911 24851d05cddcSAtari911 foreach ($events as $event) { 24861d05cddcSAtari911 $html .= $this->renderSidebarEvent($event, $calId, $showDate, $accentColor); 24871d05cddcSAtari911 } 24881d05cddcSAtari911 24891d05cddcSAtari911 $html .= '</div>'; 24901d05cddcSAtari911 $html .= '</div>'; 24911d05cddcSAtari911 24921d05cddcSAtari911 return $html; 24931d05cddcSAtari911 } 24941d05cddcSAtari911 24951d05cddcSAtari911 /** 24961d05cddcSAtari911 * Render individual event in sidebar - Matrix themed with dual color bars 24971d05cddcSAtari911 */ 24981d05cddcSAtari911 private function renderSidebarEvent($event, $calId, $showDate = false, $sectionColor = '#00cc07') { 24991d05cddcSAtari911 $title = isset($event['title']) ? htmlspecialchars($event['title']) : 'Untitled'; 25001d05cddcSAtari911 $time = isset($event['time']) ? $event['time'] : ''; 25011d05cddcSAtari911 $endTime = isset($event['endTime']) ? $event['endTime'] : ''; 25021d05cddcSAtari911 $eventColor = isset($event['color']) ? htmlspecialchars($event['color']) : '#00cc07'; 25031d05cddcSAtari911 $date = isset($event['date']) ? $event['date'] : ''; 25041d05cddcSAtari911 $isTask = isset($event['isTask']) && $event['isTask']; 25051d05cddcSAtari911 $completed = isset($event['completed']) && $event['completed']; 25061d05cddcSAtari911 25071d05cddcSAtari911 // Check for conflicts 25081d05cddcSAtari911 $hasConflict = isset($event['conflicts']) && !empty($event['conflicts']); 25091d05cddcSAtari911 2510*231d0edbSAtari911 $html = '<div style="padding:4px 6px; border-bottom:1px solid rgba(0, 204, 7, 0.2); font-size:10px; display:flex; align-items:stretch; gap:6px; background:rgba(36, 36, 36, 0.3); min-height:20px;">'; 25111d05cddcSAtari911 2512*231d0edbSAtari911 // Event's assigned color bar (single bar on the left) 2513*231d0edbSAtari911 $html .= '<div style="width:3px; align-self:stretch; background:' . $eventColor . '; border-radius:1px; flex-shrink:0; box-shadow:0 0 3px ' . $eventColor . ';"></div>'; 25141d05cddcSAtari911 25151d05cddcSAtari911 // Content 25161d05cddcSAtari911 $html .= '<div style="flex:1; min-width:0;">'; 25171d05cddcSAtari911 25181d05cddcSAtari911 // Time + title 25191d05cddcSAtari911 $html .= '<div style="font-weight:600; color:#00cc07; word-wrap:break-word; font-family:system-ui, sans-serif; text-shadow:0 0 3px rgba(0, 204, 7, 0.4);">'; 25201d05cddcSAtari911 25211d05cddcSAtari911 if ($time) { 25221d05cddcSAtari911 $displayTime = $this->formatTimeDisplay($time, $endTime); 25231d05cddcSAtari911 $html .= '<span style="color:#00dd00; font-weight:500; font-size:9px;">' . htmlspecialchars($displayTime) . '</span> '; 25241d05cddcSAtari911 } 25251d05cddcSAtari911 25261d05cddcSAtari911 // Task checkbox 25271d05cddcSAtari911 if ($isTask) { 25281d05cddcSAtari911 $checkIcon = $completed ? '☑' : '☐'; 25291d05cddcSAtari911 $html .= '<span style="font-size:11px; color:#00ff00;">' . $checkIcon . '</span> '; 25301d05cddcSAtari911 } 25311d05cddcSAtari911 25321d05cddcSAtari911 $html .= htmlspecialchars($title); 25331d05cddcSAtari911 25341d05cddcSAtari911 // Conflict badge 25351d05cddcSAtari911 if ($hasConflict) { 25361d05cddcSAtari911 $conflictCount = count($event['conflicts']); 25371d05cddcSAtari911 $html .= ' <span style="background:#ff0000; color:#000; padding:1px 3px; border-radius:2px; font-size:8px; font-weight:700; box-shadow:0 0 4px #ff0000;">⚠ ' . $conflictCount . '</span>'; 25381d05cddcSAtari911 } 25391d05cddcSAtari911 25401d05cddcSAtari911 $html .= '</div>'; 25411d05cddcSAtari911 25421d05cddcSAtari911 // Date display BELOW event name for Important events 25431d05cddcSAtari911 if ($showDate && $date) { 25441d05cddcSAtari911 $dateObj = new DateTime($date); 25451d05cddcSAtari911 $displayDate = $dateObj->format('D, M j'); // e.g., "Mon, Feb 10" 25461d05cddcSAtari911 $html .= '<div style="font-size:8px; color:#00aa00; font-weight:500; margin-top:2px; text-shadow:0 0 2px rgba(0, 170, 0, 0.3);">' . htmlspecialchars($displayDate) . '</div>'; 25471d05cddcSAtari911 } 25481d05cddcSAtari911 25491d05cddcSAtari911 $html .= '</div>'; 25501d05cddcSAtari911 $html .= '</div>'; 25511d05cddcSAtari911 25521d05cddcSAtari911 return $html; 25531d05cddcSAtari911 } 25541d05cddcSAtari911 25551d05cddcSAtari911 /** 25561d05cddcSAtari911 * Format time display (12-hour format with optional end time) 25571d05cddcSAtari911 */ 25581d05cddcSAtari911 private function formatTimeDisplay($startTime, $endTime = '') { 25591d05cddcSAtari911 // Convert start time 25601d05cddcSAtari911 list($hour, $minute) = explode(':', $startTime); 25611d05cddcSAtari911 $hour = (int)$hour; 25621d05cddcSAtari911 $ampm = $hour >= 12 ? 'PM' : 'AM'; 25631d05cddcSAtari911 $displayHour = $hour % 12; 25641d05cddcSAtari911 if ($displayHour === 0) $displayHour = 12; 25651d05cddcSAtari911 25661d05cddcSAtari911 $display = $displayHour . ':' . $minute . ' ' . $ampm; 25671d05cddcSAtari911 25681d05cddcSAtari911 // Add end time if provided 25691d05cddcSAtari911 if ($endTime && $endTime !== '') { 25701d05cddcSAtari911 list($endHour, $endMinute) = explode(':', $endTime); 25711d05cddcSAtari911 $endHour = (int)$endHour; 25721d05cddcSAtari911 $endAmpm = $endHour >= 12 ? 'PM' : 'AM'; 25731d05cddcSAtari911 $endDisplayHour = $endHour % 12; 25741d05cddcSAtari911 if ($endDisplayHour === 0) $endDisplayHour = 12; 25751d05cddcSAtari911 25761d05cddcSAtari911 $display .= '-' . $endDisplayHour . ':' . $endMinute . ' ' . $endAmpm; 25771d05cddcSAtari911 } 25781d05cddcSAtari911 25791d05cddcSAtari911 return $display; 25801d05cddcSAtari911 } 25811d05cddcSAtari911 25821d05cddcSAtari911 /** 25831d05cddcSAtari911 * Render DokuWiki syntax to HTML 25841d05cddcSAtari911 * Converts **bold**, //italic//, [[links]], etc. to HTML 25851d05cddcSAtari911 */ 25861d05cddcSAtari911 private function renderDokuWikiToHtml($text) { 25871d05cddcSAtari911 if (empty($text)) return ''; 25881d05cddcSAtari911 25891d05cddcSAtari911 // Use DokuWiki's parser to render the text 25901d05cddcSAtari911 $instructions = p_get_instructions($text); 25911d05cddcSAtari911 25921d05cddcSAtari911 // Render instructions to XHTML 25931d05cddcSAtari911 $xhtml = p_render('xhtml', $instructions, $info); 25941d05cddcSAtari911 25951d05cddcSAtari911 // Remove surrounding <p> tags if present (we're rendering inline) 25961d05cddcSAtari911 $xhtml = preg_replace('/^<p>(.*)<\/p>$/s', '$1', trim($xhtml)); 25971d05cddcSAtari911 25981d05cddcSAtari911 return $xhtml; 25991d05cddcSAtari911 } 26001d05cddcSAtari911 26011d05cddcSAtari911 // Keep old scanForNamespaces for backward compatibility (not used anymore) 26021d05cddcSAtari911 private function scanForNamespaces($dir, $baseNamespace, &$namespaces) { 26031d05cddcSAtari911 if (!is_dir($dir)) return; 26041d05cddcSAtari911 26051d05cddcSAtari911 $items = scandir($dir); 26061d05cddcSAtari911 foreach ($items as $item) { 26071d05cddcSAtari911 if ($item === '.' || $item === '..' || $item === 'calendar') continue; 26081d05cddcSAtari911 26091d05cddcSAtari911 $path = $dir . $item; 26101d05cddcSAtari911 if (is_dir($path)) { 26111d05cddcSAtari911 $namespace = $baseNamespace ? $baseNamespace . ':' . $item : $item; 26121d05cddcSAtari911 $namespaces[] = $namespace; 26131d05cddcSAtari911 $this->scanForNamespaces($path . '/', $namespace, $namespaces); 26141d05cddcSAtari911 } 26151d05cddcSAtari911 } 26161d05cddcSAtari911 } 261719378907SAtari911} 2618