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 116*1d05cddcSAtari911 $html = '<div class="calendar-compact-container" id="' . $calId . '" data-namespace="' . htmlspecialchars($namespace) . '" data-original-namespace="' . htmlspecialchars($namespace) . '" data-year="' . $year . '" data-month="' . $month . '">'; 117*1d05cddcSAtari911 118*1d05cddcSAtari911 // Load calendar JavaScript manually (not through DokuWiki concatenation) 119*1d05cddcSAtari911 $html .= '<script src="' . DOKU_BASE . 'lib/plugins/calendar/calendar-main.js"></script>'; 120*1d05cddcSAtari911 121*1d05cddcSAtari911 // Initialize DOKU_BASE for JavaScript 122*1d05cddcSAtari911 $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 138*1d05cddcSAtari911 // Namespace filter indicator - only show if actively filtering a specific namespace 139*1d05cddcSAtari911 if ($namespace && $namespace !== '*' && strpos($namespace, '*') === false && strpos($namespace, ';') === false) { 140*1d05cddcSAtari911 $html .= '<div class="calendar-namespace-filter" id="namespace-filter-' . $calId . '">'; 141*1d05cddcSAtari911 $html .= '<span class="namespace-filter-label">Filtering:</span>'; 142*1d05cddcSAtari911 $html .= '<span class="namespace-filter-name">' . htmlspecialchars($namespace) . '</span>'; 143*1d05cddcSAtari911 $html .= '<button class="namespace-filter-clear" onclick="clearNamespaceFilter(\'' . $calId . '\')" title="Clear filter and show all namespaces">✕</button>'; 144*1d05cddcSAtari911 $html .= '</div>'; 145*1d05cddcSAtari911 } 146*1d05cddcSAtari911 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>'; 288*1d05cddcSAtari911 289*1d05cddcSAtari911 // Search bar in header 290*1d05cddcSAtari911 $html .= '<div class="event-search-container-inline">'; 291*1d05cddcSAtari911 $html .= '<input type="text" class="event-search-input-inline" id="event-search-' . $calId . '" placeholder=" Search..." oninput="filterEvents(\'' . $calId . '\', this.value)">'; 292*1d05cddcSAtari911 $html .= '<button class="event-search-clear-inline" id="search-clear-' . $calId . '" onclick="clearEventSearch(\'' . $calId . '\')" style="display:none;">✕</button>'; 293*1d05cddcSAtari911 $html .= '</div>'; 294*1d05cddcSAtari911 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 320*1d05cddcSAtari911 // Check for time conflicts 321*1d05cddcSAtari911 $events = $this->checkTimeConflicts($events); 322*1d05cddcSAtari911 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) { 329*1d05cddcSAtari911 $timeA = isset($a['time']) && !empty($a['time']) ? $a['time'] : null; 330*1d05cddcSAtari911 $timeB = isset($b['time']) && !empty($b['time']) ? $b['time'] : null; 331*1d05cddcSAtari911 332*1d05cddcSAtari911 // All-day events (no time) go to the TOP 333*1d05cddcSAtari911 if ($timeA === null && $timeB !== null) return -1; // A before B 334*1d05cddcSAtari911 if ($timeA !== null && $timeB === null) return 1; // A after B 335*1d05cddcSAtari911 if ($timeA === null && $timeB === null) return 0; // Both all-day, equal 336*1d05cddcSAtari911 337*1d05cddcSAtari911 // 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 347*1d05cddcSAtari911 // Helper function to check if event is past (with 15-minute grace period for timed events) 348*1d05cddcSAtari911 $isEventPast = function($dateKey, $time) use ($today) { 349*1d05cddcSAtari911 // If event is on a past date, it's definitely past 350*1d05cddcSAtari911 if ($dateKey < $today) { 351*1d05cddcSAtari911 return true; 352*1d05cddcSAtari911 } 353*1d05cddcSAtari911 354*1d05cddcSAtari911 // If event is on a future date, it's definitely not past 355*1d05cddcSAtari911 if ($dateKey > $today) { 356*1d05cddcSAtari911 return false; 357*1d05cddcSAtari911 } 358*1d05cddcSAtari911 359*1d05cddcSAtari911 // Event is today - check time with grace period 360*1d05cddcSAtari911 if ($time && $time !== '') { 361*1d05cddcSAtari911 try { 362*1d05cddcSAtari911 $currentDateTime = new DateTime(); 363*1d05cddcSAtari911 $eventDateTime = new DateTime($dateKey . ' ' . $time); 364*1d05cddcSAtari911 365*1d05cddcSAtari911 // Add 15-minute grace period 366*1d05cddcSAtari911 $eventDateTime->modify('+15 minutes'); 367*1d05cddcSAtari911 368*1d05cddcSAtari911 // Event is past if current time > event time + 15 minutes 369*1d05cddcSAtari911 return $currentDateTime > $eventDateTime; 370*1d05cddcSAtari911 } catch (Exception $e) { 371*1d05cddcSAtari911 // If time parsing fails, fall back to date-only comparison 372*1d05cddcSAtari911 return false; 373*1d05cddcSAtari911 } 374*1d05cddcSAtari911 } 375*1d05cddcSAtari911 376*1d05cddcSAtari911 // No time specified for today's event, treat as future 377*1d05cddcSAtari911 return false; 378*1d05cddcSAtari911 }; 379*1d05cddcSAtari911 380*1d05cddcSAtari911 // Build HTML for each event - separate past/completed from future 381*1d05cddcSAtari911 $pastHtml = ''; 382*1d05cddcSAtari911 $futureHtml = ''; 383*1d05cddcSAtari911 $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'; 394*1d05cddcSAtari911 $timeRaw = isset($event['time']) ? $event['time'] : ''; 395*1d05cddcSAtari911 $time = htmlspecialchars($timeRaw); 396*1d05cddcSAtari911 $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 403*1d05cddcSAtari911 // Use helper function to determine if event is past (with grace period) 404*1d05cddcSAtari911 $isPast = $isEventPast($dateKey, $timeRaw); 405*1d05cddcSAtari911 $isToday = $dateKey === $today; 406*1d05cddcSAtari911 407*1d05cddcSAtari911 // Check if event should be in past section 408*1d05cddcSAtari911 // EXCEPTION: Uncompleted tasks (isTask && !completed) should stay visible even if past 409*1d05cddcSAtari911 $isPastOrCompleted = ($isPast && (!$isTask || $completed)) || $completed; 410*1d05cddcSAtari911 if ($isPastOrCompleted) { 411*1d05cddcSAtari911 $pastCount++; 412*1d05cddcSAtari911 } 413*1d05cddcSAtari911 414*1d05cddcSAtari911 // Determine if task is past due (past date, is task, not completed) 415*1d05cddcSAtari911 $isPastDue = $isPast && $isTask && !$completed; 416*1d05cddcSAtari911 41719378907SAtari911 // Process description for wiki syntax, HTML, images, and links 41819378907SAtari911 $renderedDescription = $this->renderDescription($description); 41919378907SAtari911 420*1d05cddcSAtari911 // 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'); 426*1d05cddcSAtari911 427*1d05cddcSAtari911 // Add end time if present and different from start time 428*1d05cddcSAtari911 if ($endTime && $endTime !== $time) { 429*1d05cddcSAtari911 $endTimeObj = DateTime::createFromFormat('H:i', $endTime); 430*1d05cddcSAtari911 if ($endTimeObj) { 431*1d05cddcSAtari911 $displayTime .= ' - ' . $endTimeObj->format('g:i A'); 432*1d05cddcSAtari911 } 433*1d05cddcSAtari911 } 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' : ''; 453*1d05cddcSAtari911 // Don't grey out past due tasks - they need attention! 454*1d05cddcSAtari911 $pastClass = ($isPast && !$isPastDue) ? ' event-past' : ''; 455*1d05cddcSAtari911 $pastDueClass = $isPastDue ? ' event-pastdue' : ''; 456e3a9f44cSAtari911 $firstFutureAttr = ($firstFutureEventId === $eventId) ? ' data-first-future="true"' : ''; 45719378907SAtari911 458*1d05cddcSAtari911 $eventHtml = '<div class="event-compact-item' . $completedClass . $pastClass . $pastDueClass . '" data-event-id="' . $eventId . '" data-date="' . $dateKey . '" style="border-left-color: ' . $color . ';"' . $firstFutureAttr . '>'; 45919378907SAtari911 460*1d05cddcSAtari911 $eventHtml .= '<div class="event-info">'; 461*1d05cddcSAtari911 $eventHtml .= '<div class="event-title-row">'; 462*1d05cddcSAtari911 $eventHtml .= '<span class="event-title-compact">' . $title . '</span>'; 463*1d05cddcSAtari911 $eventHtml .= '</div>'; 46419378907SAtari911 465e3a9f44cSAtari911 // For past events, hide meta and description (collapsed) 466*1d05cddcSAtari911 // EXCEPTION: Past due tasks should show their details 467*1d05cddcSAtari911 if (!$isPast || $isPastDue) { 468*1d05cddcSAtari911 $eventHtml .= '<div class="event-meta-compact">'; 469*1d05cddcSAtari911 $eventHtml .= '<span class="event-date-time">' . $displayDate . $multiDay; 47019378907SAtari911 if ($displayTime) { 471*1d05cddcSAtari911 $eventHtml .= ' • ' . $displayTime; 47219378907SAtari911 } 473*1d05cddcSAtari911 // Add TODAY badge for today's events OR PAST DUE for uncompleted past tasks 474*1d05cddcSAtari911 if ($isPastDue) { 475*1d05cddcSAtari911 $eventHtml .= ' <span class="event-pastdue-badge">PAST DUE</span>'; 476*1d05cddcSAtari911 } elseif ($isToday) { 477*1d05cddcSAtari911 $eventHtml .= ' <span class="event-today-badge">TODAY</span>'; 478e3a9f44cSAtari911 } 479*1d05cddcSAtari911 // 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 } 484*1d05cddcSAtari911 // Show badge if namespace exists and is not empty 485*1d05cddcSAtari911 if ($eventNamespace && $eventNamespace !== '') { 486*1d05cddcSAtari911 $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 } 488*1d05cddcSAtari911 489*1d05cddcSAtari911 // Add conflict warning if event has time conflicts 490*1d05cddcSAtari911 if (isset($event['hasConflict']) && $event['hasConflict'] && isset($event['conflictsWith'])) { 491*1d05cddcSAtari911 $conflictList = []; 492*1d05cddcSAtari911 foreach ($event['conflictsWith'] as $conflict) { 493*1d05cddcSAtari911 $conflictText = htmlspecialchars($conflict['title']); 494*1d05cddcSAtari911 if (!empty($conflict['time'])) { 495*1d05cddcSAtari911 // Format time range 496*1d05cddcSAtari911 $startTimeObj = DateTime::createFromFormat('H:i', $conflict['time']); 497*1d05cddcSAtari911 $startTimeFormatted = $startTimeObj ? $startTimeObj->format('g:i A') : $conflict['time']; 498*1d05cddcSAtari911 499*1d05cddcSAtari911 if (!empty($conflict['endTime']) && $conflict['endTime'] !== $conflict['time']) { 500*1d05cddcSAtari911 $endTimeObj = DateTime::createFromFormat('H:i', $conflict['endTime']); 501*1d05cddcSAtari911 $endTimeFormatted = $endTimeObj ? $endTimeObj->format('g:i A') : $conflict['endTime']; 502*1d05cddcSAtari911 $conflictText .= ' (' . $startTimeFormatted . ' - ' . $endTimeFormatted . ')'; 503*1d05cddcSAtari911 } else { 504*1d05cddcSAtari911 $conflictText .= ' (' . $startTimeFormatted . ')'; 505*1d05cddcSAtari911 } 506*1d05cddcSAtari911 } 507*1d05cddcSAtari911 $conflictList[] = $conflictText; 508*1d05cddcSAtari911 } 509*1d05cddcSAtari911 $conflictCount = count($event['conflictsWith']); 510*1d05cddcSAtari911 $conflictJson = htmlspecialchars(json_encode($conflictList), ENT_QUOTES, 'UTF-8'); 511*1d05cddcSAtari911 $eventHtml .= ' <span class="event-conflict-badge" data-conflicts="' . $conflictJson . '" onmouseenter="showConflictTooltip(this)" onmouseleave="hideConflictTooltip()">⚠️ ' . $conflictCount . '</span>'; 512*1d05cddcSAtari911 } 513*1d05cddcSAtari911 514*1d05cddcSAtari911 $eventHtml .= '</span>'; 515*1d05cddcSAtari911 $eventHtml .= '</div>'; 51619378907SAtari911 51719378907SAtari911 if ($description) { 518*1d05cddcSAtari911 $eventHtml .= '<div class="event-desc-compact">' . $renderedDescription . '</div>'; 519*1d05cddcSAtari911 } 520*1d05cddcSAtari911 } else { 521*1d05cddcSAtari911 // Past events: render with display:none for click-to-expand 522*1d05cddcSAtari911 $eventHtml .= '<div class="event-meta-compact" style="display:none;">'; 523*1d05cddcSAtari911 $eventHtml .= '<span class="event-date-time">' . $displayDate . $multiDay; 524*1d05cddcSAtari911 if ($displayTime) { 525*1d05cddcSAtari911 $eventHtml .= ' • ' . $displayTime; 526*1d05cddcSAtari911 } 527*1d05cddcSAtari911 $eventNamespace = isset($event['namespace']) ? $event['namespace'] : ''; 528*1d05cddcSAtari911 if (!$eventNamespace && isset($event['_namespace'])) { 529*1d05cddcSAtari911 $eventNamespace = $event['_namespace']; 530*1d05cddcSAtari911 } 531*1d05cddcSAtari911 if ($eventNamespace && $eventNamespace !== '') { 532*1d05cddcSAtari911 $eventHtml .= ' <span class="event-namespace-badge" onclick="filterCalendarByNamespace(\'' . $calId . '\', \'' . htmlspecialchars($eventNamespace) . '\')" style="cursor:pointer;" title="Click to filter by this namespace">' . htmlspecialchars($eventNamespace) . '</span>'; 533*1d05cddcSAtari911 } 534*1d05cddcSAtari911 535*1d05cddcSAtari911 // Add conflict warning if event has time conflicts 536*1d05cddcSAtari911 if (isset($event['hasConflict']) && $event['hasConflict'] && isset($event['conflictsWith'])) { 537*1d05cddcSAtari911 $conflictList = []; 538*1d05cddcSAtari911 foreach ($event['conflictsWith'] as $conflict) { 539*1d05cddcSAtari911 $conflictText = htmlspecialchars($conflict['title']); 540*1d05cddcSAtari911 if (!empty($conflict['time'])) { 541*1d05cddcSAtari911 $startTimeObj = DateTime::createFromFormat('H:i', $conflict['time']); 542*1d05cddcSAtari911 $startTimeFormatted = $startTimeObj ? $startTimeObj->format('g:i A') : $conflict['time']; 543*1d05cddcSAtari911 544*1d05cddcSAtari911 if (!empty($conflict['endTime']) && $conflict['endTime'] !== $conflict['time']) { 545*1d05cddcSAtari911 $endTimeObj = DateTime::createFromFormat('H:i', $conflict['endTime']); 546*1d05cddcSAtari911 $endTimeFormatted = $endTimeObj ? $endTimeObj->format('g:i A') : $conflict['endTime']; 547*1d05cddcSAtari911 $conflictText .= ' (' . $startTimeFormatted . ' - ' . $endTimeFormatted . ')'; 548*1d05cddcSAtari911 } else { 549*1d05cddcSAtari911 $conflictText .= ' (' . $startTimeFormatted . ')'; 550*1d05cddcSAtari911 } 551*1d05cddcSAtari911 } 552*1d05cddcSAtari911 $conflictList[] = $conflictText; 553*1d05cddcSAtari911 } 554*1d05cddcSAtari911 $conflictCount = count($event['conflictsWith']); 555*1d05cddcSAtari911 $conflictJson = htmlspecialchars(json_encode($conflictList), ENT_QUOTES, 'UTF-8'); 556*1d05cddcSAtari911 $eventHtml .= ' <span class="event-conflict-badge" data-conflicts="' . $conflictJson . '" onmouseenter="showConflictTooltip(this)" onmouseleave="hideConflictTooltip()">⚠️ ' . $conflictCount . '</span>'; 557*1d05cddcSAtari911 } 558*1d05cddcSAtari911 559*1d05cddcSAtari911 $eventHtml .= '</span>'; 560*1d05cddcSAtari911 $eventHtml .= '</div>'; 561*1d05cddcSAtari911 562*1d05cddcSAtari911 if ($description) { 563*1d05cddcSAtari911 $eventHtml .= '<div class="event-desc-compact" style="display:none;">' . $renderedDescription . '</div>'; 56419378907SAtari911 } 565e3a9f44cSAtari911 } 56619378907SAtari911 567*1d05cddcSAtari911 $eventHtml .= '</div>'; // event-info 56819378907SAtari911 569e3a9f44cSAtari911 // Use stored namespace from event, fallback to passed namespace 570e3a9f44cSAtari911 $buttonNamespace = isset($event['namespace']) ? $event['namespace'] : $namespace; 571e3a9f44cSAtari911 572*1d05cddcSAtari911 $eventHtml .= '<div class="event-actions-compact">'; 573*1d05cddcSAtari911 $eventHtml .= '<button class="event-action-btn" onclick="deleteEvent(\'' . $calId . '\', \'' . $eventId . '\', \'' . $dateKey . '\', \'' . $buttonNamespace . '\')">️</button>'; 574*1d05cddcSAtari911 $eventHtml .= '<button class="event-action-btn" onclick="editEvent(\'' . $calId . '\', \'' . $eventId . '\', \'' . $dateKey . '\', \'' . $buttonNamespace . '\')">✏️</button>'; 575*1d05cddcSAtari911 $eventHtml .= '</div>'; 57619378907SAtari911 57719378907SAtari911 // Checkbox for tasks - ON THE FAR RIGHT 57819378907SAtari911 if ($isTask) { 57919378907SAtari911 $checked = $completed ? 'checked' : ''; 580*1d05cddcSAtari911 $eventHtml .= '<input type="checkbox" class="task-checkbox" ' . $checked . ' onclick="toggleTaskComplete(\'' . $calId . '\', \'' . $eventId . '\', \'' . $dateKey . '\', \'' . $buttonNamespace . '\', this.checked)">'; 58119378907SAtari911 } 58219378907SAtari911 583*1d05cddcSAtari911 $eventHtml .= '</div>'; 584*1d05cddcSAtari911 585*1d05cddcSAtari911 // Add to appropriate section 586*1d05cddcSAtari911 if ($isPastOrCompleted) { 587*1d05cddcSAtari911 $pastHtml .= $eventHtml; 588*1d05cddcSAtari911 } else { 589*1d05cddcSAtari911 $futureHtml .= $eventHtml; 590*1d05cddcSAtari911 } 591*1d05cddcSAtari911 } 592*1d05cddcSAtari911 } 593*1d05cddcSAtari911 594*1d05cddcSAtari911 // Build final HTML with collapsible past events section 595*1d05cddcSAtari911 $html = ''; 596*1d05cddcSAtari911 597*1d05cddcSAtari911 // Add collapsible past events section if any exist 598*1d05cddcSAtari911 if ($pastCount > 0) { 599*1d05cddcSAtari911 $html .= '<div class="past-events-section">'; 600*1d05cddcSAtari911 $html .= '<div class="past-events-toggle" onclick="togglePastEvents(\'' . $calId . '\')">'; 601*1d05cddcSAtari911 $html .= '<span class="past-events-arrow" id="past-arrow-' . $calId . '">▶</span> '; 602*1d05cddcSAtari911 $html .= '<span class="past-events-label">Past Events (' . $pastCount . ')</span>'; 60319378907SAtari911 $html .= '</div>'; 604*1d05cddcSAtari911 $html .= '<div class="past-events-content" id="past-events-' . $calId . '" style="display:none;">'; 605*1d05cddcSAtari911 $html .= $pastHtml; 606*1d05cddcSAtari911 $html .= '</div>'; 607*1d05cddcSAtari911 $html .= '</div>'; 608*1d05cddcSAtari911 } 609e3a9f44cSAtari911 610*1d05cddcSAtari911 // Add future events 611*1d05cddcSAtari911 $html .= $futureHtml; 61219378907SAtari911 61319378907SAtari911 return $html; 61419378907SAtari911 } 61519378907SAtari911 616*1d05cddcSAtari911 /** 617*1d05cddcSAtari911 * Check for time conflicts between events 618*1d05cddcSAtari911 */ 619*1d05cddcSAtari911 private function checkTimeConflicts($events) { 620*1d05cddcSAtari911 // Group events by date 621*1d05cddcSAtari911 $eventsByDate = []; 622*1d05cddcSAtari911 foreach ($events as $date => $dateEvents) { 623*1d05cddcSAtari911 if (!is_array($dateEvents)) continue; 624*1d05cddcSAtari911 625*1d05cddcSAtari911 foreach ($dateEvents as $evt) { 626*1d05cddcSAtari911 if (empty($evt['time'])) continue; // Skip all-day events 627*1d05cddcSAtari911 628*1d05cddcSAtari911 if (!isset($eventsByDate[$date])) { 629*1d05cddcSAtari911 $eventsByDate[$date] = []; 630*1d05cddcSAtari911 } 631*1d05cddcSAtari911 $eventsByDate[$date][] = $evt; 632*1d05cddcSAtari911 } 633*1d05cddcSAtari911 } 634*1d05cddcSAtari911 635*1d05cddcSAtari911 // Check for overlaps on each date 636*1d05cddcSAtari911 foreach ($eventsByDate as $date => $dateEvents) { 637*1d05cddcSAtari911 for ($i = 0; $i < count($dateEvents); $i++) { 638*1d05cddcSAtari911 for ($j = $i + 1; $j < count($dateEvents); $j++) { 639*1d05cddcSAtari911 if ($this->eventsOverlap($dateEvents[$i], $dateEvents[$j])) { 640*1d05cddcSAtari911 // Mark both events as conflicting 641*1d05cddcSAtari911 $dateEvents[$i]['hasConflict'] = true; 642*1d05cddcSAtari911 $dateEvents[$j]['hasConflict'] = true; 643*1d05cddcSAtari911 644*1d05cddcSAtari911 // Store conflict info 645*1d05cddcSAtari911 if (!isset($dateEvents[$i]['conflictsWith'])) { 646*1d05cddcSAtari911 $dateEvents[$i]['conflictsWith'] = []; 647*1d05cddcSAtari911 } 648*1d05cddcSAtari911 if (!isset($dateEvents[$j]['conflictsWith'])) { 649*1d05cddcSAtari911 $dateEvents[$j]['conflictsWith'] = []; 650*1d05cddcSAtari911 } 651*1d05cddcSAtari911 652*1d05cddcSAtari911 $dateEvents[$i]['conflictsWith'][] = [ 653*1d05cddcSAtari911 'id' => $dateEvents[$j]['id'], 654*1d05cddcSAtari911 'title' => $dateEvents[$j]['title'], 655*1d05cddcSAtari911 'time' => $dateEvents[$j]['time'], 656*1d05cddcSAtari911 'endTime' => isset($dateEvents[$j]['endTime']) ? $dateEvents[$j]['endTime'] : '' 657*1d05cddcSAtari911 ]; 658*1d05cddcSAtari911 659*1d05cddcSAtari911 $dateEvents[$j]['conflictsWith'][] = [ 660*1d05cddcSAtari911 'id' => $dateEvents[$i]['id'], 661*1d05cddcSAtari911 'title' => $dateEvents[$i]['title'], 662*1d05cddcSAtari911 'time' => $dateEvents[$i]['time'], 663*1d05cddcSAtari911 'endTime' => isset($dateEvents[$i]['endTime']) ? $dateEvents[$i]['endTime'] : '' 664*1d05cddcSAtari911 ]; 665*1d05cddcSAtari911 } 666*1d05cddcSAtari911 } 667*1d05cddcSAtari911 } 668*1d05cddcSAtari911 669*1d05cddcSAtari911 // Update the events array with conflict information 670*1d05cddcSAtari911 foreach ($events[$date] as &$evt) { 671*1d05cddcSAtari911 foreach ($dateEvents as $checkedEvt) { 672*1d05cddcSAtari911 if ($evt['id'] === $checkedEvt['id']) { 673*1d05cddcSAtari911 if (isset($checkedEvt['hasConflict'])) { 674*1d05cddcSAtari911 $evt['hasConflict'] = $checkedEvt['hasConflict']; 675*1d05cddcSAtari911 } 676*1d05cddcSAtari911 if (isset($checkedEvt['conflictsWith'])) { 677*1d05cddcSAtari911 $evt['conflictsWith'] = $checkedEvt['conflictsWith']; 678*1d05cddcSAtari911 } 679*1d05cddcSAtari911 break; 680*1d05cddcSAtari911 } 681*1d05cddcSAtari911 } 682*1d05cddcSAtari911 } 683*1d05cddcSAtari911 } 684*1d05cddcSAtari911 685*1d05cddcSAtari911 return $events; 686*1d05cddcSAtari911 } 687*1d05cddcSAtari911 688*1d05cddcSAtari911 /** 689*1d05cddcSAtari911 * Check if two events overlap in time 690*1d05cddcSAtari911 */ 691*1d05cddcSAtari911 private function eventsOverlap($evt1, $evt2) { 692*1d05cddcSAtari911 if (empty($evt1['time']) || empty($evt2['time'])) { 693*1d05cddcSAtari911 return false; // All-day events don't conflict 694*1d05cddcSAtari911 } 695*1d05cddcSAtari911 696*1d05cddcSAtari911 $start1 = $evt1['time']; 697*1d05cddcSAtari911 $end1 = isset($evt1['endTime']) && !empty($evt1['endTime']) ? $evt1['endTime'] : $evt1['time']; 698*1d05cddcSAtari911 699*1d05cddcSAtari911 $start2 = $evt2['time']; 700*1d05cddcSAtari911 $end2 = isset($evt2['endTime']) && !empty($evt2['endTime']) ? $evt2['endTime'] : $evt2['time']; 701*1d05cddcSAtari911 702*1d05cddcSAtari911 // Convert to minutes for easier comparison 703*1d05cddcSAtari911 $start1Mins = $this->timeToMinutes($start1); 704*1d05cddcSAtari911 $end1Mins = $this->timeToMinutes($end1); 705*1d05cddcSAtari911 $start2Mins = $this->timeToMinutes($start2); 706*1d05cddcSAtari911 $end2Mins = $this->timeToMinutes($end2); 707*1d05cddcSAtari911 708*1d05cddcSAtari911 // Check for overlap: start1 < end2 AND start2 < end1 709*1d05cddcSAtari911 return $start1Mins < $end2Mins && $start2Mins < $end1Mins; 710*1d05cddcSAtari911 } 711*1d05cddcSAtari911 712*1d05cddcSAtari911 /** 713*1d05cddcSAtari911 * Convert HH:MM time to minutes since midnight 714*1d05cddcSAtari911 */ 715*1d05cddcSAtari911 private function timeToMinutes($timeStr) { 716*1d05cddcSAtari911 $parts = explode(':', $timeStr); 717*1d05cddcSAtari911 if (count($parts) !== 2) return 0; 718*1d05cddcSAtari911 719*1d05cddcSAtari911 return (int)$parts[0] * 60 + (int)$parts[1]; 720*1d05cddcSAtari911 } 721*1d05cddcSAtari911 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 759*1d05cddcSAtari911 $html = '<div class="event-panel-standalone" id="' . $calId . '" data-height="' . htmlspecialchars($height) . '" data-namespace="' . htmlspecialchars($namespace) . '" data-original-namespace="' . htmlspecialchars($namespace) . '">'; 76019378907SAtari911 761*1d05cddcSAtari911 // Load calendar JavaScript manually (not through DokuWiki concatenation) 762*1d05cddcSAtari911 $html .= '<script src="' . DOKU_BASE . 'lib/plugins/calendar/calendar-main.js"></script>'; 763*1d05cddcSAtari911 764*1d05cddcSAtari911 // Initialize DOKU_BASE for JavaScript 765*1d05cddcSAtari911 $html .= '<script>if(typeof DOKU_BASE==="undefined"){window.DOKU_BASE="' . DOKU_BASE . '";}</script>'; 766*1d05cddcSAtari911 767*1d05cddcSAtari911 // Compact two-row header designed for ~500px width 768*1d05cddcSAtari911 $html .= '<div class="panel-header-compact">'; 769*1d05cddcSAtari911 770*1d05cddcSAtari911 // Row 1: Navigation and title 771*1d05cddcSAtari911 $html .= '<div class="panel-header-row-1">'; 772*1d05cddcSAtari911 $html .= '<button class="panel-nav-btn" onclick="navEventPanel(\'' . $calId . '\', ' . $prevYear . ', ' . $prevMonth . ', \'' . $namespace . '\')">‹</button>'; 773*1d05cddcSAtari911 774*1d05cddcSAtari911 // Compact month name (e.g. "Feb 2026" instead of "February 2026 Events") 775*1d05cddcSAtari911 $shortMonthName = date('M Y', mktime(0, 0, 0, $month, 1, $year)); 776*1d05cddcSAtari911 $html .= '<h3 class="panel-month-title" onclick="openMonthPickerPanel(\'' . $calId . '\', ' . $year . ', ' . $month . ', \'' . $namespace . '\')" title="Click to jump to month">' . $shortMonthName . '</h3>'; 777*1d05cddcSAtari911 778*1d05cddcSAtari911 $html .= '<button class="panel-nav-btn" onclick="navEventPanel(\'' . $calId . '\', ' . $nextYear . ', ' . $nextMonth . ', \'' . $namespace . '\')">›</button>'; 779*1d05cddcSAtari911 780*1d05cddcSAtari911 // Namespace badge (if applicable) 78187ac9bf3SAtari911 if ($namespace) { 782e3a9f44cSAtari911 if ($isMultiNamespace) { 783e3a9f44cSAtari911 if (strpos($namespace, '*') !== false) { 784*1d05cddcSAtari911 $html .= '<span class="panel-ns-badge" title="' . htmlspecialchars($namespace) . '">' . htmlspecialchars($namespace) . '</span>'; 785e3a9f44cSAtari911 } else { 786e3a9f44cSAtari911 $namespaceList = array_map('trim', explode(';', $namespace)); 787*1d05cddcSAtari911 $nsCount = count($namespaceList); 788*1d05cddcSAtari911 $html .= '<span class="panel-ns-badge" title="' . htmlspecialchars(implode(', ', $namespaceList)) . '">' . $nsCount . ' NS</span>'; 789e3a9f44cSAtari911 } 790e3a9f44cSAtari911 } else { 791*1d05cddcSAtari911 $isFiltering = ($namespace !== '*' && strpos($namespace, '*') === false && strpos($namespace, ';') === false); 792*1d05cddcSAtari911 if ($isFiltering) { 793*1d05cddcSAtari911 $html .= '<span class="panel-ns-badge filter-on" title="Filtering by ' . htmlspecialchars($namespace) . ' - click to clear" onclick="clearNamespaceFilterPanel(\'' . $calId . '\')">' . htmlspecialchars($namespace) . ' ✕</span>'; 794*1d05cddcSAtari911 } else { 795*1d05cddcSAtari911 $html .= '<span class="panel-ns-badge" title="' . htmlspecialchars($namespace) . '">' . htmlspecialchars($namespace) . '</span>'; 79687ac9bf3SAtari911 } 797e3a9f44cSAtari911 } 798*1d05cddcSAtari911 } 799*1d05cddcSAtari911 800*1d05cddcSAtari911 $html .= '<button class="panel-today-btn" onclick="jumpTodayPanel(\'' . $calId . '\', \'' . $namespace . '\')">Today</button>'; 80119378907SAtari911 $html .= '</div>'; 80219378907SAtari911 803*1d05cddcSAtari911 // Row 2: Search and add button 804*1d05cddcSAtari911 $html .= '<div class="panel-header-row-2">'; 805*1d05cddcSAtari911 $html .= '<div class="panel-search-box">'; 806*1d05cddcSAtari911 $html .= '<input type="text" class="panel-search-input" id="event-search-' . $calId . '" placeholder="Search events..." oninput="filterEvents(\'' . $calId . '\', this.value)">'; 807*1d05cddcSAtari911 $html .= '<button class="panel-search-clear" id="search-clear-' . $calId . '" onclick="clearEventSearch(\'' . $calId . '\')" style="display:none;">✕</button>'; 808*1d05cddcSAtari911 $html .= '</div>'; 809*1d05cddcSAtari911 $html .= '<button class="panel-add-btn" onclick="openAddEventPanel(\'' . $calId . '\', \'' . $namespace . '\')">+ Add</button>'; 810*1d05cddcSAtari911 $html .= '</div>'; 811*1d05cddcSAtari911 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']; 830*1d05cddcSAtari911 // If no namespace specified, show all namespaces 831*1d05cddcSAtari911 if (empty($namespace)) { 832*1d05cddcSAtari911 $namespace = '*'; 833*1d05cddcSAtari911 } 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; 839*1d05cddcSAtari911 $showchecked = isset($data['showchecked']) ? true : false; // New parameter 840*1d05cddcSAtari911 $noheader = isset($data['noheader']) ? true : false; // New parameter to hide header 84119378907SAtari911 842e3a9f44cSAtari911 // Handle "range" parameter - day, week, or month 843e3a9f44cSAtari911 if ($range === 'day') { 844*1d05cddcSAtari911 $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') { 848*1d05cddcSAtari911 $startDate = date('Y-m-d', strtotime('-30 days')); // Include past 30 days for past due tasks 849*1d05cddcSAtari911 $endDateTime = new DateTime(); 850e3a9f44cSAtari911 $endDateTime->modify('+7 days'); 851e3a9f44cSAtari911 $endDate = $endDateTime->format('Y-m-d'); 852e3a9f44cSAtari911 $headerText = 'This Week'; 853e3a9f44cSAtari911 } elseif ($range === 'month') { 854*1d05cddcSAtari911 $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 856*1d05cddcSAtari911 $dt = new DateTime(); 857e3a9f44cSAtari911 $headerText = $dt->format('F Y'); 858e3a9f44cSAtari911 } elseif ($sidebar) { 859*1d05cddcSAtari911 // NEW: Sidebar widget - load current week's events 860*1d05cddcSAtari911 $weekStart = date('Y-m-d', strtotime('monday this week')); 861*1d05cddcSAtari911 $weekEnd = date('Y-m-d', strtotime('sunday this week')); 862*1d05cddcSAtari911 863*1d05cddcSAtari911 // Load events for the entire week 864*1d05cddcSAtari911 $start = new DateTime($weekStart); 865*1d05cddcSAtari911 $end = new DateTime($weekEnd); 866*1d05cddcSAtari911 $end->modify('+1 day'); // DatePeriod excludes end date 867*1d05cddcSAtari911 $interval = new DateInterval('P1D'); 868*1d05cddcSAtari911 $period = new DatePeriod($start, $interval, $end); 869*1d05cddcSAtari911 870*1d05cddcSAtari911 $isMultiNamespace = !empty($namespace) && (strpos($namespace, ';') !== false || strpos($namespace, '*') !== false); 871*1d05cddcSAtari911 $allEvents = []; 872*1d05cddcSAtari911 $loadedMonths = []; 873*1d05cddcSAtari911 874*1d05cddcSAtari911 foreach ($period as $dt) { 875*1d05cddcSAtari911 $year = (int)$dt->format('Y'); 876*1d05cddcSAtari911 $month = (int)$dt->format('n'); 877*1d05cddcSAtari911 $dateKey = $dt->format('Y-m-d'); 878*1d05cddcSAtari911 879*1d05cddcSAtari911 $monthKey = $year . '-' . $month . '-' . $namespace; 880*1d05cddcSAtari911 881*1d05cddcSAtari911 if (!isset($loadedMonths[$monthKey])) { 882*1d05cddcSAtari911 if ($isMultiNamespace) { 883*1d05cddcSAtari911 $loadedMonths[$monthKey] = $this->loadEventsMultiNamespace($namespace, $year, $month); 884*1d05cddcSAtari911 } else { 885*1d05cddcSAtari911 $loadedMonths[$monthKey] = $this->loadEvents($namespace, $year, $month); 886*1d05cddcSAtari911 } 887*1d05cddcSAtari911 } 888*1d05cddcSAtari911 889*1d05cddcSAtari911 $monthEvents = $loadedMonths[$monthKey]; 890*1d05cddcSAtari911 891*1d05cddcSAtari911 if (isset($monthEvents[$dateKey]) && !empty($monthEvents[$dateKey])) { 892*1d05cddcSAtari911 $allEvents[$dateKey] = $monthEvents[$dateKey]; 893*1d05cddcSAtari911 } 894*1d05cddcSAtari911 } 895*1d05cddcSAtari911 896*1d05cddcSAtari911 // Apply time conflict detection 897*1d05cddcSAtari911 $allEvents = $this->checkTimeConflicts($allEvents); 898*1d05cddcSAtari911 899*1d05cddcSAtari911 $calId = 'sidebar-' . substr(md5($namespace . $weekStart), 0, 8); 900*1d05cddcSAtari911 901*1d05cddcSAtari911 // Render sidebar widget and return immediately 902*1d05cddcSAtari911 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 960*1d05cddcSAtari911 // Sort events by date (already sorted by dateKey), then by time within each day 961*1d05cddcSAtari911 foreach ($allEvents as $dateKey => &$dayEvents) { 962*1d05cddcSAtari911 usort($dayEvents, function($a, $b) { 963*1d05cddcSAtari911 $timeA = isset($a['time']) && !empty($a['time']) ? $a['time'] : null; 964*1d05cddcSAtari911 $timeB = isset($b['time']) && !empty($b['time']) ? $b['time'] : null; 965*1d05cddcSAtari911 966*1d05cddcSAtari911 // All-day events (no time) go to the TOP 967*1d05cddcSAtari911 if ($timeA === null && $timeB !== null) return -1; // A before B 968*1d05cddcSAtari911 if ($timeA !== null && $timeB === null) return 1; // A after B 969*1d05cddcSAtari911 if ($timeA === null && $timeB === null) return 0; // Both all-day, equal 970*1d05cddcSAtari911 971*1d05cddcSAtari911 // Both have times, sort chronologically 972*1d05cddcSAtari911 return strcmp($timeA, $timeB); 973*1d05cddcSAtari911 }); 974*1d05cddcSAtari911 } 975*1d05cddcSAtari911 unset($dayEvents); // Break reference 976*1d05cddcSAtari911 977e3a9f44cSAtari911 // Simple 2-line display widget 978*1d05cddcSAtari911 $calId = 'eventlist_' . uniqid(); 979*1d05cddcSAtari911 $html = '<div class="eventlist-simple" id="' . $calId . '">'; 980*1d05cddcSAtari911 981*1d05cddcSAtari911 // Load calendar JavaScript manually (not through DokuWiki concatenation) 982*1d05cddcSAtari911 $html .= '<script src="' . DOKU_BASE . 'lib/plugins/calendar/calendar-main.js"></script>'; 983*1d05cddcSAtari911 984*1d05cddcSAtari911 // Initialize DOKU_BASE for JavaScript 985*1d05cddcSAtari911 $html .= '<script>if(typeof DOKU_BASE==="undefined"){window.DOKU_BASE="' . DOKU_BASE . '";}</script>'; 986*1d05cddcSAtari911 987*1d05cddcSAtari911 // Add compact header with date and clock for "today" mode (unless noheader is set) 988*1d05cddcSAtari911 if ($today && !empty($allEvents) && !$noheader) { 989*1d05cddcSAtari911 $todayDate = new DateTime(); 990*1d05cddcSAtari911 $displayDate = $todayDate->format('D, M j, Y'); // "Fri, Jan 30, 2026" 991*1d05cddcSAtari911 $currentTime = $todayDate->format('g:i:s A'); // "2:45:30 PM" 992*1d05cddcSAtari911 993*1d05cddcSAtari911 $html .= '<div class="eventlist-today-header">'; 994*1d05cddcSAtari911 $html .= '<span class="eventlist-today-clock" id="clock-' . $calId . '">' . $currentTime . '</span>'; 995*1d05cddcSAtari911 $html .= '<div class="eventlist-bottom-info">'; 996*1d05cddcSAtari911 $html .= '<span class="eventlist-weather"><span id="weather-icon-' . $calId . '">️</span> <span id="weather-temp-' . $calId . '">--°</span></span>'; 997*1d05cddcSAtari911 $html .= '<span class="eventlist-today-date">' . $displayDate . '</span>'; 998*1d05cddcSAtari911 $html .= '</div>'; 999*1d05cddcSAtari911 1000*1d05cddcSAtari911 // Three CPU/Memory bars (all update live) 1001*1d05cddcSAtari911 $html .= '<div class="eventlist-stats-container">'; 1002*1d05cddcSAtari911 1003*1d05cddcSAtari911 // 5-minute load average (green, updates every 2 seconds) 1004*1d05cddcSAtari911 $html .= '<div class="eventlist-cpu-bar" onmouseover="showTooltip_' . $calId . '(\'green\')" onmouseout="hideTooltip_' . $calId . '(\'green\')">'; 1005*1d05cddcSAtari911 $html .= '<div class="eventlist-cpu-fill" id="cpu-5min-' . $calId . '" style="width: 0%;"></div>'; 1006*1d05cddcSAtari911 $html .= '<div class="system-tooltip" id="tooltip-green-' . $calId . '" style="display:none;"></div>'; 1007*1d05cddcSAtari911 $html .= '</div>'; 1008*1d05cddcSAtari911 1009*1d05cddcSAtari911 // Real-time CPU (purple, updates with 5-sec average) 1010*1d05cddcSAtari911 $html .= '<div class="eventlist-cpu-bar eventlist-cpu-realtime" onmouseover="showTooltip_' . $calId . '(\'purple\')" onmouseout="hideTooltip_' . $calId . '(\'purple\')">'; 1011*1d05cddcSAtari911 $html .= '<div class="eventlist-cpu-fill eventlist-cpu-fill-purple" id="cpu-realtime-' . $calId . '" style="width: 0%;"></div>'; 1012*1d05cddcSAtari911 $html .= '<div class="system-tooltip" id="tooltip-purple-' . $calId . '" style="display:none;"></div>'; 1013*1d05cddcSAtari911 $html .= '</div>'; 1014*1d05cddcSAtari911 1015*1d05cddcSAtari911 // Real-time Memory (orange, updates) 1016*1d05cddcSAtari911 $html .= '<div class="eventlist-cpu-bar eventlist-mem-realtime" onmouseover="showTooltip_' . $calId . '(\'orange\')" onmouseout="hideTooltip_' . $calId . '(\'orange\')">'; 1017*1d05cddcSAtari911 $html .= '<div class="eventlist-cpu-fill eventlist-cpu-fill-orange" id="mem-realtime-' . $calId . '" style="width: 0%;"></div>'; 1018*1d05cddcSAtari911 $html .= '<div class="system-tooltip" id="tooltip-orange-' . $calId . '" style="display:none;"></div>'; 1019*1d05cddcSAtari911 $html .= '</div>'; 1020*1d05cddcSAtari911 1021*1d05cddcSAtari911 $html .= '</div>'; 1022*1d05cddcSAtari911 $html .= '</div>'; 1023*1d05cddcSAtari911 1024*1d05cddcSAtari911 // Add JavaScript to update clock and weather 1025*1d05cddcSAtari911 $html .= '<script> 1026*1d05cddcSAtari911(function() { 1027*1d05cddcSAtari911 // Update clock every second 1028*1d05cddcSAtari911 function updateClock() { 1029*1d05cddcSAtari911 const now = new Date(); 1030*1d05cddcSAtari911 let hours = now.getHours(); 1031*1d05cddcSAtari911 const minutes = String(now.getMinutes()).padStart(2, "0"); 1032*1d05cddcSAtari911 const seconds = String(now.getSeconds()).padStart(2, "0"); 1033*1d05cddcSAtari911 const ampm = hours >= 12 ? "PM" : "AM"; 1034*1d05cddcSAtari911 hours = hours % 12 || 12; 1035*1d05cddcSAtari911 const timeStr = hours + ":" + minutes + ":" + seconds + " " + ampm; 1036*1d05cddcSAtari911 const clockEl = document.getElementById("clock-' . $calId . '"); 1037*1d05cddcSAtari911 if (clockEl) clockEl.textContent = timeStr; 1038*1d05cddcSAtari911 } 1039*1d05cddcSAtari911 setInterval(updateClock, 1000); 1040*1d05cddcSAtari911 1041*1d05cddcSAtari911 // Fetch weather (geolocation-based) 1042*1d05cddcSAtari911 function updateWeather() { 1043*1d05cddcSAtari911 if ("geolocation" in navigator) { 1044*1d05cddcSAtari911 navigator.geolocation.getCurrentPosition(function(position) { 1045*1d05cddcSAtari911 const lat = position.coords.latitude; 1046*1d05cddcSAtari911 const lon = position.coords.longitude; 1047*1d05cddcSAtari911 1048*1d05cddcSAtari911 // Use Open-Meteo API (free, no key required) 1049*1d05cddcSAtari911 fetch(`https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}¤t_weather=true&temperature_unit=fahrenheit`) 1050*1d05cddcSAtari911 .then(response => response.json()) 1051*1d05cddcSAtari911 .then(data => { 1052*1d05cddcSAtari911 if (data.current_weather) { 1053*1d05cddcSAtari911 const temp = Math.round(data.current_weather.temperature); 1054*1d05cddcSAtari911 const weatherCode = data.current_weather.weathercode; 1055*1d05cddcSAtari911 const icon = getWeatherIcon(weatherCode); 1056*1d05cddcSAtari911 const iconEl = document.getElementById("weather-icon-' . $calId . '"); 1057*1d05cddcSAtari911 const tempEl = document.getElementById("weather-temp-' . $calId . '"); 1058*1d05cddcSAtari911 if (iconEl) iconEl.textContent = icon; 1059*1d05cddcSAtari911 if (tempEl) tempEl.innerHTML = temp + "°"; 1060*1d05cddcSAtari911 } 1061*1d05cddcSAtari911 }) 1062*1d05cddcSAtari911 .catch(error => { 1063*1d05cddcSAtari911 console.log("Weather fetch error:", error); 1064*1d05cddcSAtari911 }); 1065*1d05cddcSAtari911 }, function(error) { 1066*1d05cddcSAtari911 // If geolocation fails, use Sacramento as default 1067*1d05cddcSAtari911 fetch("https://api.open-meteo.com/v1/forecast?latitude=38.5816&longitude=-121.4944¤t_weather=true&temperature_unit=fahrenheit") 1068*1d05cddcSAtari911 .then(response => response.json()) 1069*1d05cddcSAtari911 .then(data => { 1070*1d05cddcSAtari911 if (data.current_weather) { 1071*1d05cddcSAtari911 const temp = Math.round(data.current_weather.temperature); 1072*1d05cddcSAtari911 const weatherCode = data.current_weather.weathercode; 1073*1d05cddcSAtari911 const icon = getWeatherIcon(weatherCode); 1074*1d05cddcSAtari911 const iconEl = document.getElementById("weather-icon-' . $calId . '"); 1075*1d05cddcSAtari911 const tempEl = document.getElementById("weather-temp-' . $calId . '"); 1076*1d05cddcSAtari911 if (iconEl) iconEl.textContent = icon; 1077*1d05cddcSAtari911 if (tempEl) tempEl.innerHTML = temp + "°"; 1078*1d05cddcSAtari911 } 1079*1d05cddcSAtari911 }) 1080*1d05cddcSAtari911 .catch(err => console.log("Weather error:", err)); 1081*1d05cddcSAtari911 }); 1082*1d05cddcSAtari911 } else { 1083*1d05cddcSAtari911 // No geolocation, use Sacramento 1084*1d05cddcSAtari911 fetch("https://api.open-meteo.com/v1/forecast?latitude=38.5816&longitude=-121.4944¤t_weather=true&temperature_unit=fahrenheit") 1085*1d05cddcSAtari911 .then(response => response.json()) 1086*1d05cddcSAtari911 .then(data => { 1087*1d05cddcSAtari911 if (data.current_weather) { 1088*1d05cddcSAtari911 const temp = Math.round(data.current_weather.temperature); 1089*1d05cddcSAtari911 const weatherCode = data.current_weather.weathercode; 1090*1d05cddcSAtari911 const icon = getWeatherIcon(weatherCode); 1091*1d05cddcSAtari911 const iconEl = document.getElementById("weather-icon-' . $calId . '"); 1092*1d05cddcSAtari911 const tempEl = document.getElementById("weather-temp-' . $calId . '"); 1093*1d05cddcSAtari911 if (iconEl) iconEl.textContent = icon; 1094*1d05cddcSAtari911 if (tempEl) tempEl.innerHTML = temp + "°"; 1095*1d05cddcSAtari911 } 1096*1d05cddcSAtari911 }) 1097*1d05cddcSAtari911 .catch(err => console.log("Weather error:", err)); 1098*1d05cddcSAtari911 } 1099*1d05cddcSAtari911 } 1100*1d05cddcSAtari911 1101*1d05cddcSAtari911 // WMO Weather interpretation codes 1102*1d05cddcSAtari911 function getWeatherIcon(code) { 1103*1d05cddcSAtari911 const icons = { 1104*1d05cddcSAtari911 0: "☀️", // Clear sky 1105*1d05cddcSAtari911 1: "️", // Mainly clear 1106*1d05cddcSAtari911 2: "⛅", // Partly cloudy 1107*1d05cddcSAtari911 3: "☁️", // Overcast 1108*1d05cddcSAtari911 45: "️", // Fog 1109*1d05cddcSAtari911 48: "️", // Depositing rime fog 1110*1d05cddcSAtari911 51: "️", // Light drizzle 1111*1d05cddcSAtari911 53: "️", // Moderate drizzle 1112*1d05cddcSAtari911 55: "️", // Dense drizzle 1113*1d05cddcSAtari911 61: "️", // Slight rain 1114*1d05cddcSAtari911 63: "️", // Moderate rain 1115*1d05cddcSAtari911 65: "⛈️", // Heavy rain 1116*1d05cddcSAtari911 71: "️", // Slight snow 1117*1d05cddcSAtari911 73: "️", // Moderate snow 1118*1d05cddcSAtari911 75: "❄️", // Heavy snow 1119*1d05cddcSAtari911 77: "️", // Snow grains 1120*1d05cddcSAtari911 80: "️", // Slight rain showers 1121*1d05cddcSAtari911 81: "️", // Moderate rain showers 1122*1d05cddcSAtari911 82: "⛈️", // Violent rain showers 1123*1d05cddcSAtari911 85: "️", // Slight snow showers 1124*1d05cddcSAtari911 86: "❄️", // Heavy snow showers 1125*1d05cddcSAtari911 95: "⛈️", // Thunderstorm 1126*1d05cddcSAtari911 96: "⛈️", // Thunderstorm with slight hail 1127*1d05cddcSAtari911 99: "⛈️" // Thunderstorm with heavy hail 1128*1d05cddcSAtari911 }; 1129*1d05cddcSAtari911 return icons[code] || "️"; 1130*1d05cddcSAtari911 } 1131*1d05cddcSAtari911 1132*1d05cddcSAtari911 // Update weather immediately and every 10 minutes 1133*1d05cddcSAtari911 updateWeather(); 1134*1d05cddcSAtari911 setInterval(updateWeather, 600000); 1135*1d05cddcSAtari911 1136*1d05cddcSAtari911 // CPU load history for 4-second rolling average 1137*1d05cddcSAtari911 const cpuHistory = []; 1138*1d05cddcSAtari911 const CPU_HISTORY_SIZE = 2; // 2 samples × 2 seconds = 4 seconds 1139*1d05cddcSAtari911 1140*1d05cddcSAtari911 // Store latest system stats for tooltips 1141*1d05cddcSAtari911 let latestStats = { 1142*1d05cddcSAtari911 load: {"1min": 0, "5min": 0, "15min": 0}, 1143*1d05cddcSAtari911 uptime: "", 1144*1d05cddcSAtari911 memory_details: {}, 1145*1d05cddcSAtari911 top_processes: [] 1146*1d05cddcSAtari911 }; 1147*1d05cddcSAtari911 1148*1d05cddcSAtari911 // Tooltip functions 1149*1d05cddcSAtari911 window["showTooltip_' . $calId . '"] = function(color) { 1150*1d05cddcSAtari911 const tooltip = document.getElementById("tooltip-" + color + "-' . $calId . '"); 1151*1d05cddcSAtari911 if (!tooltip) { 1152*1d05cddcSAtari911 console.log("Tooltip element not found for color:", color); 1153*1d05cddcSAtari911 return; 1154*1d05cddcSAtari911 } 1155*1d05cddcSAtari911 1156*1d05cddcSAtari911 console.log("Showing tooltip for:", color, "latestStats:", latestStats); 1157*1d05cddcSAtari911 1158*1d05cddcSAtari911 let content = ""; 1159*1d05cddcSAtari911 1160*1d05cddcSAtari911 if (color === "green") { 1161*1d05cddcSAtari911 // Green bar: Load averages and uptime 1162*1d05cddcSAtari911 content = "<div class=\"tooltip-title\">CPU Load Average</div>"; 1163*1d05cddcSAtari911 content += "<div>1 min: " + (latestStats.load["1min"] || "N/A") + "</div>"; 1164*1d05cddcSAtari911 content += "<div>5 min: " + (latestStats.load["5min"] || "N/A") + "</div>"; 1165*1d05cddcSAtari911 content += "<div>15 min: " + (latestStats.load["15min"] || "N/A") + "</div>"; 1166*1d05cddcSAtari911 if (latestStats.uptime) { 1167*1d05cddcSAtari911 content += "<div style=\"margin-top:3px; padding-top:2px; border-top:1px solid rgba(0,204,7,0.3);\">Uptime: " + latestStats.uptime + "</div>"; 1168*1d05cddcSAtari911 } 1169*1d05cddcSAtari911 tooltip.style.borderColor = "#00cc07"; 1170*1d05cddcSAtari911 tooltip.style.color = "#00cc07"; 1171*1d05cddcSAtari911 } else if (color === "purple") { 1172*1d05cddcSAtari911 // Purple bar: Load averages (short-term) and top processes 1173*1d05cddcSAtari911 content = "<div class=\"tooltip-title\">CPU Load (Short-term)</div>"; 1174*1d05cddcSAtari911 content += "<div>1 min: " + (latestStats.load["1min"] || "N/A") + "</div>"; 1175*1d05cddcSAtari911 content += "<div>5 min: " + (latestStats.load["5min"] || "N/A") + "</div>"; 1176*1d05cddcSAtari911 if (latestStats.top_processes && latestStats.top_processes.length > 0) { 1177*1d05cddcSAtari911 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>"; 1178*1d05cddcSAtari911 latestStats.top_processes.slice(0, 5).forEach(proc => { 1179*1d05cddcSAtari911 content += "<div>" + proc.cpu + " " + proc.command + "</div>"; 1180*1d05cddcSAtari911 }); 1181*1d05cddcSAtari911 } 1182*1d05cddcSAtari911 tooltip.style.borderColor = "#9b59b6"; 1183*1d05cddcSAtari911 tooltip.style.color = "#9b59b6"; 1184*1d05cddcSAtari911 } else if (color === "orange") { 1185*1d05cddcSAtari911 // Orange bar: Memory details and top processes 1186*1d05cddcSAtari911 content = "<div class=\"tooltip-title\">Memory Usage</div>"; 1187*1d05cddcSAtari911 if (latestStats.memory_details && latestStats.memory_details.total) { 1188*1d05cddcSAtari911 content += "<div>Total: " + latestStats.memory_details.total + "</div>"; 1189*1d05cddcSAtari911 content += "<div>Used: " + latestStats.memory_details.used + "</div>"; 1190*1d05cddcSAtari911 content += "<div>Available: " + latestStats.memory_details.available + "</div>"; 1191*1d05cddcSAtari911 if (latestStats.memory_details.cached) { 1192*1d05cddcSAtari911 content += "<div>Cached: " + latestStats.memory_details.cached + "</div>"; 1193*1d05cddcSAtari911 } 1194*1d05cddcSAtari911 } else { 1195*1d05cddcSAtari911 content += "<div>Loading...</div>"; 1196*1d05cddcSAtari911 } 1197*1d05cddcSAtari911 if (latestStats.top_processes && latestStats.top_processes.length > 0) { 1198*1d05cddcSAtari911 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>"; 1199*1d05cddcSAtari911 latestStats.top_processes.slice(0, 5).forEach(proc => { 1200*1d05cddcSAtari911 content += "<div>" + proc.cpu + " " + proc.command + "</div>"; 1201*1d05cddcSAtari911 }); 1202*1d05cddcSAtari911 } 1203*1d05cddcSAtari911 tooltip.style.borderColor = "#ff9800"; 1204*1d05cddcSAtari911 tooltip.style.color = "#ff9800"; 1205*1d05cddcSAtari911 } 1206*1d05cddcSAtari911 1207*1d05cddcSAtari911 console.log("Tooltip content:", content); 1208*1d05cddcSAtari911 tooltip.innerHTML = content; 1209*1d05cddcSAtari911 tooltip.style.display = "block"; 1210*1d05cddcSAtari911 1211*1d05cddcSAtari911 // Position tooltip using fixed positioning above the bar 1212*1d05cddcSAtari911 const bar = tooltip.parentElement; 1213*1d05cddcSAtari911 const barRect = bar.getBoundingClientRect(); 1214*1d05cddcSAtari911 const tooltipRect = tooltip.getBoundingClientRect(); 1215*1d05cddcSAtari911 1216*1d05cddcSAtari911 // Center horizontally on the bar 1217*1d05cddcSAtari911 const left = barRect.left + (barRect.width / 2) - (tooltipRect.width / 2); 1218*1d05cddcSAtari911 // Position above the bar with 8px gap 1219*1d05cddcSAtari911 const top = barRect.top - tooltipRect.height - 8; 1220*1d05cddcSAtari911 1221*1d05cddcSAtari911 tooltip.style.left = left + "px"; 1222*1d05cddcSAtari911 tooltip.style.top = top + "px"; 1223*1d05cddcSAtari911 }; 1224*1d05cddcSAtari911 1225*1d05cddcSAtari911 window["hideTooltip_' . $calId . '"] = function(color) { 1226*1d05cddcSAtari911 const tooltip = document.getElementById("tooltip-" + color + "-' . $calId . '"); 1227*1d05cddcSAtari911 if (tooltip) { 1228*1d05cddcSAtari911 tooltip.style.display = "none"; 1229*1d05cddcSAtari911 } 1230*1d05cddcSAtari911 }; 1231*1d05cddcSAtari911 1232*1d05cddcSAtari911 // Update CPU and memory bars every 2 seconds 1233*1d05cddcSAtari911 function updateSystemStats() { 1234*1d05cddcSAtari911 // Fetch real system stats from server 1235*1d05cddcSAtari911 fetch("' . DOKU_BASE . 'lib/plugins/calendar/get_system_stats.php") 1236*1d05cddcSAtari911 .then(response => response.json()) 1237*1d05cddcSAtari911 .then(data => { 1238*1d05cddcSAtari911 console.log("System stats received:", data); 1239*1d05cddcSAtari911 1240*1d05cddcSAtari911 // Store data for tooltips 1241*1d05cddcSAtari911 latestStats = { 1242*1d05cddcSAtari911 load: data.load || {"1min": 0, "5min": 0, "15min": 0}, 1243*1d05cddcSAtari911 uptime: data.uptime || "", 1244*1d05cddcSAtari911 memory_details: data.memory_details || {}, 1245*1d05cddcSAtari911 top_processes: data.top_processes || [] 1246*1d05cddcSAtari911 }; 1247*1d05cddcSAtari911 1248*1d05cddcSAtari911 console.log("latestStats updated to:", latestStats); 1249*1d05cddcSAtari911 1250*1d05cddcSAtari911 // Update green bar (5-minute average) - updates live now! 1251*1d05cddcSAtari911 const greenBar = document.getElementById("cpu-5min-' . $calId . '"); 1252*1d05cddcSAtari911 if (greenBar) { 1253*1d05cddcSAtari911 greenBar.style.width = Math.min(100, data.cpu_5min) + "%"; 1254*1d05cddcSAtari911 } 1255*1d05cddcSAtari911 1256*1d05cddcSAtari911 // Add current CPU to history for purple bar 1257*1d05cddcSAtari911 cpuHistory.push(data.cpu); 1258*1d05cddcSAtari911 if (cpuHistory.length > CPU_HISTORY_SIZE) { 1259*1d05cddcSAtari911 cpuHistory.shift(); // Remove oldest 1260*1d05cddcSAtari911 } 1261*1d05cddcSAtari911 1262*1d05cddcSAtari911 // Calculate 5-second average for CPU 1263*1d05cddcSAtari911 const cpuAverage = cpuHistory.reduce((sum, val) => sum + val, 0) / cpuHistory.length; 1264*1d05cddcSAtari911 1265*1d05cddcSAtari911 // Update CPU bar (purple) with 5-second average 1266*1d05cddcSAtari911 const cpuBar = document.getElementById("cpu-realtime-' . $calId . '"); 1267*1d05cddcSAtari911 if (cpuBar) { 1268*1d05cddcSAtari911 cpuBar.style.width = Math.min(100, cpuAverage) + "%"; 1269*1d05cddcSAtari911 } 1270*1d05cddcSAtari911 1271*1d05cddcSAtari911 // Update memory bar (orange) with real data 1272*1d05cddcSAtari911 const memBar = document.getElementById("mem-realtime-' . $calId . '"); 1273*1d05cddcSAtari911 if (memBar) { 1274*1d05cddcSAtari911 memBar.style.width = Math.min(100, data.memory) + "%"; 1275*1d05cddcSAtari911 } 1276*1d05cddcSAtari911 }) 1277*1d05cddcSAtari911 .catch(error => { 1278*1d05cddcSAtari911 console.log("System stats error:", error); 1279*1d05cddcSAtari911 // Fallback to client-side estimates on error 1280*1d05cddcSAtari911 const cpuFallback = Math.random() * 100; 1281*1d05cddcSAtari911 cpuHistory.push(cpuFallback); 1282*1d05cddcSAtari911 if (cpuHistory.length > CPU_HISTORY_SIZE) { 1283*1d05cddcSAtari911 cpuHistory.shift(); 1284*1d05cddcSAtari911 } 1285*1d05cddcSAtari911 const cpuAverage = cpuHistory.reduce((sum, val) => sum + val, 0) / cpuHistory.length; 1286*1d05cddcSAtari911 1287*1d05cddcSAtari911 const greenBar = document.getElementById("cpu-5min-' . $calId . '"); 1288*1d05cddcSAtari911 if (greenBar) greenBar.style.width = Math.min(100, cpuFallback) + "%"; 1289*1d05cddcSAtari911 1290*1d05cddcSAtari911 const cpuBar = document.getElementById("cpu-realtime-' . $calId . '"); 1291*1d05cddcSAtari911 if (cpuBar) cpuBar.style.width = Math.min(100, cpuAverage) + "%"; 1292*1d05cddcSAtari911 1293*1d05cddcSAtari911 let memoryUsage = 0; 1294*1d05cddcSAtari911 if (performance.memory) { 1295*1d05cddcSAtari911 memoryUsage = (performance.memory.usedJSHeapSize / performance.memory.jsHeapSizeLimit) * 100; 1296*1d05cddcSAtari911 } else { 1297*1d05cddcSAtari911 memoryUsage = Math.random() * 100; 1298*1d05cddcSAtari911 } 1299*1d05cddcSAtari911 const memBar = document.getElementById("mem-realtime-' . $calId . '"); 1300*1d05cddcSAtari911 if (memBar) memBar.style.width = Math.min(100, memoryUsage) + "%"; 1301*1d05cddcSAtari911 }); 1302*1d05cddcSAtari911 } 1303*1d05cddcSAtari911 1304*1d05cddcSAtari911 // Update immediately and then every 2 seconds 1305*1d05cddcSAtari911 updateSystemStats(); 1306*1d05cddcSAtari911 setInterval(updateSystemStats, 2000); 1307*1d05cddcSAtari911})(); 1308*1d05cddcSAtari911</script>'; 1309*1d05cddcSAtari911 } 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 1322*1d05cddcSAtari911 $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 1329*1d05cddcSAtari911 // 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); 1332*1d05cddcSAtari911 $isToday = $enableHighlighting && ($dateKey === $todayStr); 1333e3a9f44cSAtari911 $isTomorrow = $enableHighlighting && ($dateKey === $tomorrow); 1334*1d05cddcSAtari911 $isPast = $dateKey < $todayStr; 133519378907SAtari911 133619378907SAtari911 foreach ($dayEvents as $event) { 1337*1d05cddcSAtari911 // Check if this is a task and if it's completed 1338*1d05cddcSAtari911 $isTask = !empty($event['isTask']); 1339*1d05cddcSAtari911 $completed = !empty($event['completed']); 1340*1d05cddcSAtari911 1341*1d05cddcSAtari911 // ALWAYS skip completed tasks UNLESS showchecked is explicitly set 1342*1d05cddcSAtari911 if (!$showchecked && $isTask && $completed) { 1343e3a9f44cSAtari911 continue; 1344e3a9f44cSAtari911 } 134519378907SAtari911 1346*1d05cddcSAtari911 // Skip past events that are NOT tasks (only show past due tasks from the past) 1347*1d05cddcSAtari911 if ($isPast && !$isTask) { 1348*1d05cddcSAtari911 continue; 1349*1d05cddcSAtari911 } 1350*1d05cddcSAtari911 1351*1d05cddcSAtari911 // Determine if task is past due (past date, is task, not completed) 1352*1d05cddcSAtari911 $isPastDue = $isPast && $isTask && !$completed; 1353*1d05cddcSAtari911 1354e3a9f44cSAtari911 // Line 1: Header (Title, Time, Date, Namespace) 1355e3a9f44cSAtari911 $todayClass = $isToday ? ' eventlist-simple-today' : ''; 1356e3a9f44cSAtari911 $tomorrowClass = $isTomorrow ? ' eventlist-simple-tomorrow' : ''; 1357*1d05cddcSAtari911 $pastDueClass = $isPastDue ? ' eventlist-simple-pastdue' : ''; 1358*1d05cddcSAtari911 $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 1380*1d05cddcSAtari911 // Badge: PAST DUE, TODAY, or nothing 1381*1d05cddcSAtari911 if ($isPastDue) { 1382*1d05cddcSAtari911 $html .= ' <span class="eventlist-simple-pastdue-badge">PAST DUE</span>'; 1383*1d05cddcSAtari911 } 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 1432*1d05cddcSAtari911 // 1. TITLE 1433*1d05cddcSAtari911 $html .= '<div class="form-field">'; 1434*1d05cddcSAtari911 $html .= '<label class="field-label"> Title</label>'; 1435*1d05cddcSAtari911 $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 1438*1d05cddcSAtari911 // 1.5 NAMESPACE SELECTOR (Searchable with fuzzy matching) 1439*1d05cddcSAtari911 $html .= '<div class="form-field">'; 1440*1d05cddcSAtari911 $html .= '<label class="field-label"> Namespace</label>'; 1441*1d05cddcSAtari911 1442*1d05cddcSAtari911 // Hidden field to store actual selected namespace 1443*1d05cddcSAtari911 $html .= '<input type="hidden" id="event-namespace-' . $calId . '" name="namespace" value="">'; 1444*1d05cddcSAtari911 1445*1d05cddcSAtari911 // Searchable input 1446*1d05cddcSAtari911 $html .= '<div class="namespace-search-wrapper">'; 1447*1d05cddcSAtari911 $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">'; 1448*1d05cddcSAtari911 $html .= '<div class="namespace-dropdown" id="event-namespace-dropdown-' . $calId . '" style="display:none;"></div>'; 1449*1d05cddcSAtari911 $html .= '</div>'; 1450*1d05cddcSAtari911 1451*1d05cddcSAtari911 // Store namespaces as JSON for JavaScript 1452*1d05cddcSAtari911 $allNamespaces = $this->getAllNamespaces(); 1453*1d05cddcSAtari911 $html .= '<script type="application/json" id="namespaces-data-' . $calId . '">' . json_encode($allNamespaces) . '</script>'; 1454*1d05cddcSAtari911 1455*1d05cddcSAtari911 $html .= '</div>'; 1456*1d05cddcSAtari911 1457*1d05cddcSAtari911 // 2. DESCRIPTION 1458*1d05cddcSAtari911 $html .= '<div class="form-field">'; 1459*1d05cddcSAtari911 $html .= '<label class="field-label"> Description</label>'; 1460*1d05cddcSAtari911 $html .= '<textarea id="event-desc-' . $calId . '" name="description" rows="1" class="input-sleek textarea-sleek textarea-compact" placeholder="Optional details..."></textarea>'; 1461*1d05cddcSAtari911 $html .= '</div>'; 1462*1d05cddcSAtari911 1463*1d05cddcSAtari911 // 3. START DATE - END DATE (inline) 146419378907SAtari911 $html .= '<div class="form-row-group">'; 146519378907SAtari911 1466*1d05cddcSAtari911 $html .= '<div class="form-field form-field-half">'; 1467*1d05cddcSAtari911 $html .= '<label class="field-label-compact"> Start Date</label>'; 1468*1d05cddcSAtari911 $html .= '<input type="date" id="event-date-' . $calId . '" name="date" required class="input-sleek input-date input-compact">'; 146919378907SAtari911 $html .= '</div>'; 147019378907SAtari911 1471*1d05cddcSAtari911 $html .= '<div class="form-field form-field-half">'; 1472*1d05cddcSAtari911 $html .= '<label class="field-label-compact"> End Date</label>'; 1473*1d05cddcSAtari911 $html .= '<input type="date" id="event-end-date-' . $calId . '" name="endDate" class="input-sleek input-date input-compact" placeholder="Optional">'; 147419378907SAtari911 $html .= '</div>'; 147519378907SAtari911 1476*1d05cddcSAtari911 $html .= '</div>'; // End row 147719378907SAtari911 1478*1d05cddcSAtari911 // 4. IS REPEATING CHECKBOX 1479*1d05cddcSAtari911 $html .= '<div class="form-field form-field-checkbox form-field-checkbox-compact">'; 1480*1d05cddcSAtari911 $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 1486*1d05cddcSAtari911 // Recurring options (shown when checkbox is checked) 148787ac9bf3SAtari911 $html .= '<div id="recurring-options-' . $calId . '" class="recurring-options" style="display:none;">'; 148887ac9bf3SAtari911 1489*1d05cddcSAtari911 $html .= '<div class="form-row-group">'; 1490*1d05cddcSAtari911 1491*1d05cddcSAtari911 $html .= '<div class="form-field form-field-half">'; 1492*1d05cddcSAtari911 $html .= '<label class="field-label-compact">Repeat Every</label>'; 1493*1d05cddcSAtari911 $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 1501*1d05cddcSAtari911 $html .= '<div class="form-field form-field-half">'; 1502*1d05cddcSAtari911 $html .= '<label class="field-label-compact">Repeat Until</label>'; 1503*1d05cddcSAtari911 $html .= '<input type="date" id="event-recurrence-end-' . $calId . '" name="recurrenceEnd" class="input-sleek input-date input-compact" placeholder="Optional">'; 150487ac9bf3SAtari911 $html .= '</div>'; 150587ac9bf3SAtari911 1506*1d05cddcSAtari911 $html .= '</div>'; // End row 1507*1d05cddcSAtari911 $html .= '</div>'; // End recurring options 150887ac9bf3SAtari911 1509*1d05cddcSAtari911 // 5. TIME (Start & End) - COLOR (inline) 1510*1d05cddcSAtari911 $html .= '<div class="form-row-group">'; 1511*1d05cddcSAtari911 1512*1d05cddcSAtari911 $html .= '<div class="form-field form-field-half">'; 1513*1d05cddcSAtari911 $html .= '<label class="field-label-compact"> Start Time</label>'; 1514*1d05cddcSAtari911 $html .= '<select id="event-time-' . $calId . '" name="time" class="input-sleek input-compact" onchange="updateEndTimeOptions(\'' . $calId . '\')">'; 1515*1d05cddcSAtari911 $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 1531*1d05cddcSAtari911 $html .= '<div class="form-field form-field-half">'; 1532*1d05cddcSAtari911 $html .= '<label class="field-label-compact"> End Time</label>'; 1533*1d05cddcSAtari911 $html .= '<select id="event-end-time-' . $calId . '" name="endTime" class="input-sleek input-compact">'; 1534*1d05cddcSAtari911 $html .= '<option value="">Same as start</option>'; 1535*1d05cddcSAtari911 1536*1d05cddcSAtari911 // Generate time options in 15-minute intervals 1537*1d05cddcSAtari911 for ($hour = 0; $hour < 24; $hour++) { 1538*1d05cddcSAtari911 for ($minute = 0; $minute < 60; $minute += 15) { 1539*1d05cddcSAtari911 $timeValue = sprintf('%02d:%02d', $hour, $minute); 1540*1d05cddcSAtari911 $displayHour = $hour == 0 ? 12 : ($hour > 12 ? $hour - 12 : $hour); 1541*1d05cddcSAtari911 $ampm = $hour < 12 ? 'AM' : 'PM'; 1542*1d05cddcSAtari911 $displayTime = sprintf('%d:%02d %s', $displayHour, $minute, $ampm); 1543*1d05cddcSAtari911 $html .= '<option value="' . $timeValue . '">' . $displayTime . '</option>'; 1544*1d05cddcSAtari911 } 1545*1d05cddcSAtari911 } 1546*1d05cddcSAtari911 1547*1d05cddcSAtari911 $html .= '</select>'; 154819378907SAtari911 $html .= '</div>'; 154919378907SAtari911 1550*1d05cddcSAtari911 $html .= '</div>'; // End row 1551*1d05cddcSAtari911 1552*1d05cddcSAtari911 // Color field (new row) 1553*1d05cddcSAtari911 $html .= '<div class="form-row-group">'; 1554*1d05cddcSAtari911 1555*1d05cddcSAtari911 $html .= '<div class="form-field form-field-full">'; 1556*1d05cddcSAtari911 $html .= '<label class="field-label-compact"> Color</label>'; 1557*1d05cddcSAtari911 $html .= '<div class="color-picker-wrapper">'; 1558*1d05cddcSAtari911 $html .= '<select id="event-color-' . $calId . '" name="color" class="input-sleek input-compact color-select" onchange="updateCustomColorPicker(\'' . $calId . '\')">'; 1559*1d05cddcSAtari911 $html .= '<option value="#3498db" style="background:#3498db;color:white"> Blue</option>'; 1560*1d05cddcSAtari911 $html .= '<option value="#2ecc71" style="background:#2ecc71;color:white"> Green</option>'; 1561*1d05cddcSAtari911 $html .= '<option value="#e74c3c" style="background:#e74c3c;color:white"> Red</option>'; 1562*1d05cddcSAtari911 $html .= '<option value="#f39c12" style="background:#f39c12;color:white"> Orange</option>'; 1563*1d05cddcSAtari911 $html .= '<option value="#9b59b6" style="background:#9b59b6;color:white"> Purple</option>'; 1564*1d05cddcSAtari911 $html .= '<option value="#e91e63" style="background:#e91e63;color:white"> Pink</option>'; 1565*1d05cddcSAtari911 $html .= '<option value="#1abc9c" style="background:#1abc9c;color:white"> Teal</option>'; 1566*1d05cddcSAtari911 $html .= '<option value="custom"> Custom...</option>'; 1567*1d05cddcSAtari911 $html .= '</select>'; 1568*1d05cddcSAtari911 $html .= '<input type="color" id="event-color-custom-' . $calId . '" class="color-picker-input color-picker-compact" value="#3498db" onchange="updateColorFromPicker(\'' . $calId . '\')">'; 1569*1d05cddcSAtari911 $html .= '</div>'; 157019378907SAtari911 $html .= '</div>'; 157119378907SAtari911 1572*1d05cddcSAtari911 $html .= '</div>'; // End row 1573*1d05cddcSAtari911 1574*1d05cddcSAtari911 // Task checkbox 1575*1d05cddcSAtari911 $html .= '<div class="form-field form-field-checkbox form-field-checkbox-compact">'; 1576*1d05cddcSAtari911 $html .= '<label class="checkbox-label checkbox-label-compact">'; 1577*1d05cddcSAtari911 $html .= '<input type="checkbox" id="event-is-task-' . $calId . '" name="isTask" class="task-toggle">'; 1578*1d05cddcSAtari911 $html .= '<span> This is a task (can be checked off)</span>'; 1579*1d05cddcSAtari911 $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 } 1879*1d05cddcSAtari911 1880*1d05cddcSAtari911 private function getAllNamespaces() { 1881*1d05cddcSAtari911 $dataDir = DOKU_INC . 'data/meta/'; 1882*1d05cddcSAtari911 $namespaces = []; 1883*1d05cddcSAtari911 1884*1d05cddcSAtari911 // Scan for namespaces that have calendar data 1885*1d05cddcSAtari911 $this->scanForCalendarNamespaces($dataDir, '', $namespaces); 1886*1d05cddcSAtari911 1887*1d05cddcSAtari911 // Sort alphabetically 1888*1d05cddcSAtari911 sort($namespaces); 1889*1d05cddcSAtari911 1890*1d05cddcSAtari911 return $namespaces; 1891*1d05cddcSAtari911 } 1892*1d05cddcSAtari911 1893*1d05cddcSAtari911 private function scanForCalendarNamespaces($dir, $baseNamespace, &$namespaces) { 1894*1d05cddcSAtari911 if (!is_dir($dir)) return; 1895*1d05cddcSAtari911 1896*1d05cddcSAtari911 $items = scandir($dir); 1897*1d05cddcSAtari911 foreach ($items as $item) { 1898*1d05cddcSAtari911 if ($item === '.' || $item === '..') continue; 1899*1d05cddcSAtari911 1900*1d05cddcSAtari911 $path = $dir . $item; 1901*1d05cddcSAtari911 if (is_dir($path)) { 1902*1d05cddcSAtari911 // Check if this directory has a calendar subdirectory with data 1903*1d05cddcSAtari911 $calendarDir = $path . '/calendar/'; 1904*1d05cddcSAtari911 if (is_dir($calendarDir)) { 1905*1d05cddcSAtari911 // Check if there are any JSON files in the calendar directory 1906*1d05cddcSAtari911 $jsonFiles = glob($calendarDir . '*.json'); 1907*1d05cddcSAtari911 if (!empty($jsonFiles)) { 1908*1d05cddcSAtari911 // This namespace has calendar data 1909*1d05cddcSAtari911 $namespace = $baseNamespace ? $baseNamespace . ':' . $item : $item; 1910*1d05cddcSAtari911 $namespaces[] = $namespace; 1911*1d05cddcSAtari911 } 1912*1d05cddcSAtari911 } 1913*1d05cddcSAtari911 1914*1d05cddcSAtari911 // Recurse into subdirectories 1915*1d05cddcSAtari911 $namespace = $baseNamespace ? $baseNamespace . ':' . $item : $item; 1916*1d05cddcSAtari911 $this->scanForCalendarNamespaces($path . '/', $namespace, $namespaces); 1917*1d05cddcSAtari911 } 1918*1d05cddcSAtari911 } 1919*1d05cddcSAtari911 } 1920*1d05cddcSAtari911 1921*1d05cddcSAtari911 /** 1922*1d05cddcSAtari911 * Render new sidebar widget - Week at a glance itinerary (200px wide) 1923*1d05cddcSAtari911 */ 1924*1d05cddcSAtari911 private function renderSidebarWidget($events, $namespace, $calId) { 1925*1d05cddcSAtari911 if (empty($events)) { 1926*1d05cddcSAtari911 return '<div style="width:200px; padding:12px; text-align:center; color:#999; font-size:11px;">No events this week</div>'; 1927*1d05cddcSAtari911 } 1928*1d05cddcSAtari911 1929*1d05cddcSAtari911 // Get important namespaces from config 1930*1d05cddcSAtari911 $configFile = DOKU_PLUGIN . 'calendar/sync_config.php'; 1931*1d05cddcSAtari911 $importantNsList = ['important']; // default 1932*1d05cddcSAtari911 if (file_exists($configFile)) { 1933*1d05cddcSAtari911 $config = include $configFile; 1934*1d05cddcSAtari911 if (isset($config['important_namespaces']) && !empty($config['important_namespaces'])) { 1935*1d05cddcSAtari911 $importantNsList = array_map('trim', explode(',', $config['important_namespaces'])); 1936*1d05cddcSAtari911 } 1937*1d05cddcSAtari911 } 1938*1d05cddcSAtari911 1939*1d05cddcSAtari911 // Calculate date ranges 1940*1d05cddcSAtari911 $todayStr = date('Y-m-d'); 1941*1d05cddcSAtari911 $tomorrowStr = date('Y-m-d', strtotime('+1 day')); 1942*1d05cddcSAtari911 $weekStart = date('Y-m-d', strtotime('monday this week')); 1943*1d05cddcSAtari911 $weekEnd = date('Y-m-d', strtotime('sunday this week')); 1944*1d05cddcSAtari911 1945*1d05cddcSAtari911 // Group events by category 1946*1d05cddcSAtari911 $todayEvents = []; 1947*1d05cddcSAtari911 $tomorrowEvents = []; 1948*1d05cddcSAtari911 $importantEvents = []; 1949*1d05cddcSAtari911 $weekEvents = []; // For week grid 1950*1d05cddcSAtari911 1951*1d05cddcSAtari911 // Process all events 1952*1d05cddcSAtari911 foreach ($events as $dateKey => $dayEvents) { 1953*1d05cddcSAtari911 // Skip events before this week 1954*1d05cddcSAtari911 if ($dateKey < $weekStart) continue; 1955*1d05cddcSAtari911 1956*1d05cddcSAtari911 // Initialize week grid day if in current week 1957*1d05cddcSAtari911 if ($dateKey >= $weekStart && $dateKey <= $weekEnd) { 1958*1d05cddcSAtari911 if (!isset($weekEvents[$dateKey])) { 1959*1d05cddcSAtari911 $weekEvents[$dateKey] = []; 1960*1d05cddcSAtari911 } 1961*1d05cddcSAtari911 } 1962*1d05cddcSAtari911 1963*1d05cddcSAtari911 foreach ($dayEvents as $event) { 1964*1d05cddcSAtari911 // Add to week grid if in week range 1965*1d05cddcSAtari911 if ($dateKey >= $weekStart && $dateKey <= $weekEnd) { 1966*1d05cddcSAtari911 // Pre-render DokuWiki syntax to HTML for JavaScript display 1967*1d05cddcSAtari911 $eventWithHtml = $event; 1968*1d05cddcSAtari911 if (isset($event['title'])) { 1969*1d05cddcSAtari911 $eventWithHtml['title_html'] = $this->renderDokuWikiToHtml($event['title']); 1970*1d05cddcSAtari911 } 1971*1d05cddcSAtari911 if (isset($event['description'])) { 1972*1d05cddcSAtari911 $eventWithHtml['description_html'] = $this->renderDokuWikiToHtml($event['description']); 1973*1d05cddcSAtari911 } 1974*1d05cddcSAtari911 $weekEvents[$dateKey][] = $eventWithHtml; 1975*1d05cddcSAtari911 } 1976*1d05cddcSAtari911 1977*1d05cddcSAtari911 // Categorize for detailed sections 1978*1d05cddcSAtari911 if ($dateKey === $todayStr) { 1979*1d05cddcSAtari911 $todayEvents[] = array_merge($event, ['date' => $dateKey]); 1980*1d05cddcSAtari911 } elseif ($dateKey === $tomorrowStr) { 1981*1d05cddcSAtari911 $tomorrowEvents[] = array_merge($event, ['date' => $dateKey]); 1982*1d05cddcSAtari911 } else { 1983*1d05cddcSAtari911 // Check if this is an important namespace 1984*1d05cddcSAtari911 $eventNs = isset($event['namespace']) ? $event['namespace'] : ''; 1985*1d05cddcSAtari911 $isImportant = false; 1986*1d05cddcSAtari911 foreach ($importantNsList as $impNs) { 1987*1d05cddcSAtari911 if ($eventNs === $impNs || strpos($eventNs, $impNs . ':') === 0) { 1988*1d05cddcSAtari911 $isImportant = true; 1989*1d05cddcSAtari911 break; 1990*1d05cddcSAtari911 } 1991*1d05cddcSAtari911 } 1992*1d05cddcSAtari911 1993*1d05cddcSAtari911 // Important events: this week but not today/tomorrow 1994*1d05cddcSAtari911 if ($isImportant && $dateKey >= $weekStart && $dateKey <= $weekEnd) { 1995*1d05cddcSAtari911 $importantEvents[] = array_merge($event, ['date' => $dateKey]); 1996*1d05cddcSAtari911 } 1997*1d05cddcSAtari911 } 1998*1d05cddcSAtari911 } 1999*1d05cddcSAtari911 } 2000*1d05cddcSAtari911 2001*1d05cddcSAtari911 // Start building HTML - Dynamic width with default font 2002*1d05cddcSAtari911 $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);">'; 2003*1d05cddcSAtari911 2004*1d05cddcSAtari911 // Sanitize calId for use in JavaScript variable names (remove dashes) 2005*1d05cddcSAtari911 $jsCalId = str_replace('-', '_', $calId); 2006*1d05cddcSAtari911 2007*1d05cddcSAtari911 // CRITICAL: Add ALL JavaScript FIRST before any HTML that uses it 2008*1d05cddcSAtari911 $html .= '<script> 2009*1d05cddcSAtari911(function() { 2010*1d05cddcSAtari911 // Shared state for system stats and tooltips 2011*1d05cddcSAtari911 const sharedState_' . $jsCalId . ' = { 2012*1d05cddcSAtari911 latestStats: { 2013*1d05cddcSAtari911 load: {"1min": 0, "5min": 0, "15min": 0}, 2014*1d05cddcSAtari911 uptime: "", 2015*1d05cddcSAtari911 memory_details: {}, 2016*1d05cddcSAtari911 top_processes: [] 2017*1d05cddcSAtari911 }, 2018*1d05cddcSAtari911 cpuHistory: [], 2019*1d05cddcSAtari911 CPU_HISTORY_SIZE: 2 2020*1d05cddcSAtari911 }; 2021*1d05cddcSAtari911 2022*1d05cddcSAtari911 // Tooltip functions - MUST be defined before HTML uses them 2023*1d05cddcSAtari911 window["showTooltip_' . $jsCalId . '"] = function(color) { 2024*1d05cddcSAtari911 const tooltip = document.getElementById("tooltip-" + color + "-' . $calId . '"); 2025*1d05cddcSAtari911 if (!tooltip) { 2026*1d05cddcSAtari911 console.log("Tooltip element not found for color:", color); 2027*1d05cddcSAtari911 return; 2028*1d05cddcSAtari911 } 2029*1d05cddcSAtari911 2030*1d05cddcSAtari911 const latestStats = sharedState_' . $jsCalId . '.latestStats; 2031*1d05cddcSAtari911 let content = ""; 2032*1d05cddcSAtari911 2033*1d05cddcSAtari911 if (color === "green") { 2034*1d05cddcSAtari911 content = "<div class=\\"tooltip-title\\">CPU Load Average</div>"; 2035*1d05cddcSAtari911 content += "<div>1 min: " + (latestStats.load["1min"] || "N/A") + "</div>"; 2036*1d05cddcSAtari911 content += "<div>5 min: " + (latestStats.load["5min"] || "N/A") + "</div>"; 2037*1d05cddcSAtari911 content += "<div>15 min: " + (latestStats.load["15min"] || "N/A") + "</div>"; 2038*1d05cddcSAtari911 if (latestStats.uptime) { 2039*1d05cddcSAtari911 content += "<div style=\\"margin-top:3px; padding-top:2px; border-top:1px solid rgba(0,204,7,0.3);\\">Uptime: " + latestStats.uptime + "</div>"; 2040*1d05cddcSAtari911 } 2041*1d05cddcSAtari911 tooltip.style.borderColor = "#00cc07"; 2042*1d05cddcSAtari911 tooltip.style.color = "#00cc07"; 2043*1d05cddcSAtari911 } else if (color === "purple") { 2044*1d05cddcSAtari911 content = "<div class=\\"tooltip-title\\">CPU Load (Short-term)</div>"; 2045*1d05cddcSAtari911 content += "<div>1 min: " + (latestStats.load["1min"] || "N/A") + "</div>"; 2046*1d05cddcSAtari911 content += "<div>5 min: " + (latestStats.load["5min"] || "N/A") + "</div>"; 2047*1d05cddcSAtari911 if (latestStats.top_processes && latestStats.top_processes.length > 0) { 2048*1d05cddcSAtari911 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>"; 2049*1d05cddcSAtari911 latestStats.top_processes.slice(0, 5).forEach(proc => { 2050*1d05cddcSAtari911 content += "<div>" + proc.cpu + " " + proc.command + "</div>"; 2051*1d05cddcSAtari911 }); 2052*1d05cddcSAtari911 } 2053*1d05cddcSAtari911 tooltip.style.borderColor = "#9b59b6"; 2054*1d05cddcSAtari911 tooltip.style.color = "#9b59b6"; 2055*1d05cddcSAtari911 } else if (color === "orange") { 2056*1d05cddcSAtari911 content = "<div class=\\"tooltip-title\\">Memory Usage</div>"; 2057*1d05cddcSAtari911 if (latestStats.memory_details && latestStats.memory_details.total) { 2058*1d05cddcSAtari911 content += "<div>Total: " + latestStats.memory_details.total + "</div>"; 2059*1d05cddcSAtari911 content += "<div>Used: " + latestStats.memory_details.used + "</div>"; 2060*1d05cddcSAtari911 content += "<div>Available: " + latestStats.memory_details.available + "</div>"; 2061*1d05cddcSAtari911 if (latestStats.memory_details.cached) { 2062*1d05cddcSAtari911 content += "<div>Cached: " + latestStats.memory_details.cached + "</div>"; 2063*1d05cddcSAtari911 } 2064*1d05cddcSAtari911 } else { 2065*1d05cddcSAtari911 content += "<div>Loading...</div>"; 2066*1d05cddcSAtari911 } 2067*1d05cddcSAtari911 if (latestStats.top_processes && latestStats.top_processes.length > 0) { 2068*1d05cddcSAtari911 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>"; 2069*1d05cddcSAtari911 latestStats.top_processes.slice(0, 5).forEach(proc => { 2070*1d05cddcSAtari911 content += "<div>" + proc.cpu + " " + proc.command + "</div>"; 2071*1d05cddcSAtari911 }); 2072*1d05cddcSAtari911 } 2073*1d05cddcSAtari911 tooltip.style.borderColor = "#ff9800"; 2074*1d05cddcSAtari911 tooltip.style.color = "#ff9800"; 2075*1d05cddcSAtari911 } 2076*1d05cddcSAtari911 2077*1d05cddcSAtari911 tooltip.innerHTML = content; 2078*1d05cddcSAtari911 tooltip.style.display = "block"; 2079*1d05cddcSAtari911 2080*1d05cddcSAtari911 const bar = tooltip.parentElement; 2081*1d05cddcSAtari911 const barRect = bar.getBoundingClientRect(); 2082*1d05cddcSAtari911 const tooltipRect = tooltip.getBoundingClientRect(); 2083*1d05cddcSAtari911 2084*1d05cddcSAtari911 const left = barRect.left + (barRect.width / 2) - (tooltipRect.width / 2); 2085*1d05cddcSAtari911 const top = barRect.top - tooltipRect.height - 8; 2086*1d05cddcSAtari911 2087*1d05cddcSAtari911 tooltip.style.left = left + "px"; 2088*1d05cddcSAtari911 tooltip.style.top = top + "px"; 2089*1d05cddcSAtari911 }; 2090*1d05cddcSAtari911 2091*1d05cddcSAtari911 window["hideTooltip_' . $jsCalId . '"] = function(color) { 2092*1d05cddcSAtari911 const tooltip = document.getElementById("tooltip-" + color + "-' . $calId . '"); 2093*1d05cddcSAtari911 if (tooltip) { 2094*1d05cddcSAtari911 tooltip.style.display = "none"; 2095*1d05cddcSAtari911 } 2096*1d05cddcSAtari911 }; 2097*1d05cddcSAtari911 2098*1d05cddcSAtari911 // Update clock every second 2099*1d05cddcSAtari911 function updateClock() { 2100*1d05cddcSAtari911 const now = new Date(); 2101*1d05cddcSAtari911 let hours = now.getHours(); 2102*1d05cddcSAtari911 const minutes = String(now.getMinutes()).padStart(2, "0"); 2103*1d05cddcSAtari911 const seconds = String(now.getSeconds()).padStart(2, "0"); 2104*1d05cddcSAtari911 const ampm = hours >= 12 ? "PM" : "AM"; 2105*1d05cddcSAtari911 hours = hours % 12 || 12; 2106*1d05cddcSAtari911 const timeStr = hours + ":" + minutes + ":" + seconds + " " + ampm; 2107*1d05cddcSAtari911 const clockEl = document.getElementById("clock-' . $calId . '"); 2108*1d05cddcSAtari911 if (clockEl) clockEl.textContent = timeStr; 2109*1d05cddcSAtari911 } 2110*1d05cddcSAtari911 setInterval(updateClock, 1000); 2111*1d05cddcSAtari911 2112*1d05cddcSAtari911 // Weather update function 2113*1d05cddcSAtari911 function updateWeather() { 2114*1d05cddcSAtari911 if ("geolocation" in navigator) { 2115*1d05cddcSAtari911 navigator.geolocation.getCurrentPosition(function(position) { 2116*1d05cddcSAtari911 const lat = position.coords.latitude; 2117*1d05cddcSAtari911 const lon = position.coords.longitude; 2118*1d05cddcSAtari911 2119*1d05cddcSAtari911 fetch(`https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}¤t_weather=true&temperature_unit=fahrenheit`) 2120*1d05cddcSAtari911 .then(response => response.json()) 2121*1d05cddcSAtari911 .then(data => { 2122*1d05cddcSAtari911 if (data.current_weather) { 2123*1d05cddcSAtari911 const temp = Math.round(data.current_weather.temperature); 2124*1d05cddcSAtari911 const weatherCode = data.current_weather.weathercode; 2125*1d05cddcSAtari911 const icon = getWeatherIcon(weatherCode); 2126*1d05cddcSAtari911 const iconEl = document.getElementById("weather-icon-' . $calId . '"); 2127*1d05cddcSAtari911 const tempEl = document.getElementById("weather-temp-' . $calId . '"); 2128*1d05cddcSAtari911 if (iconEl) iconEl.textContent = icon; 2129*1d05cddcSAtari911 if (tempEl) tempEl.innerHTML = temp + "°"; 2130*1d05cddcSAtari911 } 2131*1d05cddcSAtari911 }) 2132*1d05cddcSAtari911 .catch(error => console.log("Weather fetch error:", error)); 2133*1d05cddcSAtari911 }, function(error) { 2134*1d05cddcSAtari911 // If geolocation fails, use default location (Irvine, CA) 2135*1d05cddcSAtari911 fetch("https://api.open-meteo.com/v1/forecast?latitude=33.6846&longitude=-117.8265¤t_weather=true&temperature_unit=fahrenheit") 2136*1d05cddcSAtari911 .then(response => response.json()) 2137*1d05cddcSAtari911 .then(data => { 2138*1d05cddcSAtari911 if (data.current_weather) { 2139*1d05cddcSAtari911 const temp = Math.round(data.current_weather.temperature); 2140*1d05cddcSAtari911 const weatherCode = data.current_weather.weathercode; 2141*1d05cddcSAtari911 const icon = getWeatherIcon(weatherCode); 2142*1d05cddcSAtari911 const iconEl = document.getElementById("weather-icon-' . $calId . '"); 2143*1d05cddcSAtari911 const tempEl = document.getElementById("weather-temp-' . $calId . '"); 2144*1d05cddcSAtari911 if (iconEl) iconEl.textContent = icon; 2145*1d05cddcSAtari911 if (tempEl) tempEl.innerHTML = temp + "°"; 2146*1d05cddcSAtari911 } 2147*1d05cddcSAtari911 }) 2148*1d05cddcSAtari911 .catch(err => console.log("Weather error:", err)); 2149*1d05cddcSAtari911 }); 2150*1d05cddcSAtari911 } else { 2151*1d05cddcSAtari911 // No geolocation, use default (Irvine, CA) 2152*1d05cddcSAtari911 fetch("https://api.open-meteo.com/v1/forecast?latitude=33.6846&longitude=-117.8265¤t_weather=true&temperature_unit=fahrenheit") 2153*1d05cddcSAtari911 .then(response => response.json()) 2154*1d05cddcSAtari911 .then(data => { 2155*1d05cddcSAtari911 if (data.current_weather) { 2156*1d05cddcSAtari911 const temp = Math.round(data.current_weather.temperature); 2157*1d05cddcSAtari911 const weatherCode = data.current_weather.weathercode; 2158*1d05cddcSAtari911 const icon = getWeatherIcon(weatherCode); 2159*1d05cddcSAtari911 const iconEl = document.getElementById("weather-icon-' . $calId . '"); 2160*1d05cddcSAtari911 const tempEl = document.getElementById("weather-temp-' . $calId . '"); 2161*1d05cddcSAtari911 if (iconEl) iconEl.textContent = icon; 2162*1d05cddcSAtari911 if (tempEl) tempEl.innerHTML = temp + "°"; 2163*1d05cddcSAtari911 } 2164*1d05cddcSAtari911 }) 2165*1d05cddcSAtari911 .catch(err => console.log("Weather error:", err)); 2166*1d05cddcSAtari911 } 2167*1d05cddcSAtari911 } 2168*1d05cddcSAtari911 2169*1d05cddcSAtari911 function getWeatherIcon(code) { 2170*1d05cddcSAtari911 const icons = { 2171*1d05cddcSAtari911 0: "☀️", 1: "️", 2: "⛅", 3: "☁️", 2172*1d05cddcSAtari911 45: "️", 48: "️", 51: "️", 53: "️", 55: "️", 2173*1d05cddcSAtari911 61: "️", 63: "️", 65: "⛈️", 71: "️", 73: "️", 2174*1d05cddcSAtari911 75: "❄️", 77: "️", 80: "️", 81: "️", 82: "⛈️", 2175*1d05cddcSAtari911 85: "️", 86: "❄️", 95: "⛈️", 96: "⛈️", 99: "⛈️" 2176*1d05cddcSAtari911 }; 2177*1d05cddcSAtari911 return icons[code] || "️"; 2178*1d05cddcSAtari911 } 2179*1d05cddcSAtari911 2180*1d05cddcSAtari911 // Update weather immediately and every 10 minutes 2181*1d05cddcSAtari911 updateWeather(); 2182*1d05cddcSAtari911 setInterval(updateWeather, 600000); 2183*1d05cddcSAtari911 2184*1d05cddcSAtari911 // Update system stats and tooltips data 2185*1d05cddcSAtari911 function updateSystemStats() { 2186*1d05cddcSAtari911 fetch("' . DOKU_BASE . 'lib/plugins/calendar/get_system_stats.php") 2187*1d05cddcSAtari911 .then(response => response.json()) 2188*1d05cddcSAtari911 .then(data => { 2189*1d05cddcSAtari911 sharedState_' . $jsCalId . '.latestStats = { 2190*1d05cddcSAtari911 load: data.load || {"1min": 0, "5min": 0, "15min": 0}, 2191*1d05cddcSAtari911 uptime: data.uptime || "", 2192*1d05cddcSAtari911 memory_details: data.memory_details || {}, 2193*1d05cddcSAtari911 top_processes: data.top_processes || [] 2194*1d05cddcSAtari911 }; 2195*1d05cddcSAtari911 2196*1d05cddcSAtari911 const greenBar = document.getElementById("cpu-5min-' . $calId . '"); 2197*1d05cddcSAtari911 if (greenBar) { 2198*1d05cddcSAtari911 greenBar.style.width = Math.min(100, data.cpu_5min) + "%"; 2199*1d05cddcSAtari911 } 2200*1d05cddcSAtari911 2201*1d05cddcSAtari911 sharedState_' . $jsCalId . '.cpuHistory.push(data.cpu); 2202*1d05cddcSAtari911 if (sharedState_' . $jsCalId . '.cpuHistory.length > sharedState_' . $jsCalId . '.CPU_HISTORY_SIZE) { 2203*1d05cddcSAtari911 sharedState_' . $jsCalId . '.cpuHistory.shift(); 2204*1d05cddcSAtari911 } 2205*1d05cddcSAtari911 2206*1d05cddcSAtari911 const cpuAverage = sharedState_' . $jsCalId . '.cpuHistory.reduce((sum, val) => sum + val, 0) / sharedState_' . $jsCalId . '.cpuHistory.length; 2207*1d05cddcSAtari911 2208*1d05cddcSAtari911 const cpuBar = document.getElementById("cpu-realtime-' . $calId . '"); 2209*1d05cddcSAtari911 if (cpuBar) { 2210*1d05cddcSAtari911 cpuBar.style.width = Math.min(100, cpuAverage) + "%"; 2211*1d05cddcSAtari911 } 2212*1d05cddcSAtari911 2213*1d05cddcSAtari911 const memBar = document.getElementById("mem-realtime-' . $calId . '"); 2214*1d05cddcSAtari911 if (memBar) { 2215*1d05cddcSAtari911 memBar.style.width = Math.min(100, data.memory) + "%"; 2216*1d05cddcSAtari911 } 2217*1d05cddcSAtari911 }) 2218*1d05cddcSAtari911 .catch(error => { 2219*1d05cddcSAtari911 console.log("System stats error:", error); 2220*1d05cddcSAtari911 }); 2221*1d05cddcSAtari911 } 2222*1d05cddcSAtari911 2223*1d05cddcSAtari911 updateSystemStats(); 2224*1d05cddcSAtari911 setInterval(updateSystemStats, 2000); 2225*1d05cddcSAtari911})(); 2226*1d05cddcSAtari911</script>'; 2227*1d05cddcSAtari911 2228*1d05cddcSAtari911 // NOW add the header HTML (after JavaScript is defined) 2229*1d05cddcSAtari911 $todayDate = new DateTime(); 2230*1d05cddcSAtari911 $displayDate = $todayDate->format('D, M j, Y'); 2231*1d05cddcSAtari911 $currentTime = $todayDate->format('g:i:s A'); 2232*1d05cddcSAtari911 2233*1d05cddcSAtari911 $html .= '<div class="eventlist-today-header">'; 2234*1d05cddcSAtari911 $html .= '<span class="eventlist-today-clock" id="clock-' . $calId . '">' . $currentTime . '</span>'; 2235*1d05cddcSAtari911 $html .= '<div class="eventlist-bottom-info">'; 2236*1d05cddcSAtari911 $html .= '<span class="eventlist-weather"><span id="weather-icon-' . $calId . '">️</span> <span id="weather-temp-' . $calId . '">--°</span></span>'; 2237*1d05cddcSAtari911 $html .= '<span class="eventlist-today-date">' . $displayDate . '</span>'; 2238*1d05cddcSAtari911 $html .= '</div>'; 2239*1d05cddcSAtari911 2240*1d05cddcSAtari911 // Three CPU/Memory bars (all update live) 2241*1d05cddcSAtari911 $html .= '<div class="eventlist-stats-container">'; 2242*1d05cddcSAtari911 2243*1d05cddcSAtari911 // 5-minute load average (green, updates every 2 seconds) 2244*1d05cddcSAtari911 $html .= '<div class="eventlist-cpu-bar" onmouseover="showTooltip_' . $jsCalId . '(\'green\')" onmouseout="hideTooltip_' . $jsCalId . '(\'green\')">'; 2245*1d05cddcSAtari911 $html .= '<div class="eventlist-cpu-fill" id="cpu-5min-' . $calId . '" style="width: 0%;"></div>'; 2246*1d05cddcSAtari911 $html .= '<div class="system-tooltip" id="tooltip-green-' . $calId . '" style="display:none;"></div>'; 2247*1d05cddcSAtari911 $html .= '</div>'; 2248*1d05cddcSAtari911 2249*1d05cddcSAtari911 // Real-time CPU (purple, updates with 5-sec average) 2250*1d05cddcSAtari911 $html .= '<div class="eventlist-cpu-bar eventlist-cpu-realtime" onmouseover="showTooltip_' . $jsCalId . '(\'purple\')" onmouseout="hideTooltip_' . $jsCalId . '(\'purple\')">'; 2251*1d05cddcSAtari911 $html .= '<div class="eventlist-cpu-fill eventlist-cpu-fill-purple" id="cpu-realtime-' . $calId . '" style="width: 0%;"></div>'; 2252*1d05cddcSAtari911 $html .= '<div class="system-tooltip" id="tooltip-purple-' . $calId . '" style="display:none;"></div>'; 2253*1d05cddcSAtari911 $html .= '</div>'; 2254*1d05cddcSAtari911 2255*1d05cddcSAtari911 // Real-time Memory (orange, updates) 2256*1d05cddcSAtari911 $html .= '<div class="eventlist-cpu-bar eventlist-mem-realtime" onmouseover="showTooltip_' . $jsCalId . '(\'orange\')" onmouseout="hideTooltip_' . $jsCalId . '(\'orange\')">'; 2257*1d05cddcSAtari911 $html .= '<div class="eventlist-cpu-fill eventlist-cpu-fill-orange" id="mem-realtime-' . $calId . '" style="width: 0%;"></div>'; 2258*1d05cddcSAtari911 $html .= '<div class="system-tooltip" id="tooltip-orange-' . $calId . '" style="display:none;"></div>'; 2259*1d05cddcSAtari911 $html .= '</div>'; 2260*1d05cddcSAtari911 2261*1d05cddcSAtari911 $html .= '</div>'; 2262*1d05cddcSAtari911 $html .= '</div>'; 2263*1d05cddcSAtari911 2264*1d05cddcSAtari911 // Ultra-thin orange "Add Event" bar between header and week grid (6px max height) 2265*1d05cddcSAtari911 $html .= '<div style="background:#ff9800; padding:0; height:6px; line-height:6px; text-align:center; cursor:pointer; border-top:1px solid rgba(255, 152, 0, 0.3); border-bottom:1px solid rgba(255, 152, 0, 0.3); box-shadow:0 0 8px rgba(255, 152, 0, 0.4); transition:all 0.2s; overflow:hidden;" onclick="window.location.href=\'?do=admin&page=calendar&tab=manage\'" onmouseover="this.style.background=\'#ff7700\'; this.style.boxShadow=\'0 0 12px rgba(255, 152, 0, 0.6)\';" onmouseout="this.style.background=\'#ff9800\'; this.style.boxShadow=\'0 0 8px rgba(255, 152, 0, 0.4)\';">'; 2266*1d05cddcSAtari911 $html .= '<span style="color:#000; font-size:7px; font-weight:700; letter-spacing:0.3px; font-family:system-ui, sans-serif; text-shadow:0 0 2px rgba(255, 255, 255, 0.3); vertical-align:middle;">+ ADD EVENT</span>'; 2267*1d05cddcSAtari911 $html .= '</div>'; 2268*1d05cddcSAtari911 2269*1d05cddcSAtari911 // Week grid (7 cells) 2270*1d05cddcSAtari911 $html .= $this->renderWeekGrid($weekEvents, $weekStart); 2271*1d05cddcSAtari911 2272*1d05cddcSAtari911 // Today section (orange) 2273*1d05cddcSAtari911 if (!empty($todayEvents)) { 2274*1d05cddcSAtari911 $html .= $this->renderSidebarSection('Today', $todayEvents, '#ff9800', $calId); 2275*1d05cddcSAtari911 } 2276*1d05cddcSAtari911 2277*1d05cddcSAtari911 // Tomorrow section (green) 2278*1d05cddcSAtari911 if (!empty($tomorrowEvents)) { 2279*1d05cddcSAtari911 $html .= $this->renderSidebarSection('Tomorrow', $tomorrowEvents, '#4caf50', $calId); 2280*1d05cddcSAtari911 } 2281*1d05cddcSAtari911 2282*1d05cddcSAtari911 // Important events section (purple) 2283*1d05cddcSAtari911 if (!empty($importantEvents)) { 2284*1d05cddcSAtari911 $html .= $this->renderSidebarSection('Important Events', $importantEvents, '#9b59b6', $calId); 2285*1d05cddcSAtari911 } 2286*1d05cddcSAtari911 2287*1d05cddcSAtari911 $html .= '</div>'; 2288*1d05cddcSAtari911 2289*1d05cddcSAtari911 return $html; 2290*1d05cddcSAtari911 } 2291*1d05cddcSAtari911 2292*1d05cddcSAtari911 /** 2293*1d05cddcSAtari911 * Render compact week grid (7 cells with event bars) - Matrix themed with clickable days 2294*1d05cddcSAtari911 */ 2295*1d05cddcSAtari911 private function renderWeekGrid($weekEvents, $weekStart) { 2296*1d05cddcSAtari911 // Generate unique ID for this calendar instance - sanitize for JavaScript 2297*1d05cddcSAtari911 $calId = 'cal_' . substr(md5($weekStart . microtime()), 0, 8); 2298*1d05cddcSAtari911 $jsCalId = str_replace('-', '_', $calId); // Sanitize for JS variable names 2299*1d05cddcSAtari911 2300*1d05cddcSAtari911 $html = '<div style="display:grid; grid-template-columns:repeat(7, 1fr); gap:1px; background:#1a3d1a; border-bottom:2px solid #00cc07;">'; 2301*1d05cddcSAtari911 2302*1d05cddcSAtari911 $dayNames = ['M', 'T', 'W', 'T', 'F', 'S', 'S']; 2303*1d05cddcSAtari911 $today = date('Y-m-d'); 2304*1d05cddcSAtari911 2305*1d05cddcSAtari911 for ($i = 0; $i < 7; $i++) { 2306*1d05cddcSAtari911 $date = date('Y-m-d', strtotime($weekStart . ' +' . $i . ' days')); 2307*1d05cddcSAtari911 $dayNum = date('j', strtotime($date)); 2308*1d05cddcSAtari911 $isToday = $date === $today; 2309*1d05cddcSAtari911 2310*1d05cddcSAtari911 $events = isset($weekEvents[$date]) ? $weekEvents[$date] : []; 2311*1d05cddcSAtari911 $eventCount = count($events); 2312*1d05cddcSAtari911 2313*1d05cddcSAtari911 $bgColor = $isToday ? '#2a4d2a' : '#242424'; 2314*1d05cddcSAtari911 $textColor = $isToday ? '#00ff00' : '#00cc07'; 2315*1d05cddcSAtari911 $fontWeight = $isToday ? '700' : '500'; 2316*1d05cddcSAtari911 $textShadow = $isToday ? 'text-shadow:0 0 6px rgba(0, 255, 0, 0.6);' : 'text-shadow:0 0 4px rgba(0, 204, 7, 0.4);'; 2317*1d05cddcSAtari911 2318*1d05cddcSAtari911 $hasEvents = $eventCount > 0; 2319*1d05cddcSAtari911 $clickableStyle = $hasEvents ? 'cursor:pointer;' : ''; 2320*1d05cddcSAtari911 $clickHandler = $hasEvents ? ' onclick="showDayEvents_' . $jsCalId . '(\'' . $date . '\')"' : ''; 2321*1d05cddcSAtari911 2322*1d05cddcSAtari911 $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 . '>'; 2323*1d05cddcSAtari911 2324*1d05cddcSAtari911 // Day letter 2325*1d05cddcSAtari911 $html .= '<div style="font-size:9px; color:#00cc07; font-weight:500; font-family:system-ui, sans-serif; ' . $textShadow . '">' . $dayNames[$i] . '</div>'; 2326*1d05cddcSAtari911 2327*1d05cddcSAtari911 // Day number 2328*1d05cddcSAtari911 $html .= '<div style="font-size:12px; color:' . $textColor . '; font-weight:' . $fontWeight . '; margin:2px 0; font-family:system-ui, sans-serif; ' . $textShadow . '">' . $dayNum . '</div>'; 2329*1d05cddcSAtari911 2330*1d05cddcSAtari911 // Event bars (max 3 visible) with glow effect 2331*1d05cddcSAtari911 if ($eventCount > 0) { 2332*1d05cddcSAtari911 $showCount = min($eventCount, 3); 2333*1d05cddcSAtari911 for ($j = 0; $j < $showCount; $j++) { 2334*1d05cddcSAtari911 $event = $events[$j]; 2335*1d05cddcSAtari911 $color = isset($event['color']) ? $event['color'] : '#00cc07'; 2336*1d05cddcSAtari911 $html .= '<div style="height:2px; background:' . htmlspecialchars($color) . '; margin:1px 0; border-radius:1px; box-shadow:0 0 3px ' . htmlspecialchars($color) . ';"></div>'; 2337*1d05cddcSAtari911 } 2338*1d05cddcSAtari911 2339*1d05cddcSAtari911 // Show "+N more" if more than 3 2340*1d05cddcSAtari911 if ($eventCount > 3) { 2341*1d05cddcSAtari911 $html .= '<div style="font-size:7px; color:#00cc07; margin-top:1px; font-family:system-ui, sans-serif;">+' . ($eventCount - 3) . '</div>'; 2342*1d05cddcSAtari911 } 2343*1d05cddcSAtari911 } 2344*1d05cddcSAtari911 2345*1d05cddcSAtari911 $html .= '</div>'; 2346*1d05cddcSAtari911 } 2347*1d05cddcSAtari911 2348*1d05cddcSAtari911 $html .= '</div>'; 2349*1d05cddcSAtari911 2350*1d05cddcSAtari911 // Add container for selected day events display (with unique ID) 2351*1d05cddcSAtari911 $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);">'; 2352*1d05cddcSAtari911 $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;">'; 2353*1d05cddcSAtari911 $html .= '<span id="selected-day-title-' . $calId . '"></span>'; 2354*1d05cddcSAtari911 $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>'; 2355*1d05cddcSAtari911 $html .= '</div>'; 2356*1d05cddcSAtari911 $html .= '<div id="selected-day-content-' . $calId . '" style="padding:4px 0; background:rgba(36, 36, 36, 0.5);"></div>'; 2357*1d05cddcSAtari911 $html .= '</div>'; 2358*1d05cddcSAtari911 2359*1d05cddcSAtari911 // Add JavaScript for day selection with event data 2360*1d05cddcSAtari911 $html .= '<script>'; 2361*1d05cddcSAtari911 // Sanitize calId for JavaScript variable names 2362*1d05cddcSAtari911 $jsCalId = str_replace('-', '_', $calId); 2363*1d05cddcSAtari911 $html .= 'window.weekEventsData_' . $jsCalId . ' = ' . json_encode($weekEvents) . ';'; 2364*1d05cddcSAtari911 $html .= ' 2365*1d05cddcSAtari911 window.showDayEvents_' . $jsCalId . ' = function(dateKey) { 2366*1d05cddcSAtari911 const eventsData = window.weekEventsData_' . $jsCalId . '; 2367*1d05cddcSAtari911 const container = document.getElementById("selected-day-events-' . $calId . '"); 2368*1d05cddcSAtari911 const title = document.getElementById("selected-day-title-' . $calId . '"); 2369*1d05cddcSAtari911 const content = document.getElementById("selected-day-content-' . $calId . '"); 2370*1d05cddcSAtari911 2371*1d05cddcSAtari911 if (!eventsData[dateKey] || eventsData[dateKey].length === 0) return; 2372*1d05cddcSAtari911 2373*1d05cddcSAtari911 // Format date for display 2374*1d05cddcSAtari911 const dateObj = new Date(dateKey + "T00:00:00"); 2375*1d05cddcSAtari911 const dayName = dateObj.toLocaleDateString("en-US", { weekday: "long" }); 2376*1d05cddcSAtari911 const monthDay = dateObj.toLocaleDateString("en-US", { month: "short", day: "numeric" }); 2377*1d05cddcSAtari911 title.textContent = dayName + ", " + monthDay; 2378*1d05cddcSAtari911 2379*1d05cddcSAtari911 // Clear content 2380*1d05cddcSAtari911 content.innerHTML = ""; 2381*1d05cddcSAtari911 2382*1d05cddcSAtari911 // Sort events by time (events with time first, then all-day events) 2383*1d05cddcSAtari911 const sortedEvents = [...eventsData[dateKey]].sort((a, b) => { 2384*1d05cddcSAtari911 // Events without time go to the end 2385*1d05cddcSAtari911 if (!a.time && !b.time) return 0; 2386*1d05cddcSAtari911 if (!a.time) return 1; 2387*1d05cddcSAtari911 if (!b.time) return -1; 2388*1d05cddcSAtari911 2389*1d05cddcSAtari911 // Compare times (format: "HH:MM") 2390*1d05cddcSAtari911 const timeA = a.time.split(":").map(Number); 2391*1d05cddcSAtari911 const timeB = b.time.split(":").map(Number); 2392*1d05cddcSAtari911 const minutesA = timeA[0] * 60 + timeA[1]; 2393*1d05cddcSAtari911 const minutesB = timeB[0] * 60 + timeB[1]; 2394*1d05cddcSAtari911 2395*1d05cddcSAtari911 return minutesA - minutesB; 2396*1d05cddcSAtari911 }); 2397*1d05cddcSAtari911 2398*1d05cddcSAtari911 // Build events HTML with dual color bars 2399*1d05cddcSAtari911 sortedEvents.forEach(event => { 2400*1d05cddcSAtari911 const eventColor = event.color || "#00cc07"; 2401*1d05cddcSAtari911 const sectionColor = "#3498db"; // Blue for selected day 2402*1d05cddcSAtari911 2403*1d05cddcSAtari911 const eventDiv = document.createElement("div"); 2404*1d05cddcSAtari911 eventDiv.style.cssText = "padding:4px 6px; border-bottom:1px solid rgba(0, 204, 7, 0.2); font-size:10px; display:flex; align-items:start; gap:6px; background:rgba(36, 36, 36, 0.3);"; 2405*1d05cddcSAtari911 2406*1d05cddcSAtari911 let eventHTML = ""; 2407*1d05cddcSAtari911 2408*1d05cddcSAtari911 // Section color bar (wider, blue for selected day) 2409*1d05cddcSAtari911 eventHTML += "<div style=\\"width:4px; height:100%; background:" + sectionColor + "; border-radius:2px; flex-shrink:0; box-shadow:0 0 4px " + sectionColor + ";\\"></div>"; 2410*1d05cddcSAtari911 2411*1d05cddcSAtari911 // Event assigned color bar (medium width) 2412*1d05cddcSAtari911 eventHTML += "<div style=\\"width:3px; height:100%; background:" + eventColor + "; border-radius:1px; flex-shrink:0; box-shadow:0 0 3px " + eventColor + ";\\"></div>"; 2413*1d05cddcSAtari911 2414*1d05cddcSAtari911 // Content 2415*1d05cddcSAtari911 eventHTML += "<div style=\\"flex:1; min-width:0;\\">"; 2416*1d05cddcSAtari911 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);\\">"; 2417*1d05cddcSAtari911 2418*1d05cddcSAtari911 // Time 2419*1d05cddcSAtari911 if (event.time) { 2420*1d05cddcSAtari911 const timeParts = event.time.split(":"); 2421*1d05cddcSAtari911 let hours = parseInt(timeParts[0]); 2422*1d05cddcSAtari911 const minutes = timeParts[1]; 2423*1d05cddcSAtari911 const ampm = hours >= 12 ? "PM" : "AM"; 2424*1d05cddcSAtari911 hours = hours % 12 || 12; 2425*1d05cddcSAtari911 eventHTML += "<span style=\\"color:#00dd00; font-weight:500; font-size:9px;\\">" + hours + ":" + minutes + " " + ampm + "</span> "; 2426*1d05cddcSAtari911 } 2427*1d05cddcSAtari911 2428*1d05cddcSAtari911 // Title - use HTML version if available 2429*1d05cddcSAtari911 const titleHTML = event.title_html || event.title || "Untitled"; 2430*1d05cddcSAtari911 eventHTML += titleHTML; 2431*1d05cddcSAtari911 eventHTML += "</div>"; 2432*1d05cddcSAtari911 2433*1d05cddcSAtari911 // Description if present - use HTML version 2434*1d05cddcSAtari911 if (event.description_html || event.description) { 2435*1d05cddcSAtari911 const descHTML = event.description_html || event.description; 2436*1d05cddcSAtari911 eventHTML += "<div style=\\"font-size:9px; color:#00aa00; margin-top:2px;\\">" + descHTML + "</div>"; 2437*1d05cddcSAtari911 } 2438*1d05cddcSAtari911 2439*1d05cddcSAtari911 eventHTML += "</div>"; 2440*1d05cddcSAtari911 2441*1d05cddcSAtari911 eventDiv.innerHTML = eventHTML; 2442*1d05cddcSAtari911 content.appendChild(eventDiv); 2443*1d05cddcSAtari911 }); 2444*1d05cddcSAtari911 2445*1d05cddcSAtari911 container.style.display = "block"; 2446*1d05cddcSAtari911 }; 2447*1d05cddcSAtari911 '; 2448*1d05cddcSAtari911 $html .= '</script>'; 2449*1d05cddcSAtari911 2450*1d05cddcSAtari911 return $html; 2451*1d05cddcSAtari911 } 2452*1d05cddcSAtari911 2453*1d05cddcSAtari911 /** 2454*1d05cddcSAtari911 * Render a sidebar section (Today/Tomorrow/Important) - Matrix themed with colored borders 2455*1d05cddcSAtari911 */ 2456*1d05cddcSAtari911 private function renderSidebarSection($title, $events, $accentColor, $calId) { 2457*1d05cddcSAtari911 // Keep the original accent colors for borders 2458*1d05cddcSAtari911 $borderColor = $accentColor; 2459*1d05cddcSAtari911 2460*1d05cddcSAtari911 // Show date for Important Events section 2461*1d05cddcSAtari911 $showDate = ($title === 'Important Events'); 2462*1d05cddcSAtari911 2463*1d05cddcSAtari911 $html = '<div style="border-left:3px solid ' . $borderColor . '; margin:8px 4px; box-shadow:0 0 5px rgba(0, 204, 7, 0.2);">'; 2464*1d05cddcSAtari911 2465*1d05cddcSAtari911 // Section header with accent color background - smaller, not all caps 2466*1d05cddcSAtari911 $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 . ';">'; 2467*1d05cddcSAtari911 $html .= htmlspecialchars($title); 2468*1d05cddcSAtari911 $html .= '</div>'; 2469*1d05cddcSAtari911 2470*1d05cddcSAtari911 // Events 2471*1d05cddcSAtari911 $html .= '<div style="padding:4px 0; background:rgba(36, 36, 36, 0.5);">'; 2472*1d05cddcSAtari911 2473*1d05cddcSAtari911 foreach ($events as $event) { 2474*1d05cddcSAtari911 $html .= $this->renderSidebarEvent($event, $calId, $showDate, $accentColor); 2475*1d05cddcSAtari911 } 2476*1d05cddcSAtari911 2477*1d05cddcSAtari911 $html .= '</div>'; 2478*1d05cddcSAtari911 $html .= '</div>'; 2479*1d05cddcSAtari911 2480*1d05cddcSAtari911 return $html; 2481*1d05cddcSAtari911 } 2482*1d05cddcSAtari911 2483*1d05cddcSAtari911 /** 2484*1d05cddcSAtari911 * Render individual event in sidebar - Matrix themed with dual color bars 2485*1d05cddcSAtari911 */ 2486*1d05cddcSAtari911 private function renderSidebarEvent($event, $calId, $showDate = false, $sectionColor = '#00cc07') { 2487*1d05cddcSAtari911 $title = isset($event['title']) ? htmlspecialchars($event['title']) : 'Untitled'; 2488*1d05cddcSAtari911 $time = isset($event['time']) ? $event['time'] : ''; 2489*1d05cddcSAtari911 $endTime = isset($event['endTime']) ? $event['endTime'] : ''; 2490*1d05cddcSAtari911 $eventColor = isset($event['color']) ? htmlspecialchars($event['color']) : '#00cc07'; 2491*1d05cddcSAtari911 $date = isset($event['date']) ? $event['date'] : ''; 2492*1d05cddcSAtari911 $isTask = isset($event['isTask']) && $event['isTask']; 2493*1d05cddcSAtari911 $completed = isset($event['completed']) && $event['completed']; 2494*1d05cddcSAtari911 2495*1d05cddcSAtari911 // Check for conflicts 2496*1d05cddcSAtari911 $hasConflict = isset($event['conflicts']) && !empty($event['conflicts']); 2497*1d05cddcSAtari911 2498*1d05cddcSAtari911 $html = '<div style="padding:4px 6px; border-bottom:1px solid rgba(0, 204, 7, 0.2); font-size:10px; display:flex; align-items:start; gap:6px; background:rgba(36, 36, 36, 0.3);">'; 2499*1d05cddcSAtari911 2500*1d05cddcSAtari911 // Section color bar (wider, on the left) 2501*1d05cddcSAtari911 $html .= '<div style="width:4px; height:100%; background:' . htmlspecialchars($sectionColor) . '; border-radius:2px; flex-shrink:0; box-shadow:0 0 4px ' . htmlspecialchars($sectionColor) . ';"></div>'; 2502*1d05cddcSAtari911 2503*1d05cddcSAtari911 // Event's assigned color bar (medium width, next to section bar) 2504*1d05cddcSAtari911 $html .= '<div style="width:3px; height:100%; background:' . $eventColor . '; border-radius:1px; flex-shrink:0; box-shadow:0 0 3px ' . $eventColor . ';"></div>'; 2505*1d05cddcSAtari911 2506*1d05cddcSAtari911 // Content 2507*1d05cddcSAtari911 $html .= '<div style="flex:1; min-width:0;">'; 2508*1d05cddcSAtari911 2509*1d05cddcSAtari911 // Time + title 2510*1d05cddcSAtari911 $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);">'; 2511*1d05cddcSAtari911 2512*1d05cddcSAtari911 if ($time) { 2513*1d05cddcSAtari911 $displayTime = $this->formatTimeDisplay($time, $endTime); 2514*1d05cddcSAtari911 $html .= '<span style="color:#00dd00; font-weight:500; font-size:9px;">' . htmlspecialchars($displayTime) . '</span> '; 2515*1d05cddcSAtari911 } 2516*1d05cddcSAtari911 2517*1d05cddcSAtari911 // Task checkbox 2518*1d05cddcSAtari911 if ($isTask) { 2519*1d05cddcSAtari911 $checkIcon = $completed ? '☑' : '☐'; 2520*1d05cddcSAtari911 $html .= '<span style="font-size:11px; color:#00ff00;">' . $checkIcon . '</span> '; 2521*1d05cddcSAtari911 } 2522*1d05cddcSAtari911 2523*1d05cddcSAtari911 $html .= htmlspecialchars($title); 2524*1d05cddcSAtari911 2525*1d05cddcSAtari911 // Conflict badge 2526*1d05cddcSAtari911 if ($hasConflict) { 2527*1d05cddcSAtari911 $conflictCount = count($event['conflicts']); 2528*1d05cddcSAtari911 $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>'; 2529*1d05cddcSAtari911 } 2530*1d05cddcSAtari911 2531*1d05cddcSAtari911 $html .= '</div>'; 2532*1d05cddcSAtari911 2533*1d05cddcSAtari911 // Date display BELOW event name for Important events 2534*1d05cddcSAtari911 if ($showDate && $date) { 2535*1d05cddcSAtari911 $dateObj = new DateTime($date); 2536*1d05cddcSAtari911 $displayDate = $dateObj->format('D, M j'); // e.g., "Mon, Feb 10" 2537*1d05cddcSAtari911 $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>'; 2538*1d05cddcSAtari911 } 2539*1d05cddcSAtari911 2540*1d05cddcSAtari911 $html .= '</div>'; 2541*1d05cddcSAtari911 $html .= '</div>'; 2542*1d05cddcSAtari911 2543*1d05cddcSAtari911 return $html; 2544*1d05cddcSAtari911 } 2545*1d05cddcSAtari911 2546*1d05cddcSAtari911 /** 2547*1d05cddcSAtari911 * Format time display (12-hour format with optional end time) 2548*1d05cddcSAtari911 */ 2549*1d05cddcSAtari911 private function formatTimeDisplay($startTime, $endTime = '') { 2550*1d05cddcSAtari911 // Convert start time 2551*1d05cddcSAtari911 list($hour, $minute) = explode(':', $startTime); 2552*1d05cddcSAtari911 $hour = (int)$hour; 2553*1d05cddcSAtari911 $ampm = $hour >= 12 ? 'PM' : 'AM'; 2554*1d05cddcSAtari911 $displayHour = $hour % 12; 2555*1d05cddcSAtari911 if ($displayHour === 0) $displayHour = 12; 2556*1d05cddcSAtari911 2557*1d05cddcSAtari911 $display = $displayHour . ':' . $minute . ' ' . $ampm; 2558*1d05cddcSAtari911 2559*1d05cddcSAtari911 // Add end time if provided 2560*1d05cddcSAtari911 if ($endTime && $endTime !== '') { 2561*1d05cddcSAtari911 list($endHour, $endMinute) = explode(':', $endTime); 2562*1d05cddcSAtari911 $endHour = (int)$endHour; 2563*1d05cddcSAtari911 $endAmpm = $endHour >= 12 ? 'PM' : 'AM'; 2564*1d05cddcSAtari911 $endDisplayHour = $endHour % 12; 2565*1d05cddcSAtari911 if ($endDisplayHour === 0) $endDisplayHour = 12; 2566*1d05cddcSAtari911 2567*1d05cddcSAtari911 $display .= '-' . $endDisplayHour . ':' . $endMinute . ' ' . $endAmpm; 2568*1d05cddcSAtari911 } 2569*1d05cddcSAtari911 2570*1d05cddcSAtari911 return $display; 2571*1d05cddcSAtari911 } 2572*1d05cddcSAtari911 2573*1d05cddcSAtari911 /** 2574*1d05cddcSAtari911 * Render DokuWiki syntax to HTML 2575*1d05cddcSAtari911 * Converts **bold**, //italic//, [[links]], etc. to HTML 2576*1d05cddcSAtari911 */ 2577*1d05cddcSAtari911 private function renderDokuWikiToHtml($text) { 2578*1d05cddcSAtari911 if (empty($text)) return ''; 2579*1d05cddcSAtari911 2580*1d05cddcSAtari911 // Use DokuWiki's parser to render the text 2581*1d05cddcSAtari911 $instructions = p_get_instructions($text); 2582*1d05cddcSAtari911 2583*1d05cddcSAtari911 // Render instructions to XHTML 2584*1d05cddcSAtari911 $xhtml = p_render('xhtml', $instructions, $info); 2585*1d05cddcSAtari911 2586*1d05cddcSAtari911 // Remove surrounding <p> tags if present (we're rendering inline) 2587*1d05cddcSAtari911 $xhtml = preg_replace('/^<p>(.*)<\/p>$/s', '$1', trim($xhtml)); 2588*1d05cddcSAtari911 2589*1d05cddcSAtari911 return $xhtml; 2590*1d05cddcSAtari911 } 2591*1d05cddcSAtari911 2592*1d05cddcSAtari911 // Keep old scanForNamespaces for backward compatibility (not used anymore) 2593*1d05cddcSAtari911 private function scanForNamespaces($dir, $baseNamespace, &$namespaces) { 2594*1d05cddcSAtari911 if (!is_dir($dir)) return; 2595*1d05cddcSAtari911 2596*1d05cddcSAtari911 $items = scandir($dir); 2597*1d05cddcSAtari911 foreach ($items as $item) { 2598*1d05cddcSAtari911 if ($item === '.' || $item === '..' || $item === 'calendar') continue; 2599*1d05cddcSAtari911 2600*1d05cddcSAtari911 $path = $dir . $item; 2601*1d05cddcSAtari911 if (is_dir($path)) { 2602*1d05cddcSAtari911 $namespace = $baseNamespace ? $baseNamespace . ':' . $item : $item; 2603*1d05cddcSAtari911 $namespaces[] = $namespace; 2604*1d05cddcSAtari911 $this->scanForNamespaces($path . '/', $namespace, $namespaces); 2605*1d05cddcSAtari911 } 2606*1d05cddcSAtari911 } 2607*1d05cddcSAtari911 } 260819378907SAtari911} 2609