xref: /plugin/calendar/admin.php (revision 4590242deeee3fc24853b3bd5112bfa389075604)
11d05cddcSAtari911<?php
21d05cddcSAtari911/**
31d05cddcSAtari911 * Calendar Plugin - Admin Interface
41d05cddcSAtari911 * Clean rewrite - Configuration only
51d05cddcSAtari911 * Version: 3.3
61d05cddcSAtari911 */
71d05cddcSAtari911
81d05cddcSAtari911if(!defined('DOKU_INC')) die();
91d05cddcSAtari911
101d05cddcSAtari911class admin_plugin_calendar extends DokuWiki_Admin_Plugin {
111d05cddcSAtari911
121d05cddcSAtari911    public function getMenuText($language) {
131d05cddcSAtari911        return 'Calendar Management';
141d05cddcSAtari911    }
151d05cddcSAtari911
161d05cddcSAtari911    public function getMenuSort() {
171d05cddcSAtari911        return 100;
181d05cddcSAtari911    }
191d05cddcSAtari911
201d05cddcSAtari911    public function forAdminOnly() {
211d05cddcSAtari911        return true;
221d05cddcSAtari911    }
231d05cddcSAtari911
241d05cddcSAtari911    public function handle() {
251d05cddcSAtari911        global $INPUT;
261d05cddcSAtari911
271d05cddcSAtari911        $action = $INPUT->str('action');
281d05cddcSAtari911
291d05cddcSAtari911        if ($action === 'clear_cache') {
301d05cddcSAtari911            $this->clearCache();
311d05cddcSAtari911        } elseif ($action === 'save_config') {
321d05cddcSAtari911            $this->saveConfig();
331d05cddcSAtari911        } elseif ($action === 'delete_recurring_series') {
341d05cddcSAtari911            $this->deleteRecurringSeries();
351d05cddcSAtari911        } elseif ($action === 'edit_recurring_series') {
361d05cddcSAtari911            $this->editRecurringSeries();
371d05cddcSAtari911        } elseif ($action === 'move_selected_events') {
381d05cddcSAtari911            $this->moveEvents();
391d05cddcSAtari911        } elseif ($action === 'move_single_event') {
401d05cddcSAtari911            $this->moveSingleEvent();
411d05cddcSAtari911        } elseif ($action === 'delete_selected_events') {
421d05cddcSAtari911            $this->deleteSelectedEvents();
431d05cddcSAtari911        } elseif ($action === 'create_namespace') {
441d05cddcSAtari911            $this->createNamespace();
451d05cddcSAtari911        } elseif ($action === 'delete_namespace') {
461d05cddcSAtari911            $this->deleteNamespace();
479ccd446eSAtari911        } elseif ($action === 'rename_namespace') {
489ccd446eSAtari911            $this->renameNamespace();
491d05cddcSAtari911        } elseif ($action === 'run_sync') {
501d05cddcSAtari911            $this->runSync();
511d05cddcSAtari911        } elseif ($action === 'stop_sync') {
521d05cddcSAtari911            $this->stopSync();
531d05cddcSAtari911        } elseif ($action === 'upload_update') {
541d05cddcSAtari911            $this->uploadUpdate();
551d05cddcSAtari911        } elseif ($action === 'delete_backup') {
561d05cddcSAtari911            $this->deleteBackup();
571d05cddcSAtari911        } elseif ($action === 'rename_backup') {
581d05cddcSAtari911            $this->renameBackup();
591d05cddcSAtari911        } elseif ($action === 'restore_backup') {
601d05cddcSAtari911            $this->restoreBackup();
619ccd446eSAtari911        } elseif ($action === 'create_manual_backup') {
629ccd446eSAtari911            $this->createManualBackup();
631d05cddcSAtari911        } elseif ($action === 'export_config') {
641d05cddcSAtari911            $this->exportConfig();
651d05cddcSAtari911        } elseif ($action === 'import_config') {
661d05cddcSAtari911            $this->importConfig();
671d05cddcSAtari911        } elseif ($action === 'get_log') {
681d05cddcSAtari911            $this->getLog();
691d05cddcSAtari911        } elseif ($action === 'clear_log') {
701d05cddcSAtari911            $this->clearLogFile();
711d05cddcSAtari911        } elseif ($action === 'download_log') {
721d05cddcSAtari911            $this->downloadLog();
731d05cddcSAtari911        } elseif ($action === 'rescan_events') {
741d05cddcSAtari911            $this->rescanEvents();
751d05cddcSAtari911        } elseif ($action === 'export_all_events') {
761d05cddcSAtari911            $this->exportAllEvents();
771d05cddcSAtari911        } elseif ($action === 'import_all_events') {
781d05cddcSAtari911            $this->importAllEvents();
791d05cddcSAtari911        } elseif ($action === 'preview_cleanup') {
801d05cddcSAtari911            $this->previewCleanup();
811d05cddcSAtari911        } elseif ($action === 'cleanup_events') {
821d05cddcSAtari911            $this->cleanupEvents();
83*4590242dSAtari911        } elseif ($action === 'save_important_namespaces') {
84*4590242dSAtari911            $this->saveImportantNamespaces();
851d05cddcSAtari911        }
861d05cddcSAtari911    }
871d05cddcSAtari911
881d05cddcSAtari911    public function html() {
891d05cddcSAtari911        global $INPUT;
901d05cddcSAtari911
919ccd446eSAtari911        // Get current tab - default to 'manage' (Manage Events tab)
929ccd446eSAtari911        $tab = $INPUT->str('tab', 'manage');
931d05cddcSAtari911
949ccd446eSAtari911        // Get template colors
959ccd446eSAtari911        $colors = $this->getTemplateColors();
969ccd446eSAtari911        $accentColor = '#00cc07'; // Keep calendar plugin accent color
979ccd446eSAtari911
989ccd446eSAtari911        // Tab navigation (Manage Events, Update Plugin, Outlook Sync, Themes)
999ccd446eSAtari911        echo '<div style="border-bottom:2px solid ' . $colors['border'] . '; margin:10px 0 15px 0;">';
1009ccd446eSAtari911        echo '<a href="?do=admin&page=calendar&tab=manage" style="display:inline-block; padding:8px 16px; text-decoration:none; color:' . ($tab === 'manage' ? $accentColor : $colors['text']) . '; border-bottom:3px solid ' . ($tab === 'manage' ? $accentColor : 'transparent') . '; font-weight:' . ($tab === 'manage' ? 'bold' : 'normal') . ';">�� Manage Events</a>';
1019ccd446eSAtari911        echo '<a href="?do=admin&page=calendar&tab=update" style="display:inline-block; padding:8px 16px; text-decoration:none; color:' . ($tab === 'update' ? $accentColor : $colors['text']) . '; border-bottom:3px solid ' . ($tab === 'update' ? $accentColor : 'transparent') . '; font-weight:' . ($tab === 'update' ? 'bold' : 'normal') . ';">�� Update Plugin</a>';
1029ccd446eSAtari911        echo '<a href="?do=admin&page=calendar&tab=config" style="display:inline-block; padding:8px 16px; text-decoration:none; color:' . ($tab === 'config' ? $accentColor : $colors['text']) . '; border-bottom:3px solid ' . ($tab === 'config' ? $accentColor : 'transparent') . '; font-weight:' . ($tab === 'config' ? 'bold' : 'normal') . ';">⚙️ Outlook Sync</a>';
1039ccd446eSAtari911        echo '<a href="?do=admin&page=calendar&tab=themes" style="display:inline-block; padding:8px 16px; text-decoration:none; color:' . ($tab === 'themes' ? $accentColor : $colors['text']) . '; border-bottom:3px solid ' . ($tab === 'themes' ? $accentColor : 'transparent') . '; font-weight:' . ($tab === 'themes' ? 'bold' : 'normal') . ';">�� Themes</a>';
1041d05cddcSAtari911        echo '</div>';
1051d05cddcSAtari911
1061d05cddcSAtari911        // Render appropriate tab
1071d05cddcSAtari911        if ($tab === 'config') {
1089ccd446eSAtari911            $this->renderConfigTab($colors);
1091d05cddcSAtari911        } elseif ($tab === 'manage') {
1109ccd446eSAtari911            $this->renderManageTab($colors);
1119ccd446eSAtari911        } elseif ($tab === 'themes') {
1129ccd446eSAtari911            $this->renderThemesTab($colors);
1131d05cddcSAtari911        } else {
1149ccd446eSAtari911            $this->renderUpdateTab($colors);
1151d05cddcSAtari911        }
1161d05cddcSAtari911    }
1171d05cddcSAtari911
1189ccd446eSAtari911    private function renderConfigTab($colors = null) {
1191d05cddcSAtari911        global $INPUT;
1201d05cddcSAtari911
1219ccd446eSAtari911        // Use defaults if not provided
1229ccd446eSAtari911        if ($colors === null) {
1239ccd446eSAtari911            $colors = $this->getTemplateColors();
1249ccd446eSAtari911        }
1259ccd446eSAtari911
1261d05cddcSAtari911        // Load current config
1271d05cddcSAtari911        $configFile = DOKU_PLUGIN . 'calendar/sync_config.php';
1281d05cddcSAtari911        $config = [];
1291d05cddcSAtari911        if (file_exists($configFile)) {
1301d05cddcSAtari911            $config = include $configFile;
1311d05cddcSAtari911        }
1321d05cddcSAtari911
1331d05cddcSAtari911        // Show message if present
1341d05cddcSAtari911        if ($INPUT->has('msg')) {
1351d05cddcSAtari911            $msg = hsc($INPUT->str('msg'));
1361d05cddcSAtari911            $type = $INPUT->str('msgtype', 'success');
1371d05cddcSAtari911            $class = ($type === 'success') ? 'msg success' : 'msg error';
1381d05cddcSAtari911            echo "<div class=\"$class\" style=\"padding:10px; margin:10px 0; border-left:3px solid " . ($type === 'success' ? '#28a745' : '#dc3545') . "; background:" . ($type === 'success' ? '#d4edda' : '#f8d7da') . "; border-radius:3px;\">";
1391d05cddcSAtari911            echo $msg;
1401d05cddcSAtari911            echo "</div>";
1411d05cddcSAtari911        }
1421d05cddcSAtari911
1431d05cddcSAtari911        echo '<h2 style="margin:10px 0; font-size:20px;">Outlook Sync Configuration</h2>';
1441d05cddcSAtari911
1451d05cddcSAtari911        // Import/Export buttons
1461d05cddcSAtari911        echo '<div style="display:flex; gap:10px; margin-bottom:15px;">';
1471d05cddcSAtari911        echo '<button type="button" onclick="exportConfig()" style="background:#00cc07; color:white; padding:8px 16px; border:none; border-radius:3px; cursor:pointer; font-size:13px; font-weight:bold;">�� Export Config</button>';
1481d05cddcSAtari911        echo '<button type="button" onclick="document.getElementById(\'importFileInput\').click()" style="background:#7b1fa2; color:white; padding:8px 16px; border:none; border-radius:3px; cursor:pointer; font-size:13px; font-weight:bold;">�� Import Config</button>';
1491d05cddcSAtari911        echo '<input type="file" id="importFileInput" accept=".enc" style="display:none;" onchange="importConfig(this)">';
1501d05cddcSAtari911        echo '<span id="importStatus" style="margin-left:10px; font-size:12px;"></span>';
1511d05cddcSAtari911        echo '</div>';
1521d05cddcSAtari911
1531d05cddcSAtari911        echo '<form method="post" action="?do=admin&page=calendar" style="max-width:900px;">';
1541d05cddcSAtari911        echo '<input type="hidden" name="action" value="save_config">';
1551d05cddcSAtari911
1561d05cddcSAtari911        // Azure Credentials
1579ccd446eSAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px;">';
1581d05cddcSAtari911        echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">Microsoft Azure App Credentials</h3>';
1599ccd446eSAtari911        echo '<p style="color:' . $colors['text'] . '; font-size:0.85em; margin:0 0 10px 0;">Register at <a href="https://portal.azure.com" target="_blank" style="color:#00cc07;">Azure Portal</a> → App registrations</p>';
1601d05cddcSAtari911
1611d05cddcSAtari911        echo '<label style="display:block; font-weight:bold; margin:8px 0 3px; font-size:13px;">Tenant ID</label>';
1629ccd446eSAtari911        echo '<input type="text" name="tenant_id" value="' . hsc($config['tenant_id'] ?? '') . '" placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" required style="width:100%; padding:6px; border:1px solid ' . $colors['border'] . '; border-radius:3px; font-size:13px;">';
1631d05cddcSAtari911
1641d05cddcSAtari911        echo '<label style="display:block; font-weight:bold; margin:8px 0 3px; font-size:13px;">Client ID (Application ID)</label>';
1659ccd446eSAtari911        echo '<input type="text" name="client_id" value="' . hsc($config['client_id'] ?? '') . '" placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" required style="width:100%; padding:6px; border:1px solid ' . $colors['border'] . '; border-radius:3px; font-size:13px;">';
1661d05cddcSAtari911
1671d05cddcSAtari911        echo '<label style="display:block; font-weight:bold; margin:8px 0 3px; font-size:13px;">Client Secret</label>';
1689ccd446eSAtari911        echo '<input type="password" name="client_secret" value="' . hsc($config['client_secret'] ?? '') . '" placeholder="Enter client secret" required style="width:100%; padding:6px; border:1px solid ' . $colors['border'] . '; border-radius:3px; font-size:13px;">';
1691d05cddcSAtari911        echo '<p style="color:#999; font-size:0.8em; margin:3px 0 0;">⚠️ Keep this secret safe!</p>';
1701d05cddcSAtari911        echo '</div>';
1711d05cddcSAtari911
1721d05cddcSAtari911        // Outlook Settings
1739ccd446eSAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px;">';
1741d05cddcSAtari911        echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">Outlook Settings</h3>';
1751d05cddcSAtari911
1761d05cddcSAtari911        echo '<div style="display:grid; grid-template-columns:1fr 1fr; gap:10px;">';
1771d05cddcSAtari911
1781d05cddcSAtari911        echo '<div>';
1791d05cddcSAtari911        echo '<label style="display:block; font-weight:bold; margin:0 0 3px; font-size:13px;">User Email</label>';
1809ccd446eSAtari911        echo '<input type="email" name="user_email" value="' . hsc($config['user_email'] ?? '') . '" placeholder="your.email@company.com" required style="width:100%; padding:6px; border:1px solid ' . $colors['border'] . '; border-radius:3px; font-size:13px;">';
1811d05cddcSAtari911        echo '</div>';
1821d05cddcSAtari911
1831d05cddcSAtari911        echo '<div>';
1841d05cddcSAtari911        echo '<label style="display:block; font-weight:bold; margin:0 0 3px; font-size:13px;">Timezone</label>';
1859ccd446eSAtari911        echo '<input type="text" name="timezone" value="' . hsc($config['timezone'] ?? 'America/Los_Angeles') . '" placeholder="America/Los_Angeles" style="width:100%; padding:6px; border:1px solid ' . $colors['border'] . '; border-radius:3px; font-size:13px;">';
1861d05cddcSAtari911        echo '</div>';
1871d05cddcSAtari911
1881d05cddcSAtari911        echo '<div>';
1891d05cddcSAtari911        echo '<label style="display:block; font-weight:bold; margin:0 0 3px; font-size:13px;">Default Category</label>';
1909ccd446eSAtari911        echo '<input type="text" name="default_category" value="' . hsc($config['default_category'] ?? 'Blue category') . '" placeholder="Blue category" style="width:100%; padding:6px; border:1px solid ' . $colors['border'] . '; border-radius:3px; font-size:13px;">';
1911d05cddcSAtari911        echo '</div>';
1921d05cddcSAtari911
1931d05cddcSAtari911        echo '<div>';
1941d05cddcSAtari911        echo '<label style="display:block; font-weight:bold; margin:0 0 3px; font-size:13px;">Reminder (minutes)</label>';
1959ccd446eSAtari911        echo '<input type="number" name="reminder_minutes" value="' . hsc($config['reminder_minutes'] ?? 15) . '" placeholder="15" style="width:100%; padding:6px; border:1px solid ' . $colors['border'] . '; border-radius:3px; font-size:13px;">';
1961d05cddcSAtari911        echo '</div>';
1971d05cddcSAtari911
1981d05cddcSAtari911        echo '</div>'; // end grid
1991d05cddcSAtari911        echo '</div>';
2001d05cddcSAtari911
2011d05cddcSAtari911        // Sync Options
2029ccd446eSAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px;">';
2031d05cddcSAtari911        echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">Sync Options</h3>';
2041d05cddcSAtari911
2051d05cddcSAtari911        $syncCompleted = isset($config['sync_completed_tasks']) ? $config['sync_completed_tasks'] : false;
2061d05cddcSAtari911        echo '<label style="display:inline-block; margin:5px 15px 5px 0; font-size:13px;"><input type="checkbox" name="sync_completed_tasks" value="1" ' . ($syncCompleted ? 'checked' : '') . '> Sync completed tasks</label>';
2071d05cddcSAtari911
2081d05cddcSAtari911        $deleteOutlook = isset($config['delete_outlook_events']) ? $config['delete_outlook_events'] : true;
2091d05cddcSAtari911        echo '<label style="display:inline-block; margin:5px 15px 5px 0; font-size:13px;"><input type="checkbox" name="delete_outlook_events" value="1" ' . ($deleteOutlook ? 'checked' : '') . '> Delete from Outlook when removed</label>';
2101d05cddcSAtari911
2111d05cddcSAtari911        $syncAll = isset($config['sync_all_namespaces']) ? $config['sync_all_namespaces'] : true;
2121d05cddcSAtari911        echo '<label style="display:inline-block; margin:5px 0; font-size:13px;"><input type="checkbox" name="sync_all_namespaces" value="1" onclick="toggleNamespaceSelection(this)" ' . ($syncAll ? 'checked' : '') . '> Sync all namespaces</label>';
2131d05cddcSAtari911
2141d05cddcSAtari911        // Namespace selection (shown when sync_all is unchecked)
2151d05cddcSAtari911        echo '<div id="namespace_selection" style="margin-top:10px; ' . ($syncAll ? 'display:none;' : '') . '">';
2161d05cddcSAtari911        echo '<label style="display:block; font-weight:bold; margin-bottom:5px; font-size:13px;">Select namespaces to sync:</label>';
2171d05cddcSAtari911
2181d05cddcSAtari911        // Get available namespaces
2191d05cddcSAtari911        $availableNamespaces = $this->getAllNamespaces();
2201d05cddcSAtari911        $selectedNamespaces = isset($config['sync_namespaces']) ? $config['sync_namespaces'] : [];
2211d05cddcSAtari911
2229ccd446eSAtari911        echo '<div style="max-height:150px; overflow-y:auto; border:1px solid ' . $colors['border'] . '; border-radius:3px; padding:8px; background:' . $colors['bg'] . ';">';
2231d05cddcSAtari911        echo '<label style="display:block; margin:3px 0;"><input type="checkbox" name="sync_namespaces[]" value=""> (default)</label>';
2241d05cddcSAtari911        foreach ($availableNamespaces as $ns) {
2251d05cddcSAtari911            if ($ns !== '') {
2261d05cddcSAtari911                $checked = in_array($ns, $selectedNamespaces) ? 'checked' : '';
2271d05cddcSAtari911                echo '<label style="display:block; margin:3px 0;"><input type="checkbox" name="sync_namespaces[]" value="' . hsc($ns) . '" ' . $checked . '> ' . hsc($ns) . '</label>';
2281d05cddcSAtari911            }
2291d05cddcSAtari911        }
2301d05cddcSAtari911        echo '</div>';
2311d05cddcSAtari911        echo '</div>';
2321d05cddcSAtari911
2331d05cddcSAtari911        echo '<script>
2341d05cddcSAtari911        function toggleNamespaceSelection(checkbox) {
2351d05cddcSAtari911            document.getElementById("namespace_selection").style.display = checkbox.checked ? "none" : "block";
2361d05cddcSAtari911        }
2371d05cddcSAtari911        </script>';
2381d05cddcSAtari911
2391d05cddcSAtari911        echo '</div>';
2401d05cddcSAtari911
2411d05cddcSAtari911        // Namespace and Color Mapping - Side by Side
2421d05cddcSAtari911        echo '<div style="display:grid; grid-template-columns:1fr 1fr; gap:10px; margin:10px 0;">';
2431d05cddcSAtari911
2441d05cddcSAtari911        // Namespace Mapping
2459ccd446eSAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:12px; border-left:3px solid #00cc07; border-radius:3px;">';
2461d05cddcSAtari911        echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">Namespace → Category</h3>';
2479ccd446eSAtari911        echo '<p style="color:' . $colors['text'] . '; font-size:0.8em; margin:0 0 5px;">One per line: namespace=Category</p>';
2489ccd446eSAtari911        echo '<textarea name="category_mapping" rows="6" style="width:100%; padding:6px; border:1px solid ' . $colors['border'] . '; border-radius:3px; font-family:monospace; font-size:12px; resize:vertical;" placeholder="work=Blue category&#10;personal=Green category">';
2491d05cddcSAtari911        if (isset($config['category_mapping']) && is_array($config['category_mapping'])) {
2501d05cddcSAtari911            foreach ($config['category_mapping'] as $ns => $cat) {
2511d05cddcSAtari911                echo hsc($ns) . '=' . hsc($cat) . "\n";
2521d05cddcSAtari911            }
2531d05cddcSAtari911        }
2541d05cddcSAtari911        echo '</textarea>';
2551d05cddcSAtari911        echo '</div>';
2561d05cddcSAtari911
2571d05cddcSAtari911        // Color Mapping with Color Picker
2589ccd446eSAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:12px; border-left:3px solid #00cc07; border-radius:3px;">';
2591d05cddcSAtari911        echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">�� Event Color → Category</h3>';
2609ccd446eSAtari911        echo '<p style="color:' . $colors['text'] . '; font-size:0.8em; margin:0 0 8px;">Map calendar colors to Outlook categories</p>';
2611d05cddcSAtari911
2621d05cddcSAtari911        // Define calendar colors and Outlook categories (only the main 6 colors)
2631d05cddcSAtari911        $calendarColors = [
2641d05cddcSAtari911            '#3498db' => 'Blue',
2651d05cddcSAtari911            '#2ecc71' => 'Green',
2661d05cddcSAtari911            '#e74c3c' => 'Red',
2671d05cddcSAtari911            '#f39c12' => 'Orange',
2681d05cddcSAtari911            '#9b59b6' => 'Purple',
2691d05cddcSAtari911            '#1abc9c' => 'Teal'
2701d05cddcSAtari911        ];
2711d05cddcSAtari911
2721d05cddcSAtari911        $outlookCategories = [
2731d05cddcSAtari911            'Blue category',
2741d05cddcSAtari911            'Green category',
2751d05cddcSAtari911            'Orange category',
2761d05cddcSAtari911            'Red category',
2771d05cddcSAtari911            'Yellow category',
2781d05cddcSAtari911            'Purple category'
2791d05cddcSAtari911        ];
2801d05cddcSAtari911
2811d05cddcSAtari911        // Load existing color mappings
2821d05cddcSAtari911        $existingMappings = isset($config['color_mapping']) && is_array($config['color_mapping'])
2831d05cddcSAtari911            ? $config['color_mapping']
2841d05cddcSAtari911            : [];
2851d05cddcSAtari911
2861d05cddcSAtari911        // Display color mapping rows
2871d05cddcSAtari911        echo '<div id="colorMappings" style="max-height:200px; overflow-y:auto;">';
2881d05cddcSAtari911
2891d05cddcSAtari911        $rowIndex = 0;
2901d05cddcSAtari911        foreach ($calendarColors as $hexColor => $colorName) {
2911d05cddcSAtari911            $selectedCategory = isset($existingMappings[$hexColor]) ? $existingMappings[$hexColor] : '';
2921d05cddcSAtari911
2931d05cddcSAtari911            echo '<div style="display:flex; gap:8px; align-items:center; margin-bottom:6px;">';
2941d05cddcSAtari911
2951d05cddcSAtari911            // Color preview box
2961d05cddcSAtari911            echo '<div style="width:24px; height:24px; background:' . $hexColor . '; border:2px solid #ddd; border-radius:3px; flex-shrink:0;"></div>';
2971d05cddcSAtari911
2981d05cddcSAtari911            // Color name
2999ccd446eSAtari911            echo '<span style="font-size:12px; min-width:90px; color:' . $colors['text'] . ';">' . $colorName . '</span>';
3001d05cddcSAtari911
3011d05cddcSAtari911            // Arrow
3021d05cddcSAtari911            echo '<span style="color:#999; font-size:12px;">→</span>';
3031d05cddcSAtari911
3041d05cddcSAtari911            // Outlook category dropdown
3059ccd446eSAtari911            echo '<select name="color_map_' . $rowIndex . '" style="flex:1; padding:4px; border:1px solid ' . $colors['border'] . '; border-radius:3px; font-size:12px;">';
3061d05cddcSAtari911            echo '<option value="">-- None --</option>';
3071d05cddcSAtari911            foreach ($outlookCategories as $category) {
3081d05cddcSAtari911                $selected = ($selectedCategory === $category) ? 'selected' : '';
3091d05cddcSAtari911                echo '<option value="' . hsc($category) . '" ' . $selected . '>' . hsc($category) . '</option>';
3101d05cddcSAtari911            }
3111d05cddcSAtari911            echo '</select>';
3121d05cddcSAtari911
3131d05cddcSAtari911            // Hidden input for the hex color
3141d05cddcSAtari911            echo '<input type="hidden" name="color_hex_' . $rowIndex . '" value="' . $hexColor . '">';
3151d05cddcSAtari911
3161d05cddcSAtari911            echo '</div>';
3171d05cddcSAtari911            $rowIndex++;
3181d05cddcSAtari911        }
3191d05cddcSAtari911
3201d05cddcSAtari911        echo '</div>';
3211d05cddcSAtari911
3221d05cddcSAtari911        // Hidden input to track number of color mappings
3231d05cddcSAtari911        echo '<input type="hidden" name="color_mapping_count" value="' . $rowIndex . '">';
3241d05cddcSAtari911
3251d05cddcSAtari911        echo '</div>';
3261d05cddcSAtari911
3271d05cddcSAtari911        echo '</div>'; // end grid
3281d05cddcSAtari911
3291d05cddcSAtari911        // Submit button
3301d05cddcSAtari911        echo '<button type="submit" style="background:#00cc07; color:white; padding:10px 20px; border:none; border-radius:3px; cursor:pointer; font-size:14px; font-weight:bold; margin:10px 0;">�� Save Configuration</button>';
3311d05cddcSAtari911        echo '</form>';
3321d05cddcSAtari911
3331d05cddcSAtari911        // JavaScript for Import/Export
3341d05cddcSAtari911        echo '<script>
3351d05cddcSAtari911        async function exportConfig() {
3361d05cddcSAtari911            try {
3371d05cddcSAtari911                const response = await fetch("?do=admin&page=calendar&action=export_config&call=ajax", {
3381d05cddcSAtari911                    method: "POST"
3391d05cddcSAtari911                });
3401d05cddcSAtari911                const data = await response.json();
3411d05cddcSAtari911
3421d05cddcSAtari911                if (data.success) {
3431d05cddcSAtari911                    // Create download link
3441d05cddcSAtari911                    const blob = new Blob([data.encrypted], {type: "application/octet-stream"});
3451d05cddcSAtari911                    const url = URL.createObjectURL(blob);
3461d05cddcSAtari911                    const a = document.createElement("a");
3471d05cddcSAtari911                    a.href = url;
3481d05cddcSAtari911                    a.download = "sync_config_" + new Date().toISOString().split("T")[0] + ".enc";
3491d05cddcSAtari911                    document.body.appendChild(a);
3501d05cddcSAtari911                    a.click();
3511d05cddcSAtari911                    document.body.removeChild(a);
3521d05cddcSAtari911                    URL.revokeObjectURL(url);
3531d05cddcSAtari911
3541d05cddcSAtari911                    alert("✅ Config exported successfully!\\n\\n⚠️ This file contains encrypted credentials.\\nKeep it secure!");
3551d05cddcSAtari911                } else {
3561d05cddcSAtari911                    alert("❌ Export failed: " + data.message);
3571d05cddcSAtari911                }
3581d05cddcSAtari911            } catch (error) {
3591d05cddcSAtari911                alert("❌ Error: " + error.message);
3601d05cddcSAtari911            }
3611d05cddcSAtari911        }
3621d05cddcSAtari911
3631d05cddcSAtari911        async function importConfig(input) {
3641d05cddcSAtari911            const file = input.files[0];
3651d05cddcSAtari911            if (!file) return;
3661d05cddcSAtari911
3671d05cddcSAtari911            const status = document.getElementById("importStatus");
3681d05cddcSAtari911            status.textContent = "⏳ Importing...";
3691d05cddcSAtari911            status.style.color = "#00cc07";
3701d05cddcSAtari911
3711d05cddcSAtari911            try {
3721d05cddcSAtari911                const encrypted = await file.text();
3731d05cddcSAtari911
3741d05cddcSAtari911                const formData = new FormData();
3751d05cddcSAtari911                formData.append("encrypted_config", encrypted);
3761d05cddcSAtari911
3771d05cddcSAtari911                const response = await fetch("?do=admin&page=calendar&action=import_config&call=ajax", {
3781d05cddcSAtari911                    method: "POST",
3791d05cddcSAtari911                    body: formData
3801d05cddcSAtari911                });
3811d05cddcSAtari911                const data = await response.json();
3821d05cddcSAtari911
3831d05cddcSAtari911                if (data.success) {
3841d05cddcSAtari911                    status.textContent = "✅ Import successful! Reloading...";
3851d05cddcSAtari911                    status.style.color = "#28a745";
3861d05cddcSAtari911                    setTimeout(() => {
3871d05cddcSAtari911                        window.location.reload();
3881d05cddcSAtari911                    }, 1500);
3891d05cddcSAtari911                } else {
3901d05cddcSAtari911                    status.textContent = "❌ Import failed: " + data.message;
3911d05cddcSAtari911                    status.style.color = "#dc3545";
3921d05cddcSAtari911                }
3931d05cddcSAtari911            } catch (error) {
3941d05cddcSAtari911                status.textContent = "❌ Error: " + error.message;
3951d05cddcSAtari911                status.style.color = "#dc3545";
3961d05cddcSAtari911            }
3971d05cddcSAtari911
3981d05cddcSAtari911            // Reset file input
3991d05cddcSAtari911            input.value = "";
4001d05cddcSAtari911        }
4011d05cddcSAtari911        </script>';
4021d05cddcSAtari911
4031d05cddcSAtari911        // Sync Controls Section
4049ccd446eSAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:15px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:900px;">';
4051d05cddcSAtari911        echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">�� Sync Controls</h3>';
4061d05cddcSAtari911
4071d05cddcSAtari911        // Check cron job status
4081d05cddcSAtari911        $cronStatus = $this->getCronStatus();
4091d05cddcSAtari911
4101d05cddcSAtari911        // Check log file permissions
4111d05cddcSAtari911        $logFile = DOKU_PLUGIN . 'calendar/sync.log';
4121d05cddcSAtari911        $logWritable = is_writable($logFile) || is_writable(dirname($logFile));
4131d05cddcSAtari911
4141d05cddcSAtari911        echo '<div style="display:flex; gap:10px; align-items:center; margin-bottom:10px;">';
4151d05cddcSAtari911        echo '<button onclick="runSyncNow()" id="syncBtn" style="background:#00cc07; color:white; padding:8px 16px; border:none; border-radius:3px; cursor:pointer; font-size:13px; font-weight:bold;">▶️ Run Sync Now</button>';
4161d05cddcSAtari911        echo '<button onclick="stopSyncNow()" id="stopBtn" style="background:#e74c3c; color:white; padding:8px 16px; border:none; border-radius:3px; cursor:pointer; font-size:13px; font-weight:bold; display:none;">⏹️ Stop Sync</button>';
4171d05cddcSAtari911
4181d05cddcSAtari911        if ($cronStatus['active']) {
4199ccd446eSAtari911            echo '<span style="color:' . $colors['text'] . '; font-size:12px;">⏰ ' . hsc($cronStatus['frequency']) . '</span>';
4201d05cddcSAtari911        } else {
4211d05cddcSAtari911            echo '<span style="color:#999; font-size:12px;">⚠️ No cron job detected</span>';
4221d05cddcSAtari911        }
4231d05cddcSAtari911
4249ccd446eSAtari911        echo '<span id="syncStatus" style="color:' . $colors['text'] . '; font-size:12px; margin-left:auto;"></span>';
4251d05cddcSAtari911        echo '</div>';
4261d05cddcSAtari911
4271d05cddcSAtari911        // Show permission warning if log not writable
4281d05cddcSAtari911        if (!$logWritable) {
4291d05cddcSAtari911            echo '<div style="background:#fff3e0; border-left:3px solid #ff9800; padding:8px; margin:8px 0; border-radius:3px;">';
4301d05cddcSAtari911            echo '<span style="color:#e65100; font-size:11px;">⚠️ Log file not writable. Run: <code style="background:#f0f0f0; padding:2px 4px; border-radius:2px;">chmod 666 ' . $logFile . '</code></span>';
4311d05cddcSAtari911            echo '</div>';
4321d05cddcSAtari911        }
4331d05cddcSAtari911
4341d05cddcSAtari911        // Show debug info if cron detected
4351d05cddcSAtari911        if ($cronStatus['active'] && !empty($cronStatus['full_line'])) {
4361d05cddcSAtari911            echo '<details style="margin-top:5px;">';
4371d05cddcSAtari911            echo '<summary style="cursor:pointer; color:#999; font-size:11px;">Show cron details</summary>';
4381d05cddcSAtari911            echo '<pre style="background:#f0f0f0; padding:8px; border-radius:3px; font-size:10px; margin:5px 0; overflow-x:auto;">' . hsc($cronStatus['full_line']) . '</pre>';
4391d05cddcSAtari911            echo '</details>';
4401d05cddcSAtari911        }
4411d05cddcSAtari911
4421d05cddcSAtari911        if (!$cronStatus['active']) {
4431d05cddcSAtari911            echo '<p style="color:#999; font-size:11px; margin:5px 0;">To enable automatic syncing, add to crontab: <code style="background:#f0f0f0; padding:2px 4px; border-radius:2px;">*/30 * * * * cd ' . DOKU_PLUGIN . 'calendar && php sync_outlook.php</code></p>';
4441d05cddcSAtari911        }
4451d05cddcSAtari911
4461d05cddcSAtari911        echo '</div>';
4471d05cddcSAtari911
4481d05cddcSAtari911        // JavaScript for Run Sync Now
4491d05cddcSAtari911        echo '<script>
4501d05cddcSAtari911        let syncAbortController = null;
4511d05cddcSAtari911
4521d05cddcSAtari911        function runSyncNow() {
4531d05cddcSAtari911            const btn = document.getElementById("syncBtn");
4541d05cddcSAtari911            const stopBtn = document.getElementById("stopBtn");
4551d05cddcSAtari911            const status = document.getElementById("syncStatus");
4561d05cddcSAtari911
4571d05cddcSAtari911            btn.disabled = true;
4581d05cddcSAtari911            btn.style.display = "none";
4591d05cddcSAtari911            stopBtn.style.display = "inline-block";
4601d05cddcSAtari911            btn.textContent = "⏳ Running...";
4611d05cddcSAtari911            btn.style.background = "#999";
4621d05cddcSAtari911            status.textContent = "Starting sync...";
4631d05cddcSAtari911            status.style.color = "#00cc07";
4641d05cddcSAtari911
4651d05cddcSAtari911            // Create abort controller for this sync
4661d05cddcSAtari911            syncAbortController = new AbortController();
4671d05cddcSAtari911
4681d05cddcSAtari911            fetch("?do=admin&page=calendar&action=run_sync&call=ajax", {
4691d05cddcSAtari911                method: "POST",
4701d05cddcSAtari911                signal: syncAbortController.signal
4711d05cddcSAtari911            })
4721d05cddcSAtari911                .then(response => response.json())
4731d05cddcSAtari911                .then(data => {
4741d05cddcSAtari911                    if (data.success) {
4751d05cddcSAtari911                        status.textContent = "✅ " + data.message;
4761d05cddcSAtari911                        status.style.color = "#28a745";
4771d05cddcSAtari911                    } else {
4781d05cddcSAtari911                        status.textContent = "❌ " + data.message;
4791d05cddcSAtari911                        status.style.color = "#dc3545";
4801d05cddcSAtari911                    }
4811d05cddcSAtari911                    btn.disabled = false;
4821d05cddcSAtari911                    btn.style.display = "inline-block";
4831d05cddcSAtari911                    stopBtn.style.display = "none";
4841d05cddcSAtari911                    btn.textContent = "▶️ Run Sync Now";
4851d05cddcSAtari911                    btn.style.background = "#00cc07";
4861d05cddcSAtari911                    syncAbortController = null;
4871d05cddcSAtari911
4881d05cddcSAtari911                    // Clear status after 10 seconds
4891d05cddcSAtari911                    setTimeout(() => {
4901d05cddcSAtari911                        status.textContent = "";
4911d05cddcSAtari911                    }, 10000);
4921d05cddcSAtari911                })
4931d05cddcSAtari911                .catch(error => {
4941d05cddcSAtari911                    if (error.name === "AbortError") {
4951d05cddcSAtari911                        status.textContent = "⏹️ Sync stopped by user";
4961d05cddcSAtari911                        status.style.color = "#ff9800";
4971d05cddcSAtari911                    } else {
4981d05cddcSAtari911                        status.textContent = "❌ Error: " + error.message;
4991d05cddcSAtari911                        status.style.color = "#dc3545";
5001d05cddcSAtari911                    }
5011d05cddcSAtari911                    btn.disabled = false;
5021d05cddcSAtari911                    btn.style.display = "inline-block";
5031d05cddcSAtari911                    stopBtn.style.display = "none";
5041d05cddcSAtari911                    btn.textContent = "▶️ Run Sync Now";
5051d05cddcSAtari911                    btn.style.background = "#00cc07";
5061d05cddcSAtari911                    syncAbortController = null;
5071d05cddcSAtari911                });
5081d05cddcSAtari911        }
5091d05cddcSAtari911
5101d05cddcSAtari911        function stopSyncNow() {
5111d05cddcSAtari911            const status = document.getElementById("syncStatus");
5121d05cddcSAtari911
5131d05cddcSAtari911            status.textContent = "⏹️ Sending stop signal...";
5141d05cddcSAtari911            status.style.color = "#ff9800";
5151d05cddcSAtari911
5161d05cddcSAtari911            // First, send stop signal to server
5171d05cddcSAtari911            fetch("?do=admin&page=calendar&action=stop_sync&call=ajax", {
5181d05cddcSAtari911                method: "POST"
5191d05cddcSAtari911            })
5201d05cddcSAtari911            .then(response => response.json())
5211d05cddcSAtari911            .then(data => {
5221d05cddcSAtari911                if (data.success) {
5231d05cddcSAtari911                    status.textContent = "⏹️ Stop signal sent - sync will abort soon";
5241d05cddcSAtari911                    status.style.color = "#ff9800";
5251d05cddcSAtari911                } else {
5261d05cddcSAtari911                    status.textContent = "⚠️ " + data.message;
5271d05cddcSAtari911                    status.style.color = "#ff9800";
5281d05cddcSAtari911                }
5291d05cddcSAtari911            })
5301d05cddcSAtari911            .catch(error => {
5311d05cddcSAtari911                status.textContent = "⚠️ Error sending stop signal: " + error.message;
5321d05cddcSAtari911                status.style.color = "#ff9800";
5331d05cddcSAtari911            });
5341d05cddcSAtari911
5351d05cddcSAtari911            // Also abort the fetch request
5361d05cddcSAtari911            if (syncAbortController) {
5371d05cddcSAtari911                syncAbortController.abort();
5381d05cddcSAtari911                status.textContent = "⏹️ Stopping sync...";
5391d05cddcSAtari911                status.style.color = "#ff9800";
5401d05cddcSAtari911            }
5411d05cddcSAtari911        }
5421d05cddcSAtari911        </script>';
5431d05cddcSAtari911
5441d05cddcSAtari911        // Log Viewer Section - More Compact
5459ccd446eSAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:15px 0 10px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:900px;">';
5461d05cddcSAtari911        echo '<h3 style="margin:0 0 5px 0; color:#00cc07; font-size:16px;">�� Live Sync Log</h3>';
5479ccd446eSAtari911        echo '<p style="color:' . $colors['text'] . '; font-size:0.8em; margin:0 0 8px;">Updates every 2 seconds</p>';
5481d05cddcSAtari911
5491d05cddcSAtari911        // Log viewer container
5501d05cddcSAtari911        echo '<div style="background:#1e1e1e; border-radius:5px; overflow:hidden; box-shadow:0 2px 4px rgba(0,0,0,0.3);">';
5511d05cddcSAtari911
5521d05cddcSAtari911        // Log header - More compact
5531d05cddcSAtari911        echo '<div style="background:#2d2d2d; padding:6px 10px; display:flex; justify-content:space-between; align-items:center; border-bottom:1px solid #444;">';
5541d05cddcSAtari911        echo '<span style="color:#00cc07; font-family:monospace; font-weight:bold; font-size:12px;">sync.log</span>';
5551d05cddcSAtari911        echo '<div>';
5561d05cddcSAtari911        echo '<button id="pauseBtn" onclick="togglePause()" style="background:#666; color:white; border:none; padding:4px 8px; border-radius:3px; cursor:pointer; margin-right:4px; font-size:11px;">⏸ Pause</button>';
5571d05cddcSAtari911        echo '<button onclick="clearLog()" style="background:#e74c3c; color:white; border:none; padding:4px 8px; border-radius:3px; cursor:pointer; margin-right:4px; font-size:11px;">��️ Clear</button>';
5581d05cddcSAtari911        echo '<button onclick="downloadLog()" style="background:#666; color:white; border:none; padding:4px 8px; border-radius:3px; cursor:pointer; font-size:11px;">�� Download</button>';
5591d05cddcSAtari911        echo '</div>';
5601d05cddcSAtari911        echo '</div>';
5611d05cddcSAtari911
5621d05cddcSAtari911        // Log content - Reduced height to 250px
5631d05cddcSAtari911        echo '<pre id="logContent" style="background:#1e1e1e; color:#00cc07; font-family:monospace; font-size:11px; padding:10px; margin:0; overflow-x:auto; white-space:pre-wrap; word-wrap:break-word; line-height:1.4; max-height:250px; overflow-y:auto;">Loading log...</pre>';
5641d05cddcSAtari911
5651d05cddcSAtari911        echo '</div>';
5661d05cddcSAtari911        echo '</div>';
5671d05cddcSAtari911
5681d05cddcSAtari911        // JavaScript for log viewer
5691d05cddcSAtari911        echo '<script>
5701d05cddcSAtari911        let refreshInterval = null;
5711d05cddcSAtari911        let isPaused = false;
5721d05cddcSAtari911
5731d05cddcSAtari911        function refreshLog() {
5741d05cddcSAtari911            if (isPaused) return;
5751d05cddcSAtari911
5761d05cddcSAtari911            fetch("?do=admin&page=calendar&action=get_log&call=ajax")
5771d05cddcSAtari911                .then(response => response.json())
5781d05cddcSAtari911                .then(data => {
5791d05cddcSAtari911                    const logContent = document.getElementById("logContent");
5801d05cddcSAtari911                    if (logContent) {
5811d05cddcSAtari911                        logContent.textContent = data.log || "No log data available";
5821d05cddcSAtari911                        logContent.scrollTop = logContent.scrollHeight;
5831d05cddcSAtari911                    }
5841d05cddcSAtari911                })
5851d05cddcSAtari911                .catch(error => {
5861d05cddcSAtari911                    console.error("Error fetching log:", error);
5871d05cddcSAtari911                });
5881d05cddcSAtari911        }
5891d05cddcSAtari911
5901d05cddcSAtari911        function togglePause() {
5911d05cddcSAtari911            isPaused = !isPaused;
5921d05cddcSAtari911            const btn = document.getElementById("pauseBtn");
5931d05cddcSAtari911            if (isPaused) {
5941d05cddcSAtari911                btn.textContent = "▶ Resume";
5951d05cddcSAtari911                btn.style.background = "#00cc07";
5961d05cddcSAtari911            } else {
5971d05cddcSAtari911                btn.textContent = "⏸ Pause";
5981d05cddcSAtari911                btn.style.background = "#666";
5991d05cddcSAtari911                refreshLog();
6001d05cddcSAtari911            }
6011d05cddcSAtari911        }
6021d05cddcSAtari911
6031d05cddcSAtari911        function clearLog() {
6041d05cddcSAtari911            if (!confirm("Clear the sync log file?\\n\\nThis will delete all log entries.")) {
6051d05cddcSAtari911                return;
6061d05cddcSAtari911            }
6071d05cddcSAtari911
6081d05cddcSAtari911            fetch("?do=admin&page=calendar&action=clear_log&call=ajax", {
6091d05cddcSAtari911                method: "POST"
6101d05cddcSAtari911            })
6111d05cddcSAtari911                .then(response => response.json())
6121d05cddcSAtari911                .then(data => {
6131d05cddcSAtari911                    if (data.success) {
6141d05cddcSAtari911                        refreshLog();
6151d05cddcSAtari911                        alert("Log cleared successfully");
6161d05cddcSAtari911                    } else {
6171d05cddcSAtari911                        alert("Error clearing log: " + data.message);
6181d05cddcSAtari911                    }
6191d05cddcSAtari911                })
6201d05cddcSAtari911                .catch(error => {
6211d05cddcSAtari911                    alert("Error: " + error.message);
6221d05cddcSAtari911                });
6231d05cddcSAtari911        }
6241d05cddcSAtari911
6251d05cddcSAtari911        function downloadLog() {
6261d05cddcSAtari911            window.location.href = "?do=admin&page=calendar&action=download_log";
6271d05cddcSAtari911        }
6281d05cddcSAtari911
6291d05cddcSAtari911        // Start auto-refresh
6301d05cddcSAtari911        refreshLog();
6311d05cddcSAtari911        refreshInterval = setInterval(refreshLog, 2000);
6321d05cddcSAtari911
6331d05cddcSAtari911        // Cleanup on page unload
6341d05cddcSAtari911        window.addEventListener("beforeunload", function() {
6351d05cddcSAtari911            if (refreshInterval) {
6361d05cddcSAtari911                clearInterval(refreshInterval);
6371d05cddcSAtari911            }
6381d05cddcSAtari911        });
6391d05cddcSAtari911        </script>';
6401d05cddcSAtari911    }
6411d05cddcSAtari911
6429ccd446eSAtari911    private function renderManageTab($colors = null) {
6431d05cddcSAtari911        global $INPUT;
6441d05cddcSAtari911
6459ccd446eSAtari911        // Use defaults if not provided
6469ccd446eSAtari911        if ($colors === null) {
6479ccd446eSAtari911            $colors = $this->getTemplateColors();
6489ccd446eSAtari911        }
6499ccd446eSAtari911
6501d05cddcSAtari911        // Show message if present
6511d05cddcSAtari911        if ($INPUT->has('msg')) {
6521d05cddcSAtari911            $msg = hsc($INPUT->str('msg'));
6531d05cddcSAtari911            $type = $INPUT->str('msgtype', 'success');
6541d05cddcSAtari911            echo "<div style=\"padding:10px; margin:10px 0; border-left:3px solid " . ($type === 'success' ? '#28a745' : '#dc3545') . "; background:" . ($type === 'success' ? '#d4edda' : '#f8d7da') . "; border-radius:3px;\">";
6551d05cddcSAtari911            echo $msg;
6561d05cddcSAtari911            echo "</div>";
6571d05cddcSAtari911        }
6581d05cddcSAtari911
6591d05cddcSAtari911        echo '<h2 style="margin:10px 0; font-size:20px;">Manage Calendar Events</h2>';
6601d05cddcSAtari911
6619ccd446eSAtari911        // Events Manager Section
6629ccd446eSAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:1200px;">';
6631d05cddcSAtari911        echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">�� Events Manager</h3>';
6649ccd446eSAtari911        echo '<p style="color:' . $colors['text'] . '; font-size:11px; margin:0 0 10px;">Scan, export, and import all calendar events across all namespaces.</p>';
6651d05cddcSAtari911
6661d05cddcSAtari911        // Get event statistics
6671d05cddcSAtari911        $stats = $this->getEventStatistics();
6681d05cddcSAtari911
6691d05cddcSAtari911        // Statistics display
6709ccd446eSAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:10px; border-radius:3px; margin-bottom:10px; border:1px solid ' . $colors['border'] . ';">';
6711d05cddcSAtari911        echo '<div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(150px, 1fr)); gap:10px; font-size:12px;">';
6721d05cddcSAtari911
6731d05cddcSAtari911        echo '<div style="background:#f3e5f5; padding:8px; border-radius:3px; text-align:center;">';
6741d05cddcSAtari911        echo '<div style="font-size:24px; font-weight:bold; color:#7b1fa2;">' . $stats['total_events'] . '</div>';
6759ccd446eSAtari911        echo '<div style="color:' . $colors['text'] . '; font-size:10px;">Total Events</div>';
6761d05cddcSAtari911        echo '</div>';
6771d05cddcSAtari911
6781d05cddcSAtari911        echo '<div style="background:#f3e5f5; padding:8px; border-radius:3px; text-align:center;">';
6791d05cddcSAtari911        echo '<div style="font-size:24px; font-weight:bold; color:#7b1fa2;">' . $stats['total_namespaces'] . '</div>';
6809ccd446eSAtari911        echo '<div style="color:' . $colors['text'] . '; font-size:10px;">Namespaces</div>';
6811d05cddcSAtari911        echo '</div>';
6821d05cddcSAtari911
6831d05cddcSAtari911        echo '<div style="background:#e8f5e9; padding:8px; border-radius:3px; text-align:center;">';
6841d05cddcSAtari911        echo '<div style="font-size:24px; font-weight:bold; color:#388e3c;">' . $stats['total_files'] . '</div>';
6859ccd446eSAtari911        echo '<div style="color:' . $colors['text'] . '; font-size:10px;">JSON Files</div>';
6861d05cddcSAtari911        echo '</div>';
6871d05cddcSAtari911
6881d05cddcSAtari911        echo '<div style="background:#fff3e0; padding:8px; border-radius:3px; text-align:center;">';
6891d05cddcSAtari911        echo '<div style="font-size:24px; font-weight:bold; color:#f57c00;">' . $stats['total_recurring'] . '</div>';
6909ccd446eSAtari911        echo '<div style="color:' . $colors['text'] . '; font-size:10px;">Recurring</div>';
6911d05cddcSAtari911        echo '</div>';
6921d05cddcSAtari911
6931d05cddcSAtari911        echo '</div>';
6941d05cddcSAtari911
6951d05cddcSAtari911        // Last scan time
6961d05cddcSAtari911        if (!empty($stats['last_scan'])) {
6979ccd446eSAtari911            echo '<div style="margin-top:8px; color:' . $colors['text'] . '; font-size:10px;">Last scanned: ' . hsc($stats['last_scan']) . '</div>';
6981d05cddcSAtari911        }
6991d05cddcSAtari911
7001d05cddcSAtari911        echo '</div>';
7011d05cddcSAtari911
7021d05cddcSAtari911        // Action buttons
7031d05cddcSAtari911        echo '<div style="display:flex; gap:10px; flex-wrap:wrap; margin-top:10px;">';
7041d05cddcSAtari911
7051d05cddcSAtari911        // Rescan button
7061d05cddcSAtari911        echo '<form method="post" action="?do=admin&page=calendar&tab=manage" style="display:inline;">';
7071d05cddcSAtari911        echo '<input type="hidden" name="action" value="rescan_events">';
7081d05cddcSAtari911        echo '<button type="submit" style="background:#00cc07; color:white; border:none; padding:8px 16px; border-radius:3px; cursor:pointer; font-size:12px; display:flex; align-items:center; gap:6px;">';
7091d05cddcSAtari911        echo '<span>��</span><span>Re-scan Events</span>';
7101d05cddcSAtari911        echo '</button>';
7111d05cddcSAtari911        echo '</form>';
7121d05cddcSAtari911
7131d05cddcSAtari911        // Export button
7141d05cddcSAtari911        echo '<form method="post" action="?do=admin&page=calendar&tab=manage" style="display:inline;">';
7151d05cddcSAtari911        echo '<input type="hidden" name="action" value="export_all_events">';
7161d05cddcSAtari911        echo '<button type="submit" style="background:#7b1fa2; color:white; border:none; padding:8px 16px; border-radius:3px; cursor:pointer; font-size:12px; display:flex; align-items:center; gap:6px;">';
7171d05cddcSAtari911        echo '<span>��</span><span>Export All Events</span>';
7181d05cddcSAtari911        echo '</button>';
7191d05cddcSAtari911        echo '</form>';
7201d05cddcSAtari911
7211d05cddcSAtari911        // Import button (with file upload)
7221d05cddcSAtari911        echo '<form method="post" action="?do=admin&page=calendar&tab=manage" enctype="multipart/form-data" style="display:inline;" onsubmit="return confirm(\'Import will merge with existing events. Continue?\')">';
7231d05cddcSAtari911        echo '<input type="hidden" name="action" value="import_all_events">';
7241d05cddcSAtari911        echo '<label style="background:#7b1fa2; color:white; border:none; padding:8px 16px; border-radius:3px; cursor:pointer; font-size:12px; display:inline-flex; align-items:center; gap:6px;">';
7251d05cddcSAtari911        echo '<span>��</span><span>Import Events</span>';
7261d05cddcSAtari911        echo '<input type="file" name="import_file" accept=".json,.zip" required style="display:none;" onchange="this.form.submit()">';
7271d05cddcSAtari911        echo '</label>';
7281d05cddcSAtari911        echo '</form>';
7291d05cddcSAtari911
7301d05cddcSAtari911        echo '</div>';
7311d05cddcSAtari911
7321d05cddcSAtari911        // Breakdown by namespace
7331d05cddcSAtari911        if (!empty($stats['by_namespace'])) {
7341d05cddcSAtari911            echo '<details style="margin-top:12px;">';
7351d05cddcSAtari911            echo '<summary style="cursor:pointer; padding:6px; background:#e9e9e9; border-radius:3px; font-size:11px; font-weight:bold;">View Breakdown by Namespace</summary>';
7369ccd446eSAtari911            echo '<div style="margin-top:8px; max-height:200px; overflow-y:auto; border:1px solid ' . $colors['border'] . '; border-radius:3px;">';
7371d05cddcSAtari911            echo '<table style="width:100%; border-collapse:collapse; font-size:11px;">';
7381d05cddcSAtari911            echo '<thead style="position:sticky; top:0; background:#f5f5f5;">';
7391d05cddcSAtari911            echo '<tr>';
7401d05cddcSAtari911            echo '<th style="padding:4px 6px; text-align:left; border-bottom:2px solid #ddd;">Namespace</th>';
7411d05cddcSAtari911            echo '<th style="padding:4px 6px; text-align:right; border-bottom:2px solid #ddd;">Events</th>';
7421d05cddcSAtari911            echo '<th style="padding:4px 6px; text-align:right; border-bottom:2px solid #ddd;">Files</th>';
7431d05cddcSAtari911            echo '</tr></thead><tbody>';
7441d05cddcSAtari911
7451d05cddcSAtari911            foreach ($stats['by_namespace'] as $ns => $nsStats) {
7461d05cddcSAtari911                echo '<tr style="border-bottom:1px solid #eee;">';
7471d05cddcSAtari911                echo '<td style="padding:4px 6px;"><code style="background:#f0f0f0; padding:1px 3px; border-radius:2px; font-size:10px;">' . hsc($ns ?: '(default)') . '</code></td>';
7481d05cddcSAtari911                echo '<td style="padding:4px 6px; text-align:right;"><strong>' . $nsStats['events'] . '</strong></td>';
7491d05cddcSAtari911                echo '<td style="padding:4px 6px; text-align:right;">' . $nsStats['files'] . '</td>';
7501d05cddcSAtari911                echo '</tr>';
7511d05cddcSAtari911            }
7521d05cddcSAtari911
7531d05cddcSAtari911            echo '</tbody></table>';
7541d05cddcSAtari911            echo '</div>';
7551d05cddcSAtari911            echo '</details>';
7561d05cddcSAtari911        }
7571d05cddcSAtari911
7581d05cddcSAtari911        echo '</div>';
7591d05cddcSAtari911
760*4590242dSAtari911        // Important Namespaces Section
761*4590242dSAtari911        $configFile = DOKU_PLUGIN . 'calendar/sync_config.php';
762*4590242dSAtari911        $importantConfig = [];
763*4590242dSAtari911        if (file_exists($configFile)) {
764*4590242dSAtari911            $importantConfig = include $configFile;
765*4590242dSAtari911        }
766*4590242dSAtari911        $importantNsValue = isset($importantConfig['important_namespaces']) ? $importantConfig['important_namespaces'] : 'important';
767*4590242dSAtari911
768*4590242dSAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:1200px;">';
769*4590242dSAtari911        echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">�� Important Namespaces (Sidebar Widget)</h3>';
770*4590242dSAtari911        echo '<p style="color:' . $colors['text'] . '; font-size:11px; margin:0 0 8px;">Events from these namespaces will be highlighted in purple in the sidebar widget\'s "Important Events" section.</p>';
771*4590242dSAtari911        echo '<form method="post" action="?do=admin&page=calendar&tab=manage" style="display:flex; gap:8px; align-items:center;">';
772*4590242dSAtari911        echo '<input type="hidden" name="action" value="save_important_namespaces">';
773*4590242dSAtari911        echo '<input type="text" name="important_namespaces" value="' . hsc($importantNsValue) . '" style="flex:1; padding:6px; border:1px solid ' . $colors['border'] . '; border-radius:3px; font-size:12px;" placeholder="important,urgent,priority">';
774*4590242dSAtari911        echo '<button type="submit" style="background:#00cc07; color:white; padding:6px 16px; border:none; border-radius:3px; cursor:pointer; font-size:12px; font-weight:bold; white-space:nowrap;">Save</button>';
775*4590242dSAtari911        echo '</form>';
776*4590242dSAtari911        echo '<p style="color:' . $colors['text'] . '; font-size:10px; margin:4px 0 0;">Comma-separated list of namespace names</p>';
777*4590242dSAtari911        echo '</div>';
778*4590242dSAtari911
7799ccd446eSAtari911        // Cleanup Events Section
7809ccd446eSAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:1200px;">';
7819ccd446eSAtari911        echo '<h3 style="margin:0 0 6px 0; color:#00cc07; font-size:16px;">�� Cleanup Old Events</h3>';
7829ccd446eSAtari911        echo '<p style="color:' . $colors['text'] . '; font-size:11px; margin:0 0 12px;">Delete events based on criteria below. Automatic backup created before deletion.</p>';
7831d05cddcSAtari911
7841d05cddcSAtari911        echo '<form method="post" action="?do=admin&page=calendar&tab=manage" id="cleanupForm">';
7851d05cddcSAtari911        echo '<input type="hidden" name="action" value="cleanup_events">';
7861d05cddcSAtari911
7871d05cddcSAtari911        // Compact options layout
7889ccd446eSAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:10px; border:1px solid ' . $colors['border'] . '; border-radius:3px; margin-bottom:10px;">';
7891d05cddcSAtari911
7901d05cddcSAtari911        // Radio buttons in a row
7911d05cddcSAtari911        echo '<div style="display:flex; gap:20px; margin-bottom:10px; padding-bottom:8px; border-bottom:1px solid #f0f0f0;">';
7921d05cddcSAtari911        echo '<label style="cursor:pointer; font-size:12px; font-weight:600; display:flex; align-items:center; gap:4px;">';
7931d05cddcSAtari911        echo '<input type="radio" name="cleanup_type" value="age" checked onchange="updateCleanupOptions()">';
7941d05cddcSAtari911        echo '<span>By Age</span>';
7951d05cddcSAtari911        echo '</label>';
7961d05cddcSAtari911        echo '<label style="cursor:pointer; font-size:12px; font-weight:600; display:flex; align-items:center; gap:4px;">';
7971d05cddcSAtari911        echo '<input type="radio" name="cleanup_type" value="status" onchange="updateCleanupOptions()">';
7981d05cddcSAtari911        echo '<span>By Status</span>';
7991d05cddcSAtari911        echo '</label>';
8001d05cddcSAtari911        echo '<label style="cursor:pointer; font-size:12px; font-weight:600; display:flex; align-items:center; gap:4px;">';
8011d05cddcSAtari911        echo '<input type="radio" name="cleanup_type" value="range" onchange="updateCleanupOptions()">';
8021d05cddcSAtari911        echo '<span>By Date Range</span>';
8031d05cddcSAtari911        echo '</label>';
8041d05cddcSAtari911        echo '</div>';
8051d05cddcSAtari911
8061d05cddcSAtari911        // Age options
8071d05cddcSAtari911        echo '<div id="age-options" style="padding:6px 0;">';
8089ccd446eSAtari911        echo '<span style="font-size:11px; color:' . $colors['text'] . '; margin-right:8px;">Delete events older than:</span>';
8091d05cddcSAtari911        echo '<select name="age_value" style="width:50px; padding:3px 4px; font-size:11px; border:1px solid #d0d0d0; border-radius:3px; margin-right:4px;">';
8101d05cddcSAtari911        for ($i = 1; $i <= 24; $i++) {
8111d05cddcSAtari911            $sel = $i === 6 ? ' selected' : '';
8121d05cddcSAtari911            echo '<option value="' . $i . '"' . $sel . '>' . $i . '</option>';
8131d05cddcSAtari911        }
8141d05cddcSAtari911        echo '</select>';
8151d05cddcSAtari911        echo '<select name="age_unit" style="width:80px; padding:3px 4px; font-size:11px; border:1px solid #d0d0d0; border-radius:3px;">';
8161d05cddcSAtari911        echo '<option value="months" selected>months</option>';
8171d05cddcSAtari911        echo '<option value="years">years</option>';
8181d05cddcSAtari911        echo '</select>';
8191d05cddcSAtari911        echo '</div>';
8201d05cddcSAtari911
8211d05cddcSAtari911        // Status options
8221d05cddcSAtari911        echo '<div id="status-options" style="padding:6px 0; opacity:0.4;">';
8239ccd446eSAtari911        echo '<span style="font-size:11px; color:' . $colors['text'] . '; margin-right:8px;">Delete:</span>';
8241d05cddcSAtari911        echo '<label style="display:inline-block; font-size:11px; margin-right:12px; cursor:pointer;"><input type="checkbox" name="delete_completed" value="1" style="margin-right:3px;"> Completed tasks</label>';
8251d05cddcSAtari911        echo '<label style="display:inline-block; font-size:11px; cursor:pointer;"><input type="checkbox" name="delete_past" value="1" style="margin-right:3px;"> Past events</label>';
8261d05cddcSAtari911        echo '</div>';
8271d05cddcSAtari911
8281d05cddcSAtari911        // Range options
8291d05cddcSAtari911        echo '<div id="range-options" style="padding:6px 0; opacity:0.4;">';
8309ccd446eSAtari911        echo '<span style="font-size:11px; color:' . $colors['text'] . '; margin-right:8px;">From:</span>';
8311d05cddcSAtari911        echo '<input type="date" name="range_start" style="padding:3px 6px; font-size:11px; border:1px solid #d0d0d0; border-radius:3px; margin-right:10px;">';
8329ccd446eSAtari911        echo '<span style="font-size:11px; color:' . $colors['text'] . '; margin-right:8px;">To:</span>';
8331d05cddcSAtari911        echo '<input type="date" name="range_end" style="padding:3px 6px; font-size:11px; border:1px solid #d0d0d0; border-radius:3px;">';
8341d05cddcSAtari911        echo '</div>';
8351d05cddcSAtari911
8361d05cddcSAtari911        echo '</div>';
8371d05cddcSAtari911
8381d05cddcSAtari911        // Namespace filter - compact
8399ccd446eSAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:8px 10px; border:1px solid ' . $colors['border'] . '; border-radius:3px; margin-bottom:10px; display:flex; align-items:center; gap:8px;">';
8401d05cddcSAtari911        echo '<label style="font-size:11px; font-weight:600; white-space:nowrap; color:#555;">Namespace:</label>';
8411d05cddcSAtari911        echo '<input type="text" name="namespace_filter" placeholder="Leave empty for all, or specify: work, personal, etc." style="flex:1; padding:4px 8px; font-size:11px; border:1px solid #d0d0d0; border-radius:3px;">';
8421d05cddcSAtari911        echo '</div>';
8431d05cddcSAtari911
8441d05cddcSAtari911        // Action buttons - compact row
8451d05cddcSAtari911        echo '<div style="display:flex; gap:8px; align-items:center;">';
8461d05cddcSAtari911        echo '<button type="button" onclick="previewCleanup()" style="background:#7b1fa2; color:white; border:none; padding:6px 14px; border-radius:3px; cursor:pointer; font-size:11px; font-weight:600;">��️ Preview</button>';
8471d05cddcSAtari911        echo '<button type="submit" onclick="return confirmCleanup()" style="background:#dc3545; color:white; border:none; padding:6px 14px; border-radius:3px; cursor:pointer; font-size:11px; font-weight:600;">��️ Delete</button>';
8481d05cddcSAtari911        echo '<span style="font-size:10px; color:#999;">⚠️ Backup created automatically</span>';
8491d05cddcSAtari911        echo '</div>';
8501d05cddcSAtari911
8511d05cddcSAtari911        echo '</form>';
8521d05cddcSAtari911
8531d05cddcSAtari911        // Preview results area
8541d05cddcSAtari911        echo '<div id="cleanup-preview" style="margin-top:10px; display:none;"></div>';
8551d05cddcSAtari911
8561d05cddcSAtari911        echo '<script>
8571d05cddcSAtari911        function updateCleanupOptions() {
8581d05cddcSAtari911            const type = document.querySelector(\'input[name="cleanup_type"]:checked\').value;
8591d05cddcSAtari911
8601d05cddcSAtari911            // Show selected, gray out others
8611d05cddcSAtari911            document.getElementById(\'age-options\').style.opacity = type === \'age\' ? \'1\' : \'0.4\';
8621d05cddcSAtari911            document.getElementById(\'status-options\').style.opacity = type === \'status\' ? \'1\' : \'0.4\';
8631d05cddcSAtari911            document.getElementById(\'range-options\').style.opacity = type === \'range\' ? \'1\' : \'0.4\';
8641d05cddcSAtari911
8651d05cddcSAtari911            // Enable/disable inputs
8661d05cddcSAtari911            document.querySelectorAll(\'#age-options select\').forEach(el => el.disabled = type !== \'age\');
8671d05cddcSAtari911            document.querySelectorAll(\'#status-options input\').forEach(el => el.disabled = type !== \'status\');
8681d05cddcSAtari911            document.querySelectorAll(\'#range-options input\').forEach(el => el.disabled = type !== \'range\');
8691d05cddcSAtari911        }
8701d05cddcSAtari911
8711d05cddcSAtari911        function previewCleanup() {
8721d05cddcSAtari911            const form = document.getElementById(\'cleanupForm\');
8731d05cddcSAtari911            const formData = new FormData(form);
8741d05cddcSAtari911            formData.set(\'action\', \'preview_cleanup\');
8751d05cddcSAtari911
8761d05cddcSAtari911            const preview = document.getElementById(\'cleanup-preview\');
8779ccd446eSAtari911            preview.innerHTML = \'<div style="text-align:center; padding:20px; color:' . $colors['text'] . ';">Loading preview...</div>\';
8781d05cddcSAtari911            preview.style.display = \'block\';
8791d05cddcSAtari911
8801d05cddcSAtari911            fetch(\'?do=admin&page=calendar&tab=manage\', {
8811d05cddcSAtari911                method: \'POST\',
8821d05cddcSAtari911                body: new URLSearchParams(formData)
8831d05cddcSAtari911            })
8841d05cddcSAtari911            .then(r => r.json())
8851d05cddcSAtari911            .then(data => {
8861d05cddcSAtari911                if (data.count === 0) {
8871d05cddcSAtari911                    let html = \'<div style="background:#d4edda; border:1px solid #c3e6cb; padding:10px; border-radius:3px; font-size:12px; color:#155724;">✅ No events match the criteria. Nothing would be deleted.</div>\';
8881d05cddcSAtari911
8891d05cddcSAtari911                    // Show debug info if available
8901d05cddcSAtari911                    if (data.debug) {
8919ccd446eSAtari911                        html += \'<details style="margin-top:8px; font-size:11px; color:' . $colors['text'] . ';">\';
8921d05cddcSAtari911                        html += \'<summary style="cursor:pointer;">Debug Info</summary>\';
8931d05cddcSAtari911                        html += \'<pre style="background:#f5f5f5; padding:6px; margin-top:4px; border-radius:3px; overflow-x:auto;">\' + JSON.stringify(data.debug, null, 2) + \'</pre>\';
8941d05cddcSAtari911                        html += \'</details>\';
8951d05cddcSAtari911                    }
8961d05cddcSAtari911
8971d05cddcSAtari911                    preview.innerHTML = html;
8981d05cddcSAtari911                } else {
8991d05cddcSAtari911                    let html = \'<div style="background:#f8d7da; border:1px solid #f5c6cb; padding:10px; border-radius:3px; font-size:12px; color:#721c24;">\';
9001d05cddcSAtari911                    html += \'<strong>⚠️ Warning:</strong> The following \' + data.count + \' event(s) would be deleted:<br><br>\';
9019ccd446eSAtari911                    html += \'<div style="max-height:150px; overflow-y:auto; margin-top:6px; background:' . $colors['bg'] . '; padding:6px; border-radius:3px;">\';
9021d05cddcSAtari911                    data.events.forEach(evt => {
9031d05cddcSAtari911                        html += \'<div style="padding:3px; border-bottom:1px solid #eee; font-size:11px;">\';
9041d05cddcSAtari911                        html += \'\' + evt.title + \' (\' + evt.date + \')\';
9051d05cddcSAtari911                        if (evt.namespace) html += \' <span style="background:#e3f2fd; padding:1px 4px; border-radius:2px; font-size:9px;">\' + evt.namespace + \'</span>\';
9061d05cddcSAtari911                        html += \'</div>\';
9071d05cddcSAtari911                    });
9081d05cddcSAtari911                    html += \'</div></div>\';
9091d05cddcSAtari911                    preview.innerHTML = html;
9101d05cddcSAtari911                }
9111d05cddcSAtari911            })
9121d05cddcSAtari911            .catch(err => {
9131d05cddcSAtari911                preview.innerHTML = \'<div style="background:#f8d7da; border:1px solid #f5c6cb; padding:10px; border-radius:3px; font-size:12px; color:#721c24;">Error loading preview</div>\';
9141d05cddcSAtari911            });
9151d05cddcSAtari911        }
9161d05cddcSAtari911
9171d05cddcSAtari911        function confirmCleanup() {
9181d05cddcSAtari911            return confirm(\'Are you sure you want to delete these events? A backup will be created first, but this action cannot be easily undone.\');
9191d05cddcSAtari911        }
9201d05cddcSAtari911
9211d05cddcSAtari911        updateCleanupOptions();
9221d05cddcSAtari911        </script>';
9231d05cddcSAtari911
9241d05cddcSAtari911        echo '</div>';
9251d05cddcSAtari911
9261d05cddcSAtari911        // Recurring Events Section
9279ccd446eSAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:1200px;">';
9281d05cddcSAtari911        echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">�� Recurring Events</h3>';
9291d05cddcSAtari911
9301d05cddcSAtari911        $recurringEvents = $this->findRecurringEvents();
9311d05cddcSAtari911
9321d05cddcSAtari911        if (empty($recurringEvents)) {
9339ccd446eSAtari911            echo '<p style="color:' . $colors['text'] . '; font-size:13px; margin:5px 0;">No recurring events found.</p>';
9341d05cddcSAtari911        } else {
9351d05cddcSAtari911            // Search bar
9361d05cddcSAtari911            echo '<div style="margin-bottom:8px;">';
9379ccd446eSAtari911            echo '<input type="text" id="searchRecurring" onkeyup="filterRecurringEvents()" placeholder="�� Search recurring events..." style="width:100%; padding:6px 10px; border:1px solid ' . $colors['border'] . '; border-radius:3px; font-size:12px;">';
9381d05cddcSAtari911            echo '</div>';
9391d05cddcSAtari911
9401d05cddcSAtari911            echo '<style>
9411d05cddcSAtari911                .sort-arrow {
9421d05cddcSAtari911                    color: #999;
9431d05cddcSAtari911                    font-size: 10px;
9441d05cddcSAtari911                    margin-left: 3px;
9451d05cddcSAtari911                    display: inline-block;
9461d05cddcSAtari911                }
9471d05cddcSAtari911                #recurringTable th:hover {
9481d05cddcSAtari911                    background: #ddd;
9491d05cddcSAtari911                }
9501d05cddcSAtari911                #recurringTable th:hover .sort-arrow {
9511d05cddcSAtari911                    color: #00cc07;
9521d05cddcSAtari911                }
9531d05cddcSAtari911                .recurring-row-hidden {
9541d05cddcSAtari911                    display: none;
9551d05cddcSAtari911                }
9561d05cddcSAtari911            </style>';
9579ccd446eSAtari911            echo '<div style="max-height:250px; overflow-y:auto; border:1px solid ' . $colors['border'] . '; border-radius:3px;">';
9581d05cddcSAtari911            echo '<table id="recurringTable" style="width:100%; border-collapse:collapse; font-size:11px;">';
9591d05cddcSAtari911            echo '<thead style="position:sticky; top:0; background:#e9e9e9;">';
9601d05cddcSAtari911            echo '<tr>';
9611d05cddcSAtari911            echo '<th onclick="sortRecurringTable(0)" style="padding:4px 6px; text-align:left; border-bottom:2px solid #ddd; cursor:pointer; user-select:none;">Title <span class="sort-arrow">⇅</span></th>';
9621d05cddcSAtari911            echo '<th onclick="sortRecurringTable(1)" style="padding:4px 6px; text-align:left; border-bottom:2px solid #ddd; cursor:pointer; user-select:none;">Namespace <span class="sort-arrow">⇅</span></th>';
9631d05cddcSAtari911            echo '<th onclick="sortRecurringTable(2)" style="padding:4px 6px; text-align:left; border-bottom:2px solid #ddd; cursor:pointer; user-select:none;">Pattern <span class="sort-arrow">⇅</span></th>';
9641d05cddcSAtari911            echo '<th onclick="sortRecurringTable(3)" style="padding:4px 6px; text-align:left; border-bottom:2px solid #ddd; cursor:pointer; user-select:none;">First <span class="sort-arrow">⇅</span></th>';
9651d05cddcSAtari911            echo '<th onclick="sortRecurringTable(4)" style="padding:4px 6px; text-align:left; border-bottom:2px solid #ddd; cursor:pointer; user-select:none;">Count <span class="sort-arrow">⇅</span></th>';
9661d05cddcSAtari911            echo '<th style="padding:4px 6px; text-align:left; border-bottom:2px solid #ddd;">Actions</th>';
9671d05cddcSAtari911            echo '</tr></thead><tbody id="recurringTableBody">';
9681d05cddcSAtari911
9691d05cddcSAtari911            foreach ($recurringEvents as $series) {
9701d05cddcSAtari911                echo '<tr style="border-bottom:1px solid #eee;">';
9711d05cddcSAtari911                echo '<td style="padding:4px 6px;">' . hsc($series['title']) . '</td>';
9721d05cddcSAtari911                echo '<td style="padding:4px 6px;"><code style="background:#f0f0f0; padding:1px 3px; border-radius:2px; font-size:10px;">' . hsc($series['namespace'] ?: '(default)') . '</code></td>';
9731d05cddcSAtari911                echo '<td style="padding:4px 6px;">' . hsc($series['pattern']) . '</td>';
9741d05cddcSAtari911                echo '<td style="padding:4px 6px;">' . hsc($series['firstDate']) . '</td>';
9751d05cddcSAtari911                echo '<td style="padding:4px 6px;"><strong>' . $series['count'] . '</strong></td>';
9761d05cddcSAtari911                echo '<td style="padding:4px 6px; white-space:nowrap;">';
9771d05cddcSAtari911                echo '<button onclick="editRecurringSeries(\'' . hsc(addslashes($series['title'])) . '\', \'' . hsc($series['namespace']) . '\')" style="background:#00cc07; color:white; border:none; padding:2px 6px; border-radius:2px; cursor:pointer; font-size:10px; margin-right:2px;">Edit</button>';
9781d05cddcSAtari911                echo '<button onclick="deleteRecurringSeries(\'' . hsc(addslashes($series['title'])) . '\', \'' . hsc($series['namespace']) . '\')" style="background:#e74c3c; color:white; border:none; padding:2px 6px; border-radius:2px; cursor:pointer; font-size:10px;">Del</button>';
9791d05cddcSAtari911                echo '</td>';
9801d05cddcSAtari911                echo '</tr>';
9811d05cddcSAtari911            }
9821d05cddcSAtari911
9831d05cddcSAtari911            echo '</tbody></table>';
9841d05cddcSAtari911            echo '</div>';
9859ccd446eSAtari911            echo '<p style="color:' . $colors['text'] . '; font-size:10px; margin:5px 0 0;">Total: ' . count($recurringEvents) . ' series</p>';
9861d05cddcSAtari911        }
9871d05cddcSAtari911        echo '</div>';
9881d05cddcSAtari911
9891d05cddcSAtari911        // Compact Tree-based Namespace Manager
9909ccd446eSAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:1200px;">';
9911d05cddcSAtari911        echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">�� Namespace Explorer</h3>';
9929ccd446eSAtari911        echo '<p style="color:' . $colors['text'] . '; font-size:11px; margin:0 0 8px;">Select events and move between namespaces. Drag & drop also supported.</p>';
9931d05cddcSAtari911
9941d05cddcSAtari911        // Search bar
9951d05cddcSAtari911        echo '<div style="margin-bottom:8px;">';
9969ccd446eSAtari911        echo '<input type="text" id="searchEvents" onkeyup="filterEvents()" placeholder="�� Search events by title..." style="width:100%; padding:6px 10px; border:1px solid ' . $colors['border'] . '; border-radius:3px; font-size:12px;">';
9971d05cddcSAtari911        echo '</div>';
9981d05cddcSAtari911
9991d05cddcSAtari911        $eventsByNamespace = $this->getEventsByNamespace();
10001d05cddcSAtari911
10011d05cddcSAtari911        // Control bar
10021d05cddcSAtari911        echo '<form method="post" action="?do=admin&page=calendar&tab=manage" id="moveForm">';
10031d05cddcSAtari911        echo '<input type="hidden" name="action" value="move_selected_events" id="formAction">';
10041d05cddcSAtari911        echo '<div style="background:#2d2d2d; color:white; padding:6px 10px; border-radius:3px; margin-bottom:8px; display:flex; gap:8px; align-items:center; font-size:12px;">';
10051d05cddcSAtari911        echo '<button type="button" onclick="selectAll()" style="background:#00cc07; color:white; border:none; padding:4px 8px; border-radius:2px; cursor:pointer; font-size:11px;">☑ All</button>';
10061d05cddcSAtari911        echo '<button type="button" onclick="deselectAll()" style="background:#666; color:white; border:none; padding:4px 8px; border-radius:2px; cursor:pointer; font-size:11px;">☐ None</button>';
10071d05cddcSAtari911        echo '<button type="button" onclick="deleteSelected()" style="background:#e74c3c; color:white; border:none; padding:4px 8px; border-radius:2px; cursor:pointer; font-size:11px; margin-left:10px;">��️ Delete</button>';
10081d05cddcSAtari911        echo '<span style="margin-left:10px;">Move to:</span>';
10099ccd446eSAtari911        echo '<input list="namespaceList" name="target_namespace" required style="padding:3px 6px; border:1px solid ' . $colors['border'] . '; border-radius:2px; font-size:11px; min-width:150px;" placeholder="Type or select...">';
10101d05cddcSAtari911        echo '<datalist id="namespaceList">';
10111d05cddcSAtari911        echo '<option value="">(default)</option>';
10121d05cddcSAtari911        foreach (array_keys($eventsByNamespace) as $ns) {
10131d05cddcSAtari911            if ($ns !== '') {
10141d05cddcSAtari911                echo '<option value="' . hsc($ns) . '">' . hsc($ns) . '</option>';
10151d05cddcSAtari911            }
10161d05cddcSAtari911        }
10171d05cddcSAtari911        echo '</datalist>';
10181d05cddcSAtari911        echo '<button type="submit" style="background:#00cc07; color:white; border:none; padding:4px 10px; border-radius:2px; cursor:pointer; font-size:11px; font-weight:bold;">➡️ Move</button>';
10191d05cddcSAtari911        echo '<button type="button" onclick="createNewNamespace()" style="background:#7b1fa2; color:white; border:none; padding:4px 10px; border-radius:2px; cursor:pointer; font-size:11px; font-weight:bold; margin-left:5px;">➕ New Namespace</button>';
10201d05cddcSAtari911        echo '<span id="selectedCount" style="margin-left:auto; color:#00cc07; font-size:11px;">0 selected</span>';
10211d05cddcSAtari911        echo '</div>';
10221d05cddcSAtari911
10231d05cddcSAtari911        echo '<div style="display:grid; grid-template-columns:1fr 1fr; gap:10px;">';
10241d05cddcSAtari911
10251d05cddcSAtari911        // Event list with checkboxes
10261d05cddcSAtari911        echo '<div>';
10279ccd446eSAtari911        echo '<div style="max-height:450px; overflow-y:auto; border:1px solid ' . $colors['border'] . '; border-radius:3px; background:' . $colors['bg'] . ';">';
10281d05cddcSAtari911
10291d05cddcSAtari911        foreach ($eventsByNamespace as $namespace => $data) {
10301d05cddcSAtari911            $nsId = 'ns_' . md5($namespace);
10311d05cddcSAtari911            $eventCount = count($data['events']);
10321d05cddcSAtari911
10331d05cddcSAtari911            echo '<div style="border-bottom:1px solid #ddd;">';
10341d05cddcSAtari911
10351d05cddcSAtari911            // Namespace header - ultra compact
10361d05cddcSAtari911            echo '<div style="background:#f5f5f5; padding:3px 6px; display:flex; justify-content:space-between; align-items:center; font-size:11px;">';
10371d05cddcSAtari911            echo '<div style="display:flex; align-items:center; gap:4px;">';
10381d05cddcSAtari911            echo '<span onclick="toggleNamespace(\'' . $nsId . '\')" style="cursor:pointer; width:12px; display:inline-block; font-size:10px;"><span id="' . $nsId . '_arrow">▶</span></span>';
10391d05cddcSAtari911            echo '<input type="checkbox" onclick="toggleNamespaceSelect(\'' . $nsId . '\')" id="' . $nsId . '_check" style="margin:0; width:12px; height:12px;">';
10401d05cddcSAtari911            echo '<span onclick="toggleNamespace(\'' . $nsId . '\')" style="cursor:pointer; font-weight:600; font-size:11px;">�� ' . hsc($namespace ?: '(default)') . '</span>';
10411d05cddcSAtari911            echo '</div>';
10421d05cddcSAtari911            echo '<div style="display:flex; gap:3px; align-items:center;">';
10431d05cddcSAtari911            echo '<span style="background:#00cc07; color:white; padding:0px 4px; border-radius:6px; font-size:9px; line-height:14px;">' . $eventCount . '</span>';
10449ccd446eSAtari911            echo '<button type="button" onclick="renameNamespace(\'' . hsc($namespace) . '\')" style="background:#3498db; color:white; border:none; padding:1px 4px; border-radius:2px; cursor:pointer; font-size:9px; line-height:14px;" title="Rename namespace">✏️</button>';
10451d05cddcSAtari911            echo '<button type="button" onclick="deleteNamespace(\'' . hsc($namespace) . '\')" style="background:#e74c3c; color:white; border:none; padding:1px 4px; border-radius:2px; cursor:pointer; font-size:9px; line-height:14px;">��️</button>';
10461d05cddcSAtari911            echo '</div>';
10471d05cddcSAtari911            echo '</div>';
10481d05cddcSAtari911
10491d05cddcSAtari911            // Events - ultra compact
10501d05cddcSAtari911            echo '<div id="' . $nsId . '" style="display:none; max-height:150px; overflow-y:auto;">';
10511d05cddcSAtari911            foreach ($data['events'] as $event) {
10521d05cddcSAtari911                $eventId = $event['id'] . '|' . $namespace . '|' . $event['date'] . '|' . $event['month'];
10531d05cddcSAtari911                $checkId = 'evt_' . md5($eventId);
10541d05cddcSAtari911
10551d05cddcSAtari911                echo '<div draggable="true" ondragstart="dragStart(event, \'' . hsc($eventId) . '\')" style="padding:2px 6px 2px 16px; border-bottom:1px solid #f8f8f8; display:flex; align-items:center; gap:4px; font-size:10px; cursor:move;" class="event-row" onmouseover="this.style.background=\'#f9f9f9\'" onmouseout="this.style.background=\'white\'">';
10561d05cddcSAtari911                echo '<input type="checkbox" name="events[]" value="' . hsc($eventId) . '" class="event-checkbox ' . $nsId . '_events" id="' . $checkId . '" onclick="updateCount()" style="margin:0; width:12px; height:12px;">';
10571d05cddcSAtari911                echo '<div style="flex:1; min-width:0;">';
10581d05cddcSAtari911                echo '<div style="font-weight:500; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; font-size:10px;">' . hsc($event['title']) . '</div>';
10591d05cddcSAtari911                echo '<div style="color:#999; font-size:9px;">' . hsc($event['date']) . ($event['startTime'] ? ' • ' . hsc($event['startTime']) : '') . '</div>';
10601d05cddcSAtari911                echo '</div>';
10611d05cddcSAtari911                echo '</div>';
10621d05cddcSAtari911            }
10631d05cddcSAtari911            echo '</div>';
10641d05cddcSAtari911            echo '</div>';
10651d05cddcSAtari911        }
10661d05cddcSAtari911
10671d05cddcSAtari911        echo '</div>';
10681d05cddcSAtari911        echo '</div>';
10691d05cddcSAtari911
10701d05cddcSAtari911        // Drop zones - ultra compact
10711d05cddcSAtari911        echo '<div>';
10721d05cddcSAtari911        echo '<div style="background:#00cc07; color:white; padding:3px 6px; border-radius:3px 3px 0 0; font-size:11px; font-weight:bold;">�� Drop Target</div>';
10739ccd446eSAtari911        echo '<div style="border:1px solid ' . $colors['border'] . '; border-top:none; border-radius:0 0 3px 3px; max-height:450px; overflow-y:auto; background:' . $colors['bg'] . ';">';
10741d05cddcSAtari911
10751d05cddcSAtari911        foreach (array_keys($eventsByNamespace) as $namespace) {
10769ccd446eSAtari911            echo '<div ondrop="drop(event, \'' . hsc($namespace) . '\')" ondragover="allowDrop(event)" style="padding:5px 6px; border-bottom:1px solid #eee; background:' . $colors['bg'] . '; min-height:28px;" onmouseover="this.style.background=\'#f0fff0\'" onmouseout="this.style.background=\'white\'">';
10771d05cddcSAtari911            echo '<div style="font-size:11px; font-weight:600; color:#00cc07;">�� ' . hsc($namespace ?: '(default)') . '</div>';
10781d05cddcSAtari911            echo '<div style="color:#999; font-size:9px; margin-top:1px;">Drop here</div>';
10791d05cddcSAtari911            echo '</div>';
10801d05cddcSAtari911        }
10811d05cddcSAtari911
10821d05cddcSAtari911        echo '</div>';
10831d05cddcSAtari911        echo '</div>';
10841d05cddcSAtari911
10851d05cddcSAtari911        echo '</div>'; // end grid
10861d05cddcSAtari911        echo '</form>';
10871d05cddcSAtari911
10881d05cddcSAtari911        echo '</div>';
10891d05cddcSAtari911
10901d05cddcSAtari911        // JavaScript
10911d05cddcSAtari911        echo '<script>
10921d05cddcSAtari911        // Table sorting functionality - defined early so onclick handlers work
10931d05cddcSAtari911        let sortDirection = {}; // Track sort direction for each column
10941d05cddcSAtari911
10951d05cddcSAtari911        function sortRecurringTable(columnIndex) {
10961d05cddcSAtari911            const table = document.getElementById("recurringTable");
10971d05cddcSAtari911            const tbody = document.getElementById("recurringTableBody");
10981d05cddcSAtari911
10999ccd446eSAtari911            if (!table || !tbody) return;
11001d05cddcSAtari911
11011d05cddcSAtari911            const rows = Array.from(tbody.querySelectorAll("tr"));
11029ccd446eSAtari911            if (rows.length === 0) return;
11031d05cddcSAtari911
11041d05cddcSAtari911            // Toggle sort direction for this column
11051d05cddcSAtari911            if (!sortDirection[columnIndex]) {
11061d05cddcSAtari911                sortDirection[columnIndex] = "asc";
11071d05cddcSAtari911            } else {
11081d05cddcSAtari911                sortDirection[columnIndex] = sortDirection[columnIndex] === "asc" ? "desc" : "asc";
11091d05cddcSAtari911            }
11101d05cddcSAtari911
11111d05cddcSAtari911            const direction = sortDirection[columnIndex];
11121d05cddcSAtari911            const isNumeric = columnIndex === 4; // Count column
11131d05cddcSAtari911
11141d05cddcSAtari911            // Sort rows
11151d05cddcSAtari911            rows.sort((a, b) => {
11161d05cddcSAtari911                let aValue = a.cells[columnIndex].textContent.trim();
11171d05cddcSAtari911                let bValue = b.cells[columnIndex].textContent.trim();
11181d05cddcSAtari911
11191d05cddcSAtari911                // Extract text from code elements for namespace column
11201d05cddcSAtari911                if (columnIndex === 1) {
11211d05cddcSAtari911                    const aCode = a.cells[columnIndex].querySelector("code");
11221d05cddcSAtari911                    const bCode = b.cells[columnIndex].querySelector("code");
11231d05cddcSAtari911                    aValue = aCode ? aCode.textContent.trim() : aValue;
11241d05cddcSAtari911                    bValue = bCode ? bCode.textContent.trim() : bValue;
11251d05cddcSAtari911                }
11261d05cddcSAtari911
11271d05cddcSAtari911                // Extract number from strong elements for count column
11281d05cddcSAtari911                if (isNumeric) {
11291d05cddcSAtari911                    const aStrong = a.cells[columnIndex].querySelector("strong");
11301d05cddcSAtari911                    const bStrong = b.cells[columnIndex].querySelector("strong");
11311d05cddcSAtari911                    aValue = aStrong ? parseInt(aStrong.textContent.trim()) : 0;
11321d05cddcSAtari911                    bValue = bStrong ? parseInt(bStrong.textContent.trim()) : 0;
11331d05cddcSAtari911
11341d05cddcSAtari911                    return direction === "asc" ? aValue - bValue : bValue - aValue;
11351d05cddcSAtari911                }
11361d05cddcSAtari911
11371d05cddcSAtari911                // String comparison
11381d05cddcSAtari911                if (direction === "asc") {
11391d05cddcSAtari911                    return aValue.localeCompare(bValue);
11401d05cddcSAtari911                } else {
11411d05cddcSAtari911                    return bValue.localeCompare(aValue);
11421d05cddcSAtari911                }
11431d05cddcSAtari911            });
11441d05cddcSAtari911
11451d05cddcSAtari911            // Update arrows
11461d05cddcSAtari911            const headers = table.querySelectorAll("th");
11471d05cddcSAtari911            headers.forEach((header, index) => {
11481d05cddcSAtari911                const arrow = header.querySelector(".sort-arrow");
11491d05cddcSAtari911                if (arrow) {
11501d05cddcSAtari911                    if (index === columnIndex) {
11511d05cddcSAtari911                        arrow.textContent = direction === "asc" ? "↑" : "↓";
11521d05cddcSAtari911                        arrow.style.color = "#00cc07";
11531d05cddcSAtari911                    } else {
11541d05cddcSAtari911                        arrow.textContent = "⇅";
11551d05cddcSAtari911                        arrow.style.color = "#999";
11561d05cddcSAtari911                    }
11571d05cddcSAtari911                }
11581d05cddcSAtari911            });
11591d05cddcSAtari911
11601d05cddcSAtari911            // Rebuild tbody
11611d05cddcSAtari911            rows.forEach(row => tbody.appendChild(row));
11621d05cddcSAtari911        }
11631d05cddcSAtari911
11641d05cddcSAtari911        function filterRecurringEvents() {
11651d05cddcSAtari911            const searchInput = document.getElementById("searchRecurring");
11661d05cddcSAtari911            const filter = normalizeText(searchInput.value);
11671d05cddcSAtari911            const tbody = document.getElementById("recurringTableBody");
11681d05cddcSAtari911            const rows = tbody.getElementsByTagName("tr");
11691d05cddcSAtari911
11701d05cddcSAtari911            for (let i = 0; i < rows.length; i++) {
11711d05cddcSAtari911                const row = rows[i];
11721d05cddcSAtari911                const titleCell = row.getElementsByTagName("td")[0];
11731d05cddcSAtari911
11741d05cddcSAtari911                if (titleCell) {
11751d05cddcSAtari911                    const titleText = normalizeText(titleCell.textContent || titleCell.innerText);
11761d05cddcSAtari911
11771d05cddcSAtari911                    if (titleText.indexOf(filter) > -1) {
11781d05cddcSAtari911                        row.classList.remove("recurring-row-hidden");
11791d05cddcSAtari911                    } else {
11801d05cddcSAtari911                        row.classList.add("recurring-row-hidden");
11811d05cddcSAtari911                    }
11821d05cddcSAtari911                }
11831d05cddcSAtari911            }
11841d05cddcSAtari911        }
11851d05cddcSAtari911
11861d05cddcSAtari911        function normalizeText(text) {
11871d05cddcSAtari911            // Convert to lowercase
11881d05cddcSAtari911            text = text.toLowerCase();
11891d05cddcSAtari911
11901d05cddcSAtari911            // Remove apostrophes and quotes
11911d05cddcSAtari911            text = text.replace(/[\'\"]/g, "");
11921d05cddcSAtari911
11931d05cddcSAtari911            // Replace accented characters with regular ones
11941d05cddcSAtari911            text = text.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
11951d05cddcSAtari911
11961d05cddcSAtari911            // Remove special characters except spaces and alphanumeric
11971d05cddcSAtari911            text = text.replace(/[^a-z0-9\s]/g, "");
11981d05cddcSAtari911
11991d05cddcSAtari911            // Collapse multiple spaces
12001d05cddcSAtari911            text = text.replace(/\s+/g, " ");
12011d05cddcSAtari911
12021d05cddcSAtari911            return text.trim();
12031d05cddcSAtari911        }
12041d05cddcSAtari911
12051d05cddcSAtari911        function filterEvents() {
12061d05cddcSAtari911            const searchText = normalizeText(document.getElementById("searchEvents").value);
12071d05cddcSAtari911            const eventRows = document.querySelectorAll(".event-row");
12081d05cddcSAtari911            let visibleCount = 0;
12091d05cddcSAtari911
12101d05cddcSAtari911            eventRows.forEach(row => {
12111d05cddcSAtari911                const titleElement = row.querySelector("div div");
12121d05cddcSAtari911                const originalTitle = titleElement.getAttribute("data-original-title") || titleElement.textContent;
12131d05cddcSAtari911
12141d05cddcSAtari911                // Store original title if not already stored
12151d05cddcSAtari911                if (!titleElement.getAttribute("data-original-title")) {
12161d05cddcSAtari911                    titleElement.setAttribute("data-original-title", originalTitle);
12171d05cddcSAtari911                }
12181d05cddcSAtari911
12191d05cddcSAtari911                const normalizedTitle = normalizeText(originalTitle);
12201d05cddcSAtari911
12211d05cddcSAtari911                if (normalizedTitle.includes(searchText) || searchText === "") {
12221d05cddcSAtari911                    row.style.display = "flex";
12231d05cddcSAtari911                    visibleCount++;
12241d05cddcSAtari911                } else {
12251d05cddcSAtari911                    row.style.display = "none";
12261d05cddcSAtari911                }
12271d05cddcSAtari911            });
12281d05cddcSAtari911
12291d05cddcSAtari911            // Update namespace visibility and counts
12301d05cddcSAtari911            document.querySelectorAll("[id^=ns_]").forEach(nsDiv => {
12311d05cddcSAtari911                if (nsDiv.id.endsWith("_arrow") || nsDiv.id.endsWith("_check")) return;
12321d05cddcSAtari911
12331d05cddcSAtari911                const visibleEvents = nsDiv.querySelectorAll(".event-row[style*=\\"display: flex\\"], .event-row:not([style*=\\"display: none\\"])").length;
12341d05cddcSAtari911                const nsId = nsDiv.id;
12351d05cddcSAtari911                const arrow = document.getElementById(nsId + "_arrow");
12361d05cddcSAtari911
12371d05cddcSAtari911                // Auto-expand namespaces with matches when searching
12381d05cddcSAtari911                if (searchText && visibleEvents > 0) {
12391d05cddcSAtari911                    nsDiv.style.display = "block";
12401d05cddcSAtari911                    if (arrow) arrow.textContent = "▼";
12411d05cddcSAtari911                }
12421d05cddcSAtari911            });
12431d05cddcSAtari911        }
12441d05cddcSAtari911
12451d05cddcSAtari911        function toggleNamespace(id) {
12461d05cddcSAtari911            const elem = document.getElementById(id);
12471d05cddcSAtari911            const arrow = document.getElementById(id + "_arrow");
12481d05cddcSAtari911            if (elem.style.display === "none") {
12491d05cddcSAtari911                elem.style.display = "block";
12501d05cddcSAtari911                arrow.textContent = "▼";
12511d05cddcSAtari911            } else {
12521d05cddcSAtari911                elem.style.display = "none";
12531d05cddcSAtari911                arrow.textContent = "▶";
12541d05cddcSAtari911            }
12551d05cddcSAtari911        }
12561d05cddcSAtari911
12571d05cddcSAtari911        function toggleNamespaceSelect(nsId) {
12581d05cddcSAtari911            const checkbox = document.getElementById(nsId + "_check");
12591d05cddcSAtari911            const events = document.querySelectorAll("." + nsId + "_events");
12601d05cddcSAtari911
12611d05cddcSAtari911            // Only select visible events (not hidden by search)
12621d05cddcSAtari911            events.forEach(cb => {
12631d05cddcSAtari911                const eventRow = cb.closest(".event-row");
12641d05cddcSAtari911                if (eventRow && eventRow.style.display !== "none") {
12651d05cddcSAtari911                    cb.checked = checkbox.checked;
12661d05cddcSAtari911                }
12671d05cddcSAtari911            });
12681d05cddcSAtari911            updateCount();
12691d05cddcSAtari911        }
12701d05cddcSAtari911
12711d05cddcSAtari911        function selectAll() {
12721d05cddcSAtari911            // Only select visible events
12731d05cddcSAtari911            document.querySelectorAll(".event-checkbox").forEach(cb => {
12741d05cddcSAtari911                const eventRow = cb.closest(".event-row");
12751d05cddcSAtari911                if (eventRow && eventRow.style.display !== "none") {
12761d05cddcSAtari911                    cb.checked = true;
12771d05cddcSAtari911                }
12781d05cddcSAtari911            });
12791d05cddcSAtari911            // Update namespace checkboxes to indeterminate if partially selected
12801d05cddcSAtari911            document.querySelectorAll("input[id$=_check]").forEach(nsCheckbox => {
12811d05cddcSAtari911                const nsId = nsCheckbox.id.replace("_check", "");
12821d05cddcSAtari911                const events = document.querySelectorAll("." + nsId + "_events");
12831d05cddcSAtari911                const visibleEvents = Array.from(events).filter(cb => {
12841d05cddcSAtari911                    const row = cb.closest(".event-row");
12851d05cddcSAtari911                    return row && row.style.display !== "none";
12861d05cddcSAtari911                });
12871d05cddcSAtari911                const checkedVisible = visibleEvents.filter(cb => cb.checked);
12881d05cddcSAtari911
12891d05cddcSAtari911                if (checkedVisible.length === visibleEvents.length && visibleEvents.length > 0) {
12901d05cddcSAtari911                    nsCheckbox.checked = true;
12911d05cddcSAtari911                } else if (checkedVisible.length > 0) {
12921d05cddcSAtari911                    nsCheckbox.indeterminate = true;
12931d05cddcSAtari911                } else {
12941d05cddcSAtari911                    nsCheckbox.checked = false;
12951d05cddcSAtari911                }
12961d05cddcSAtari911            });
12971d05cddcSAtari911            updateCount();
12981d05cddcSAtari911        }
12991d05cddcSAtari911
13001d05cddcSAtari911        function deselectAll() {
13011d05cddcSAtari911            document.querySelectorAll(".event-checkbox").forEach(cb => cb.checked = false);
13021d05cddcSAtari911            document.querySelectorAll("input[id$=_check]").forEach(cb => {
13031d05cddcSAtari911                cb.checked = false;
13041d05cddcSAtari911                cb.indeterminate = false;
13051d05cddcSAtari911            });
13061d05cddcSAtari911            updateCount();
13071d05cddcSAtari911        }
13081d05cddcSAtari911
13091d05cddcSAtari911        function deleteSelected() {
13101d05cddcSAtari911            const checkedBoxes = document.querySelectorAll(".event-checkbox:checked");
13111d05cddcSAtari911            if (checkedBoxes.length === 0) {
13121d05cddcSAtari911                alert("No events selected");
13131d05cddcSAtari911                return;
13141d05cddcSAtari911            }
13151d05cddcSAtari911
13161d05cddcSAtari911            const count = checkedBoxes.length;
13171d05cddcSAtari911            if (!confirm(`Delete ${count} selected event(s)?\\n\\nThis cannot be undone!`)) {
13181d05cddcSAtari911                return;
13191d05cddcSAtari911            }
13201d05cddcSAtari911
13211d05cddcSAtari911            const form = document.createElement("form");
13221d05cddcSAtari911            form.method = "POST";
13231d05cddcSAtari911            form.action = "?do=admin&page=calendar&tab=manage";
13241d05cddcSAtari911
13251d05cddcSAtari911            const actionInput = document.createElement("input");
13261d05cddcSAtari911            actionInput.type = "hidden";
13271d05cddcSAtari911            actionInput.name = "action";
13281d05cddcSAtari911            actionInput.value = "delete_selected_events";
13291d05cddcSAtari911            form.appendChild(actionInput);
13301d05cddcSAtari911
13311d05cddcSAtari911            checkedBoxes.forEach(cb => {
13321d05cddcSAtari911                const eventInput = document.createElement("input");
13331d05cddcSAtari911                eventInput.type = "hidden";
13341d05cddcSAtari911                eventInput.name = "events[]";
13351d05cddcSAtari911                eventInput.value = cb.value;
13361d05cddcSAtari911                form.appendChild(eventInput);
13371d05cddcSAtari911            });
13381d05cddcSAtari911
13391d05cddcSAtari911            document.body.appendChild(form);
13401d05cddcSAtari911            form.submit();
13411d05cddcSAtari911        }
13421d05cddcSAtari911
13431d05cddcSAtari911        function createNewNamespace() {
13441d05cddcSAtari911            const namespaceName = prompt("Enter new namespace name:\\n\\nExamples:\\n- work\\n- personal\\n- projects:alpha\\n- aspen:travel:2025");
13451d05cddcSAtari911
13461d05cddcSAtari911            if (!namespaceName) {
13471d05cddcSAtari911                return; // Cancelled
13481d05cddcSAtari911            }
13491d05cddcSAtari911
13501d05cddcSAtari911            // Validate namespace name
13511d05cddcSAtari911            if (!/^[a-zA-Z0-9_:-]+$/.test(namespaceName)) {
13521d05cddcSAtari911                alert("Invalid namespace name.\\n\\nUse only letters, numbers, underscore, hyphen, and colon.\\nExample: work:projects:alpha");
13531d05cddcSAtari911                return;
13541d05cddcSAtari911            }
13551d05cddcSAtari911
13561d05cddcSAtari911            // Submit form to create namespace
13571d05cddcSAtari911            const form = document.createElement("form");
13581d05cddcSAtari911            form.method = "POST";
13591d05cddcSAtari911            form.action = "?do=admin&page=calendar&tab=manage";
13601d05cddcSAtari911
13611d05cddcSAtari911            const actionInput = document.createElement("input");
13621d05cddcSAtari911            actionInput.type = "hidden";
13631d05cddcSAtari911            actionInput.name = "action";
13641d05cddcSAtari911            actionInput.value = "create_namespace";
13651d05cddcSAtari911            form.appendChild(actionInput);
13661d05cddcSAtari911
13671d05cddcSAtari911            const namespaceInput = document.createElement("input");
13681d05cddcSAtari911            namespaceInput.type = "hidden";
13691d05cddcSAtari911            namespaceInput.name = "namespace_name";
13701d05cddcSAtari911            namespaceInput.value = namespaceName;
13711d05cddcSAtari911            form.appendChild(namespaceInput);
13721d05cddcSAtari911
13731d05cddcSAtari911            document.body.appendChild(form);
13741d05cddcSAtari911            form.submit();
13751d05cddcSAtari911        }
13761d05cddcSAtari911
13771d05cddcSAtari911        function updateCount() {
13781d05cddcSAtari911            const count = document.querySelectorAll(".event-checkbox:checked").length;
13791d05cddcSAtari911            document.getElementById("selectedCount").textContent = count + " selected";
13801d05cddcSAtari911        }
13811d05cddcSAtari911
13821d05cddcSAtari911        function deleteNamespace(namespace) {
13831d05cddcSAtari911            const displayName = namespace || "(default)";
13841d05cddcSAtari911            if (!confirm("Delete ENTIRE namespace: " + displayName + "?\\n\\nThis will delete ALL events in this namespace!\\n\\nThis cannot be undone!")) {
13851d05cddcSAtari911                return;
13861d05cddcSAtari911            }
13871d05cddcSAtari911            const form = document.createElement("form");
13881d05cddcSAtari911            form.method = "POST";
13891d05cddcSAtari911            form.action = "?do=admin&page=calendar&tab=manage";
13901d05cddcSAtari911            const actionInput = document.createElement("input");
13911d05cddcSAtari911            actionInput.type = "hidden";
13921d05cddcSAtari911            actionInput.name = "action";
13931d05cddcSAtari911            actionInput.value = "delete_namespace";
13941d05cddcSAtari911            form.appendChild(actionInput);
13951d05cddcSAtari911            const nsInput = document.createElement("input");
13961d05cddcSAtari911            nsInput.type = "hidden";
13971d05cddcSAtari911            nsInput.name = "namespace";
13981d05cddcSAtari911            nsInput.value = namespace;
13991d05cddcSAtari911            form.appendChild(nsInput);
14001d05cddcSAtari911            document.body.appendChild(form);
14011d05cddcSAtari911            form.submit();
14021d05cddcSAtari911        }
14031d05cddcSAtari911
14049ccd446eSAtari911        function renameNamespace(oldNamespace) {
14059ccd446eSAtari911            const displayName = oldNamespace || "(default)";
14069ccd446eSAtari911            const newName = prompt("Rename namespace: " + displayName + "\\n\\nEnter new name:", oldNamespace);
14079ccd446eSAtari911            if (newName === null || newName === oldNamespace) {
14089ccd446eSAtari911                return; // Cancelled or no change
14099ccd446eSAtari911            }
14109ccd446eSAtari911            const form = document.createElement("form");
14119ccd446eSAtari911            form.method = "POST";
14129ccd446eSAtari911            form.action = "?do=admin&page=calendar&tab=manage";
14139ccd446eSAtari911            const actionInput = document.createElement("input");
14149ccd446eSAtari911            actionInput.type = "hidden";
14159ccd446eSAtari911            actionInput.name = "action";
14169ccd446eSAtari911            actionInput.value = "rename_namespace";
14179ccd446eSAtari911            form.appendChild(actionInput);
14189ccd446eSAtari911            const oldInput = document.createElement("input");
14199ccd446eSAtari911            oldInput.type = "hidden";
14209ccd446eSAtari911            oldInput.name = "old_namespace";
14219ccd446eSAtari911            oldInput.value = oldNamespace;
14229ccd446eSAtari911            form.appendChild(oldInput);
14239ccd446eSAtari911            const newInput = document.createElement("input");
14249ccd446eSAtari911            newInput.type = "hidden";
14259ccd446eSAtari911            newInput.name = "new_namespace";
14269ccd446eSAtari911            newInput.value = newName;
14279ccd446eSAtari911            form.appendChild(newInput);
14289ccd446eSAtari911            document.body.appendChild(form);
14299ccd446eSAtari911            form.submit();
14309ccd446eSAtari911        }
14319ccd446eSAtari911
14321d05cddcSAtari911        let draggedEvent = null;
14331d05cddcSAtari911
14341d05cddcSAtari911        function dragStart(event, eventId) {
14351d05cddcSAtari911            const checkbox = event.target.closest(".event-row").querySelector(".event-checkbox");
14361d05cddcSAtari911
14371d05cddcSAtari911            // If this event is checked, drag all checked events
14381d05cddcSAtari911            const checkedBoxes = document.querySelectorAll(".event-checkbox:checked");
14391d05cddcSAtari911            if (checkbox && checkbox.checked && checkedBoxes.length > 1) {
14401d05cddcSAtari911                // Dragging multiple selected events
14411d05cddcSAtari911                draggedEvent = "MULTIPLE";
14421d05cddcSAtari911                event.dataTransfer.setData("text/plain", "MULTIPLE");
14431d05cddcSAtari911            } else {
14441d05cddcSAtari911                // Dragging single event
14451d05cddcSAtari911                draggedEvent = eventId;
14461d05cddcSAtari911                event.dataTransfer.setData("text/plain", eventId);
14471d05cddcSAtari911            }
14481d05cddcSAtari911            event.dataTransfer.effectAllowed = "move";
14491d05cddcSAtari911            event.target.style.opacity = "0.5";
14501d05cddcSAtari911        }
14511d05cddcSAtari911
14521d05cddcSAtari911        function allowDrop(event) {
14531d05cddcSAtari911            event.preventDefault();
14541d05cddcSAtari911            event.dataTransfer.dropEffect = "move";
14551d05cddcSAtari911        }
14561d05cddcSAtari911
14571d05cddcSAtari911        function drop(event, targetNamespace) {
14581d05cddcSAtari911            event.preventDefault();
14591d05cddcSAtari911
14601d05cddcSAtari911            if (draggedEvent === "MULTIPLE") {
14611d05cddcSAtari911                // Move all selected events
14621d05cddcSAtari911                const checkedBoxes = document.querySelectorAll(".event-checkbox:checked");
14631d05cddcSAtari911                if (checkedBoxes.length === 0) return;
14641d05cddcSAtari911
14651d05cddcSAtari911                const form = document.createElement("form");
14661d05cddcSAtari911                form.method = "POST";
14671d05cddcSAtari911                form.action = "?do=admin&page=calendar&tab=manage";
14681d05cddcSAtari911
14691d05cddcSAtari911                const actionInput = document.createElement("input");
14701d05cddcSAtari911                actionInput.type = "hidden";
14711d05cddcSAtari911                actionInput.name = "action";
14721d05cddcSAtari911                actionInput.value = "move_selected_events";
14731d05cddcSAtari911                form.appendChild(actionInput);
14741d05cddcSAtari911
14751d05cddcSAtari911                checkedBoxes.forEach(cb => {
14761d05cddcSAtari911                    const eventInput = document.createElement("input");
14771d05cddcSAtari911                    eventInput.type = "hidden";
14781d05cddcSAtari911                    eventInput.name = "events[]";
14791d05cddcSAtari911                    eventInput.value = cb.value;
14801d05cddcSAtari911                    form.appendChild(eventInput);
14811d05cddcSAtari911                });
14821d05cddcSAtari911
14831d05cddcSAtari911                const targetInput = document.createElement("input");
14841d05cddcSAtari911                targetInput.type = "hidden";
14851d05cddcSAtari911                targetInput.name = "target_namespace";
14861d05cddcSAtari911                targetInput.value = targetNamespace;
14871d05cddcSAtari911                form.appendChild(targetInput);
14881d05cddcSAtari911
14891d05cddcSAtari911                document.body.appendChild(form);
14901d05cddcSAtari911                form.submit();
14911d05cddcSAtari911            } else {
14921d05cddcSAtari911                // Move single event
14931d05cddcSAtari911                if (!draggedEvent) return;
14941d05cddcSAtari911                const parts = draggedEvent.split("|");
14951d05cddcSAtari911                const sourceNamespace = parts[1];
14961d05cddcSAtari911                if (sourceNamespace === targetNamespace) return;
14971d05cddcSAtari911
14981d05cddcSAtari911                const form = document.createElement("form");
14991d05cddcSAtari911                form.method = "POST";
15001d05cddcSAtari911                form.action = "?do=admin&page=calendar&tab=manage";
15011d05cddcSAtari911                const actionInput = document.createElement("input");
15021d05cddcSAtari911                actionInput.type = "hidden";
15031d05cddcSAtari911                actionInput.name = "action";
15041d05cddcSAtari911                actionInput.value = "move_single_event";
15051d05cddcSAtari911                form.appendChild(actionInput);
15061d05cddcSAtari911                const eventInput = document.createElement("input");
15071d05cddcSAtari911                eventInput.type = "hidden";
15081d05cddcSAtari911                eventInput.name = "event";
15091d05cddcSAtari911                eventInput.value = draggedEvent;
15101d05cddcSAtari911                form.appendChild(eventInput);
15111d05cddcSAtari911                const targetInput = document.createElement("input");
15121d05cddcSAtari911                targetInput.type = "hidden";
15131d05cddcSAtari911                targetInput.name = "target_namespace";
15141d05cddcSAtari911                targetInput.value = targetNamespace;
15151d05cddcSAtari911                form.appendChild(targetInput);
15161d05cddcSAtari911                document.body.appendChild(form);
15171d05cddcSAtari911                form.submit();
15181d05cddcSAtari911            }
15191d05cddcSAtari911        }
15201d05cddcSAtari911
15211d05cddcSAtari911        function editRecurringSeries(title, namespace) {
15229ccd446eSAtari911            // Get available namespaces from the namespace explorer
15239ccd446eSAtari911            const namespaces = new Set();
15241d05cddcSAtari911
15259ccd446eSAtari911            // Method 1: Try to get from namespace explorer folder names
15269ccd446eSAtari911            document.querySelectorAll("[id^=ns_]").forEach(el => {
15279ccd446eSAtari911                const nsSpan = el.querySelector("span:nth-child(3)");
15289ccd446eSAtari911                if (nsSpan) {
15299ccd446eSAtari911                    let nsText = nsSpan.textContent.replace("�� ", "").trim();
15309ccd446eSAtari911                    if (nsText && nsText !== "(default)") {
15319ccd446eSAtari911                        namespaces.add(nsText);
15329ccd446eSAtari911                    }
15339ccd446eSAtari911                }
15349ccd446eSAtari911            });
15359ccd446eSAtari911
15369ccd446eSAtari911            // Method 2: Get from datalist if it exists
15379ccd446eSAtari911            document.querySelectorAll("#namespaceList option").forEach(opt => {
15389ccd446eSAtari911                if (opt.value && opt.value !== "") {
15399ccd446eSAtari911                    namespaces.add(opt.value);
15409ccd446eSAtari911                }
15419ccd446eSAtari911            });
15429ccd446eSAtari911
15439ccd446eSAtari911            // Convert to sorted array
15449ccd446eSAtari911            const nsArray = Array.from(namespaces).sort();
15459ccd446eSAtari911
15469ccd446eSAtari911            // Build options - include current namespace AND all others
15479ccd446eSAtari911            let nsOptions = "<option value=\\"\\">(default)</option>";
15489ccd446eSAtari911
15499ccd446eSAtari911            // Add current namespace if it\'s not default
15509ccd446eSAtari911            if (namespace && namespace !== "") {
15519ccd446eSAtari911                nsOptions += "<option value=\\"" + namespace + "\\" selected>" + namespace + " (current)</option>";
15529ccd446eSAtari911            }
15539ccd446eSAtari911
15549ccd446eSAtari911            // Add all other namespaces
15559ccd446eSAtari911            for (const ns of nsArray) {
15569ccd446eSAtari911                if (ns !== namespace) {
15579ccd446eSAtari911                    nsOptions += "<option value=\\"" + ns + "\\">" + ns + "</option>";
15581d05cddcSAtari911                }
15591d05cddcSAtari911            }
15601d05cddcSAtari911
15611d05cddcSAtari911            // Show edit dialog for recurring events
15621d05cddcSAtari911            const dialog = document.createElement("div");
15631d05cddcSAtari911            dialog.style.cssText = "position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.5); display:flex; align-items:center; justify-content:center; z-index:10000;";
15641d05cddcSAtari911
15651d05cddcSAtari911            // Close on clicking background
15661d05cddcSAtari911            dialog.addEventListener("click", function(e) {
15671d05cddcSAtari911                if (e.target === dialog) {
15681d05cddcSAtari911                    dialog.remove();
15691d05cddcSAtari911                }
15701d05cddcSAtari911            });
15711d05cddcSAtari911
15721d05cddcSAtari911            dialog.innerHTML = `
15739ccd446eSAtari911                <div style="background:' . $colors['bg'] . '; padding:20px; border-radius:8px; min-width:500px; max-width:700px; max-height:90vh; overflow-y:auto;">
15741d05cddcSAtari911                    <h3 style="margin:0 0 15px; color:#00cc07;">Edit Recurring Event</h3>
15759ccd446eSAtari911                    <p style="margin:0 0 15px; color:' . $colors['text'] . '; font-size:13px;">Changes will apply to ALL occurrences of: <strong>${title}</strong></p>
15761d05cddcSAtari911
15771d05cddcSAtari911                    <form id="editRecurringForm" style="display:flex; flex-direction:column; gap:12px;">
15781d05cddcSAtari911                        <div>
15791d05cddcSAtari911                            <label style="display:block; font-weight:bold; margin-bottom:4px; font-size:13px;">New Title:</label>
15809ccd446eSAtari911                            <input type="text" name="new_title" value="${title}" style="width:100%; padding:8px; border:1px solid ' . $colors['border'] . '; border-radius:3px;" required>
15811d05cddcSAtari911                        </div>
15821d05cddcSAtari911
15831d05cddcSAtari911                        <div style="display:grid; grid-template-columns:1fr 1fr; gap:10px;">
15841d05cddcSAtari911                            <div>
15851d05cddcSAtari911                                <label style="display:block; font-weight:bold; margin-bottom:4px; font-size:13px;">Start Time:</label>
15869ccd446eSAtari911                                <input type="time" name="start_time" style="width:100%; padding:8px; border:1px solid ' . $colors['border'] . '; border-radius:3px;">
15871d05cddcSAtari911                                <small style="color:#999; font-size:11px;">Leave blank to keep current</small>
15881d05cddcSAtari911                            </div>
15891d05cddcSAtari911                            <div>
15901d05cddcSAtari911                                <label style="display:block; font-weight:bold; margin-bottom:4px; font-size:13px;">End Time:</label>
15919ccd446eSAtari911                                <input type="time" name="end_time" style="width:100%; padding:8px; border:1px solid ' . $colors['border'] . '; border-radius:3px;">
15921d05cddcSAtari911                                <small style="color:#999; font-size:11px;">Leave blank to keep current</small>
15931d05cddcSAtari911                            </div>
15941d05cddcSAtari911                        </div>
15951d05cddcSAtari911
15961d05cddcSAtari911                        <div>
15971d05cddcSAtari911                            <label style="display:block; font-weight:bold; margin-bottom:4px; font-size:13px;">Interval (days between occurrences):</label>
15989ccd446eSAtari911                            <select name="interval" style="width:100%; padding:8px; border:1px solid ' . $colors['border'] . '; border-radius:3px;">
15991d05cddcSAtari911                                <option value="">Keep current interval</option>
16001d05cddcSAtari911                                <option value="1">Daily (1 day)</option>
16011d05cddcSAtari911                                <option value="7">Weekly (7 days)</option>
16021d05cddcSAtari911                                <option value="14">Bi-weekly (14 days)</option>
16031d05cddcSAtari911                                <option value="30">Monthly (30 days)</option>
16041d05cddcSAtari911                                <option value="365">Yearly (365 days)</option>
16051d05cddcSAtari911                            </select>
16061d05cddcSAtari911                        </div>
16071d05cddcSAtari911
16081d05cddcSAtari911                        <div>
16091d05cddcSAtari911                            <label style="display:block; font-weight:bold; margin-bottom:4px; font-size:13px;">Move to Namespace:</label>
16109ccd446eSAtari911                            <select name="new_namespace" style="width:100%; padding:8px; border:1px solid ' . $colors['border'] . '; border-radius:3px;">
16111d05cddcSAtari911                                ${nsOptions}
16121d05cddcSAtari911                            </select>
16131d05cddcSAtari911                        </div>
16141d05cddcSAtari911
16151d05cddcSAtari911                        <div style="display:flex; gap:10px; margin-top:10px;">
16161d05cddcSAtari911                            <button type="submit" style="flex:1; background:#00cc07; color:white; padding:10px; border:none; border-radius:3px; cursor:pointer; font-weight:bold;">Save Changes</button>
16171d05cddcSAtari911                            <button type="button" onclick="closeEditDialog()" style="flex:1; background:#999; color:white; padding:10px; border:none; border-radius:3px; cursor:pointer;">Cancel</button>
16181d05cddcSAtari911                        </div>
16191d05cddcSAtari911                    </form>
16201d05cddcSAtari911                </div>
16211d05cddcSAtari911            `;
16221d05cddcSAtari911
16231d05cddcSAtari911            document.body.appendChild(dialog);
16241d05cddcSAtari911
16251d05cddcSAtari911            // Add close function to window
16261d05cddcSAtari911            window.closeEditDialog = function() {
16271d05cddcSAtari911                dialog.remove();
16281d05cddcSAtari911            };
16291d05cddcSAtari911
16301d05cddcSAtari911            // Handle form submission
16311d05cddcSAtari911            dialog.querySelector("#editRecurringForm").addEventListener("submit", function(e) {
16321d05cddcSAtari911                e.preventDefault();
16331d05cddcSAtari911                const formData = new FormData(this);
16341d05cddcSAtari911
16351d05cddcSAtari911                // Submit the edit
16361d05cddcSAtari911                const form = document.createElement("form");
16371d05cddcSAtari911                form.method = "POST";
16381d05cddcSAtari911                form.action = "?do=admin&page=calendar&tab=manage";
16391d05cddcSAtari911
16401d05cddcSAtari911                const actionInput = document.createElement("input");
16411d05cddcSAtari911                actionInput.type = "hidden";
16421d05cddcSAtari911                actionInput.name = "action";
16431d05cddcSAtari911                actionInput.value = "edit_recurring_series";
16441d05cddcSAtari911                form.appendChild(actionInput);
16451d05cddcSAtari911
16461d05cddcSAtari911                const oldTitleInput = document.createElement("input");
16471d05cddcSAtari911                oldTitleInput.type = "hidden";
16481d05cddcSAtari911                oldTitleInput.name = "old_title";
16491d05cddcSAtari911                oldTitleInput.value = title;
16501d05cddcSAtari911                form.appendChild(oldTitleInput);
16511d05cddcSAtari911
16521d05cddcSAtari911                const oldNamespaceInput = document.createElement("input");
16531d05cddcSAtari911                oldNamespaceInput.type = "hidden";
16541d05cddcSAtari911                oldNamespaceInput.name = "old_namespace";
16551d05cddcSAtari911                oldNamespaceInput.value = namespace;
16561d05cddcSAtari911                form.appendChild(oldNamespaceInput);
16571d05cddcSAtari911
16581d05cddcSAtari911                // Add all form fields
16591d05cddcSAtari911                for (let [key, value] of formData.entries()) {
16601d05cddcSAtari911                    const input = document.createElement("input");
16611d05cddcSAtari911                    input.type = "hidden";
16621d05cddcSAtari911                    input.name = key;
16631d05cddcSAtari911                    input.value = value;
16641d05cddcSAtari911                    form.appendChild(input);
16651d05cddcSAtari911                }
16661d05cddcSAtari911
16671d05cddcSAtari911                document.body.appendChild(form);
16681d05cddcSAtari911                form.submit();
16691d05cddcSAtari911            });
16701d05cddcSAtari911        }
16711d05cddcSAtari911
16721d05cddcSAtari911        function deleteRecurringSeries(title, namespace) {
16731d05cddcSAtari911            const displayNs = namespace || "(default)";
16741d05cddcSAtari911            if (!confirm("Delete ALL occurrences of: " + title + " (" + displayNs + ")?\\n\\nThis cannot be undone!")) {
16751d05cddcSAtari911                return;
16761d05cddcSAtari911            }
16771d05cddcSAtari911            const form = document.createElement("form");
16781d05cddcSAtari911            form.method = "POST";
16791d05cddcSAtari911            form.action = "?do=admin&page=calendar&tab=manage";
16801d05cddcSAtari911            const actionInput = document.createElement("input");
16811d05cddcSAtari911            actionInput.type = "hidden";
16821d05cddcSAtari911            actionInput.name = "action";
16831d05cddcSAtari911            actionInput.value = "delete_recurring_series";
16841d05cddcSAtari911            form.appendChild(actionInput);
16851d05cddcSAtari911            const titleInput = document.createElement("input");
16861d05cddcSAtari911            titleInput.type = "hidden";
16871d05cddcSAtari911            titleInput.name = "event_title";
16881d05cddcSAtari911            titleInput.value = title;
16891d05cddcSAtari911            form.appendChild(titleInput);
16901d05cddcSAtari911            const namespaceInput = document.createElement("input");
16911d05cddcSAtari911            namespaceInput.type = "hidden";
16921d05cddcSAtari911            namespaceInput.name = "namespace";
16931d05cddcSAtari911            namespaceInput.value = namespace;
16941d05cddcSAtari911            form.appendChild(namespaceInput);
16951d05cddcSAtari911            document.body.appendChild(form);
16961d05cddcSAtari911            form.submit();
16971d05cddcSAtari911        }
16981d05cddcSAtari911
16991d05cddcSAtari911        document.addEventListener("dragend", function(e) {
17001d05cddcSAtari911            if (e.target.draggable) {
17011d05cddcSAtari911                e.target.style.opacity = "1";
17021d05cddcSAtari911            }
17031d05cddcSAtari911        });
17041d05cddcSAtari911        </script>';
17051d05cddcSAtari911    }
17061d05cddcSAtari911
17079ccd446eSAtari911    private function renderUpdateTab($colors = null) {
17081d05cddcSAtari911        global $INPUT;
17091d05cddcSAtari911
17109ccd446eSAtari911        // Use defaults if not provided
17119ccd446eSAtari911        if ($colors === null) {
17129ccd446eSAtari911            $colors = $this->getTemplateColors();
17139ccd446eSAtari911        }
17141d05cddcSAtari911
17159ccd446eSAtari911        echo '<h2 style="margin:10px 0; font-size:20px;">�� Update Plugin</h2>';
17161d05cddcSAtari911
17171d05cddcSAtari911        // Show message if present
17181d05cddcSAtari911        if ($INPUT->has('msg')) {
17191d05cddcSAtari911            $msg = hsc($INPUT->str('msg'));
17201d05cddcSAtari911            $type = $INPUT->str('msgtype', 'success');
17211d05cddcSAtari911            $class = ($type === 'success') ? 'msg success' : 'msg error';
17229ccd446eSAtari911            echo "<div class=\"$class\" style=\"padding:10px; margin:10px 0; border-left:3px solid " . ($type === 'success' ? '#28a745' : '#dc3545') . "; background:" . ($type === 'success' ? '#d4edda' : '#f8d7da') . "; border-radius:3px; max-width:1200px;\">";
17231d05cddcSAtari911            echo $msg;
17241d05cddcSAtari911            echo "</div>";
17251d05cddcSAtari911        }
17261d05cddcSAtari911
17279ccd446eSAtari911        // Show current version FIRST (MOVED TO TOP)
17281d05cddcSAtari911        $pluginInfo = DOKU_PLUGIN . 'calendar/plugin.info.txt';
17291d05cddcSAtari911        $info = ['version' => 'Unknown', 'date' => 'Unknown', 'name' => 'Calendar Plugin', 'author' => 'Unknown', 'email' => '', 'desc' => ''];
17301d05cddcSAtari911        if (file_exists($pluginInfo)) {
17311d05cddcSAtari911            $info = array_merge($info, confToHash($pluginInfo));
17321d05cddcSAtari911        }
17331d05cddcSAtari911
17349ccd446eSAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:1200px;">';
17359ccd446eSAtari911        echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">�� Current Version</h3>';
17361d05cddcSAtari911        echo '<div style="font-size:12px; line-height:1.6;">';
17371d05cddcSAtari911        echo '<div style="margin:4px 0;"><strong>Version:</strong> ' . hsc($info['version']) . ' (' . hsc($info['date']) . ')</div>';
17381d05cddcSAtari911        echo '<div style="margin:4px 0;"><strong>Author:</strong> ' . hsc($info['author']) . ($info['email'] ? ' &lt;' . hsc($info['email']) . '&gt;' : '') . '</div>';
17391d05cddcSAtari911        if ($info['desc']) {
17401d05cddcSAtari911            echo '<div style="margin:4px 0;"><strong>Description:</strong> ' . hsc($info['desc']) . '</div>';
17411d05cddcSAtari911        }
17421d05cddcSAtari911        echo '<div style="margin:4px 0;"><strong>Location:</strong> <code style="background:#f0f0f0; padding:2px 4px; border-radius:2px;">' . DOKU_PLUGIN . 'calendar/</code></div>';
17431d05cddcSAtari911        echo '</div>';
17441d05cddcSAtari911
17451d05cddcSAtari911        // Check permissions
17461d05cddcSAtari911        $pluginDir = DOKU_PLUGIN . 'calendar/';
17471d05cddcSAtari911        $pluginWritable = is_writable($pluginDir);
17481d05cddcSAtari911        $parentWritable = is_writable(DOKU_PLUGIN);
17491d05cddcSAtari911
17509ccd446eSAtari911        echo '<div style="margin-top:8px; padding-top:8px; border-top:1px solid ' . $colors['border'] . ';">';
17511d05cddcSAtari911        if ($pluginWritable && $parentWritable) {
17521d05cddcSAtari911            echo '<p style="margin:5px 0; font-size:13px; color:#28a745;"><strong>✅ Permissions:</strong> OK - ready to update</p>';
17531d05cddcSAtari911        } else {
17541d05cddcSAtari911            echo '<p style="margin:5px 0; font-size:13px; color:#dc3545;"><strong>❌ Permissions:</strong> Issues detected</p>';
17551d05cddcSAtari911            if (!$pluginWritable) {
17561d05cddcSAtari911                echo '<p style="margin:2px 0 2px 20px; font-size:12px; color:#dc3545;">Plugin directory not writable</p>';
17571d05cddcSAtari911            }
17581d05cddcSAtari911            if (!$parentWritable) {
17591d05cddcSAtari911                echo '<p style="margin:2px 0 2px 20px; font-size:12px; color:#dc3545;">Parent directory not writable</p>';
17601d05cddcSAtari911            }
17619ccd446eSAtari911            echo '<p style="margin:5px 0; font-size:12px; color:' . $colors['text'] . ';">Fix with: <code style="background:#f0f0f0; padding:2px 4px; border-radius:2px;">chmod -R 755 ' . DOKU_PLUGIN . 'calendar/</code></p>';
17629ccd446eSAtari911            echo '<p style="margin:2px 0; font-size:12px; color:' . $colors['text'] . ';">Or: <code style="background:#f0f0f0; padding:2px 4px; border-radius:2px;">chown -R www-data:www-data ' . DOKU_PLUGIN . 'calendar/</code></p>';
17631d05cddcSAtari911        }
17641d05cddcSAtari911        echo '</div>';
17651d05cddcSAtari911
17661d05cddcSAtari911        echo '</div>';
17671d05cddcSAtari911
17689ccd446eSAtari911        // Combined upload and notes section (SIDE BY SIDE)
17699ccd446eSAtari911        echo '<div style="display:flex; gap:15px; max-width:1200px; margin:10px 0;">';
17701d05cddcSAtari911
17719ccd446eSAtari911        // Left side - Upload form (60% width)
17729ccd446eSAtari911        echo '<div style="flex:1; min-width:0; background:' . $colors['bg'] . '; padding:12px; border-left:3px solid #00cc07; border-radius:3px;">';
17739ccd446eSAtari911        echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">�� Upload New Version</h3>';
17749ccd446eSAtari911        echo '<p style="color:' . $colors['text'] . '; font-size:13px; margin:0 0 10px;">Upload a calendar plugin ZIP file to update. Your configuration will be preserved.</p>';
17751d05cddcSAtari911
17761d05cddcSAtari911        echo '<form method="post" action="?do=admin&page=calendar&tab=update" enctype="multipart/form-data" id="uploadForm">';
17771d05cddcSAtari911        echo '<input type="hidden" name="action" value="upload_update">';
17781d05cddcSAtari911        echo '<div style="margin:10px 0;">';
17799ccd446eSAtari911        echo '<input type="file" name="plugin_zip" accept=".zip" required style="padding:8px; border:1px solid ' . $colors['border'] . '; border-radius:3px; font-size:13px; width:100%;">';
17801d05cddcSAtari911        echo '</div>';
17811d05cddcSAtari911        echo '<div style="margin:10px 0;">';
17821d05cddcSAtari911        echo '<label style="display:flex; align-items:center; gap:8px; font-size:13px;">';
17831d05cddcSAtari911        echo '<input type="checkbox" name="backup_first" value="1" checked>';
17841d05cddcSAtari911        echo '<span>Create backup before updating (Recommended)</span>';
17851d05cddcSAtari911        echo '</label>';
17861d05cddcSAtari911        echo '</div>';
17879ccd446eSAtari911
17889ccd446eSAtari911        // Buttons side by side
17899ccd446eSAtari911        echo '<div style="display:flex; gap:10px; align-items:center; flex-wrap:wrap;">';
17901d05cddcSAtari911        echo '<button type="submit" onclick="return confirmUpload()" style="background:#00cc07; color:white; padding:10px 20px; border:none; border-radius:3px; cursor:pointer; font-size:14px; font-weight:bold;">�� Upload & Install</button>';
17911d05cddcSAtari911        echo '</form>';
17929ccd446eSAtari911
17939ccd446eSAtari911        // Clear Cache button (next to Upload button)
17949ccd446eSAtari911        echo '<form method="post" action="?do=admin&page=calendar&tab=update" style="display:inline; margin:0;">';
17959ccd446eSAtari911        echo '<input type="hidden" name="action" value="clear_cache">';
17969ccd446eSAtari911        echo '<input type="hidden" name="tab" value="update">';
17979ccd446eSAtari911        echo '<button type="submit" onclick="return confirm(\'Clear all DokuWiki cache? This will refresh all plugin files.\')" style="background:#ff9800; color:white; padding:10px 20px; border:none; border-radius:3px; cursor:pointer; font-size:14px; font-weight:bold;">��️ Clear Cache</button>';
17989ccd446eSAtari911        echo '</form>';
17991d05cddcSAtari911        echo '</div>';
18001d05cddcSAtari911
18019ccd446eSAtari911        echo '<p style="margin:8px 0 0 0; font-size:12px; color:' . $colors['text'] . ';">Clear the DokuWiki cache if changes aren\'t appearing or after updating the plugin.</p>';
18029ccd446eSAtari911        echo '</div>';
18039ccd446eSAtari911
18049ccd446eSAtari911        // Right side - Important Notes (40% width)
18059ccd446eSAtari911        echo '<div style="flex:0 0 350px; min-width:0; background:#fff3e0; border-left:3px solid #ff9800; padding:12px; border-radius:3px;">';
18061d05cddcSAtari911        echo '<h4 style="margin:0 0 5px 0; color:#e65100; font-size:14px;">⚠️ Important Notes</h4>';
18079ccd446eSAtari911        echo '<ul style="margin:5px 0; padding-left:20px; font-size:12px; color:#e65100; line-height:1.6;">';
18081d05cddcSAtari911        echo '<li>This will replace all plugin files</li>';
18091d05cddcSAtari911        echo '<li>Configuration files (sync_config.php) will be preserved</li>';
18101d05cddcSAtari911        echo '<li>Event data will not be affected</li>';
18119ccd446eSAtari911        echo '<li>Backup will be saved to: <code style="font-size:10px;">calendar.backup.vX.X.X.YYYY-MM-DD_HH-MM-SS.zip</code></li>';
18121d05cddcSAtari911        echo '<li>Make sure the ZIP file is a valid calendar plugin</li>';
18131d05cddcSAtari911        echo '</ul>';
18141d05cddcSAtari911        echo '</div>';
18151d05cddcSAtari911
18169ccd446eSAtari911        echo '</div>'; // End flex container
18179ccd446eSAtari911
18189ccd446eSAtari911        // Changelog section - Timeline viewer
18199ccd446eSAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #7b1fa2; border-radius:3px; max-width:1200px;">';
18209ccd446eSAtari911        echo '<h3 style="margin:0 0 8px 0; color:#7b1fa2; font-size:16px;">�� Version History</h3>';
18219ccd446eSAtari911
18229ccd446eSAtari911        $changelogFile = DOKU_PLUGIN . 'calendar/CHANGELOG.md';
18239ccd446eSAtari911        if (file_exists($changelogFile)) {
18249ccd446eSAtari911            $changelog = file_get_contents($changelogFile);
18259ccd446eSAtari911
18269ccd446eSAtari911            // Parse ALL versions into structured data
18279ccd446eSAtari911            $lines = explode("\n", $changelog);
18289ccd446eSAtari911            $versions = [];
18299ccd446eSAtari911            $currentVersion = null;
18309ccd446eSAtari911
18319ccd446eSAtari911            foreach ($lines as $line) {
18329ccd446eSAtari911                $line = trim($line);
18339ccd446eSAtari911
18349ccd446eSAtari911                // Version header (## Version X.X.X or ## Version X.X.X (date) - title)
18359ccd446eSAtari911                if (preg_match('/^## Version (.+?)(?:\s*\(([^)]+)\))?\s*(?:-\s*(.+))?$/', $line, $matches)) {
18369ccd446eSAtari911                    if ($currentVersion !== null) {
18379ccd446eSAtari911                        $versions[] = $currentVersion;
18389ccd446eSAtari911                    }
18399ccd446eSAtari911                    $currentVersion = [
18409ccd446eSAtari911                        'number' => trim($matches[1]),
18419ccd446eSAtari911                        'date' => isset($matches[2]) ? trim($matches[2]) : '',
18429ccd446eSAtari911                        'title' => isset($matches[3]) ? trim($matches[3]) : '',
18439ccd446eSAtari911                        'items' => []
18449ccd446eSAtari911                    ];
18459ccd446eSAtari911                }
18469ccd446eSAtari911                // List items (- **Type:** description)
18479ccd446eSAtari911                elseif ($currentVersion !== null && preg_match('/^- \*\*(.+?):\*\* (.+)$/', $line, $matches)) {
18489ccd446eSAtari911                    $currentVersion['items'][] = [
18499ccd446eSAtari911                        'type' => $matches[1],
18509ccd446eSAtari911                        'desc' => $matches[2]
18519ccd446eSAtari911                    ];
18529ccd446eSAtari911                }
18539ccd446eSAtari911            }
18549ccd446eSAtari911            // Don\'t forget last version
18559ccd446eSAtari911            if ($currentVersion !== null) {
18569ccd446eSAtari911                $versions[] = $currentVersion;
18579ccd446eSAtari911            }
18589ccd446eSAtari911
18599ccd446eSAtari911            $totalVersions = count($versions);
18609ccd446eSAtari911            $uniqueId = 'changelog_' . substr(md5(microtime()), 0, 6);
18619ccd446eSAtari911
18629ccd446eSAtari911            if ($totalVersions > 0) {
18639ccd446eSAtari911                // Timeline navigation bar
18649ccd446eSAtari911                echo '<div id="' . $uniqueId . '_wrap" style="position:relative;">';
18659ccd446eSAtari911
18669ccd446eSAtari911                // Nav controls
18679ccd446eSAtari911                echo '<div style="display:flex; align-items:center; gap:8px; margin-bottom:10px;">';
18689ccd446eSAtari911                echo '<button id="' . $uniqueId . '_prev" onclick="changelogNav(\'' . $uniqueId . '\', -1)" style="background:none; border:1px solid ' . $colors['border'] . '; color:' . $colors['text'] . '; width:32px; height:32px; border-radius:50%; cursor:pointer; font-size:16px; display:flex; align-items:center; justify-content:center; transition:all 0.15s;" onmouseover="this.style.borderColor=\'#7b1fa2\'; this.style.color=\'#7b1fa2\'" onmouseout="this.style.borderColor=\'' . $colors['border'] . '\'; this.style.color=\'' . $colors['text'] . '\'">‹</button>';
18699ccd446eSAtari911                echo '<div style="flex:1; text-align:center;">';
18709ccd446eSAtari911                echo '<span id="' . $uniqueId . '_counter" style="font-size:11px; color:' . $colors['text'] . '; opacity:0.7;">1 of ' . $totalVersions . '</span>';
18719ccd446eSAtari911                echo '</div>';
18729ccd446eSAtari911                echo '<button id="' . $uniqueId . '_next" onclick="changelogNav(\'' . $uniqueId . '\', 1)" style="background:none; border:1px solid ' . $colors['border'] . '; color:' . $colors['text'] . '; width:32px; height:32px; border-radius:50%; cursor:pointer; font-size:16px; display:flex; align-items:center; justify-content:center; transition:all 0.15s;" onmouseover="this.style.borderColor=\'#7b1fa2\'; this.style.color=\'#7b1fa2\'" onmouseout="this.style.borderColor=\'' . $colors['border'] . '\'; this.style.color=\'' . $colors['text'] . '\'">›</button>';
18739ccd446eSAtari911                echo '</div>';
18749ccd446eSAtari911
18759ccd446eSAtari911                // Version cards (one per version, only first visible)
18769ccd446eSAtari911                foreach ($versions as $i => $ver) {
18779ccd446eSAtari911                    $display = ($i === 0) ? 'block' : 'none';
18789ccd446eSAtari911                    echo '<div class="' . $uniqueId . '_card" id="' . $uniqueId . '_card_' . $i . '" style="display:' . $display . '; padding:10px; background:' . $colors['bg'] . '; border:1px solid ' . $colors['border'] . '; border-left:3px solid #7b1fa2; border-radius:4px; transition:opacity 0.2s;">';
18799ccd446eSAtari911
18809ccd446eSAtari911                    // Version header
18819ccd446eSAtari911                    echo '<div style="display:flex; align-items:baseline; gap:8px; margin-bottom:8px;">';
18829ccd446eSAtari911                    echo '<span style="font-weight:bold; color:#7b1fa2; font-size:14px;">v' . hsc($ver['number']) . '</span>';
18839ccd446eSAtari911                    if ($ver['date']) {
18849ccd446eSAtari911                        echo '<span style="font-size:11px; color:' . $colors['text'] . '; opacity:0.6;">' . hsc($ver['date']) . '</span>';
18859ccd446eSAtari911                    }
18869ccd446eSAtari911                    echo '</div>';
18879ccd446eSAtari911                    if ($ver['title']) {
18889ccd446eSAtari911                        echo '<div style="font-size:12px; font-weight:600; color:' . $colors['text'] . '; margin-bottom:8px;">' . hsc($ver['title']) . '</div>';
18899ccd446eSAtari911                    }
18909ccd446eSAtari911
18919ccd446eSAtari911                    // Change items
18929ccd446eSAtari911                    if (!empty($ver['items'])) {
18939ccd446eSAtari911                        echo '<div style="font-size:12px; line-height:1.7;">';
18949ccd446eSAtari911                        foreach ($ver['items'] as $item) {
18959ccd446eSAtari911                            $color = '#666'; $icon = '•';
18969ccd446eSAtari911                            $t = $item['type'];
18979ccd446eSAtari911                            if ($t === 'Added') { $color = '#28a745'; $icon = '✨'; }
18989ccd446eSAtari911                            elseif ($t === 'Fixed') { $color = '#dc3545'; $icon = '��'; }
18999ccd446eSAtari911                            elseif ($t === 'Changed') { $color = '#7b1fa2'; $icon = '��'; }
19009ccd446eSAtari911                            elseif ($t === 'Improved') { $color = '#ff9800'; $icon = '⚡'; }
19019ccd446eSAtari911                            elseif ($t === 'Removed') { $color = '#e91e63'; $icon = '��️'; }
19029ccd446eSAtari911                            elseif ($t === 'Development' || $t === 'Refactored') { $color = '#6c757d'; $icon = '��️'; }
19039ccd446eSAtari911                            elseif ($t === 'Result') { $color = '#2196f3'; $icon = '✅'; }
19049ccd446eSAtari911
19059ccd446eSAtari911                            echo '<div style="margin:2px 0; padding-left:4px;">';
19069ccd446eSAtari911                            echo '<span style="color:' . $color . '; font-weight:600;">' . $icon . ' ' . hsc($item['type']) . ':</span> ';
19079ccd446eSAtari911                            echo '<span style="color:' . $colors['text'] . ';">' . hsc($item['desc']) . '</span>';
19089ccd446eSAtari911                            echo '</div>';
19099ccd446eSAtari911                        }
19109ccd446eSAtari911                        echo '</div>';
19119ccd446eSAtari911                    } else {
19129ccd446eSAtari911                        echo '<div style="font-size:11px; color:' . $colors['text'] . '; opacity:0.5; font-style:italic;">No detailed changes recorded</div>';
19139ccd446eSAtari911                    }
19149ccd446eSAtari911
19159ccd446eSAtari911                    echo '</div>';
19169ccd446eSAtari911                }
19179ccd446eSAtari911
19189ccd446eSAtari911                echo '</div>'; // wrap
19199ccd446eSAtari911
19209ccd446eSAtari911                // JavaScript for navigation
19219ccd446eSAtari911                echo '<script>
19229ccd446eSAtari911                (function() {
19239ccd446eSAtari911                    var id = "' . $uniqueId . '";
19249ccd446eSAtari911                    var total = ' . $totalVersions . ';
19259ccd446eSAtari911                    var current = 0;
19269ccd446eSAtari911
19279ccd446eSAtari911                    window.changelogNav = function(uid, dir) {
19289ccd446eSAtari911                        if (uid !== id) return;
19299ccd446eSAtari911                        var next = current + dir;
19309ccd446eSAtari911                        if (next < 0 || next >= total) return;
19319ccd446eSAtari911
19329ccd446eSAtari911                        // Hide current
19339ccd446eSAtari911                        var curCard = document.getElementById(id + "_card_" + current);
19349ccd446eSAtari911                        if (curCard) curCard.style.display = "none";
19359ccd446eSAtari911
19369ccd446eSAtari911                        // Show next
19379ccd446eSAtari911                        current = next;
19389ccd446eSAtari911                        var nextCard = document.getElementById(id + "_card_" + current);
19399ccd446eSAtari911                        if (nextCard) nextCard.style.display = "block";
19409ccd446eSAtari911
19419ccd446eSAtari911                        // Update counter
19429ccd446eSAtari911                        var counter = document.getElementById(id + "_counter");
19439ccd446eSAtari911                        if (counter) counter.textContent = (current + 1) + " of " + total;
19449ccd446eSAtari911
19459ccd446eSAtari911                        // Update button states
19469ccd446eSAtari911                        var prevBtn = document.getElementById(id + "_prev");
19479ccd446eSAtari911                        var nextBtn = document.getElementById(id + "_next");
19489ccd446eSAtari911                        if (prevBtn) prevBtn.style.opacity = (current === 0) ? "0.3" : "1";
19499ccd446eSAtari911                        if (nextBtn) nextBtn.style.opacity = (current === total - 1) ? "0.3" : "1";
19509ccd446eSAtari911                    };
19519ccd446eSAtari911
19529ccd446eSAtari911                    // Initialize button states
19539ccd446eSAtari911                    var prevBtn = document.getElementById(id + "_prev");
19549ccd446eSAtari911                    if (prevBtn) prevBtn.style.opacity = "0.3";
19559ccd446eSAtari911                })();
19569ccd446eSAtari911                </script>';
19579ccd446eSAtari911
19589ccd446eSAtari911            } else {
19599ccd446eSAtari911                echo '<p style="color:#999; font-size:13px; font-style:italic;">No versions found in changelog</p>';
19609ccd446eSAtari911            }
19619ccd446eSAtari911        } else {
19629ccd446eSAtari911            echo '<p style="color:#999; font-size:13px; font-style:italic;">Changelog not available</p>';
19639ccd446eSAtari911        }
19649ccd446eSAtari911
19659ccd446eSAtari911        echo '</div>';
19669ccd446eSAtari911
19679ccd446eSAtari911        // Backup list or manual backup section
19681d05cddcSAtari911        $backupDir = DOKU_PLUGIN;
19691d05cddcSAtari911        $backups = glob($backupDir . 'calendar*.zip');
19701d05cddcSAtari911
19711d05cddcSAtari911        // Filter to only show files that look like backups (not the uploaded plugin files)
19721d05cddcSAtari911        $backups = array_filter($backups, function($file) {
19731d05cddcSAtari911            $name = basename($file);
19741d05cddcSAtari911            // Include files that start with "calendar" but exclude files that are just "calendar.zip" (uploaded plugin)
19751d05cddcSAtari911            return $name !== 'calendar.zip';
19761d05cddcSAtari911        });
19771d05cddcSAtari911
19789ccd446eSAtari911        // Always show backup section (even if no backups yet)
19799ccd446eSAtari911        echo '<div id="backupSection" style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:900px;">';
19809ccd446eSAtari911        echo '<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:8px;">';
19819ccd446eSAtari911        echo '<h3 style="margin:0; color:#00cc07; font-size:16px;">�� Backups</h3>';
19829ccd446eSAtari911
19839ccd446eSAtari911        // Manual backup button
19849ccd446eSAtari911        echo '<form method="post" action="?do=admin&page=calendar&tab=update" style="margin:0;">';
19859ccd446eSAtari911        echo '<input type="hidden" name="action" value="create_manual_backup">';
19869ccd446eSAtari911        echo '<button type="submit" onclick="return confirm(\'Create a backup of the current plugin version?\')" style="background:#00cc07; color:white; padding:6px 12px; border:none; border-radius:3px; cursor:pointer; font-size:12px; font-weight:bold;">�� Create Backup Now</button>';
19879ccd446eSAtari911        echo '</form>';
19889ccd446eSAtari911        echo '</div>';
19899ccd446eSAtari911
19901d05cddcSAtari911        if (!empty($backups)) {
19911d05cddcSAtari911            rsort($backups); // Newest first
19929ccd446eSAtari911            echo '<div style="max-height:200px; overflow-y:auto; border:1px solid ' . $colors['border'] . '; border-radius:3px; background:' . $colors['bg'] . ';">';
19939ccd446eSAtari911            echo '<table id="backupTable" style="width:100%; border-collapse:collapse; font-size:12px;">';
19941d05cddcSAtari911            echo '<thead style="position:sticky; top:0; background:#e9e9e9;">';
19951d05cddcSAtari911            echo '<tr>';
19969ccd446eSAtari911            echo '<th style="padding:6px; text-align:left; border-bottom:2px solid ' . $colors['border'] . ';">Backup File</th>';
19979ccd446eSAtari911            echo '<th style="padding:6px; text-align:left; border-bottom:2px solid ' . $colors['border'] . ';">Size</th>';
19989ccd446eSAtari911            echo '<th style="padding:6px; text-align:left; border-bottom:2px solid ' . $colors['border'] . ';">Actions</th>';
19991d05cddcSAtari911            echo '</tr></thead><tbody>';
20001d05cddcSAtari911
20011d05cddcSAtari911            foreach ($backups as $backup) {
20021d05cddcSAtari911                $filename = basename($backup);
20031d05cddcSAtari911                $size = $this->formatBytes(filesize($backup));
20041d05cddcSAtari911                echo '<tr style="border-bottom:1px solid #eee;">';
20051d05cddcSAtari911                echo '<td style="padding:6px;"><code style="font-size:11px;">' . hsc($filename) . '</code></td>';
20061d05cddcSAtari911                echo '<td style="padding:6px;">' . $size . '</td>';
20071d05cddcSAtari911                echo '<td style="padding:6px; white-space:nowrap;">';
20081d05cddcSAtari911                echo '<a href="' . DOKU_BASE . 'lib/plugins/' . hsc($filename) . '" download style="color:#00cc07; text-decoration:none; font-size:11px; margin-right:10px;">�� Download</a>';
20091d05cddcSAtari911                echo '<button onclick="renameBackup(\'' . hsc(addslashes($filename)) . '\')" style="background:#f39c12; color:white; border:none; padding:2px 6px; border-radius:2px; cursor:pointer; font-size:10px; margin-right:5px;">✏️ Rename</button>';
20101d05cddcSAtari911                echo '<button onclick="restoreBackup(\'' . hsc(addslashes($filename)) . '\')" style="background:#7b1fa2; color:white; border:none; padding:2px 6px; border-radius:2px; cursor:pointer; font-size:10px; margin-right:5px;">�� Restore</button>';
20111d05cddcSAtari911                echo '<button onclick="deleteBackup(\'' . hsc(addslashes($filename)) . '\')" style="background:#e74c3c; color:white; border:none; padding:2px 6px; border-radius:2px; cursor:pointer; font-size:10px;">��️ Delete</button>';
20121d05cddcSAtari911                echo '</td>';
20131d05cddcSAtari911                echo '</tr>';
20141d05cddcSAtari911            }
20151d05cddcSAtari911
20161d05cddcSAtari911            echo '</tbody></table>';
20171d05cddcSAtari911            echo '</div>';
20189ccd446eSAtari911        } else {
20199ccd446eSAtari911            echo '<p style="color:' . $colors['text'] . '; font-size:13px; margin:8px 0;">No backups yet. Click "Create Backup Now" to create your first backup.</p>';
20201d05cddcSAtari911        }
20219ccd446eSAtari911        echo '</div>';
20221d05cddcSAtari911
20231d05cddcSAtari911        echo '<script>
20241d05cddcSAtari911        function confirmUpload() {
20251d05cddcSAtari911            const fileInput = document.querySelector(\'input[name="plugin_zip"]\');
20261d05cddcSAtari911            if (!fileInput.files[0]) {
20271d05cddcSAtari911                alert("Please select a ZIP file");
20281d05cddcSAtari911                return false;
20291d05cddcSAtari911            }
20301d05cddcSAtari911
20311d05cddcSAtari911            const fileName = fileInput.files[0].name;
20321d05cddcSAtari911            if (!fileName.endsWith(".zip")) {
20331d05cddcSAtari911                alert("Please select a ZIP file");
20341d05cddcSAtari911                return false;
20351d05cddcSAtari911            }
20361d05cddcSAtari911
20371d05cddcSAtari911            return confirm("Upload and install: " + fileName + "?\\n\\nThis will replace all plugin files.\\nYour configuration and data will be preserved.\\n\\nContinue?");
20381d05cddcSAtari911        }
20391d05cddcSAtari911
20401d05cddcSAtari911        function deleteBackup(filename) {
20411d05cddcSAtari911            if (!confirm("Delete backup: " + filename + "?\\n\\nThis cannot be undone!")) {
20421d05cddcSAtari911                return;
20431d05cddcSAtari911            }
20441d05cddcSAtari911
20459ccd446eSAtari911            // Use AJAX to delete without page refresh
20469ccd446eSAtari911            const formData = new FormData();
20479ccd446eSAtari911            formData.append(\'action\', \'delete_backup\');
20489ccd446eSAtari911            formData.append(\'backup_file\', filename);
20491d05cddcSAtari911
20509ccd446eSAtari911            fetch(\'?do=admin&page=calendar&tab=update\', {
20519ccd446eSAtari911                method: \'POST\',
20529ccd446eSAtari911                body: formData
20539ccd446eSAtari911            })
20549ccd446eSAtari911            .then(response => response.text())
20559ccd446eSAtari911            .then(data => {
20569ccd446eSAtari911                // Remove the row from the table
20579ccd446eSAtari911                const rows = document.querySelectorAll(\'tr\');
20589ccd446eSAtari911                rows.forEach(row => {
20599ccd446eSAtari911                    if (row.textContent.includes(filename)) {
20609ccd446eSAtari911                        row.style.transition = \'opacity 0.3s\';
20619ccd446eSAtari911                        row.style.opacity = \'0\';
20629ccd446eSAtari911                        setTimeout(() => {
20639ccd446eSAtari911                            row.remove();
20649ccd446eSAtari911                            // Check if table is now empty
20659ccd446eSAtari911                            const tbody = document.querySelector(\'#backupTable tbody\');
20669ccd446eSAtari911                            if (tbody && tbody.children.length === 0) {
20679ccd446eSAtari911                                const backupSection = document.querySelector(\'#backupSection\');
20689ccd446eSAtari911                                if (backupSection) {
20699ccd446eSAtari911                                    backupSection.style.transition = \'opacity 0.3s\';
20709ccd446eSAtari911                                    backupSection.style.opacity = \'0\';
20719ccd446eSAtari911                                    setTimeout(() => backupSection.remove(), 300);
20729ccd446eSAtari911                                }
20739ccd446eSAtari911                            }
20749ccd446eSAtari911                        }, 300);
20759ccd446eSAtari911                    }
20769ccd446eSAtari911                });
20771d05cddcSAtari911
20789ccd446eSAtari911                // Show success message
20799ccd446eSAtari911                const msg = document.createElement(\'div\');
20809ccd446eSAtari911                msg.style.cssText = \'padding:10px; margin:10px 0; border-left:3px solid #28a745; background:#d4edda; border-radius:3px; max-width:900px; transition:opacity 0.3s;\';
20819ccd446eSAtari911                msg.textContent = \'✓ Backup deleted: \' + filename;
20829ccd446eSAtari911                document.querySelector(\'h2\').after(msg);
20839ccd446eSAtari911                setTimeout(() => {
20849ccd446eSAtari911                    msg.style.opacity = \'0\';
20859ccd446eSAtari911                    setTimeout(() => msg.remove(), 300);
20869ccd446eSAtari911                }, 3000);
20879ccd446eSAtari911            })
20889ccd446eSAtari911            .catch(error => {
20899ccd446eSAtari911                alert(\'Error deleting backup: \' + error);
20909ccd446eSAtari911            });
20911d05cddcSAtari911        }
20921d05cddcSAtari911
20931d05cddcSAtari911        function restoreBackup(filename) {
20941d05cddcSAtari911            if (!confirm("Restore from backup: " + filename + "?\\n\\nThis will replace all current plugin files with the backup version.\\nYour current configuration will be replaced with the backed up configuration.\\n\\nContinue?")) {
20951d05cddcSAtari911                return;
20961d05cddcSAtari911            }
20971d05cddcSAtari911
20981d05cddcSAtari911            const form = document.createElement("form");
20991d05cddcSAtari911            form.method = "POST";
21001d05cddcSAtari911            form.action = "?do=admin&page=calendar&tab=update";
21011d05cddcSAtari911
21021d05cddcSAtari911            const actionInput = document.createElement("input");
21031d05cddcSAtari911            actionInput.type = "hidden";
21041d05cddcSAtari911            actionInput.name = "action";
21051d05cddcSAtari911            actionInput.value = "restore_backup";
21061d05cddcSAtari911            form.appendChild(actionInput);
21071d05cddcSAtari911
21081d05cddcSAtari911            const filenameInput = document.createElement("input");
21091d05cddcSAtari911            filenameInput.type = "hidden";
21101d05cddcSAtari911            filenameInput.name = "backup_file";
21111d05cddcSAtari911            filenameInput.value = filename;
21121d05cddcSAtari911            form.appendChild(filenameInput);
21131d05cddcSAtari911
21141d05cddcSAtari911            document.body.appendChild(form);
21151d05cddcSAtari911            form.submit();
21161d05cddcSAtari911        }
21171d05cddcSAtari911
21181d05cddcSAtari911        function renameBackup(filename) {
21191d05cddcSAtari911            const newName = prompt("Enter new backup name (without .zip extension):\\n\\nCurrent: " + filename.replace(/\\.zip$/, ""), filename.replace(/\\.zip$/, ""));
21201d05cddcSAtari911            if (!newName || newName === filename.replace(/\\.zip$/, "")) {
21211d05cddcSAtari911                return;
21221d05cddcSAtari911            }
21231d05cddcSAtari911
21241d05cddcSAtari911            // Add .zip if not present
21251d05cddcSAtari911            const newFilename = newName.endsWith(".zip") ? newName : newName + ".zip";
21261d05cddcSAtari911
21271d05cddcSAtari911            // Basic validation
21281d05cddcSAtari911            if (!/^[a-zA-Z0-9._-]+$/.test(newFilename.replace(/\\.zip$/, ""))) {
21291d05cddcSAtari911                alert("Invalid filename. Use only letters, numbers, dots, dashes, and underscores.");
21301d05cddcSAtari911                return;
21311d05cddcSAtari911            }
21321d05cddcSAtari911
21331d05cddcSAtari911            const form = document.createElement("form");
21341d05cddcSAtari911            form.method = "POST";
21351d05cddcSAtari911            form.action = "?do=admin&page=calendar&tab=update";
21361d05cddcSAtari911
21371d05cddcSAtari911            const actionInput = document.createElement("input");
21381d05cddcSAtari911            actionInput.type = "hidden";
21391d05cddcSAtari911            actionInput.name = "action";
21401d05cddcSAtari911            actionInput.value = "rename_backup";
21411d05cddcSAtari911            form.appendChild(actionInput);
21421d05cddcSAtari911
21431d05cddcSAtari911            const oldNameInput = document.createElement("input");
21441d05cddcSAtari911            oldNameInput.type = "hidden";
21451d05cddcSAtari911            oldNameInput.name = "old_name";
21461d05cddcSAtari911            oldNameInput.value = filename;
21471d05cddcSAtari911            form.appendChild(oldNameInput);
21481d05cddcSAtari911
21491d05cddcSAtari911            const newNameInput = document.createElement("input");
21501d05cddcSAtari911            newNameInput.type = "hidden";
21511d05cddcSAtari911            newNameInput.name = "new_name";
21521d05cddcSAtari911            newNameInput.value = newFilename;
21531d05cddcSAtari911            form.appendChild(newNameInput);
21541d05cddcSAtari911
21551d05cddcSAtari911            document.body.appendChild(form);
21561d05cddcSAtari911            form.submit();
21571d05cddcSAtari911        }
21581d05cddcSAtari911        </script>';
21591d05cddcSAtari911    }
21601d05cddcSAtari911
21611d05cddcSAtari911    private function saveConfig() {
21621d05cddcSAtari911        global $INPUT;
21631d05cddcSAtari911
21641d05cddcSAtari911        // Load existing config to preserve all settings
21651d05cddcSAtari911        $configFile = DOKU_PLUGIN . 'calendar/sync_config.php';
21661d05cddcSAtari911        $existingConfig = [];
21671d05cddcSAtari911        if (file_exists($configFile)) {
21681d05cddcSAtari911            $existingConfig = include $configFile;
21691d05cddcSAtari911        }
21701d05cddcSAtari911
21711d05cddcSAtari911        // Update only the fields from the form - preserve everything else
21721d05cddcSAtari911        $config = $existingConfig;
21731d05cddcSAtari911
21741d05cddcSAtari911        // Update basic fields
21751d05cddcSAtari911        $config['tenant_id'] = $INPUT->str('tenant_id');
21761d05cddcSAtari911        $config['client_id'] = $INPUT->str('client_id');
21771d05cddcSAtari911        $config['client_secret'] = $INPUT->str('client_secret');
21781d05cddcSAtari911        $config['user_email'] = $INPUT->str('user_email');
21791d05cddcSAtari911        $config['timezone'] = $INPUT->str('timezone', 'America/Los_Angeles');
21801d05cddcSAtari911        $config['default_category'] = $INPUT->str('default_category', 'Blue category');
21811d05cddcSAtari911        $config['reminder_minutes'] = $INPUT->int('reminder_minutes', 15);
21821d05cddcSAtari911        $config['sync_completed_tasks'] = $INPUT->bool('sync_completed_tasks');
21831d05cddcSAtari911        $config['delete_outlook_events'] = $INPUT->bool('delete_outlook_events');
21841d05cddcSAtari911        $config['sync_all_namespaces'] = $INPUT->bool('sync_all_namespaces');
21851d05cddcSAtari911        $config['sync_namespaces'] = $INPUT->arr('sync_namespaces');
2186*4590242dSAtari911        // important_namespaces is managed from the Manage tab, preserve existing value
2187*4590242dSAtari911        if (!isset($config['important_namespaces'])) {
2188*4590242dSAtari911            $config['important_namespaces'] = 'important';
2189*4590242dSAtari911        }
21901d05cddcSAtari911
21911d05cddcSAtari911        // Parse category mapping
21921d05cddcSAtari911        $config['category_mapping'] = [];
21931d05cddcSAtari911        $mappingText = $INPUT->str('category_mapping');
21941d05cddcSAtari911        if ($mappingText) {
21951d05cddcSAtari911            $lines = explode("\n", $mappingText);
21961d05cddcSAtari911            foreach ($lines as $line) {
21971d05cddcSAtari911                $line = trim($line);
21981d05cddcSAtari911                if (empty($line)) continue;
21991d05cddcSAtari911                $parts = explode('=', $line, 2);
22001d05cddcSAtari911                if (count($parts) === 2) {
22011d05cddcSAtari911                    $config['category_mapping'][trim($parts[0])] = trim($parts[1]);
22021d05cddcSAtari911                }
22031d05cddcSAtari911            }
22041d05cddcSAtari911        }
22051d05cddcSAtari911
22061d05cddcSAtari911        // Parse color mapping from dropdown selections
22071d05cddcSAtari911        $config['color_mapping'] = [];
22081d05cddcSAtari911        $colorMappingCount = $INPUT->int('color_mapping_count', 0);
22091d05cddcSAtari911        for ($i = 0; $i < $colorMappingCount; $i++) {
22101d05cddcSAtari911            $hexColor = $INPUT->str('color_hex_' . $i);
22111d05cddcSAtari911            $category = $INPUT->str('color_map_' . $i);
22121d05cddcSAtari911
22131d05cddcSAtari911            if (!empty($hexColor) && !empty($category)) {
22141d05cddcSAtari911                $config['color_mapping'][$hexColor] = $category;
22151d05cddcSAtari911            }
22161d05cddcSAtari911        }
22171d05cddcSAtari911
22181d05cddcSAtari911        // Build file content using return format
22191d05cddcSAtari911        $content = "<?php\n";
22201d05cddcSAtari911        $content .= "/**\n";
22211d05cddcSAtari911        $content .= " * DokuWiki Calendar → Outlook Sync - Configuration\n";
22221d05cddcSAtari911        $content .= " * \n";
22231d05cddcSAtari911        $content .= " * SECURITY: Add this file to .gitignore!\n";
22241d05cddcSAtari911        $content .= " * Never commit credentials to version control.\n";
22251d05cddcSAtari911        $content .= " */\n\n";
22261d05cddcSAtari911        $content .= "return " . var_export($config, true) . ";\n";
22271d05cddcSAtari911
22281d05cddcSAtari911        // Save file
22291d05cddcSAtari911        if (file_put_contents($configFile, $content)) {
22301d05cddcSAtari911            $this->redirect('Configuration saved successfully!', 'success');
22311d05cddcSAtari911        } else {
22321d05cddcSAtari911            $this->redirect('Error: Could not save configuration file', 'error');
22331d05cddcSAtari911        }
22341d05cddcSAtari911    }
22351d05cddcSAtari911
22361d05cddcSAtari911    private function clearCache() {
22371d05cddcSAtari911        // Clear DokuWiki cache
22381d05cddcSAtari911        $cacheDir = DOKU_INC . 'data/cache';
22391d05cddcSAtari911
22401d05cddcSAtari911        if (is_dir($cacheDir)) {
22411d05cddcSAtari911            $this->recursiveDelete($cacheDir, false);
22421d05cddcSAtari911            $this->redirect('Cache cleared successfully!', 'success', 'update');
22431d05cddcSAtari911        } else {
22441d05cddcSAtari911            $this->redirect('Cache directory not found', 'error', 'update');
22451d05cddcSAtari911        }
22461d05cddcSAtari911    }
22471d05cddcSAtari911
22481d05cddcSAtari911    private function recursiveDelete($dir, $deleteRoot = true) {
22491d05cddcSAtari911        if (!is_dir($dir)) return;
22501d05cddcSAtari911
22511d05cddcSAtari911        $files = array_diff(scandir($dir), array('.', '..'));
22521d05cddcSAtari911        foreach ($files as $file) {
22531d05cddcSAtari911            $path = $dir . '/' . $file;
22541d05cddcSAtari911            if (is_dir($path)) {
22551d05cddcSAtari911                $this->recursiveDelete($path, true);
22561d05cddcSAtari911            } else {
22571d05cddcSAtari911                @unlink($path);
22581d05cddcSAtari911            }
22591d05cddcSAtari911        }
22601d05cddcSAtari911
22611d05cddcSAtari911        if ($deleteRoot) {
22621d05cddcSAtari911            @rmdir($dir);
22631d05cddcSAtari911        }
22641d05cddcSAtari911    }
22651d05cddcSAtari911
22661d05cddcSAtari911    private function findRecurringEvents() {
22671d05cddcSAtari911        $dataDir = DOKU_INC . 'data/meta/';
22681d05cddcSAtari911        $recurring = [];
22691d05cddcSAtari911        $allEvents = []; // Track all events to detect patterns
22701d05cddcSAtari911
22711d05cddcSAtari911        // Check root calendar directory first (blank/default namespace)
22721d05cddcSAtari911        $rootCalendarDir = $dataDir . 'calendar';
22731d05cddcSAtari911        if (is_dir($rootCalendarDir)) {
22741d05cddcSAtari911            foreach (glob($rootCalendarDir . '/*.json') as $file) {
22751d05cddcSAtari911                $data = json_decode(file_get_contents($file), true);
22761d05cddcSAtari911                if (!$data) continue;
22771d05cddcSAtari911
22781d05cddcSAtari911                foreach ($data as $dateKey => $events) {
22791d05cddcSAtari911                    foreach ($events as $event) {
22801d05cddcSAtari911                        // Group by title + namespace (events with same title are likely recurring)
22811d05cddcSAtari911                        $groupKey = strtolower(trim($event['title'])) . '_';
22821d05cddcSAtari911
22831d05cddcSAtari911                        if (!isset($allEvents[$groupKey])) {
22841d05cddcSAtari911                            $allEvents[$groupKey] = [
22851d05cddcSAtari911                                'title' => $event['title'],
22861d05cddcSAtari911                                'namespace' => '',
22871d05cddcSAtari911                                'dates' => [],
22881d05cddcSAtari911                                'events' => []
22891d05cddcSAtari911                            ];
22901d05cddcSAtari911                        }
22911d05cddcSAtari911                        $allEvents[$groupKey]['dates'][] = $dateKey;
22921d05cddcSAtari911                        $allEvents[$groupKey]['events'][] = $event;
22931d05cddcSAtari911                    }
22941d05cddcSAtari911                }
22951d05cddcSAtari911            }
22961d05cddcSAtari911        }
22971d05cddcSAtari911
22981d05cddcSAtari911        // Scan all namespace directories
22991d05cddcSAtari911        foreach (glob($dataDir . '*', GLOB_ONLYDIR) as $nsDir) {
23001d05cddcSAtari911            $namespace = basename($nsDir);
23011d05cddcSAtari911
23021d05cddcSAtari911            // Skip the root 'calendar' dir (already processed above)
23031d05cddcSAtari911            if ($namespace === 'calendar') continue;
23041d05cddcSAtari911
23051d05cddcSAtari911            $calendarDir = $nsDir . '/calendar';
23061d05cddcSAtari911
23071d05cddcSAtari911            if (!is_dir($calendarDir)) continue;
23081d05cddcSAtari911
23091d05cddcSAtari911            // Scan all calendar files
23101d05cddcSAtari911            foreach (glob($calendarDir . '/*.json') as $file) {
23111d05cddcSAtari911                $data = json_decode(file_get_contents($file), true);
23121d05cddcSAtari911                if (!$data) continue;
23131d05cddcSAtari911
23141d05cddcSAtari911                foreach ($data as $dateKey => $events) {
23151d05cddcSAtari911                    foreach ($events as $event) {
23161d05cddcSAtari911                        $groupKey = strtolower(trim($event['title'])) . '_' . ($event['namespace'] ?? '');
23171d05cddcSAtari911
23181d05cddcSAtari911                        if (!isset($allEvents[$groupKey])) {
23191d05cddcSAtari911                            $allEvents[$groupKey] = [
23201d05cddcSAtari911                                'title' => $event['title'],
23211d05cddcSAtari911                                'namespace' => $event['namespace'] ?? '',
23221d05cddcSAtari911                                'dates' => [],
23231d05cddcSAtari911                                'events' => []
23241d05cddcSAtari911                            ];
23251d05cddcSAtari911                        }
23261d05cddcSAtari911                        $allEvents[$groupKey]['dates'][] = $dateKey;
23271d05cddcSAtari911                        $allEvents[$groupKey]['events'][] = $event;
23281d05cddcSAtari911                    }
23291d05cddcSAtari911                }
23301d05cddcSAtari911            }
23311d05cddcSAtari911        }
23321d05cddcSAtari911
23331d05cddcSAtari911        // Analyze patterns - only include if 3+ occurrences
23341d05cddcSAtari911        foreach ($allEvents as $groupKey => $group) {
23351d05cddcSAtari911            if (count($group['dates']) >= 3) {
23361d05cddcSAtari911                // Sort dates
23371d05cddcSAtari911                sort($group['dates']);
23381d05cddcSAtari911
23391d05cddcSAtari911                // Calculate interval between first and second occurrence
23401d05cddcSAtari911                $date1 = new DateTime($group['dates'][0]);
23411d05cddcSAtari911                $date2 = new DateTime($group['dates'][1]);
23421d05cddcSAtari911                $interval = $date1->diff($date2);
23431d05cddcSAtari911
23441d05cddcSAtari911                // Determine pattern
23451d05cddcSAtari911                $pattern = 'Custom';
23461d05cddcSAtari911                if ($interval->days == 1) {
23471d05cddcSAtari911                    $pattern = 'Daily';
23481d05cddcSAtari911                } elseif ($interval->days == 7) {
23491d05cddcSAtari911                    $pattern = 'Weekly';
23501d05cddcSAtari911                } elseif ($interval->days >= 14 && $interval->days <= 16) {
23511d05cddcSAtari911                    $pattern = 'Bi-weekly';
23521d05cddcSAtari911                } elseif ($interval->days >= 28 && $interval->days <= 31) {
23531d05cddcSAtari911                    $pattern = 'Monthly';
23541d05cddcSAtari911                } elseif ($interval->days >= 365 && $interval->days <= 366) {
23551d05cddcSAtari911                    $pattern = 'Yearly';
23561d05cddcSAtari911                }
23571d05cddcSAtari911
23581d05cddcSAtari911                // Use first event's ID or create a synthetic one
23591d05cddcSAtari911                $baseId = isset($group['events'][0]['recurringId'])
23601d05cddcSAtari911                    ? $group['events'][0]['recurringId']
23611d05cddcSAtari911                    : md5($group['title'] . $group['namespace']);
23621d05cddcSAtari911
23631d05cddcSAtari911                $recurring[] = [
23641d05cddcSAtari911                    'baseId' => $baseId,
23651d05cddcSAtari911                    'title' => $group['title'],
23661d05cddcSAtari911                    'namespace' => $group['namespace'],
23671d05cddcSAtari911                    'pattern' => $pattern,
23681d05cddcSAtari911                    'count' => count($group['dates']),
23691d05cddcSAtari911                    'firstDate' => $group['dates'][0],
23701d05cddcSAtari911                    'interval' => $interval->days
23711d05cddcSAtari911                ];
23721d05cddcSAtari911            }
23731d05cddcSAtari911        }
23741d05cddcSAtari911
23751d05cddcSAtari911        return $recurring;
23761d05cddcSAtari911    }
23771d05cddcSAtari911
23781d05cddcSAtari911    private function getEventsByNamespace() {
23791d05cddcSAtari911        $dataDir = DOKU_INC . 'data/meta/';
23801d05cddcSAtari911        $result = [];
23811d05cddcSAtari911
23821d05cddcSAtari911        // Check root calendar directory first (blank/default namespace)
23831d05cddcSAtari911        $rootCalendarDir = $dataDir . 'calendar';
23841d05cddcSAtari911        if (is_dir($rootCalendarDir)) {
23851d05cddcSAtari911            $hasFiles = false;
23861d05cddcSAtari911            $events = [];
23871d05cddcSAtari911
23881d05cddcSAtari911            foreach (glob($rootCalendarDir . '/*.json') as $file) {
23891d05cddcSAtari911                $hasFiles = true;
23901d05cddcSAtari911                $month = basename($file, '.json');
23911d05cddcSAtari911                $data = json_decode(file_get_contents($file), true);
23921d05cddcSAtari911                if (!$data) continue;
23931d05cddcSAtari911
23941d05cddcSAtari911                foreach ($data as $dateKey => $eventList) {
23951d05cddcSAtari911                    foreach ($eventList as $event) {
23961d05cddcSAtari911                        $events[] = [
23971d05cddcSAtari911                            'id' => $event['id'],
23981d05cddcSAtari911                            'title' => $event['title'],
23991d05cddcSAtari911                            'date' => $dateKey,
24001d05cddcSAtari911                            'startTime' => $event['startTime'] ?? null,
24011d05cddcSAtari911                            'month' => $month
24021d05cddcSAtari911                        ];
24031d05cddcSAtari911                    }
24041d05cddcSAtari911                }
24051d05cddcSAtari911            }
24061d05cddcSAtari911
24071d05cddcSAtari911            // Add if it has JSON files (even if empty)
24081d05cddcSAtari911            if ($hasFiles) {
24091d05cddcSAtari911                $result[''] = ['events' => $events];
24101d05cddcSAtari911            }
24111d05cddcSAtari911        }
24121d05cddcSAtari911
24131d05cddcSAtari911        // Recursively scan all namespace directories including sub-namespaces
24141d05cddcSAtari911        $this->scanNamespaceRecursive($dataDir, '', $result);
24151d05cddcSAtari911
24161d05cddcSAtari911        // Sort namespaces, but keep '' (default) first
24171d05cddcSAtari911        uksort($result, function($a, $b) {
24181d05cddcSAtari911            if ($a === '') return -1;
24191d05cddcSAtari911            if ($b === '') return 1;
24201d05cddcSAtari911            return strcmp($a, $b);
24211d05cddcSAtari911        });
24221d05cddcSAtari911
24231d05cddcSAtari911        return $result;
24241d05cddcSAtari911    }
24251d05cddcSAtari911
24261d05cddcSAtari911    private function scanNamespaceRecursive($baseDir, $parentNamespace, &$result) {
24271d05cddcSAtari911        foreach (glob($baseDir . '*', GLOB_ONLYDIR) as $nsDir) {
24281d05cddcSAtari911            $dirName = basename($nsDir);
24291d05cddcSAtari911
24301d05cddcSAtari911            // Skip the root 'calendar' dir
24311d05cddcSAtari911            if ($dirName === 'calendar' && empty($parentNamespace)) continue;
24321d05cddcSAtari911
24331d05cddcSAtari911            // Build namespace path
24341d05cddcSAtari911            $namespace = empty($parentNamespace) ? $dirName : $parentNamespace . ':' . $dirName;
24351d05cddcSAtari911
24361d05cddcSAtari911            // Check for calendar directory
24371d05cddcSAtari911            $calendarDir = $nsDir . '/calendar';
24381d05cddcSAtari911            if (is_dir($calendarDir)) {
24391d05cddcSAtari911                $hasFiles = false;
24401d05cddcSAtari911                $events = [];
24411d05cddcSAtari911
24421d05cddcSAtari911                // Scan all calendar files
24431d05cddcSAtari911                foreach (glob($calendarDir . '/*.json') as $file) {
24441d05cddcSAtari911                    $hasFiles = true;
24451d05cddcSAtari911                    $month = basename($file, '.json');
24461d05cddcSAtari911                    $data = json_decode(file_get_contents($file), true);
24471d05cddcSAtari911                    if (!$data) continue;
24481d05cddcSAtari911
24491d05cddcSAtari911                    foreach ($data as $dateKey => $eventList) {
24501d05cddcSAtari911                        foreach ($eventList as $event) {
24511d05cddcSAtari911                            $events[] = [
24521d05cddcSAtari911                                'id' => $event['id'],
24531d05cddcSAtari911                                'title' => $event['title'],
24541d05cddcSAtari911                                'date' => $dateKey,
24551d05cddcSAtari911                                'startTime' => $event['startTime'] ?? null,
24561d05cddcSAtari911                                'month' => $month
24571d05cddcSAtari911                            ];
24581d05cddcSAtari911                        }
24591d05cddcSAtari911                    }
24601d05cddcSAtari911                }
24611d05cddcSAtari911
24621d05cddcSAtari911                // Add namespace if it has JSON files (even if empty)
24631d05cddcSAtari911                if ($hasFiles) {
24641d05cddcSAtari911                    $result[$namespace] = ['events' => $events];
24651d05cddcSAtari911                }
24661d05cddcSAtari911            }
24671d05cddcSAtari911
24681d05cddcSAtari911            // Recursively scan sub-directories
24691d05cddcSAtari911            $this->scanNamespaceRecursive($nsDir . '/', $namespace, $result);
24701d05cddcSAtari911        }
24711d05cddcSAtari911    }
24721d05cddcSAtari911
24731d05cddcSAtari911    private function getAllNamespaces() {
24741d05cddcSAtari911        $dataDir = DOKU_INC . 'data/meta/';
24751d05cddcSAtari911        $namespaces = [];
24761d05cddcSAtari911
24771d05cddcSAtari911        // Check root calendar directory first
24781d05cddcSAtari911        $rootCalendarDir = $dataDir . 'calendar';
24791d05cddcSAtari911        if (is_dir($rootCalendarDir)) {
24801d05cddcSAtari911            $namespaces[] = '';  // Blank/default namespace
24811d05cddcSAtari911        }
24821d05cddcSAtari911
24831d05cddcSAtari911        // Check all other namespace directories
24841d05cddcSAtari911        foreach (glob($dataDir . '*', GLOB_ONLYDIR) as $nsDir) {
24851d05cddcSAtari911            $namespace = basename($nsDir);
24861d05cddcSAtari911
24871d05cddcSAtari911            // Skip the root 'calendar' dir (already added as '')
24881d05cddcSAtari911            if ($namespace === 'calendar') continue;
24891d05cddcSAtari911
24901d05cddcSAtari911            $calendarDir = $nsDir . '/calendar';
24911d05cddcSAtari911            if (is_dir($calendarDir)) {
24921d05cddcSAtari911                $namespaces[] = $namespace;
24931d05cddcSAtari911            }
24941d05cddcSAtari911        }
24951d05cddcSAtari911
24961d05cddcSAtari911        return $namespaces;
24971d05cddcSAtari911    }
24981d05cddcSAtari911
24991d05cddcSAtari911    private function searchEvents($search, $filterNamespace) {
25001d05cddcSAtari911        $dataDir = DOKU_INC . 'data/meta/';
25011d05cddcSAtari911        $results = [];
25021d05cddcSAtari911
25031d05cddcSAtari911        $search = strtolower(trim($search));
25041d05cddcSAtari911
25051d05cddcSAtari911        foreach (glob($dataDir . '*', GLOB_ONLYDIR) as $nsDir) {
25061d05cddcSAtari911            $namespace = basename($nsDir);
25071d05cddcSAtari911            $calendarDir = $nsDir . '/calendar';
25081d05cddcSAtari911
25091d05cddcSAtari911            if (!is_dir($calendarDir)) continue;
25101d05cddcSAtari911            if ($filterNamespace !== '' && $namespace !== $filterNamespace) continue;
25111d05cddcSAtari911
25121d05cddcSAtari911            foreach (glob($calendarDir . '/*.json') as $file) {
25131d05cddcSAtari911                $month = basename($file, '.json');
25141d05cddcSAtari911                $data = json_decode(file_get_contents($file), true);
25151d05cddcSAtari911                if (!$data) continue;
25161d05cddcSAtari911
25171d05cddcSAtari911                foreach ($data as $dateKey => $events) {
25181d05cddcSAtari911                    foreach ($events as $event) {
25191d05cddcSAtari911                        if ($search === '' || strpos(strtolower($event['title']), $search) !== false) {
25201d05cddcSAtari911                            $results[] = [
25211d05cddcSAtari911                                'id' => $event['id'],
25221d05cddcSAtari911                                'title' => $event['title'],
25231d05cddcSAtari911                                'date' => $dateKey,
25241d05cddcSAtari911                                'startTime' => $event['startTime'] ?? null,
25251d05cddcSAtari911                                'namespace' => $event['namespace'] ?? '',
25261d05cddcSAtari911                                'month' => $month
25271d05cddcSAtari911                            ];
25281d05cddcSAtari911                        }
25291d05cddcSAtari911                    }
25301d05cddcSAtari911                }
25311d05cddcSAtari911            }
25321d05cddcSAtari911        }
25331d05cddcSAtari911
25341d05cddcSAtari911        return $results;
25351d05cddcSAtari911    }
25361d05cddcSAtari911
25371d05cddcSAtari911    private function deleteRecurringSeries() {
25381d05cddcSAtari911        global $INPUT;
25391d05cddcSAtari911
25401d05cddcSAtari911        $eventTitle = $INPUT->str('event_title');
25411d05cddcSAtari911        $namespace = $INPUT->str('namespace');
25421d05cddcSAtari911
25431d05cddcSAtari911        // Determine calendar directory
25441d05cddcSAtari911        if ($namespace === '') {
25451d05cddcSAtari911            $dataDir = DOKU_INC . 'data/meta/calendar';
25461d05cddcSAtari911        } else {
25471d05cddcSAtari911            $dataDir = DOKU_INC . 'data/meta/' . $namespace . '/calendar';
25481d05cddcSAtari911        }
25491d05cddcSAtari911
25501d05cddcSAtari911        $count = 0;
25511d05cddcSAtari911
25521d05cddcSAtari911        if (is_dir($dataDir)) {
25531d05cddcSAtari911            foreach (glob($dataDir . '/*.json') as $file) {
25541d05cddcSAtari911                $data = json_decode(file_get_contents($file), true);
25551d05cddcSAtari911                if (!$data) continue;
25561d05cddcSAtari911
25571d05cddcSAtari911                $modified = false;
25581d05cddcSAtari911                foreach ($data as $dateKey => $events) {
25591d05cddcSAtari911                    $filtered = [];
25601d05cddcSAtari911                    foreach ($events as $event) {
25611d05cddcSAtari911                        // Match by title (case-insensitive)
25621d05cddcSAtari911                        if (strtolower(trim($event['title'])) === strtolower(trim($eventTitle))) {
25631d05cddcSAtari911                            $count++;
25641d05cddcSAtari911                            $modified = true;
25651d05cddcSAtari911                        } else {
25661d05cddcSAtari911                            $filtered[] = $event;
25671d05cddcSAtari911                        }
25681d05cddcSAtari911                    }
25691d05cddcSAtari911                    $data[$dateKey] = $filtered;
25701d05cddcSAtari911                }
25711d05cddcSAtari911
25721d05cddcSAtari911                if ($modified) {
25739ccd446eSAtari911                    // Clean up empty date keys
25749ccd446eSAtari911                    foreach ($data as $dk => $evts) {
25759ccd446eSAtari911                        if (empty($evts)) unset($data[$dk]);
25769ccd446eSAtari911                    }
25779ccd446eSAtari911
25789ccd446eSAtari911                    if (empty($data)) {
25799ccd446eSAtari911                        unlink($file);
25809ccd446eSAtari911                    } else {
25811d05cddcSAtari911                        file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT));
25821d05cddcSAtari911                    }
25831d05cddcSAtari911                }
25841d05cddcSAtari911            }
25859ccd446eSAtari911        }
25861d05cddcSAtari911
25879ccd446eSAtari911        $this->clearStatsCache();
25881d05cddcSAtari911        $this->redirect("Deleted $count occurrences of recurring event: " . $eventTitle, 'success', 'manage');
25891d05cddcSAtari911    }
25901d05cddcSAtari911
25911d05cddcSAtari911    private function editRecurringSeries() {
25921d05cddcSAtari911        global $INPUT;
25931d05cddcSAtari911
25941d05cddcSAtari911        $oldTitle = $INPUT->str('old_title');
25951d05cddcSAtari911        $oldNamespace = $INPUT->str('old_namespace');
25961d05cddcSAtari911        $newTitle = $INPUT->str('new_title');
25971d05cddcSAtari911        $startTime = $INPUT->str('start_time');
25981d05cddcSAtari911        $endTime = $INPUT->str('end_time');
25991d05cddcSAtari911        $interval = $INPUT->int('interval', 0);
26001d05cddcSAtari911        $newNamespace = $INPUT->str('new_namespace');
26011d05cddcSAtari911
26021d05cddcSAtari911        // Use old namespace if new namespace is empty (keep current)
26031d05cddcSAtari911        if (empty($newNamespace) && !isset($_POST['new_namespace'])) {
26041d05cddcSAtari911            $newNamespace = $oldNamespace;
26051d05cddcSAtari911        }
26061d05cddcSAtari911
26071d05cddcSAtari911        // Determine old calendar directory
26081d05cddcSAtari911        if ($oldNamespace === '') {
26091d05cddcSAtari911            $oldDataDir = DOKU_INC . 'data/meta/calendar';
26101d05cddcSAtari911        } else {
26111d05cddcSAtari911            $oldDataDir = DOKU_INC . 'data/meta/' . $oldNamespace . '/calendar';
26121d05cddcSAtari911        }
26131d05cddcSAtari911
26141d05cddcSAtari911        $count = 0;
26151d05cddcSAtari911        $eventsToMove = [];
26169ccd446eSAtari911        $firstEventDate = null;
26171d05cddcSAtari911
26181d05cddcSAtari911        if (is_dir($oldDataDir)) {
26191d05cddcSAtari911            foreach (glob($oldDataDir . '/*.json') as $file) {
26201d05cddcSAtari911                $data = json_decode(file_get_contents($file), true);
26211d05cddcSAtari911                if (!$data) continue;
26221d05cddcSAtari911
26231d05cddcSAtari911                $modified = false;
26241d05cddcSAtari911                foreach ($data as $dateKey => $events) {
26251d05cddcSAtari911                    foreach ($events as $key => $event) {
26261d05cddcSAtari911                        // Match by old title (case-insensitive)
26271d05cddcSAtari911                        if (strtolower(trim($event['title'])) === strtolower(trim($oldTitle))) {
26281d05cddcSAtari911                            // Update the title
26291d05cddcSAtari911                            $data[$dateKey][$key]['title'] = $newTitle;
26301d05cddcSAtari911
26311d05cddcSAtari911                            // Update start time if provided
26321d05cddcSAtari911                            if (!empty($startTime)) {
26339ccd446eSAtari911                                $data[$dateKey][$key]['time'] = $startTime;
26341d05cddcSAtari911                            }
26351d05cddcSAtari911
26361d05cddcSAtari911                            // Update end time if provided
26371d05cddcSAtari911                            if (!empty($endTime)) {
26389ccd446eSAtari911                                $data[$dateKey][$key]['endTime'] = $endTime;
26391d05cddcSAtari911                            }
26401d05cddcSAtari911
26411d05cddcSAtari911                            // Update namespace
26421d05cddcSAtari911                            $data[$dateKey][$key]['namespace'] = $newNamespace;
26431d05cddcSAtari911
26441d05cddcSAtari911                            // If changing interval, calculate new date
26451d05cddcSAtari911                            if ($interval > 0 && $count > 0) {
26461d05cddcSAtari911                                // Get the first event date as base
26471d05cddcSAtari911                                if (empty($firstEventDate)) {
26481d05cddcSAtari911                                    $firstEventDate = $dateKey;
26491d05cddcSAtari911                                }
26501d05cddcSAtari911
26511d05cddcSAtari911                                // Calculate new date based on interval
26521d05cddcSAtari911                                $newDate = date('Y-m-d', strtotime($firstEventDate . ' +' . ($count * $interval) . ' days'));
26531d05cddcSAtari911
26541d05cddcSAtari911                                // Store for moving
26551d05cddcSAtari911                                $eventsToMove[] = [
26561d05cddcSAtari911                                    'oldDate' => $dateKey,
26571d05cddcSAtari911                                    'newDate' => $newDate,
26581d05cddcSAtari911                                    'event' => $data[$dateKey][$key],
26591d05cddcSAtari911                                    'key' => $key
26601d05cddcSAtari911                                ];
26611d05cddcSAtari911                            }
26621d05cddcSAtari911
26631d05cddcSAtari911                            $count++;
26641d05cddcSAtari911                            $modified = true;
26651d05cddcSAtari911                        }
26661d05cddcSAtari911                    }
26671d05cddcSAtari911                }
26681d05cddcSAtari911
26691d05cddcSAtari911                if ($modified) {
26701d05cddcSAtari911                    file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT));
26711d05cddcSAtari911                }
26721d05cddcSAtari911            }
26731d05cddcSAtari911
26741d05cddcSAtari911            // Handle interval changes by moving events to new dates
26751d05cddcSAtari911            if (!empty($eventsToMove)) {
26761d05cddcSAtari911                // Remove from old dates first
26771d05cddcSAtari911                foreach (glob($oldDataDir . '/*.json') as $file) {
26781d05cddcSAtari911                    $data = json_decode(file_get_contents($file), true);
26791d05cddcSAtari911                    if (!$data) continue;
26801d05cddcSAtari911
26811d05cddcSAtari911                    $modified = false;
26821d05cddcSAtari911                    foreach ($eventsToMove as $moveData) {
26831d05cddcSAtari911                        $oldMonth = substr($moveData['oldDate'], 0, 7);
26841d05cddcSAtari911                        $fileMonth = basename($file, '.json');
26851d05cddcSAtari911
26861d05cddcSAtari911                        if ($oldMonth === $fileMonth && isset($data[$moveData['oldDate']])) {
26871d05cddcSAtari911                            foreach ($data[$moveData['oldDate']] as $k => $evt) {
26881d05cddcSAtari911                                if ($evt['id'] === $moveData['event']['id']) {
26891d05cddcSAtari911                                    unset($data[$moveData['oldDate']][$k]);
26901d05cddcSAtari911                                    $data[$moveData['oldDate']] = array_values($data[$moveData['oldDate']]);
26911d05cddcSAtari911                                    $modified = true;
26921d05cddcSAtari911                                }
26931d05cddcSAtari911                            }
26941d05cddcSAtari911                        }
26951d05cddcSAtari911                    }
26961d05cddcSAtari911
26971d05cddcSAtari911                    if ($modified) {
26981d05cddcSAtari911                        file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT));
26991d05cddcSAtari911                    }
27001d05cddcSAtari911                }
27011d05cddcSAtari911
27021d05cddcSAtari911                // Add to new dates
27031d05cddcSAtari911                foreach ($eventsToMove as $moveData) {
27041d05cddcSAtari911                    $newMonth = substr($moveData['newDate'], 0, 7);
27051d05cddcSAtari911                    $targetDir = ($newNamespace === '') ? DOKU_INC . 'data/meta/calendar' : DOKU_INC . 'data/meta/' . $newNamespace . '/calendar';
27061d05cddcSAtari911
27071d05cddcSAtari911                    if (!is_dir($targetDir)) {
27081d05cddcSAtari911                        mkdir($targetDir, 0755, true);
27091d05cddcSAtari911                    }
27101d05cddcSAtari911
27111d05cddcSAtari911                    $targetFile = $targetDir . '/' . $newMonth . '.json';
27121d05cddcSAtari911                    $targetData = file_exists($targetFile) ? json_decode(file_get_contents($targetFile), true) : [];
27131d05cddcSAtari911
27141d05cddcSAtari911                    if (!isset($targetData[$moveData['newDate']])) {
27151d05cddcSAtari911                        $targetData[$moveData['newDate']] = [];
27161d05cddcSAtari911                    }
27171d05cddcSAtari911
27181d05cddcSAtari911                    $targetData[$moveData['newDate']][] = $moveData['event'];
27191d05cddcSAtari911                    file_put_contents($targetFile, json_encode($targetData, JSON_PRETTY_PRINT));
27201d05cddcSAtari911                }
27211d05cddcSAtari911            }
27221d05cddcSAtari911
27231d05cddcSAtari911            // Handle namespace change without interval change
27241d05cddcSAtari911            if ($newNamespace !== $oldNamespace && empty($eventsToMove)) {
27251d05cddcSAtari911                foreach (glob($oldDataDir . '/*.json') as $file) {
27261d05cddcSAtari911                    $data = json_decode(file_get_contents($file), true);
27271d05cddcSAtari911                    if (!$data) continue;
27281d05cddcSAtari911
27291d05cddcSAtari911                    $month = basename($file, '.json');
27301d05cddcSAtari911                    $targetDir = ($newNamespace === '') ? DOKU_INC . 'data/meta/calendar' : DOKU_INC . 'data/meta/' . $newNamespace . '/calendar';
27311d05cddcSAtari911
27321d05cddcSAtari911                    if (!is_dir($targetDir)) {
27331d05cddcSAtari911                        mkdir($targetDir, 0755, true);
27341d05cddcSAtari911                    }
27351d05cddcSAtari911
27361d05cddcSAtari911                    $targetFile = $targetDir . '/' . $month . '.json';
27371d05cddcSAtari911                    $targetData = file_exists($targetFile) ? json_decode(file_get_contents($targetFile), true) : [];
27381d05cddcSAtari911
27391d05cddcSAtari911                    $modified = false;
27401d05cddcSAtari911                    foreach ($data as $dateKey => $events) {
27411d05cddcSAtari911                        foreach ($events as $k => $event) {
27421d05cddcSAtari911                            if (isset($event['namespace']) && $event['namespace'] === $newNamespace &&
27431d05cddcSAtari911                                strtolower(trim($event['title'])) === strtolower(trim($newTitle))) {
27441d05cddcSAtari911                                // Move this event
27451d05cddcSAtari911                                if (!isset($targetData[$dateKey])) {
27461d05cddcSAtari911                                    $targetData[$dateKey] = [];
27471d05cddcSAtari911                                }
27481d05cddcSAtari911                                $targetData[$dateKey][] = $event;
27491d05cddcSAtari911                                unset($data[$dateKey][$k]);
27501d05cddcSAtari911                                $data[$dateKey] = array_values($data[$dateKey]);
27511d05cddcSAtari911                                $modified = true;
27521d05cddcSAtari911                            }
27531d05cddcSAtari911                        }
27541d05cddcSAtari911                    }
27551d05cddcSAtari911
27561d05cddcSAtari911                    if ($modified) {
27571d05cddcSAtari911                        file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT));
27581d05cddcSAtari911                        file_put_contents($targetFile, json_encode($targetData, JSON_PRETTY_PRINT));
27591d05cddcSAtari911                    }
27601d05cddcSAtari911                }
27611d05cddcSAtari911            }
27621d05cddcSAtari911        }
27631d05cddcSAtari911
27641d05cddcSAtari911        $changes = [];
27651d05cddcSAtari911        if ($oldTitle !== $newTitle) $changes[] = "title";
27661d05cddcSAtari911        if (!empty($startTime) || !empty($endTime)) $changes[] = "time";
27671d05cddcSAtari911        if ($interval > 0) $changes[] = "interval";
27681d05cddcSAtari911        if ($newNamespace !== $oldNamespace) $changes[] = "namespace";
27691d05cddcSAtari911
27701d05cddcSAtari911        $changeStr = !empty($changes) ? " (" . implode(", ", $changes) . ")" : "";
27719ccd446eSAtari911        $this->clearStatsCache();
27721d05cddcSAtari911        $this->redirect("Updated $count occurrences of recurring event$changeStr", 'success', 'manage');
27731d05cddcSAtari911    }
27741d05cddcSAtari911
27751d05cddcSAtari911    private function moveEvents() {
27761d05cddcSAtari911        global $INPUT;
27771d05cddcSAtari911
27781d05cddcSAtari911        $events = $INPUT->arr('events');
27791d05cddcSAtari911        $targetNamespace = $INPUT->str('target_namespace');
27801d05cddcSAtari911
27811d05cddcSAtari911        if (empty($events)) {
27821d05cddcSAtari911            $this->redirect('No events selected', 'error', 'manage');
27831d05cddcSAtari911        }
27841d05cddcSAtari911
27851d05cddcSAtari911        $moved = 0;
27861d05cddcSAtari911
27871d05cddcSAtari911        foreach ($events as $eventData) {
27881d05cddcSAtari911            list($id, $namespace, $date, $month) = explode('|', $eventData);
27891d05cddcSAtari911
27901d05cddcSAtari911            // Determine old file path
27911d05cddcSAtari911            if ($namespace === '') {
27921d05cddcSAtari911                $oldFile = DOKU_INC . 'data/meta/calendar/' . $month . '.json';
27931d05cddcSAtari911            } else {
27941d05cddcSAtari911                $oldFile = DOKU_INC . 'data/meta/' . $namespace . '/calendar/' . $month . '.json';
27951d05cddcSAtari911            }
27961d05cddcSAtari911
27971d05cddcSAtari911            if (!file_exists($oldFile)) continue;
27981d05cddcSAtari911
27991d05cddcSAtari911            $oldData = json_decode(file_get_contents($oldFile), true);
28001d05cddcSAtari911            if (!$oldData) continue;
28011d05cddcSAtari911
28021d05cddcSAtari911            // Find and remove event from old file
28031d05cddcSAtari911            $event = null;
28049ccd446eSAtari911            if (isset($oldData[$date])) {
28051d05cddcSAtari911                foreach ($oldData[$date] as $key => $evt) {
28061d05cddcSAtari911                    if ($evt['id'] === $id) {
28071d05cddcSAtari911                        $event = $evt;
28081d05cddcSAtari911                        unset($oldData[$date][$key]);
28091d05cddcSAtari911                        $oldData[$date] = array_values($oldData[$date]);
28101d05cddcSAtari911                        break;
28111d05cddcSAtari911                    }
28121d05cddcSAtari911                }
28131d05cddcSAtari911
28149ccd446eSAtari911                // Remove empty date arrays
28159ccd446eSAtari911                if (empty($oldData[$date])) {
28169ccd446eSAtari911                    unset($oldData[$date]);
28179ccd446eSAtari911                }
28189ccd446eSAtari911            }
28199ccd446eSAtari911
28201d05cddcSAtari911            if (!$event) continue;
28211d05cddcSAtari911
28221d05cddcSAtari911            // Save old file
28231d05cddcSAtari911            file_put_contents($oldFile, json_encode($oldData, JSON_PRETTY_PRINT));
28241d05cddcSAtari911
28251d05cddcSAtari911            // Update event namespace
28261d05cddcSAtari911            $event['namespace'] = $targetNamespace;
28271d05cddcSAtari911
28281d05cddcSAtari911            // Determine new file path
28291d05cddcSAtari911            if ($targetNamespace === '') {
28301d05cddcSAtari911                $newFile = DOKU_INC . 'data/meta/calendar/' . $month . '.json';
28311d05cddcSAtari911                $newDir = dirname($newFile);
28321d05cddcSAtari911            } else {
28331d05cddcSAtari911                $newFile = DOKU_INC . 'data/meta/' . $targetNamespace . '/calendar/' . $month . '.json';
28341d05cddcSAtari911                $newDir = dirname($newFile);
28351d05cddcSAtari911            }
28361d05cddcSAtari911
28371d05cddcSAtari911            if (!is_dir($newDir)) {
28381d05cddcSAtari911                mkdir($newDir, 0755, true);
28391d05cddcSAtari911            }
28401d05cddcSAtari911
28411d05cddcSAtari911            $newData = [];
28421d05cddcSAtari911            if (file_exists($newFile)) {
28431d05cddcSAtari911                $newData = json_decode(file_get_contents($newFile), true) ?: [];
28441d05cddcSAtari911            }
28451d05cddcSAtari911
28461d05cddcSAtari911            if (!isset($newData[$date])) {
28471d05cddcSAtari911                $newData[$date] = [];
28481d05cddcSAtari911            }
28491d05cddcSAtari911            $newData[$date][] = $event;
28501d05cddcSAtari911
28511d05cddcSAtari911            file_put_contents($newFile, json_encode($newData, JSON_PRETTY_PRINT));
28521d05cddcSAtari911            $moved++;
28531d05cddcSAtari911        }
28541d05cddcSAtari911
28551d05cddcSAtari911        $displayTarget = $targetNamespace ?: '(default)';
28569ccd446eSAtari911        $this->clearStatsCache();
28571d05cddcSAtari911        $this->redirect("Moved $moved event(s) to namespace: " . $displayTarget, 'success', 'manage');
28581d05cddcSAtari911    }
28591d05cddcSAtari911
28601d05cddcSAtari911    private function moveSingleEvent() {
28611d05cddcSAtari911        global $INPUT;
28621d05cddcSAtari911
28631d05cddcSAtari911        $eventData = $INPUT->str('event');
28641d05cddcSAtari911        $targetNamespace = $INPUT->str('target_namespace');
28651d05cddcSAtari911
28661d05cddcSAtari911        list($id, $namespace, $date, $month) = explode('|', $eventData);
28671d05cddcSAtari911
28681d05cddcSAtari911        // Determine old file path
28691d05cddcSAtari911        if ($namespace === '') {
28701d05cddcSAtari911            $oldFile = DOKU_INC . 'data/meta/calendar/' . $month . '.json';
28711d05cddcSAtari911        } else {
28721d05cddcSAtari911            $oldFile = DOKU_INC . 'data/meta/' . $namespace . '/calendar/' . $month . '.json';
28731d05cddcSAtari911        }
28741d05cddcSAtari911
28751d05cddcSAtari911        if (!file_exists($oldFile)) {
28761d05cddcSAtari911            $this->redirect('Event file not found', 'error', 'manage');
28771d05cddcSAtari911        }
28781d05cddcSAtari911
28791d05cddcSAtari911        $oldData = json_decode(file_get_contents($oldFile), true);
28801d05cddcSAtari911        if (!$oldData) {
28811d05cddcSAtari911            $this->redirect('Could not read event file', 'error', 'manage');
28821d05cddcSAtari911        }
28831d05cddcSAtari911
28841d05cddcSAtari911        // Find and remove event from old file
28851d05cddcSAtari911        $event = null;
28869ccd446eSAtari911        if (isset($oldData[$date])) {
28871d05cddcSAtari911            foreach ($oldData[$date] as $key => $evt) {
28881d05cddcSAtari911                if ($evt['id'] === $id) {
28891d05cddcSAtari911                    $event = $evt;
28901d05cddcSAtari911                    unset($oldData[$date][$key]);
28911d05cddcSAtari911                    $oldData[$date] = array_values($oldData[$date]);
28921d05cddcSAtari911                    break;
28931d05cddcSAtari911                }
28941d05cddcSAtari911            }
28951d05cddcSAtari911
28969ccd446eSAtari911            // Remove empty date arrays
28979ccd446eSAtari911            if (empty($oldData[$date])) {
28989ccd446eSAtari911                unset($oldData[$date]);
28999ccd446eSAtari911            }
29009ccd446eSAtari911        }
29019ccd446eSAtari911
29021d05cddcSAtari911        if (!$event) {
29031d05cddcSAtari911            $this->redirect('Event not found', 'error', 'manage');
29041d05cddcSAtari911        }
29051d05cddcSAtari911
29069ccd446eSAtari911        // Save old file (or delete if empty)
29079ccd446eSAtari911        if (empty($oldData)) {
29089ccd446eSAtari911            unlink($oldFile);
29099ccd446eSAtari911        } else {
29101d05cddcSAtari911            file_put_contents($oldFile, json_encode($oldData, JSON_PRETTY_PRINT));
29119ccd446eSAtari911        }
29121d05cddcSAtari911
29131d05cddcSAtari911        // Update event namespace
29141d05cddcSAtari911        $event['namespace'] = $targetNamespace;
29151d05cddcSAtari911
29161d05cddcSAtari911        // Determine new file path
29171d05cddcSAtari911        if ($targetNamespace === '') {
29181d05cddcSAtari911            $newFile = DOKU_INC . 'data/meta/calendar/' . $month . '.json';
29191d05cddcSAtari911            $newDir = dirname($newFile);
29201d05cddcSAtari911        } else {
29211d05cddcSAtari911            $newFile = DOKU_INC . 'data/meta/' . $targetNamespace . '/calendar/' . $month . '.json';
29221d05cddcSAtari911            $newDir = dirname($newFile);
29231d05cddcSAtari911        }
29241d05cddcSAtari911
29251d05cddcSAtari911        if (!is_dir($newDir)) {
29261d05cddcSAtari911            mkdir($newDir, 0755, true);
29271d05cddcSAtari911        }
29281d05cddcSAtari911
29291d05cddcSAtari911        $newData = [];
29301d05cddcSAtari911        if (file_exists($newFile)) {
29311d05cddcSAtari911            $newData = json_decode(file_get_contents($newFile), true) ?: [];
29321d05cddcSAtari911        }
29331d05cddcSAtari911
29341d05cddcSAtari911        if (!isset($newData[$date])) {
29351d05cddcSAtari911            $newData[$date] = [];
29361d05cddcSAtari911        }
29371d05cddcSAtari911        $newData[$date][] = $event;
29381d05cddcSAtari911
29391d05cddcSAtari911        file_put_contents($newFile, json_encode($newData, JSON_PRETTY_PRINT));
29401d05cddcSAtari911
29411d05cddcSAtari911        $displayTarget = $targetNamespace ?: '(default)';
29429ccd446eSAtari911        $this->clearStatsCache();
29431d05cddcSAtari911        $this->redirect('Moved "' . $event['title'] . '" to ' . $displayTarget, 'success', 'manage');
29441d05cddcSAtari911    }
29451d05cddcSAtari911
29461d05cddcSAtari911    private function createNamespace() {
29471d05cddcSAtari911        global $INPUT;
29481d05cddcSAtari911
29491d05cddcSAtari911        $namespaceName = $INPUT->str('namespace_name');
29501d05cddcSAtari911
29511d05cddcSAtari911        // Validate namespace name
29521d05cddcSAtari911        if (empty($namespaceName)) {
29531d05cddcSAtari911            $this->redirect('Namespace name cannot be empty', 'error', 'manage');
29541d05cddcSAtari911        }
29551d05cddcSAtari911
29561d05cddcSAtari911        if (!preg_match('/^[a-zA-Z0-9_:-]+$/', $namespaceName)) {
29571d05cddcSAtari911            $this->redirect('Invalid namespace name. Use only letters, numbers, underscore, hyphen, and colon.', 'error', 'manage');
29581d05cddcSAtari911        }
29591d05cddcSAtari911
29601d05cddcSAtari911        // Convert namespace to directory path
29611d05cddcSAtari911        $namespacePath = str_replace(':', '/', $namespaceName);
29621d05cddcSAtari911        $calendarDir = DOKU_INC . 'data/meta/' . $namespacePath . '/calendar';
29631d05cddcSAtari911
29641d05cddcSAtari911        // Check if already exists
29651d05cddcSAtari911        if (is_dir($calendarDir)) {
29661d05cddcSAtari911            // Check if it has any JSON files
29671d05cddcSAtari911            $hasFiles = !empty(glob($calendarDir . '/*.json'));
29681d05cddcSAtari911            if ($hasFiles) {
29691d05cddcSAtari911                $this->redirect("Namespace '$namespaceName' already exists with events", 'info', 'manage');
29701d05cddcSAtari911            }
29711d05cddcSAtari911            // If directory exists but empty, continue to create placeholder
29721d05cddcSAtari911        }
29731d05cddcSAtari911
29741d05cddcSAtari911        // Create the directory
29751d05cddcSAtari911        if (!is_dir($calendarDir)) {
29761d05cddcSAtari911            if (!mkdir($calendarDir, 0755, true)) {
29771d05cddcSAtari911                $this->redirect("Failed to create namespace directory", 'error', 'manage');
29781d05cddcSAtari911            }
29791d05cddcSAtari911        }
29801d05cddcSAtari911
29811d05cddcSAtari911        // Create a placeholder JSON file with an empty structure for current month
29821d05cddcSAtari911        // This ensures the namespace appears in the list immediately
29831d05cddcSAtari911        $currentMonth = date('Y-m');
29841d05cddcSAtari911        $placeholderFile = $calendarDir . '/' . $currentMonth . '.json';
29851d05cddcSAtari911
29861d05cddcSAtari911        if (!file_exists($placeholderFile)) {
29871d05cddcSAtari911            file_put_contents($placeholderFile, json_encode([], JSON_PRETTY_PRINT));
29881d05cddcSAtari911        }
29891d05cddcSAtari911
29901d05cddcSAtari911        $this->redirect("Created namespace: $namespaceName", 'success', 'manage');
29911d05cddcSAtari911    }
29921d05cddcSAtari911
29931d05cddcSAtari911    private function deleteNamespace() {
29941d05cddcSAtari911        global $INPUT;
29951d05cddcSAtari911
29961d05cddcSAtari911        $namespace = $INPUT->str('namespace');
29971d05cddcSAtari911
29981d05cddcSAtari911        // Convert namespace to directory path (e.g., "work:projects" → "work/projects")
29991d05cddcSAtari911        $namespacePath = str_replace(':', '/', $namespace);
30001d05cddcSAtari911
30011d05cddcSAtari911        // Determine calendar directory
30021d05cddcSAtari911        if ($namespace === '') {
30031d05cddcSAtari911            $calendarDir = DOKU_INC . 'data/meta/calendar';
30041d05cddcSAtari911            $namespaceDir = null; // Don't delete root
30051d05cddcSAtari911        } else {
30061d05cddcSAtari911            $calendarDir = DOKU_INC . 'data/meta/' . $namespacePath . '/calendar';
30071d05cddcSAtari911            $namespaceDir = DOKU_INC . 'data/meta/' . $namespacePath;
30081d05cddcSAtari911        }
30091d05cddcSAtari911
30101d05cddcSAtari911        // Check if directory exists
30111d05cddcSAtari911        if (!is_dir($calendarDir)) {
30121d05cddcSAtari911            // Maybe it was never created or already deleted
30131d05cddcSAtari911            $this->redirect("Namespace directory not found: $calendarDir", 'error', 'manage');
30141d05cddcSAtari911            return;
30151d05cddcSAtari911        }
30161d05cddcSAtari911
30171d05cddcSAtari911        $filesDeleted = 0;
30181d05cddcSAtari911        $eventsDeleted = 0;
30191d05cddcSAtari911
30201d05cddcSAtari911        // Delete all calendar JSON files (including empty ones)
30211d05cddcSAtari911        foreach (glob($calendarDir . '/*.json') as $file) {
30221d05cddcSAtari911            $data = json_decode(file_get_contents($file), true);
30231d05cddcSAtari911            if ($data) {
30241d05cddcSAtari911                foreach ($data as $events) {
30251d05cddcSAtari911                    $eventsDeleted += count($events);
30261d05cddcSAtari911                }
30271d05cddcSAtari911            }
30281d05cddcSAtari911            unlink($file);
30291d05cddcSAtari911            $filesDeleted++;
30301d05cddcSAtari911        }
30311d05cddcSAtari911
30321d05cddcSAtari911        // Delete any other files in calendar directory
30331d05cddcSAtari911        foreach (glob($calendarDir . '/*') as $file) {
30341d05cddcSAtari911            if (is_file($file)) {
30351d05cddcSAtari911                unlink($file);
30361d05cddcSAtari911            }
30371d05cddcSAtari911        }
30381d05cddcSAtari911
30391d05cddcSAtari911        // Remove the calendar directory
30401d05cddcSAtari911        if ($namespace !== '') {
30411d05cddcSAtari911            @rmdir($calendarDir);
30421d05cddcSAtari911
30431d05cddcSAtari911            // Try to remove parent directories if they're empty
30441d05cddcSAtari911            // This handles nested namespaces like work:projects:alpha
30451d05cddcSAtari911            $currentDir = dirname($calendarDir);
30461d05cddcSAtari911            $metaDir = DOKU_INC . 'data/meta';
30471d05cddcSAtari911
30481d05cddcSAtari911            while ($currentDir !== $metaDir && $currentDir !== dirname($metaDir)) {
30491d05cddcSAtari911                if (is_dir($currentDir)) {
30501d05cddcSAtari911                    // Check if directory is empty
30511d05cddcSAtari911                    $contents = scandir($currentDir);
30521d05cddcSAtari911                    $isEmpty = count($contents) === 2; // Only . and ..
30531d05cddcSAtari911
30541d05cddcSAtari911                    if ($isEmpty) {
30551d05cddcSAtari911                        @rmdir($currentDir);
30561d05cddcSAtari911                        $currentDir = dirname($currentDir);
30571d05cddcSAtari911                    } else {
30581d05cddcSAtari911                        break; // Directory not empty, stop
30591d05cddcSAtari911                    }
30601d05cddcSAtari911                } else {
30611d05cddcSAtari911                    break;
30621d05cddcSAtari911                }
30631d05cddcSAtari911            }
30641d05cddcSAtari911        }
30651d05cddcSAtari911
30661d05cddcSAtari911        $displayName = $namespace ?: '(default)';
30679ccd446eSAtari911        $this->clearStatsCache();
30681d05cddcSAtari911        $this->redirect("Deleted namespace '$displayName': $eventsDeleted events in $filesDeleted files", 'success', 'manage');
30691d05cddcSAtari911    }
30701d05cddcSAtari911
30719ccd446eSAtari911    private function renameNamespace() {
30729ccd446eSAtari911        global $INPUT;
30739ccd446eSAtari911
30749ccd446eSAtari911        $oldNamespace = $INPUT->str('old_namespace');
30759ccd446eSAtari911        $newNamespace = $INPUT->str('new_namespace');
30769ccd446eSAtari911
30779ccd446eSAtari911        // Validate new namespace name
30789ccd446eSAtari911        if ($newNamespace === '') {
30799ccd446eSAtari911            $this->redirect("Cannot rename to empty namespace", 'error', 'manage');
30809ccd446eSAtari911            return;
30819ccd446eSAtari911        }
30829ccd446eSAtari911
30839ccd446eSAtari911        // Convert namespaces to directory paths
30849ccd446eSAtari911        $oldPath = str_replace(':', '/', $oldNamespace);
30859ccd446eSAtari911        $newPath = str_replace(':', '/', $newNamespace);
30869ccd446eSAtari911
30879ccd446eSAtari911        // Determine source and destination directories
30889ccd446eSAtari911        if ($oldNamespace === '') {
30899ccd446eSAtari911            $sourceDir = DOKU_INC . 'data/meta/calendar';
30909ccd446eSAtari911        } else {
30919ccd446eSAtari911            $sourceDir = DOKU_INC . 'data/meta/' . $oldPath . '/calendar';
30929ccd446eSAtari911        }
30939ccd446eSAtari911
30949ccd446eSAtari911        if ($newNamespace === '') {
30959ccd446eSAtari911            $targetDir = DOKU_INC . 'data/meta/calendar';
30969ccd446eSAtari911        } else {
30979ccd446eSAtari911            $targetDir = DOKU_INC . 'data/meta/' . $newPath . '/calendar';
30989ccd446eSAtari911        }
30999ccd446eSAtari911
31009ccd446eSAtari911        // Check if source exists
31019ccd446eSAtari911        if (!is_dir($sourceDir)) {
31029ccd446eSAtari911            $this->redirect("Source namespace not found: $oldNamespace", 'error', 'manage');
31039ccd446eSAtari911            return;
31049ccd446eSAtari911        }
31059ccd446eSAtari911
31069ccd446eSAtari911        // Check if target already exists
31079ccd446eSAtari911        if (is_dir($targetDir)) {
31089ccd446eSAtari911            $this->redirect("Target namespace already exists: $newNamespace", 'error', 'manage');
31099ccd446eSAtari911            return;
31109ccd446eSAtari911        }
31119ccd446eSAtari911
31129ccd446eSAtari911        // Create target directory
31139ccd446eSAtari911        if (!file_exists(dirname($targetDir))) {
31149ccd446eSAtari911            mkdir(dirname($targetDir), 0755, true);
31159ccd446eSAtari911        }
31169ccd446eSAtari911
31179ccd446eSAtari911        // Rename directory
31189ccd446eSAtari911        if (!rename($sourceDir, $targetDir)) {
31199ccd446eSAtari911            $this->redirect("Failed to rename namespace", 'error', 'manage');
31209ccd446eSAtari911            return;
31219ccd446eSAtari911        }
31229ccd446eSAtari911
31239ccd446eSAtari911        // Update event namespace field in all JSON files
31249ccd446eSAtari911        $eventsUpdated = 0;
31259ccd446eSAtari911        foreach (glob($targetDir . '/*.json') as $file) {
31269ccd446eSAtari911            $data = json_decode(file_get_contents($file), true);
31279ccd446eSAtari911            if ($data) {
31289ccd446eSAtari911                foreach ($data as $date => &$events) {
31299ccd446eSAtari911                    foreach ($events as &$event) {
31309ccd446eSAtari911                        if (isset($event['namespace']) && $event['namespace'] === $oldNamespace) {
31319ccd446eSAtari911                            $event['namespace'] = $newNamespace;
31329ccd446eSAtari911                            $eventsUpdated++;
31339ccd446eSAtari911                        }
31349ccd446eSAtari911                    }
31359ccd446eSAtari911                }
31369ccd446eSAtari911                file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT));
31379ccd446eSAtari911            }
31389ccd446eSAtari911        }
31399ccd446eSAtari911
31409ccd446eSAtari911        // Clean up old directory structure if empty
31419ccd446eSAtari911        if ($oldNamespace !== '') {
31429ccd446eSAtari911            $currentDir = dirname($sourceDir);
31439ccd446eSAtari911            $metaDir = DOKU_INC . 'data/meta';
31449ccd446eSAtari911
31459ccd446eSAtari911            while ($currentDir !== $metaDir && $currentDir !== dirname($metaDir)) {
31469ccd446eSAtari911                if (is_dir($currentDir)) {
31479ccd446eSAtari911                    $contents = scandir($currentDir);
31489ccd446eSAtari911                    $isEmpty = count($contents) === 2; // Only . and ..
31499ccd446eSAtari911
31509ccd446eSAtari911                    if ($isEmpty) {
31519ccd446eSAtari911                        @rmdir($currentDir);
31529ccd446eSAtari911                        $currentDir = dirname($currentDir);
31539ccd446eSAtari911                    } else {
31549ccd446eSAtari911                        break;
31559ccd446eSAtari911                    }
31569ccd446eSAtari911                } else {
31579ccd446eSAtari911                    break;
31589ccd446eSAtari911                }
31599ccd446eSAtari911            }
31609ccd446eSAtari911        }
31619ccd446eSAtari911
31629ccd446eSAtari911        $this->clearStatsCache();
31639ccd446eSAtari911        $this->redirect("Renamed namespace from '$oldNamespace' to '$newNamespace' ($eventsUpdated events updated)", 'success', 'manage');
31649ccd446eSAtari911    }
31659ccd446eSAtari911
31661d05cddcSAtari911    private function deleteSelectedEvents() {
31671d05cddcSAtari911        global $INPUT;
31681d05cddcSAtari911
31691d05cddcSAtari911        $events = $INPUT->arr('events');
31701d05cddcSAtari911
31711d05cddcSAtari911        if (empty($events)) {
31721d05cddcSAtari911            $this->redirect('No events selected', 'error', 'manage');
31731d05cddcSAtari911        }
31741d05cddcSAtari911
31751d05cddcSAtari911        $deletedCount = 0;
31761d05cddcSAtari911
31771d05cddcSAtari911        foreach ($events as $eventData) {
31781d05cddcSAtari911            list($id, $namespace, $date, $month) = explode('|', $eventData);
31791d05cddcSAtari911
31801d05cddcSAtari911            // Determine file path
31811d05cddcSAtari911            if ($namespace === '') {
31821d05cddcSAtari911                $file = DOKU_INC . 'data/meta/calendar/' . $month . '.json';
31831d05cddcSAtari911            } else {
31841d05cddcSAtari911                $file = DOKU_INC . 'data/meta/' . $namespace . '/calendar/' . $month . '.json';
31851d05cddcSAtari911            }
31861d05cddcSAtari911
31871d05cddcSAtari911            if (!file_exists($file)) continue;
31881d05cddcSAtari911
31891d05cddcSAtari911            $data = json_decode(file_get_contents($file), true);
31901d05cddcSAtari911            if (!$data) continue;
31911d05cddcSAtari911
31921d05cddcSAtari911            // Find and remove event
31931d05cddcSAtari911            if (isset($data[$date])) {
31941d05cddcSAtari911                foreach ($data[$date] as $key => $evt) {
31951d05cddcSAtari911                    if ($evt['id'] === $id) {
31961d05cddcSAtari911                        unset($data[$date][$key]);
31971d05cddcSAtari911                        $data[$date] = array_values($data[$date]);
31981d05cddcSAtari911                        $deletedCount++;
31991d05cddcSAtari911                        break;
32001d05cddcSAtari911                    }
32011d05cddcSAtari911                }
32021d05cddcSAtari911
32031d05cddcSAtari911                // Remove empty date arrays
32041d05cddcSAtari911                if (empty($data[$date])) {
32051d05cddcSAtari911                    unset($data[$date]);
32061d05cddcSAtari911                }
32071d05cddcSAtari911
32081d05cddcSAtari911                // Save file
32091d05cddcSAtari911                file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT));
32101d05cddcSAtari911            }
32111d05cddcSAtari911        }
32121d05cddcSAtari911
32139ccd446eSAtari911        $this->clearStatsCache();
32141d05cddcSAtari911        $this->redirect("Deleted $deletedCount event(s)", 'success', 'manage');
32151d05cddcSAtari911    }
32161d05cddcSAtari911
32179ccd446eSAtari911    /**
32189ccd446eSAtari911     * Clear the event statistics cache so counts refresh after mutations
32199ccd446eSAtari911     */
3220*4590242dSAtari911    private function saveImportantNamespaces() {
3221*4590242dSAtari911        global $INPUT;
3222*4590242dSAtari911
3223*4590242dSAtari911        $configFile = DOKU_PLUGIN . 'calendar/sync_config.php';
3224*4590242dSAtari911        $config = [];
3225*4590242dSAtari911        if (file_exists($configFile)) {
3226*4590242dSAtari911            $config = include $configFile;
3227*4590242dSAtari911        }
3228*4590242dSAtari911
3229*4590242dSAtari911        $config['important_namespaces'] = $INPUT->str('important_namespaces', 'important');
3230*4590242dSAtari911
3231*4590242dSAtari911        $content = "<?php\nreturn " . var_export($config, true) . ";\n";
3232*4590242dSAtari911        if (file_put_contents($configFile, $content)) {
3233*4590242dSAtari911            $this->redirect('Important namespaces saved', 'success', 'manage');
3234*4590242dSAtari911        } else {
3235*4590242dSAtari911            $this->redirect('Error: Could not save configuration', 'error', 'manage');
3236*4590242dSAtari911        }
3237*4590242dSAtari911    }
3238*4590242dSAtari911
32399ccd446eSAtari911    private function clearStatsCache() {
32409ccd446eSAtari911        $cacheFile = DOKU_PLUGIN . 'calendar/.event_stats_cache';
32419ccd446eSAtari911        if (file_exists($cacheFile)) {
32429ccd446eSAtari911            unlink($cacheFile);
32439ccd446eSAtari911        }
32449ccd446eSAtari911    }
32459ccd446eSAtari911
32461d05cddcSAtari911    private function getCronStatus() {
32471d05cddcSAtari911        // Try to read root's crontab first, then current user
32481d05cddcSAtari911        $output = [];
32491d05cddcSAtari911        exec('sudo crontab -l 2>/dev/null', $output);
32501d05cddcSAtari911
32511d05cddcSAtari911        // If sudo doesn't work, try current user
32521d05cddcSAtari911        if (empty($output)) {
32531d05cddcSAtari911            exec('crontab -l 2>/dev/null', $output);
32541d05cddcSAtari911        }
32551d05cddcSAtari911
32561d05cddcSAtari911        // Also check system crontab files
32571d05cddcSAtari911        if (empty($output)) {
32581d05cddcSAtari911            $cronFiles = [
32591d05cddcSAtari911                '/etc/crontab',
32601d05cddcSAtari911                '/etc/cron.d/calendar',
32611d05cddcSAtari911                '/var/spool/cron/root',
32621d05cddcSAtari911                '/var/spool/cron/crontabs/root'
32631d05cddcSAtari911            ];
32641d05cddcSAtari911
32651d05cddcSAtari911            foreach ($cronFiles as $file) {
32661d05cddcSAtari911                if (file_exists($file) && is_readable($file)) {
32671d05cddcSAtari911                    $content = file_get_contents($file);
32681d05cddcSAtari911                    $output = explode("\n", $content);
32691d05cddcSAtari911                    break;
32701d05cddcSAtari911                }
32711d05cddcSAtari911            }
32721d05cddcSAtari911        }
32731d05cddcSAtari911
32741d05cddcSAtari911        // Look for sync_outlook.php in the cron entries
32751d05cddcSAtari911        foreach ($output as $line) {
32761d05cddcSAtari911            $line = trim($line);
32771d05cddcSAtari911
32781d05cddcSAtari911            // Skip empty lines and comments
32791d05cddcSAtari911            if (empty($line) || $line[0] === '#') continue;
32801d05cddcSAtari911
32811d05cddcSAtari911            // Check if line contains sync_outlook.php
32821d05cddcSAtari911            if (strpos($line, 'sync_outlook.php') !== false) {
32831d05cddcSAtari911                // Parse cron expression
32841d05cddcSAtari911                // Format: minute hour day month weekday [user] command
32851d05cddcSAtari911                $parts = preg_split('/\s+/', $line, 7);
32861d05cddcSAtari911
32871d05cddcSAtari911                if (count($parts) >= 5) {
32881d05cddcSAtari911                    // Determine if this has a user field (system crontab format)
32891d05cddcSAtari911                    $hasUser = (count($parts) >= 6 && !preg_match('/^[\/\*]/', $parts[5]));
32901d05cddcSAtari911                    $offset = $hasUser ? 1 : 0;
32911d05cddcSAtari911
32921d05cddcSAtari911                    $frequency = $this->parseCronExpression($parts[0], $parts[1], $parts[2], $parts[3], $parts[4]);
32931d05cddcSAtari911                    return [
32941d05cddcSAtari911                        'active' => true,
32951d05cddcSAtari911                        'frequency' => $frequency,
32961d05cddcSAtari911                        'expression' => implode(' ', array_slice($parts, 0, 5)),
32971d05cddcSAtari911                        'full_line' => $line
32981d05cddcSAtari911                    ];
32991d05cddcSAtari911                }
33001d05cddcSAtari911            }
33011d05cddcSAtari911        }
33021d05cddcSAtari911
33031d05cddcSAtari911        return ['active' => false, 'frequency' => '', 'expression' => '', 'full_line' => ''];
33041d05cddcSAtari911    }
33051d05cddcSAtari911
33061d05cddcSAtari911    private function parseCronExpression($minute, $hour, $day, $month, $weekday) {
33071d05cddcSAtari911        // Parse minute field
33081d05cddcSAtari911        if ($minute === '*') {
33091d05cddcSAtari911            return 'Runs every minute';
33101d05cddcSAtari911        } elseif (strpos($minute, '*/') === 0) {
33111d05cddcSAtari911            $interval = substr($minute, 2);
33121d05cddcSAtari911            if ($interval == 1) {
33131d05cddcSAtari911                return 'Runs every minute';
33141d05cddcSAtari911            } elseif ($interval == 5) {
33151d05cddcSAtari911                return 'Runs every 5 minutes';
33161d05cddcSAtari911            } elseif ($interval == 8) {
33171d05cddcSAtari911                return 'Runs every 8 minutes';
33181d05cddcSAtari911            } elseif ($interval == 10) {
33191d05cddcSAtari911                return 'Runs every 10 minutes';
33201d05cddcSAtari911            } elseif ($interval == 15) {
33211d05cddcSAtari911                return 'Runs every 15 minutes';
33221d05cddcSAtari911            } elseif ($interval == 30) {
33231d05cddcSAtari911                return 'Runs every 30 minutes';
33241d05cddcSAtari911            } else {
33251d05cddcSAtari911                return "Runs every $interval minutes";
33261d05cddcSAtari911            }
33271d05cddcSAtari911        }
33281d05cddcSAtari911
33291d05cddcSAtari911        // Parse hour field
33301d05cddcSAtari911        if ($hour === '*' && $minute !== '*') {
33311d05cddcSAtari911            return 'Runs hourly';
33321d05cddcSAtari911        } elseif (strpos($hour, '*/') === 0 && $minute !== '*') {
33331d05cddcSAtari911            $interval = substr($hour, 2);
33341d05cddcSAtari911            if ($interval == 1) {
33351d05cddcSAtari911                return 'Runs every hour';
33361d05cddcSAtari911            } else {
33371d05cddcSAtari911                return "Runs every $interval hours";
33381d05cddcSAtari911            }
33391d05cddcSAtari911        }
33401d05cddcSAtari911
33411d05cddcSAtari911        // Parse day field
33421d05cddcSAtari911        if ($day === '*' && $hour !== '*' && $minute !== '*') {
33431d05cddcSAtari911            return 'Runs daily';
33441d05cddcSAtari911        }
33451d05cddcSAtari911
33461d05cddcSAtari911        // Default
33471d05cddcSAtari911        return 'Custom schedule';
33481d05cddcSAtari911    }
33491d05cddcSAtari911
33501d05cddcSAtari911    private function runSync() {
33511d05cddcSAtari911        global $INPUT;
33521d05cddcSAtari911
33531d05cddcSAtari911        if ($INPUT->str('call') === 'ajax') {
33541d05cddcSAtari911            header('Content-Type: application/json');
33551d05cddcSAtari911
33561d05cddcSAtari911            $syncScript = DOKU_PLUGIN . 'calendar/sync_outlook.php';
33571d05cddcSAtari911            $abortFile = DOKU_PLUGIN . 'calendar/.sync_abort';
33581d05cddcSAtari911
33591d05cddcSAtari911            // Remove any existing abort flag
33601d05cddcSAtari911            if (file_exists($abortFile)) {
33611d05cddcSAtari911                @unlink($abortFile);
33621d05cddcSAtari911            }
33631d05cddcSAtari911
33641d05cddcSAtari911            if (!file_exists($syncScript)) {
33651d05cddcSAtari911                echo json_encode(['success' => false, 'message' => 'Sync script not found at: ' . $syncScript]);
33661d05cddcSAtari911                exit;
33671d05cddcSAtari911            }
33681d05cddcSAtari911
33691d05cddcSAtari911            // Change to plugin directory
33701d05cddcSAtari911            $pluginDir = DOKU_PLUGIN . 'calendar';
33711d05cddcSAtari911            $logFile = $pluginDir . '/sync.log';
33721d05cddcSAtari911
33731d05cddcSAtari911            // Ensure log file exists and is writable
33741d05cddcSAtari911            if (!file_exists($logFile)) {
33751d05cddcSAtari911                @touch($logFile);
33761d05cddcSAtari911                @chmod($logFile, 0666);
33771d05cddcSAtari911            }
33781d05cddcSAtari911
33791d05cddcSAtari911            // Try to log the execution (but don't fail if we can't)
33801d05cddcSAtari911            if (is_writable($logFile)) {
33811d05cddcSAtari911                $tz = new DateTimeZone('America/Los_Angeles');
33821d05cddcSAtari911                $now = new DateTime('now', $tz);
33831d05cddcSAtari911                $timestamp = $now->format('Y-m-d H:i:s');
33841d05cddcSAtari911                @file_put_contents($logFile, "[$timestamp] [ADMIN] Manual sync triggered via admin panel\n", FILE_APPEND);
33851d05cddcSAtari911            }
33861d05cddcSAtari911
33871d05cddcSAtari911            // Find PHP binary - try multiple methods
33881d05cddcSAtari911            $phpPath = $this->findPhpBinary();
33891d05cddcSAtari911
33901d05cddcSAtari911            // Build command
33911d05cddcSAtari911            $command = sprintf(
33921d05cddcSAtari911                'cd %s && %s %s 2>&1',
33931d05cddcSAtari911                escapeshellarg($pluginDir),
33941d05cddcSAtari911                $phpPath,
33951d05cddcSAtari911                escapeshellarg(basename($syncScript))
33961d05cddcSAtari911            );
33971d05cddcSAtari911
33981d05cddcSAtari911            // Execute and capture output
33991d05cddcSAtari911            $output = [];
34001d05cddcSAtari911            $returnCode = 0;
34011d05cddcSAtari911            exec($command, $output, $returnCode);
34021d05cddcSAtari911
34031d05cddcSAtari911            // Check if sync completed
34041d05cddcSAtari911            $lastLines = array_slice($output, -5);
34051d05cddcSAtari911            $completed = false;
34061d05cddcSAtari911            foreach ($lastLines as $line) {
34071d05cddcSAtari911                if (strpos($line, 'Sync Complete') !== false || strpos($line, 'Created:') !== false) {
34081d05cddcSAtari911                    $completed = true;
34091d05cddcSAtari911                    break;
34101d05cddcSAtari911                }
34111d05cddcSAtari911            }
34121d05cddcSAtari911
34131d05cddcSAtari911            if ($returnCode === 0 && $completed) {
34141d05cddcSAtari911                echo json_encode([
34151d05cddcSAtari911                    'success' => true,
34161d05cddcSAtari911                    'message' => 'Sync completed successfully! Check log below.'
34171d05cddcSAtari911                ]);
34181d05cddcSAtari911            } elseif ($returnCode === 0) {
34191d05cddcSAtari911                echo json_encode([
34201d05cddcSAtari911                    'success' => true,
34211d05cddcSAtari911                    'message' => 'Sync started. Check log below for progress.'
34221d05cddcSAtari911                ]);
34231d05cddcSAtari911            } else {
34241d05cddcSAtari911                // Include output for debugging
34251d05cddcSAtari911                $errorMsg = 'Sync failed with error code: ' . $returnCode;
34261d05cddcSAtari911                if (!empty($output)) {
34271d05cddcSAtari911                    $errorMsg .= ' | ' . implode(' | ', array_slice($output, -3));
34281d05cddcSAtari911                }
34291d05cddcSAtari911                echo json_encode([
34301d05cddcSAtari911                    'success' => false,
34311d05cddcSAtari911                    'message' => $errorMsg
34321d05cddcSAtari911                ]);
34331d05cddcSAtari911            }
34341d05cddcSAtari911            exit;
34351d05cddcSAtari911        }
34361d05cddcSAtari911    }
34371d05cddcSAtari911
34381d05cddcSAtari911    private function stopSync() {
34391d05cddcSAtari911        global $INPUT;
34401d05cddcSAtari911
34411d05cddcSAtari911        if ($INPUT->str('call') === 'ajax') {
34421d05cddcSAtari911            header('Content-Type: application/json');
34431d05cddcSAtari911
34441d05cddcSAtari911            $abortFile = DOKU_PLUGIN . 'calendar/.sync_abort';
34451d05cddcSAtari911
34461d05cddcSAtari911            // Create abort flag file
34471d05cddcSAtari911            if (file_put_contents($abortFile, date('Y-m-d H:i:s')) !== false) {
34481d05cddcSAtari911                echo json_encode([
34491d05cddcSAtari911                    'success' => true,
34501d05cddcSAtari911                    'message' => 'Stop signal sent to sync process'
34511d05cddcSAtari911                ]);
34521d05cddcSAtari911            } else {
34531d05cddcSAtari911                echo json_encode([
34541d05cddcSAtari911                    'success' => false,
34551d05cddcSAtari911                    'message' => 'Failed to create abort flag'
34561d05cddcSAtari911                ]);
34571d05cddcSAtari911            }
34581d05cddcSAtari911            exit;
34591d05cddcSAtari911        }
34601d05cddcSAtari911    }
34611d05cddcSAtari911
34621d05cddcSAtari911    private function uploadUpdate() {
34631d05cddcSAtari911        if (!isset($_FILES['plugin_zip']) || $_FILES['plugin_zip']['error'] !== UPLOAD_ERR_OK) {
34641d05cddcSAtari911            $this->redirect('Upload failed: ' . ($_FILES['plugin_zip']['error'] ?? 'No file uploaded'), 'error', 'update');
34651d05cddcSAtari911            return;
34661d05cddcSAtari911        }
34671d05cddcSAtari911
34681d05cddcSAtari911        $uploadedFile = $_FILES['plugin_zip']['tmp_name'];
34691d05cddcSAtari911        $pluginDir = DOKU_PLUGIN . 'calendar/';
34701d05cddcSAtari911        $backupFirst = isset($_POST['backup_first']);
34711d05cddcSAtari911
34721d05cddcSAtari911        // Check if plugin directory is writable
34731d05cddcSAtari911        if (!is_writable($pluginDir)) {
34741d05cddcSAtari911            $this->redirect('Plugin directory is not writable. Please check permissions: ' . $pluginDir, 'error', 'update');
34751d05cddcSAtari911            return;
34761d05cddcSAtari911        }
34771d05cddcSAtari911
34781d05cddcSAtari911        // Check if parent directory is writable (for backup and temp files)
34791d05cddcSAtari911        if (!is_writable(DOKU_PLUGIN)) {
34801d05cddcSAtari911            $this->redirect('Plugin parent directory is not writable. Please check permissions: ' . DOKU_PLUGIN, 'error', 'update');
34811d05cddcSAtari911            return;
34821d05cddcSAtari911        }
34831d05cddcSAtari911
34841d05cddcSAtari911        // Verify it's a ZIP file
34851d05cddcSAtari911        $finfo = finfo_open(FILEINFO_MIME_TYPE);
34861d05cddcSAtari911        $mimeType = finfo_file($finfo, $uploadedFile);
34871d05cddcSAtari911        finfo_close($finfo);
34881d05cddcSAtari911
34891d05cddcSAtari911        if ($mimeType !== 'application/zip' && $mimeType !== 'application/x-zip-compressed') {
34901d05cddcSAtari911            $this->redirect('Invalid file type. Please upload a ZIP file.', 'error', 'update');
34911d05cddcSAtari911            return;
34921d05cddcSAtari911        }
34931d05cddcSAtari911
34941d05cddcSAtari911        // Create backup if requested
34951d05cddcSAtari911        if ($backupFirst) {
34961d05cddcSAtari911            // Get current version
34971d05cddcSAtari911            $pluginInfo = $pluginDir . 'plugin.info.txt';
34981d05cddcSAtari911            $version = 'unknown';
34991d05cddcSAtari911            if (file_exists($pluginInfo)) {
35001d05cddcSAtari911                $info = confToHash($pluginInfo);
35011d05cddcSAtari911                $version = $info['version'] ?? ($info['date'] ?? 'unknown');
35021d05cddcSAtari911            }
35031d05cddcSAtari911
35041d05cddcSAtari911            $backupName = 'calendar.backup.v' . $version . '.' . date('Y-m-d_H-i-s') . '.zip';
35051d05cddcSAtari911            $backupPath = DOKU_PLUGIN . $backupName;
35061d05cddcSAtari911
35071d05cddcSAtari911            try {
35081d05cddcSAtari911                $zip = new ZipArchive();
35091d05cddcSAtari911                if ($zip->open($backupPath, ZipArchive::CREATE) === TRUE) {
35109ccd446eSAtari911                    $fileCount = $this->addDirectoryToZip($zip, $pluginDir, 'calendar/');
35111d05cddcSAtari911                    $zip->close();
35129ccd446eSAtari911
35139ccd446eSAtari911                    // Verify backup was created and has content
35149ccd446eSAtari911                    if (!file_exists($backupPath)) {
35159ccd446eSAtari911                        $this->redirect('Backup file was not created', 'error', 'update');
35169ccd446eSAtari911                        return;
35179ccd446eSAtari911                    }
35189ccd446eSAtari911
35199ccd446eSAtari911                    $backupSize = filesize($backupPath);
35209ccd446eSAtari911                    if ($backupSize < 1000) { // Backup should be at least 1KB
35219ccd446eSAtari911                        @unlink($backupPath);
35229ccd446eSAtari911                        $this->redirect('Backup file is too small (' . $backupSize . ' bytes). Only ' . $fileCount . ' files were added. Backup aborted.', 'error', 'update');
35239ccd446eSAtari911                        return;
35249ccd446eSAtari911                    }
35259ccd446eSAtari911
35269ccd446eSAtari911                    if ($fileCount < 10) { // Should have at least 10 files
35279ccd446eSAtari911                        @unlink($backupPath);
35289ccd446eSAtari911                        $this->redirect('Backup incomplete: Only ' . $fileCount . ' files were added (expected 30+). Backup aborted.', 'error', 'update');
35299ccd446eSAtari911                        return;
35309ccd446eSAtari911                    }
35311d05cddcSAtari911                } else {
35321d05cddcSAtari911                    $this->redirect('Failed to create backup ZIP file', 'error', 'update');
35331d05cddcSAtari911                    return;
35341d05cddcSAtari911                }
35351d05cddcSAtari911            } catch (Exception $e) {
35369ccd446eSAtari911                if (file_exists($backupPath)) {
35379ccd446eSAtari911                    @unlink($backupPath);
35389ccd446eSAtari911                }
35391d05cddcSAtari911                $this->redirect('Backup failed: ' . $e->getMessage(), 'error', 'update');
35401d05cddcSAtari911                return;
35411d05cddcSAtari911            }
35421d05cddcSAtari911        }
35431d05cddcSAtari911
35441d05cddcSAtari911        // Extract uploaded ZIP
35451d05cddcSAtari911        $zip = new ZipArchive();
35461d05cddcSAtari911        if ($zip->open($uploadedFile) !== TRUE) {
35471d05cddcSAtari911            $this->redirect('Failed to open ZIP file', 'error', 'update');
35481d05cddcSAtari911            return;
35491d05cddcSAtari911        }
35501d05cddcSAtari911
35511d05cddcSAtari911        // Check if ZIP contains calendar folder
35521d05cddcSAtari911        $hasCalendarFolder = false;
35531d05cddcSAtari911        for ($i = 0; $i < $zip->numFiles; $i++) {
35541d05cddcSAtari911            $filename = $zip->getNameIndex($i);
35551d05cddcSAtari911            if (strpos($filename, 'calendar/') === 0) {
35561d05cddcSAtari911                $hasCalendarFolder = true;
35571d05cddcSAtari911                break;
35581d05cddcSAtari911            }
35591d05cddcSAtari911        }
35601d05cddcSAtari911
35611d05cddcSAtari911        // Extract to temp directory first
35621d05cddcSAtari911        $tempDir = DOKU_PLUGIN . 'calendar_update_temp/';
35631d05cddcSAtari911        if (is_dir($tempDir)) {
35641d05cddcSAtari911            $this->deleteDirectory($tempDir);
35651d05cddcSAtari911        }
35661d05cddcSAtari911        mkdir($tempDir);
35671d05cddcSAtari911
35681d05cddcSAtari911        $zip->extractTo($tempDir);
35691d05cddcSAtari911        $zip->close();
35701d05cddcSAtari911
35711d05cddcSAtari911        // Determine source directory
35721d05cddcSAtari911        if ($hasCalendarFolder) {
35731d05cddcSAtari911            $sourceDir = $tempDir . 'calendar/';
35741d05cddcSAtari911        } else {
35751d05cddcSAtari911            $sourceDir = $tempDir;
35761d05cddcSAtari911        }
35771d05cddcSAtari911
35781d05cddcSAtari911        // Preserve configuration files
35791d05cddcSAtari911        $preserveFiles = ['sync_config.php', 'sync_state.json', 'sync.log'];
35801d05cddcSAtari911        $preserved = [];
35811d05cddcSAtari911        foreach ($preserveFiles as $file) {
35821d05cddcSAtari911            $oldFile = $pluginDir . $file;
35831d05cddcSAtari911            if (file_exists($oldFile)) {
35841d05cddcSAtari911                $preserved[$file] = file_get_contents($oldFile);
35851d05cddcSAtari911            }
35861d05cddcSAtari911        }
35871d05cddcSAtari911
35881d05cddcSAtari911        // Delete old plugin files (except data files)
35891d05cddcSAtari911        $this->deleteDirectoryContents($pluginDir, $preserveFiles);
35901d05cddcSAtari911
35911d05cddcSAtari911        // Copy new files
35921d05cddcSAtari911        $this->recursiveCopy($sourceDir, $pluginDir);
35931d05cddcSAtari911
35941d05cddcSAtari911        // Restore preserved files
35951d05cddcSAtari911        foreach ($preserved as $file => $content) {
35961d05cddcSAtari911            file_put_contents($pluginDir . $file, $content);
35971d05cddcSAtari911        }
35981d05cddcSAtari911
35991d05cddcSAtari911        // Update version and date in plugin.info.txt
36001d05cddcSAtari911        $pluginInfo = $pluginDir . 'plugin.info.txt';
36011d05cddcSAtari911        if (file_exists($pluginInfo)) {
36021d05cddcSAtari911            $info = confToHash($pluginInfo);
36031d05cddcSAtari911
36041d05cddcSAtari911            // Get new version from uploaded plugin
36051d05cddcSAtari911            $newVersion = $info['version'] ?? 'unknown';
36061d05cddcSAtari911
36071d05cddcSAtari911            // Update date to current
36081d05cddcSAtari911            $info['date'] = date('Y-m-d');
36091d05cddcSAtari911
36101d05cddcSAtari911            // Write updated info back
36111d05cddcSAtari911            $lines = [];
36121d05cddcSAtari911            foreach ($info as $key => $value) {
36131d05cddcSAtari911                $lines[] = str_pad($key, 8) . ' ' . $value;
36141d05cddcSAtari911            }
36151d05cddcSAtari911            file_put_contents($pluginInfo, implode("\n", $lines) . "\n");
36161d05cddcSAtari911        }
36171d05cddcSAtari911
36181d05cddcSAtari911        // Cleanup temp directory
36191d05cddcSAtari911        $this->deleteDirectory($tempDir);
36201d05cddcSAtari911
36211d05cddcSAtari911        $message = 'Plugin updated successfully!';
36221d05cddcSAtari911        if ($backupFirst) {
36231d05cddcSAtari911            $message .= ' Backup saved as: ' . $backupName;
36241d05cddcSAtari911        }
36251d05cddcSAtari911        $this->redirect($message, 'success', 'update');
36261d05cddcSAtari911    }
36271d05cddcSAtari911
36281d05cddcSAtari911    private function deleteBackup() {
36291d05cddcSAtari911        global $INPUT;
36301d05cddcSAtari911
36311d05cddcSAtari911        $filename = $INPUT->str('backup_file');
36321d05cddcSAtari911
36331d05cddcSAtari911        if (empty($filename)) {
36341d05cddcSAtari911            $this->redirect('No backup file specified', 'error', 'update');
36351d05cddcSAtari911            return;
36361d05cddcSAtari911        }
36371d05cddcSAtari911
36381d05cddcSAtari911        // Security: only allow files starting with "calendar" and ending with .zip, no directory traversal
36391d05cddcSAtari911        if (!preg_match('/^calendar[a-zA-Z0-9._-]*\.zip$/', $filename)) {
36401d05cddcSAtari911            $this->redirect('Invalid backup filename', 'error', 'update');
36411d05cddcSAtari911            return;
36421d05cddcSAtari911        }
36431d05cddcSAtari911
36441d05cddcSAtari911        $backupPath = DOKU_PLUGIN . $filename;
36451d05cddcSAtari911
36461d05cddcSAtari911        if (!file_exists($backupPath)) {
36471d05cddcSAtari911            $this->redirect('Backup file not found', 'error', 'update');
36481d05cddcSAtari911            return;
36491d05cddcSAtari911        }
36501d05cddcSAtari911
36511d05cddcSAtari911        if (@unlink($backupPath)) {
36521d05cddcSAtari911            $this->redirect('Backup deleted: ' . $filename, 'success', 'update');
36531d05cddcSAtari911        } else {
36541d05cddcSAtari911            $this->redirect('Failed to delete backup. Check file permissions.', 'error', 'update');
36551d05cddcSAtari911        }
36561d05cddcSAtari911    }
36571d05cddcSAtari911
36581d05cddcSAtari911    private function renameBackup() {
36591d05cddcSAtari911        global $INPUT;
36601d05cddcSAtari911
36611d05cddcSAtari911        $oldName = $INPUT->str('old_name');
36621d05cddcSAtari911        $newName = $INPUT->str('new_name');
36631d05cddcSAtari911
36641d05cddcSAtari911        if (empty($oldName) || empty($newName)) {
36651d05cddcSAtari911            $this->redirect('Missing filename(s)', 'error', 'update');
36661d05cddcSAtari911            return;
36671d05cddcSAtari911        }
36681d05cddcSAtari911
36691d05cddcSAtari911        // Security: validate filenames
36701d05cddcSAtari911        if (!preg_match('/^[a-zA-Z0-9._-]+\.zip$/', $oldName) || !preg_match('/^[a-zA-Z0-9._-]+\.zip$/', $newName)) {
36711d05cddcSAtari911            $this->redirect('Invalid filename format', 'error', 'update');
36721d05cddcSAtari911            return;
36731d05cddcSAtari911        }
36741d05cddcSAtari911
36751d05cddcSAtari911        $oldPath = DOKU_PLUGIN . $oldName;
36761d05cddcSAtari911        $newPath = DOKU_PLUGIN . $newName;
36771d05cddcSAtari911
36781d05cddcSAtari911        if (!file_exists($oldPath)) {
36791d05cddcSAtari911            $this->redirect('Backup file not found', 'error', 'update');
36801d05cddcSAtari911            return;
36811d05cddcSAtari911        }
36821d05cddcSAtari911
36831d05cddcSAtari911        if (file_exists($newPath)) {
36841d05cddcSAtari911            $this->redirect('A file with the new name already exists', 'error', 'update');
36851d05cddcSAtari911            return;
36861d05cddcSAtari911        }
36871d05cddcSAtari911
36881d05cddcSAtari911        if (@rename($oldPath, $newPath)) {
36891d05cddcSAtari911            $this->redirect('Backup renamed: ' . $oldName . ' → ' . $newName, 'success', 'update');
36901d05cddcSAtari911        } else {
36911d05cddcSAtari911            $this->redirect('Failed to rename backup. Check file permissions.', 'error', 'update');
36921d05cddcSAtari911        }
36931d05cddcSAtari911    }
36941d05cddcSAtari911
36951d05cddcSAtari911    private function restoreBackup() {
36961d05cddcSAtari911        global $INPUT;
36971d05cddcSAtari911
36981d05cddcSAtari911        $filename = $INPUT->str('backup_file');
36991d05cddcSAtari911
37001d05cddcSAtari911        if (empty($filename)) {
37011d05cddcSAtari911            $this->redirect('No backup file specified', 'error', 'update');
37021d05cddcSAtari911            return;
37031d05cddcSAtari911        }
37041d05cddcSAtari911
37051d05cddcSAtari911        // Security: only allow files starting with "calendar" and ending with .zip, no directory traversal
37061d05cddcSAtari911        if (!preg_match('/^calendar[a-zA-Z0-9._-]*\.zip$/', $filename)) {
37071d05cddcSAtari911            $this->redirect('Invalid backup filename', 'error', 'update');
37081d05cddcSAtari911            return;
37091d05cddcSAtari911        }
37101d05cddcSAtari911
37111d05cddcSAtari911        $backupPath = DOKU_PLUGIN . $filename;
37121d05cddcSAtari911        $pluginDir = DOKU_PLUGIN . 'calendar/';
37131d05cddcSAtari911
37141d05cddcSAtari911        if (!file_exists($backupPath)) {
37151d05cddcSAtari911            $this->redirect('Backup file not found', 'error', 'update');
37161d05cddcSAtari911            return;
37171d05cddcSAtari911        }
37181d05cddcSAtari911
37191d05cddcSAtari911        // Check if plugin directory is writable
37201d05cddcSAtari911        if (!is_writable($pluginDir)) {
37211d05cddcSAtari911            $this->redirect('Plugin directory is not writable. Please check permissions.', 'error', 'update');
37221d05cddcSAtari911            return;
37231d05cddcSAtari911        }
37241d05cddcSAtari911
37251d05cddcSAtari911        // Extract backup to temp directory
37261d05cddcSAtari911        $tempDir = DOKU_PLUGIN . 'calendar_restore_temp/';
37271d05cddcSAtari911        if (is_dir($tempDir)) {
37281d05cddcSAtari911            $this->deleteDirectory($tempDir);
37291d05cddcSAtari911        }
37301d05cddcSAtari911        mkdir($tempDir);
37311d05cddcSAtari911
37321d05cddcSAtari911        $zip = new ZipArchive();
37331d05cddcSAtari911        if ($zip->open($backupPath) !== TRUE) {
37341d05cddcSAtari911            $this->redirect('Failed to open backup ZIP file', 'error', 'update');
37351d05cddcSAtari911            return;
37361d05cddcSAtari911        }
37371d05cddcSAtari911
37381d05cddcSAtari911        $zip->extractTo($tempDir);
37391d05cddcSAtari911        $zip->close();
37401d05cddcSAtari911
37411d05cddcSAtari911        // The backup contains a "calendar/" folder
37421d05cddcSAtari911        $sourceDir = $tempDir . 'calendar/';
37431d05cddcSAtari911
37441d05cddcSAtari911        if (!is_dir($sourceDir)) {
37451d05cddcSAtari911            $this->deleteDirectory($tempDir);
37461d05cddcSAtari911            $this->redirect('Invalid backup structure', 'error', 'update');
37471d05cddcSAtari911            return;
37481d05cddcSAtari911        }
37491d05cddcSAtari911
37501d05cddcSAtari911        // Delete current plugin directory contents
37511d05cddcSAtari911        $this->deleteDirectoryContents($pluginDir, []);
37521d05cddcSAtari911
37531d05cddcSAtari911        // Copy backup files to plugin directory
37541d05cddcSAtari911        $this->recursiveCopy($sourceDir, $pluginDir);
37551d05cddcSAtari911
37561d05cddcSAtari911        // Cleanup temp directory
37571d05cddcSAtari911        $this->deleteDirectory($tempDir);
37581d05cddcSAtari911
37591d05cddcSAtari911        $this->redirect('Plugin restored from backup: ' . $filename, 'success', 'update');
37601d05cddcSAtari911    }
37611d05cddcSAtari911
37629ccd446eSAtari911    private function createManualBackup() {
37639ccd446eSAtari911        $pluginDir = DOKU_PLUGIN . 'calendar/';
37649ccd446eSAtari911
37659ccd446eSAtari911        // Check if plugin directory is readable
37669ccd446eSAtari911        if (!is_readable($pluginDir)) {
37679ccd446eSAtari911            $this->redirect('Plugin directory is not readable. Please check permissions.', 'error', 'update');
37689ccd446eSAtari911            return;
37699ccd446eSAtari911        }
37709ccd446eSAtari911
37719ccd446eSAtari911        // Check if parent directory is writable (for saving backup)
37729ccd446eSAtari911        if (!is_writable(DOKU_PLUGIN)) {
37739ccd446eSAtari911            $this->redirect('Plugin parent directory is not writable. Cannot save backup.', 'error', 'update');
37749ccd446eSAtari911            return;
37759ccd446eSAtari911        }
37769ccd446eSAtari911
37779ccd446eSAtari911        // Get current version
37789ccd446eSAtari911        $pluginInfo = $pluginDir . 'plugin.info.txt';
37799ccd446eSAtari911        $version = 'unknown';
37809ccd446eSAtari911        if (file_exists($pluginInfo)) {
37819ccd446eSAtari911            $info = confToHash($pluginInfo);
37829ccd446eSAtari911            $version = $info['version'] ?? ($info['date'] ?? 'unknown');
37839ccd446eSAtari911        }
37849ccd446eSAtari911
37859ccd446eSAtari911        $backupName = 'calendar.backup.v' . $version . '.manual.' . date('Y-m-d_H-i-s') . '.zip';
37869ccd446eSAtari911        $backupPath = DOKU_PLUGIN . $backupName;
37879ccd446eSAtari911
37889ccd446eSAtari911        try {
37899ccd446eSAtari911            $zip = new ZipArchive();
37909ccd446eSAtari911            if ($zip->open($backupPath, ZipArchive::CREATE) === TRUE) {
37919ccd446eSAtari911                $fileCount = $this->addDirectoryToZip($zip, $pluginDir, 'calendar/');
37929ccd446eSAtari911                $zip->close();
37939ccd446eSAtari911
37949ccd446eSAtari911                // Verify backup was created and has content
37959ccd446eSAtari911                if (!file_exists($backupPath)) {
37969ccd446eSAtari911                    $this->redirect('Backup file was not created', 'error', 'update');
37979ccd446eSAtari911                    return;
37989ccd446eSAtari911                }
37999ccd446eSAtari911
38009ccd446eSAtari911                $backupSize = filesize($backupPath);
38019ccd446eSAtari911                if ($backupSize < 1000) { // Backup should be at least 1KB
38029ccd446eSAtari911                    @unlink($backupPath);
38039ccd446eSAtari911                    $this->redirect('Backup file is too small (' . $this->formatBytes($backupSize) . '). Only ' . $fileCount . ' files were added. Backup failed.', 'error', 'update');
38049ccd446eSAtari911                    return;
38059ccd446eSAtari911                }
38069ccd446eSAtari911
38079ccd446eSAtari911                if ($fileCount < 10) { // Should have at least 10 files
38089ccd446eSAtari911                    @unlink($backupPath);
38099ccd446eSAtari911                    $this->redirect('Backup incomplete: Only ' . $fileCount . ' files were added (expected 30+). Backup failed.', 'error', 'update');
38109ccd446eSAtari911                    return;
38119ccd446eSAtari911                }
38129ccd446eSAtari911
38139ccd446eSAtari911                // Success!
38149ccd446eSAtari911                $this->redirect('✓ Manual backup created successfully: ' . $backupName . ' (' . $this->formatBytes($backupSize) . ', ' . $fileCount . ' files)', 'success', 'update');
38159ccd446eSAtari911
38169ccd446eSAtari911            } else {
38179ccd446eSAtari911                $this->redirect('Failed to create backup ZIP file', 'error', 'update');
38189ccd446eSAtari911                return;
38199ccd446eSAtari911            }
38209ccd446eSAtari911        } catch (Exception $e) {
38219ccd446eSAtari911            if (file_exists($backupPath)) {
38229ccd446eSAtari911                @unlink($backupPath);
38239ccd446eSAtari911            }
38249ccd446eSAtari911            $this->redirect('Backup failed: ' . $e->getMessage(), 'error', 'update');
38259ccd446eSAtari911            return;
38269ccd446eSAtari911        }
38279ccd446eSAtari911    }
38289ccd446eSAtari911
38291d05cddcSAtari911    private function addDirectoryToZip($zip, $dir, $zipPath = '') {
38309ccd446eSAtari911        $fileCount = 0;
38319ccd446eSAtari911        $errors = [];
38329ccd446eSAtari911
38339ccd446eSAtari911        if (!is_dir($dir)) {
38349ccd446eSAtari911            throw new Exception("Directory does not exist: $dir");
38359ccd446eSAtari911        }
38369ccd446eSAtari911
38379ccd446eSAtari911        if (!is_readable($dir)) {
38389ccd446eSAtari911            throw new Exception("Directory is not readable: $dir");
38399ccd446eSAtari911        }
38409ccd446eSAtari911
38411d05cddcSAtari911        try {
38421d05cddcSAtari911            $files = new RecursiveIteratorIterator(
38431d05cddcSAtari911                new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS),
38441d05cddcSAtari911                RecursiveIteratorIterator::LEAVES_ONLY
38451d05cddcSAtari911            );
38461d05cddcSAtari911
38471d05cddcSAtari911            foreach ($files as $file) {
38481d05cddcSAtari911                if (!$file->isDir()) {
38491d05cddcSAtari911                    $filePath = $file->getRealPath();
38501d05cddcSAtari911                    if ($filePath && is_readable($filePath)) {
38511d05cddcSAtari911                        $relativePath = $zipPath . substr($filePath, strlen($dir));
38529ccd446eSAtari911
38539ccd446eSAtari911                        if ($zip->addFile($filePath, $relativePath)) {
38549ccd446eSAtari911                            $fileCount++;
38559ccd446eSAtari911                        } else {
38569ccd446eSAtari911                            $errors[] = "Failed to add: " . basename($filePath);
38579ccd446eSAtari911                        }
38589ccd446eSAtari911                    } else {
38599ccd446eSAtari911                        $errors[] = "Cannot read: " . ($filePath ? basename($filePath) : 'unknown');
38601d05cddcSAtari911                    }
38611d05cddcSAtari911                }
38621d05cddcSAtari911            }
38639ccd446eSAtari911
38649ccd446eSAtari911            // Log any errors but don't fail if we got most files
38659ccd446eSAtari911            if (!empty($errors) && count($errors) < 5) {
38669ccd446eSAtari911                foreach ($errors as $error) {
38679ccd446eSAtari911                    error_log('Calendar plugin backup warning: ' . $error);
38689ccd446eSAtari911                }
38699ccd446eSAtari911            }
38709ccd446eSAtari911
38719ccd446eSAtari911            // If too many errors, fail
38729ccd446eSAtari911            if (count($errors) > 5) {
38739ccd446eSAtari911                throw new Exception("Too many errors adding files to backup: " . implode(', ', array_slice($errors, 0, 5)));
38749ccd446eSAtari911            }
38759ccd446eSAtari911
38761d05cddcSAtari911        } catch (Exception $e) {
38779ccd446eSAtari911            error_log('Calendar plugin backup error: ' . $e->getMessage());
38789ccd446eSAtari911            throw $e;
38791d05cddcSAtari911        }
38809ccd446eSAtari911
38819ccd446eSAtari911        return $fileCount;
38821d05cddcSAtari911    }
38831d05cddcSAtari911
38841d05cddcSAtari911    private function deleteDirectory($dir) {
38851d05cddcSAtari911        if (!is_dir($dir)) return;
38861d05cddcSAtari911
38871d05cddcSAtari911        try {
38881d05cddcSAtari911            $files = new RecursiveIteratorIterator(
38891d05cddcSAtari911                new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS),
38901d05cddcSAtari911                RecursiveIteratorIterator::CHILD_FIRST
38911d05cddcSAtari911            );
38921d05cddcSAtari911
38931d05cddcSAtari911            foreach ($files as $file) {
38941d05cddcSAtari911                if ($file->isDir()) {
38951d05cddcSAtari911                    @rmdir($file->getRealPath());
38961d05cddcSAtari911                } else {
38971d05cddcSAtari911                    @unlink($file->getRealPath());
38981d05cddcSAtari911                }
38991d05cddcSAtari911            }
39001d05cddcSAtari911
39011d05cddcSAtari911            @rmdir($dir);
39021d05cddcSAtari911        } catch (Exception $e) {
39031d05cddcSAtari911            error_log('Calendar plugin delete directory error: ' . $e->getMessage());
39041d05cddcSAtari911        }
39051d05cddcSAtari911    }
39061d05cddcSAtari911
39071d05cddcSAtari911    private function deleteDirectoryContents($dir, $preserve = []) {
39081d05cddcSAtari911        if (!is_dir($dir)) return;
39091d05cddcSAtari911
39101d05cddcSAtari911        $items = scandir($dir);
39111d05cddcSAtari911        foreach ($items as $item) {
39121d05cddcSAtari911            if ($item === '.' || $item === '..') continue;
39131d05cddcSAtari911            if (in_array($item, $preserve)) continue;
39141d05cddcSAtari911
39151d05cddcSAtari911            $path = $dir . $item;
39161d05cddcSAtari911            if (is_dir($path)) {
39171d05cddcSAtari911                $this->deleteDirectory($path);
39181d05cddcSAtari911            } else {
39191d05cddcSAtari911                unlink($path);
39201d05cddcSAtari911            }
39211d05cddcSAtari911        }
39221d05cddcSAtari911    }
39231d05cddcSAtari911
39241d05cddcSAtari911    private function recursiveCopy($src, $dst) {
39251d05cddcSAtari911        $dir = opendir($src);
39261d05cddcSAtari911        @mkdir($dst);
39271d05cddcSAtari911
39281d05cddcSAtari911        while (($file = readdir($dir)) !== false) {
39291d05cddcSAtari911            if ($file !== '.' && $file !== '..') {
39301d05cddcSAtari911                if (is_dir($src . '/' . $file)) {
39311d05cddcSAtari911                    $this->recursiveCopy($src . '/' . $file, $dst . '/' . $file);
39321d05cddcSAtari911                } else {
39331d05cddcSAtari911                    copy($src . '/' . $file, $dst . '/' . $file);
39341d05cddcSAtari911                }
39351d05cddcSAtari911            }
39361d05cddcSAtari911        }
39371d05cddcSAtari911
39381d05cddcSAtari911        closedir($dir);
39391d05cddcSAtari911    }
39401d05cddcSAtari911
39411d05cddcSAtari911    private function formatBytes($bytes) {
39421d05cddcSAtari911        if ($bytes >= 1073741824) {
39431d05cddcSAtari911            return number_format($bytes / 1073741824, 2) . ' GB';
39441d05cddcSAtari911        } elseif ($bytes >= 1048576) {
39451d05cddcSAtari911            return number_format($bytes / 1048576, 2) . ' MB';
39461d05cddcSAtari911        } elseif ($bytes >= 1024) {
39471d05cddcSAtari911            return number_format($bytes / 1024, 2) . ' KB';
39481d05cddcSAtari911        } else {
39491d05cddcSAtari911            return $bytes . ' bytes';
39501d05cddcSAtari911        }
39511d05cddcSAtari911    }
39521d05cddcSAtari911
39531d05cddcSAtari911    private function findPhpBinary() {
39541d05cddcSAtari911        // Try PHP_BINARY constant first (most reliable if available)
39551d05cddcSAtari911        if (defined('PHP_BINARY') && !empty(PHP_BINARY) && is_executable(PHP_BINARY)) {
39561d05cddcSAtari911            return escapeshellarg(PHP_BINARY);
39571d05cddcSAtari911        }
39581d05cddcSAtari911
39591d05cddcSAtari911        // Try common PHP binary locations
39601d05cddcSAtari911        $possiblePaths = [
39611d05cddcSAtari911            '/usr/bin/php',
39621d05cddcSAtari911            '/usr/bin/php8.1',
39631d05cddcSAtari911            '/usr/bin/php8.2',
39641d05cddcSAtari911            '/usr/bin/php8.3',
39651d05cddcSAtari911            '/usr/bin/php7.4',
39661d05cddcSAtari911            '/usr/local/bin/php',
39671d05cddcSAtari911            'php' // Last resort - rely on PATH
39681d05cddcSAtari911        ];
39691d05cddcSAtari911
39701d05cddcSAtari911        foreach ($possiblePaths as $path) {
39711d05cddcSAtari911            // Test if this PHP binary works
39721d05cddcSAtari911            $testOutput = [];
39731d05cddcSAtari911            $testReturn = 0;
39741d05cddcSAtari911            exec($path . ' -v 2>&1', $testOutput, $testReturn);
39751d05cddcSAtari911
39761d05cddcSAtari911            if ($testReturn === 0) {
39771d05cddcSAtari911                return ($path === 'php') ? 'php' : escapeshellarg($path);
39781d05cddcSAtari911            }
39791d05cddcSAtari911        }
39801d05cddcSAtari911
39811d05cddcSAtari911        // Fallback to 'php' and hope it's in PATH
39821d05cddcSAtari911        return 'php';
39831d05cddcSAtari911    }
39841d05cddcSAtari911
39851d05cddcSAtari911    private function redirect($message, $type = 'success', $tab = null) {
39861d05cddcSAtari911        $url = '?do=admin&page=calendar';
39871d05cddcSAtari911        if ($tab) {
39881d05cddcSAtari911            $url .= '&tab=' . $tab;
39891d05cddcSAtari911        }
39901d05cddcSAtari911        $url .= '&msg=' . urlencode($message) . '&msgtype=' . $type;
39911d05cddcSAtari911        header('Location: ' . $url);
39921d05cddcSAtari911        exit;
39931d05cddcSAtari911    }
39941d05cddcSAtari911
39951d05cddcSAtari911    private function getLog() {
39961d05cddcSAtari911        global $INPUT;
39971d05cddcSAtari911
39981d05cddcSAtari911        if ($INPUT->str('call') === 'ajax') {
39991d05cddcSAtari911            header('Content-Type: application/json');
40001d05cddcSAtari911
40011d05cddcSAtari911            $logFile = DOKU_PLUGIN . 'calendar/sync.log';
40021d05cddcSAtari911            $log = '';
40031d05cddcSAtari911
40041d05cddcSAtari911            if (file_exists($logFile)) {
40051d05cddcSAtari911                // Get last 500 lines
40061d05cddcSAtari911                $lines = file($logFile);
40071d05cddcSAtari911                if ($lines !== false) {
40081d05cddcSAtari911                    $lines = array_slice($lines, -500);
40091d05cddcSAtari911                    $log = implode('', $lines);
40101d05cddcSAtari911                }
40111d05cddcSAtari911            } else {
40121d05cddcSAtari911                $log = "No log file found. Sync hasn't run yet.";
40131d05cddcSAtari911            }
40141d05cddcSAtari911
40151d05cddcSAtari911            echo json_encode(['log' => $log]);
40161d05cddcSAtari911            exit;
40171d05cddcSAtari911        }
40181d05cddcSAtari911    }
40191d05cddcSAtari911
40201d05cddcSAtari911    private function exportConfig() {
40211d05cddcSAtari911        global $INPUT;
40221d05cddcSAtari911
40231d05cddcSAtari911        if ($INPUT->str('call') === 'ajax') {
40241d05cddcSAtari911            header('Content-Type: application/json');
40251d05cddcSAtari911
40261d05cddcSAtari911            try {
40271d05cddcSAtari911                $configFile = DOKU_PLUGIN . 'calendar/sync_config.php';
40281d05cddcSAtari911
40291d05cddcSAtari911                if (!file_exists($configFile)) {
40301d05cddcSAtari911                    echo json_encode([
40311d05cddcSAtari911                        'success' => false,
40321d05cddcSAtari911                        'message' => 'Config file not found'
40331d05cddcSAtari911                    ]);
40341d05cddcSAtari911                    exit;
40351d05cddcSAtari911                }
40361d05cddcSAtari911
40371d05cddcSAtari911                // Read config file
40381d05cddcSAtari911                $configContent = file_get_contents($configFile);
40391d05cddcSAtari911
40401d05cddcSAtari911                // Generate encryption key from DokuWiki secret
40411d05cddcSAtari911                $key = $this->getEncryptionKey();
40421d05cddcSAtari911
40431d05cddcSAtari911                // Encrypt config
40441d05cddcSAtari911                $encrypted = $this->encryptData($configContent, $key);
40451d05cddcSAtari911
40461d05cddcSAtari911                echo json_encode([
40471d05cddcSAtari911                    'success' => true,
40481d05cddcSAtari911                    'encrypted' => $encrypted,
40491d05cddcSAtari911                    'message' => 'Config exported successfully'
40501d05cddcSAtari911                ]);
40511d05cddcSAtari911                exit;
40521d05cddcSAtari911
40531d05cddcSAtari911            } catch (Exception $e) {
40541d05cddcSAtari911                echo json_encode([
40551d05cddcSAtari911                    'success' => false,
40561d05cddcSAtari911                    'message' => $e->getMessage()
40571d05cddcSAtari911                ]);
40581d05cddcSAtari911                exit;
40591d05cddcSAtari911            }
40601d05cddcSAtari911        }
40611d05cddcSAtari911    }
40621d05cddcSAtari911
40631d05cddcSAtari911    private function importConfig() {
40641d05cddcSAtari911        global $INPUT;
40651d05cddcSAtari911
40661d05cddcSAtari911        if ($INPUT->str('call') === 'ajax') {
40671d05cddcSAtari911            header('Content-Type: application/json');
40681d05cddcSAtari911
40691d05cddcSAtari911            try {
40701d05cddcSAtari911                $encrypted = $_POST['encrypted_config'] ?? '';
40711d05cddcSAtari911
40721d05cddcSAtari911                if (empty($encrypted)) {
40731d05cddcSAtari911                    echo json_encode([
40741d05cddcSAtari911                        'success' => false,
40751d05cddcSAtari911                        'message' => 'No config data provided'
40761d05cddcSAtari911                    ]);
40771d05cddcSAtari911                    exit;
40781d05cddcSAtari911                }
40791d05cddcSAtari911
40801d05cddcSAtari911                // Generate encryption key from DokuWiki secret
40811d05cddcSAtari911                $key = $this->getEncryptionKey();
40821d05cddcSAtari911
40831d05cddcSAtari911                // Decrypt config
40841d05cddcSAtari911                $configContent = $this->decryptData($encrypted, $key);
40851d05cddcSAtari911
40861d05cddcSAtari911                if ($configContent === false) {
40871d05cddcSAtari911                    echo json_encode([
40881d05cddcSAtari911                        'success' => false,
40891d05cddcSAtari911                        'message' => 'Decryption failed. Invalid key or corrupted file.'
40901d05cddcSAtari911                    ]);
40911d05cddcSAtari911                    exit;
40921d05cddcSAtari911                }
40931d05cddcSAtari911
40941d05cddcSAtari911                // Validate PHP syntax
40951d05cddcSAtari911                $valid = @eval('?>' . $configContent);
40961d05cddcSAtari911                if ($valid === false) {
40971d05cddcSAtari911                    echo json_encode([
40981d05cddcSAtari911                        'success' => false,
40991d05cddcSAtari911                        'message' => 'Invalid config file format'
41001d05cddcSAtari911                    ]);
41011d05cddcSAtari911                    exit;
41021d05cddcSAtari911                }
41031d05cddcSAtari911
41041d05cddcSAtari911                // Write to config file
41051d05cddcSAtari911                $configFile = DOKU_PLUGIN . 'calendar/sync_config.php';
41061d05cddcSAtari911
41071d05cddcSAtari911                // Backup existing config
41081d05cddcSAtari911                if (file_exists($configFile)) {
41091d05cddcSAtari911                    $backupFile = $configFile . '.backup.' . date('Y-m-d_H-i-s');
41101d05cddcSAtari911                    copy($configFile, $backupFile);
41111d05cddcSAtari911                }
41121d05cddcSAtari911
41131d05cddcSAtari911                // Write new config
41141d05cddcSAtari911                if (file_put_contents($configFile, $configContent) === false) {
41151d05cddcSAtari911                    echo json_encode([
41161d05cddcSAtari911                        'success' => false,
41171d05cddcSAtari911                        'message' => 'Failed to write config file'
41181d05cddcSAtari911                    ]);
41191d05cddcSAtari911                    exit;
41201d05cddcSAtari911                }
41211d05cddcSAtari911
41221d05cddcSAtari911                echo json_encode([
41231d05cddcSAtari911                    'success' => true,
41241d05cddcSAtari911                    'message' => 'Config imported successfully'
41251d05cddcSAtari911                ]);
41261d05cddcSAtari911                exit;
41271d05cddcSAtari911
41281d05cddcSAtari911            } catch (Exception $e) {
41291d05cddcSAtari911                echo json_encode([
41301d05cddcSAtari911                    'success' => false,
41311d05cddcSAtari911                    'message' => $e->getMessage()
41321d05cddcSAtari911                ]);
41331d05cddcSAtari911                exit;
41341d05cddcSAtari911            }
41351d05cddcSAtari911        }
41361d05cddcSAtari911    }
41371d05cddcSAtari911
41381d05cddcSAtari911    private function getEncryptionKey() {
41391d05cddcSAtari911        global $conf;
41401d05cddcSAtari911        // Use DokuWiki's secret as the base for encryption
41411d05cddcSAtari911        // This ensures the key is unique per installation
41421d05cddcSAtari911        return hash('sha256', $conf['secret'] . 'calendar_config_encryption', true);
41431d05cddcSAtari911    }
41441d05cddcSAtari911
41451d05cddcSAtari911    private function encryptData($data, $key) {
41461d05cddcSAtari911        // Use AES-256-CBC encryption
41471d05cddcSAtari911        $ivLength = openssl_cipher_iv_length('aes-256-cbc');
41481d05cddcSAtari911        $iv = openssl_random_pseudo_bytes($ivLength);
41491d05cddcSAtari911
41501d05cddcSAtari911        $encrypted = openssl_encrypt($data, 'aes-256-cbc', $key, 0, $iv);
41511d05cddcSAtari911
41521d05cddcSAtari911        // Combine IV and encrypted data, then base64 encode
41531d05cddcSAtari911        return base64_encode($iv . $encrypted);
41541d05cddcSAtari911    }
41551d05cddcSAtari911
41561d05cddcSAtari911    private function decryptData($encryptedData, $key) {
41571d05cddcSAtari911        // Decode base64
41581d05cddcSAtari911        $data = base64_decode($encryptedData);
41591d05cddcSAtari911
41601d05cddcSAtari911        if ($data === false) {
41611d05cddcSAtari911            return false;
41621d05cddcSAtari911        }
41631d05cddcSAtari911
41641d05cddcSAtari911        // Extract IV and encrypted content
41651d05cddcSAtari911        $ivLength = openssl_cipher_iv_length('aes-256-cbc');
41661d05cddcSAtari911        $iv = substr($data, 0, $ivLength);
41671d05cddcSAtari911        $encrypted = substr($data, $ivLength);
41681d05cddcSAtari911
41691d05cddcSAtari911        // Decrypt
41701d05cddcSAtari911        $decrypted = openssl_decrypt($encrypted, 'aes-256-cbc', $key, 0, $iv);
41711d05cddcSAtari911
41721d05cddcSAtari911        return $decrypted;
41731d05cddcSAtari911    }
41741d05cddcSAtari911
41751d05cddcSAtari911    private function clearLogFile() {
41761d05cddcSAtari911        global $INPUT;
41771d05cddcSAtari911
41781d05cddcSAtari911        if ($INPUT->str('call') === 'ajax') {
41791d05cddcSAtari911            header('Content-Type: application/json');
41801d05cddcSAtari911
41811d05cddcSAtari911            $logFile = DOKU_PLUGIN . 'calendar/sync.log';
41821d05cddcSAtari911
41831d05cddcSAtari911            if (file_exists($logFile)) {
41841d05cddcSAtari911                if (file_put_contents($logFile, '')) {
41851d05cddcSAtari911                    echo json_encode(['success' => true]);
41861d05cddcSAtari911                } else {
41871d05cddcSAtari911                    echo json_encode(['success' => false, 'message' => 'Could not clear log file']);
41881d05cddcSAtari911                }
41891d05cddcSAtari911            } else {
41901d05cddcSAtari911                echo json_encode(['success' => true, 'message' => 'No log file to clear']);
41911d05cddcSAtari911            }
41921d05cddcSAtari911            exit;
41931d05cddcSAtari911        }
41941d05cddcSAtari911    }
41951d05cddcSAtari911
41961d05cddcSAtari911    private function downloadLog() {
41971d05cddcSAtari911        $logFile = DOKU_PLUGIN . 'calendar/sync.log';
41981d05cddcSAtari911
41991d05cddcSAtari911        if (file_exists($logFile)) {
42001d05cddcSAtari911            header('Content-Type: text/plain');
42011d05cddcSAtari911            header('Content-Disposition: attachment; filename="calendar-sync-' . date('Y-m-d-His') . '.log"');
42021d05cddcSAtari911            readfile($logFile);
42031d05cddcSAtari911            exit;
42041d05cddcSAtari911        } else {
42051d05cddcSAtari911            echo 'No log file found';
42061d05cddcSAtari911            exit;
42071d05cddcSAtari911        }
42081d05cddcSAtari911    }
42091d05cddcSAtari911
42101d05cddcSAtari911    private function getEventStatistics() {
42111d05cddcSAtari911        $stats = [
42121d05cddcSAtari911            'total_events' => 0,
42131d05cddcSAtari911            'total_namespaces' => 0,
42141d05cddcSAtari911            'total_files' => 0,
42151d05cddcSAtari911            'total_recurring' => 0,
42161d05cddcSAtari911            'by_namespace' => [],
42171d05cddcSAtari911            'last_scan' => ''
42181d05cddcSAtari911        ];
42191d05cddcSAtari911
42201d05cddcSAtari911        $metaDir = DOKU_INC . 'data/meta/';
42211d05cddcSAtari911        $cacheFile = DOKU_PLUGIN . 'calendar/.event_stats_cache';
42221d05cddcSAtari911
42231d05cddcSAtari911        // Check if we have cached stats (less than 5 minutes old)
42241d05cddcSAtari911        if (file_exists($cacheFile)) {
42251d05cddcSAtari911            $cacheData = json_decode(file_get_contents($cacheFile), true);
42261d05cddcSAtari911            if ($cacheData && (time() - $cacheData['timestamp']) < 300) {
42271d05cddcSAtari911                return $cacheData['stats'];
42281d05cddcSAtari911            }
42291d05cddcSAtari911        }
42301d05cddcSAtari911
42311d05cddcSAtari911        // Scan for events
42321d05cddcSAtari911        $this->scanDirectoryForStats($metaDir, '', $stats);
42331d05cddcSAtari911
42341d05cddcSAtari911        // Count recurring events
42351d05cddcSAtari911        $recurringEvents = $this->findRecurringEvents();
42361d05cddcSAtari911        $stats['total_recurring'] = count($recurringEvents);
42371d05cddcSAtari911
42381d05cddcSAtari911        $stats['total_namespaces'] = count($stats['by_namespace']);
42391d05cddcSAtari911        $stats['last_scan'] = date('Y-m-d H:i:s');
42401d05cddcSAtari911
42411d05cddcSAtari911        // Cache the results
42421d05cddcSAtari911        file_put_contents($cacheFile, json_encode([
42431d05cddcSAtari911            'timestamp' => time(),
42441d05cddcSAtari911            'stats' => $stats
42451d05cddcSAtari911        ]));
42461d05cddcSAtari911
42471d05cddcSAtari911        return $stats;
42481d05cddcSAtari911    }
42491d05cddcSAtari911
42501d05cddcSAtari911    private function scanDirectoryForStats($dir, $namespace, &$stats) {
42511d05cddcSAtari911        if (!is_dir($dir)) return;
42521d05cddcSAtari911
42531d05cddcSAtari911        $items = scandir($dir);
42541d05cddcSAtari911        foreach ($items as $item) {
42551d05cddcSAtari911            if ($item === '.' || $item === '..') continue;
42561d05cddcSAtari911
42571d05cddcSAtari911            $path = $dir . $item;
42581d05cddcSAtari911
42591d05cddcSAtari911            // Check if this is a calendar directory
42601d05cddcSAtari911            if ($item === 'calendar' && is_dir($path)) {
42611d05cddcSAtari911                $jsonFiles = glob($path . '/*.json');
42621d05cddcSAtari911                $eventCount = 0;
42631d05cddcSAtari911
42641d05cddcSAtari911                foreach ($jsonFiles as $file) {
42651d05cddcSAtari911                    $stats['total_files']++;
42661d05cddcSAtari911                    $data = json_decode(file_get_contents($file), true);
42671d05cddcSAtari911                    if ($data) {
42681d05cddcSAtari911                        foreach ($data as $dateEvents) {
42691d05cddcSAtari911                            $eventCount += count($dateEvents);
42701d05cddcSAtari911                        }
42711d05cddcSAtari911                    }
42721d05cddcSAtari911                }
42731d05cddcSAtari911
42741d05cddcSAtari911                $stats['total_events'] += $eventCount;
42751d05cddcSAtari911
42761d05cddcSAtari911                if ($eventCount > 0) {
42771d05cddcSAtari911                    $stats['by_namespace'][$namespace] = [
42781d05cddcSAtari911                        'events' => $eventCount,
42791d05cddcSAtari911                        'files' => count($jsonFiles)
42801d05cddcSAtari911                    ];
42811d05cddcSAtari911                }
42821d05cddcSAtari911            } elseif (is_dir($path)) {
42831d05cddcSAtari911                // Recurse into subdirectories
42841d05cddcSAtari911                $newNamespace = $namespace ? $namespace . ':' . $item : $item;
42851d05cddcSAtari911                $this->scanDirectoryForStats($path . '/', $newNamespace, $stats);
42861d05cddcSAtari911            }
42871d05cddcSAtari911        }
42881d05cddcSAtari911    }
42891d05cddcSAtari911
42901d05cddcSAtari911    private function rescanEvents() {
42911d05cddcSAtari911        // Clear the cache to force a rescan
42929ccd446eSAtari911        $this->clearStatsCache();
42931d05cddcSAtari911
42941d05cddcSAtari911        // Get fresh statistics
42951d05cddcSAtari911        $stats = $this->getEventStatistics();
42961d05cddcSAtari911
42971d05cddcSAtari911        // Build absolute redirect URL
42981d05cddcSAtari911        $redirectUrl = DOKU_URL . 'doku.php?do=admin&page=calendar&tab=manage&msg=' . urlencode('Events rescanned! Found ' . $stats['total_events'] . ' events in ' . $stats['total_namespaces'] . ' namespaces.') . '&msgtype=success';
42991d05cddcSAtari911
43001d05cddcSAtari911        // Redirect with success message using absolute URL
43011d05cddcSAtari911        header('Location: ' . $redirectUrl, true, 303);
43021d05cddcSAtari911        exit;
43031d05cddcSAtari911    }
43041d05cddcSAtari911
43051d05cddcSAtari911    private function exportAllEvents() {
43061d05cddcSAtari911        $metaDir = DOKU_INC . 'data/meta/';
43071d05cddcSAtari911        $allEvents = [];
43081d05cddcSAtari911
43091d05cddcSAtari911        // Collect all events
43101d05cddcSAtari911        $this->collectAllEvents($metaDir, '', $allEvents);
43111d05cddcSAtari911
43121d05cddcSAtari911        // Create export package
43139ccd446eSAtari911        // Get current version
43149ccd446eSAtari911        $pluginInfo = DOKU_PLUGIN . 'calendar/plugin.info.txt';
43159ccd446eSAtari911        $info = file_exists($pluginInfo) ? confToHash($pluginInfo) : [];
43169ccd446eSAtari911        $currentVersion = isset($info['version']) ? trim($info['version']) : 'unknown';
43179ccd446eSAtari911
43181d05cddcSAtari911        $exportData = [
43191d05cddcSAtari911            'export_date' => date('Y-m-d H:i:s'),
43209ccd446eSAtari911            'version' => $currentVersion,
43211d05cddcSAtari911            'total_events' => 0,
43221d05cddcSAtari911            'namespaces' => []
43231d05cddcSAtari911        ];
43241d05cddcSAtari911
43251d05cddcSAtari911        foreach ($allEvents as $namespace => $files) {
43261d05cddcSAtari911            $exportData['namespaces'][$namespace] = [];
43271d05cddcSAtari911            foreach ($files as $filename => $events) {
43281d05cddcSAtari911                $exportData['namespaces'][$namespace][$filename] = $events;
43291d05cddcSAtari911                foreach ($events as $dateEvents) {
43301d05cddcSAtari911                    $exportData['total_events'] += count($dateEvents);
43311d05cddcSAtari911                }
43321d05cddcSAtari911            }
43331d05cddcSAtari911        }
43341d05cddcSAtari911
43351d05cddcSAtari911        // Send as download
43361d05cddcSAtari911        header('Content-Type: application/json');
43371d05cddcSAtari911        header('Content-Disposition: attachment; filename="calendar-events-export-' . date('Y-m-d-His') . '.json"');
43381d05cddcSAtari911        echo json_encode($exportData, JSON_PRETTY_PRINT);
43391d05cddcSAtari911        exit;
43401d05cddcSAtari911    }
43411d05cddcSAtari911
43421d05cddcSAtari911    private function collectAllEvents($dir, $namespace, &$allEvents) {
43431d05cddcSAtari911        if (!is_dir($dir)) return;
43441d05cddcSAtari911
43451d05cddcSAtari911        $items = scandir($dir);
43461d05cddcSAtari911        foreach ($items as $item) {
43471d05cddcSAtari911            if ($item === '.' || $item === '..') continue;
43481d05cddcSAtari911
43491d05cddcSAtari911            $path = $dir . $item;
43501d05cddcSAtari911
43511d05cddcSAtari911            // Check if this is a calendar directory
43521d05cddcSAtari911            if ($item === 'calendar' && is_dir($path)) {
43531d05cddcSAtari911                $jsonFiles = glob($path . '/*.json');
43541d05cddcSAtari911
43551d05cddcSAtari911                if (!isset($allEvents[$namespace])) {
43561d05cddcSAtari911                    $allEvents[$namespace] = [];
43571d05cddcSAtari911                }
43581d05cddcSAtari911
43591d05cddcSAtari911                foreach ($jsonFiles as $file) {
43601d05cddcSAtari911                    $filename = basename($file);
43611d05cddcSAtari911                    $data = json_decode(file_get_contents($file), true);
43621d05cddcSAtari911                    if ($data) {
43631d05cddcSAtari911                        $allEvents[$namespace][$filename] = $data;
43641d05cddcSAtari911                    }
43651d05cddcSAtari911                }
43661d05cddcSAtari911            } elseif (is_dir($path)) {
43671d05cddcSAtari911                // Recurse into subdirectories
43681d05cddcSAtari911                $newNamespace = $namespace ? $namespace . ':' . $item : $item;
43691d05cddcSAtari911                $this->collectAllEvents($path . '/', $newNamespace, $allEvents);
43701d05cddcSAtari911            }
43711d05cddcSAtari911        }
43721d05cddcSAtari911    }
43731d05cddcSAtari911
43741d05cddcSAtari911    private function importAllEvents() {
43751d05cddcSAtari911        global $INPUT;
43761d05cddcSAtari911
43771d05cddcSAtari911        if (!isset($_FILES['import_file'])) {
43781d05cddcSAtari911            $redirectUrl = DOKU_URL . 'doku.php?do=admin&page=calendar&tab=manage&msg=' . urlencode('No file uploaded') . '&msgtype=error';
43791d05cddcSAtari911            header('Location: ' . $redirectUrl, true, 303);
43801d05cddcSAtari911            exit;
43811d05cddcSAtari911        }
43821d05cddcSAtari911
43831d05cddcSAtari911        $file = $_FILES['import_file'];
43841d05cddcSAtari911
43851d05cddcSAtari911        if ($file['error'] !== UPLOAD_ERR_OK) {
43861d05cddcSAtari911            $redirectUrl = DOKU_URL . 'doku.php?do=admin&page=calendar&tab=manage&msg=' . urlencode('Upload error: ' . $file['error']) . '&msgtype=error';
43871d05cddcSAtari911            header('Location: ' . $redirectUrl, true, 303);
43881d05cddcSAtari911            exit;
43891d05cddcSAtari911        }
43901d05cddcSAtari911
43911d05cddcSAtari911        // Read and decode the import file
43921d05cddcSAtari911        $importData = json_decode(file_get_contents($file['tmp_name']), true);
43931d05cddcSAtari911
43941d05cddcSAtari911        if (!$importData || !isset($importData['namespaces'])) {
43951d05cddcSAtari911            $redirectUrl = DOKU_URL . 'doku.php?do=admin&page=calendar&tab=manage&msg=' . urlencode('Invalid import file format') . '&msgtype=error';
43961d05cddcSAtari911            header('Location: ' . $redirectUrl, true, 303);
43971d05cddcSAtari911            exit;
43981d05cddcSAtari911        }
43991d05cddcSAtari911
44001d05cddcSAtari911        $importedCount = 0;
44011d05cddcSAtari911        $mergedCount = 0;
44021d05cddcSAtari911
44031d05cddcSAtari911        // Import events
44041d05cddcSAtari911        foreach ($importData['namespaces'] as $namespace => $files) {
44051d05cddcSAtari911            $metaDir = DOKU_INC . 'data/meta/';
44061d05cddcSAtari911            if ($namespace) {
44071d05cddcSAtari911                $metaDir .= str_replace(':', '/', $namespace) . '/';
44081d05cddcSAtari911            }
44091d05cddcSAtari911            $calendarDir = $metaDir . 'calendar/';
44101d05cddcSAtari911
44111d05cddcSAtari911            // Create directory if needed
44121d05cddcSAtari911            if (!is_dir($calendarDir)) {
44131d05cddcSAtari911                mkdir($calendarDir, 0755, true);
44141d05cddcSAtari911            }
44151d05cddcSAtari911
44161d05cddcSAtari911            foreach ($files as $filename => $events) {
44171d05cddcSAtari911                $targetFile = $calendarDir . $filename;
44181d05cddcSAtari911
44191d05cddcSAtari911                // If file exists, merge events
44201d05cddcSAtari911                if (file_exists($targetFile)) {
44211d05cddcSAtari911                    $existing = json_decode(file_get_contents($targetFile), true);
44221d05cddcSAtari911                    if ($existing) {
44231d05cddcSAtari911                        foreach ($events as $date => $dateEvents) {
44241d05cddcSAtari911                            if (!isset($existing[$date])) {
44251d05cddcSAtari911                                $existing[$date] = [];
44261d05cddcSAtari911                            }
44271d05cddcSAtari911                            foreach ($dateEvents as $event) {
44281d05cddcSAtari911                                // Check if event with same ID exists
44291d05cddcSAtari911                                $found = false;
44301d05cddcSAtari911                                foreach ($existing[$date] as $existingEvent) {
44311d05cddcSAtari911                                    if ($existingEvent['id'] === $event['id']) {
44321d05cddcSAtari911                                        $found = true;
44331d05cddcSAtari911                                        break;
44341d05cddcSAtari911                                    }
44351d05cddcSAtari911                                }
44361d05cddcSAtari911                                if (!$found) {
44371d05cddcSAtari911                                    $existing[$date][] = $event;
44381d05cddcSAtari911                                    $importedCount++;
44391d05cddcSAtari911                                } else {
44401d05cddcSAtari911                                    $mergedCount++;
44411d05cddcSAtari911                                }
44421d05cddcSAtari911                            }
44431d05cddcSAtari911                        }
44441d05cddcSAtari911                        file_put_contents($targetFile, json_encode($existing, JSON_PRETTY_PRINT));
44451d05cddcSAtari911                    }
44461d05cddcSAtari911                } else {
44471d05cddcSAtari911                    // New file
44481d05cddcSAtari911                    file_put_contents($targetFile, json_encode($events, JSON_PRETTY_PRINT));
44491d05cddcSAtari911                    foreach ($events as $dateEvents) {
44501d05cddcSAtari911                        $importedCount += count($dateEvents);
44511d05cddcSAtari911                    }
44521d05cddcSAtari911                }
44531d05cddcSAtari911            }
44541d05cddcSAtari911        }
44551d05cddcSAtari911
44561d05cddcSAtari911        // Clear cache
44579ccd446eSAtari911        $this->clearStatsCache();
44581d05cddcSAtari911
44591d05cddcSAtari911        $message = "Import complete! Imported $importedCount new events";
44601d05cddcSAtari911        if ($mergedCount > 0) {
44611d05cddcSAtari911            $message .= ", skipped $mergedCount duplicates";
44621d05cddcSAtari911        }
44631d05cddcSAtari911
44641d05cddcSAtari911        $redirectUrl = DOKU_URL . 'doku.php?do=admin&page=calendar&tab=manage&msg=' . urlencode($message) . '&msgtype=success';
44651d05cddcSAtari911        header('Location: ' . $redirectUrl, true, 303);
44661d05cddcSAtari911        exit;
44671d05cddcSAtari911    }
44681d05cddcSAtari911
44691d05cddcSAtari911    private function previewCleanup() {
44701d05cddcSAtari911        global $INPUT;
44711d05cddcSAtari911
44721d05cddcSAtari911        $cleanupType = $INPUT->str('cleanup_type', 'age');
44731d05cddcSAtari911        $namespaceFilter = $INPUT->str('namespace_filter', '');
44741d05cddcSAtari911
44751d05cddcSAtari911        // Debug info
44761d05cddcSAtari911        $debug = [];
44771d05cddcSAtari911        $debug['cleanup_type'] = $cleanupType;
44781d05cddcSAtari911        $debug['namespace_filter'] = $namespaceFilter;
44791d05cddcSAtari911        $debug['age_value'] = $INPUT->int('age_value', 6);
44801d05cddcSAtari911        $debug['age_unit'] = $INPUT->str('age_unit', 'months');
44811d05cddcSAtari911        $debug['range_start'] = $INPUT->str('range_start', '');
44821d05cddcSAtari911        $debug['range_end'] = $INPUT->str('range_end', '');
44831d05cddcSAtari911        $debug['delete_completed'] = $INPUT->bool('delete_completed', false);
44841d05cddcSAtari911        $debug['delete_past'] = $INPUT->bool('delete_past', false);
44851d05cddcSAtari911
44861d05cddcSAtari911        $dataDir = DOKU_INC . 'data/meta/';
44871d05cddcSAtari911        $debug['data_dir'] = $dataDir;
44881d05cddcSAtari911        $debug['data_dir_exists'] = is_dir($dataDir);
44891d05cddcSAtari911
44901d05cddcSAtari911        $eventsToDelete = $this->findEventsToCleanup($cleanupType, $namespaceFilter);
44911d05cddcSAtari911
44921d05cddcSAtari911        // Merge with scan debug info
44931d05cddcSAtari911        if (isset($this->_cleanupDebug)) {
44941d05cddcSAtari911            $debug = array_merge($debug, $this->_cleanupDebug);
44951d05cddcSAtari911        }
44961d05cddcSAtari911
44971d05cddcSAtari911        // Return JSON for preview with debug info
44981d05cddcSAtari911        header('Content-Type: application/json');
44991d05cddcSAtari911        echo json_encode([
45001d05cddcSAtari911            'count' => count($eventsToDelete),
45011d05cddcSAtari911            'events' => array_slice($eventsToDelete, 0, 50), // Limit to 50 for preview
45021d05cddcSAtari911            'debug' => $debug
45031d05cddcSAtari911        ]);
45041d05cddcSAtari911        exit;
45051d05cddcSAtari911    }
45061d05cddcSAtari911
45071d05cddcSAtari911    private function cleanupEvents() {
45081d05cddcSAtari911        global $INPUT;
45091d05cddcSAtari911
45101d05cddcSAtari911        $cleanupType = $INPUT->str('cleanup_type', 'age');
45111d05cddcSAtari911        $namespaceFilter = $INPUT->str('namespace_filter', '');
45121d05cddcSAtari911
45131d05cddcSAtari911        // Create backup first
45141d05cddcSAtari911        $backupDir = DOKU_PLUGIN . 'calendar/backups/';
45151d05cddcSAtari911        if (!is_dir($backupDir)) {
45161d05cddcSAtari911            mkdir($backupDir, 0755, true);
45171d05cddcSAtari911        }
45181d05cddcSAtari911
45191d05cddcSAtari911        $backupFile = $backupDir . 'before-cleanup-' . date('Y-m-d-His') . '.zip';
45201d05cddcSAtari911        $this->createBackup($backupFile);
45211d05cddcSAtari911
45221d05cddcSAtari911        // Find events to delete
45231d05cddcSAtari911        $eventsToDelete = $this->findEventsToCleanup($cleanupType, $namespaceFilter);
45241d05cddcSAtari911        $deletedCount = 0;
45251d05cddcSAtari911
45261d05cddcSAtari911        // Group by file
45271d05cddcSAtari911        $fileGroups = [];
45281d05cddcSAtari911        foreach ($eventsToDelete as $evt) {
45291d05cddcSAtari911            $fileGroups[$evt['file']][] = $evt;
45301d05cddcSAtari911        }
45311d05cddcSAtari911
45321d05cddcSAtari911        // Delete from each file
45331d05cddcSAtari911        foreach ($fileGroups as $file => $events) {
45341d05cddcSAtari911            if (!file_exists($file)) continue;
45351d05cddcSAtari911
45361d05cddcSAtari911            $json = file_get_contents($file);
45371d05cddcSAtari911            $data = json_decode($json, true);
45381d05cddcSAtari911
45391d05cddcSAtari911            if (!$data) continue;
45401d05cddcSAtari911
45411d05cddcSAtari911            // Remove events
45421d05cddcSAtari911            foreach ($events as $evt) {
45431d05cddcSAtari911                if (isset($data[$evt['date']])) {
45441d05cddcSAtari911                    $data[$evt['date']] = array_filter($data[$evt['date']], function($e) use ($evt) {
45451d05cddcSAtari911                        return $e['id'] !== $evt['id'];
45461d05cddcSAtari911                    });
45471d05cddcSAtari911
45481d05cddcSAtari911                    // Remove date key if empty
45491d05cddcSAtari911                    if (empty($data[$evt['date']])) {
45501d05cddcSAtari911                        unset($data[$evt['date']]);
45511d05cddcSAtari911                    }
45521d05cddcSAtari911
45531d05cddcSAtari911                    $deletedCount++;
45541d05cddcSAtari911                }
45551d05cddcSAtari911            }
45561d05cddcSAtari911
45571d05cddcSAtari911            // Save file or delete if empty
45581d05cddcSAtari911            if (empty($data)) {
45591d05cddcSAtari911                unlink($file);
45601d05cddcSAtari911            } else {
45611d05cddcSAtari911                file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT));
45621d05cddcSAtari911            }
45631d05cddcSAtari911        }
45641d05cddcSAtari911
45651d05cddcSAtari911        // Clear cache
45669ccd446eSAtari911        $this->clearStatsCache();
45671d05cddcSAtari911
45681d05cddcSAtari911        $message = "Cleanup complete! Deleted $deletedCount event(s). Backup created: " . basename($backupFile);
45691d05cddcSAtari911        $redirectUrl = DOKU_URL . 'doku.php?do=admin&page=calendar&tab=manage&msg=' . urlencode($message) . '&msgtype=success';
45701d05cddcSAtari911        header('Location: ' . $redirectUrl, true, 303);
45711d05cddcSAtari911        exit;
45721d05cddcSAtari911    }
45731d05cddcSAtari911
45741d05cddcSAtari911    private function findEventsToCleanup($cleanupType, $namespaceFilter) {
45751d05cddcSAtari911        global $INPUT;
45761d05cddcSAtari911
45771d05cddcSAtari911        $eventsToDelete = [];
45781d05cddcSAtari911        $dataDir = DOKU_INC . 'data/meta/';
45791d05cddcSAtari911
45801d05cddcSAtari911        $debug = [];
45811d05cddcSAtari911        $debug['scanned_dirs'] = [];
45821d05cddcSAtari911        $debug['found_files'] = [];
45831d05cddcSAtari911
45841d05cddcSAtari911        // Calculate cutoff date for age-based cleanup
45851d05cddcSAtari911        $cutoffDate = null;
45861d05cddcSAtari911        if ($cleanupType === 'age') {
45871d05cddcSAtari911            $ageValue = $INPUT->int('age_value', 6);
45881d05cddcSAtari911            $ageUnit = $INPUT->str('age_unit', 'months');
45891d05cddcSAtari911
45901d05cddcSAtari911            if ($ageUnit === 'years') {
45911d05cddcSAtari911                $ageValue *= 12; // Convert to months
45921d05cddcSAtari911            }
45931d05cddcSAtari911
45941d05cddcSAtari911            $cutoffDate = date('Y-m-d', strtotime("-$ageValue months"));
45951d05cddcSAtari911            $debug['cutoff_date'] = $cutoffDate;
45961d05cddcSAtari911        }
45971d05cddcSAtari911
45981d05cddcSAtari911        // Get date range for range-based cleanup
45991d05cddcSAtari911        $rangeStart = $cleanupType === 'range' ? $INPUT->str('range_start', '') : null;
46001d05cddcSAtari911        $rangeEnd = $cleanupType === 'range' ? $INPUT->str('range_end', '') : null;
46011d05cddcSAtari911
46021d05cddcSAtari911        // Get status filters
46031d05cddcSAtari911        $deleteCompleted = $cleanupType === 'status' && $INPUT->bool('delete_completed', false);
46041d05cddcSAtari911        $deletePast = $cleanupType === 'status' && $INPUT->bool('delete_past', false);
46051d05cddcSAtari911
46061d05cddcSAtari911        // Check root calendar directory first (blank/default namespace)
46071d05cddcSAtari911        $rootCalendarDir = $dataDir . 'calendar';
46081d05cddcSAtari911        $debug['root_calendar_dir'] = $rootCalendarDir;
46091d05cddcSAtari911        $debug['root_exists'] = is_dir($rootCalendarDir);
46101d05cddcSAtari911
46111d05cddcSAtari911        if (is_dir($rootCalendarDir)) {
46121d05cddcSAtari911            if (!$namespaceFilter || $namespaceFilter === '' || $namespaceFilter === 'default') {
46131d05cddcSAtari911                $debug['scanned_dirs'][] = $rootCalendarDir;
46141d05cddcSAtari911                $files = glob($rootCalendarDir . '/*.json');
46151d05cddcSAtari911                $debug['found_files'] = array_merge($debug['found_files'], $files);
46161d05cddcSAtari911                $this->processCalendarFiles($rootCalendarDir, '', $eventsToDelete, $cleanupType, $cutoffDate, $rangeStart, $rangeEnd, $deleteCompleted, $deletePast);
46171d05cddcSAtari911            }
46181d05cddcSAtari911        }
46191d05cddcSAtari911
46201d05cddcSAtari911        // Scan all namespace directories
46211d05cddcSAtari911        $namespaceDirs = glob($dataDir . '*', GLOB_ONLYDIR);
46221d05cddcSAtari911        $debug['namespace_dirs_found'] = $namespaceDirs;
46231d05cddcSAtari911
46241d05cddcSAtari911        foreach ($namespaceDirs as $nsDir) {
46251d05cddcSAtari911            $namespace = basename($nsDir);
46261d05cddcSAtari911
46271d05cddcSAtari911            // Skip the root 'calendar' dir (already processed above)
46281d05cddcSAtari911            if ($namespace === 'calendar') continue;
46291d05cddcSAtari911
46301d05cddcSAtari911            // Check namespace filter
46311d05cddcSAtari911            if ($namespaceFilter && strpos($namespace, $namespaceFilter) === false) {
46321d05cddcSAtari911                continue;
46331d05cddcSAtari911            }
46341d05cddcSAtari911
46351d05cddcSAtari911            $calendarDir = $nsDir . '/calendar';
46361d05cddcSAtari911            $debug['checked_calendar_dirs'][] = $calendarDir;
46371d05cddcSAtari911
46381d05cddcSAtari911            if (!is_dir($calendarDir)) {
46391d05cddcSAtari911                $debug['missing_calendar_dirs'][] = $calendarDir;
46401d05cddcSAtari911                continue;
46411d05cddcSAtari911            }
46421d05cddcSAtari911
46431d05cddcSAtari911            $debug['scanned_dirs'][] = $calendarDir;
46441d05cddcSAtari911            $files = glob($calendarDir . '/*.json');
46451d05cddcSAtari911            $debug['found_files'] = array_merge($debug['found_files'], $files);
46461d05cddcSAtari911            $this->processCalendarFiles($calendarDir, $namespace, $eventsToDelete, $cleanupType, $cutoffDate, $rangeStart, $rangeEnd, $deleteCompleted, $deletePast);
46471d05cddcSAtari911        }
46481d05cddcSAtari911
46491d05cddcSAtari911        // Store debug info globally for preview
46501d05cddcSAtari911        $this->_cleanupDebug = $debug;
46511d05cddcSAtari911
46521d05cddcSAtari911        return $eventsToDelete;
46531d05cddcSAtari911    }
46541d05cddcSAtari911
46551d05cddcSAtari911    private function processCalendarFiles($calendarDir, $namespace, &$eventsToDelete, $cleanupType, $cutoffDate, $rangeStart, $rangeEnd, $deleteCompleted, $deletePast) {
46561d05cddcSAtari911        foreach (glob($calendarDir . '/*.json') as $file) {
46571d05cddcSAtari911            $json = file_get_contents($file);
46581d05cddcSAtari911            $data = json_decode($json, true);
46591d05cddcSAtari911
46601d05cddcSAtari911            if (!$data) continue;
46611d05cddcSAtari911
46621d05cddcSAtari911            foreach ($data as $date => $dateEvents) {
46631d05cddcSAtari911                foreach ($dateEvents as $event) {
46641d05cddcSAtari911                    $shouldDelete = false;
46651d05cddcSAtari911
46661d05cddcSAtari911                    // Age-based
46671d05cddcSAtari911                    if ($cleanupType === 'age' && $cutoffDate && $date < $cutoffDate) {
46681d05cddcSAtari911                        $shouldDelete = true;
46691d05cddcSAtari911                    }
46701d05cddcSAtari911
46711d05cddcSAtari911                    // Range-based
46721d05cddcSAtari911                    if ($cleanupType === 'range' && $rangeStart && $rangeEnd) {
46731d05cddcSAtari911                        if ($date >= $rangeStart && $date <= $rangeEnd) {
46741d05cddcSAtari911                            $shouldDelete = true;
46751d05cddcSAtari911                        }
46761d05cddcSAtari911                    }
46771d05cddcSAtari911
46781d05cddcSAtari911                    // Status-based
46791d05cddcSAtari911                    if ($cleanupType === 'status') {
46801d05cddcSAtari911                        $isTask = isset($event['isTask']) && $event['isTask'];
46811d05cddcSAtari911                        $isCompleted = isset($event['completed']) && $event['completed'];
46821d05cddcSAtari911                        $isPast = $date < date('Y-m-d');
46831d05cddcSAtari911
46841d05cddcSAtari911                        if ($deleteCompleted && $isTask && $isCompleted) {
46851d05cddcSAtari911                            $shouldDelete = true;
46861d05cddcSAtari911                        }
46871d05cddcSAtari911                        if ($deletePast && !$isTask && $isPast) {
46881d05cddcSAtari911                            $shouldDelete = true;
46891d05cddcSAtari911                        }
46901d05cddcSAtari911                    }
46911d05cddcSAtari911
46921d05cddcSAtari911                    if ($shouldDelete) {
46931d05cddcSAtari911                        $eventsToDelete[] = [
46941d05cddcSAtari911                            'id' => $event['id'],
46951d05cddcSAtari911                            'title' => $event['title'],
46961d05cddcSAtari911                            'date' => $date,
46971d05cddcSAtari911                            'namespace' => $namespace ?: 'default',
46981d05cddcSAtari911                            'file' => $file
46991d05cddcSAtari911                        ];
47001d05cddcSAtari911                    }
47011d05cddcSAtari911                }
47021d05cddcSAtari911            }
47031d05cddcSAtari911        }
47041d05cddcSAtari911    }
47059ccd446eSAtari911
47069ccd446eSAtari911    /**
47079ccd446eSAtari911     * Render Themes tab for sidebar widget theme selection
47089ccd446eSAtari911     */
47099ccd446eSAtari911    private function renderThemesTab($colors = null) {
47109ccd446eSAtari911        global $INPUT;
47119ccd446eSAtari911
47129ccd446eSAtari911        // Use defaults if not provided
47139ccd446eSAtari911        if ($colors === null) {
47149ccd446eSAtari911            $colors = $this->getTemplateColors();
47159ccd446eSAtari911        }
47169ccd446eSAtari911
47179ccd446eSAtari911        // Handle theme save
47189ccd446eSAtari911        if ($INPUT->str('action') === 'save_theme') {
47199ccd446eSAtari911            $theme = $INPUT->str('theme', 'matrix');
47209ccd446eSAtari911            $weekStart = $INPUT->str('week_start', 'monday');
47219ccd446eSAtari911            $this->saveSidebarTheme($theme);
47229ccd446eSAtari911            $this->saveWeekStartDay($weekStart);
47239ccd446eSAtari911            echo '<div style="background:#d4edda; border:1px solid #c3e6cb; color:#155724; padding:12px; border-radius:4px; margin-bottom:20px;">';
47249ccd446eSAtari911            echo '✓ Theme and settings saved successfully! Refresh any page with the sidebar to see changes.';
47259ccd446eSAtari911            echo '</div>';
47269ccd446eSAtari911        }
47279ccd446eSAtari911
47289ccd446eSAtari911        $currentTheme = $this->getSidebarTheme();
47299ccd446eSAtari911        $currentWeekStart = $this->getWeekStartDay();
47309ccd446eSAtari911
47319ccd446eSAtari911        echo '<h2 style="margin:0 0 20px 0; color:' . $colors['text'] . ';">�� Sidebar Widget Settings</h2>';
47329ccd446eSAtari911        echo '<p style="color:' . $colors['text'] . '; margin-bottom:20px;">Customize the appearance and behavior of the sidebar calendar widget.</p>';
47339ccd446eSAtari911
47349ccd446eSAtari911        echo '<form method="post" action="?do=admin&page=calendar&tab=themes">';
47359ccd446eSAtari911        echo '<input type="hidden" name="action" value="save_theme">';
47369ccd446eSAtari911
47379ccd446eSAtari911        // Week Start Day Section
47389ccd446eSAtari911        echo '<div style="background:' . $colors['bg'] . '; border:1px solid ' . $colors['border'] . '; border-radius:6px; padding:20px; margin-bottom:30px;">';
47399ccd446eSAtari911        echo '<h3 style="margin:0 0 15px 0; color:' . $colors['text'] . '; font-size:16px;">�� Week Start Day</h3>';
47409ccd446eSAtari911        echo '<p style="color:' . $colors['text'] . '; margin-bottom:15px; font-size:13px;">Choose which day the week calendar grid starts with:</p>';
47419ccd446eSAtari911
47429ccd446eSAtari911        echo '<div style="display:flex; gap:15px;">';
47439ccd446eSAtari911        echo '<label style="flex:1; padding:12px; border:2px solid ' . ($currentWeekStart === 'monday' ? '#00cc07' : $colors['border']) . '; border-radius:4px; background:' . ($currentWeekStart === 'monday' ? 'rgba(0, 204, 7, 0.05)' : $colors['bg']) . '; cursor:pointer; display:flex; align-items:center;">';
47449ccd446eSAtari911        echo '<input type="radio" name="week_start" value="monday" ' . ($currentWeekStart === 'monday' ? 'checked' : '') . ' style="margin-right:10px; width:18px; height:18px;">';
47459ccd446eSAtari911        echo '<div>';
47469ccd446eSAtari911        echo '<div style="font-weight:bold; color:' . $colors['text'] . '; margin-bottom:3px;">Monday</div>';
47479ccd446eSAtari911        echo '<div style="font-size:11px; color:' . $colors['text'] . ';">Week starts on Monday (ISO standard)</div>';
47489ccd446eSAtari911        echo '</div>';
47499ccd446eSAtari911        echo '</label>';
47509ccd446eSAtari911
47519ccd446eSAtari911        echo '<label style="flex:1; padding:12px; border:2px solid ' . ($currentWeekStart === 'sunday' ? '#00cc07' : $colors['border']) . '; border-radius:4px; background:' . ($currentWeekStart === 'sunday' ? 'rgba(0, 204, 7, 0.05)' : $colors['bg']) . '; cursor:pointer; display:flex; align-items:center;">';
47529ccd446eSAtari911        echo '<input type="radio" name="week_start" value="sunday" ' . ($currentWeekStart === 'sunday' ? 'checked' : '') . ' style="margin-right:10px; width:18px; height:18px;">';
47539ccd446eSAtari911        echo '<div>';
47549ccd446eSAtari911        echo '<div style="font-weight:bold; color:' . $colors['text'] . '; margin-bottom:3px;">Sunday</div>';
47559ccd446eSAtari911        echo '<div style="font-size:11px; color:' . $colors['text'] . ';">Week starts on Sunday (US/Canada standard)</div>';
47569ccd446eSAtari911        echo '</div>';
47579ccd446eSAtari911        echo '</label>';
47589ccd446eSAtari911        echo '</div>';
47599ccd446eSAtari911        echo '</div>';
47609ccd446eSAtari911
47619ccd446eSAtari911        // Visual Theme Section
47629ccd446eSAtari911        echo '<h3 style="margin:0 0 15px 0; color:' . $colors['text'] . '; font-size:16px;">�� Visual Theme</h3>';
47639ccd446eSAtari911
47649ccd446eSAtari911        // Matrix Theme
47659ccd446eSAtari911        echo '<div style="border:2px solid ' . ($currentTheme === 'matrix' ? '#00cc07' : $colors['border']) . '; border-radius:6px; padding:20px; margin-bottom:20px; background:' . ($currentTheme === 'matrix' ? 'rgba(0, 204, 7, 0.05)' : $colors['bg']) . ';">';
47669ccd446eSAtari911        echo '<label style="display:flex; align-items:center; cursor:pointer;">';
47679ccd446eSAtari911        echo '<input type="radio" name="theme" value="matrix" ' . ($currentTheme === 'matrix' ? 'checked' : '') . ' style="margin-right:12px; width:20px; height:20px;">';
47689ccd446eSAtari911        echo '<div style="flex:1;">';
47699ccd446eSAtari911        echo '<div style="font-size:18px; font-weight:bold; color:#00cc07; margin-bottom:8px;">�� Matrix Edition</div>';
47709ccd446eSAtari911        echo '<div style="color:' . $colors['text'] . '; margin-bottom:12px;">Dark green theme with Matrix-style glow effects and neon accents</div>';
47719ccd446eSAtari911        echo '<div style="display:inline-block; background:#242424; border:2px solid #00cc07; padding:8px 12px; border-radius:4px; font-size:11px; font-family:monospace; color:#00cc07; box-shadow:0 0 10px rgba(0, 204, 7, 0.3);">Preview: Matrix Theme</div>';
47729ccd446eSAtari911        echo '</div>';
47739ccd446eSAtari911        echo '</label>';
47749ccd446eSAtari911        echo '</div>';
47759ccd446eSAtari911
47769ccd446eSAtari911        // Purple Theme
47779ccd446eSAtari911        echo '<div style="border:2px solid ' . ($currentTheme === 'purple' ? '#9b59b6' : $colors['border']) . '; border-radius:6px; padding:20px; margin-bottom:20px; background:' . ($currentTheme === 'purple' ? 'rgba(155, 89, 182, 0.05)' : $colors['bg']) . ';">';
47789ccd446eSAtari911        echo '<label style="display:flex; align-items:center; cursor:pointer;">';
47799ccd446eSAtari911        echo '<input type="radio" name="theme" value="purple" ' . ($currentTheme === 'purple' ? 'checked' : '') . ' style="margin-right:12px; width:20px; height:20px;">';
47809ccd446eSAtari911        echo '<div style="flex:1;">';
47819ccd446eSAtari911        echo '<div style="font-size:18px; font-weight:bold; color:#9b59b6; margin-bottom:8px;">�� Purple Dream</div>';
47829ccd446eSAtari911        echo '<div style="color:' . $colors['text'] . '; margin-bottom:12px;">Rich purple theme with elegant violet accents and soft glow</div>';
47839ccd446eSAtari911        echo '<div style="display:inline-block; background:#2a2030; border:2px solid #9b59b6; padding:8px 12px; border-radius:4px; font-size:11px; font-family:monospace; color:#b19cd9; box-shadow:0 0 10px rgba(155, 89, 182, 0.3);">Preview: Purple Theme</div>';
47849ccd446eSAtari911        echo '</div>';
47859ccd446eSAtari911        echo '</label>';
47869ccd446eSAtari911        echo '</div>';
47879ccd446eSAtari911
47889ccd446eSAtari911        // Professional Blue Theme
47899ccd446eSAtari911        echo '<div style="border:2px solid ' . ($currentTheme === 'professional' ? '#4a90e2' : $colors['border']) . '; border-radius:6px; padding:20px; margin-bottom:20px; background:' . ($currentTheme === 'professional' ? 'rgba(74, 144, 226, 0.05)' : $colors['bg']) . ';">';
47909ccd446eSAtari911        echo '<label style="display:flex; align-items:center; cursor:pointer;">';
47919ccd446eSAtari911        echo '<input type="radio" name="theme" value="professional" ' . ($currentTheme === 'professional' ? 'checked' : '') . ' style="margin-right:12px; width:20px; height:20px;">';
47929ccd446eSAtari911        echo '<div style="flex:1;">';
47939ccd446eSAtari911        echo '<div style="font-size:18px; font-weight:bold; color:#4a90e2; margin-bottom:8px;">�� Professional Blue</div>';
47949ccd446eSAtari911        echo '<div style="color:' . $colors['text'] . '; margin-bottom:12px;">Clean blue and grey theme with modern professional styling, no glow effects</div>';
47959ccd446eSAtari911        echo '<div style="display:inline-block; background:#f5f7fa; border:2px solid #4a90e2; padding:8px 12px; border-radius:4px; font-size:11px; font-family:sans-serif; color:#2c3e50; box-shadow:0 2px 4px rgba(0, 0, 0, 0.1);">Preview: Professional Theme</div>';
47969ccd446eSAtari911        echo '</div>';
47979ccd446eSAtari911        echo '</label>';
47989ccd446eSAtari911        echo '</div>';
47999ccd446eSAtari911
48009ccd446eSAtari911        // Pink Bling Theme
48019ccd446eSAtari911        echo '<div style="border:2px solid ' . ($currentTheme === 'pink' ? '#ff1493' : $colors['border']) . '; border-radius:6px; padding:20px; margin-bottom:20px; background:' . ($currentTheme === 'pink' ? 'rgba(255, 20, 147, 0.05)' : $colors['bg']) . ';">';
48029ccd446eSAtari911        echo '<label style="display:flex; align-items:center; cursor:pointer;">';
48039ccd446eSAtari911        echo '<input type="radio" name="theme" value="pink" ' . ($currentTheme === 'pink' ? 'checked' : '') . ' style="margin-right:12px; width:20px; height:20px;">';
48049ccd446eSAtari911        echo '<div style="flex:1;">';
48059ccd446eSAtari911        echo '<div style="font-size:18px; font-weight:bold; color:#ff1493; margin-bottom:8px;">�� Pink Bling</div>';
48069ccd446eSAtari911        echo '<div style="color:' . $colors['text'] . '; margin-bottom:12px;">Glamorous hot pink theme with maximum sparkle, hearts, and diamonds ✨</div>';
48079ccd446eSAtari911        echo '<div style="display:inline-block; background:#1a0d14; border:2px solid #ff1493; padding:8px 12px; border-radius:4px; font-size:11px; font-family:monospace; color:#ff69b4; box-shadow:0 0 12px rgba(255, 20, 147, 0.6);">Preview: Pink Bling Theme ��</div>';
48089ccd446eSAtari911        echo '</div>';
48099ccd446eSAtari911        echo '</label>';
48109ccd446eSAtari911        echo '</div>';
48119ccd446eSAtari911
48129ccd446eSAtari911        // Wiki Default Theme
48139ccd446eSAtari911        echo '<div style="border:2px solid ' . ($currentTheme === 'wiki' ? '#2b73b7' : $colors['border']) . '; border-radius:6px; padding:20px; margin-bottom:20px; background:' . ($currentTheme === 'wiki' ? 'rgba(43, 115, 183, 0.05)' : $colors['bg']) . ';">';
48149ccd446eSAtari911        echo '<label style="display:flex; align-items:center; cursor:pointer;">';
48159ccd446eSAtari911        echo '<input type="radio" name="theme" value="wiki" ' . ($currentTheme === 'wiki' ? 'checked' : '') . ' style="margin-right:12px; width:20px; height:20px;">';
48169ccd446eSAtari911        echo '<div style="flex:1;">';
48179ccd446eSAtari911        echo '<div style="font-size:18px; font-weight:bold; color:#2b73b7; margin-bottom:8px;">�� Wiki Default</div>';
48189ccd446eSAtari911        echo '<div style="color:' . $colors['text'] . '; margin-bottom:12px;">Automatically matches your DokuWiki template theme using CSS variables - adapts to light and dark themes</div>';
48199ccd446eSAtari911        echo '<div style="display:inline-block; background:#f5f5f5; border:2px solid #ccc; padding:8px 12px; border-radius:4px; font-size:11px; font-family:sans-serif; color:' . $colors['text'] . '; box-shadow:0 1px 2px rgba(0, 0, 0, 0.1);">Preview: Matches Your Wiki Theme</div>';
48209ccd446eSAtari911        echo '</div>';
48219ccd446eSAtari911        echo '</label>';
48229ccd446eSAtari911        echo '</div>';
48239ccd446eSAtari911
48249ccd446eSAtari911        echo '<button type="submit" style="background:#00cc07; color:#fff; border:none; padding:12px 24px; border-radius:4px; font-size:14px; font-weight:bold; cursor:pointer; box-shadow:0 2px 4px rgba(0,0,0,0.2);">Save Settings</button>';
48259ccd446eSAtari911        echo '</form>';
48269ccd446eSAtari911    }
48279ccd446eSAtari911
48289ccd446eSAtari911    /**
48299ccd446eSAtari911     * Get current sidebar theme
48309ccd446eSAtari911     */
48319ccd446eSAtari911    private function getSidebarTheme() {
48329ccd446eSAtari911        $configFile = DOKU_INC . 'data/meta/calendar_theme.txt';
48339ccd446eSAtari911        if (file_exists($configFile)) {
48349ccd446eSAtari911            return trim(file_get_contents($configFile));
48359ccd446eSAtari911        }
48369ccd446eSAtari911        return 'matrix'; // Default
48379ccd446eSAtari911    }
48389ccd446eSAtari911
48399ccd446eSAtari911    /**
48409ccd446eSAtari911     * Save sidebar theme
48419ccd446eSAtari911     */
48429ccd446eSAtari911    private function saveSidebarTheme($theme) {
48439ccd446eSAtari911        $configFile = DOKU_INC . 'data/meta/calendar_theme.txt';
48449ccd446eSAtari911        $validThemes = ['matrix', 'purple', 'professional', 'pink', 'wiki'];
48459ccd446eSAtari911
48469ccd446eSAtari911        if (in_array($theme, $validThemes)) {
48479ccd446eSAtari911            file_put_contents($configFile, $theme);
48489ccd446eSAtari911            return true;
48499ccd446eSAtari911        }
48509ccd446eSAtari911        return false;
48519ccd446eSAtari911    }
48529ccd446eSAtari911
48539ccd446eSAtari911    /**
48549ccd446eSAtari911     * Get week start day
48559ccd446eSAtari911     */
48569ccd446eSAtari911    private function getWeekStartDay() {
48579ccd446eSAtari911        $configFile = DOKU_INC . 'data/meta/calendar_week_start.txt';
48589ccd446eSAtari911        if (file_exists($configFile)) {
48599ccd446eSAtari911            $start = trim(file_get_contents($configFile));
48609ccd446eSAtari911            if (in_array($start, ['monday', 'sunday'])) {
48619ccd446eSAtari911                return $start;
48629ccd446eSAtari911            }
48639ccd446eSAtari911        }
48649ccd446eSAtari911        return 'sunday'; // Default to Sunday (US/Canada standard)
48659ccd446eSAtari911    }
48669ccd446eSAtari911
48679ccd446eSAtari911    /**
48689ccd446eSAtari911     * Save week start day
48699ccd446eSAtari911     */
48709ccd446eSAtari911    private function saveWeekStartDay($weekStart) {
48719ccd446eSAtari911        $configFile = DOKU_INC . 'data/meta/calendar_week_start.txt';
48729ccd446eSAtari911        $validStarts = ['monday', 'sunday'];
48739ccd446eSAtari911
48749ccd446eSAtari911        if (in_array($weekStart, $validStarts)) {
48759ccd446eSAtari911            file_put_contents($configFile, $weekStart);
48769ccd446eSAtari911            return true;
48779ccd446eSAtari911        }
48789ccd446eSAtari911        return false;
48799ccd446eSAtari911    }
48809ccd446eSAtari911
48819ccd446eSAtari911    /**
48829ccd446eSAtari911     * Get colors from DokuWiki template's style.ini file
48839ccd446eSAtari911     */
48849ccd446eSAtari911    private function getTemplateColors() {
48859ccd446eSAtari911        global $conf;
48869ccd446eSAtari911
48879ccd446eSAtari911        // Get current template name
48889ccd446eSAtari911        $template = $conf['template'];
48899ccd446eSAtari911
48909ccd446eSAtari911        // Try multiple possible locations for style.ini
48919ccd446eSAtari911        $possiblePaths = [
48929ccd446eSAtari911            DOKU_INC . 'conf/tpl/' . $template . '/style.ini',
48939ccd446eSAtari911            DOKU_INC . 'lib/tpl/' . $template . '/style.ini',
48949ccd446eSAtari911        ];
48959ccd446eSAtari911
48969ccd446eSAtari911        $styleIni = null;
48979ccd446eSAtari911        foreach ($possiblePaths as $path) {
48989ccd446eSAtari911            if (file_exists($path)) {
48999ccd446eSAtari911                $styleIni = parse_ini_file($path, true);
49009ccd446eSAtari911                break;
49019ccd446eSAtari911            }
49029ccd446eSAtari911        }
49039ccd446eSAtari911
49049ccd446eSAtari911        if (!$styleIni || !isset($styleIni['replacements'])) {
49059ccd446eSAtari911            // Return defaults
49069ccd446eSAtari911            return [
49079ccd446eSAtari911                'bg' => '#fff',
49089ccd446eSAtari911                'bg_alt' => '#e8e8e8',
49099ccd446eSAtari911                'text' => '#333',
49109ccd446eSAtari911                'border' => '#ccc',
49119ccd446eSAtari911                'link' => '#2b73b7',
49129ccd446eSAtari911            ];
49139ccd446eSAtari911        }
49149ccd446eSAtari911
49159ccd446eSAtari911        $r = $styleIni['replacements'];
49169ccd446eSAtari911
49179ccd446eSAtari911        return [
49189ccd446eSAtari911            'bg' => isset($r['__background__']) ? $r['__background__'] : '#fff',
49199ccd446eSAtari911            'bg_alt' => isset($r['__background_alt__']) ? $r['__background_alt__'] : '#e8e8e8',
49209ccd446eSAtari911            'text' => isset($r['__text__']) ? $r['__text__'] : '#333',
49219ccd446eSAtari911            'border' => isset($r['__border__']) ? $r['__border__'] : '#ccc',
49229ccd446eSAtari911            'link' => isset($r['__link__']) ? $r['__link__'] : '#2b73b7',
49239ccd446eSAtari911        ];
49249ccd446eSAtari911    }
49251d05cddcSAtari911}
4926