xref: /plugin/calendar/admin.php (revision 9ccd446ecbe25932c2e89f7608c11495a1f1dbac)
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();
47*9ccd446eSAtari911        } elseif ($action === 'rename_namespace') {
48*9ccd446eSAtari911            $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();
61*9ccd446eSAtari911        } elseif ($action === 'create_manual_backup') {
62*9ccd446eSAtari911            $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();
831d05cddcSAtari911        }
841d05cddcSAtari911    }
851d05cddcSAtari911
861d05cddcSAtari911    public function html() {
871d05cddcSAtari911        global $INPUT;
881d05cddcSAtari911
89*9ccd446eSAtari911        // Get current tab - default to 'manage' (Manage Events tab)
90*9ccd446eSAtari911        $tab = $INPUT->str('tab', 'manage');
911d05cddcSAtari911
92*9ccd446eSAtari911        // Get template colors
93*9ccd446eSAtari911        $colors = $this->getTemplateColors();
94*9ccd446eSAtari911        $accentColor = '#00cc07'; // Keep calendar plugin accent color
95*9ccd446eSAtari911
96*9ccd446eSAtari911        // Tab navigation (Manage Events, Update Plugin, Outlook Sync, Themes)
97*9ccd446eSAtari911        echo '<div style="border-bottom:2px solid ' . $colors['border'] . '; margin:10px 0 15px 0;">';
98*9ccd446eSAtari911        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>';
99*9ccd446eSAtari911        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>';
100*9ccd446eSAtari911        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>';
101*9ccd446eSAtari911        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>';
1021d05cddcSAtari911        echo '</div>';
1031d05cddcSAtari911
1041d05cddcSAtari911        // Render appropriate tab
1051d05cddcSAtari911        if ($tab === 'config') {
106*9ccd446eSAtari911            $this->renderConfigTab($colors);
1071d05cddcSAtari911        } elseif ($tab === 'manage') {
108*9ccd446eSAtari911            $this->renderManageTab($colors);
109*9ccd446eSAtari911        } elseif ($tab === 'themes') {
110*9ccd446eSAtari911            $this->renderThemesTab($colors);
1111d05cddcSAtari911        } else {
112*9ccd446eSAtari911            $this->renderUpdateTab($colors);
1131d05cddcSAtari911        }
1141d05cddcSAtari911    }
1151d05cddcSAtari911
116*9ccd446eSAtari911    private function renderConfigTab($colors = null) {
1171d05cddcSAtari911        global $INPUT;
1181d05cddcSAtari911
119*9ccd446eSAtari911        // Use defaults if not provided
120*9ccd446eSAtari911        if ($colors === null) {
121*9ccd446eSAtari911            $colors = $this->getTemplateColors();
122*9ccd446eSAtari911        }
123*9ccd446eSAtari911
1241d05cddcSAtari911        // Load current config
1251d05cddcSAtari911        $configFile = DOKU_PLUGIN . 'calendar/sync_config.php';
1261d05cddcSAtari911        $config = [];
1271d05cddcSAtari911        if (file_exists($configFile)) {
1281d05cddcSAtari911            $config = include $configFile;
1291d05cddcSAtari911        }
1301d05cddcSAtari911
1311d05cddcSAtari911        // Show message if present
1321d05cddcSAtari911        if ($INPUT->has('msg')) {
1331d05cddcSAtari911            $msg = hsc($INPUT->str('msg'));
1341d05cddcSAtari911            $type = $INPUT->str('msgtype', 'success');
1351d05cddcSAtari911            $class = ($type === 'success') ? 'msg success' : 'msg error';
1361d05cddcSAtari911            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;\">";
1371d05cddcSAtari911            echo $msg;
1381d05cddcSAtari911            echo "</div>";
1391d05cddcSAtari911        }
1401d05cddcSAtari911
1411d05cddcSAtari911        echo '<h2 style="margin:10px 0; font-size:20px;">Outlook Sync Configuration</h2>';
1421d05cddcSAtari911
1431d05cddcSAtari911        // Import/Export buttons
1441d05cddcSAtari911        echo '<div style="display:flex; gap:10px; margin-bottom:15px;">';
1451d05cddcSAtari911        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>';
1461d05cddcSAtari911        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>';
1471d05cddcSAtari911        echo '<input type="file" id="importFileInput" accept=".enc" style="display:none;" onchange="importConfig(this)">';
1481d05cddcSAtari911        echo '<span id="importStatus" style="margin-left:10px; font-size:12px;"></span>';
1491d05cddcSAtari911        echo '</div>';
1501d05cddcSAtari911
1511d05cddcSAtari911        echo '<form method="post" action="?do=admin&page=calendar" style="max-width:900px;">';
1521d05cddcSAtari911        echo '<input type="hidden" name="action" value="save_config">';
1531d05cddcSAtari911
1541d05cddcSAtari911        // Azure Credentials
155*9ccd446eSAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px;">';
1561d05cddcSAtari911        echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">Microsoft Azure App Credentials</h3>';
157*9ccd446eSAtari911        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>';
1581d05cddcSAtari911
1591d05cddcSAtari911        echo '<label style="display:block; font-weight:bold; margin:8px 0 3px; font-size:13px;">Tenant ID</label>';
160*9ccd446eSAtari911        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;">';
1611d05cddcSAtari911
1621d05cddcSAtari911        echo '<label style="display:block; font-weight:bold; margin:8px 0 3px; font-size:13px;">Client ID (Application ID)</label>';
163*9ccd446eSAtari911        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;">';
1641d05cddcSAtari911
1651d05cddcSAtari911        echo '<label style="display:block; font-weight:bold; margin:8px 0 3px; font-size:13px;">Client Secret</label>';
166*9ccd446eSAtari911        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;">';
1671d05cddcSAtari911        echo '<p style="color:#999; font-size:0.8em; margin:3px 0 0;">⚠️ Keep this secret safe!</p>';
1681d05cddcSAtari911        echo '</div>';
1691d05cddcSAtari911
1701d05cddcSAtari911        // Outlook Settings
171*9ccd446eSAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px;">';
1721d05cddcSAtari911        echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">Outlook Settings</h3>';
1731d05cddcSAtari911
1741d05cddcSAtari911        echo '<div style="display:grid; grid-template-columns:1fr 1fr; gap:10px;">';
1751d05cddcSAtari911
1761d05cddcSAtari911        echo '<div>';
1771d05cddcSAtari911        echo '<label style="display:block; font-weight:bold; margin:0 0 3px; font-size:13px;">User Email</label>';
178*9ccd446eSAtari911        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;">';
1791d05cddcSAtari911        echo '</div>';
1801d05cddcSAtari911
1811d05cddcSAtari911        echo '<div>';
1821d05cddcSAtari911        echo '<label style="display:block; font-weight:bold; margin:0 0 3px; font-size:13px;">Timezone</label>';
183*9ccd446eSAtari911        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;">';
1841d05cddcSAtari911        echo '</div>';
1851d05cddcSAtari911
1861d05cddcSAtari911        echo '<div>';
1871d05cddcSAtari911        echo '<label style="display:block; font-weight:bold; margin:0 0 3px; font-size:13px;">Default Category</label>';
188*9ccd446eSAtari911        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;">';
1891d05cddcSAtari911        echo '</div>';
1901d05cddcSAtari911
1911d05cddcSAtari911        echo '<div>';
1921d05cddcSAtari911        echo '<label style="display:block; font-weight:bold; margin:0 0 3px; font-size:13px;">Reminder (minutes)</label>';
193*9ccd446eSAtari911        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;">';
1941d05cddcSAtari911        echo '</div>';
1951d05cddcSAtari911
1961d05cddcSAtari911        echo '</div>'; // end grid
1971d05cddcSAtari911        echo '</div>';
1981d05cddcSAtari911
1991d05cddcSAtari911        // Important Namespaces for Sidebar Widget
200*9ccd446eSAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #9b59b6; border-radius:3px;">';
2011d05cddcSAtari911        echo '<h3 style="margin:0 0 8px 0; color:#9b59b6; font-size:16px;">�� Important Namespaces (Sidebar Widget)</h3>';
202*9ccd446eSAtari911        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</p>';
203*9ccd446eSAtari911        echo '<input type="text" name="important_namespaces" value="' . hsc(isset($config['important_namespaces']) ? $config['important_namespaces'] : 'important') . '" style="width:100%; padding:6px; border:1px solid ' . $colors['border'] . '; border-radius:3px; font-size:12px;" placeholder="important,urgent,priority">';
204*9ccd446eSAtari911        echo '<p style="color:' . $colors['text'] . '; font-size:10px; margin:4px 0 0;">Comma-separated list of namespace names</p>';
2051d05cddcSAtari911        echo '</div>';
2061d05cddcSAtari911
2071d05cddcSAtari911        // Sync Options
208*9ccd446eSAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px;">';
2091d05cddcSAtari911        echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">Sync Options</h3>';
2101d05cddcSAtari911
2111d05cddcSAtari911        $syncCompleted = isset($config['sync_completed_tasks']) ? $config['sync_completed_tasks'] : false;
2121d05cddcSAtari911        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>';
2131d05cddcSAtari911
2141d05cddcSAtari911        $deleteOutlook = isset($config['delete_outlook_events']) ? $config['delete_outlook_events'] : true;
2151d05cddcSAtari911        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>';
2161d05cddcSAtari911
2171d05cddcSAtari911        $syncAll = isset($config['sync_all_namespaces']) ? $config['sync_all_namespaces'] : true;
2181d05cddcSAtari911        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>';
2191d05cddcSAtari911
2201d05cddcSAtari911        // Namespace selection (shown when sync_all is unchecked)
2211d05cddcSAtari911        echo '<div id="namespace_selection" style="margin-top:10px; ' . ($syncAll ? 'display:none;' : '') . '">';
2221d05cddcSAtari911        echo '<label style="display:block; font-weight:bold; margin-bottom:5px; font-size:13px;">Select namespaces to sync:</label>';
2231d05cddcSAtari911
2241d05cddcSAtari911        // Get available namespaces
2251d05cddcSAtari911        $availableNamespaces = $this->getAllNamespaces();
2261d05cddcSAtari911        $selectedNamespaces = isset($config['sync_namespaces']) ? $config['sync_namespaces'] : [];
2271d05cddcSAtari911
228*9ccd446eSAtari911        echo '<div style="max-height:150px; overflow-y:auto; border:1px solid ' . $colors['border'] . '; border-radius:3px; padding:8px; background:' . $colors['bg'] . ';">';
2291d05cddcSAtari911        echo '<label style="display:block; margin:3px 0;"><input type="checkbox" name="sync_namespaces[]" value=""> (default)</label>';
2301d05cddcSAtari911        foreach ($availableNamespaces as $ns) {
2311d05cddcSAtari911            if ($ns !== '') {
2321d05cddcSAtari911                $checked = in_array($ns, $selectedNamespaces) ? 'checked' : '';
2331d05cddcSAtari911                echo '<label style="display:block; margin:3px 0;"><input type="checkbox" name="sync_namespaces[]" value="' . hsc($ns) . '" ' . $checked . '> ' . hsc($ns) . '</label>';
2341d05cddcSAtari911            }
2351d05cddcSAtari911        }
2361d05cddcSAtari911        echo '</div>';
2371d05cddcSAtari911        echo '</div>';
2381d05cddcSAtari911
2391d05cddcSAtari911        echo '<script>
2401d05cddcSAtari911        function toggleNamespaceSelection(checkbox) {
2411d05cddcSAtari911            document.getElementById("namespace_selection").style.display = checkbox.checked ? "none" : "block";
2421d05cddcSAtari911        }
2431d05cddcSAtari911        </script>';
2441d05cddcSAtari911
2451d05cddcSAtari911        echo '</div>';
2461d05cddcSAtari911
2471d05cddcSAtari911        // Namespace and Color Mapping - Side by Side
2481d05cddcSAtari911        echo '<div style="display:grid; grid-template-columns:1fr 1fr; gap:10px; margin:10px 0;">';
2491d05cddcSAtari911
2501d05cddcSAtari911        // Namespace Mapping
251*9ccd446eSAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:12px; border-left:3px solid #00cc07; border-radius:3px;">';
2521d05cddcSAtari911        echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">Namespace → Category</h3>';
253*9ccd446eSAtari911        echo '<p style="color:' . $colors['text'] . '; font-size:0.8em; margin:0 0 5px;">One per line: namespace=Category</p>';
254*9ccd446eSAtari911        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">';
2551d05cddcSAtari911        if (isset($config['category_mapping']) && is_array($config['category_mapping'])) {
2561d05cddcSAtari911            foreach ($config['category_mapping'] as $ns => $cat) {
2571d05cddcSAtari911                echo hsc($ns) . '=' . hsc($cat) . "\n";
2581d05cddcSAtari911            }
2591d05cddcSAtari911        }
2601d05cddcSAtari911        echo '</textarea>';
2611d05cddcSAtari911        echo '</div>';
2621d05cddcSAtari911
2631d05cddcSAtari911        // Color Mapping with Color Picker
264*9ccd446eSAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:12px; border-left:3px solid #00cc07; border-radius:3px;">';
2651d05cddcSAtari911        echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">�� Event Color → Category</h3>';
266*9ccd446eSAtari911        echo '<p style="color:' . $colors['text'] . '; font-size:0.8em; margin:0 0 8px;">Map calendar colors to Outlook categories</p>';
2671d05cddcSAtari911
2681d05cddcSAtari911        // Define calendar colors and Outlook categories (only the main 6 colors)
2691d05cddcSAtari911        $calendarColors = [
2701d05cddcSAtari911            '#3498db' => 'Blue',
2711d05cddcSAtari911            '#2ecc71' => 'Green',
2721d05cddcSAtari911            '#e74c3c' => 'Red',
2731d05cddcSAtari911            '#f39c12' => 'Orange',
2741d05cddcSAtari911            '#9b59b6' => 'Purple',
2751d05cddcSAtari911            '#1abc9c' => 'Teal'
2761d05cddcSAtari911        ];
2771d05cddcSAtari911
2781d05cddcSAtari911        $outlookCategories = [
2791d05cddcSAtari911            'Blue category',
2801d05cddcSAtari911            'Green category',
2811d05cddcSAtari911            'Orange category',
2821d05cddcSAtari911            'Red category',
2831d05cddcSAtari911            'Yellow category',
2841d05cddcSAtari911            'Purple category'
2851d05cddcSAtari911        ];
2861d05cddcSAtari911
2871d05cddcSAtari911        // Load existing color mappings
2881d05cddcSAtari911        $existingMappings = isset($config['color_mapping']) && is_array($config['color_mapping'])
2891d05cddcSAtari911            ? $config['color_mapping']
2901d05cddcSAtari911            : [];
2911d05cddcSAtari911
2921d05cddcSAtari911        // Display color mapping rows
2931d05cddcSAtari911        echo '<div id="colorMappings" style="max-height:200px; overflow-y:auto;">';
2941d05cddcSAtari911
2951d05cddcSAtari911        $rowIndex = 0;
2961d05cddcSAtari911        foreach ($calendarColors as $hexColor => $colorName) {
2971d05cddcSAtari911            $selectedCategory = isset($existingMappings[$hexColor]) ? $existingMappings[$hexColor] : '';
2981d05cddcSAtari911
2991d05cddcSAtari911            echo '<div style="display:flex; gap:8px; align-items:center; margin-bottom:6px;">';
3001d05cddcSAtari911
3011d05cddcSAtari911            // Color preview box
3021d05cddcSAtari911            echo '<div style="width:24px; height:24px; background:' . $hexColor . '; border:2px solid #ddd; border-radius:3px; flex-shrink:0;"></div>';
3031d05cddcSAtari911
3041d05cddcSAtari911            // Color name
305*9ccd446eSAtari911            echo '<span style="font-size:12px; min-width:90px; color:' . $colors['text'] . ';">' . $colorName . '</span>';
3061d05cddcSAtari911
3071d05cddcSAtari911            // Arrow
3081d05cddcSAtari911            echo '<span style="color:#999; font-size:12px;">→</span>';
3091d05cddcSAtari911
3101d05cddcSAtari911            // Outlook category dropdown
311*9ccd446eSAtari911            echo '<select name="color_map_' . $rowIndex . '" style="flex:1; padding:4px; border:1px solid ' . $colors['border'] . '; border-radius:3px; font-size:12px;">';
3121d05cddcSAtari911            echo '<option value="">-- None --</option>';
3131d05cddcSAtari911            foreach ($outlookCategories as $category) {
3141d05cddcSAtari911                $selected = ($selectedCategory === $category) ? 'selected' : '';
3151d05cddcSAtari911                echo '<option value="' . hsc($category) . '" ' . $selected . '>' . hsc($category) . '</option>';
3161d05cddcSAtari911            }
3171d05cddcSAtari911            echo '</select>';
3181d05cddcSAtari911
3191d05cddcSAtari911            // Hidden input for the hex color
3201d05cddcSAtari911            echo '<input type="hidden" name="color_hex_' . $rowIndex . '" value="' . $hexColor . '">';
3211d05cddcSAtari911
3221d05cddcSAtari911            echo '</div>';
3231d05cddcSAtari911            $rowIndex++;
3241d05cddcSAtari911        }
3251d05cddcSAtari911
3261d05cddcSAtari911        echo '</div>';
3271d05cddcSAtari911
3281d05cddcSAtari911        // Hidden input to track number of color mappings
3291d05cddcSAtari911        echo '<input type="hidden" name="color_mapping_count" value="' . $rowIndex . '">';
3301d05cddcSAtari911
3311d05cddcSAtari911        echo '</div>';
3321d05cddcSAtari911
3331d05cddcSAtari911        echo '</div>'; // end grid
3341d05cddcSAtari911
3351d05cddcSAtari911        // Submit button
3361d05cddcSAtari911        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>';
3371d05cddcSAtari911        echo '</form>';
3381d05cddcSAtari911
3391d05cddcSAtari911        // JavaScript for Import/Export
3401d05cddcSAtari911        echo '<script>
3411d05cddcSAtari911        async function exportConfig() {
3421d05cddcSAtari911            try {
3431d05cddcSAtari911                const response = await fetch("?do=admin&page=calendar&action=export_config&call=ajax", {
3441d05cddcSAtari911                    method: "POST"
3451d05cddcSAtari911                });
3461d05cddcSAtari911                const data = await response.json();
3471d05cddcSAtari911
3481d05cddcSAtari911                if (data.success) {
3491d05cddcSAtari911                    // Create download link
3501d05cddcSAtari911                    const blob = new Blob([data.encrypted], {type: "application/octet-stream"});
3511d05cddcSAtari911                    const url = URL.createObjectURL(blob);
3521d05cddcSAtari911                    const a = document.createElement("a");
3531d05cddcSAtari911                    a.href = url;
3541d05cddcSAtari911                    a.download = "sync_config_" + new Date().toISOString().split("T")[0] + ".enc";
3551d05cddcSAtari911                    document.body.appendChild(a);
3561d05cddcSAtari911                    a.click();
3571d05cddcSAtari911                    document.body.removeChild(a);
3581d05cddcSAtari911                    URL.revokeObjectURL(url);
3591d05cddcSAtari911
3601d05cddcSAtari911                    alert("✅ Config exported successfully!\\n\\n⚠️ This file contains encrypted credentials.\\nKeep it secure!");
3611d05cddcSAtari911                } else {
3621d05cddcSAtari911                    alert("❌ Export failed: " + data.message);
3631d05cddcSAtari911                }
3641d05cddcSAtari911            } catch (error) {
3651d05cddcSAtari911                alert("❌ Error: " + error.message);
3661d05cddcSAtari911            }
3671d05cddcSAtari911        }
3681d05cddcSAtari911
3691d05cddcSAtari911        async function importConfig(input) {
3701d05cddcSAtari911            const file = input.files[0];
3711d05cddcSAtari911            if (!file) return;
3721d05cddcSAtari911
3731d05cddcSAtari911            const status = document.getElementById("importStatus");
3741d05cddcSAtari911            status.textContent = "⏳ Importing...";
3751d05cddcSAtari911            status.style.color = "#00cc07";
3761d05cddcSAtari911
3771d05cddcSAtari911            try {
3781d05cddcSAtari911                const encrypted = await file.text();
3791d05cddcSAtari911
3801d05cddcSAtari911                const formData = new FormData();
3811d05cddcSAtari911                formData.append("encrypted_config", encrypted);
3821d05cddcSAtari911
3831d05cddcSAtari911                const response = await fetch("?do=admin&page=calendar&action=import_config&call=ajax", {
3841d05cddcSAtari911                    method: "POST",
3851d05cddcSAtari911                    body: formData
3861d05cddcSAtari911                });
3871d05cddcSAtari911                const data = await response.json();
3881d05cddcSAtari911
3891d05cddcSAtari911                if (data.success) {
3901d05cddcSAtari911                    status.textContent = "✅ Import successful! Reloading...";
3911d05cddcSAtari911                    status.style.color = "#28a745";
3921d05cddcSAtari911                    setTimeout(() => {
3931d05cddcSAtari911                        window.location.reload();
3941d05cddcSAtari911                    }, 1500);
3951d05cddcSAtari911                } else {
3961d05cddcSAtari911                    status.textContent = "❌ Import failed: " + data.message;
3971d05cddcSAtari911                    status.style.color = "#dc3545";
3981d05cddcSAtari911                }
3991d05cddcSAtari911            } catch (error) {
4001d05cddcSAtari911                status.textContent = "❌ Error: " + error.message;
4011d05cddcSAtari911                status.style.color = "#dc3545";
4021d05cddcSAtari911            }
4031d05cddcSAtari911
4041d05cddcSAtari911            // Reset file input
4051d05cddcSAtari911            input.value = "";
4061d05cddcSAtari911        }
4071d05cddcSAtari911        </script>';
4081d05cddcSAtari911
4091d05cddcSAtari911        // Sync Controls Section
410*9ccd446eSAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:15px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:900px;">';
4111d05cddcSAtari911        echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">�� Sync Controls</h3>';
4121d05cddcSAtari911
4131d05cddcSAtari911        // Check cron job status
4141d05cddcSAtari911        $cronStatus = $this->getCronStatus();
4151d05cddcSAtari911
4161d05cddcSAtari911        // Check log file permissions
4171d05cddcSAtari911        $logFile = DOKU_PLUGIN . 'calendar/sync.log';
4181d05cddcSAtari911        $logWritable = is_writable($logFile) || is_writable(dirname($logFile));
4191d05cddcSAtari911
4201d05cddcSAtari911        echo '<div style="display:flex; gap:10px; align-items:center; margin-bottom:10px;">';
4211d05cddcSAtari911        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>';
4221d05cddcSAtari911        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>';
4231d05cddcSAtari911
4241d05cddcSAtari911        if ($cronStatus['active']) {
425*9ccd446eSAtari911            echo '<span style="color:' . $colors['text'] . '; font-size:12px;">⏰ ' . hsc($cronStatus['frequency']) . '</span>';
4261d05cddcSAtari911        } else {
4271d05cddcSAtari911            echo '<span style="color:#999; font-size:12px;">⚠️ No cron job detected</span>';
4281d05cddcSAtari911        }
4291d05cddcSAtari911
430*9ccd446eSAtari911        echo '<span id="syncStatus" style="color:' . $colors['text'] . '; font-size:12px; margin-left:auto;"></span>';
4311d05cddcSAtari911        echo '</div>';
4321d05cddcSAtari911
4331d05cddcSAtari911        // Show permission warning if log not writable
4341d05cddcSAtari911        if (!$logWritable) {
4351d05cddcSAtari911            echo '<div style="background:#fff3e0; border-left:3px solid #ff9800; padding:8px; margin:8px 0; border-radius:3px;">';
4361d05cddcSAtari911            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>';
4371d05cddcSAtari911            echo '</div>';
4381d05cddcSAtari911        }
4391d05cddcSAtari911
4401d05cddcSAtari911        // Show debug info if cron detected
4411d05cddcSAtari911        if ($cronStatus['active'] && !empty($cronStatus['full_line'])) {
4421d05cddcSAtari911            echo '<details style="margin-top:5px;">';
4431d05cddcSAtari911            echo '<summary style="cursor:pointer; color:#999; font-size:11px;">Show cron details</summary>';
4441d05cddcSAtari911            echo '<pre style="background:#f0f0f0; padding:8px; border-radius:3px; font-size:10px; margin:5px 0; overflow-x:auto;">' . hsc($cronStatus['full_line']) . '</pre>';
4451d05cddcSAtari911            echo '</details>';
4461d05cddcSAtari911        }
4471d05cddcSAtari911
4481d05cddcSAtari911        if (!$cronStatus['active']) {
4491d05cddcSAtari911            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>';
4501d05cddcSAtari911        }
4511d05cddcSAtari911
4521d05cddcSAtari911        echo '</div>';
4531d05cddcSAtari911
4541d05cddcSAtari911        // JavaScript for Run Sync Now
4551d05cddcSAtari911        echo '<script>
4561d05cddcSAtari911        let syncAbortController = null;
4571d05cddcSAtari911
4581d05cddcSAtari911        function runSyncNow() {
4591d05cddcSAtari911            const btn = document.getElementById("syncBtn");
4601d05cddcSAtari911            const stopBtn = document.getElementById("stopBtn");
4611d05cddcSAtari911            const status = document.getElementById("syncStatus");
4621d05cddcSAtari911
4631d05cddcSAtari911            btn.disabled = true;
4641d05cddcSAtari911            btn.style.display = "none";
4651d05cddcSAtari911            stopBtn.style.display = "inline-block";
4661d05cddcSAtari911            btn.textContent = "⏳ Running...";
4671d05cddcSAtari911            btn.style.background = "#999";
4681d05cddcSAtari911            status.textContent = "Starting sync...";
4691d05cddcSAtari911            status.style.color = "#00cc07";
4701d05cddcSAtari911
4711d05cddcSAtari911            // Create abort controller for this sync
4721d05cddcSAtari911            syncAbortController = new AbortController();
4731d05cddcSAtari911
4741d05cddcSAtari911            fetch("?do=admin&page=calendar&action=run_sync&call=ajax", {
4751d05cddcSAtari911                method: "POST",
4761d05cddcSAtari911                signal: syncAbortController.signal
4771d05cddcSAtari911            })
4781d05cddcSAtari911                .then(response => response.json())
4791d05cddcSAtari911                .then(data => {
4801d05cddcSAtari911                    if (data.success) {
4811d05cddcSAtari911                        status.textContent = "✅ " + data.message;
4821d05cddcSAtari911                        status.style.color = "#28a745";
4831d05cddcSAtari911                    } else {
4841d05cddcSAtari911                        status.textContent = "❌ " + data.message;
4851d05cddcSAtari911                        status.style.color = "#dc3545";
4861d05cddcSAtari911                    }
4871d05cddcSAtari911                    btn.disabled = false;
4881d05cddcSAtari911                    btn.style.display = "inline-block";
4891d05cddcSAtari911                    stopBtn.style.display = "none";
4901d05cddcSAtari911                    btn.textContent = "▶️ Run Sync Now";
4911d05cddcSAtari911                    btn.style.background = "#00cc07";
4921d05cddcSAtari911                    syncAbortController = null;
4931d05cddcSAtari911
4941d05cddcSAtari911                    // Clear status after 10 seconds
4951d05cddcSAtari911                    setTimeout(() => {
4961d05cddcSAtari911                        status.textContent = "";
4971d05cddcSAtari911                    }, 10000);
4981d05cddcSAtari911                })
4991d05cddcSAtari911                .catch(error => {
5001d05cddcSAtari911                    if (error.name === "AbortError") {
5011d05cddcSAtari911                        status.textContent = "⏹️ Sync stopped by user";
5021d05cddcSAtari911                        status.style.color = "#ff9800";
5031d05cddcSAtari911                    } else {
5041d05cddcSAtari911                        status.textContent = "❌ Error: " + error.message;
5051d05cddcSAtari911                        status.style.color = "#dc3545";
5061d05cddcSAtari911                    }
5071d05cddcSAtari911                    btn.disabled = false;
5081d05cddcSAtari911                    btn.style.display = "inline-block";
5091d05cddcSAtari911                    stopBtn.style.display = "none";
5101d05cddcSAtari911                    btn.textContent = "▶️ Run Sync Now";
5111d05cddcSAtari911                    btn.style.background = "#00cc07";
5121d05cddcSAtari911                    syncAbortController = null;
5131d05cddcSAtari911                });
5141d05cddcSAtari911        }
5151d05cddcSAtari911
5161d05cddcSAtari911        function stopSyncNow() {
5171d05cddcSAtari911            const status = document.getElementById("syncStatus");
5181d05cddcSAtari911
5191d05cddcSAtari911            status.textContent = "⏹️ Sending stop signal...";
5201d05cddcSAtari911            status.style.color = "#ff9800";
5211d05cddcSAtari911
5221d05cddcSAtari911            // First, send stop signal to server
5231d05cddcSAtari911            fetch("?do=admin&page=calendar&action=stop_sync&call=ajax", {
5241d05cddcSAtari911                method: "POST"
5251d05cddcSAtari911            })
5261d05cddcSAtari911            .then(response => response.json())
5271d05cddcSAtari911            .then(data => {
5281d05cddcSAtari911                if (data.success) {
5291d05cddcSAtari911                    status.textContent = "⏹️ Stop signal sent - sync will abort soon";
5301d05cddcSAtari911                    status.style.color = "#ff9800";
5311d05cddcSAtari911                } else {
5321d05cddcSAtari911                    status.textContent = "⚠️ " + data.message;
5331d05cddcSAtari911                    status.style.color = "#ff9800";
5341d05cddcSAtari911                }
5351d05cddcSAtari911            })
5361d05cddcSAtari911            .catch(error => {
5371d05cddcSAtari911                status.textContent = "⚠️ Error sending stop signal: " + error.message;
5381d05cddcSAtari911                status.style.color = "#ff9800";
5391d05cddcSAtari911            });
5401d05cddcSAtari911
5411d05cddcSAtari911            // Also abort the fetch request
5421d05cddcSAtari911            if (syncAbortController) {
5431d05cddcSAtari911                syncAbortController.abort();
5441d05cddcSAtari911                status.textContent = "⏹️ Stopping sync...";
5451d05cddcSAtari911                status.style.color = "#ff9800";
5461d05cddcSAtari911            }
5471d05cddcSAtari911        }
5481d05cddcSAtari911        </script>';
5491d05cddcSAtari911
5501d05cddcSAtari911        // Log Viewer Section - More Compact
551*9ccd446eSAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:15px 0 10px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:900px;">';
5521d05cddcSAtari911        echo '<h3 style="margin:0 0 5px 0; color:#00cc07; font-size:16px;">�� Live Sync Log</h3>';
553*9ccd446eSAtari911        echo '<p style="color:' . $colors['text'] . '; font-size:0.8em; margin:0 0 8px;">Updates every 2 seconds</p>';
5541d05cddcSAtari911
5551d05cddcSAtari911        // Log viewer container
5561d05cddcSAtari911        echo '<div style="background:#1e1e1e; border-radius:5px; overflow:hidden; box-shadow:0 2px 4px rgba(0,0,0,0.3);">';
5571d05cddcSAtari911
5581d05cddcSAtari911        // Log header - More compact
5591d05cddcSAtari911        echo '<div style="background:#2d2d2d; padding:6px 10px; display:flex; justify-content:space-between; align-items:center; border-bottom:1px solid #444;">';
5601d05cddcSAtari911        echo '<span style="color:#00cc07; font-family:monospace; font-weight:bold; font-size:12px;">sync.log</span>';
5611d05cddcSAtari911        echo '<div>';
5621d05cddcSAtari911        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>';
5631d05cddcSAtari911        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>';
5641d05cddcSAtari911        echo '<button onclick="downloadLog()" style="background:#666; color:white; border:none; padding:4px 8px; border-radius:3px; cursor:pointer; font-size:11px;">�� Download</button>';
5651d05cddcSAtari911        echo '</div>';
5661d05cddcSAtari911        echo '</div>';
5671d05cddcSAtari911
5681d05cddcSAtari911        // Log content - Reduced height to 250px
5691d05cddcSAtari911        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>';
5701d05cddcSAtari911
5711d05cddcSAtari911        echo '</div>';
5721d05cddcSAtari911        echo '</div>';
5731d05cddcSAtari911
5741d05cddcSAtari911        // JavaScript for log viewer
5751d05cddcSAtari911        echo '<script>
5761d05cddcSAtari911        let refreshInterval = null;
5771d05cddcSAtari911        let isPaused = false;
5781d05cddcSAtari911
5791d05cddcSAtari911        function refreshLog() {
5801d05cddcSAtari911            if (isPaused) return;
5811d05cddcSAtari911
5821d05cddcSAtari911            fetch("?do=admin&page=calendar&action=get_log&call=ajax")
5831d05cddcSAtari911                .then(response => response.json())
5841d05cddcSAtari911                .then(data => {
5851d05cddcSAtari911                    const logContent = document.getElementById("logContent");
5861d05cddcSAtari911                    if (logContent) {
5871d05cddcSAtari911                        logContent.textContent = data.log || "No log data available";
5881d05cddcSAtari911                        logContent.scrollTop = logContent.scrollHeight;
5891d05cddcSAtari911                    }
5901d05cddcSAtari911                })
5911d05cddcSAtari911                .catch(error => {
5921d05cddcSAtari911                    console.error("Error fetching log:", error);
5931d05cddcSAtari911                });
5941d05cddcSAtari911        }
5951d05cddcSAtari911
5961d05cddcSAtari911        function togglePause() {
5971d05cddcSAtari911            isPaused = !isPaused;
5981d05cddcSAtari911            const btn = document.getElementById("pauseBtn");
5991d05cddcSAtari911            if (isPaused) {
6001d05cddcSAtari911                btn.textContent = "▶ Resume";
6011d05cddcSAtari911                btn.style.background = "#00cc07";
6021d05cddcSAtari911            } else {
6031d05cddcSAtari911                btn.textContent = "⏸ Pause";
6041d05cddcSAtari911                btn.style.background = "#666";
6051d05cddcSAtari911                refreshLog();
6061d05cddcSAtari911            }
6071d05cddcSAtari911        }
6081d05cddcSAtari911
6091d05cddcSAtari911        function clearLog() {
6101d05cddcSAtari911            if (!confirm("Clear the sync log file?\\n\\nThis will delete all log entries.")) {
6111d05cddcSAtari911                return;
6121d05cddcSAtari911            }
6131d05cddcSAtari911
6141d05cddcSAtari911            fetch("?do=admin&page=calendar&action=clear_log&call=ajax", {
6151d05cddcSAtari911                method: "POST"
6161d05cddcSAtari911            })
6171d05cddcSAtari911                .then(response => response.json())
6181d05cddcSAtari911                .then(data => {
6191d05cddcSAtari911                    if (data.success) {
6201d05cddcSAtari911                        refreshLog();
6211d05cddcSAtari911                        alert("Log cleared successfully");
6221d05cddcSAtari911                    } else {
6231d05cddcSAtari911                        alert("Error clearing log: " + data.message);
6241d05cddcSAtari911                    }
6251d05cddcSAtari911                })
6261d05cddcSAtari911                .catch(error => {
6271d05cddcSAtari911                    alert("Error: " + error.message);
6281d05cddcSAtari911                });
6291d05cddcSAtari911        }
6301d05cddcSAtari911
6311d05cddcSAtari911        function downloadLog() {
6321d05cddcSAtari911            window.location.href = "?do=admin&page=calendar&action=download_log";
6331d05cddcSAtari911        }
6341d05cddcSAtari911
6351d05cddcSAtari911        // Start auto-refresh
6361d05cddcSAtari911        refreshLog();
6371d05cddcSAtari911        refreshInterval = setInterval(refreshLog, 2000);
6381d05cddcSAtari911
6391d05cddcSAtari911        // Cleanup on page unload
6401d05cddcSAtari911        window.addEventListener("beforeunload", function() {
6411d05cddcSAtari911            if (refreshInterval) {
6421d05cddcSAtari911                clearInterval(refreshInterval);
6431d05cddcSAtari911            }
6441d05cddcSAtari911        });
6451d05cddcSAtari911        </script>';
6461d05cddcSAtari911    }
6471d05cddcSAtari911
648*9ccd446eSAtari911    private function renderManageTab($colors = null) {
6491d05cddcSAtari911        global $INPUT;
6501d05cddcSAtari911
651*9ccd446eSAtari911        // Use defaults if not provided
652*9ccd446eSAtari911        if ($colors === null) {
653*9ccd446eSAtari911            $colors = $this->getTemplateColors();
654*9ccd446eSAtari911        }
655*9ccd446eSAtari911
6561d05cddcSAtari911        // Show message if present
6571d05cddcSAtari911        if ($INPUT->has('msg')) {
6581d05cddcSAtari911            $msg = hsc($INPUT->str('msg'));
6591d05cddcSAtari911            $type = $INPUT->str('msgtype', 'success');
6601d05cddcSAtari911            echo "<div style=\"padding:10px; margin:10px 0; border-left:3px solid " . ($type === 'success' ? '#28a745' : '#dc3545') . "; background:" . ($type === 'success' ? '#d4edda' : '#f8d7da') . "; border-radius:3px;\">";
6611d05cddcSAtari911            echo $msg;
6621d05cddcSAtari911            echo "</div>";
6631d05cddcSAtari911        }
6641d05cddcSAtari911
6651d05cddcSAtari911        echo '<h2 style="margin:10px 0; font-size:20px;">Manage Calendar Events</h2>';
6661d05cddcSAtari911
667*9ccd446eSAtari911        // Events Manager Section
668*9ccd446eSAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:1200px;">';
6691d05cddcSAtari911        echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">�� Events Manager</h3>';
670*9ccd446eSAtari911        echo '<p style="color:' . $colors['text'] . '; font-size:11px; margin:0 0 10px;">Scan, export, and import all calendar events across all namespaces.</p>';
6711d05cddcSAtari911
6721d05cddcSAtari911        // Get event statistics
6731d05cddcSAtari911        $stats = $this->getEventStatistics();
6741d05cddcSAtari911
6751d05cddcSAtari911        // Statistics display
676*9ccd446eSAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:10px; border-radius:3px; margin-bottom:10px; border:1px solid ' . $colors['border'] . ';">';
6771d05cddcSAtari911        echo '<div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(150px, 1fr)); gap:10px; font-size:12px;">';
6781d05cddcSAtari911
6791d05cddcSAtari911        echo '<div style="background:#f3e5f5; padding:8px; border-radius:3px; text-align:center;">';
6801d05cddcSAtari911        echo '<div style="font-size:24px; font-weight:bold; color:#7b1fa2;">' . $stats['total_events'] . '</div>';
681*9ccd446eSAtari911        echo '<div style="color:' . $colors['text'] . '; font-size:10px;">Total Events</div>';
6821d05cddcSAtari911        echo '</div>';
6831d05cddcSAtari911
6841d05cddcSAtari911        echo '<div style="background:#f3e5f5; padding:8px; border-radius:3px; text-align:center;">';
6851d05cddcSAtari911        echo '<div style="font-size:24px; font-weight:bold; color:#7b1fa2;">' . $stats['total_namespaces'] . '</div>';
686*9ccd446eSAtari911        echo '<div style="color:' . $colors['text'] . '; font-size:10px;">Namespaces</div>';
6871d05cddcSAtari911        echo '</div>';
6881d05cddcSAtari911
6891d05cddcSAtari911        echo '<div style="background:#e8f5e9; padding:8px; border-radius:3px; text-align:center;">';
6901d05cddcSAtari911        echo '<div style="font-size:24px; font-weight:bold; color:#388e3c;">' . $stats['total_files'] . '</div>';
691*9ccd446eSAtari911        echo '<div style="color:' . $colors['text'] . '; font-size:10px;">JSON Files</div>';
6921d05cddcSAtari911        echo '</div>';
6931d05cddcSAtari911
6941d05cddcSAtari911        echo '<div style="background:#fff3e0; padding:8px; border-radius:3px; text-align:center;">';
6951d05cddcSAtari911        echo '<div style="font-size:24px; font-weight:bold; color:#f57c00;">' . $stats['total_recurring'] . '</div>';
696*9ccd446eSAtari911        echo '<div style="color:' . $colors['text'] . '; font-size:10px;">Recurring</div>';
6971d05cddcSAtari911        echo '</div>';
6981d05cddcSAtari911
6991d05cddcSAtari911        echo '</div>';
7001d05cddcSAtari911
7011d05cddcSAtari911        // Last scan time
7021d05cddcSAtari911        if (!empty($stats['last_scan'])) {
703*9ccd446eSAtari911            echo '<div style="margin-top:8px; color:' . $colors['text'] . '; font-size:10px;">Last scanned: ' . hsc($stats['last_scan']) . '</div>';
7041d05cddcSAtari911        }
7051d05cddcSAtari911
7061d05cddcSAtari911        echo '</div>';
7071d05cddcSAtari911
7081d05cddcSAtari911        // Action buttons
7091d05cddcSAtari911        echo '<div style="display:flex; gap:10px; flex-wrap:wrap; margin-top:10px;">';
7101d05cddcSAtari911
7111d05cddcSAtari911        // Rescan button
7121d05cddcSAtari911        echo '<form method="post" action="?do=admin&page=calendar&tab=manage" style="display:inline;">';
7131d05cddcSAtari911        echo '<input type="hidden" name="action" value="rescan_events">';
7141d05cddcSAtari911        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;">';
7151d05cddcSAtari911        echo '<span>��</span><span>Re-scan Events</span>';
7161d05cddcSAtari911        echo '</button>';
7171d05cddcSAtari911        echo '</form>';
7181d05cddcSAtari911
7191d05cddcSAtari911        // Export button
7201d05cddcSAtari911        echo '<form method="post" action="?do=admin&page=calendar&tab=manage" style="display:inline;">';
7211d05cddcSAtari911        echo '<input type="hidden" name="action" value="export_all_events">';
7221d05cddcSAtari911        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;">';
7231d05cddcSAtari911        echo '<span>��</span><span>Export All Events</span>';
7241d05cddcSAtari911        echo '</button>';
7251d05cddcSAtari911        echo '</form>';
7261d05cddcSAtari911
7271d05cddcSAtari911        // Import button (with file upload)
7281d05cddcSAtari911        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?\')">';
7291d05cddcSAtari911        echo '<input type="hidden" name="action" value="import_all_events">';
7301d05cddcSAtari911        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;">';
7311d05cddcSAtari911        echo '<span>��</span><span>Import Events</span>';
7321d05cddcSAtari911        echo '<input type="file" name="import_file" accept=".json,.zip" required style="display:none;" onchange="this.form.submit()">';
7331d05cddcSAtari911        echo '</label>';
7341d05cddcSAtari911        echo '</form>';
7351d05cddcSAtari911
7361d05cddcSAtari911        echo '</div>';
7371d05cddcSAtari911
7381d05cddcSAtari911        // Breakdown by namespace
7391d05cddcSAtari911        if (!empty($stats['by_namespace'])) {
7401d05cddcSAtari911            echo '<details style="margin-top:12px;">';
7411d05cddcSAtari911            echo '<summary style="cursor:pointer; padding:6px; background:#e9e9e9; border-radius:3px; font-size:11px; font-weight:bold;">View Breakdown by Namespace</summary>';
742*9ccd446eSAtari911            echo '<div style="margin-top:8px; max-height:200px; overflow-y:auto; border:1px solid ' . $colors['border'] . '; border-radius:3px;">';
7431d05cddcSAtari911            echo '<table style="width:100%; border-collapse:collapse; font-size:11px;">';
7441d05cddcSAtari911            echo '<thead style="position:sticky; top:0; background:#f5f5f5;">';
7451d05cddcSAtari911            echo '<tr>';
7461d05cddcSAtari911            echo '<th style="padding:4px 6px; text-align:left; border-bottom:2px solid #ddd;">Namespace</th>';
7471d05cddcSAtari911            echo '<th style="padding:4px 6px; text-align:right; border-bottom:2px solid #ddd;">Events</th>';
7481d05cddcSAtari911            echo '<th style="padding:4px 6px; text-align:right; border-bottom:2px solid #ddd;">Files</th>';
7491d05cddcSAtari911            echo '</tr></thead><tbody>';
7501d05cddcSAtari911
7511d05cddcSAtari911            foreach ($stats['by_namespace'] as $ns => $nsStats) {
7521d05cddcSAtari911                echo '<tr style="border-bottom:1px solid #eee;">';
7531d05cddcSAtari911                echo '<td style="padding:4px 6px;"><code style="background:#f0f0f0; padding:1px 3px; border-radius:2px; font-size:10px;">' . hsc($ns ?: '(default)') . '</code></td>';
7541d05cddcSAtari911                echo '<td style="padding:4px 6px; text-align:right;"><strong>' . $nsStats['events'] . '</strong></td>';
7551d05cddcSAtari911                echo '<td style="padding:4px 6px; text-align:right;">' . $nsStats['files'] . '</td>';
7561d05cddcSAtari911                echo '</tr>';
7571d05cddcSAtari911            }
7581d05cddcSAtari911
7591d05cddcSAtari911            echo '</tbody></table>';
7601d05cddcSAtari911            echo '</div>';
7611d05cddcSAtari911            echo '</details>';
7621d05cddcSAtari911        }
7631d05cddcSAtari911
7641d05cddcSAtari911        echo '</div>';
7651d05cddcSAtari911
766*9ccd446eSAtari911        // Cleanup Events Section
767*9ccd446eSAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:1200px;">';
768*9ccd446eSAtari911        echo '<h3 style="margin:0 0 6px 0; color:#00cc07; font-size:16px;">�� Cleanup Old Events</h3>';
769*9ccd446eSAtari911        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>';
7701d05cddcSAtari911
7711d05cddcSAtari911        echo '<form method="post" action="?do=admin&page=calendar&tab=manage" id="cleanupForm">';
7721d05cddcSAtari911        echo '<input type="hidden" name="action" value="cleanup_events">';
7731d05cddcSAtari911
7741d05cddcSAtari911        // Compact options layout
775*9ccd446eSAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:10px; border:1px solid ' . $colors['border'] . '; border-radius:3px; margin-bottom:10px;">';
7761d05cddcSAtari911
7771d05cddcSAtari911        // Radio buttons in a row
7781d05cddcSAtari911        echo '<div style="display:flex; gap:20px; margin-bottom:10px; padding-bottom:8px; border-bottom:1px solid #f0f0f0;">';
7791d05cddcSAtari911        echo '<label style="cursor:pointer; font-size:12px; font-weight:600; display:flex; align-items:center; gap:4px;">';
7801d05cddcSAtari911        echo '<input type="radio" name="cleanup_type" value="age" checked onchange="updateCleanupOptions()">';
7811d05cddcSAtari911        echo '<span>By Age</span>';
7821d05cddcSAtari911        echo '</label>';
7831d05cddcSAtari911        echo '<label style="cursor:pointer; font-size:12px; font-weight:600; display:flex; align-items:center; gap:4px;">';
7841d05cddcSAtari911        echo '<input type="radio" name="cleanup_type" value="status" onchange="updateCleanupOptions()">';
7851d05cddcSAtari911        echo '<span>By Status</span>';
7861d05cddcSAtari911        echo '</label>';
7871d05cddcSAtari911        echo '<label style="cursor:pointer; font-size:12px; font-weight:600; display:flex; align-items:center; gap:4px;">';
7881d05cddcSAtari911        echo '<input type="radio" name="cleanup_type" value="range" onchange="updateCleanupOptions()">';
7891d05cddcSAtari911        echo '<span>By Date Range</span>';
7901d05cddcSAtari911        echo '</label>';
7911d05cddcSAtari911        echo '</div>';
7921d05cddcSAtari911
7931d05cddcSAtari911        // Age options
7941d05cddcSAtari911        echo '<div id="age-options" style="padding:6px 0;">';
795*9ccd446eSAtari911        echo '<span style="font-size:11px; color:' . $colors['text'] . '; margin-right:8px;">Delete events older than:</span>';
7961d05cddcSAtari911        echo '<select name="age_value" style="width:50px; padding:3px 4px; font-size:11px; border:1px solid #d0d0d0; border-radius:3px; margin-right:4px;">';
7971d05cddcSAtari911        for ($i = 1; $i <= 24; $i++) {
7981d05cddcSAtari911            $sel = $i === 6 ? ' selected' : '';
7991d05cddcSAtari911            echo '<option value="' . $i . '"' . $sel . '>' . $i . '</option>';
8001d05cddcSAtari911        }
8011d05cddcSAtari911        echo '</select>';
8021d05cddcSAtari911        echo '<select name="age_unit" style="width:80px; padding:3px 4px; font-size:11px; border:1px solid #d0d0d0; border-radius:3px;">';
8031d05cddcSAtari911        echo '<option value="months" selected>months</option>';
8041d05cddcSAtari911        echo '<option value="years">years</option>';
8051d05cddcSAtari911        echo '</select>';
8061d05cddcSAtari911        echo '</div>';
8071d05cddcSAtari911
8081d05cddcSAtari911        // Status options
8091d05cddcSAtari911        echo '<div id="status-options" style="padding:6px 0; opacity:0.4;">';
810*9ccd446eSAtari911        echo '<span style="font-size:11px; color:' . $colors['text'] . '; margin-right:8px;">Delete:</span>';
8111d05cddcSAtari911        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>';
8121d05cddcSAtari911        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>';
8131d05cddcSAtari911        echo '</div>';
8141d05cddcSAtari911
8151d05cddcSAtari911        // Range options
8161d05cddcSAtari911        echo '<div id="range-options" style="padding:6px 0; opacity:0.4;">';
817*9ccd446eSAtari911        echo '<span style="font-size:11px; color:' . $colors['text'] . '; margin-right:8px;">From:</span>';
8181d05cddcSAtari911        echo '<input type="date" name="range_start" style="padding:3px 6px; font-size:11px; border:1px solid #d0d0d0; border-radius:3px; margin-right:10px;">';
819*9ccd446eSAtari911        echo '<span style="font-size:11px; color:' . $colors['text'] . '; margin-right:8px;">To:</span>';
8201d05cddcSAtari911        echo '<input type="date" name="range_end" style="padding:3px 6px; font-size:11px; border:1px solid #d0d0d0; border-radius:3px;">';
8211d05cddcSAtari911        echo '</div>';
8221d05cddcSAtari911
8231d05cddcSAtari911        echo '</div>';
8241d05cddcSAtari911
8251d05cddcSAtari911        // Namespace filter - compact
826*9ccd446eSAtari911        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;">';
8271d05cddcSAtari911        echo '<label style="font-size:11px; font-weight:600; white-space:nowrap; color:#555;">Namespace:</label>';
8281d05cddcSAtari911        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;">';
8291d05cddcSAtari911        echo '</div>';
8301d05cddcSAtari911
8311d05cddcSAtari911        // Action buttons - compact row
8321d05cddcSAtari911        echo '<div style="display:flex; gap:8px; align-items:center;">';
8331d05cddcSAtari911        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>';
8341d05cddcSAtari911        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>';
8351d05cddcSAtari911        echo '<span style="font-size:10px; color:#999;">⚠️ Backup created automatically</span>';
8361d05cddcSAtari911        echo '</div>';
8371d05cddcSAtari911
8381d05cddcSAtari911        echo '</form>';
8391d05cddcSAtari911
8401d05cddcSAtari911        // Preview results area
8411d05cddcSAtari911        echo '<div id="cleanup-preview" style="margin-top:10px; display:none;"></div>';
8421d05cddcSAtari911
8431d05cddcSAtari911        echo '<script>
8441d05cddcSAtari911        function updateCleanupOptions() {
8451d05cddcSAtari911            const type = document.querySelector(\'input[name="cleanup_type"]:checked\').value;
8461d05cddcSAtari911
8471d05cddcSAtari911            // Show selected, gray out others
8481d05cddcSAtari911            document.getElementById(\'age-options\').style.opacity = type === \'age\' ? \'1\' : \'0.4\';
8491d05cddcSAtari911            document.getElementById(\'status-options\').style.opacity = type === \'status\' ? \'1\' : \'0.4\';
8501d05cddcSAtari911            document.getElementById(\'range-options\').style.opacity = type === \'range\' ? \'1\' : \'0.4\';
8511d05cddcSAtari911
8521d05cddcSAtari911            // Enable/disable inputs
8531d05cddcSAtari911            document.querySelectorAll(\'#age-options select\').forEach(el => el.disabled = type !== \'age\');
8541d05cddcSAtari911            document.querySelectorAll(\'#status-options input\').forEach(el => el.disabled = type !== \'status\');
8551d05cddcSAtari911            document.querySelectorAll(\'#range-options input\').forEach(el => el.disabled = type !== \'range\');
8561d05cddcSAtari911        }
8571d05cddcSAtari911
8581d05cddcSAtari911        function previewCleanup() {
8591d05cddcSAtari911            const form = document.getElementById(\'cleanupForm\');
8601d05cddcSAtari911            const formData = new FormData(form);
8611d05cddcSAtari911            formData.set(\'action\', \'preview_cleanup\');
8621d05cddcSAtari911
8631d05cddcSAtari911            const preview = document.getElementById(\'cleanup-preview\');
864*9ccd446eSAtari911            preview.innerHTML = \'<div style="text-align:center; padding:20px; color:' . $colors['text'] . ';">Loading preview...</div>\';
8651d05cddcSAtari911            preview.style.display = \'block\';
8661d05cddcSAtari911
8671d05cddcSAtari911            fetch(\'?do=admin&page=calendar&tab=manage\', {
8681d05cddcSAtari911                method: \'POST\',
8691d05cddcSAtari911                body: new URLSearchParams(formData)
8701d05cddcSAtari911            })
8711d05cddcSAtari911            .then(r => r.json())
8721d05cddcSAtari911            .then(data => {
8731d05cddcSAtari911                if (data.count === 0) {
8741d05cddcSAtari911                    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>\';
8751d05cddcSAtari911
8761d05cddcSAtari911                    // Show debug info if available
8771d05cddcSAtari911                    if (data.debug) {
878*9ccd446eSAtari911                        html += \'<details style="margin-top:8px; font-size:11px; color:' . $colors['text'] . ';">\';
8791d05cddcSAtari911                        html += \'<summary style="cursor:pointer;">Debug Info</summary>\';
8801d05cddcSAtari911                        html += \'<pre style="background:#f5f5f5; padding:6px; margin-top:4px; border-radius:3px; overflow-x:auto;">\' + JSON.stringify(data.debug, null, 2) + \'</pre>\';
8811d05cddcSAtari911                        html += \'</details>\';
8821d05cddcSAtari911                    }
8831d05cddcSAtari911
8841d05cddcSAtari911                    preview.innerHTML = html;
8851d05cddcSAtari911                } else {
8861d05cddcSAtari911                    let html = \'<div style="background:#f8d7da; border:1px solid #f5c6cb; padding:10px; border-radius:3px; font-size:12px; color:#721c24;">\';
8871d05cddcSAtari911                    html += \'<strong>⚠️ Warning:</strong> The following \' + data.count + \' event(s) would be deleted:<br><br>\';
888*9ccd446eSAtari911                    html += \'<div style="max-height:150px; overflow-y:auto; margin-top:6px; background:' . $colors['bg'] . '; padding:6px; border-radius:3px;">\';
8891d05cddcSAtari911                    data.events.forEach(evt => {
8901d05cddcSAtari911                        html += \'<div style="padding:3px; border-bottom:1px solid #eee; font-size:11px;">\';
8911d05cddcSAtari911                        html += \'\' + evt.title + \' (\' + evt.date + \')\';
8921d05cddcSAtari911                        if (evt.namespace) html += \' <span style="background:#e3f2fd; padding:1px 4px; border-radius:2px; font-size:9px;">\' + evt.namespace + \'</span>\';
8931d05cddcSAtari911                        html += \'</div>\';
8941d05cddcSAtari911                    });
8951d05cddcSAtari911                    html += \'</div></div>\';
8961d05cddcSAtari911                    preview.innerHTML = html;
8971d05cddcSAtari911                }
8981d05cddcSAtari911            })
8991d05cddcSAtari911            .catch(err => {
9001d05cddcSAtari911                preview.innerHTML = \'<div style="background:#f8d7da; border:1px solid #f5c6cb; padding:10px; border-radius:3px; font-size:12px; color:#721c24;">Error loading preview</div>\';
9011d05cddcSAtari911            });
9021d05cddcSAtari911        }
9031d05cddcSAtari911
9041d05cddcSAtari911        function confirmCleanup() {
9051d05cddcSAtari911            return confirm(\'Are you sure you want to delete these events? A backup will be created first, but this action cannot be easily undone.\');
9061d05cddcSAtari911        }
9071d05cddcSAtari911
9081d05cddcSAtari911        updateCleanupOptions();
9091d05cddcSAtari911        </script>';
9101d05cddcSAtari911
9111d05cddcSAtari911        echo '</div>';
9121d05cddcSAtari911
9131d05cddcSAtari911        // Recurring Events Section
914*9ccd446eSAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:1200px;">';
9151d05cddcSAtari911        echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">�� Recurring Events</h3>';
9161d05cddcSAtari911
9171d05cddcSAtari911        $recurringEvents = $this->findRecurringEvents();
9181d05cddcSAtari911
9191d05cddcSAtari911        if (empty($recurringEvents)) {
920*9ccd446eSAtari911            echo '<p style="color:' . $colors['text'] . '; font-size:13px; margin:5px 0;">No recurring events found.</p>';
9211d05cddcSAtari911        } else {
9221d05cddcSAtari911            // Search bar
9231d05cddcSAtari911            echo '<div style="margin-bottom:8px;">';
924*9ccd446eSAtari911            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;">';
9251d05cddcSAtari911            echo '</div>';
9261d05cddcSAtari911
9271d05cddcSAtari911            echo '<style>
9281d05cddcSAtari911                .sort-arrow {
9291d05cddcSAtari911                    color: #999;
9301d05cddcSAtari911                    font-size: 10px;
9311d05cddcSAtari911                    margin-left: 3px;
9321d05cddcSAtari911                    display: inline-block;
9331d05cddcSAtari911                }
9341d05cddcSAtari911                #recurringTable th:hover {
9351d05cddcSAtari911                    background: #ddd;
9361d05cddcSAtari911                }
9371d05cddcSAtari911                #recurringTable th:hover .sort-arrow {
9381d05cddcSAtari911                    color: #00cc07;
9391d05cddcSAtari911                }
9401d05cddcSAtari911                .recurring-row-hidden {
9411d05cddcSAtari911                    display: none;
9421d05cddcSAtari911                }
9431d05cddcSAtari911            </style>';
944*9ccd446eSAtari911            echo '<div style="max-height:250px; overflow-y:auto; border:1px solid ' . $colors['border'] . '; border-radius:3px;">';
9451d05cddcSAtari911            echo '<table id="recurringTable" style="width:100%; border-collapse:collapse; font-size:11px;">';
9461d05cddcSAtari911            echo '<thead style="position:sticky; top:0; background:#e9e9e9;">';
9471d05cddcSAtari911            echo '<tr>';
9481d05cddcSAtari911            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>';
9491d05cddcSAtari911            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>';
9501d05cddcSAtari911            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>';
9511d05cddcSAtari911            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>';
9521d05cddcSAtari911            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>';
9531d05cddcSAtari911            echo '<th style="padding:4px 6px; text-align:left; border-bottom:2px solid #ddd;">Actions</th>';
9541d05cddcSAtari911            echo '</tr></thead><tbody id="recurringTableBody">';
9551d05cddcSAtari911
9561d05cddcSAtari911            foreach ($recurringEvents as $series) {
9571d05cddcSAtari911                echo '<tr style="border-bottom:1px solid #eee;">';
9581d05cddcSAtari911                echo '<td style="padding:4px 6px;">' . hsc($series['title']) . '</td>';
9591d05cddcSAtari911                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>';
9601d05cddcSAtari911                echo '<td style="padding:4px 6px;">' . hsc($series['pattern']) . '</td>';
9611d05cddcSAtari911                echo '<td style="padding:4px 6px;">' . hsc($series['firstDate']) . '</td>';
9621d05cddcSAtari911                echo '<td style="padding:4px 6px;"><strong>' . $series['count'] . '</strong></td>';
9631d05cddcSAtari911                echo '<td style="padding:4px 6px; white-space:nowrap;">';
9641d05cddcSAtari911                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>';
9651d05cddcSAtari911                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>';
9661d05cddcSAtari911                echo '</td>';
9671d05cddcSAtari911                echo '</tr>';
9681d05cddcSAtari911            }
9691d05cddcSAtari911
9701d05cddcSAtari911            echo '</tbody></table>';
9711d05cddcSAtari911            echo '</div>';
972*9ccd446eSAtari911            echo '<p style="color:' . $colors['text'] . '; font-size:10px; margin:5px 0 0;">Total: ' . count($recurringEvents) . ' series</p>';
9731d05cddcSAtari911        }
9741d05cddcSAtari911        echo '</div>';
9751d05cddcSAtari911
9761d05cddcSAtari911        // Compact Tree-based Namespace Manager
977*9ccd446eSAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:1200px;">';
9781d05cddcSAtari911        echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">�� Namespace Explorer</h3>';
979*9ccd446eSAtari911        echo '<p style="color:' . $colors['text'] . '; font-size:11px; margin:0 0 8px;">Select events and move between namespaces. Drag & drop also supported.</p>';
9801d05cddcSAtari911
9811d05cddcSAtari911        // Search bar
9821d05cddcSAtari911        echo '<div style="margin-bottom:8px;">';
983*9ccd446eSAtari911        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;">';
9841d05cddcSAtari911        echo '</div>';
9851d05cddcSAtari911
9861d05cddcSAtari911        $eventsByNamespace = $this->getEventsByNamespace();
9871d05cddcSAtari911
9881d05cddcSAtari911        // Control bar
9891d05cddcSAtari911        echo '<form method="post" action="?do=admin&page=calendar&tab=manage" id="moveForm">';
9901d05cddcSAtari911        echo '<input type="hidden" name="action" value="move_selected_events" id="formAction">';
9911d05cddcSAtari911        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;">';
9921d05cddcSAtari911        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>';
9931d05cddcSAtari911        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>';
9941d05cddcSAtari911        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>';
9951d05cddcSAtari911        echo '<span style="margin-left:10px;">Move to:</span>';
996*9ccd446eSAtari911        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...">';
9971d05cddcSAtari911        echo '<datalist id="namespaceList">';
9981d05cddcSAtari911        echo '<option value="">(default)</option>';
9991d05cddcSAtari911        foreach (array_keys($eventsByNamespace) as $ns) {
10001d05cddcSAtari911            if ($ns !== '') {
10011d05cddcSAtari911                echo '<option value="' . hsc($ns) . '">' . hsc($ns) . '</option>';
10021d05cddcSAtari911            }
10031d05cddcSAtari911        }
10041d05cddcSAtari911        echo '</datalist>';
10051d05cddcSAtari911        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>';
10061d05cddcSAtari911        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>';
10071d05cddcSAtari911        echo '<span id="selectedCount" style="margin-left:auto; color:#00cc07; font-size:11px;">0 selected</span>';
10081d05cddcSAtari911        echo '</div>';
10091d05cddcSAtari911
10101d05cddcSAtari911        echo '<div style="display:grid; grid-template-columns:1fr 1fr; gap:10px;">';
10111d05cddcSAtari911
10121d05cddcSAtari911        // Event list with checkboxes
10131d05cddcSAtari911        echo '<div>';
1014*9ccd446eSAtari911        echo '<div style="max-height:450px; overflow-y:auto; border:1px solid ' . $colors['border'] . '; border-radius:3px; background:' . $colors['bg'] . ';">';
10151d05cddcSAtari911
10161d05cddcSAtari911        foreach ($eventsByNamespace as $namespace => $data) {
10171d05cddcSAtari911            $nsId = 'ns_' . md5($namespace);
10181d05cddcSAtari911            $eventCount = count($data['events']);
10191d05cddcSAtari911
10201d05cddcSAtari911            echo '<div style="border-bottom:1px solid #ddd;">';
10211d05cddcSAtari911
10221d05cddcSAtari911            // Namespace header - ultra compact
10231d05cddcSAtari911            echo '<div style="background:#f5f5f5; padding:3px 6px; display:flex; justify-content:space-between; align-items:center; font-size:11px;">';
10241d05cddcSAtari911            echo '<div style="display:flex; align-items:center; gap:4px;">';
10251d05cddcSAtari911            echo '<span onclick="toggleNamespace(\'' . $nsId . '\')" style="cursor:pointer; width:12px; display:inline-block; font-size:10px;"><span id="' . $nsId . '_arrow">▶</span></span>';
10261d05cddcSAtari911            echo '<input type="checkbox" onclick="toggleNamespaceSelect(\'' . $nsId . '\')" id="' . $nsId . '_check" style="margin:0; width:12px; height:12px;">';
10271d05cddcSAtari911            echo '<span onclick="toggleNamespace(\'' . $nsId . '\')" style="cursor:pointer; font-weight:600; font-size:11px;">�� ' . hsc($namespace ?: '(default)') . '</span>';
10281d05cddcSAtari911            echo '</div>';
10291d05cddcSAtari911            echo '<div style="display:flex; gap:3px; align-items:center;">';
10301d05cddcSAtari911            echo '<span style="background:#00cc07; color:white; padding:0px 4px; border-radius:6px; font-size:9px; line-height:14px;">' . $eventCount . '</span>';
1031*9ccd446eSAtari911            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>';
10321d05cddcSAtari911            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>';
10331d05cddcSAtari911            echo '</div>';
10341d05cddcSAtari911            echo '</div>';
10351d05cddcSAtari911
10361d05cddcSAtari911            // Events - ultra compact
10371d05cddcSAtari911            echo '<div id="' . $nsId . '" style="display:none; max-height:150px; overflow-y:auto;">';
10381d05cddcSAtari911            foreach ($data['events'] as $event) {
10391d05cddcSAtari911                $eventId = $event['id'] . '|' . $namespace . '|' . $event['date'] . '|' . $event['month'];
10401d05cddcSAtari911                $checkId = 'evt_' . md5($eventId);
10411d05cddcSAtari911
10421d05cddcSAtari911                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\'">';
10431d05cddcSAtari911                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;">';
10441d05cddcSAtari911                echo '<div style="flex:1; min-width:0;">';
10451d05cddcSAtari911                echo '<div style="font-weight:500; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; font-size:10px;">' . hsc($event['title']) . '</div>';
10461d05cddcSAtari911                echo '<div style="color:#999; font-size:9px;">' . hsc($event['date']) . ($event['startTime'] ? ' • ' . hsc($event['startTime']) : '') . '</div>';
10471d05cddcSAtari911                echo '</div>';
10481d05cddcSAtari911                echo '</div>';
10491d05cddcSAtari911            }
10501d05cddcSAtari911            echo '</div>';
10511d05cddcSAtari911            echo '</div>';
10521d05cddcSAtari911        }
10531d05cddcSAtari911
10541d05cddcSAtari911        echo '</div>';
10551d05cddcSAtari911        echo '</div>';
10561d05cddcSAtari911
10571d05cddcSAtari911        // Drop zones - ultra compact
10581d05cddcSAtari911        echo '<div>';
10591d05cddcSAtari911        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>';
1060*9ccd446eSAtari911        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'] . ';">';
10611d05cddcSAtari911
10621d05cddcSAtari911        foreach (array_keys($eventsByNamespace) as $namespace) {
1063*9ccd446eSAtari911            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\'">';
10641d05cddcSAtari911            echo '<div style="font-size:11px; font-weight:600; color:#00cc07;">�� ' . hsc($namespace ?: '(default)') . '</div>';
10651d05cddcSAtari911            echo '<div style="color:#999; font-size:9px; margin-top:1px;">Drop here</div>';
10661d05cddcSAtari911            echo '</div>';
10671d05cddcSAtari911        }
10681d05cddcSAtari911
10691d05cddcSAtari911        echo '</div>';
10701d05cddcSAtari911        echo '</div>';
10711d05cddcSAtari911
10721d05cddcSAtari911        echo '</div>'; // end grid
10731d05cddcSAtari911        echo '</form>';
10741d05cddcSAtari911
10751d05cddcSAtari911        echo '</div>';
10761d05cddcSAtari911
10771d05cddcSAtari911        // JavaScript
10781d05cddcSAtari911        echo '<script>
10791d05cddcSAtari911        // Table sorting functionality - defined early so onclick handlers work
10801d05cddcSAtari911        let sortDirection = {}; // Track sort direction for each column
10811d05cddcSAtari911
10821d05cddcSAtari911        function sortRecurringTable(columnIndex) {
10831d05cddcSAtari911            const table = document.getElementById("recurringTable");
10841d05cddcSAtari911            const tbody = document.getElementById("recurringTableBody");
10851d05cddcSAtari911
1086*9ccd446eSAtari911            if (!table || !tbody) return;
10871d05cddcSAtari911
10881d05cddcSAtari911            const rows = Array.from(tbody.querySelectorAll("tr"));
1089*9ccd446eSAtari911            if (rows.length === 0) return;
10901d05cddcSAtari911
10911d05cddcSAtari911            // Toggle sort direction for this column
10921d05cddcSAtari911            if (!sortDirection[columnIndex]) {
10931d05cddcSAtari911                sortDirection[columnIndex] = "asc";
10941d05cddcSAtari911            } else {
10951d05cddcSAtari911                sortDirection[columnIndex] = sortDirection[columnIndex] === "asc" ? "desc" : "asc";
10961d05cddcSAtari911            }
10971d05cddcSAtari911
10981d05cddcSAtari911            const direction = sortDirection[columnIndex];
10991d05cddcSAtari911            const isNumeric = columnIndex === 4; // Count column
11001d05cddcSAtari911
11011d05cddcSAtari911            // Sort rows
11021d05cddcSAtari911            rows.sort((a, b) => {
11031d05cddcSAtari911                let aValue = a.cells[columnIndex].textContent.trim();
11041d05cddcSAtari911                let bValue = b.cells[columnIndex].textContent.trim();
11051d05cddcSAtari911
11061d05cddcSAtari911                // Extract text from code elements for namespace column
11071d05cddcSAtari911                if (columnIndex === 1) {
11081d05cddcSAtari911                    const aCode = a.cells[columnIndex].querySelector("code");
11091d05cddcSAtari911                    const bCode = b.cells[columnIndex].querySelector("code");
11101d05cddcSAtari911                    aValue = aCode ? aCode.textContent.trim() : aValue;
11111d05cddcSAtari911                    bValue = bCode ? bCode.textContent.trim() : bValue;
11121d05cddcSAtari911                }
11131d05cddcSAtari911
11141d05cddcSAtari911                // Extract number from strong elements for count column
11151d05cddcSAtari911                if (isNumeric) {
11161d05cddcSAtari911                    const aStrong = a.cells[columnIndex].querySelector("strong");
11171d05cddcSAtari911                    const bStrong = b.cells[columnIndex].querySelector("strong");
11181d05cddcSAtari911                    aValue = aStrong ? parseInt(aStrong.textContent.trim()) : 0;
11191d05cddcSAtari911                    bValue = bStrong ? parseInt(bStrong.textContent.trim()) : 0;
11201d05cddcSAtari911
11211d05cddcSAtari911                    return direction === "asc" ? aValue - bValue : bValue - aValue;
11221d05cddcSAtari911                }
11231d05cddcSAtari911
11241d05cddcSAtari911                // String comparison
11251d05cddcSAtari911                if (direction === "asc") {
11261d05cddcSAtari911                    return aValue.localeCompare(bValue);
11271d05cddcSAtari911                } else {
11281d05cddcSAtari911                    return bValue.localeCompare(aValue);
11291d05cddcSAtari911                }
11301d05cddcSAtari911            });
11311d05cddcSAtari911
11321d05cddcSAtari911            // Update arrows
11331d05cddcSAtari911            const headers = table.querySelectorAll("th");
11341d05cddcSAtari911            headers.forEach((header, index) => {
11351d05cddcSAtari911                const arrow = header.querySelector(".sort-arrow");
11361d05cddcSAtari911                if (arrow) {
11371d05cddcSAtari911                    if (index === columnIndex) {
11381d05cddcSAtari911                        arrow.textContent = direction === "asc" ? "↑" : "↓";
11391d05cddcSAtari911                        arrow.style.color = "#00cc07";
11401d05cddcSAtari911                    } else {
11411d05cddcSAtari911                        arrow.textContent = "⇅";
11421d05cddcSAtari911                        arrow.style.color = "#999";
11431d05cddcSAtari911                    }
11441d05cddcSAtari911                }
11451d05cddcSAtari911            });
11461d05cddcSAtari911
11471d05cddcSAtari911            // Rebuild tbody
11481d05cddcSAtari911            rows.forEach(row => tbody.appendChild(row));
11491d05cddcSAtari911        }
11501d05cddcSAtari911
11511d05cddcSAtari911        function filterRecurringEvents() {
11521d05cddcSAtari911            const searchInput = document.getElementById("searchRecurring");
11531d05cddcSAtari911            const filter = normalizeText(searchInput.value);
11541d05cddcSAtari911            const tbody = document.getElementById("recurringTableBody");
11551d05cddcSAtari911            const rows = tbody.getElementsByTagName("tr");
11561d05cddcSAtari911
11571d05cddcSAtari911            for (let i = 0; i < rows.length; i++) {
11581d05cddcSAtari911                const row = rows[i];
11591d05cddcSAtari911                const titleCell = row.getElementsByTagName("td")[0];
11601d05cddcSAtari911
11611d05cddcSAtari911                if (titleCell) {
11621d05cddcSAtari911                    const titleText = normalizeText(titleCell.textContent || titleCell.innerText);
11631d05cddcSAtari911
11641d05cddcSAtari911                    if (titleText.indexOf(filter) > -1) {
11651d05cddcSAtari911                        row.classList.remove("recurring-row-hidden");
11661d05cddcSAtari911                    } else {
11671d05cddcSAtari911                        row.classList.add("recurring-row-hidden");
11681d05cddcSAtari911                    }
11691d05cddcSAtari911                }
11701d05cddcSAtari911            }
11711d05cddcSAtari911        }
11721d05cddcSAtari911
11731d05cddcSAtari911        function normalizeText(text) {
11741d05cddcSAtari911            // Convert to lowercase
11751d05cddcSAtari911            text = text.toLowerCase();
11761d05cddcSAtari911
11771d05cddcSAtari911            // Remove apostrophes and quotes
11781d05cddcSAtari911            text = text.replace(/[\'\"]/g, "");
11791d05cddcSAtari911
11801d05cddcSAtari911            // Replace accented characters with regular ones
11811d05cddcSAtari911            text = text.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
11821d05cddcSAtari911
11831d05cddcSAtari911            // Remove special characters except spaces and alphanumeric
11841d05cddcSAtari911            text = text.replace(/[^a-z0-9\s]/g, "");
11851d05cddcSAtari911
11861d05cddcSAtari911            // Collapse multiple spaces
11871d05cddcSAtari911            text = text.replace(/\s+/g, " ");
11881d05cddcSAtari911
11891d05cddcSAtari911            return text.trim();
11901d05cddcSAtari911        }
11911d05cddcSAtari911
11921d05cddcSAtari911        function filterEvents() {
11931d05cddcSAtari911            const searchText = normalizeText(document.getElementById("searchEvents").value);
11941d05cddcSAtari911            const eventRows = document.querySelectorAll(".event-row");
11951d05cddcSAtari911            let visibleCount = 0;
11961d05cddcSAtari911
11971d05cddcSAtari911            eventRows.forEach(row => {
11981d05cddcSAtari911                const titleElement = row.querySelector("div div");
11991d05cddcSAtari911                const originalTitle = titleElement.getAttribute("data-original-title") || titleElement.textContent;
12001d05cddcSAtari911
12011d05cddcSAtari911                // Store original title if not already stored
12021d05cddcSAtari911                if (!titleElement.getAttribute("data-original-title")) {
12031d05cddcSAtari911                    titleElement.setAttribute("data-original-title", originalTitle);
12041d05cddcSAtari911                }
12051d05cddcSAtari911
12061d05cddcSAtari911                const normalizedTitle = normalizeText(originalTitle);
12071d05cddcSAtari911
12081d05cddcSAtari911                if (normalizedTitle.includes(searchText) || searchText === "") {
12091d05cddcSAtari911                    row.style.display = "flex";
12101d05cddcSAtari911                    visibleCount++;
12111d05cddcSAtari911                } else {
12121d05cddcSAtari911                    row.style.display = "none";
12131d05cddcSAtari911                }
12141d05cddcSAtari911            });
12151d05cddcSAtari911
12161d05cddcSAtari911            // Update namespace visibility and counts
12171d05cddcSAtari911            document.querySelectorAll("[id^=ns_]").forEach(nsDiv => {
12181d05cddcSAtari911                if (nsDiv.id.endsWith("_arrow") || nsDiv.id.endsWith("_check")) return;
12191d05cddcSAtari911
12201d05cddcSAtari911                const visibleEvents = nsDiv.querySelectorAll(".event-row[style*=\\"display: flex\\"], .event-row:not([style*=\\"display: none\\"])").length;
12211d05cddcSAtari911                const nsId = nsDiv.id;
12221d05cddcSAtari911                const arrow = document.getElementById(nsId + "_arrow");
12231d05cddcSAtari911
12241d05cddcSAtari911                // Auto-expand namespaces with matches when searching
12251d05cddcSAtari911                if (searchText && visibleEvents > 0) {
12261d05cddcSAtari911                    nsDiv.style.display = "block";
12271d05cddcSAtari911                    if (arrow) arrow.textContent = "▼";
12281d05cddcSAtari911                }
12291d05cddcSAtari911            });
12301d05cddcSAtari911        }
12311d05cddcSAtari911
12321d05cddcSAtari911        function toggleNamespace(id) {
12331d05cddcSAtari911            const elem = document.getElementById(id);
12341d05cddcSAtari911            const arrow = document.getElementById(id + "_arrow");
12351d05cddcSAtari911            if (elem.style.display === "none") {
12361d05cddcSAtari911                elem.style.display = "block";
12371d05cddcSAtari911                arrow.textContent = "▼";
12381d05cddcSAtari911            } else {
12391d05cddcSAtari911                elem.style.display = "none";
12401d05cddcSAtari911                arrow.textContent = "▶";
12411d05cddcSAtari911            }
12421d05cddcSAtari911        }
12431d05cddcSAtari911
12441d05cddcSAtari911        function toggleNamespaceSelect(nsId) {
12451d05cddcSAtari911            const checkbox = document.getElementById(nsId + "_check");
12461d05cddcSAtari911            const events = document.querySelectorAll("." + nsId + "_events");
12471d05cddcSAtari911
12481d05cddcSAtari911            // Only select visible events (not hidden by search)
12491d05cddcSAtari911            events.forEach(cb => {
12501d05cddcSAtari911                const eventRow = cb.closest(".event-row");
12511d05cddcSAtari911                if (eventRow && eventRow.style.display !== "none") {
12521d05cddcSAtari911                    cb.checked = checkbox.checked;
12531d05cddcSAtari911                }
12541d05cddcSAtari911            });
12551d05cddcSAtari911            updateCount();
12561d05cddcSAtari911        }
12571d05cddcSAtari911
12581d05cddcSAtari911        function selectAll() {
12591d05cddcSAtari911            // Only select visible events
12601d05cddcSAtari911            document.querySelectorAll(".event-checkbox").forEach(cb => {
12611d05cddcSAtari911                const eventRow = cb.closest(".event-row");
12621d05cddcSAtari911                if (eventRow && eventRow.style.display !== "none") {
12631d05cddcSAtari911                    cb.checked = true;
12641d05cddcSAtari911                }
12651d05cddcSAtari911            });
12661d05cddcSAtari911            // Update namespace checkboxes to indeterminate if partially selected
12671d05cddcSAtari911            document.querySelectorAll("input[id$=_check]").forEach(nsCheckbox => {
12681d05cddcSAtari911                const nsId = nsCheckbox.id.replace("_check", "");
12691d05cddcSAtari911                const events = document.querySelectorAll("." + nsId + "_events");
12701d05cddcSAtari911                const visibleEvents = Array.from(events).filter(cb => {
12711d05cddcSAtari911                    const row = cb.closest(".event-row");
12721d05cddcSAtari911                    return row && row.style.display !== "none";
12731d05cddcSAtari911                });
12741d05cddcSAtari911                const checkedVisible = visibleEvents.filter(cb => cb.checked);
12751d05cddcSAtari911
12761d05cddcSAtari911                if (checkedVisible.length === visibleEvents.length && visibleEvents.length > 0) {
12771d05cddcSAtari911                    nsCheckbox.checked = true;
12781d05cddcSAtari911                } else if (checkedVisible.length > 0) {
12791d05cddcSAtari911                    nsCheckbox.indeterminate = true;
12801d05cddcSAtari911                } else {
12811d05cddcSAtari911                    nsCheckbox.checked = false;
12821d05cddcSAtari911                }
12831d05cddcSAtari911            });
12841d05cddcSAtari911            updateCount();
12851d05cddcSAtari911        }
12861d05cddcSAtari911
12871d05cddcSAtari911        function deselectAll() {
12881d05cddcSAtari911            document.querySelectorAll(".event-checkbox").forEach(cb => cb.checked = false);
12891d05cddcSAtari911            document.querySelectorAll("input[id$=_check]").forEach(cb => {
12901d05cddcSAtari911                cb.checked = false;
12911d05cddcSAtari911                cb.indeterminate = false;
12921d05cddcSAtari911            });
12931d05cddcSAtari911            updateCount();
12941d05cddcSAtari911        }
12951d05cddcSAtari911
12961d05cddcSAtari911        function deleteSelected() {
12971d05cddcSAtari911            const checkedBoxes = document.querySelectorAll(".event-checkbox:checked");
12981d05cddcSAtari911            if (checkedBoxes.length === 0) {
12991d05cddcSAtari911                alert("No events selected");
13001d05cddcSAtari911                return;
13011d05cddcSAtari911            }
13021d05cddcSAtari911
13031d05cddcSAtari911            const count = checkedBoxes.length;
13041d05cddcSAtari911            if (!confirm(`Delete ${count} selected event(s)?\\n\\nThis cannot be undone!`)) {
13051d05cddcSAtari911                return;
13061d05cddcSAtari911            }
13071d05cddcSAtari911
13081d05cddcSAtari911            const form = document.createElement("form");
13091d05cddcSAtari911            form.method = "POST";
13101d05cddcSAtari911            form.action = "?do=admin&page=calendar&tab=manage";
13111d05cddcSAtari911
13121d05cddcSAtari911            const actionInput = document.createElement("input");
13131d05cddcSAtari911            actionInput.type = "hidden";
13141d05cddcSAtari911            actionInput.name = "action";
13151d05cddcSAtari911            actionInput.value = "delete_selected_events";
13161d05cddcSAtari911            form.appendChild(actionInput);
13171d05cddcSAtari911
13181d05cddcSAtari911            checkedBoxes.forEach(cb => {
13191d05cddcSAtari911                const eventInput = document.createElement("input");
13201d05cddcSAtari911                eventInput.type = "hidden";
13211d05cddcSAtari911                eventInput.name = "events[]";
13221d05cddcSAtari911                eventInput.value = cb.value;
13231d05cddcSAtari911                form.appendChild(eventInput);
13241d05cddcSAtari911            });
13251d05cddcSAtari911
13261d05cddcSAtari911            document.body.appendChild(form);
13271d05cddcSAtari911            form.submit();
13281d05cddcSAtari911        }
13291d05cddcSAtari911
13301d05cddcSAtari911        function createNewNamespace() {
13311d05cddcSAtari911            const namespaceName = prompt("Enter new namespace name:\\n\\nExamples:\\n- work\\n- personal\\n- projects:alpha\\n- aspen:travel:2025");
13321d05cddcSAtari911
13331d05cddcSAtari911            if (!namespaceName) {
13341d05cddcSAtari911                return; // Cancelled
13351d05cddcSAtari911            }
13361d05cddcSAtari911
13371d05cddcSAtari911            // Validate namespace name
13381d05cddcSAtari911            if (!/^[a-zA-Z0-9_:-]+$/.test(namespaceName)) {
13391d05cddcSAtari911                alert("Invalid namespace name.\\n\\nUse only letters, numbers, underscore, hyphen, and colon.\\nExample: work:projects:alpha");
13401d05cddcSAtari911                return;
13411d05cddcSAtari911            }
13421d05cddcSAtari911
13431d05cddcSAtari911            // Submit form to create namespace
13441d05cddcSAtari911            const form = document.createElement("form");
13451d05cddcSAtari911            form.method = "POST";
13461d05cddcSAtari911            form.action = "?do=admin&page=calendar&tab=manage";
13471d05cddcSAtari911
13481d05cddcSAtari911            const actionInput = document.createElement("input");
13491d05cddcSAtari911            actionInput.type = "hidden";
13501d05cddcSAtari911            actionInput.name = "action";
13511d05cddcSAtari911            actionInput.value = "create_namespace";
13521d05cddcSAtari911            form.appendChild(actionInput);
13531d05cddcSAtari911
13541d05cddcSAtari911            const namespaceInput = document.createElement("input");
13551d05cddcSAtari911            namespaceInput.type = "hidden";
13561d05cddcSAtari911            namespaceInput.name = "namespace_name";
13571d05cddcSAtari911            namespaceInput.value = namespaceName;
13581d05cddcSAtari911            form.appendChild(namespaceInput);
13591d05cddcSAtari911
13601d05cddcSAtari911            document.body.appendChild(form);
13611d05cddcSAtari911            form.submit();
13621d05cddcSAtari911        }
13631d05cddcSAtari911
13641d05cddcSAtari911        function updateCount() {
13651d05cddcSAtari911            const count = document.querySelectorAll(".event-checkbox:checked").length;
13661d05cddcSAtari911            document.getElementById("selectedCount").textContent = count + " selected";
13671d05cddcSAtari911        }
13681d05cddcSAtari911
13691d05cddcSAtari911        function deleteNamespace(namespace) {
13701d05cddcSAtari911            const displayName = namespace || "(default)";
13711d05cddcSAtari911            if (!confirm("Delete ENTIRE namespace: " + displayName + "?\\n\\nThis will delete ALL events in this namespace!\\n\\nThis cannot be undone!")) {
13721d05cddcSAtari911                return;
13731d05cddcSAtari911            }
13741d05cddcSAtari911            const form = document.createElement("form");
13751d05cddcSAtari911            form.method = "POST";
13761d05cddcSAtari911            form.action = "?do=admin&page=calendar&tab=manage";
13771d05cddcSAtari911            const actionInput = document.createElement("input");
13781d05cddcSAtari911            actionInput.type = "hidden";
13791d05cddcSAtari911            actionInput.name = "action";
13801d05cddcSAtari911            actionInput.value = "delete_namespace";
13811d05cddcSAtari911            form.appendChild(actionInput);
13821d05cddcSAtari911            const nsInput = document.createElement("input");
13831d05cddcSAtari911            nsInput.type = "hidden";
13841d05cddcSAtari911            nsInput.name = "namespace";
13851d05cddcSAtari911            nsInput.value = namespace;
13861d05cddcSAtari911            form.appendChild(nsInput);
13871d05cddcSAtari911            document.body.appendChild(form);
13881d05cddcSAtari911            form.submit();
13891d05cddcSAtari911        }
13901d05cddcSAtari911
1391*9ccd446eSAtari911        function renameNamespace(oldNamespace) {
1392*9ccd446eSAtari911            const displayName = oldNamespace || "(default)";
1393*9ccd446eSAtari911            const newName = prompt("Rename namespace: " + displayName + "\\n\\nEnter new name:", oldNamespace);
1394*9ccd446eSAtari911            if (newName === null || newName === oldNamespace) {
1395*9ccd446eSAtari911                return; // Cancelled or no change
1396*9ccd446eSAtari911            }
1397*9ccd446eSAtari911            const form = document.createElement("form");
1398*9ccd446eSAtari911            form.method = "POST";
1399*9ccd446eSAtari911            form.action = "?do=admin&page=calendar&tab=manage";
1400*9ccd446eSAtari911            const actionInput = document.createElement("input");
1401*9ccd446eSAtari911            actionInput.type = "hidden";
1402*9ccd446eSAtari911            actionInput.name = "action";
1403*9ccd446eSAtari911            actionInput.value = "rename_namespace";
1404*9ccd446eSAtari911            form.appendChild(actionInput);
1405*9ccd446eSAtari911            const oldInput = document.createElement("input");
1406*9ccd446eSAtari911            oldInput.type = "hidden";
1407*9ccd446eSAtari911            oldInput.name = "old_namespace";
1408*9ccd446eSAtari911            oldInput.value = oldNamespace;
1409*9ccd446eSAtari911            form.appendChild(oldInput);
1410*9ccd446eSAtari911            const newInput = document.createElement("input");
1411*9ccd446eSAtari911            newInput.type = "hidden";
1412*9ccd446eSAtari911            newInput.name = "new_namespace";
1413*9ccd446eSAtari911            newInput.value = newName;
1414*9ccd446eSAtari911            form.appendChild(newInput);
1415*9ccd446eSAtari911            document.body.appendChild(form);
1416*9ccd446eSAtari911            form.submit();
1417*9ccd446eSAtari911        }
1418*9ccd446eSAtari911
14191d05cddcSAtari911        let draggedEvent = null;
14201d05cddcSAtari911
14211d05cddcSAtari911        function dragStart(event, eventId) {
14221d05cddcSAtari911            const checkbox = event.target.closest(".event-row").querySelector(".event-checkbox");
14231d05cddcSAtari911
14241d05cddcSAtari911            // If this event is checked, drag all checked events
14251d05cddcSAtari911            const checkedBoxes = document.querySelectorAll(".event-checkbox:checked");
14261d05cddcSAtari911            if (checkbox && checkbox.checked && checkedBoxes.length > 1) {
14271d05cddcSAtari911                // Dragging multiple selected events
14281d05cddcSAtari911                draggedEvent = "MULTIPLE";
14291d05cddcSAtari911                event.dataTransfer.setData("text/plain", "MULTIPLE");
14301d05cddcSAtari911            } else {
14311d05cddcSAtari911                // Dragging single event
14321d05cddcSAtari911                draggedEvent = eventId;
14331d05cddcSAtari911                event.dataTransfer.setData("text/plain", eventId);
14341d05cddcSAtari911            }
14351d05cddcSAtari911            event.dataTransfer.effectAllowed = "move";
14361d05cddcSAtari911            event.target.style.opacity = "0.5";
14371d05cddcSAtari911        }
14381d05cddcSAtari911
14391d05cddcSAtari911        function allowDrop(event) {
14401d05cddcSAtari911            event.preventDefault();
14411d05cddcSAtari911            event.dataTransfer.dropEffect = "move";
14421d05cddcSAtari911        }
14431d05cddcSAtari911
14441d05cddcSAtari911        function drop(event, targetNamespace) {
14451d05cddcSAtari911            event.preventDefault();
14461d05cddcSAtari911
14471d05cddcSAtari911            if (draggedEvent === "MULTIPLE") {
14481d05cddcSAtari911                // Move all selected events
14491d05cddcSAtari911                const checkedBoxes = document.querySelectorAll(".event-checkbox:checked");
14501d05cddcSAtari911                if (checkedBoxes.length === 0) return;
14511d05cddcSAtari911
14521d05cddcSAtari911                const form = document.createElement("form");
14531d05cddcSAtari911                form.method = "POST";
14541d05cddcSAtari911                form.action = "?do=admin&page=calendar&tab=manage";
14551d05cddcSAtari911
14561d05cddcSAtari911                const actionInput = document.createElement("input");
14571d05cddcSAtari911                actionInput.type = "hidden";
14581d05cddcSAtari911                actionInput.name = "action";
14591d05cddcSAtari911                actionInput.value = "move_selected_events";
14601d05cddcSAtari911                form.appendChild(actionInput);
14611d05cddcSAtari911
14621d05cddcSAtari911                checkedBoxes.forEach(cb => {
14631d05cddcSAtari911                    const eventInput = document.createElement("input");
14641d05cddcSAtari911                    eventInput.type = "hidden";
14651d05cddcSAtari911                    eventInput.name = "events[]";
14661d05cddcSAtari911                    eventInput.value = cb.value;
14671d05cddcSAtari911                    form.appendChild(eventInput);
14681d05cddcSAtari911                });
14691d05cddcSAtari911
14701d05cddcSAtari911                const targetInput = document.createElement("input");
14711d05cddcSAtari911                targetInput.type = "hidden";
14721d05cddcSAtari911                targetInput.name = "target_namespace";
14731d05cddcSAtari911                targetInput.value = targetNamespace;
14741d05cddcSAtari911                form.appendChild(targetInput);
14751d05cddcSAtari911
14761d05cddcSAtari911                document.body.appendChild(form);
14771d05cddcSAtari911                form.submit();
14781d05cddcSAtari911            } else {
14791d05cddcSAtari911                // Move single event
14801d05cddcSAtari911                if (!draggedEvent) return;
14811d05cddcSAtari911                const parts = draggedEvent.split("|");
14821d05cddcSAtari911                const sourceNamespace = parts[1];
14831d05cddcSAtari911                if (sourceNamespace === targetNamespace) return;
14841d05cddcSAtari911
14851d05cddcSAtari911                const form = document.createElement("form");
14861d05cddcSAtari911                form.method = "POST";
14871d05cddcSAtari911                form.action = "?do=admin&page=calendar&tab=manage";
14881d05cddcSAtari911                const actionInput = document.createElement("input");
14891d05cddcSAtari911                actionInput.type = "hidden";
14901d05cddcSAtari911                actionInput.name = "action";
14911d05cddcSAtari911                actionInput.value = "move_single_event";
14921d05cddcSAtari911                form.appendChild(actionInput);
14931d05cddcSAtari911                const eventInput = document.createElement("input");
14941d05cddcSAtari911                eventInput.type = "hidden";
14951d05cddcSAtari911                eventInput.name = "event";
14961d05cddcSAtari911                eventInput.value = draggedEvent;
14971d05cddcSAtari911                form.appendChild(eventInput);
14981d05cddcSAtari911                const targetInput = document.createElement("input");
14991d05cddcSAtari911                targetInput.type = "hidden";
15001d05cddcSAtari911                targetInput.name = "target_namespace";
15011d05cddcSAtari911                targetInput.value = targetNamespace;
15021d05cddcSAtari911                form.appendChild(targetInput);
15031d05cddcSAtari911                document.body.appendChild(form);
15041d05cddcSAtari911                form.submit();
15051d05cddcSAtari911            }
15061d05cddcSAtari911        }
15071d05cddcSAtari911
15081d05cddcSAtari911        function editRecurringSeries(title, namespace) {
1509*9ccd446eSAtari911            // Get available namespaces from the namespace explorer
1510*9ccd446eSAtari911            const namespaces = new Set();
15111d05cddcSAtari911
1512*9ccd446eSAtari911            // Method 1: Try to get from namespace explorer folder names
1513*9ccd446eSAtari911            document.querySelectorAll("[id^=ns_]").forEach(el => {
1514*9ccd446eSAtari911                const nsSpan = el.querySelector("span:nth-child(3)");
1515*9ccd446eSAtari911                if (nsSpan) {
1516*9ccd446eSAtari911                    let nsText = nsSpan.textContent.replace("�� ", "").trim();
1517*9ccd446eSAtari911                    if (nsText && nsText !== "(default)") {
1518*9ccd446eSAtari911                        namespaces.add(nsText);
1519*9ccd446eSAtari911                    }
1520*9ccd446eSAtari911                }
1521*9ccd446eSAtari911            });
1522*9ccd446eSAtari911
1523*9ccd446eSAtari911            // Method 2: Get from datalist if it exists
1524*9ccd446eSAtari911            document.querySelectorAll("#namespaceList option").forEach(opt => {
1525*9ccd446eSAtari911                if (opt.value && opt.value !== "") {
1526*9ccd446eSAtari911                    namespaces.add(opt.value);
1527*9ccd446eSAtari911                }
1528*9ccd446eSAtari911            });
1529*9ccd446eSAtari911
1530*9ccd446eSAtari911            // Convert to sorted array
1531*9ccd446eSAtari911            const nsArray = Array.from(namespaces).sort();
1532*9ccd446eSAtari911
1533*9ccd446eSAtari911            // Build options - include current namespace AND all others
1534*9ccd446eSAtari911            let nsOptions = "<option value=\\"\\">(default)</option>";
1535*9ccd446eSAtari911
1536*9ccd446eSAtari911            // Add current namespace if it\'s not default
1537*9ccd446eSAtari911            if (namespace && namespace !== "") {
1538*9ccd446eSAtari911                nsOptions += "<option value=\\"" + namespace + "\\" selected>" + namespace + " (current)</option>";
1539*9ccd446eSAtari911            }
1540*9ccd446eSAtari911
1541*9ccd446eSAtari911            // Add all other namespaces
1542*9ccd446eSAtari911            for (const ns of nsArray) {
1543*9ccd446eSAtari911                if (ns !== namespace) {
1544*9ccd446eSAtari911                    nsOptions += "<option value=\\"" + ns + "\\">" + ns + "</option>";
15451d05cddcSAtari911                }
15461d05cddcSAtari911            }
15471d05cddcSAtari911
15481d05cddcSAtari911            // Show edit dialog for recurring events
15491d05cddcSAtari911            const dialog = document.createElement("div");
15501d05cddcSAtari911            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;";
15511d05cddcSAtari911
15521d05cddcSAtari911            // Close on clicking background
15531d05cddcSAtari911            dialog.addEventListener("click", function(e) {
15541d05cddcSAtari911                if (e.target === dialog) {
15551d05cddcSAtari911                    dialog.remove();
15561d05cddcSAtari911                }
15571d05cddcSAtari911            });
15581d05cddcSAtari911
15591d05cddcSAtari911            dialog.innerHTML = `
1560*9ccd446eSAtari911                <div style="background:' . $colors['bg'] . '; padding:20px; border-radius:8px; min-width:500px; max-width:700px; max-height:90vh; overflow-y:auto;">
15611d05cddcSAtari911                    <h3 style="margin:0 0 15px; color:#00cc07;">Edit Recurring Event</h3>
1562*9ccd446eSAtari911                    <p style="margin:0 0 15px; color:' . $colors['text'] . '; font-size:13px;">Changes will apply to ALL occurrences of: <strong>${title}</strong></p>
15631d05cddcSAtari911
15641d05cddcSAtari911                    <form id="editRecurringForm" style="display:flex; flex-direction:column; gap:12px;">
15651d05cddcSAtari911                        <div>
15661d05cddcSAtari911                            <label style="display:block; font-weight:bold; margin-bottom:4px; font-size:13px;">New Title:</label>
1567*9ccd446eSAtari911                            <input type="text" name="new_title" value="${title}" style="width:100%; padding:8px; border:1px solid ' . $colors['border'] . '; border-radius:3px;" required>
15681d05cddcSAtari911                        </div>
15691d05cddcSAtari911
15701d05cddcSAtari911                        <div style="display:grid; grid-template-columns:1fr 1fr; gap:10px;">
15711d05cddcSAtari911                            <div>
15721d05cddcSAtari911                                <label style="display:block; font-weight:bold; margin-bottom:4px; font-size:13px;">Start Time:</label>
1573*9ccd446eSAtari911                                <input type="time" name="start_time" style="width:100%; padding:8px; border:1px solid ' . $colors['border'] . '; border-radius:3px;">
15741d05cddcSAtari911                                <small style="color:#999; font-size:11px;">Leave blank to keep current</small>
15751d05cddcSAtari911                            </div>
15761d05cddcSAtari911                            <div>
15771d05cddcSAtari911                                <label style="display:block; font-weight:bold; margin-bottom:4px; font-size:13px;">End Time:</label>
1578*9ccd446eSAtari911                                <input type="time" name="end_time" style="width:100%; padding:8px; border:1px solid ' . $colors['border'] . '; border-radius:3px;">
15791d05cddcSAtari911                                <small style="color:#999; font-size:11px;">Leave blank to keep current</small>
15801d05cddcSAtari911                            </div>
15811d05cddcSAtari911                        </div>
15821d05cddcSAtari911
15831d05cddcSAtari911                        <div>
15841d05cddcSAtari911                            <label style="display:block; font-weight:bold; margin-bottom:4px; font-size:13px;">Interval (days between occurrences):</label>
1585*9ccd446eSAtari911                            <select name="interval" style="width:100%; padding:8px; border:1px solid ' . $colors['border'] . '; border-radius:3px;">
15861d05cddcSAtari911                                <option value="">Keep current interval</option>
15871d05cddcSAtari911                                <option value="1">Daily (1 day)</option>
15881d05cddcSAtari911                                <option value="7">Weekly (7 days)</option>
15891d05cddcSAtari911                                <option value="14">Bi-weekly (14 days)</option>
15901d05cddcSAtari911                                <option value="30">Monthly (30 days)</option>
15911d05cddcSAtari911                                <option value="365">Yearly (365 days)</option>
15921d05cddcSAtari911                            </select>
15931d05cddcSAtari911                        </div>
15941d05cddcSAtari911
15951d05cddcSAtari911                        <div>
15961d05cddcSAtari911                            <label style="display:block; font-weight:bold; margin-bottom:4px; font-size:13px;">Move to Namespace:</label>
1597*9ccd446eSAtari911                            <select name="new_namespace" style="width:100%; padding:8px; border:1px solid ' . $colors['border'] . '; border-radius:3px;">
15981d05cddcSAtari911                                ${nsOptions}
15991d05cddcSAtari911                            </select>
16001d05cddcSAtari911                        </div>
16011d05cddcSAtari911
16021d05cddcSAtari911                        <div style="display:flex; gap:10px; margin-top:10px;">
16031d05cddcSAtari911                            <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>
16041d05cddcSAtari911                            <button type="button" onclick="closeEditDialog()" style="flex:1; background:#999; color:white; padding:10px; border:none; border-radius:3px; cursor:pointer;">Cancel</button>
16051d05cddcSAtari911                        </div>
16061d05cddcSAtari911                    </form>
16071d05cddcSAtari911                </div>
16081d05cddcSAtari911            `;
16091d05cddcSAtari911
16101d05cddcSAtari911            document.body.appendChild(dialog);
16111d05cddcSAtari911
16121d05cddcSAtari911            // Add close function to window
16131d05cddcSAtari911            window.closeEditDialog = function() {
16141d05cddcSAtari911                dialog.remove();
16151d05cddcSAtari911            };
16161d05cddcSAtari911
16171d05cddcSAtari911            // Handle form submission
16181d05cddcSAtari911            dialog.querySelector("#editRecurringForm").addEventListener("submit", function(e) {
16191d05cddcSAtari911                e.preventDefault();
16201d05cddcSAtari911                const formData = new FormData(this);
16211d05cddcSAtari911
16221d05cddcSAtari911                // Submit the edit
16231d05cddcSAtari911                const form = document.createElement("form");
16241d05cddcSAtari911                form.method = "POST";
16251d05cddcSAtari911                form.action = "?do=admin&page=calendar&tab=manage";
16261d05cddcSAtari911
16271d05cddcSAtari911                const actionInput = document.createElement("input");
16281d05cddcSAtari911                actionInput.type = "hidden";
16291d05cddcSAtari911                actionInput.name = "action";
16301d05cddcSAtari911                actionInput.value = "edit_recurring_series";
16311d05cddcSAtari911                form.appendChild(actionInput);
16321d05cddcSAtari911
16331d05cddcSAtari911                const oldTitleInput = document.createElement("input");
16341d05cddcSAtari911                oldTitleInput.type = "hidden";
16351d05cddcSAtari911                oldTitleInput.name = "old_title";
16361d05cddcSAtari911                oldTitleInput.value = title;
16371d05cddcSAtari911                form.appendChild(oldTitleInput);
16381d05cddcSAtari911
16391d05cddcSAtari911                const oldNamespaceInput = document.createElement("input");
16401d05cddcSAtari911                oldNamespaceInput.type = "hidden";
16411d05cddcSAtari911                oldNamespaceInput.name = "old_namespace";
16421d05cddcSAtari911                oldNamespaceInput.value = namespace;
16431d05cddcSAtari911                form.appendChild(oldNamespaceInput);
16441d05cddcSAtari911
16451d05cddcSAtari911                // Add all form fields
16461d05cddcSAtari911                for (let [key, value] of formData.entries()) {
16471d05cddcSAtari911                    const input = document.createElement("input");
16481d05cddcSAtari911                    input.type = "hidden";
16491d05cddcSAtari911                    input.name = key;
16501d05cddcSAtari911                    input.value = value;
16511d05cddcSAtari911                    form.appendChild(input);
16521d05cddcSAtari911                }
16531d05cddcSAtari911
16541d05cddcSAtari911                document.body.appendChild(form);
16551d05cddcSAtari911                form.submit();
16561d05cddcSAtari911            });
16571d05cddcSAtari911        }
16581d05cddcSAtari911
16591d05cddcSAtari911        function deleteRecurringSeries(title, namespace) {
16601d05cddcSAtari911            const displayNs = namespace || "(default)";
16611d05cddcSAtari911            if (!confirm("Delete ALL occurrences of: " + title + " (" + displayNs + ")?\\n\\nThis cannot be undone!")) {
16621d05cddcSAtari911                return;
16631d05cddcSAtari911            }
16641d05cddcSAtari911            const form = document.createElement("form");
16651d05cddcSAtari911            form.method = "POST";
16661d05cddcSAtari911            form.action = "?do=admin&page=calendar&tab=manage";
16671d05cddcSAtari911            const actionInput = document.createElement("input");
16681d05cddcSAtari911            actionInput.type = "hidden";
16691d05cddcSAtari911            actionInput.name = "action";
16701d05cddcSAtari911            actionInput.value = "delete_recurring_series";
16711d05cddcSAtari911            form.appendChild(actionInput);
16721d05cddcSAtari911            const titleInput = document.createElement("input");
16731d05cddcSAtari911            titleInput.type = "hidden";
16741d05cddcSAtari911            titleInput.name = "event_title";
16751d05cddcSAtari911            titleInput.value = title;
16761d05cddcSAtari911            form.appendChild(titleInput);
16771d05cddcSAtari911            const namespaceInput = document.createElement("input");
16781d05cddcSAtari911            namespaceInput.type = "hidden";
16791d05cddcSAtari911            namespaceInput.name = "namespace";
16801d05cddcSAtari911            namespaceInput.value = namespace;
16811d05cddcSAtari911            form.appendChild(namespaceInput);
16821d05cddcSAtari911            document.body.appendChild(form);
16831d05cddcSAtari911            form.submit();
16841d05cddcSAtari911        }
16851d05cddcSAtari911
16861d05cddcSAtari911        document.addEventListener("dragend", function(e) {
16871d05cddcSAtari911            if (e.target.draggable) {
16881d05cddcSAtari911                e.target.style.opacity = "1";
16891d05cddcSAtari911            }
16901d05cddcSAtari911        });
16911d05cddcSAtari911        </script>';
16921d05cddcSAtari911    }
16931d05cddcSAtari911
1694*9ccd446eSAtari911    private function renderUpdateTab($colors = null) {
16951d05cddcSAtari911        global $INPUT;
16961d05cddcSAtari911
1697*9ccd446eSAtari911        // Use defaults if not provided
1698*9ccd446eSAtari911        if ($colors === null) {
1699*9ccd446eSAtari911            $colors = $this->getTemplateColors();
1700*9ccd446eSAtari911        }
17011d05cddcSAtari911
1702*9ccd446eSAtari911        echo '<h2 style="margin:10px 0; font-size:20px;">�� Update Plugin</h2>';
17031d05cddcSAtari911
17041d05cddcSAtari911        // Show message if present
17051d05cddcSAtari911        if ($INPUT->has('msg')) {
17061d05cddcSAtari911            $msg = hsc($INPUT->str('msg'));
17071d05cddcSAtari911            $type = $INPUT->str('msgtype', 'success');
17081d05cddcSAtari911            $class = ($type === 'success') ? 'msg success' : 'msg error';
1709*9ccd446eSAtari911            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;\">";
17101d05cddcSAtari911            echo $msg;
17111d05cddcSAtari911            echo "</div>";
17121d05cddcSAtari911        }
17131d05cddcSAtari911
1714*9ccd446eSAtari911        // Show current version FIRST (MOVED TO TOP)
17151d05cddcSAtari911        $pluginInfo = DOKU_PLUGIN . 'calendar/plugin.info.txt';
17161d05cddcSAtari911        $info = ['version' => 'Unknown', 'date' => 'Unknown', 'name' => 'Calendar Plugin', 'author' => 'Unknown', 'email' => '', 'desc' => ''];
17171d05cddcSAtari911        if (file_exists($pluginInfo)) {
17181d05cddcSAtari911            $info = array_merge($info, confToHash($pluginInfo));
17191d05cddcSAtari911        }
17201d05cddcSAtari911
1721*9ccd446eSAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:1200px;">';
1722*9ccd446eSAtari911        echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">�� Current Version</h3>';
17231d05cddcSAtari911        echo '<div style="font-size:12px; line-height:1.6;">';
17241d05cddcSAtari911        echo '<div style="margin:4px 0;"><strong>Version:</strong> ' . hsc($info['version']) . ' (' . hsc($info['date']) . ')</div>';
17251d05cddcSAtari911        echo '<div style="margin:4px 0;"><strong>Author:</strong> ' . hsc($info['author']) . ($info['email'] ? ' &lt;' . hsc($info['email']) . '&gt;' : '') . '</div>';
17261d05cddcSAtari911        if ($info['desc']) {
17271d05cddcSAtari911            echo '<div style="margin:4px 0;"><strong>Description:</strong> ' . hsc($info['desc']) . '</div>';
17281d05cddcSAtari911        }
17291d05cddcSAtari911        echo '<div style="margin:4px 0;"><strong>Location:</strong> <code style="background:#f0f0f0; padding:2px 4px; border-radius:2px;">' . DOKU_PLUGIN . 'calendar/</code></div>';
17301d05cddcSAtari911        echo '</div>';
17311d05cddcSAtari911
17321d05cddcSAtari911        // Check permissions
17331d05cddcSAtari911        $pluginDir = DOKU_PLUGIN . 'calendar/';
17341d05cddcSAtari911        $pluginWritable = is_writable($pluginDir);
17351d05cddcSAtari911        $parentWritable = is_writable(DOKU_PLUGIN);
17361d05cddcSAtari911
1737*9ccd446eSAtari911        echo '<div style="margin-top:8px; padding-top:8px; border-top:1px solid ' . $colors['border'] . ';">';
17381d05cddcSAtari911        if ($pluginWritable && $parentWritable) {
17391d05cddcSAtari911            echo '<p style="margin:5px 0; font-size:13px; color:#28a745;"><strong>✅ Permissions:</strong> OK - ready to update</p>';
17401d05cddcSAtari911        } else {
17411d05cddcSAtari911            echo '<p style="margin:5px 0; font-size:13px; color:#dc3545;"><strong>❌ Permissions:</strong> Issues detected</p>';
17421d05cddcSAtari911            if (!$pluginWritable) {
17431d05cddcSAtari911                echo '<p style="margin:2px 0 2px 20px; font-size:12px; color:#dc3545;">Plugin directory not writable</p>';
17441d05cddcSAtari911            }
17451d05cddcSAtari911            if (!$parentWritable) {
17461d05cddcSAtari911                echo '<p style="margin:2px 0 2px 20px; font-size:12px; color:#dc3545;">Parent directory not writable</p>';
17471d05cddcSAtari911            }
1748*9ccd446eSAtari911            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>';
1749*9ccd446eSAtari911            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>';
17501d05cddcSAtari911        }
17511d05cddcSAtari911        echo '</div>';
17521d05cddcSAtari911
17531d05cddcSAtari911        echo '</div>';
17541d05cddcSAtari911
1755*9ccd446eSAtari911        // Combined upload and notes section (SIDE BY SIDE)
1756*9ccd446eSAtari911        echo '<div style="display:flex; gap:15px; max-width:1200px; margin:10px 0;">';
17571d05cddcSAtari911
1758*9ccd446eSAtari911        // Left side - Upload form (60% width)
1759*9ccd446eSAtari911        echo '<div style="flex:1; min-width:0; background:' . $colors['bg'] . '; padding:12px; border-left:3px solid #00cc07; border-radius:3px;">';
1760*9ccd446eSAtari911        echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">�� Upload New Version</h3>';
1761*9ccd446eSAtari911        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>';
17621d05cddcSAtari911
17631d05cddcSAtari911        echo '<form method="post" action="?do=admin&page=calendar&tab=update" enctype="multipart/form-data" id="uploadForm">';
17641d05cddcSAtari911        echo '<input type="hidden" name="action" value="upload_update">';
17651d05cddcSAtari911        echo '<div style="margin:10px 0;">';
1766*9ccd446eSAtari911        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%;">';
17671d05cddcSAtari911        echo '</div>';
17681d05cddcSAtari911        echo '<div style="margin:10px 0;">';
17691d05cddcSAtari911        echo '<label style="display:flex; align-items:center; gap:8px; font-size:13px;">';
17701d05cddcSAtari911        echo '<input type="checkbox" name="backup_first" value="1" checked>';
17711d05cddcSAtari911        echo '<span>Create backup before updating (Recommended)</span>';
17721d05cddcSAtari911        echo '</label>';
17731d05cddcSAtari911        echo '</div>';
1774*9ccd446eSAtari911
1775*9ccd446eSAtari911        // Buttons side by side
1776*9ccd446eSAtari911        echo '<div style="display:flex; gap:10px; align-items:center; flex-wrap:wrap;">';
17771d05cddcSAtari911        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>';
17781d05cddcSAtari911        echo '</form>';
1779*9ccd446eSAtari911
1780*9ccd446eSAtari911        // Clear Cache button (next to Upload button)
1781*9ccd446eSAtari911        echo '<form method="post" action="?do=admin&page=calendar&tab=update" style="display:inline; margin:0;">';
1782*9ccd446eSAtari911        echo '<input type="hidden" name="action" value="clear_cache">';
1783*9ccd446eSAtari911        echo '<input type="hidden" name="tab" value="update">';
1784*9ccd446eSAtari911        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>';
1785*9ccd446eSAtari911        echo '</form>';
17861d05cddcSAtari911        echo '</div>';
17871d05cddcSAtari911
1788*9ccd446eSAtari911        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>';
1789*9ccd446eSAtari911        echo '</div>';
1790*9ccd446eSAtari911
1791*9ccd446eSAtari911        // Right side - Important Notes (40% width)
1792*9ccd446eSAtari911        echo '<div style="flex:0 0 350px; min-width:0; background:#fff3e0; border-left:3px solid #ff9800; padding:12px; border-radius:3px;">';
17931d05cddcSAtari911        echo '<h4 style="margin:0 0 5px 0; color:#e65100; font-size:14px;">⚠️ Important Notes</h4>';
1794*9ccd446eSAtari911        echo '<ul style="margin:5px 0; padding-left:20px; font-size:12px; color:#e65100; line-height:1.6;">';
17951d05cddcSAtari911        echo '<li>This will replace all plugin files</li>';
17961d05cddcSAtari911        echo '<li>Configuration files (sync_config.php) will be preserved</li>';
17971d05cddcSAtari911        echo '<li>Event data will not be affected</li>';
1798*9ccd446eSAtari911        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>';
17991d05cddcSAtari911        echo '<li>Make sure the ZIP file is a valid calendar plugin</li>';
18001d05cddcSAtari911        echo '</ul>';
18011d05cddcSAtari911        echo '</div>';
18021d05cddcSAtari911
1803*9ccd446eSAtari911        echo '</div>'; // End flex container
1804*9ccd446eSAtari911
1805*9ccd446eSAtari911        // Changelog section - Timeline viewer
1806*9ccd446eSAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #7b1fa2; border-radius:3px; max-width:1200px;">';
1807*9ccd446eSAtari911        echo '<h3 style="margin:0 0 8px 0; color:#7b1fa2; font-size:16px;">�� Version History</h3>';
1808*9ccd446eSAtari911
1809*9ccd446eSAtari911        $changelogFile = DOKU_PLUGIN . 'calendar/CHANGELOG.md';
1810*9ccd446eSAtari911        if (file_exists($changelogFile)) {
1811*9ccd446eSAtari911            $changelog = file_get_contents($changelogFile);
1812*9ccd446eSAtari911
1813*9ccd446eSAtari911            // Parse ALL versions into structured data
1814*9ccd446eSAtari911            $lines = explode("\n", $changelog);
1815*9ccd446eSAtari911            $versions = [];
1816*9ccd446eSAtari911            $currentVersion = null;
1817*9ccd446eSAtari911
1818*9ccd446eSAtari911            foreach ($lines as $line) {
1819*9ccd446eSAtari911                $line = trim($line);
1820*9ccd446eSAtari911
1821*9ccd446eSAtari911                // Version header (## Version X.X.X or ## Version X.X.X (date) - title)
1822*9ccd446eSAtari911                if (preg_match('/^## Version (.+?)(?:\s*\(([^)]+)\))?\s*(?:-\s*(.+))?$/', $line, $matches)) {
1823*9ccd446eSAtari911                    if ($currentVersion !== null) {
1824*9ccd446eSAtari911                        $versions[] = $currentVersion;
1825*9ccd446eSAtari911                    }
1826*9ccd446eSAtari911                    $currentVersion = [
1827*9ccd446eSAtari911                        'number' => trim($matches[1]),
1828*9ccd446eSAtari911                        'date' => isset($matches[2]) ? trim($matches[2]) : '',
1829*9ccd446eSAtari911                        'title' => isset($matches[3]) ? trim($matches[3]) : '',
1830*9ccd446eSAtari911                        'items' => []
1831*9ccd446eSAtari911                    ];
1832*9ccd446eSAtari911                }
1833*9ccd446eSAtari911                // List items (- **Type:** description)
1834*9ccd446eSAtari911                elseif ($currentVersion !== null && preg_match('/^- \*\*(.+?):\*\* (.+)$/', $line, $matches)) {
1835*9ccd446eSAtari911                    $currentVersion['items'][] = [
1836*9ccd446eSAtari911                        'type' => $matches[1],
1837*9ccd446eSAtari911                        'desc' => $matches[2]
1838*9ccd446eSAtari911                    ];
1839*9ccd446eSAtari911                }
1840*9ccd446eSAtari911            }
1841*9ccd446eSAtari911            // Don\'t forget last version
1842*9ccd446eSAtari911            if ($currentVersion !== null) {
1843*9ccd446eSAtari911                $versions[] = $currentVersion;
1844*9ccd446eSAtari911            }
1845*9ccd446eSAtari911
1846*9ccd446eSAtari911            $totalVersions = count($versions);
1847*9ccd446eSAtari911            $uniqueId = 'changelog_' . substr(md5(microtime()), 0, 6);
1848*9ccd446eSAtari911
1849*9ccd446eSAtari911            if ($totalVersions > 0) {
1850*9ccd446eSAtari911                // Timeline navigation bar
1851*9ccd446eSAtari911                echo '<div id="' . $uniqueId . '_wrap" style="position:relative;">';
1852*9ccd446eSAtari911
1853*9ccd446eSAtari911                // Nav controls
1854*9ccd446eSAtari911                echo '<div style="display:flex; align-items:center; gap:8px; margin-bottom:10px;">';
1855*9ccd446eSAtari911                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>';
1856*9ccd446eSAtari911                echo '<div style="flex:1; text-align:center;">';
1857*9ccd446eSAtari911                echo '<span id="' . $uniqueId . '_counter" style="font-size:11px; color:' . $colors['text'] . '; opacity:0.7;">1 of ' . $totalVersions . '</span>';
1858*9ccd446eSAtari911                echo '</div>';
1859*9ccd446eSAtari911                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>';
1860*9ccd446eSAtari911                echo '</div>';
1861*9ccd446eSAtari911
1862*9ccd446eSAtari911                // Version cards (one per version, only first visible)
1863*9ccd446eSAtari911                foreach ($versions as $i => $ver) {
1864*9ccd446eSAtari911                    $display = ($i === 0) ? 'block' : 'none';
1865*9ccd446eSAtari911                    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;">';
1866*9ccd446eSAtari911
1867*9ccd446eSAtari911                    // Version header
1868*9ccd446eSAtari911                    echo '<div style="display:flex; align-items:baseline; gap:8px; margin-bottom:8px;">';
1869*9ccd446eSAtari911                    echo '<span style="font-weight:bold; color:#7b1fa2; font-size:14px;">v' . hsc($ver['number']) . '</span>';
1870*9ccd446eSAtari911                    if ($ver['date']) {
1871*9ccd446eSAtari911                        echo '<span style="font-size:11px; color:' . $colors['text'] . '; opacity:0.6;">' . hsc($ver['date']) . '</span>';
1872*9ccd446eSAtari911                    }
1873*9ccd446eSAtari911                    echo '</div>';
1874*9ccd446eSAtari911                    if ($ver['title']) {
1875*9ccd446eSAtari911                        echo '<div style="font-size:12px; font-weight:600; color:' . $colors['text'] . '; margin-bottom:8px;">' . hsc($ver['title']) . '</div>';
1876*9ccd446eSAtari911                    }
1877*9ccd446eSAtari911
1878*9ccd446eSAtari911                    // Change items
1879*9ccd446eSAtari911                    if (!empty($ver['items'])) {
1880*9ccd446eSAtari911                        echo '<div style="font-size:12px; line-height:1.7;">';
1881*9ccd446eSAtari911                        foreach ($ver['items'] as $item) {
1882*9ccd446eSAtari911                            $color = '#666'; $icon = '•';
1883*9ccd446eSAtari911                            $t = $item['type'];
1884*9ccd446eSAtari911                            if ($t === 'Added') { $color = '#28a745'; $icon = '✨'; }
1885*9ccd446eSAtari911                            elseif ($t === 'Fixed') { $color = '#dc3545'; $icon = '��'; }
1886*9ccd446eSAtari911                            elseif ($t === 'Changed') { $color = '#7b1fa2'; $icon = '��'; }
1887*9ccd446eSAtari911                            elseif ($t === 'Improved') { $color = '#ff9800'; $icon = '⚡'; }
1888*9ccd446eSAtari911                            elseif ($t === 'Removed') { $color = '#e91e63'; $icon = '��️'; }
1889*9ccd446eSAtari911                            elseif ($t === 'Development' || $t === 'Refactored') { $color = '#6c757d'; $icon = '��️'; }
1890*9ccd446eSAtari911                            elseif ($t === 'Result') { $color = '#2196f3'; $icon = '✅'; }
1891*9ccd446eSAtari911
1892*9ccd446eSAtari911                            echo '<div style="margin:2px 0; padding-left:4px;">';
1893*9ccd446eSAtari911                            echo '<span style="color:' . $color . '; font-weight:600;">' . $icon . ' ' . hsc($item['type']) . ':</span> ';
1894*9ccd446eSAtari911                            echo '<span style="color:' . $colors['text'] . ';">' . hsc($item['desc']) . '</span>';
1895*9ccd446eSAtari911                            echo '</div>';
1896*9ccd446eSAtari911                        }
1897*9ccd446eSAtari911                        echo '</div>';
1898*9ccd446eSAtari911                    } else {
1899*9ccd446eSAtari911                        echo '<div style="font-size:11px; color:' . $colors['text'] . '; opacity:0.5; font-style:italic;">No detailed changes recorded</div>';
1900*9ccd446eSAtari911                    }
1901*9ccd446eSAtari911
1902*9ccd446eSAtari911                    echo '</div>';
1903*9ccd446eSAtari911                }
1904*9ccd446eSAtari911
1905*9ccd446eSAtari911                echo '</div>'; // wrap
1906*9ccd446eSAtari911
1907*9ccd446eSAtari911                // JavaScript for navigation
1908*9ccd446eSAtari911                echo '<script>
1909*9ccd446eSAtari911                (function() {
1910*9ccd446eSAtari911                    var id = "' . $uniqueId . '";
1911*9ccd446eSAtari911                    var total = ' . $totalVersions . ';
1912*9ccd446eSAtari911                    var current = 0;
1913*9ccd446eSAtari911
1914*9ccd446eSAtari911                    window.changelogNav = function(uid, dir) {
1915*9ccd446eSAtari911                        if (uid !== id) return;
1916*9ccd446eSAtari911                        var next = current + dir;
1917*9ccd446eSAtari911                        if (next < 0 || next >= total) return;
1918*9ccd446eSAtari911
1919*9ccd446eSAtari911                        // Hide current
1920*9ccd446eSAtari911                        var curCard = document.getElementById(id + "_card_" + current);
1921*9ccd446eSAtari911                        if (curCard) curCard.style.display = "none";
1922*9ccd446eSAtari911
1923*9ccd446eSAtari911                        // Show next
1924*9ccd446eSAtari911                        current = next;
1925*9ccd446eSAtari911                        var nextCard = document.getElementById(id + "_card_" + current);
1926*9ccd446eSAtari911                        if (nextCard) nextCard.style.display = "block";
1927*9ccd446eSAtari911
1928*9ccd446eSAtari911                        // Update counter
1929*9ccd446eSAtari911                        var counter = document.getElementById(id + "_counter");
1930*9ccd446eSAtari911                        if (counter) counter.textContent = (current + 1) + " of " + total;
1931*9ccd446eSAtari911
1932*9ccd446eSAtari911                        // Update button states
1933*9ccd446eSAtari911                        var prevBtn = document.getElementById(id + "_prev");
1934*9ccd446eSAtari911                        var nextBtn = document.getElementById(id + "_next");
1935*9ccd446eSAtari911                        if (prevBtn) prevBtn.style.opacity = (current === 0) ? "0.3" : "1";
1936*9ccd446eSAtari911                        if (nextBtn) nextBtn.style.opacity = (current === total - 1) ? "0.3" : "1";
1937*9ccd446eSAtari911                    };
1938*9ccd446eSAtari911
1939*9ccd446eSAtari911                    // Initialize button states
1940*9ccd446eSAtari911                    var prevBtn = document.getElementById(id + "_prev");
1941*9ccd446eSAtari911                    if (prevBtn) prevBtn.style.opacity = "0.3";
1942*9ccd446eSAtari911                })();
1943*9ccd446eSAtari911                </script>';
1944*9ccd446eSAtari911
1945*9ccd446eSAtari911            } else {
1946*9ccd446eSAtari911                echo '<p style="color:#999; font-size:13px; font-style:italic;">No versions found in changelog</p>';
1947*9ccd446eSAtari911            }
1948*9ccd446eSAtari911        } else {
1949*9ccd446eSAtari911            echo '<p style="color:#999; font-size:13px; font-style:italic;">Changelog not available</p>';
1950*9ccd446eSAtari911        }
1951*9ccd446eSAtari911
1952*9ccd446eSAtari911        echo '</div>';
1953*9ccd446eSAtari911
1954*9ccd446eSAtari911        // Backup list or manual backup section
19551d05cddcSAtari911        $backupDir = DOKU_PLUGIN;
19561d05cddcSAtari911        $backups = glob($backupDir . 'calendar*.zip');
19571d05cddcSAtari911
19581d05cddcSAtari911        // Filter to only show files that look like backups (not the uploaded plugin files)
19591d05cddcSAtari911        $backups = array_filter($backups, function($file) {
19601d05cddcSAtari911            $name = basename($file);
19611d05cddcSAtari911            // Include files that start with "calendar" but exclude files that are just "calendar.zip" (uploaded plugin)
19621d05cddcSAtari911            return $name !== 'calendar.zip';
19631d05cddcSAtari911        });
19641d05cddcSAtari911
1965*9ccd446eSAtari911        // Always show backup section (even if no backups yet)
1966*9ccd446eSAtari911        echo '<div id="backupSection" style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:900px;">';
1967*9ccd446eSAtari911        echo '<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:8px;">';
1968*9ccd446eSAtari911        echo '<h3 style="margin:0; color:#00cc07; font-size:16px;">�� Backups</h3>';
1969*9ccd446eSAtari911
1970*9ccd446eSAtari911        // Manual backup button
1971*9ccd446eSAtari911        echo '<form method="post" action="?do=admin&page=calendar&tab=update" style="margin:0;">';
1972*9ccd446eSAtari911        echo '<input type="hidden" name="action" value="create_manual_backup">';
1973*9ccd446eSAtari911        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>';
1974*9ccd446eSAtari911        echo '</form>';
1975*9ccd446eSAtari911        echo '</div>';
1976*9ccd446eSAtari911
19771d05cddcSAtari911        if (!empty($backups)) {
19781d05cddcSAtari911            rsort($backups); // Newest first
1979*9ccd446eSAtari911            echo '<div style="max-height:200px; overflow-y:auto; border:1px solid ' . $colors['border'] . '; border-radius:3px; background:' . $colors['bg'] . ';">';
1980*9ccd446eSAtari911            echo '<table id="backupTable" style="width:100%; border-collapse:collapse; font-size:12px;">';
19811d05cddcSAtari911            echo '<thead style="position:sticky; top:0; background:#e9e9e9;">';
19821d05cddcSAtari911            echo '<tr>';
1983*9ccd446eSAtari911            echo '<th style="padding:6px; text-align:left; border-bottom:2px solid ' . $colors['border'] . ';">Backup File</th>';
1984*9ccd446eSAtari911            echo '<th style="padding:6px; text-align:left; border-bottom:2px solid ' . $colors['border'] . ';">Size</th>';
1985*9ccd446eSAtari911            echo '<th style="padding:6px; text-align:left; border-bottom:2px solid ' . $colors['border'] . ';">Actions</th>';
19861d05cddcSAtari911            echo '</tr></thead><tbody>';
19871d05cddcSAtari911
19881d05cddcSAtari911            foreach ($backups as $backup) {
19891d05cddcSAtari911                $filename = basename($backup);
19901d05cddcSAtari911                $size = $this->formatBytes(filesize($backup));
19911d05cddcSAtari911                echo '<tr style="border-bottom:1px solid #eee;">';
19921d05cddcSAtari911                echo '<td style="padding:6px;"><code style="font-size:11px;">' . hsc($filename) . '</code></td>';
19931d05cddcSAtari911                echo '<td style="padding:6px;">' . $size . '</td>';
19941d05cddcSAtari911                echo '<td style="padding:6px; white-space:nowrap;">';
19951d05cddcSAtari911                echo '<a href="' . DOKU_BASE . 'lib/plugins/' . hsc($filename) . '" download style="color:#00cc07; text-decoration:none; font-size:11px; margin-right:10px;">�� Download</a>';
19961d05cddcSAtari911                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>';
19971d05cddcSAtari911                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>';
19981d05cddcSAtari911                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>';
19991d05cddcSAtari911                echo '</td>';
20001d05cddcSAtari911                echo '</tr>';
20011d05cddcSAtari911            }
20021d05cddcSAtari911
20031d05cddcSAtari911            echo '</tbody></table>';
20041d05cddcSAtari911            echo '</div>';
2005*9ccd446eSAtari911        } else {
2006*9ccd446eSAtari911            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>';
20071d05cddcSAtari911        }
2008*9ccd446eSAtari911        echo '</div>';
20091d05cddcSAtari911
20101d05cddcSAtari911        echo '<script>
20111d05cddcSAtari911        function confirmUpload() {
20121d05cddcSAtari911            const fileInput = document.querySelector(\'input[name="plugin_zip"]\');
20131d05cddcSAtari911            if (!fileInput.files[0]) {
20141d05cddcSAtari911                alert("Please select a ZIP file");
20151d05cddcSAtari911                return false;
20161d05cddcSAtari911            }
20171d05cddcSAtari911
20181d05cddcSAtari911            const fileName = fileInput.files[0].name;
20191d05cddcSAtari911            if (!fileName.endsWith(".zip")) {
20201d05cddcSAtari911                alert("Please select a ZIP file");
20211d05cddcSAtari911                return false;
20221d05cddcSAtari911            }
20231d05cddcSAtari911
20241d05cddcSAtari911            return confirm("Upload and install: " + fileName + "?\\n\\nThis will replace all plugin files.\\nYour configuration and data will be preserved.\\n\\nContinue?");
20251d05cddcSAtari911        }
20261d05cddcSAtari911
20271d05cddcSAtari911        function deleteBackup(filename) {
20281d05cddcSAtari911            if (!confirm("Delete backup: " + filename + "?\\n\\nThis cannot be undone!")) {
20291d05cddcSAtari911                return;
20301d05cddcSAtari911            }
20311d05cddcSAtari911
2032*9ccd446eSAtari911            // Use AJAX to delete without page refresh
2033*9ccd446eSAtari911            const formData = new FormData();
2034*9ccd446eSAtari911            formData.append(\'action\', \'delete_backup\');
2035*9ccd446eSAtari911            formData.append(\'backup_file\', filename);
20361d05cddcSAtari911
2037*9ccd446eSAtari911            fetch(\'?do=admin&page=calendar&tab=update\', {
2038*9ccd446eSAtari911                method: \'POST\',
2039*9ccd446eSAtari911                body: formData
2040*9ccd446eSAtari911            })
2041*9ccd446eSAtari911            .then(response => response.text())
2042*9ccd446eSAtari911            .then(data => {
2043*9ccd446eSAtari911                // Remove the row from the table
2044*9ccd446eSAtari911                const rows = document.querySelectorAll(\'tr\');
2045*9ccd446eSAtari911                rows.forEach(row => {
2046*9ccd446eSAtari911                    if (row.textContent.includes(filename)) {
2047*9ccd446eSAtari911                        row.style.transition = \'opacity 0.3s\';
2048*9ccd446eSAtari911                        row.style.opacity = \'0\';
2049*9ccd446eSAtari911                        setTimeout(() => {
2050*9ccd446eSAtari911                            row.remove();
2051*9ccd446eSAtari911                            // Check if table is now empty
2052*9ccd446eSAtari911                            const tbody = document.querySelector(\'#backupTable tbody\');
2053*9ccd446eSAtari911                            if (tbody && tbody.children.length === 0) {
2054*9ccd446eSAtari911                                const backupSection = document.querySelector(\'#backupSection\');
2055*9ccd446eSAtari911                                if (backupSection) {
2056*9ccd446eSAtari911                                    backupSection.style.transition = \'opacity 0.3s\';
2057*9ccd446eSAtari911                                    backupSection.style.opacity = \'0\';
2058*9ccd446eSAtari911                                    setTimeout(() => backupSection.remove(), 300);
2059*9ccd446eSAtari911                                }
2060*9ccd446eSAtari911                            }
2061*9ccd446eSAtari911                        }, 300);
2062*9ccd446eSAtari911                    }
2063*9ccd446eSAtari911                });
20641d05cddcSAtari911
2065*9ccd446eSAtari911                // Show success message
2066*9ccd446eSAtari911                const msg = document.createElement(\'div\');
2067*9ccd446eSAtari911                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;\';
2068*9ccd446eSAtari911                msg.textContent = \'✓ Backup deleted: \' + filename;
2069*9ccd446eSAtari911                document.querySelector(\'h2\').after(msg);
2070*9ccd446eSAtari911                setTimeout(() => {
2071*9ccd446eSAtari911                    msg.style.opacity = \'0\';
2072*9ccd446eSAtari911                    setTimeout(() => msg.remove(), 300);
2073*9ccd446eSAtari911                }, 3000);
2074*9ccd446eSAtari911            })
2075*9ccd446eSAtari911            .catch(error => {
2076*9ccd446eSAtari911                alert(\'Error deleting backup: \' + error);
2077*9ccd446eSAtari911            });
20781d05cddcSAtari911        }
20791d05cddcSAtari911
20801d05cddcSAtari911        function restoreBackup(filename) {
20811d05cddcSAtari911            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?")) {
20821d05cddcSAtari911                return;
20831d05cddcSAtari911            }
20841d05cddcSAtari911
20851d05cddcSAtari911            const form = document.createElement("form");
20861d05cddcSAtari911            form.method = "POST";
20871d05cddcSAtari911            form.action = "?do=admin&page=calendar&tab=update";
20881d05cddcSAtari911
20891d05cddcSAtari911            const actionInput = document.createElement("input");
20901d05cddcSAtari911            actionInput.type = "hidden";
20911d05cddcSAtari911            actionInput.name = "action";
20921d05cddcSAtari911            actionInput.value = "restore_backup";
20931d05cddcSAtari911            form.appendChild(actionInput);
20941d05cddcSAtari911
20951d05cddcSAtari911            const filenameInput = document.createElement("input");
20961d05cddcSAtari911            filenameInput.type = "hidden";
20971d05cddcSAtari911            filenameInput.name = "backup_file";
20981d05cddcSAtari911            filenameInput.value = filename;
20991d05cddcSAtari911            form.appendChild(filenameInput);
21001d05cddcSAtari911
21011d05cddcSAtari911            document.body.appendChild(form);
21021d05cddcSAtari911            form.submit();
21031d05cddcSAtari911        }
21041d05cddcSAtari911
21051d05cddcSAtari911        function renameBackup(filename) {
21061d05cddcSAtari911            const newName = prompt("Enter new backup name (without .zip extension):\\n\\nCurrent: " + filename.replace(/\\.zip$/, ""), filename.replace(/\\.zip$/, ""));
21071d05cddcSAtari911            if (!newName || newName === filename.replace(/\\.zip$/, "")) {
21081d05cddcSAtari911                return;
21091d05cddcSAtari911            }
21101d05cddcSAtari911
21111d05cddcSAtari911            // Add .zip if not present
21121d05cddcSAtari911            const newFilename = newName.endsWith(".zip") ? newName : newName + ".zip";
21131d05cddcSAtari911
21141d05cddcSAtari911            // Basic validation
21151d05cddcSAtari911            if (!/^[a-zA-Z0-9._-]+$/.test(newFilename.replace(/\\.zip$/, ""))) {
21161d05cddcSAtari911                alert("Invalid filename. Use only letters, numbers, dots, dashes, and underscores.");
21171d05cddcSAtari911                return;
21181d05cddcSAtari911            }
21191d05cddcSAtari911
21201d05cddcSAtari911            const form = document.createElement("form");
21211d05cddcSAtari911            form.method = "POST";
21221d05cddcSAtari911            form.action = "?do=admin&page=calendar&tab=update";
21231d05cddcSAtari911
21241d05cddcSAtari911            const actionInput = document.createElement("input");
21251d05cddcSAtari911            actionInput.type = "hidden";
21261d05cddcSAtari911            actionInput.name = "action";
21271d05cddcSAtari911            actionInput.value = "rename_backup";
21281d05cddcSAtari911            form.appendChild(actionInput);
21291d05cddcSAtari911
21301d05cddcSAtari911            const oldNameInput = document.createElement("input");
21311d05cddcSAtari911            oldNameInput.type = "hidden";
21321d05cddcSAtari911            oldNameInput.name = "old_name";
21331d05cddcSAtari911            oldNameInput.value = filename;
21341d05cddcSAtari911            form.appendChild(oldNameInput);
21351d05cddcSAtari911
21361d05cddcSAtari911            const newNameInput = document.createElement("input");
21371d05cddcSAtari911            newNameInput.type = "hidden";
21381d05cddcSAtari911            newNameInput.name = "new_name";
21391d05cddcSAtari911            newNameInput.value = newFilename;
21401d05cddcSAtari911            form.appendChild(newNameInput);
21411d05cddcSAtari911
21421d05cddcSAtari911            document.body.appendChild(form);
21431d05cddcSAtari911            form.submit();
21441d05cddcSAtari911        }
21451d05cddcSAtari911        </script>';
21461d05cddcSAtari911    }
21471d05cddcSAtari911
21481d05cddcSAtari911    private function saveConfig() {
21491d05cddcSAtari911        global $INPUT;
21501d05cddcSAtari911
21511d05cddcSAtari911        // Load existing config to preserve all settings
21521d05cddcSAtari911        $configFile = DOKU_PLUGIN . 'calendar/sync_config.php';
21531d05cddcSAtari911        $existingConfig = [];
21541d05cddcSAtari911        if (file_exists($configFile)) {
21551d05cddcSAtari911            $existingConfig = include $configFile;
21561d05cddcSAtari911        }
21571d05cddcSAtari911
21581d05cddcSAtari911        // Update only the fields from the form - preserve everything else
21591d05cddcSAtari911        $config = $existingConfig;
21601d05cddcSAtari911
21611d05cddcSAtari911        // Update basic fields
21621d05cddcSAtari911        $config['tenant_id'] = $INPUT->str('tenant_id');
21631d05cddcSAtari911        $config['client_id'] = $INPUT->str('client_id');
21641d05cddcSAtari911        $config['client_secret'] = $INPUT->str('client_secret');
21651d05cddcSAtari911        $config['user_email'] = $INPUT->str('user_email');
21661d05cddcSAtari911        $config['timezone'] = $INPUT->str('timezone', 'America/Los_Angeles');
21671d05cddcSAtari911        $config['default_category'] = $INPUT->str('default_category', 'Blue category');
21681d05cddcSAtari911        $config['reminder_minutes'] = $INPUT->int('reminder_minutes', 15);
21691d05cddcSAtari911        $config['sync_completed_tasks'] = $INPUT->bool('sync_completed_tasks');
21701d05cddcSAtari911        $config['delete_outlook_events'] = $INPUT->bool('delete_outlook_events');
21711d05cddcSAtari911        $config['sync_all_namespaces'] = $INPUT->bool('sync_all_namespaces');
21721d05cddcSAtari911        $config['sync_namespaces'] = $INPUT->arr('sync_namespaces');
21731d05cddcSAtari911        $config['important_namespaces'] = $INPUT->str('important_namespaces', 'important');
21741d05cddcSAtari911
21751d05cddcSAtari911        // Parse category mapping
21761d05cddcSAtari911        $config['category_mapping'] = [];
21771d05cddcSAtari911        $mappingText = $INPUT->str('category_mapping');
21781d05cddcSAtari911        if ($mappingText) {
21791d05cddcSAtari911            $lines = explode("\n", $mappingText);
21801d05cddcSAtari911            foreach ($lines as $line) {
21811d05cddcSAtari911                $line = trim($line);
21821d05cddcSAtari911                if (empty($line)) continue;
21831d05cddcSAtari911                $parts = explode('=', $line, 2);
21841d05cddcSAtari911                if (count($parts) === 2) {
21851d05cddcSAtari911                    $config['category_mapping'][trim($parts[0])] = trim($parts[1]);
21861d05cddcSAtari911                }
21871d05cddcSAtari911            }
21881d05cddcSAtari911        }
21891d05cddcSAtari911
21901d05cddcSAtari911        // Parse color mapping from dropdown selections
21911d05cddcSAtari911        $config['color_mapping'] = [];
21921d05cddcSAtari911        $colorMappingCount = $INPUT->int('color_mapping_count', 0);
21931d05cddcSAtari911        for ($i = 0; $i < $colorMappingCount; $i++) {
21941d05cddcSAtari911            $hexColor = $INPUT->str('color_hex_' . $i);
21951d05cddcSAtari911            $category = $INPUT->str('color_map_' . $i);
21961d05cddcSAtari911
21971d05cddcSAtari911            if (!empty($hexColor) && !empty($category)) {
21981d05cddcSAtari911                $config['color_mapping'][$hexColor] = $category;
21991d05cddcSAtari911            }
22001d05cddcSAtari911        }
22011d05cddcSAtari911
22021d05cddcSAtari911        // Build file content using return format
22031d05cddcSAtari911        $content = "<?php\n";
22041d05cddcSAtari911        $content .= "/**\n";
22051d05cddcSAtari911        $content .= " * DokuWiki Calendar → Outlook Sync - Configuration\n";
22061d05cddcSAtari911        $content .= " * \n";
22071d05cddcSAtari911        $content .= " * SECURITY: Add this file to .gitignore!\n";
22081d05cddcSAtari911        $content .= " * Never commit credentials to version control.\n";
22091d05cddcSAtari911        $content .= " */\n\n";
22101d05cddcSAtari911        $content .= "return " . var_export($config, true) . ";\n";
22111d05cddcSAtari911
22121d05cddcSAtari911        // Save file
22131d05cddcSAtari911        if (file_put_contents($configFile, $content)) {
22141d05cddcSAtari911            $this->redirect('Configuration saved successfully!', 'success');
22151d05cddcSAtari911        } else {
22161d05cddcSAtari911            $this->redirect('Error: Could not save configuration file', 'error');
22171d05cddcSAtari911        }
22181d05cddcSAtari911    }
22191d05cddcSAtari911
22201d05cddcSAtari911    private function clearCache() {
22211d05cddcSAtari911        // Clear DokuWiki cache
22221d05cddcSAtari911        $cacheDir = DOKU_INC . 'data/cache';
22231d05cddcSAtari911
22241d05cddcSAtari911        if (is_dir($cacheDir)) {
22251d05cddcSAtari911            $this->recursiveDelete($cacheDir, false);
22261d05cddcSAtari911            $this->redirect('Cache cleared successfully!', 'success', 'update');
22271d05cddcSAtari911        } else {
22281d05cddcSAtari911            $this->redirect('Cache directory not found', 'error', 'update');
22291d05cddcSAtari911        }
22301d05cddcSAtari911    }
22311d05cddcSAtari911
22321d05cddcSAtari911    private function recursiveDelete($dir, $deleteRoot = true) {
22331d05cddcSAtari911        if (!is_dir($dir)) return;
22341d05cddcSAtari911
22351d05cddcSAtari911        $files = array_diff(scandir($dir), array('.', '..'));
22361d05cddcSAtari911        foreach ($files as $file) {
22371d05cddcSAtari911            $path = $dir . '/' . $file;
22381d05cddcSAtari911            if (is_dir($path)) {
22391d05cddcSAtari911                $this->recursiveDelete($path, true);
22401d05cddcSAtari911            } else {
22411d05cddcSAtari911                @unlink($path);
22421d05cddcSAtari911            }
22431d05cddcSAtari911        }
22441d05cddcSAtari911
22451d05cddcSAtari911        if ($deleteRoot) {
22461d05cddcSAtari911            @rmdir($dir);
22471d05cddcSAtari911        }
22481d05cddcSAtari911    }
22491d05cddcSAtari911
22501d05cddcSAtari911    private function findRecurringEvents() {
22511d05cddcSAtari911        $dataDir = DOKU_INC . 'data/meta/';
22521d05cddcSAtari911        $recurring = [];
22531d05cddcSAtari911        $allEvents = []; // Track all events to detect patterns
22541d05cddcSAtari911
22551d05cddcSAtari911        // Check root calendar directory first (blank/default namespace)
22561d05cddcSAtari911        $rootCalendarDir = $dataDir . 'calendar';
22571d05cddcSAtari911        if (is_dir($rootCalendarDir)) {
22581d05cddcSAtari911            foreach (glob($rootCalendarDir . '/*.json') as $file) {
22591d05cddcSAtari911                $data = json_decode(file_get_contents($file), true);
22601d05cddcSAtari911                if (!$data) continue;
22611d05cddcSAtari911
22621d05cddcSAtari911                foreach ($data as $dateKey => $events) {
22631d05cddcSAtari911                    foreach ($events as $event) {
22641d05cddcSAtari911                        // Group by title + namespace (events with same title are likely recurring)
22651d05cddcSAtari911                        $groupKey = strtolower(trim($event['title'])) . '_';
22661d05cddcSAtari911
22671d05cddcSAtari911                        if (!isset($allEvents[$groupKey])) {
22681d05cddcSAtari911                            $allEvents[$groupKey] = [
22691d05cddcSAtari911                                'title' => $event['title'],
22701d05cddcSAtari911                                'namespace' => '',
22711d05cddcSAtari911                                'dates' => [],
22721d05cddcSAtari911                                'events' => []
22731d05cddcSAtari911                            ];
22741d05cddcSAtari911                        }
22751d05cddcSAtari911                        $allEvents[$groupKey]['dates'][] = $dateKey;
22761d05cddcSAtari911                        $allEvents[$groupKey]['events'][] = $event;
22771d05cddcSAtari911                    }
22781d05cddcSAtari911                }
22791d05cddcSAtari911            }
22801d05cddcSAtari911        }
22811d05cddcSAtari911
22821d05cddcSAtari911        // Scan all namespace directories
22831d05cddcSAtari911        foreach (glob($dataDir . '*', GLOB_ONLYDIR) as $nsDir) {
22841d05cddcSAtari911            $namespace = basename($nsDir);
22851d05cddcSAtari911
22861d05cddcSAtari911            // Skip the root 'calendar' dir (already processed above)
22871d05cddcSAtari911            if ($namespace === 'calendar') continue;
22881d05cddcSAtari911
22891d05cddcSAtari911            $calendarDir = $nsDir . '/calendar';
22901d05cddcSAtari911
22911d05cddcSAtari911            if (!is_dir($calendarDir)) continue;
22921d05cddcSAtari911
22931d05cddcSAtari911            // Scan all calendar files
22941d05cddcSAtari911            foreach (glob($calendarDir . '/*.json') as $file) {
22951d05cddcSAtari911                $data = json_decode(file_get_contents($file), true);
22961d05cddcSAtari911                if (!$data) continue;
22971d05cddcSAtari911
22981d05cddcSAtari911                foreach ($data as $dateKey => $events) {
22991d05cddcSAtari911                    foreach ($events as $event) {
23001d05cddcSAtari911                        $groupKey = strtolower(trim($event['title'])) . '_' . ($event['namespace'] ?? '');
23011d05cddcSAtari911
23021d05cddcSAtari911                        if (!isset($allEvents[$groupKey])) {
23031d05cddcSAtari911                            $allEvents[$groupKey] = [
23041d05cddcSAtari911                                'title' => $event['title'],
23051d05cddcSAtari911                                'namespace' => $event['namespace'] ?? '',
23061d05cddcSAtari911                                'dates' => [],
23071d05cddcSAtari911                                'events' => []
23081d05cddcSAtari911                            ];
23091d05cddcSAtari911                        }
23101d05cddcSAtari911                        $allEvents[$groupKey]['dates'][] = $dateKey;
23111d05cddcSAtari911                        $allEvents[$groupKey]['events'][] = $event;
23121d05cddcSAtari911                    }
23131d05cddcSAtari911                }
23141d05cddcSAtari911            }
23151d05cddcSAtari911        }
23161d05cddcSAtari911
23171d05cddcSAtari911        // Analyze patterns - only include if 3+ occurrences
23181d05cddcSAtari911        foreach ($allEvents as $groupKey => $group) {
23191d05cddcSAtari911            if (count($group['dates']) >= 3) {
23201d05cddcSAtari911                // Sort dates
23211d05cddcSAtari911                sort($group['dates']);
23221d05cddcSAtari911
23231d05cddcSAtari911                // Calculate interval between first and second occurrence
23241d05cddcSAtari911                $date1 = new DateTime($group['dates'][0]);
23251d05cddcSAtari911                $date2 = new DateTime($group['dates'][1]);
23261d05cddcSAtari911                $interval = $date1->diff($date2);
23271d05cddcSAtari911
23281d05cddcSAtari911                // Determine pattern
23291d05cddcSAtari911                $pattern = 'Custom';
23301d05cddcSAtari911                if ($interval->days == 1) {
23311d05cddcSAtari911                    $pattern = 'Daily';
23321d05cddcSAtari911                } elseif ($interval->days == 7) {
23331d05cddcSAtari911                    $pattern = 'Weekly';
23341d05cddcSAtari911                } elseif ($interval->days >= 14 && $interval->days <= 16) {
23351d05cddcSAtari911                    $pattern = 'Bi-weekly';
23361d05cddcSAtari911                } elseif ($interval->days >= 28 && $interval->days <= 31) {
23371d05cddcSAtari911                    $pattern = 'Monthly';
23381d05cddcSAtari911                } elseif ($interval->days >= 365 && $interval->days <= 366) {
23391d05cddcSAtari911                    $pattern = 'Yearly';
23401d05cddcSAtari911                }
23411d05cddcSAtari911
23421d05cddcSAtari911                // Use first event's ID or create a synthetic one
23431d05cddcSAtari911                $baseId = isset($group['events'][0]['recurringId'])
23441d05cddcSAtari911                    ? $group['events'][0]['recurringId']
23451d05cddcSAtari911                    : md5($group['title'] . $group['namespace']);
23461d05cddcSAtari911
23471d05cddcSAtari911                $recurring[] = [
23481d05cddcSAtari911                    'baseId' => $baseId,
23491d05cddcSAtari911                    'title' => $group['title'],
23501d05cddcSAtari911                    'namespace' => $group['namespace'],
23511d05cddcSAtari911                    'pattern' => $pattern,
23521d05cddcSAtari911                    'count' => count($group['dates']),
23531d05cddcSAtari911                    'firstDate' => $group['dates'][0],
23541d05cddcSAtari911                    'interval' => $interval->days
23551d05cddcSAtari911                ];
23561d05cddcSAtari911            }
23571d05cddcSAtari911        }
23581d05cddcSAtari911
23591d05cddcSAtari911        return $recurring;
23601d05cddcSAtari911    }
23611d05cddcSAtari911
23621d05cddcSAtari911    private function getEventsByNamespace() {
23631d05cddcSAtari911        $dataDir = DOKU_INC . 'data/meta/';
23641d05cddcSAtari911        $result = [];
23651d05cddcSAtari911
23661d05cddcSAtari911        // Check root calendar directory first (blank/default namespace)
23671d05cddcSAtari911        $rootCalendarDir = $dataDir . 'calendar';
23681d05cddcSAtari911        if (is_dir($rootCalendarDir)) {
23691d05cddcSAtari911            $hasFiles = false;
23701d05cddcSAtari911            $events = [];
23711d05cddcSAtari911
23721d05cddcSAtari911            foreach (glob($rootCalendarDir . '/*.json') as $file) {
23731d05cddcSAtari911                $hasFiles = true;
23741d05cddcSAtari911                $month = basename($file, '.json');
23751d05cddcSAtari911                $data = json_decode(file_get_contents($file), true);
23761d05cddcSAtari911                if (!$data) continue;
23771d05cddcSAtari911
23781d05cddcSAtari911                foreach ($data as $dateKey => $eventList) {
23791d05cddcSAtari911                    foreach ($eventList as $event) {
23801d05cddcSAtari911                        $events[] = [
23811d05cddcSAtari911                            'id' => $event['id'],
23821d05cddcSAtari911                            'title' => $event['title'],
23831d05cddcSAtari911                            'date' => $dateKey,
23841d05cddcSAtari911                            'startTime' => $event['startTime'] ?? null,
23851d05cddcSAtari911                            'month' => $month
23861d05cddcSAtari911                        ];
23871d05cddcSAtari911                    }
23881d05cddcSAtari911                }
23891d05cddcSAtari911            }
23901d05cddcSAtari911
23911d05cddcSAtari911            // Add if it has JSON files (even if empty)
23921d05cddcSAtari911            if ($hasFiles) {
23931d05cddcSAtari911                $result[''] = ['events' => $events];
23941d05cddcSAtari911            }
23951d05cddcSAtari911        }
23961d05cddcSAtari911
23971d05cddcSAtari911        // Recursively scan all namespace directories including sub-namespaces
23981d05cddcSAtari911        $this->scanNamespaceRecursive($dataDir, '', $result);
23991d05cddcSAtari911
24001d05cddcSAtari911        // Sort namespaces, but keep '' (default) first
24011d05cddcSAtari911        uksort($result, function($a, $b) {
24021d05cddcSAtari911            if ($a === '') return -1;
24031d05cddcSAtari911            if ($b === '') return 1;
24041d05cddcSAtari911            return strcmp($a, $b);
24051d05cddcSAtari911        });
24061d05cddcSAtari911
24071d05cddcSAtari911        return $result;
24081d05cddcSAtari911    }
24091d05cddcSAtari911
24101d05cddcSAtari911    private function scanNamespaceRecursive($baseDir, $parentNamespace, &$result) {
24111d05cddcSAtari911        foreach (glob($baseDir . '*', GLOB_ONLYDIR) as $nsDir) {
24121d05cddcSAtari911            $dirName = basename($nsDir);
24131d05cddcSAtari911
24141d05cddcSAtari911            // Skip the root 'calendar' dir
24151d05cddcSAtari911            if ($dirName === 'calendar' && empty($parentNamespace)) continue;
24161d05cddcSAtari911
24171d05cddcSAtari911            // Build namespace path
24181d05cddcSAtari911            $namespace = empty($parentNamespace) ? $dirName : $parentNamespace . ':' . $dirName;
24191d05cddcSAtari911
24201d05cddcSAtari911            // Check for calendar directory
24211d05cddcSAtari911            $calendarDir = $nsDir . '/calendar';
24221d05cddcSAtari911            if (is_dir($calendarDir)) {
24231d05cddcSAtari911                $hasFiles = false;
24241d05cddcSAtari911                $events = [];
24251d05cddcSAtari911
24261d05cddcSAtari911                // Scan all calendar files
24271d05cddcSAtari911                foreach (glob($calendarDir . '/*.json') as $file) {
24281d05cddcSAtari911                    $hasFiles = true;
24291d05cddcSAtari911                    $month = basename($file, '.json');
24301d05cddcSAtari911                    $data = json_decode(file_get_contents($file), true);
24311d05cddcSAtari911                    if (!$data) continue;
24321d05cddcSAtari911
24331d05cddcSAtari911                    foreach ($data as $dateKey => $eventList) {
24341d05cddcSAtari911                        foreach ($eventList as $event) {
24351d05cddcSAtari911                            $events[] = [
24361d05cddcSAtari911                                'id' => $event['id'],
24371d05cddcSAtari911                                'title' => $event['title'],
24381d05cddcSAtari911                                'date' => $dateKey,
24391d05cddcSAtari911                                'startTime' => $event['startTime'] ?? null,
24401d05cddcSAtari911                                'month' => $month
24411d05cddcSAtari911                            ];
24421d05cddcSAtari911                        }
24431d05cddcSAtari911                    }
24441d05cddcSAtari911                }
24451d05cddcSAtari911
24461d05cddcSAtari911                // Add namespace if it has JSON files (even if empty)
24471d05cddcSAtari911                if ($hasFiles) {
24481d05cddcSAtari911                    $result[$namespace] = ['events' => $events];
24491d05cddcSAtari911                }
24501d05cddcSAtari911            }
24511d05cddcSAtari911
24521d05cddcSAtari911            // Recursively scan sub-directories
24531d05cddcSAtari911            $this->scanNamespaceRecursive($nsDir . '/', $namespace, $result);
24541d05cddcSAtari911        }
24551d05cddcSAtari911    }
24561d05cddcSAtari911
24571d05cddcSAtari911    private function getAllNamespaces() {
24581d05cddcSAtari911        $dataDir = DOKU_INC . 'data/meta/';
24591d05cddcSAtari911        $namespaces = [];
24601d05cddcSAtari911
24611d05cddcSAtari911        // Check root calendar directory first
24621d05cddcSAtari911        $rootCalendarDir = $dataDir . 'calendar';
24631d05cddcSAtari911        if (is_dir($rootCalendarDir)) {
24641d05cddcSAtari911            $namespaces[] = '';  // Blank/default namespace
24651d05cddcSAtari911        }
24661d05cddcSAtari911
24671d05cddcSAtari911        // Check all other namespace directories
24681d05cddcSAtari911        foreach (glob($dataDir . '*', GLOB_ONLYDIR) as $nsDir) {
24691d05cddcSAtari911            $namespace = basename($nsDir);
24701d05cddcSAtari911
24711d05cddcSAtari911            // Skip the root 'calendar' dir (already added as '')
24721d05cddcSAtari911            if ($namespace === 'calendar') continue;
24731d05cddcSAtari911
24741d05cddcSAtari911            $calendarDir = $nsDir . '/calendar';
24751d05cddcSAtari911            if (is_dir($calendarDir)) {
24761d05cddcSAtari911                $namespaces[] = $namespace;
24771d05cddcSAtari911            }
24781d05cddcSAtari911        }
24791d05cddcSAtari911
24801d05cddcSAtari911        return $namespaces;
24811d05cddcSAtari911    }
24821d05cddcSAtari911
24831d05cddcSAtari911    private function searchEvents($search, $filterNamespace) {
24841d05cddcSAtari911        $dataDir = DOKU_INC . 'data/meta/';
24851d05cddcSAtari911        $results = [];
24861d05cddcSAtari911
24871d05cddcSAtari911        $search = strtolower(trim($search));
24881d05cddcSAtari911
24891d05cddcSAtari911        foreach (glob($dataDir . '*', GLOB_ONLYDIR) as $nsDir) {
24901d05cddcSAtari911            $namespace = basename($nsDir);
24911d05cddcSAtari911            $calendarDir = $nsDir . '/calendar';
24921d05cddcSAtari911
24931d05cddcSAtari911            if (!is_dir($calendarDir)) continue;
24941d05cddcSAtari911            if ($filterNamespace !== '' && $namespace !== $filterNamespace) continue;
24951d05cddcSAtari911
24961d05cddcSAtari911            foreach (glob($calendarDir . '/*.json') as $file) {
24971d05cddcSAtari911                $month = basename($file, '.json');
24981d05cddcSAtari911                $data = json_decode(file_get_contents($file), true);
24991d05cddcSAtari911                if (!$data) continue;
25001d05cddcSAtari911
25011d05cddcSAtari911                foreach ($data as $dateKey => $events) {
25021d05cddcSAtari911                    foreach ($events as $event) {
25031d05cddcSAtari911                        if ($search === '' || strpos(strtolower($event['title']), $search) !== false) {
25041d05cddcSAtari911                            $results[] = [
25051d05cddcSAtari911                                'id' => $event['id'],
25061d05cddcSAtari911                                'title' => $event['title'],
25071d05cddcSAtari911                                'date' => $dateKey,
25081d05cddcSAtari911                                'startTime' => $event['startTime'] ?? null,
25091d05cddcSAtari911                                'namespace' => $event['namespace'] ?? '',
25101d05cddcSAtari911                                'month' => $month
25111d05cddcSAtari911                            ];
25121d05cddcSAtari911                        }
25131d05cddcSAtari911                    }
25141d05cddcSAtari911                }
25151d05cddcSAtari911            }
25161d05cddcSAtari911        }
25171d05cddcSAtari911
25181d05cddcSAtari911        return $results;
25191d05cddcSAtari911    }
25201d05cddcSAtari911
25211d05cddcSAtari911    private function deleteRecurringSeries() {
25221d05cddcSAtari911        global $INPUT;
25231d05cddcSAtari911
25241d05cddcSAtari911        $eventTitle = $INPUT->str('event_title');
25251d05cddcSAtari911        $namespace = $INPUT->str('namespace');
25261d05cddcSAtari911
25271d05cddcSAtari911        // Determine calendar directory
25281d05cddcSAtari911        if ($namespace === '') {
25291d05cddcSAtari911            $dataDir = DOKU_INC . 'data/meta/calendar';
25301d05cddcSAtari911        } else {
25311d05cddcSAtari911            $dataDir = DOKU_INC . 'data/meta/' . $namespace . '/calendar';
25321d05cddcSAtari911        }
25331d05cddcSAtari911
25341d05cddcSAtari911        $count = 0;
25351d05cddcSAtari911
25361d05cddcSAtari911        if (is_dir($dataDir)) {
25371d05cddcSAtari911            foreach (glob($dataDir . '/*.json') as $file) {
25381d05cddcSAtari911                $data = json_decode(file_get_contents($file), true);
25391d05cddcSAtari911                if (!$data) continue;
25401d05cddcSAtari911
25411d05cddcSAtari911                $modified = false;
25421d05cddcSAtari911                foreach ($data as $dateKey => $events) {
25431d05cddcSAtari911                    $filtered = [];
25441d05cddcSAtari911                    foreach ($events as $event) {
25451d05cddcSAtari911                        // Match by title (case-insensitive)
25461d05cddcSAtari911                        if (strtolower(trim($event['title'])) === strtolower(trim($eventTitle))) {
25471d05cddcSAtari911                            $count++;
25481d05cddcSAtari911                            $modified = true;
25491d05cddcSAtari911                        } else {
25501d05cddcSAtari911                            $filtered[] = $event;
25511d05cddcSAtari911                        }
25521d05cddcSAtari911                    }
25531d05cddcSAtari911                    $data[$dateKey] = $filtered;
25541d05cddcSAtari911                }
25551d05cddcSAtari911
25561d05cddcSAtari911                if ($modified) {
2557*9ccd446eSAtari911                    // Clean up empty date keys
2558*9ccd446eSAtari911                    foreach ($data as $dk => $evts) {
2559*9ccd446eSAtari911                        if (empty($evts)) unset($data[$dk]);
2560*9ccd446eSAtari911                    }
2561*9ccd446eSAtari911
2562*9ccd446eSAtari911                    if (empty($data)) {
2563*9ccd446eSAtari911                        unlink($file);
2564*9ccd446eSAtari911                    } else {
25651d05cddcSAtari911                        file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT));
25661d05cddcSAtari911                    }
25671d05cddcSAtari911                }
25681d05cddcSAtari911            }
2569*9ccd446eSAtari911        }
25701d05cddcSAtari911
2571*9ccd446eSAtari911        $this->clearStatsCache();
25721d05cddcSAtari911        $this->redirect("Deleted $count occurrences of recurring event: " . $eventTitle, 'success', 'manage');
25731d05cddcSAtari911    }
25741d05cddcSAtari911
25751d05cddcSAtari911    private function editRecurringSeries() {
25761d05cddcSAtari911        global $INPUT;
25771d05cddcSAtari911
25781d05cddcSAtari911        $oldTitle = $INPUT->str('old_title');
25791d05cddcSAtari911        $oldNamespace = $INPUT->str('old_namespace');
25801d05cddcSAtari911        $newTitle = $INPUT->str('new_title');
25811d05cddcSAtari911        $startTime = $INPUT->str('start_time');
25821d05cddcSAtari911        $endTime = $INPUT->str('end_time');
25831d05cddcSAtari911        $interval = $INPUT->int('interval', 0);
25841d05cddcSAtari911        $newNamespace = $INPUT->str('new_namespace');
25851d05cddcSAtari911
25861d05cddcSAtari911        // Use old namespace if new namespace is empty (keep current)
25871d05cddcSAtari911        if (empty($newNamespace) && !isset($_POST['new_namespace'])) {
25881d05cddcSAtari911            $newNamespace = $oldNamespace;
25891d05cddcSAtari911        }
25901d05cddcSAtari911
25911d05cddcSAtari911        // Determine old calendar directory
25921d05cddcSAtari911        if ($oldNamespace === '') {
25931d05cddcSAtari911            $oldDataDir = DOKU_INC . 'data/meta/calendar';
25941d05cddcSAtari911        } else {
25951d05cddcSAtari911            $oldDataDir = DOKU_INC . 'data/meta/' . $oldNamespace . '/calendar';
25961d05cddcSAtari911        }
25971d05cddcSAtari911
25981d05cddcSAtari911        $count = 0;
25991d05cddcSAtari911        $eventsToMove = [];
2600*9ccd446eSAtari911        $firstEventDate = null;
26011d05cddcSAtari911
26021d05cddcSAtari911        if (is_dir($oldDataDir)) {
26031d05cddcSAtari911            foreach (glob($oldDataDir . '/*.json') as $file) {
26041d05cddcSAtari911                $data = json_decode(file_get_contents($file), true);
26051d05cddcSAtari911                if (!$data) continue;
26061d05cddcSAtari911
26071d05cddcSAtari911                $modified = false;
26081d05cddcSAtari911                foreach ($data as $dateKey => $events) {
26091d05cddcSAtari911                    foreach ($events as $key => $event) {
26101d05cddcSAtari911                        // Match by old title (case-insensitive)
26111d05cddcSAtari911                        if (strtolower(trim($event['title'])) === strtolower(trim($oldTitle))) {
26121d05cddcSAtari911                            // Update the title
26131d05cddcSAtari911                            $data[$dateKey][$key]['title'] = $newTitle;
26141d05cddcSAtari911
26151d05cddcSAtari911                            // Update start time if provided
26161d05cddcSAtari911                            if (!empty($startTime)) {
2617*9ccd446eSAtari911                                $data[$dateKey][$key]['time'] = $startTime;
26181d05cddcSAtari911                            }
26191d05cddcSAtari911
26201d05cddcSAtari911                            // Update end time if provided
26211d05cddcSAtari911                            if (!empty($endTime)) {
2622*9ccd446eSAtari911                                $data[$dateKey][$key]['endTime'] = $endTime;
26231d05cddcSAtari911                            }
26241d05cddcSAtari911
26251d05cddcSAtari911                            // Update namespace
26261d05cddcSAtari911                            $data[$dateKey][$key]['namespace'] = $newNamespace;
26271d05cddcSAtari911
26281d05cddcSAtari911                            // If changing interval, calculate new date
26291d05cddcSAtari911                            if ($interval > 0 && $count > 0) {
26301d05cddcSAtari911                                // Get the first event date as base
26311d05cddcSAtari911                                if (empty($firstEventDate)) {
26321d05cddcSAtari911                                    $firstEventDate = $dateKey;
26331d05cddcSAtari911                                }
26341d05cddcSAtari911
26351d05cddcSAtari911                                // Calculate new date based on interval
26361d05cddcSAtari911                                $newDate = date('Y-m-d', strtotime($firstEventDate . ' +' . ($count * $interval) . ' days'));
26371d05cddcSAtari911
26381d05cddcSAtari911                                // Store for moving
26391d05cddcSAtari911                                $eventsToMove[] = [
26401d05cddcSAtari911                                    'oldDate' => $dateKey,
26411d05cddcSAtari911                                    'newDate' => $newDate,
26421d05cddcSAtari911                                    'event' => $data[$dateKey][$key],
26431d05cddcSAtari911                                    'key' => $key
26441d05cddcSAtari911                                ];
26451d05cddcSAtari911                            }
26461d05cddcSAtari911
26471d05cddcSAtari911                            $count++;
26481d05cddcSAtari911                            $modified = true;
26491d05cddcSAtari911                        }
26501d05cddcSAtari911                    }
26511d05cddcSAtari911                }
26521d05cddcSAtari911
26531d05cddcSAtari911                if ($modified) {
26541d05cddcSAtari911                    file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT));
26551d05cddcSAtari911                }
26561d05cddcSAtari911            }
26571d05cddcSAtari911
26581d05cddcSAtari911            // Handle interval changes by moving events to new dates
26591d05cddcSAtari911            if (!empty($eventsToMove)) {
26601d05cddcSAtari911                // Remove from old dates first
26611d05cddcSAtari911                foreach (glob($oldDataDir . '/*.json') as $file) {
26621d05cddcSAtari911                    $data = json_decode(file_get_contents($file), true);
26631d05cddcSAtari911                    if (!$data) continue;
26641d05cddcSAtari911
26651d05cddcSAtari911                    $modified = false;
26661d05cddcSAtari911                    foreach ($eventsToMove as $moveData) {
26671d05cddcSAtari911                        $oldMonth = substr($moveData['oldDate'], 0, 7);
26681d05cddcSAtari911                        $fileMonth = basename($file, '.json');
26691d05cddcSAtari911
26701d05cddcSAtari911                        if ($oldMonth === $fileMonth && isset($data[$moveData['oldDate']])) {
26711d05cddcSAtari911                            foreach ($data[$moveData['oldDate']] as $k => $evt) {
26721d05cddcSAtari911                                if ($evt['id'] === $moveData['event']['id']) {
26731d05cddcSAtari911                                    unset($data[$moveData['oldDate']][$k]);
26741d05cddcSAtari911                                    $data[$moveData['oldDate']] = array_values($data[$moveData['oldDate']]);
26751d05cddcSAtari911                                    $modified = true;
26761d05cddcSAtari911                                }
26771d05cddcSAtari911                            }
26781d05cddcSAtari911                        }
26791d05cddcSAtari911                    }
26801d05cddcSAtari911
26811d05cddcSAtari911                    if ($modified) {
26821d05cddcSAtari911                        file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT));
26831d05cddcSAtari911                    }
26841d05cddcSAtari911                }
26851d05cddcSAtari911
26861d05cddcSAtari911                // Add to new dates
26871d05cddcSAtari911                foreach ($eventsToMove as $moveData) {
26881d05cddcSAtari911                    $newMonth = substr($moveData['newDate'], 0, 7);
26891d05cddcSAtari911                    $targetDir = ($newNamespace === '') ? DOKU_INC . 'data/meta/calendar' : DOKU_INC . 'data/meta/' . $newNamespace . '/calendar';
26901d05cddcSAtari911
26911d05cddcSAtari911                    if (!is_dir($targetDir)) {
26921d05cddcSAtari911                        mkdir($targetDir, 0755, true);
26931d05cddcSAtari911                    }
26941d05cddcSAtari911
26951d05cddcSAtari911                    $targetFile = $targetDir . '/' . $newMonth . '.json';
26961d05cddcSAtari911                    $targetData = file_exists($targetFile) ? json_decode(file_get_contents($targetFile), true) : [];
26971d05cddcSAtari911
26981d05cddcSAtari911                    if (!isset($targetData[$moveData['newDate']])) {
26991d05cddcSAtari911                        $targetData[$moveData['newDate']] = [];
27001d05cddcSAtari911                    }
27011d05cddcSAtari911
27021d05cddcSAtari911                    $targetData[$moveData['newDate']][] = $moveData['event'];
27031d05cddcSAtari911                    file_put_contents($targetFile, json_encode($targetData, JSON_PRETTY_PRINT));
27041d05cddcSAtari911                }
27051d05cddcSAtari911            }
27061d05cddcSAtari911
27071d05cddcSAtari911            // Handle namespace change without interval change
27081d05cddcSAtari911            if ($newNamespace !== $oldNamespace && empty($eventsToMove)) {
27091d05cddcSAtari911                foreach (glob($oldDataDir . '/*.json') as $file) {
27101d05cddcSAtari911                    $data = json_decode(file_get_contents($file), true);
27111d05cddcSAtari911                    if (!$data) continue;
27121d05cddcSAtari911
27131d05cddcSAtari911                    $month = basename($file, '.json');
27141d05cddcSAtari911                    $targetDir = ($newNamespace === '') ? DOKU_INC . 'data/meta/calendar' : DOKU_INC . 'data/meta/' . $newNamespace . '/calendar';
27151d05cddcSAtari911
27161d05cddcSAtari911                    if (!is_dir($targetDir)) {
27171d05cddcSAtari911                        mkdir($targetDir, 0755, true);
27181d05cddcSAtari911                    }
27191d05cddcSAtari911
27201d05cddcSAtari911                    $targetFile = $targetDir . '/' . $month . '.json';
27211d05cddcSAtari911                    $targetData = file_exists($targetFile) ? json_decode(file_get_contents($targetFile), true) : [];
27221d05cddcSAtari911
27231d05cddcSAtari911                    $modified = false;
27241d05cddcSAtari911                    foreach ($data as $dateKey => $events) {
27251d05cddcSAtari911                        foreach ($events as $k => $event) {
27261d05cddcSAtari911                            if (isset($event['namespace']) && $event['namespace'] === $newNamespace &&
27271d05cddcSAtari911                                strtolower(trim($event['title'])) === strtolower(trim($newTitle))) {
27281d05cddcSAtari911                                // Move this event
27291d05cddcSAtari911                                if (!isset($targetData[$dateKey])) {
27301d05cddcSAtari911                                    $targetData[$dateKey] = [];
27311d05cddcSAtari911                                }
27321d05cddcSAtari911                                $targetData[$dateKey][] = $event;
27331d05cddcSAtari911                                unset($data[$dateKey][$k]);
27341d05cddcSAtari911                                $data[$dateKey] = array_values($data[$dateKey]);
27351d05cddcSAtari911                                $modified = true;
27361d05cddcSAtari911                            }
27371d05cddcSAtari911                        }
27381d05cddcSAtari911                    }
27391d05cddcSAtari911
27401d05cddcSAtari911                    if ($modified) {
27411d05cddcSAtari911                        file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT));
27421d05cddcSAtari911                        file_put_contents($targetFile, json_encode($targetData, JSON_PRETTY_PRINT));
27431d05cddcSAtari911                    }
27441d05cddcSAtari911                }
27451d05cddcSAtari911            }
27461d05cddcSAtari911        }
27471d05cddcSAtari911
27481d05cddcSAtari911        $changes = [];
27491d05cddcSAtari911        if ($oldTitle !== $newTitle) $changes[] = "title";
27501d05cddcSAtari911        if (!empty($startTime) || !empty($endTime)) $changes[] = "time";
27511d05cddcSAtari911        if ($interval > 0) $changes[] = "interval";
27521d05cddcSAtari911        if ($newNamespace !== $oldNamespace) $changes[] = "namespace";
27531d05cddcSAtari911
27541d05cddcSAtari911        $changeStr = !empty($changes) ? " (" . implode(", ", $changes) . ")" : "";
2755*9ccd446eSAtari911        $this->clearStatsCache();
27561d05cddcSAtari911        $this->redirect("Updated $count occurrences of recurring event$changeStr", 'success', 'manage');
27571d05cddcSAtari911    }
27581d05cddcSAtari911
27591d05cddcSAtari911    private function moveEvents() {
27601d05cddcSAtari911        global $INPUT;
27611d05cddcSAtari911
27621d05cddcSAtari911        $events = $INPUT->arr('events');
27631d05cddcSAtari911        $targetNamespace = $INPUT->str('target_namespace');
27641d05cddcSAtari911
27651d05cddcSAtari911        if (empty($events)) {
27661d05cddcSAtari911            $this->redirect('No events selected', 'error', 'manage');
27671d05cddcSAtari911        }
27681d05cddcSAtari911
27691d05cddcSAtari911        $moved = 0;
27701d05cddcSAtari911
27711d05cddcSAtari911        foreach ($events as $eventData) {
27721d05cddcSAtari911            list($id, $namespace, $date, $month) = explode('|', $eventData);
27731d05cddcSAtari911
27741d05cddcSAtari911            // Determine old file path
27751d05cddcSAtari911            if ($namespace === '') {
27761d05cddcSAtari911                $oldFile = DOKU_INC . 'data/meta/calendar/' . $month . '.json';
27771d05cddcSAtari911            } else {
27781d05cddcSAtari911                $oldFile = DOKU_INC . 'data/meta/' . $namespace . '/calendar/' . $month . '.json';
27791d05cddcSAtari911            }
27801d05cddcSAtari911
27811d05cddcSAtari911            if (!file_exists($oldFile)) continue;
27821d05cddcSAtari911
27831d05cddcSAtari911            $oldData = json_decode(file_get_contents($oldFile), true);
27841d05cddcSAtari911            if (!$oldData) continue;
27851d05cddcSAtari911
27861d05cddcSAtari911            // Find and remove event from old file
27871d05cddcSAtari911            $event = null;
2788*9ccd446eSAtari911            if (isset($oldData[$date])) {
27891d05cddcSAtari911                foreach ($oldData[$date] as $key => $evt) {
27901d05cddcSAtari911                    if ($evt['id'] === $id) {
27911d05cddcSAtari911                        $event = $evt;
27921d05cddcSAtari911                        unset($oldData[$date][$key]);
27931d05cddcSAtari911                        $oldData[$date] = array_values($oldData[$date]);
27941d05cddcSAtari911                        break;
27951d05cddcSAtari911                    }
27961d05cddcSAtari911                }
27971d05cddcSAtari911
2798*9ccd446eSAtari911                // Remove empty date arrays
2799*9ccd446eSAtari911                if (empty($oldData[$date])) {
2800*9ccd446eSAtari911                    unset($oldData[$date]);
2801*9ccd446eSAtari911                }
2802*9ccd446eSAtari911            }
2803*9ccd446eSAtari911
28041d05cddcSAtari911            if (!$event) continue;
28051d05cddcSAtari911
28061d05cddcSAtari911            // Save old file
28071d05cddcSAtari911            file_put_contents($oldFile, json_encode($oldData, JSON_PRETTY_PRINT));
28081d05cddcSAtari911
28091d05cddcSAtari911            // Update event namespace
28101d05cddcSAtari911            $event['namespace'] = $targetNamespace;
28111d05cddcSAtari911
28121d05cddcSAtari911            // Determine new file path
28131d05cddcSAtari911            if ($targetNamespace === '') {
28141d05cddcSAtari911                $newFile = DOKU_INC . 'data/meta/calendar/' . $month . '.json';
28151d05cddcSAtari911                $newDir = dirname($newFile);
28161d05cddcSAtari911            } else {
28171d05cddcSAtari911                $newFile = DOKU_INC . 'data/meta/' . $targetNamespace . '/calendar/' . $month . '.json';
28181d05cddcSAtari911                $newDir = dirname($newFile);
28191d05cddcSAtari911            }
28201d05cddcSAtari911
28211d05cddcSAtari911            if (!is_dir($newDir)) {
28221d05cddcSAtari911                mkdir($newDir, 0755, true);
28231d05cddcSAtari911            }
28241d05cddcSAtari911
28251d05cddcSAtari911            $newData = [];
28261d05cddcSAtari911            if (file_exists($newFile)) {
28271d05cddcSAtari911                $newData = json_decode(file_get_contents($newFile), true) ?: [];
28281d05cddcSAtari911            }
28291d05cddcSAtari911
28301d05cddcSAtari911            if (!isset($newData[$date])) {
28311d05cddcSAtari911                $newData[$date] = [];
28321d05cddcSAtari911            }
28331d05cddcSAtari911            $newData[$date][] = $event;
28341d05cddcSAtari911
28351d05cddcSAtari911            file_put_contents($newFile, json_encode($newData, JSON_PRETTY_PRINT));
28361d05cddcSAtari911            $moved++;
28371d05cddcSAtari911        }
28381d05cddcSAtari911
28391d05cddcSAtari911        $displayTarget = $targetNamespace ?: '(default)';
2840*9ccd446eSAtari911        $this->clearStatsCache();
28411d05cddcSAtari911        $this->redirect("Moved $moved event(s) to namespace: " . $displayTarget, 'success', 'manage');
28421d05cddcSAtari911    }
28431d05cddcSAtari911
28441d05cddcSAtari911    private function moveSingleEvent() {
28451d05cddcSAtari911        global $INPUT;
28461d05cddcSAtari911
28471d05cddcSAtari911        $eventData = $INPUT->str('event');
28481d05cddcSAtari911        $targetNamespace = $INPUT->str('target_namespace');
28491d05cddcSAtari911
28501d05cddcSAtari911        list($id, $namespace, $date, $month) = explode('|', $eventData);
28511d05cddcSAtari911
28521d05cddcSAtari911        // Determine old file path
28531d05cddcSAtari911        if ($namespace === '') {
28541d05cddcSAtari911            $oldFile = DOKU_INC . 'data/meta/calendar/' . $month . '.json';
28551d05cddcSAtari911        } else {
28561d05cddcSAtari911            $oldFile = DOKU_INC . 'data/meta/' . $namespace . '/calendar/' . $month . '.json';
28571d05cddcSAtari911        }
28581d05cddcSAtari911
28591d05cddcSAtari911        if (!file_exists($oldFile)) {
28601d05cddcSAtari911            $this->redirect('Event file not found', 'error', 'manage');
28611d05cddcSAtari911        }
28621d05cddcSAtari911
28631d05cddcSAtari911        $oldData = json_decode(file_get_contents($oldFile), true);
28641d05cddcSAtari911        if (!$oldData) {
28651d05cddcSAtari911            $this->redirect('Could not read event file', 'error', 'manage');
28661d05cddcSAtari911        }
28671d05cddcSAtari911
28681d05cddcSAtari911        // Find and remove event from old file
28691d05cddcSAtari911        $event = null;
2870*9ccd446eSAtari911        if (isset($oldData[$date])) {
28711d05cddcSAtari911            foreach ($oldData[$date] as $key => $evt) {
28721d05cddcSAtari911                if ($evt['id'] === $id) {
28731d05cddcSAtari911                    $event = $evt;
28741d05cddcSAtari911                    unset($oldData[$date][$key]);
28751d05cddcSAtari911                    $oldData[$date] = array_values($oldData[$date]);
28761d05cddcSAtari911                    break;
28771d05cddcSAtari911                }
28781d05cddcSAtari911            }
28791d05cddcSAtari911
2880*9ccd446eSAtari911            // Remove empty date arrays
2881*9ccd446eSAtari911            if (empty($oldData[$date])) {
2882*9ccd446eSAtari911                unset($oldData[$date]);
2883*9ccd446eSAtari911            }
2884*9ccd446eSAtari911        }
2885*9ccd446eSAtari911
28861d05cddcSAtari911        if (!$event) {
28871d05cddcSAtari911            $this->redirect('Event not found', 'error', 'manage');
28881d05cddcSAtari911        }
28891d05cddcSAtari911
2890*9ccd446eSAtari911        // Save old file (or delete if empty)
2891*9ccd446eSAtari911        if (empty($oldData)) {
2892*9ccd446eSAtari911            unlink($oldFile);
2893*9ccd446eSAtari911        } else {
28941d05cddcSAtari911            file_put_contents($oldFile, json_encode($oldData, JSON_PRETTY_PRINT));
2895*9ccd446eSAtari911        }
28961d05cddcSAtari911
28971d05cddcSAtari911        // Update event namespace
28981d05cddcSAtari911        $event['namespace'] = $targetNamespace;
28991d05cddcSAtari911
29001d05cddcSAtari911        // Determine new file path
29011d05cddcSAtari911        if ($targetNamespace === '') {
29021d05cddcSAtari911            $newFile = DOKU_INC . 'data/meta/calendar/' . $month . '.json';
29031d05cddcSAtari911            $newDir = dirname($newFile);
29041d05cddcSAtari911        } else {
29051d05cddcSAtari911            $newFile = DOKU_INC . 'data/meta/' . $targetNamespace . '/calendar/' . $month . '.json';
29061d05cddcSAtari911            $newDir = dirname($newFile);
29071d05cddcSAtari911        }
29081d05cddcSAtari911
29091d05cddcSAtari911        if (!is_dir($newDir)) {
29101d05cddcSAtari911            mkdir($newDir, 0755, true);
29111d05cddcSAtari911        }
29121d05cddcSAtari911
29131d05cddcSAtari911        $newData = [];
29141d05cddcSAtari911        if (file_exists($newFile)) {
29151d05cddcSAtari911            $newData = json_decode(file_get_contents($newFile), true) ?: [];
29161d05cddcSAtari911        }
29171d05cddcSAtari911
29181d05cddcSAtari911        if (!isset($newData[$date])) {
29191d05cddcSAtari911            $newData[$date] = [];
29201d05cddcSAtari911        }
29211d05cddcSAtari911        $newData[$date][] = $event;
29221d05cddcSAtari911
29231d05cddcSAtari911        file_put_contents($newFile, json_encode($newData, JSON_PRETTY_PRINT));
29241d05cddcSAtari911
29251d05cddcSAtari911        $displayTarget = $targetNamespace ?: '(default)';
2926*9ccd446eSAtari911        $this->clearStatsCache();
29271d05cddcSAtari911        $this->redirect('Moved "' . $event['title'] . '" to ' . $displayTarget, 'success', 'manage');
29281d05cddcSAtari911    }
29291d05cddcSAtari911
29301d05cddcSAtari911    private function createNamespace() {
29311d05cddcSAtari911        global $INPUT;
29321d05cddcSAtari911
29331d05cddcSAtari911        $namespaceName = $INPUT->str('namespace_name');
29341d05cddcSAtari911
29351d05cddcSAtari911        // Validate namespace name
29361d05cddcSAtari911        if (empty($namespaceName)) {
29371d05cddcSAtari911            $this->redirect('Namespace name cannot be empty', 'error', 'manage');
29381d05cddcSAtari911        }
29391d05cddcSAtari911
29401d05cddcSAtari911        if (!preg_match('/^[a-zA-Z0-9_:-]+$/', $namespaceName)) {
29411d05cddcSAtari911            $this->redirect('Invalid namespace name. Use only letters, numbers, underscore, hyphen, and colon.', 'error', 'manage');
29421d05cddcSAtari911        }
29431d05cddcSAtari911
29441d05cddcSAtari911        // Convert namespace to directory path
29451d05cddcSAtari911        $namespacePath = str_replace(':', '/', $namespaceName);
29461d05cddcSAtari911        $calendarDir = DOKU_INC . 'data/meta/' . $namespacePath . '/calendar';
29471d05cddcSAtari911
29481d05cddcSAtari911        // Check if already exists
29491d05cddcSAtari911        if (is_dir($calendarDir)) {
29501d05cddcSAtari911            // Check if it has any JSON files
29511d05cddcSAtari911            $hasFiles = !empty(glob($calendarDir . '/*.json'));
29521d05cddcSAtari911            if ($hasFiles) {
29531d05cddcSAtari911                $this->redirect("Namespace '$namespaceName' already exists with events", 'info', 'manage');
29541d05cddcSAtari911            }
29551d05cddcSAtari911            // If directory exists but empty, continue to create placeholder
29561d05cddcSAtari911        }
29571d05cddcSAtari911
29581d05cddcSAtari911        // Create the directory
29591d05cddcSAtari911        if (!is_dir($calendarDir)) {
29601d05cddcSAtari911            if (!mkdir($calendarDir, 0755, true)) {
29611d05cddcSAtari911                $this->redirect("Failed to create namespace directory", 'error', 'manage');
29621d05cddcSAtari911            }
29631d05cddcSAtari911        }
29641d05cddcSAtari911
29651d05cddcSAtari911        // Create a placeholder JSON file with an empty structure for current month
29661d05cddcSAtari911        // This ensures the namespace appears in the list immediately
29671d05cddcSAtari911        $currentMonth = date('Y-m');
29681d05cddcSAtari911        $placeholderFile = $calendarDir . '/' . $currentMonth . '.json';
29691d05cddcSAtari911
29701d05cddcSAtari911        if (!file_exists($placeholderFile)) {
29711d05cddcSAtari911            file_put_contents($placeholderFile, json_encode([], JSON_PRETTY_PRINT));
29721d05cddcSAtari911        }
29731d05cddcSAtari911
29741d05cddcSAtari911        $this->redirect("Created namespace: $namespaceName", 'success', 'manage');
29751d05cddcSAtari911    }
29761d05cddcSAtari911
29771d05cddcSAtari911    private function deleteNamespace() {
29781d05cddcSAtari911        global $INPUT;
29791d05cddcSAtari911
29801d05cddcSAtari911        $namespace = $INPUT->str('namespace');
29811d05cddcSAtari911
29821d05cddcSAtari911        // Convert namespace to directory path (e.g., "work:projects" → "work/projects")
29831d05cddcSAtari911        $namespacePath = str_replace(':', '/', $namespace);
29841d05cddcSAtari911
29851d05cddcSAtari911        // Determine calendar directory
29861d05cddcSAtari911        if ($namespace === '') {
29871d05cddcSAtari911            $calendarDir = DOKU_INC . 'data/meta/calendar';
29881d05cddcSAtari911            $namespaceDir = null; // Don't delete root
29891d05cddcSAtari911        } else {
29901d05cddcSAtari911            $calendarDir = DOKU_INC . 'data/meta/' . $namespacePath . '/calendar';
29911d05cddcSAtari911            $namespaceDir = DOKU_INC . 'data/meta/' . $namespacePath;
29921d05cddcSAtari911        }
29931d05cddcSAtari911
29941d05cddcSAtari911        // Check if directory exists
29951d05cddcSAtari911        if (!is_dir($calendarDir)) {
29961d05cddcSAtari911            // Maybe it was never created or already deleted
29971d05cddcSAtari911            $this->redirect("Namespace directory not found: $calendarDir", 'error', 'manage');
29981d05cddcSAtari911            return;
29991d05cddcSAtari911        }
30001d05cddcSAtari911
30011d05cddcSAtari911        $filesDeleted = 0;
30021d05cddcSAtari911        $eventsDeleted = 0;
30031d05cddcSAtari911
30041d05cddcSAtari911        // Delete all calendar JSON files (including empty ones)
30051d05cddcSAtari911        foreach (glob($calendarDir . '/*.json') as $file) {
30061d05cddcSAtari911            $data = json_decode(file_get_contents($file), true);
30071d05cddcSAtari911            if ($data) {
30081d05cddcSAtari911                foreach ($data as $events) {
30091d05cddcSAtari911                    $eventsDeleted += count($events);
30101d05cddcSAtari911                }
30111d05cddcSAtari911            }
30121d05cddcSAtari911            unlink($file);
30131d05cddcSAtari911            $filesDeleted++;
30141d05cddcSAtari911        }
30151d05cddcSAtari911
30161d05cddcSAtari911        // Delete any other files in calendar directory
30171d05cddcSAtari911        foreach (glob($calendarDir . '/*') as $file) {
30181d05cddcSAtari911            if (is_file($file)) {
30191d05cddcSAtari911                unlink($file);
30201d05cddcSAtari911            }
30211d05cddcSAtari911        }
30221d05cddcSAtari911
30231d05cddcSAtari911        // Remove the calendar directory
30241d05cddcSAtari911        if ($namespace !== '') {
30251d05cddcSAtari911            @rmdir($calendarDir);
30261d05cddcSAtari911
30271d05cddcSAtari911            // Try to remove parent directories if they're empty
30281d05cddcSAtari911            // This handles nested namespaces like work:projects:alpha
30291d05cddcSAtari911            $currentDir = dirname($calendarDir);
30301d05cddcSAtari911            $metaDir = DOKU_INC . 'data/meta';
30311d05cddcSAtari911
30321d05cddcSAtari911            while ($currentDir !== $metaDir && $currentDir !== dirname($metaDir)) {
30331d05cddcSAtari911                if (is_dir($currentDir)) {
30341d05cddcSAtari911                    // Check if directory is empty
30351d05cddcSAtari911                    $contents = scandir($currentDir);
30361d05cddcSAtari911                    $isEmpty = count($contents) === 2; // Only . and ..
30371d05cddcSAtari911
30381d05cddcSAtari911                    if ($isEmpty) {
30391d05cddcSAtari911                        @rmdir($currentDir);
30401d05cddcSAtari911                        $currentDir = dirname($currentDir);
30411d05cddcSAtari911                    } else {
30421d05cddcSAtari911                        break; // Directory not empty, stop
30431d05cddcSAtari911                    }
30441d05cddcSAtari911                } else {
30451d05cddcSAtari911                    break;
30461d05cddcSAtari911                }
30471d05cddcSAtari911            }
30481d05cddcSAtari911        }
30491d05cddcSAtari911
30501d05cddcSAtari911        $displayName = $namespace ?: '(default)';
3051*9ccd446eSAtari911        $this->clearStatsCache();
30521d05cddcSAtari911        $this->redirect("Deleted namespace '$displayName': $eventsDeleted events in $filesDeleted files", 'success', 'manage');
30531d05cddcSAtari911    }
30541d05cddcSAtari911
3055*9ccd446eSAtari911    private function renameNamespace() {
3056*9ccd446eSAtari911        global $INPUT;
3057*9ccd446eSAtari911
3058*9ccd446eSAtari911        $oldNamespace = $INPUT->str('old_namespace');
3059*9ccd446eSAtari911        $newNamespace = $INPUT->str('new_namespace');
3060*9ccd446eSAtari911
3061*9ccd446eSAtari911        // Validate new namespace name
3062*9ccd446eSAtari911        if ($newNamespace === '') {
3063*9ccd446eSAtari911            $this->redirect("Cannot rename to empty namespace", 'error', 'manage');
3064*9ccd446eSAtari911            return;
3065*9ccd446eSAtari911        }
3066*9ccd446eSAtari911
3067*9ccd446eSAtari911        // Convert namespaces to directory paths
3068*9ccd446eSAtari911        $oldPath = str_replace(':', '/', $oldNamespace);
3069*9ccd446eSAtari911        $newPath = str_replace(':', '/', $newNamespace);
3070*9ccd446eSAtari911
3071*9ccd446eSAtari911        // Determine source and destination directories
3072*9ccd446eSAtari911        if ($oldNamespace === '') {
3073*9ccd446eSAtari911            $sourceDir = DOKU_INC . 'data/meta/calendar';
3074*9ccd446eSAtari911        } else {
3075*9ccd446eSAtari911            $sourceDir = DOKU_INC . 'data/meta/' . $oldPath . '/calendar';
3076*9ccd446eSAtari911        }
3077*9ccd446eSAtari911
3078*9ccd446eSAtari911        if ($newNamespace === '') {
3079*9ccd446eSAtari911            $targetDir = DOKU_INC . 'data/meta/calendar';
3080*9ccd446eSAtari911        } else {
3081*9ccd446eSAtari911            $targetDir = DOKU_INC . 'data/meta/' . $newPath . '/calendar';
3082*9ccd446eSAtari911        }
3083*9ccd446eSAtari911
3084*9ccd446eSAtari911        // Check if source exists
3085*9ccd446eSAtari911        if (!is_dir($sourceDir)) {
3086*9ccd446eSAtari911            $this->redirect("Source namespace not found: $oldNamespace", 'error', 'manage');
3087*9ccd446eSAtari911            return;
3088*9ccd446eSAtari911        }
3089*9ccd446eSAtari911
3090*9ccd446eSAtari911        // Check if target already exists
3091*9ccd446eSAtari911        if (is_dir($targetDir)) {
3092*9ccd446eSAtari911            $this->redirect("Target namespace already exists: $newNamespace", 'error', 'manage');
3093*9ccd446eSAtari911            return;
3094*9ccd446eSAtari911        }
3095*9ccd446eSAtari911
3096*9ccd446eSAtari911        // Create target directory
3097*9ccd446eSAtari911        if (!file_exists(dirname($targetDir))) {
3098*9ccd446eSAtari911            mkdir(dirname($targetDir), 0755, true);
3099*9ccd446eSAtari911        }
3100*9ccd446eSAtari911
3101*9ccd446eSAtari911        // Rename directory
3102*9ccd446eSAtari911        if (!rename($sourceDir, $targetDir)) {
3103*9ccd446eSAtari911            $this->redirect("Failed to rename namespace", 'error', 'manage');
3104*9ccd446eSAtari911            return;
3105*9ccd446eSAtari911        }
3106*9ccd446eSAtari911
3107*9ccd446eSAtari911        // Update event namespace field in all JSON files
3108*9ccd446eSAtari911        $eventsUpdated = 0;
3109*9ccd446eSAtari911        foreach (glob($targetDir . '/*.json') as $file) {
3110*9ccd446eSAtari911            $data = json_decode(file_get_contents($file), true);
3111*9ccd446eSAtari911            if ($data) {
3112*9ccd446eSAtari911                foreach ($data as $date => &$events) {
3113*9ccd446eSAtari911                    foreach ($events as &$event) {
3114*9ccd446eSAtari911                        if (isset($event['namespace']) && $event['namespace'] === $oldNamespace) {
3115*9ccd446eSAtari911                            $event['namespace'] = $newNamespace;
3116*9ccd446eSAtari911                            $eventsUpdated++;
3117*9ccd446eSAtari911                        }
3118*9ccd446eSAtari911                    }
3119*9ccd446eSAtari911                }
3120*9ccd446eSAtari911                file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT));
3121*9ccd446eSAtari911            }
3122*9ccd446eSAtari911        }
3123*9ccd446eSAtari911
3124*9ccd446eSAtari911        // Clean up old directory structure if empty
3125*9ccd446eSAtari911        if ($oldNamespace !== '') {
3126*9ccd446eSAtari911            $currentDir = dirname($sourceDir);
3127*9ccd446eSAtari911            $metaDir = DOKU_INC . 'data/meta';
3128*9ccd446eSAtari911
3129*9ccd446eSAtari911            while ($currentDir !== $metaDir && $currentDir !== dirname($metaDir)) {
3130*9ccd446eSAtari911                if (is_dir($currentDir)) {
3131*9ccd446eSAtari911                    $contents = scandir($currentDir);
3132*9ccd446eSAtari911                    $isEmpty = count($contents) === 2; // Only . and ..
3133*9ccd446eSAtari911
3134*9ccd446eSAtari911                    if ($isEmpty) {
3135*9ccd446eSAtari911                        @rmdir($currentDir);
3136*9ccd446eSAtari911                        $currentDir = dirname($currentDir);
3137*9ccd446eSAtari911                    } else {
3138*9ccd446eSAtari911                        break;
3139*9ccd446eSAtari911                    }
3140*9ccd446eSAtari911                } else {
3141*9ccd446eSAtari911                    break;
3142*9ccd446eSAtari911                }
3143*9ccd446eSAtari911            }
3144*9ccd446eSAtari911        }
3145*9ccd446eSAtari911
3146*9ccd446eSAtari911        $this->clearStatsCache();
3147*9ccd446eSAtari911        $this->redirect("Renamed namespace from '$oldNamespace' to '$newNamespace' ($eventsUpdated events updated)", 'success', 'manage');
3148*9ccd446eSAtari911    }
3149*9ccd446eSAtari911
31501d05cddcSAtari911    private function deleteSelectedEvents() {
31511d05cddcSAtari911        global $INPUT;
31521d05cddcSAtari911
31531d05cddcSAtari911        $events = $INPUT->arr('events');
31541d05cddcSAtari911
31551d05cddcSAtari911        if (empty($events)) {
31561d05cddcSAtari911            $this->redirect('No events selected', 'error', 'manage');
31571d05cddcSAtari911        }
31581d05cddcSAtari911
31591d05cddcSAtari911        $deletedCount = 0;
31601d05cddcSAtari911
31611d05cddcSAtari911        foreach ($events as $eventData) {
31621d05cddcSAtari911            list($id, $namespace, $date, $month) = explode('|', $eventData);
31631d05cddcSAtari911
31641d05cddcSAtari911            // Determine file path
31651d05cddcSAtari911            if ($namespace === '') {
31661d05cddcSAtari911                $file = DOKU_INC . 'data/meta/calendar/' . $month . '.json';
31671d05cddcSAtari911            } else {
31681d05cddcSAtari911                $file = DOKU_INC . 'data/meta/' . $namespace . '/calendar/' . $month . '.json';
31691d05cddcSAtari911            }
31701d05cddcSAtari911
31711d05cddcSAtari911            if (!file_exists($file)) continue;
31721d05cddcSAtari911
31731d05cddcSAtari911            $data = json_decode(file_get_contents($file), true);
31741d05cddcSAtari911            if (!$data) continue;
31751d05cddcSAtari911
31761d05cddcSAtari911            // Find and remove event
31771d05cddcSAtari911            if (isset($data[$date])) {
31781d05cddcSAtari911                foreach ($data[$date] as $key => $evt) {
31791d05cddcSAtari911                    if ($evt['id'] === $id) {
31801d05cddcSAtari911                        unset($data[$date][$key]);
31811d05cddcSAtari911                        $data[$date] = array_values($data[$date]);
31821d05cddcSAtari911                        $deletedCount++;
31831d05cddcSAtari911                        break;
31841d05cddcSAtari911                    }
31851d05cddcSAtari911                }
31861d05cddcSAtari911
31871d05cddcSAtari911                // Remove empty date arrays
31881d05cddcSAtari911                if (empty($data[$date])) {
31891d05cddcSAtari911                    unset($data[$date]);
31901d05cddcSAtari911                }
31911d05cddcSAtari911
31921d05cddcSAtari911                // Save file
31931d05cddcSAtari911                file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT));
31941d05cddcSAtari911            }
31951d05cddcSAtari911        }
31961d05cddcSAtari911
3197*9ccd446eSAtari911        $this->clearStatsCache();
31981d05cddcSAtari911        $this->redirect("Deleted $deletedCount event(s)", 'success', 'manage');
31991d05cddcSAtari911    }
32001d05cddcSAtari911
3201*9ccd446eSAtari911    /**
3202*9ccd446eSAtari911     * Clear the event statistics cache so counts refresh after mutations
3203*9ccd446eSAtari911     */
3204*9ccd446eSAtari911    private function clearStatsCache() {
3205*9ccd446eSAtari911        $cacheFile = DOKU_PLUGIN . 'calendar/.event_stats_cache';
3206*9ccd446eSAtari911        if (file_exists($cacheFile)) {
3207*9ccd446eSAtari911            unlink($cacheFile);
3208*9ccd446eSAtari911        }
3209*9ccd446eSAtari911    }
3210*9ccd446eSAtari911
32111d05cddcSAtari911    private function getCronStatus() {
32121d05cddcSAtari911        // Try to read root's crontab first, then current user
32131d05cddcSAtari911        $output = [];
32141d05cddcSAtari911        exec('sudo crontab -l 2>/dev/null', $output);
32151d05cddcSAtari911
32161d05cddcSAtari911        // If sudo doesn't work, try current user
32171d05cddcSAtari911        if (empty($output)) {
32181d05cddcSAtari911            exec('crontab -l 2>/dev/null', $output);
32191d05cddcSAtari911        }
32201d05cddcSAtari911
32211d05cddcSAtari911        // Also check system crontab files
32221d05cddcSAtari911        if (empty($output)) {
32231d05cddcSAtari911            $cronFiles = [
32241d05cddcSAtari911                '/etc/crontab',
32251d05cddcSAtari911                '/etc/cron.d/calendar',
32261d05cddcSAtari911                '/var/spool/cron/root',
32271d05cddcSAtari911                '/var/spool/cron/crontabs/root'
32281d05cddcSAtari911            ];
32291d05cddcSAtari911
32301d05cddcSAtari911            foreach ($cronFiles as $file) {
32311d05cddcSAtari911                if (file_exists($file) && is_readable($file)) {
32321d05cddcSAtari911                    $content = file_get_contents($file);
32331d05cddcSAtari911                    $output = explode("\n", $content);
32341d05cddcSAtari911                    break;
32351d05cddcSAtari911                }
32361d05cddcSAtari911            }
32371d05cddcSAtari911        }
32381d05cddcSAtari911
32391d05cddcSAtari911        // Look for sync_outlook.php in the cron entries
32401d05cddcSAtari911        foreach ($output as $line) {
32411d05cddcSAtari911            $line = trim($line);
32421d05cddcSAtari911
32431d05cddcSAtari911            // Skip empty lines and comments
32441d05cddcSAtari911            if (empty($line) || $line[0] === '#') continue;
32451d05cddcSAtari911
32461d05cddcSAtari911            // Check if line contains sync_outlook.php
32471d05cddcSAtari911            if (strpos($line, 'sync_outlook.php') !== false) {
32481d05cddcSAtari911                // Parse cron expression
32491d05cddcSAtari911                // Format: minute hour day month weekday [user] command
32501d05cddcSAtari911                $parts = preg_split('/\s+/', $line, 7);
32511d05cddcSAtari911
32521d05cddcSAtari911                if (count($parts) >= 5) {
32531d05cddcSAtari911                    // Determine if this has a user field (system crontab format)
32541d05cddcSAtari911                    $hasUser = (count($parts) >= 6 && !preg_match('/^[\/\*]/', $parts[5]));
32551d05cddcSAtari911                    $offset = $hasUser ? 1 : 0;
32561d05cddcSAtari911
32571d05cddcSAtari911                    $frequency = $this->parseCronExpression($parts[0], $parts[1], $parts[2], $parts[3], $parts[4]);
32581d05cddcSAtari911                    return [
32591d05cddcSAtari911                        'active' => true,
32601d05cddcSAtari911                        'frequency' => $frequency,
32611d05cddcSAtari911                        'expression' => implode(' ', array_slice($parts, 0, 5)),
32621d05cddcSAtari911                        'full_line' => $line
32631d05cddcSAtari911                    ];
32641d05cddcSAtari911                }
32651d05cddcSAtari911            }
32661d05cddcSAtari911        }
32671d05cddcSAtari911
32681d05cddcSAtari911        return ['active' => false, 'frequency' => '', 'expression' => '', 'full_line' => ''];
32691d05cddcSAtari911    }
32701d05cddcSAtari911
32711d05cddcSAtari911    private function parseCronExpression($minute, $hour, $day, $month, $weekday) {
32721d05cddcSAtari911        // Parse minute field
32731d05cddcSAtari911        if ($minute === '*') {
32741d05cddcSAtari911            return 'Runs every minute';
32751d05cddcSAtari911        } elseif (strpos($minute, '*/') === 0) {
32761d05cddcSAtari911            $interval = substr($minute, 2);
32771d05cddcSAtari911            if ($interval == 1) {
32781d05cddcSAtari911                return 'Runs every minute';
32791d05cddcSAtari911            } elseif ($interval == 5) {
32801d05cddcSAtari911                return 'Runs every 5 minutes';
32811d05cddcSAtari911            } elseif ($interval == 8) {
32821d05cddcSAtari911                return 'Runs every 8 minutes';
32831d05cddcSAtari911            } elseif ($interval == 10) {
32841d05cddcSAtari911                return 'Runs every 10 minutes';
32851d05cddcSAtari911            } elseif ($interval == 15) {
32861d05cddcSAtari911                return 'Runs every 15 minutes';
32871d05cddcSAtari911            } elseif ($interval == 30) {
32881d05cddcSAtari911                return 'Runs every 30 minutes';
32891d05cddcSAtari911            } else {
32901d05cddcSAtari911                return "Runs every $interval minutes";
32911d05cddcSAtari911            }
32921d05cddcSAtari911        }
32931d05cddcSAtari911
32941d05cddcSAtari911        // Parse hour field
32951d05cddcSAtari911        if ($hour === '*' && $minute !== '*') {
32961d05cddcSAtari911            return 'Runs hourly';
32971d05cddcSAtari911        } elseif (strpos($hour, '*/') === 0 && $minute !== '*') {
32981d05cddcSAtari911            $interval = substr($hour, 2);
32991d05cddcSAtari911            if ($interval == 1) {
33001d05cddcSAtari911                return 'Runs every hour';
33011d05cddcSAtari911            } else {
33021d05cddcSAtari911                return "Runs every $interval hours";
33031d05cddcSAtari911            }
33041d05cddcSAtari911        }
33051d05cddcSAtari911
33061d05cddcSAtari911        // Parse day field
33071d05cddcSAtari911        if ($day === '*' && $hour !== '*' && $minute !== '*') {
33081d05cddcSAtari911            return 'Runs daily';
33091d05cddcSAtari911        }
33101d05cddcSAtari911
33111d05cddcSAtari911        // Default
33121d05cddcSAtari911        return 'Custom schedule';
33131d05cddcSAtari911    }
33141d05cddcSAtari911
33151d05cddcSAtari911    private function runSync() {
33161d05cddcSAtari911        global $INPUT;
33171d05cddcSAtari911
33181d05cddcSAtari911        if ($INPUT->str('call') === 'ajax') {
33191d05cddcSAtari911            header('Content-Type: application/json');
33201d05cddcSAtari911
33211d05cddcSAtari911            $syncScript = DOKU_PLUGIN . 'calendar/sync_outlook.php';
33221d05cddcSAtari911            $abortFile = DOKU_PLUGIN . 'calendar/.sync_abort';
33231d05cddcSAtari911
33241d05cddcSAtari911            // Remove any existing abort flag
33251d05cddcSAtari911            if (file_exists($abortFile)) {
33261d05cddcSAtari911                @unlink($abortFile);
33271d05cddcSAtari911            }
33281d05cddcSAtari911
33291d05cddcSAtari911            if (!file_exists($syncScript)) {
33301d05cddcSAtari911                echo json_encode(['success' => false, 'message' => 'Sync script not found at: ' . $syncScript]);
33311d05cddcSAtari911                exit;
33321d05cddcSAtari911            }
33331d05cddcSAtari911
33341d05cddcSAtari911            // Change to plugin directory
33351d05cddcSAtari911            $pluginDir = DOKU_PLUGIN . 'calendar';
33361d05cddcSAtari911            $logFile = $pluginDir . '/sync.log';
33371d05cddcSAtari911
33381d05cddcSAtari911            // Ensure log file exists and is writable
33391d05cddcSAtari911            if (!file_exists($logFile)) {
33401d05cddcSAtari911                @touch($logFile);
33411d05cddcSAtari911                @chmod($logFile, 0666);
33421d05cddcSAtari911            }
33431d05cddcSAtari911
33441d05cddcSAtari911            // Try to log the execution (but don't fail if we can't)
33451d05cddcSAtari911            if (is_writable($logFile)) {
33461d05cddcSAtari911                $tz = new DateTimeZone('America/Los_Angeles');
33471d05cddcSAtari911                $now = new DateTime('now', $tz);
33481d05cddcSAtari911                $timestamp = $now->format('Y-m-d H:i:s');
33491d05cddcSAtari911                @file_put_contents($logFile, "[$timestamp] [ADMIN] Manual sync triggered via admin panel\n", FILE_APPEND);
33501d05cddcSAtari911            }
33511d05cddcSAtari911
33521d05cddcSAtari911            // Find PHP binary - try multiple methods
33531d05cddcSAtari911            $phpPath = $this->findPhpBinary();
33541d05cddcSAtari911
33551d05cddcSAtari911            // Build command
33561d05cddcSAtari911            $command = sprintf(
33571d05cddcSAtari911                'cd %s && %s %s 2>&1',
33581d05cddcSAtari911                escapeshellarg($pluginDir),
33591d05cddcSAtari911                $phpPath,
33601d05cddcSAtari911                escapeshellarg(basename($syncScript))
33611d05cddcSAtari911            );
33621d05cddcSAtari911
33631d05cddcSAtari911            // Execute and capture output
33641d05cddcSAtari911            $output = [];
33651d05cddcSAtari911            $returnCode = 0;
33661d05cddcSAtari911            exec($command, $output, $returnCode);
33671d05cddcSAtari911
33681d05cddcSAtari911            // Check if sync completed
33691d05cddcSAtari911            $lastLines = array_slice($output, -5);
33701d05cddcSAtari911            $completed = false;
33711d05cddcSAtari911            foreach ($lastLines as $line) {
33721d05cddcSAtari911                if (strpos($line, 'Sync Complete') !== false || strpos($line, 'Created:') !== false) {
33731d05cddcSAtari911                    $completed = true;
33741d05cddcSAtari911                    break;
33751d05cddcSAtari911                }
33761d05cddcSAtari911            }
33771d05cddcSAtari911
33781d05cddcSAtari911            if ($returnCode === 0 && $completed) {
33791d05cddcSAtari911                echo json_encode([
33801d05cddcSAtari911                    'success' => true,
33811d05cddcSAtari911                    'message' => 'Sync completed successfully! Check log below.'
33821d05cddcSAtari911                ]);
33831d05cddcSAtari911            } elseif ($returnCode === 0) {
33841d05cddcSAtari911                echo json_encode([
33851d05cddcSAtari911                    'success' => true,
33861d05cddcSAtari911                    'message' => 'Sync started. Check log below for progress.'
33871d05cddcSAtari911                ]);
33881d05cddcSAtari911            } else {
33891d05cddcSAtari911                // Include output for debugging
33901d05cddcSAtari911                $errorMsg = 'Sync failed with error code: ' . $returnCode;
33911d05cddcSAtari911                if (!empty($output)) {
33921d05cddcSAtari911                    $errorMsg .= ' | ' . implode(' | ', array_slice($output, -3));
33931d05cddcSAtari911                }
33941d05cddcSAtari911                echo json_encode([
33951d05cddcSAtari911                    'success' => false,
33961d05cddcSAtari911                    'message' => $errorMsg
33971d05cddcSAtari911                ]);
33981d05cddcSAtari911            }
33991d05cddcSAtari911            exit;
34001d05cddcSAtari911        }
34011d05cddcSAtari911    }
34021d05cddcSAtari911
34031d05cddcSAtari911    private function stopSync() {
34041d05cddcSAtari911        global $INPUT;
34051d05cddcSAtari911
34061d05cddcSAtari911        if ($INPUT->str('call') === 'ajax') {
34071d05cddcSAtari911            header('Content-Type: application/json');
34081d05cddcSAtari911
34091d05cddcSAtari911            $abortFile = DOKU_PLUGIN . 'calendar/.sync_abort';
34101d05cddcSAtari911
34111d05cddcSAtari911            // Create abort flag file
34121d05cddcSAtari911            if (file_put_contents($abortFile, date('Y-m-d H:i:s')) !== false) {
34131d05cddcSAtari911                echo json_encode([
34141d05cddcSAtari911                    'success' => true,
34151d05cddcSAtari911                    'message' => 'Stop signal sent to sync process'
34161d05cddcSAtari911                ]);
34171d05cddcSAtari911            } else {
34181d05cddcSAtari911                echo json_encode([
34191d05cddcSAtari911                    'success' => false,
34201d05cddcSAtari911                    'message' => 'Failed to create abort flag'
34211d05cddcSAtari911                ]);
34221d05cddcSAtari911            }
34231d05cddcSAtari911            exit;
34241d05cddcSAtari911        }
34251d05cddcSAtari911    }
34261d05cddcSAtari911
34271d05cddcSAtari911    private function uploadUpdate() {
34281d05cddcSAtari911        if (!isset($_FILES['plugin_zip']) || $_FILES['plugin_zip']['error'] !== UPLOAD_ERR_OK) {
34291d05cddcSAtari911            $this->redirect('Upload failed: ' . ($_FILES['plugin_zip']['error'] ?? 'No file uploaded'), 'error', 'update');
34301d05cddcSAtari911            return;
34311d05cddcSAtari911        }
34321d05cddcSAtari911
34331d05cddcSAtari911        $uploadedFile = $_FILES['plugin_zip']['tmp_name'];
34341d05cddcSAtari911        $pluginDir = DOKU_PLUGIN . 'calendar/';
34351d05cddcSAtari911        $backupFirst = isset($_POST['backup_first']);
34361d05cddcSAtari911
34371d05cddcSAtari911        // Check if plugin directory is writable
34381d05cddcSAtari911        if (!is_writable($pluginDir)) {
34391d05cddcSAtari911            $this->redirect('Plugin directory is not writable. Please check permissions: ' . $pluginDir, 'error', 'update');
34401d05cddcSAtari911            return;
34411d05cddcSAtari911        }
34421d05cddcSAtari911
34431d05cddcSAtari911        // Check if parent directory is writable (for backup and temp files)
34441d05cddcSAtari911        if (!is_writable(DOKU_PLUGIN)) {
34451d05cddcSAtari911            $this->redirect('Plugin parent directory is not writable. Please check permissions: ' . DOKU_PLUGIN, 'error', 'update');
34461d05cddcSAtari911            return;
34471d05cddcSAtari911        }
34481d05cddcSAtari911
34491d05cddcSAtari911        // Verify it's a ZIP file
34501d05cddcSAtari911        $finfo = finfo_open(FILEINFO_MIME_TYPE);
34511d05cddcSAtari911        $mimeType = finfo_file($finfo, $uploadedFile);
34521d05cddcSAtari911        finfo_close($finfo);
34531d05cddcSAtari911
34541d05cddcSAtari911        if ($mimeType !== 'application/zip' && $mimeType !== 'application/x-zip-compressed') {
34551d05cddcSAtari911            $this->redirect('Invalid file type. Please upload a ZIP file.', 'error', 'update');
34561d05cddcSAtari911            return;
34571d05cddcSAtari911        }
34581d05cddcSAtari911
34591d05cddcSAtari911        // Create backup if requested
34601d05cddcSAtari911        if ($backupFirst) {
34611d05cddcSAtari911            // Get current version
34621d05cddcSAtari911            $pluginInfo = $pluginDir . 'plugin.info.txt';
34631d05cddcSAtari911            $version = 'unknown';
34641d05cddcSAtari911            if (file_exists($pluginInfo)) {
34651d05cddcSAtari911                $info = confToHash($pluginInfo);
34661d05cddcSAtari911                $version = $info['version'] ?? ($info['date'] ?? 'unknown');
34671d05cddcSAtari911            }
34681d05cddcSAtari911
34691d05cddcSAtari911            $backupName = 'calendar.backup.v' . $version . '.' . date('Y-m-d_H-i-s') . '.zip';
34701d05cddcSAtari911            $backupPath = DOKU_PLUGIN . $backupName;
34711d05cddcSAtari911
34721d05cddcSAtari911            try {
34731d05cddcSAtari911                $zip = new ZipArchive();
34741d05cddcSAtari911                if ($zip->open($backupPath, ZipArchive::CREATE) === TRUE) {
3475*9ccd446eSAtari911                    $fileCount = $this->addDirectoryToZip($zip, $pluginDir, 'calendar/');
34761d05cddcSAtari911                    $zip->close();
3477*9ccd446eSAtari911
3478*9ccd446eSAtari911                    // Verify backup was created and has content
3479*9ccd446eSAtari911                    if (!file_exists($backupPath)) {
3480*9ccd446eSAtari911                        $this->redirect('Backup file was not created', 'error', 'update');
3481*9ccd446eSAtari911                        return;
3482*9ccd446eSAtari911                    }
3483*9ccd446eSAtari911
3484*9ccd446eSAtari911                    $backupSize = filesize($backupPath);
3485*9ccd446eSAtari911                    if ($backupSize < 1000) { // Backup should be at least 1KB
3486*9ccd446eSAtari911                        @unlink($backupPath);
3487*9ccd446eSAtari911                        $this->redirect('Backup file is too small (' . $backupSize . ' bytes). Only ' . $fileCount . ' files were added. Backup aborted.', 'error', 'update');
3488*9ccd446eSAtari911                        return;
3489*9ccd446eSAtari911                    }
3490*9ccd446eSAtari911
3491*9ccd446eSAtari911                    if ($fileCount < 10) { // Should have at least 10 files
3492*9ccd446eSAtari911                        @unlink($backupPath);
3493*9ccd446eSAtari911                        $this->redirect('Backup incomplete: Only ' . $fileCount . ' files were added (expected 30+). Backup aborted.', 'error', 'update');
3494*9ccd446eSAtari911                        return;
3495*9ccd446eSAtari911                    }
34961d05cddcSAtari911                } else {
34971d05cddcSAtari911                    $this->redirect('Failed to create backup ZIP file', 'error', 'update');
34981d05cddcSAtari911                    return;
34991d05cddcSAtari911                }
35001d05cddcSAtari911            } catch (Exception $e) {
3501*9ccd446eSAtari911                if (file_exists($backupPath)) {
3502*9ccd446eSAtari911                    @unlink($backupPath);
3503*9ccd446eSAtari911                }
35041d05cddcSAtari911                $this->redirect('Backup failed: ' . $e->getMessage(), 'error', 'update');
35051d05cddcSAtari911                return;
35061d05cddcSAtari911            }
35071d05cddcSAtari911        }
35081d05cddcSAtari911
35091d05cddcSAtari911        // Extract uploaded ZIP
35101d05cddcSAtari911        $zip = new ZipArchive();
35111d05cddcSAtari911        if ($zip->open($uploadedFile) !== TRUE) {
35121d05cddcSAtari911            $this->redirect('Failed to open ZIP file', 'error', 'update');
35131d05cddcSAtari911            return;
35141d05cddcSAtari911        }
35151d05cddcSAtari911
35161d05cddcSAtari911        // Check if ZIP contains calendar folder
35171d05cddcSAtari911        $hasCalendarFolder = false;
35181d05cddcSAtari911        for ($i = 0; $i < $zip->numFiles; $i++) {
35191d05cddcSAtari911            $filename = $zip->getNameIndex($i);
35201d05cddcSAtari911            if (strpos($filename, 'calendar/') === 0) {
35211d05cddcSAtari911                $hasCalendarFolder = true;
35221d05cddcSAtari911                break;
35231d05cddcSAtari911            }
35241d05cddcSAtari911        }
35251d05cddcSAtari911
35261d05cddcSAtari911        // Extract to temp directory first
35271d05cddcSAtari911        $tempDir = DOKU_PLUGIN . 'calendar_update_temp/';
35281d05cddcSAtari911        if (is_dir($tempDir)) {
35291d05cddcSAtari911            $this->deleteDirectory($tempDir);
35301d05cddcSAtari911        }
35311d05cddcSAtari911        mkdir($tempDir);
35321d05cddcSAtari911
35331d05cddcSAtari911        $zip->extractTo($tempDir);
35341d05cddcSAtari911        $zip->close();
35351d05cddcSAtari911
35361d05cddcSAtari911        // Determine source directory
35371d05cddcSAtari911        if ($hasCalendarFolder) {
35381d05cddcSAtari911            $sourceDir = $tempDir . 'calendar/';
35391d05cddcSAtari911        } else {
35401d05cddcSAtari911            $sourceDir = $tempDir;
35411d05cddcSAtari911        }
35421d05cddcSAtari911
35431d05cddcSAtari911        // Preserve configuration files
35441d05cddcSAtari911        $preserveFiles = ['sync_config.php', 'sync_state.json', 'sync.log'];
35451d05cddcSAtari911        $preserved = [];
35461d05cddcSAtari911        foreach ($preserveFiles as $file) {
35471d05cddcSAtari911            $oldFile = $pluginDir . $file;
35481d05cddcSAtari911            if (file_exists($oldFile)) {
35491d05cddcSAtari911                $preserved[$file] = file_get_contents($oldFile);
35501d05cddcSAtari911            }
35511d05cddcSAtari911        }
35521d05cddcSAtari911
35531d05cddcSAtari911        // Delete old plugin files (except data files)
35541d05cddcSAtari911        $this->deleteDirectoryContents($pluginDir, $preserveFiles);
35551d05cddcSAtari911
35561d05cddcSAtari911        // Copy new files
35571d05cddcSAtari911        $this->recursiveCopy($sourceDir, $pluginDir);
35581d05cddcSAtari911
35591d05cddcSAtari911        // Restore preserved files
35601d05cddcSAtari911        foreach ($preserved as $file => $content) {
35611d05cddcSAtari911            file_put_contents($pluginDir . $file, $content);
35621d05cddcSAtari911        }
35631d05cddcSAtari911
35641d05cddcSAtari911        // Update version and date in plugin.info.txt
35651d05cddcSAtari911        $pluginInfo = $pluginDir . 'plugin.info.txt';
35661d05cddcSAtari911        if (file_exists($pluginInfo)) {
35671d05cddcSAtari911            $info = confToHash($pluginInfo);
35681d05cddcSAtari911
35691d05cddcSAtari911            // Get new version from uploaded plugin
35701d05cddcSAtari911            $newVersion = $info['version'] ?? 'unknown';
35711d05cddcSAtari911
35721d05cddcSAtari911            // Update date to current
35731d05cddcSAtari911            $info['date'] = date('Y-m-d');
35741d05cddcSAtari911
35751d05cddcSAtari911            // Write updated info back
35761d05cddcSAtari911            $lines = [];
35771d05cddcSAtari911            foreach ($info as $key => $value) {
35781d05cddcSAtari911                $lines[] = str_pad($key, 8) . ' ' . $value;
35791d05cddcSAtari911            }
35801d05cddcSAtari911            file_put_contents($pluginInfo, implode("\n", $lines) . "\n");
35811d05cddcSAtari911        }
35821d05cddcSAtari911
35831d05cddcSAtari911        // Cleanup temp directory
35841d05cddcSAtari911        $this->deleteDirectory($tempDir);
35851d05cddcSAtari911
35861d05cddcSAtari911        $message = 'Plugin updated successfully!';
35871d05cddcSAtari911        if ($backupFirst) {
35881d05cddcSAtari911            $message .= ' Backup saved as: ' . $backupName;
35891d05cddcSAtari911        }
35901d05cddcSAtari911        $this->redirect($message, 'success', 'update');
35911d05cddcSAtari911    }
35921d05cddcSAtari911
35931d05cddcSAtari911    private function deleteBackup() {
35941d05cddcSAtari911        global $INPUT;
35951d05cddcSAtari911
35961d05cddcSAtari911        $filename = $INPUT->str('backup_file');
35971d05cddcSAtari911
35981d05cddcSAtari911        if (empty($filename)) {
35991d05cddcSAtari911            $this->redirect('No backup file specified', 'error', 'update');
36001d05cddcSAtari911            return;
36011d05cddcSAtari911        }
36021d05cddcSAtari911
36031d05cddcSAtari911        // Security: only allow files starting with "calendar" and ending with .zip, no directory traversal
36041d05cddcSAtari911        if (!preg_match('/^calendar[a-zA-Z0-9._-]*\.zip$/', $filename)) {
36051d05cddcSAtari911            $this->redirect('Invalid backup filename', 'error', 'update');
36061d05cddcSAtari911            return;
36071d05cddcSAtari911        }
36081d05cddcSAtari911
36091d05cddcSAtari911        $backupPath = DOKU_PLUGIN . $filename;
36101d05cddcSAtari911
36111d05cddcSAtari911        if (!file_exists($backupPath)) {
36121d05cddcSAtari911            $this->redirect('Backup file not found', 'error', 'update');
36131d05cddcSAtari911            return;
36141d05cddcSAtari911        }
36151d05cddcSAtari911
36161d05cddcSAtari911        if (@unlink($backupPath)) {
36171d05cddcSAtari911            $this->redirect('Backup deleted: ' . $filename, 'success', 'update');
36181d05cddcSAtari911        } else {
36191d05cddcSAtari911            $this->redirect('Failed to delete backup. Check file permissions.', 'error', 'update');
36201d05cddcSAtari911        }
36211d05cddcSAtari911    }
36221d05cddcSAtari911
36231d05cddcSAtari911    private function renameBackup() {
36241d05cddcSAtari911        global $INPUT;
36251d05cddcSAtari911
36261d05cddcSAtari911        $oldName = $INPUT->str('old_name');
36271d05cddcSAtari911        $newName = $INPUT->str('new_name');
36281d05cddcSAtari911
36291d05cddcSAtari911        if (empty($oldName) || empty($newName)) {
36301d05cddcSAtari911            $this->redirect('Missing filename(s)', 'error', 'update');
36311d05cddcSAtari911            return;
36321d05cddcSAtari911        }
36331d05cddcSAtari911
36341d05cddcSAtari911        // Security: validate filenames
36351d05cddcSAtari911        if (!preg_match('/^[a-zA-Z0-9._-]+\.zip$/', $oldName) || !preg_match('/^[a-zA-Z0-9._-]+\.zip$/', $newName)) {
36361d05cddcSAtari911            $this->redirect('Invalid filename format', 'error', 'update');
36371d05cddcSAtari911            return;
36381d05cddcSAtari911        }
36391d05cddcSAtari911
36401d05cddcSAtari911        $oldPath = DOKU_PLUGIN . $oldName;
36411d05cddcSAtari911        $newPath = DOKU_PLUGIN . $newName;
36421d05cddcSAtari911
36431d05cddcSAtari911        if (!file_exists($oldPath)) {
36441d05cddcSAtari911            $this->redirect('Backup file not found', 'error', 'update');
36451d05cddcSAtari911            return;
36461d05cddcSAtari911        }
36471d05cddcSAtari911
36481d05cddcSAtari911        if (file_exists($newPath)) {
36491d05cddcSAtari911            $this->redirect('A file with the new name already exists', 'error', 'update');
36501d05cddcSAtari911            return;
36511d05cddcSAtari911        }
36521d05cddcSAtari911
36531d05cddcSAtari911        if (@rename($oldPath, $newPath)) {
36541d05cddcSAtari911            $this->redirect('Backup renamed: ' . $oldName . ' → ' . $newName, 'success', 'update');
36551d05cddcSAtari911        } else {
36561d05cddcSAtari911            $this->redirect('Failed to rename backup. Check file permissions.', 'error', 'update');
36571d05cddcSAtari911        }
36581d05cddcSAtari911    }
36591d05cddcSAtari911
36601d05cddcSAtari911    private function restoreBackup() {
36611d05cddcSAtari911        global $INPUT;
36621d05cddcSAtari911
36631d05cddcSAtari911        $filename = $INPUT->str('backup_file');
36641d05cddcSAtari911
36651d05cddcSAtari911        if (empty($filename)) {
36661d05cddcSAtari911            $this->redirect('No backup file specified', 'error', 'update');
36671d05cddcSAtari911            return;
36681d05cddcSAtari911        }
36691d05cddcSAtari911
36701d05cddcSAtari911        // Security: only allow files starting with "calendar" and ending with .zip, no directory traversal
36711d05cddcSAtari911        if (!preg_match('/^calendar[a-zA-Z0-9._-]*\.zip$/', $filename)) {
36721d05cddcSAtari911            $this->redirect('Invalid backup filename', 'error', 'update');
36731d05cddcSAtari911            return;
36741d05cddcSAtari911        }
36751d05cddcSAtari911
36761d05cddcSAtari911        $backupPath = DOKU_PLUGIN . $filename;
36771d05cddcSAtari911        $pluginDir = DOKU_PLUGIN . 'calendar/';
36781d05cddcSAtari911
36791d05cddcSAtari911        if (!file_exists($backupPath)) {
36801d05cddcSAtari911            $this->redirect('Backup file not found', 'error', 'update');
36811d05cddcSAtari911            return;
36821d05cddcSAtari911        }
36831d05cddcSAtari911
36841d05cddcSAtari911        // Check if plugin directory is writable
36851d05cddcSAtari911        if (!is_writable($pluginDir)) {
36861d05cddcSAtari911            $this->redirect('Plugin directory is not writable. Please check permissions.', 'error', 'update');
36871d05cddcSAtari911            return;
36881d05cddcSAtari911        }
36891d05cddcSAtari911
36901d05cddcSAtari911        // Extract backup to temp directory
36911d05cddcSAtari911        $tempDir = DOKU_PLUGIN . 'calendar_restore_temp/';
36921d05cddcSAtari911        if (is_dir($tempDir)) {
36931d05cddcSAtari911            $this->deleteDirectory($tempDir);
36941d05cddcSAtari911        }
36951d05cddcSAtari911        mkdir($tempDir);
36961d05cddcSAtari911
36971d05cddcSAtari911        $zip = new ZipArchive();
36981d05cddcSAtari911        if ($zip->open($backupPath) !== TRUE) {
36991d05cddcSAtari911            $this->redirect('Failed to open backup ZIP file', 'error', 'update');
37001d05cddcSAtari911            return;
37011d05cddcSAtari911        }
37021d05cddcSAtari911
37031d05cddcSAtari911        $zip->extractTo($tempDir);
37041d05cddcSAtari911        $zip->close();
37051d05cddcSAtari911
37061d05cddcSAtari911        // The backup contains a "calendar/" folder
37071d05cddcSAtari911        $sourceDir = $tempDir . 'calendar/';
37081d05cddcSAtari911
37091d05cddcSAtari911        if (!is_dir($sourceDir)) {
37101d05cddcSAtari911            $this->deleteDirectory($tempDir);
37111d05cddcSAtari911            $this->redirect('Invalid backup structure', 'error', 'update');
37121d05cddcSAtari911            return;
37131d05cddcSAtari911        }
37141d05cddcSAtari911
37151d05cddcSAtari911        // Delete current plugin directory contents
37161d05cddcSAtari911        $this->deleteDirectoryContents($pluginDir, []);
37171d05cddcSAtari911
37181d05cddcSAtari911        // Copy backup files to plugin directory
37191d05cddcSAtari911        $this->recursiveCopy($sourceDir, $pluginDir);
37201d05cddcSAtari911
37211d05cddcSAtari911        // Cleanup temp directory
37221d05cddcSAtari911        $this->deleteDirectory($tempDir);
37231d05cddcSAtari911
37241d05cddcSAtari911        $this->redirect('Plugin restored from backup: ' . $filename, 'success', 'update');
37251d05cddcSAtari911    }
37261d05cddcSAtari911
3727*9ccd446eSAtari911    private function createManualBackup() {
3728*9ccd446eSAtari911        $pluginDir = DOKU_PLUGIN . 'calendar/';
3729*9ccd446eSAtari911
3730*9ccd446eSAtari911        // Check if plugin directory is readable
3731*9ccd446eSAtari911        if (!is_readable($pluginDir)) {
3732*9ccd446eSAtari911            $this->redirect('Plugin directory is not readable. Please check permissions.', 'error', 'update');
3733*9ccd446eSAtari911            return;
3734*9ccd446eSAtari911        }
3735*9ccd446eSAtari911
3736*9ccd446eSAtari911        // Check if parent directory is writable (for saving backup)
3737*9ccd446eSAtari911        if (!is_writable(DOKU_PLUGIN)) {
3738*9ccd446eSAtari911            $this->redirect('Plugin parent directory is not writable. Cannot save backup.', 'error', 'update');
3739*9ccd446eSAtari911            return;
3740*9ccd446eSAtari911        }
3741*9ccd446eSAtari911
3742*9ccd446eSAtari911        // Get current version
3743*9ccd446eSAtari911        $pluginInfo = $pluginDir . 'plugin.info.txt';
3744*9ccd446eSAtari911        $version = 'unknown';
3745*9ccd446eSAtari911        if (file_exists($pluginInfo)) {
3746*9ccd446eSAtari911            $info = confToHash($pluginInfo);
3747*9ccd446eSAtari911            $version = $info['version'] ?? ($info['date'] ?? 'unknown');
3748*9ccd446eSAtari911        }
3749*9ccd446eSAtari911
3750*9ccd446eSAtari911        $backupName = 'calendar.backup.v' . $version . '.manual.' . date('Y-m-d_H-i-s') . '.zip';
3751*9ccd446eSAtari911        $backupPath = DOKU_PLUGIN . $backupName;
3752*9ccd446eSAtari911
3753*9ccd446eSAtari911        try {
3754*9ccd446eSAtari911            $zip = new ZipArchive();
3755*9ccd446eSAtari911            if ($zip->open($backupPath, ZipArchive::CREATE) === TRUE) {
3756*9ccd446eSAtari911                $fileCount = $this->addDirectoryToZip($zip, $pluginDir, 'calendar/');
3757*9ccd446eSAtari911                $zip->close();
3758*9ccd446eSAtari911
3759*9ccd446eSAtari911                // Verify backup was created and has content
3760*9ccd446eSAtari911                if (!file_exists($backupPath)) {
3761*9ccd446eSAtari911                    $this->redirect('Backup file was not created', 'error', 'update');
3762*9ccd446eSAtari911                    return;
3763*9ccd446eSAtari911                }
3764*9ccd446eSAtari911
3765*9ccd446eSAtari911                $backupSize = filesize($backupPath);
3766*9ccd446eSAtari911                if ($backupSize < 1000) { // Backup should be at least 1KB
3767*9ccd446eSAtari911                    @unlink($backupPath);
3768*9ccd446eSAtari911                    $this->redirect('Backup file is too small (' . $this->formatBytes($backupSize) . '). Only ' . $fileCount . ' files were added. Backup failed.', 'error', 'update');
3769*9ccd446eSAtari911                    return;
3770*9ccd446eSAtari911                }
3771*9ccd446eSAtari911
3772*9ccd446eSAtari911                if ($fileCount < 10) { // Should have at least 10 files
3773*9ccd446eSAtari911                    @unlink($backupPath);
3774*9ccd446eSAtari911                    $this->redirect('Backup incomplete: Only ' . $fileCount . ' files were added (expected 30+). Backup failed.', 'error', 'update');
3775*9ccd446eSAtari911                    return;
3776*9ccd446eSAtari911                }
3777*9ccd446eSAtari911
3778*9ccd446eSAtari911                // Success!
3779*9ccd446eSAtari911                $this->redirect('✓ Manual backup created successfully: ' . $backupName . ' (' . $this->formatBytes($backupSize) . ', ' . $fileCount . ' files)', 'success', 'update');
3780*9ccd446eSAtari911
3781*9ccd446eSAtari911            } else {
3782*9ccd446eSAtari911                $this->redirect('Failed to create backup ZIP file', 'error', 'update');
3783*9ccd446eSAtari911                return;
3784*9ccd446eSAtari911            }
3785*9ccd446eSAtari911        } catch (Exception $e) {
3786*9ccd446eSAtari911            if (file_exists($backupPath)) {
3787*9ccd446eSAtari911                @unlink($backupPath);
3788*9ccd446eSAtari911            }
3789*9ccd446eSAtari911            $this->redirect('Backup failed: ' . $e->getMessage(), 'error', 'update');
3790*9ccd446eSAtari911            return;
3791*9ccd446eSAtari911        }
3792*9ccd446eSAtari911    }
3793*9ccd446eSAtari911
37941d05cddcSAtari911    private function addDirectoryToZip($zip, $dir, $zipPath = '') {
3795*9ccd446eSAtari911        $fileCount = 0;
3796*9ccd446eSAtari911        $errors = [];
3797*9ccd446eSAtari911
3798*9ccd446eSAtari911        if (!is_dir($dir)) {
3799*9ccd446eSAtari911            throw new Exception("Directory does not exist: $dir");
3800*9ccd446eSAtari911        }
3801*9ccd446eSAtari911
3802*9ccd446eSAtari911        if (!is_readable($dir)) {
3803*9ccd446eSAtari911            throw new Exception("Directory is not readable: $dir");
3804*9ccd446eSAtari911        }
3805*9ccd446eSAtari911
38061d05cddcSAtari911        try {
38071d05cddcSAtari911            $files = new RecursiveIteratorIterator(
38081d05cddcSAtari911                new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS),
38091d05cddcSAtari911                RecursiveIteratorIterator::LEAVES_ONLY
38101d05cddcSAtari911            );
38111d05cddcSAtari911
38121d05cddcSAtari911            foreach ($files as $file) {
38131d05cddcSAtari911                if (!$file->isDir()) {
38141d05cddcSAtari911                    $filePath = $file->getRealPath();
38151d05cddcSAtari911                    if ($filePath && is_readable($filePath)) {
38161d05cddcSAtari911                        $relativePath = $zipPath . substr($filePath, strlen($dir));
3817*9ccd446eSAtari911
3818*9ccd446eSAtari911                        if ($zip->addFile($filePath, $relativePath)) {
3819*9ccd446eSAtari911                            $fileCount++;
3820*9ccd446eSAtari911                        } else {
3821*9ccd446eSAtari911                            $errors[] = "Failed to add: " . basename($filePath);
3822*9ccd446eSAtari911                        }
3823*9ccd446eSAtari911                    } else {
3824*9ccd446eSAtari911                        $errors[] = "Cannot read: " . ($filePath ? basename($filePath) : 'unknown');
38251d05cddcSAtari911                    }
38261d05cddcSAtari911                }
38271d05cddcSAtari911            }
3828*9ccd446eSAtari911
3829*9ccd446eSAtari911            // Log any errors but don't fail if we got most files
3830*9ccd446eSAtari911            if (!empty($errors) && count($errors) < 5) {
3831*9ccd446eSAtari911                foreach ($errors as $error) {
3832*9ccd446eSAtari911                    error_log('Calendar plugin backup warning: ' . $error);
3833*9ccd446eSAtari911                }
3834*9ccd446eSAtari911            }
3835*9ccd446eSAtari911
3836*9ccd446eSAtari911            // If too many errors, fail
3837*9ccd446eSAtari911            if (count($errors) > 5) {
3838*9ccd446eSAtari911                throw new Exception("Too many errors adding files to backup: " . implode(', ', array_slice($errors, 0, 5)));
3839*9ccd446eSAtari911            }
3840*9ccd446eSAtari911
38411d05cddcSAtari911        } catch (Exception $e) {
3842*9ccd446eSAtari911            error_log('Calendar plugin backup error: ' . $e->getMessage());
3843*9ccd446eSAtari911            throw $e;
38441d05cddcSAtari911        }
3845*9ccd446eSAtari911
3846*9ccd446eSAtari911        return $fileCount;
38471d05cddcSAtari911    }
38481d05cddcSAtari911
38491d05cddcSAtari911    private function deleteDirectory($dir) {
38501d05cddcSAtari911        if (!is_dir($dir)) return;
38511d05cddcSAtari911
38521d05cddcSAtari911        try {
38531d05cddcSAtari911            $files = new RecursiveIteratorIterator(
38541d05cddcSAtari911                new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS),
38551d05cddcSAtari911                RecursiveIteratorIterator::CHILD_FIRST
38561d05cddcSAtari911            );
38571d05cddcSAtari911
38581d05cddcSAtari911            foreach ($files as $file) {
38591d05cddcSAtari911                if ($file->isDir()) {
38601d05cddcSAtari911                    @rmdir($file->getRealPath());
38611d05cddcSAtari911                } else {
38621d05cddcSAtari911                    @unlink($file->getRealPath());
38631d05cddcSAtari911                }
38641d05cddcSAtari911            }
38651d05cddcSAtari911
38661d05cddcSAtari911            @rmdir($dir);
38671d05cddcSAtari911        } catch (Exception $e) {
38681d05cddcSAtari911            error_log('Calendar plugin delete directory error: ' . $e->getMessage());
38691d05cddcSAtari911        }
38701d05cddcSAtari911    }
38711d05cddcSAtari911
38721d05cddcSAtari911    private function deleteDirectoryContents($dir, $preserve = []) {
38731d05cddcSAtari911        if (!is_dir($dir)) return;
38741d05cddcSAtari911
38751d05cddcSAtari911        $items = scandir($dir);
38761d05cddcSAtari911        foreach ($items as $item) {
38771d05cddcSAtari911            if ($item === '.' || $item === '..') continue;
38781d05cddcSAtari911            if (in_array($item, $preserve)) continue;
38791d05cddcSAtari911
38801d05cddcSAtari911            $path = $dir . $item;
38811d05cddcSAtari911            if (is_dir($path)) {
38821d05cddcSAtari911                $this->deleteDirectory($path);
38831d05cddcSAtari911            } else {
38841d05cddcSAtari911                unlink($path);
38851d05cddcSAtari911            }
38861d05cddcSAtari911        }
38871d05cddcSAtari911    }
38881d05cddcSAtari911
38891d05cddcSAtari911    private function recursiveCopy($src, $dst) {
38901d05cddcSAtari911        $dir = opendir($src);
38911d05cddcSAtari911        @mkdir($dst);
38921d05cddcSAtari911
38931d05cddcSAtari911        while (($file = readdir($dir)) !== false) {
38941d05cddcSAtari911            if ($file !== '.' && $file !== '..') {
38951d05cddcSAtari911                if (is_dir($src . '/' . $file)) {
38961d05cddcSAtari911                    $this->recursiveCopy($src . '/' . $file, $dst . '/' . $file);
38971d05cddcSAtari911                } else {
38981d05cddcSAtari911                    copy($src . '/' . $file, $dst . '/' . $file);
38991d05cddcSAtari911                }
39001d05cddcSAtari911            }
39011d05cddcSAtari911        }
39021d05cddcSAtari911
39031d05cddcSAtari911        closedir($dir);
39041d05cddcSAtari911    }
39051d05cddcSAtari911
39061d05cddcSAtari911    private function formatBytes($bytes) {
39071d05cddcSAtari911        if ($bytes >= 1073741824) {
39081d05cddcSAtari911            return number_format($bytes / 1073741824, 2) . ' GB';
39091d05cddcSAtari911        } elseif ($bytes >= 1048576) {
39101d05cddcSAtari911            return number_format($bytes / 1048576, 2) . ' MB';
39111d05cddcSAtari911        } elseif ($bytes >= 1024) {
39121d05cddcSAtari911            return number_format($bytes / 1024, 2) . ' KB';
39131d05cddcSAtari911        } else {
39141d05cddcSAtari911            return $bytes . ' bytes';
39151d05cddcSAtari911        }
39161d05cddcSAtari911    }
39171d05cddcSAtari911
39181d05cddcSAtari911    private function findPhpBinary() {
39191d05cddcSAtari911        // Try PHP_BINARY constant first (most reliable if available)
39201d05cddcSAtari911        if (defined('PHP_BINARY') && !empty(PHP_BINARY) && is_executable(PHP_BINARY)) {
39211d05cddcSAtari911            return escapeshellarg(PHP_BINARY);
39221d05cddcSAtari911        }
39231d05cddcSAtari911
39241d05cddcSAtari911        // Try common PHP binary locations
39251d05cddcSAtari911        $possiblePaths = [
39261d05cddcSAtari911            '/usr/bin/php',
39271d05cddcSAtari911            '/usr/bin/php8.1',
39281d05cddcSAtari911            '/usr/bin/php8.2',
39291d05cddcSAtari911            '/usr/bin/php8.3',
39301d05cddcSAtari911            '/usr/bin/php7.4',
39311d05cddcSAtari911            '/usr/local/bin/php',
39321d05cddcSAtari911            'php' // Last resort - rely on PATH
39331d05cddcSAtari911        ];
39341d05cddcSAtari911
39351d05cddcSAtari911        foreach ($possiblePaths as $path) {
39361d05cddcSAtari911            // Test if this PHP binary works
39371d05cddcSAtari911            $testOutput = [];
39381d05cddcSAtari911            $testReturn = 0;
39391d05cddcSAtari911            exec($path . ' -v 2>&1', $testOutput, $testReturn);
39401d05cddcSAtari911
39411d05cddcSAtari911            if ($testReturn === 0) {
39421d05cddcSAtari911                return ($path === 'php') ? 'php' : escapeshellarg($path);
39431d05cddcSAtari911            }
39441d05cddcSAtari911        }
39451d05cddcSAtari911
39461d05cddcSAtari911        // Fallback to 'php' and hope it's in PATH
39471d05cddcSAtari911        return 'php';
39481d05cddcSAtari911    }
39491d05cddcSAtari911
39501d05cddcSAtari911    private function redirect($message, $type = 'success', $tab = null) {
39511d05cddcSAtari911        $url = '?do=admin&page=calendar';
39521d05cddcSAtari911        if ($tab) {
39531d05cddcSAtari911            $url .= '&tab=' . $tab;
39541d05cddcSAtari911        }
39551d05cddcSAtari911        $url .= '&msg=' . urlencode($message) . '&msgtype=' . $type;
39561d05cddcSAtari911        header('Location: ' . $url);
39571d05cddcSAtari911        exit;
39581d05cddcSAtari911    }
39591d05cddcSAtari911
39601d05cddcSAtari911    private function getLog() {
39611d05cddcSAtari911        global $INPUT;
39621d05cddcSAtari911
39631d05cddcSAtari911        if ($INPUT->str('call') === 'ajax') {
39641d05cddcSAtari911            header('Content-Type: application/json');
39651d05cddcSAtari911
39661d05cddcSAtari911            $logFile = DOKU_PLUGIN . 'calendar/sync.log';
39671d05cddcSAtari911            $log = '';
39681d05cddcSAtari911
39691d05cddcSAtari911            if (file_exists($logFile)) {
39701d05cddcSAtari911                // Get last 500 lines
39711d05cddcSAtari911                $lines = file($logFile);
39721d05cddcSAtari911                if ($lines !== false) {
39731d05cddcSAtari911                    $lines = array_slice($lines, -500);
39741d05cddcSAtari911                    $log = implode('', $lines);
39751d05cddcSAtari911                }
39761d05cddcSAtari911            } else {
39771d05cddcSAtari911                $log = "No log file found. Sync hasn't run yet.";
39781d05cddcSAtari911            }
39791d05cddcSAtari911
39801d05cddcSAtari911            echo json_encode(['log' => $log]);
39811d05cddcSAtari911            exit;
39821d05cddcSAtari911        }
39831d05cddcSAtari911    }
39841d05cddcSAtari911
39851d05cddcSAtari911    private function exportConfig() {
39861d05cddcSAtari911        global $INPUT;
39871d05cddcSAtari911
39881d05cddcSAtari911        if ($INPUT->str('call') === 'ajax') {
39891d05cddcSAtari911            header('Content-Type: application/json');
39901d05cddcSAtari911
39911d05cddcSAtari911            try {
39921d05cddcSAtari911                $configFile = DOKU_PLUGIN . 'calendar/sync_config.php';
39931d05cddcSAtari911
39941d05cddcSAtari911                if (!file_exists($configFile)) {
39951d05cddcSAtari911                    echo json_encode([
39961d05cddcSAtari911                        'success' => false,
39971d05cddcSAtari911                        'message' => 'Config file not found'
39981d05cddcSAtari911                    ]);
39991d05cddcSAtari911                    exit;
40001d05cddcSAtari911                }
40011d05cddcSAtari911
40021d05cddcSAtari911                // Read config file
40031d05cddcSAtari911                $configContent = file_get_contents($configFile);
40041d05cddcSAtari911
40051d05cddcSAtari911                // Generate encryption key from DokuWiki secret
40061d05cddcSAtari911                $key = $this->getEncryptionKey();
40071d05cddcSAtari911
40081d05cddcSAtari911                // Encrypt config
40091d05cddcSAtari911                $encrypted = $this->encryptData($configContent, $key);
40101d05cddcSAtari911
40111d05cddcSAtari911                echo json_encode([
40121d05cddcSAtari911                    'success' => true,
40131d05cddcSAtari911                    'encrypted' => $encrypted,
40141d05cddcSAtari911                    'message' => 'Config exported successfully'
40151d05cddcSAtari911                ]);
40161d05cddcSAtari911                exit;
40171d05cddcSAtari911
40181d05cddcSAtari911            } catch (Exception $e) {
40191d05cddcSAtari911                echo json_encode([
40201d05cddcSAtari911                    'success' => false,
40211d05cddcSAtari911                    'message' => $e->getMessage()
40221d05cddcSAtari911                ]);
40231d05cddcSAtari911                exit;
40241d05cddcSAtari911            }
40251d05cddcSAtari911        }
40261d05cddcSAtari911    }
40271d05cddcSAtari911
40281d05cddcSAtari911    private function importConfig() {
40291d05cddcSAtari911        global $INPUT;
40301d05cddcSAtari911
40311d05cddcSAtari911        if ($INPUT->str('call') === 'ajax') {
40321d05cddcSAtari911            header('Content-Type: application/json');
40331d05cddcSAtari911
40341d05cddcSAtari911            try {
40351d05cddcSAtari911                $encrypted = $_POST['encrypted_config'] ?? '';
40361d05cddcSAtari911
40371d05cddcSAtari911                if (empty($encrypted)) {
40381d05cddcSAtari911                    echo json_encode([
40391d05cddcSAtari911                        'success' => false,
40401d05cddcSAtari911                        'message' => 'No config data provided'
40411d05cddcSAtari911                    ]);
40421d05cddcSAtari911                    exit;
40431d05cddcSAtari911                }
40441d05cddcSAtari911
40451d05cddcSAtari911                // Generate encryption key from DokuWiki secret
40461d05cddcSAtari911                $key = $this->getEncryptionKey();
40471d05cddcSAtari911
40481d05cddcSAtari911                // Decrypt config
40491d05cddcSAtari911                $configContent = $this->decryptData($encrypted, $key);
40501d05cddcSAtari911
40511d05cddcSAtari911                if ($configContent === false) {
40521d05cddcSAtari911                    echo json_encode([
40531d05cddcSAtari911                        'success' => false,
40541d05cddcSAtari911                        'message' => 'Decryption failed. Invalid key or corrupted file.'
40551d05cddcSAtari911                    ]);
40561d05cddcSAtari911                    exit;
40571d05cddcSAtari911                }
40581d05cddcSAtari911
40591d05cddcSAtari911                // Validate PHP syntax
40601d05cddcSAtari911                $valid = @eval('?>' . $configContent);
40611d05cddcSAtari911                if ($valid === false) {
40621d05cddcSAtari911                    echo json_encode([
40631d05cddcSAtari911                        'success' => false,
40641d05cddcSAtari911                        'message' => 'Invalid config file format'
40651d05cddcSAtari911                    ]);
40661d05cddcSAtari911                    exit;
40671d05cddcSAtari911                }
40681d05cddcSAtari911
40691d05cddcSAtari911                // Write to config file
40701d05cddcSAtari911                $configFile = DOKU_PLUGIN . 'calendar/sync_config.php';
40711d05cddcSAtari911
40721d05cddcSAtari911                // Backup existing config
40731d05cddcSAtari911                if (file_exists($configFile)) {
40741d05cddcSAtari911                    $backupFile = $configFile . '.backup.' . date('Y-m-d_H-i-s');
40751d05cddcSAtari911                    copy($configFile, $backupFile);
40761d05cddcSAtari911                }
40771d05cddcSAtari911
40781d05cddcSAtari911                // Write new config
40791d05cddcSAtari911                if (file_put_contents($configFile, $configContent) === false) {
40801d05cddcSAtari911                    echo json_encode([
40811d05cddcSAtari911                        'success' => false,
40821d05cddcSAtari911                        'message' => 'Failed to write config file'
40831d05cddcSAtari911                    ]);
40841d05cddcSAtari911                    exit;
40851d05cddcSAtari911                }
40861d05cddcSAtari911
40871d05cddcSAtari911                echo json_encode([
40881d05cddcSAtari911                    'success' => true,
40891d05cddcSAtari911                    'message' => 'Config imported successfully'
40901d05cddcSAtari911                ]);
40911d05cddcSAtari911                exit;
40921d05cddcSAtari911
40931d05cddcSAtari911            } catch (Exception $e) {
40941d05cddcSAtari911                echo json_encode([
40951d05cddcSAtari911                    'success' => false,
40961d05cddcSAtari911                    'message' => $e->getMessage()
40971d05cddcSAtari911                ]);
40981d05cddcSAtari911                exit;
40991d05cddcSAtari911            }
41001d05cddcSAtari911        }
41011d05cddcSAtari911    }
41021d05cddcSAtari911
41031d05cddcSAtari911    private function getEncryptionKey() {
41041d05cddcSAtari911        global $conf;
41051d05cddcSAtari911        // Use DokuWiki's secret as the base for encryption
41061d05cddcSAtari911        // This ensures the key is unique per installation
41071d05cddcSAtari911        return hash('sha256', $conf['secret'] . 'calendar_config_encryption', true);
41081d05cddcSAtari911    }
41091d05cddcSAtari911
41101d05cddcSAtari911    private function encryptData($data, $key) {
41111d05cddcSAtari911        // Use AES-256-CBC encryption
41121d05cddcSAtari911        $ivLength = openssl_cipher_iv_length('aes-256-cbc');
41131d05cddcSAtari911        $iv = openssl_random_pseudo_bytes($ivLength);
41141d05cddcSAtari911
41151d05cddcSAtari911        $encrypted = openssl_encrypt($data, 'aes-256-cbc', $key, 0, $iv);
41161d05cddcSAtari911
41171d05cddcSAtari911        // Combine IV and encrypted data, then base64 encode
41181d05cddcSAtari911        return base64_encode($iv . $encrypted);
41191d05cddcSAtari911    }
41201d05cddcSAtari911
41211d05cddcSAtari911    private function decryptData($encryptedData, $key) {
41221d05cddcSAtari911        // Decode base64
41231d05cddcSAtari911        $data = base64_decode($encryptedData);
41241d05cddcSAtari911
41251d05cddcSAtari911        if ($data === false) {
41261d05cddcSAtari911            return false;
41271d05cddcSAtari911        }
41281d05cddcSAtari911
41291d05cddcSAtari911        // Extract IV and encrypted content
41301d05cddcSAtari911        $ivLength = openssl_cipher_iv_length('aes-256-cbc');
41311d05cddcSAtari911        $iv = substr($data, 0, $ivLength);
41321d05cddcSAtari911        $encrypted = substr($data, $ivLength);
41331d05cddcSAtari911
41341d05cddcSAtari911        // Decrypt
41351d05cddcSAtari911        $decrypted = openssl_decrypt($encrypted, 'aes-256-cbc', $key, 0, $iv);
41361d05cddcSAtari911
41371d05cddcSAtari911        return $decrypted;
41381d05cddcSAtari911    }
41391d05cddcSAtari911
41401d05cddcSAtari911    private function clearLogFile() {
41411d05cddcSAtari911        global $INPUT;
41421d05cddcSAtari911
41431d05cddcSAtari911        if ($INPUT->str('call') === 'ajax') {
41441d05cddcSAtari911            header('Content-Type: application/json');
41451d05cddcSAtari911
41461d05cddcSAtari911            $logFile = DOKU_PLUGIN . 'calendar/sync.log';
41471d05cddcSAtari911
41481d05cddcSAtari911            if (file_exists($logFile)) {
41491d05cddcSAtari911                if (file_put_contents($logFile, '')) {
41501d05cddcSAtari911                    echo json_encode(['success' => true]);
41511d05cddcSAtari911                } else {
41521d05cddcSAtari911                    echo json_encode(['success' => false, 'message' => 'Could not clear log file']);
41531d05cddcSAtari911                }
41541d05cddcSAtari911            } else {
41551d05cddcSAtari911                echo json_encode(['success' => true, 'message' => 'No log file to clear']);
41561d05cddcSAtari911            }
41571d05cddcSAtari911            exit;
41581d05cddcSAtari911        }
41591d05cddcSAtari911    }
41601d05cddcSAtari911
41611d05cddcSAtari911    private function downloadLog() {
41621d05cddcSAtari911        $logFile = DOKU_PLUGIN . 'calendar/sync.log';
41631d05cddcSAtari911
41641d05cddcSAtari911        if (file_exists($logFile)) {
41651d05cddcSAtari911            header('Content-Type: text/plain');
41661d05cddcSAtari911            header('Content-Disposition: attachment; filename="calendar-sync-' . date('Y-m-d-His') . '.log"');
41671d05cddcSAtari911            readfile($logFile);
41681d05cddcSAtari911            exit;
41691d05cddcSAtari911        } else {
41701d05cddcSAtari911            echo 'No log file found';
41711d05cddcSAtari911            exit;
41721d05cddcSAtari911        }
41731d05cddcSAtari911    }
41741d05cddcSAtari911
41751d05cddcSAtari911    private function getEventStatistics() {
41761d05cddcSAtari911        $stats = [
41771d05cddcSAtari911            'total_events' => 0,
41781d05cddcSAtari911            'total_namespaces' => 0,
41791d05cddcSAtari911            'total_files' => 0,
41801d05cddcSAtari911            'total_recurring' => 0,
41811d05cddcSAtari911            'by_namespace' => [],
41821d05cddcSAtari911            'last_scan' => ''
41831d05cddcSAtari911        ];
41841d05cddcSAtari911
41851d05cddcSAtari911        $metaDir = DOKU_INC . 'data/meta/';
41861d05cddcSAtari911        $cacheFile = DOKU_PLUGIN . 'calendar/.event_stats_cache';
41871d05cddcSAtari911
41881d05cddcSAtari911        // Check if we have cached stats (less than 5 minutes old)
41891d05cddcSAtari911        if (file_exists($cacheFile)) {
41901d05cddcSAtari911            $cacheData = json_decode(file_get_contents($cacheFile), true);
41911d05cddcSAtari911            if ($cacheData && (time() - $cacheData['timestamp']) < 300) {
41921d05cddcSAtari911                return $cacheData['stats'];
41931d05cddcSAtari911            }
41941d05cddcSAtari911        }
41951d05cddcSAtari911
41961d05cddcSAtari911        // Scan for events
41971d05cddcSAtari911        $this->scanDirectoryForStats($metaDir, '', $stats);
41981d05cddcSAtari911
41991d05cddcSAtari911        // Count recurring events
42001d05cddcSAtari911        $recurringEvents = $this->findRecurringEvents();
42011d05cddcSAtari911        $stats['total_recurring'] = count($recurringEvents);
42021d05cddcSAtari911
42031d05cddcSAtari911        $stats['total_namespaces'] = count($stats['by_namespace']);
42041d05cddcSAtari911        $stats['last_scan'] = date('Y-m-d H:i:s');
42051d05cddcSAtari911
42061d05cddcSAtari911        // Cache the results
42071d05cddcSAtari911        file_put_contents($cacheFile, json_encode([
42081d05cddcSAtari911            'timestamp' => time(),
42091d05cddcSAtari911            'stats' => $stats
42101d05cddcSAtari911        ]));
42111d05cddcSAtari911
42121d05cddcSAtari911        return $stats;
42131d05cddcSAtari911    }
42141d05cddcSAtari911
42151d05cddcSAtari911    private function scanDirectoryForStats($dir, $namespace, &$stats) {
42161d05cddcSAtari911        if (!is_dir($dir)) return;
42171d05cddcSAtari911
42181d05cddcSAtari911        $items = scandir($dir);
42191d05cddcSAtari911        foreach ($items as $item) {
42201d05cddcSAtari911            if ($item === '.' || $item === '..') continue;
42211d05cddcSAtari911
42221d05cddcSAtari911            $path = $dir . $item;
42231d05cddcSAtari911
42241d05cddcSAtari911            // Check if this is a calendar directory
42251d05cddcSAtari911            if ($item === 'calendar' && is_dir($path)) {
42261d05cddcSAtari911                $jsonFiles = glob($path . '/*.json');
42271d05cddcSAtari911                $eventCount = 0;
42281d05cddcSAtari911
42291d05cddcSAtari911                foreach ($jsonFiles as $file) {
42301d05cddcSAtari911                    $stats['total_files']++;
42311d05cddcSAtari911                    $data = json_decode(file_get_contents($file), true);
42321d05cddcSAtari911                    if ($data) {
42331d05cddcSAtari911                        foreach ($data as $dateEvents) {
42341d05cddcSAtari911                            $eventCount += count($dateEvents);
42351d05cddcSAtari911                        }
42361d05cddcSAtari911                    }
42371d05cddcSAtari911                }
42381d05cddcSAtari911
42391d05cddcSAtari911                $stats['total_events'] += $eventCount;
42401d05cddcSAtari911
42411d05cddcSAtari911                if ($eventCount > 0) {
42421d05cddcSAtari911                    $stats['by_namespace'][$namespace] = [
42431d05cddcSAtari911                        'events' => $eventCount,
42441d05cddcSAtari911                        'files' => count($jsonFiles)
42451d05cddcSAtari911                    ];
42461d05cddcSAtari911                }
42471d05cddcSAtari911            } elseif (is_dir($path)) {
42481d05cddcSAtari911                // Recurse into subdirectories
42491d05cddcSAtari911                $newNamespace = $namespace ? $namespace . ':' . $item : $item;
42501d05cddcSAtari911                $this->scanDirectoryForStats($path . '/', $newNamespace, $stats);
42511d05cddcSAtari911            }
42521d05cddcSAtari911        }
42531d05cddcSAtari911    }
42541d05cddcSAtari911
42551d05cddcSAtari911    private function rescanEvents() {
42561d05cddcSAtari911        // Clear the cache to force a rescan
4257*9ccd446eSAtari911        $this->clearStatsCache();
42581d05cddcSAtari911
42591d05cddcSAtari911        // Get fresh statistics
42601d05cddcSAtari911        $stats = $this->getEventStatistics();
42611d05cddcSAtari911
42621d05cddcSAtari911        // Build absolute redirect URL
42631d05cddcSAtari911        $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';
42641d05cddcSAtari911
42651d05cddcSAtari911        // Redirect with success message using absolute URL
42661d05cddcSAtari911        header('Location: ' . $redirectUrl, true, 303);
42671d05cddcSAtari911        exit;
42681d05cddcSAtari911    }
42691d05cddcSAtari911
42701d05cddcSAtari911    private function exportAllEvents() {
42711d05cddcSAtari911        $metaDir = DOKU_INC . 'data/meta/';
42721d05cddcSAtari911        $allEvents = [];
42731d05cddcSAtari911
42741d05cddcSAtari911        // Collect all events
42751d05cddcSAtari911        $this->collectAllEvents($metaDir, '', $allEvents);
42761d05cddcSAtari911
42771d05cddcSAtari911        // Create export package
4278*9ccd446eSAtari911        // Get current version
4279*9ccd446eSAtari911        $pluginInfo = DOKU_PLUGIN . 'calendar/plugin.info.txt';
4280*9ccd446eSAtari911        $info = file_exists($pluginInfo) ? confToHash($pluginInfo) : [];
4281*9ccd446eSAtari911        $currentVersion = isset($info['version']) ? trim($info['version']) : 'unknown';
4282*9ccd446eSAtari911
42831d05cddcSAtari911        $exportData = [
42841d05cddcSAtari911            'export_date' => date('Y-m-d H:i:s'),
4285*9ccd446eSAtari911            'version' => $currentVersion,
42861d05cddcSAtari911            'total_events' => 0,
42871d05cddcSAtari911            'namespaces' => []
42881d05cddcSAtari911        ];
42891d05cddcSAtari911
42901d05cddcSAtari911        foreach ($allEvents as $namespace => $files) {
42911d05cddcSAtari911            $exportData['namespaces'][$namespace] = [];
42921d05cddcSAtari911            foreach ($files as $filename => $events) {
42931d05cddcSAtari911                $exportData['namespaces'][$namespace][$filename] = $events;
42941d05cddcSAtari911                foreach ($events as $dateEvents) {
42951d05cddcSAtari911                    $exportData['total_events'] += count($dateEvents);
42961d05cddcSAtari911                }
42971d05cddcSAtari911            }
42981d05cddcSAtari911        }
42991d05cddcSAtari911
43001d05cddcSAtari911        // Send as download
43011d05cddcSAtari911        header('Content-Type: application/json');
43021d05cddcSAtari911        header('Content-Disposition: attachment; filename="calendar-events-export-' . date('Y-m-d-His') . '.json"');
43031d05cddcSAtari911        echo json_encode($exportData, JSON_PRETTY_PRINT);
43041d05cddcSAtari911        exit;
43051d05cddcSAtari911    }
43061d05cddcSAtari911
43071d05cddcSAtari911    private function collectAllEvents($dir, $namespace, &$allEvents) {
43081d05cddcSAtari911        if (!is_dir($dir)) return;
43091d05cddcSAtari911
43101d05cddcSAtari911        $items = scandir($dir);
43111d05cddcSAtari911        foreach ($items as $item) {
43121d05cddcSAtari911            if ($item === '.' || $item === '..') continue;
43131d05cddcSAtari911
43141d05cddcSAtari911            $path = $dir . $item;
43151d05cddcSAtari911
43161d05cddcSAtari911            // Check if this is a calendar directory
43171d05cddcSAtari911            if ($item === 'calendar' && is_dir($path)) {
43181d05cddcSAtari911                $jsonFiles = glob($path . '/*.json');
43191d05cddcSAtari911
43201d05cddcSAtari911                if (!isset($allEvents[$namespace])) {
43211d05cddcSAtari911                    $allEvents[$namespace] = [];
43221d05cddcSAtari911                }
43231d05cddcSAtari911
43241d05cddcSAtari911                foreach ($jsonFiles as $file) {
43251d05cddcSAtari911                    $filename = basename($file);
43261d05cddcSAtari911                    $data = json_decode(file_get_contents($file), true);
43271d05cddcSAtari911                    if ($data) {
43281d05cddcSAtari911                        $allEvents[$namespace][$filename] = $data;
43291d05cddcSAtari911                    }
43301d05cddcSAtari911                }
43311d05cddcSAtari911            } elseif (is_dir($path)) {
43321d05cddcSAtari911                // Recurse into subdirectories
43331d05cddcSAtari911                $newNamespace = $namespace ? $namespace . ':' . $item : $item;
43341d05cddcSAtari911                $this->collectAllEvents($path . '/', $newNamespace, $allEvents);
43351d05cddcSAtari911            }
43361d05cddcSAtari911        }
43371d05cddcSAtari911    }
43381d05cddcSAtari911
43391d05cddcSAtari911    private function importAllEvents() {
43401d05cddcSAtari911        global $INPUT;
43411d05cddcSAtari911
43421d05cddcSAtari911        if (!isset($_FILES['import_file'])) {
43431d05cddcSAtari911            $redirectUrl = DOKU_URL . 'doku.php?do=admin&page=calendar&tab=manage&msg=' . urlencode('No file uploaded') . '&msgtype=error';
43441d05cddcSAtari911            header('Location: ' . $redirectUrl, true, 303);
43451d05cddcSAtari911            exit;
43461d05cddcSAtari911        }
43471d05cddcSAtari911
43481d05cddcSAtari911        $file = $_FILES['import_file'];
43491d05cddcSAtari911
43501d05cddcSAtari911        if ($file['error'] !== UPLOAD_ERR_OK) {
43511d05cddcSAtari911            $redirectUrl = DOKU_URL . 'doku.php?do=admin&page=calendar&tab=manage&msg=' . urlencode('Upload error: ' . $file['error']) . '&msgtype=error';
43521d05cddcSAtari911            header('Location: ' . $redirectUrl, true, 303);
43531d05cddcSAtari911            exit;
43541d05cddcSAtari911        }
43551d05cddcSAtari911
43561d05cddcSAtari911        // Read and decode the import file
43571d05cddcSAtari911        $importData = json_decode(file_get_contents($file['tmp_name']), true);
43581d05cddcSAtari911
43591d05cddcSAtari911        if (!$importData || !isset($importData['namespaces'])) {
43601d05cddcSAtari911            $redirectUrl = DOKU_URL . 'doku.php?do=admin&page=calendar&tab=manage&msg=' . urlencode('Invalid import file format') . '&msgtype=error';
43611d05cddcSAtari911            header('Location: ' . $redirectUrl, true, 303);
43621d05cddcSAtari911            exit;
43631d05cddcSAtari911        }
43641d05cddcSAtari911
43651d05cddcSAtari911        $importedCount = 0;
43661d05cddcSAtari911        $mergedCount = 0;
43671d05cddcSAtari911
43681d05cddcSAtari911        // Import events
43691d05cddcSAtari911        foreach ($importData['namespaces'] as $namespace => $files) {
43701d05cddcSAtari911            $metaDir = DOKU_INC . 'data/meta/';
43711d05cddcSAtari911            if ($namespace) {
43721d05cddcSAtari911                $metaDir .= str_replace(':', '/', $namespace) . '/';
43731d05cddcSAtari911            }
43741d05cddcSAtari911            $calendarDir = $metaDir . 'calendar/';
43751d05cddcSAtari911
43761d05cddcSAtari911            // Create directory if needed
43771d05cddcSAtari911            if (!is_dir($calendarDir)) {
43781d05cddcSAtari911                mkdir($calendarDir, 0755, true);
43791d05cddcSAtari911            }
43801d05cddcSAtari911
43811d05cddcSAtari911            foreach ($files as $filename => $events) {
43821d05cddcSAtari911                $targetFile = $calendarDir . $filename;
43831d05cddcSAtari911
43841d05cddcSAtari911                // If file exists, merge events
43851d05cddcSAtari911                if (file_exists($targetFile)) {
43861d05cddcSAtari911                    $existing = json_decode(file_get_contents($targetFile), true);
43871d05cddcSAtari911                    if ($existing) {
43881d05cddcSAtari911                        foreach ($events as $date => $dateEvents) {
43891d05cddcSAtari911                            if (!isset($existing[$date])) {
43901d05cddcSAtari911                                $existing[$date] = [];
43911d05cddcSAtari911                            }
43921d05cddcSAtari911                            foreach ($dateEvents as $event) {
43931d05cddcSAtari911                                // Check if event with same ID exists
43941d05cddcSAtari911                                $found = false;
43951d05cddcSAtari911                                foreach ($existing[$date] as $existingEvent) {
43961d05cddcSAtari911                                    if ($existingEvent['id'] === $event['id']) {
43971d05cddcSAtari911                                        $found = true;
43981d05cddcSAtari911                                        break;
43991d05cddcSAtari911                                    }
44001d05cddcSAtari911                                }
44011d05cddcSAtari911                                if (!$found) {
44021d05cddcSAtari911                                    $existing[$date][] = $event;
44031d05cddcSAtari911                                    $importedCount++;
44041d05cddcSAtari911                                } else {
44051d05cddcSAtari911                                    $mergedCount++;
44061d05cddcSAtari911                                }
44071d05cddcSAtari911                            }
44081d05cddcSAtari911                        }
44091d05cddcSAtari911                        file_put_contents($targetFile, json_encode($existing, JSON_PRETTY_PRINT));
44101d05cddcSAtari911                    }
44111d05cddcSAtari911                } else {
44121d05cddcSAtari911                    // New file
44131d05cddcSAtari911                    file_put_contents($targetFile, json_encode($events, JSON_PRETTY_PRINT));
44141d05cddcSAtari911                    foreach ($events as $dateEvents) {
44151d05cddcSAtari911                        $importedCount += count($dateEvents);
44161d05cddcSAtari911                    }
44171d05cddcSAtari911                }
44181d05cddcSAtari911            }
44191d05cddcSAtari911        }
44201d05cddcSAtari911
44211d05cddcSAtari911        // Clear cache
4422*9ccd446eSAtari911        $this->clearStatsCache();
44231d05cddcSAtari911
44241d05cddcSAtari911        $message = "Import complete! Imported $importedCount new events";
44251d05cddcSAtari911        if ($mergedCount > 0) {
44261d05cddcSAtari911            $message .= ", skipped $mergedCount duplicates";
44271d05cddcSAtari911        }
44281d05cddcSAtari911
44291d05cddcSAtari911        $redirectUrl = DOKU_URL . 'doku.php?do=admin&page=calendar&tab=manage&msg=' . urlencode($message) . '&msgtype=success';
44301d05cddcSAtari911        header('Location: ' . $redirectUrl, true, 303);
44311d05cddcSAtari911        exit;
44321d05cddcSAtari911    }
44331d05cddcSAtari911
44341d05cddcSAtari911    private function previewCleanup() {
44351d05cddcSAtari911        global $INPUT;
44361d05cddcSAtari911
44371d05cddcSAtari911        $cleanupType = $INPUT->str('cleanup_type', 'age');
44381d05cddcSAtari911        $namespaceFilter = $INPUT->str('namespace_filter', '');
44391d05cddcSAtari911
44401d05cddcSAtari911        // Debug info
44411d05cddcSAtari911        $debug = [];
44421d05cddcSAtari911        $debug['cleanup_type'] = $cleanupType;
44431d05cddcSAtari911        $debug['namespace_filter'] = $namespaceFilter;
44441d05cddcSAtari911        $debug['age_value'] = $INPUT->int('age_value', 6);
44451d05cddcSAtari911        $debug['age_unit'] = $INPUT->str('age_unit', 'months');
44461d05cddcSAtari911        $debug['range_start'] = $INPUT->str('range_start', '');
44471d05cddcSAtari911        $debug['range_end'] = $INPUT->str('range_end', '');
44481d05cddcSAtari911        $debug['delete_completed'] = $INPUT->bool('delete_completed', false);
44491d05cddcSAtari911        $debug['delete_past'] = $INPUT->bool('delete_past', false);
44501d05cddcSAtari911
44511d05cddcSAtari911        $dataDir = DOKU_INC . 'data/meta/';
44521d05cddcSAtari911        $debug['data_dir'] = $dataDir;
44531d05cddcSAtari911        $debug['data_dir_exists'] = is_dir($dataDir);
44541d05cddcSAtari911
44551d05cddcSAtari911        $eventsToDelete = $this->findEventsToCleanup($cleanupType, $namespaceFilter);
44561d05cddcSAtari911
44571d05cddcSAtari911        // Merge with scan debug info
44581d05cddcSAtari911        if (isset($this->_cleanupDebug)) {
44591d05cddcSAtari911            $debug = array_merge($debug, $this->_cleanupDebug);
44601d05cddcSAtari911        }
44611d05cddcSAtari911
44621d05cddcSAtari911        // Return JSON for preview with debug info
44631d05cddcSAtari911        header('Content-Type: application/json');
44641d05cddcSAtari911        echo json_encode([
44651d05cddcSAtari911            'count' => count($eventsToDelete),
44661d05cddcSAtari911            'events' => array_slice($eventsToDelete, 0, 50), // Limit to 50 for preview
44671d05cddcSAtari911            'debug' => $debug
44681d05cddcSAtari911        ]);
44691d05cddcSAtari911        exit;
44701d05cddcSAtari911    }
44711d05cddcSAtari911
44721d05cddcSAtari911    private function cleanupEvents() {
44731d05cddcSAtari911        global $INPUT;
44741d05cddcSAtari911
44751d05cddcSAtari911        $cleanupType = $INPUT->str('cleanup_type', 'age');
44761d05cddcSAtari911        $namespaceFilter = $INPUT->str('namespace_filter', '');
44771d05cddcSAtari911
44781d05cddcSAtari911        // Create backup first
44791d05cddcSAtari911        $backupDir = DOKU_PLUGIN . 'calendar/backups/';
44801d05cddcSAtari911        if (!is_dir($backupDir)) {
44811d05cddcSAtari911            mkdir($backupDir, 0755, true);
44821d05cddcSAtari911        }
44831d05cddcSAtari911
44841d05cddcSAtari911        $backupFile = $backupDir . 'before-cleanup-' . date('Y-m-d-His') . '.zip';
44851d05cddcSAtari911        $this->createBackup($backupFile);
44861d05cddcSAtari911
44871d05cddcSAtari911        // Find events to delete
44881d05cddcSAtari911        $eventsToDelete = $this->findEventsToCleanup($cleanupType, $namespaceFilter);
44891d05cddcSAtari911        $deletedCount = 0;
44901d05cddcSAtari911
44911d05cddcSAtari911        // Group by file
44921d05cddcSAtari911        $fileGroups = [];
44931d05cddcSAtari911        foreach ($eventsToDelete as $evt) {
44941d05cddcSAtari911            $fileGroups[$evt['file']][] = $evt;
44951d05cddcSAtari911        }
44961d05cddcSAtari911
44971d05cddcSAtari911        // Delete from each file
44981d05cddcSAtari911        foreach ($fileGroups as $file => $events) {
44991d05cddcSAtari911            if (!file_exists($file)) continue;
45001d05cddcSAtari911
45011d05cddcSAtari911            $json = file_get_contents($file);
45021d05cddcSAtari911            $data = json_decode($json, true);
45031d05cddcSAtari911
45041d05cddcSAtari911            if (!$data) continue;
45051d05cddcSAtari911
45061d05cddcSAtari911            // Remove events
45071d05cddcSAtari911            foreach ($events as $evt) {
45081d05cddcSAtari911                if (isset($data[$evt['date']])) {
45091d05cddcSAtari911                    $data[$evt['date']] = array_filter($data[$evt['date']], function($e) use ($evt) {
45101d05cddcSAtari911                        return $e['id'] !== $evt['id'];
45111d05cddcSAtari911                    });
45121d05cddcSAtari911
45131d05cddcSAtari911                    // Remove date key if empty
45141d05cddcSAtari911                    if (empty($data[$evt['date']])) {
45151d05cddcSAtari911                        unset($data[$evt['date']]);
45161d05cddcSAtari911                    }
45171d05cddcSAtari911
45181d05cddcSAtari911                    $deletedCount++;
45191d05cddcSAtari911                }
45201d05cddcSAtari911            }
45211d05cddcSAtari911
45221d05cddcSAtari911            // Save file or delete if empty
45231d05cddcSAtari911            if (empty($data)) {
45241d05cddcSAtari911                unlink($file);
45251d05cddcSAtari911            } else {
45261d05cddcSAtari911                file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT));
45271d05cddcSAtari911            }
45281d05cddcSAtari911        }
45291d05cddcSAtari911
45301d05cddcSAtari911        // Clear cache
4531*9ccd446eSAtari911        $this->clearStatsCache();
45321d05cddcSAtari911
45331d05cddcSAtari911        $message = "Cleanup complete! Deleted $deletedCount event(s). Backup created: " . basename($backupFile);
45341d05cddcSAtari911        $redirectUrl = DOKU_URL . 'doku.php?do=admin&page=calendar&tab=manage&msg=' . urlencode($message) . '&msgtype=success';
45351d05cddcSAtari911        header('Location: ' . $redirectUrl, true, 303);
45361d05cddcSAtari911        exit;
45371d05cddcSAtari911    }
45381d05cddcSAtari911
45391d05cddcSAtari911    private function findEventsToCleanup($cleanupType, $namespaceFilter) {
45401d05cddcSAtari911        global $INPUT;
45411d05cddcSAtari911
45421d05cddcSAtari911        $eventsToDelete = [];
45431d05cddcSAtari911        $dataDir = DOKU_INC . 'data/meta/';
45441d05cddcSAtari911
45451d05cddcSAtari911        $debug = [];
45461d05cddcSAtari911        $debug['scanned_dirs'] = [];
45471d05cddcSAtari911        $debug['found_files'] = [];
45481d05cddcSAtari911
45491d05cddcSAtari911        // Calculate cutoff date for age-based cleanup
45501d05cddcSAtari911        $cutoffDate = null;
45511d05cddcSAtari911        if ($cleanupType === 'age') {
45521d05cddcSAtari911            $ageValue = $INPUT->int('age_value', 6);
45531d05cddcSAtari911            $ageUnit = $INPUT->str('age_unit', 'months');
45541d05cddcSAtari911
45551d05cddcSAtari911            if ($ageUnit === 'years') {
45561d05cddcSAtari911                $ageValue *= 12; // Convert to months
45571d05cddcSAtari911            }
45581d05cddcSAtari911
45591d05cddcSAtari911            $cutoffDate = date('Y-m-d', strtotime("-$ageValue months"));
45601d05cddcSAtari911            $debug['cutoff_date'] = $cutoffDate;
45611d05cddcSAtari911        }
45621d05cddcSAtari911
45631d05cddcSAtari911        // Get date range for range-based cleanup
45641d05cddcSAtari911        $rangeStart = $cleanupType === 'range' ? $INPUT->str('range_start', '') : null;
45651d05cddcSAtari911        $rangeEnd = $cleanupType === 'range' ? $INPUT->str('range_end', '') : null;
45661d05cddcSAtari911
45671d05cddcSAtari911        // Get status filters
45681d05cddcSAtari911        $deleteCompleted = $cleanupType === 'status' && $INPUT->bool('delete_completed', false);
45691d05cddcSAtari911        $deletePast = $cleanupType === 'status' && $INPUT->bool('delete_past', false);
45701d05cddcSAtari911
45711d05cddcSAtari911        // Check root calendar directory first (blank/default namespace)
45721d05cddcSAtari911        $rootCalendarDir = $dataDir . 'calendar';
45731d05cddcSAtari911        $debug['root_calendar_dir'] = $rootCalendarDir;
45741d05cddcSAtari911        $debug['root_exists'] = is_dir($rootCalendarDir);
45751d05cddcSAtari911
45761d05cddcSAtari911        if (is_dir($rootCalendarDir)) {
45771d05cddcSAtari911            if (!$namespaceFilter || $namespaceFilter === '' || $namespaceFilter === 'default') {
45781d05cddcSAtari911                $debug['scanned_dirs'][] = $rootCalendarDir;
45791d05cddcSAtari911                $files = glob($rootCalendarDir . '/*.json');
45801d05cddcSAtari911                $debug['found_files'] = array_merge($debug['found_files'], $files);
45811d05cddcSAtari911                $this->processCalendarFiles($rootCalendarDir, '', $eventsToDelete, $cleanupType, $cutoffDate, $rangeStart, $rangeEnd, $deleteCompleted, $deletePast);
45821d05cddcSAtari911            }
45831d05cddcSAtari911        }
45841d05cddcSAtari911
45851d05cddcSAtari911        // Scan all namespace directories
45861d05cddcSAtari911        $namespaceDirs = glob($dataDir . '*', GLOB_ONLYDIR);
45871d05cddcSAtari911        $debug['namespace_dirs_found'] = $namespaceDirs;
45881d05cddcSAtari911
45891d05cddcSAtari911        foreach ($namespaceDirs as $nsDir) {
45901d05cddcSAtari911            $namespace = basename($nsDir);
45911d05cddcSAtari911
45921d05cddcSAtari911            // Skip the root 'calendar' dir (already processed above)
45931d05cddcSAtari911            if ($namespace === 'calendar') continue;
45941d05cddcSAtari911
45951d05cddcSAtari911            // Check namespace filter
45961d05cddcSAtari911            if ($namespaceFilter && strpos($namespace, $namespaceFilter) === false) {
45971d05cddcSAtari911                continue;
45981d05cddcSAtari911            }
45991d05cddcSAtari911
46001d05cddcSAtari911            $calendarDir = $nsDir . '/calendar';
46011d05cddcSAtari911            $debug['checked_calendar_dirs'][] = $calendarDir;
46021d05cddcSAtari911
46031d05cddcSAtari911            if (!is_dir($calendarDir)) {
46041d05cddcSAtari911                $debug['missing_calendar_dirs'][] = $calendarDir;
46051d05cddcSAtari911                continue;
46061d05cddcSAtari911            }
46071d05cddcSAtari911
46081d05cddcSAtari911            $debug['scanned_dirs'][] = $calendarDir;
46091d05cddcSAtari911            $files = glob($calendarDir . '/*.json');
46101d05cddcSAtari911            $debug['found_files'] = array_merge($debug['found_files'], $files);
46111d05cddcSAtari911            $this->processCalendarFiles($calendarDir, $namespace, $eventsToDelete, $cleanupType, $cutoffDate, $rangeStart, $rangeEnd, $deleteCompleted, $deletePast);
46121d05cddcSAtari911        }
46131d05cddcSAtari911
46141d05cddcSAtari911        // Store debug info globally for preview
46151d05cddcSAtari911        $this->_cleanupDebug = $debug;
46161d05cddcSAtari911
46171d05cddcSAtari911        return $eventsToDelete;
46181d05cddcSAtari911    }
46191d05cddcSAtari911
46201d05cddcSAtari911    private function processCalendarFiles($calendarDir, $namespace, &$eventsToDelete, $cleanupType, $cutoffDate, $rangeStart, $rangeEnd, $deleteCompleted, $deletePast) {
46211d05cddcSAtari911        foreach (glob($calendarDir . '/*.json') as $file) {
46221d05cddcSAtari911            $json = file_get_contents($file);
46231d05cddcSAtari911            $data = json_decode($json, true);
46241d05cddcSAtari911
46251d05cddcSAtari911            if (!$data) continue;
46261d05cddcSAtari911
46271d05cddcSAtari911            foreach ($data as $date => $dateEvents) {
46281d05cddcSAtari911                foreach ($dateEvents as $event) {
46291d05cddcSAtari911                    $shouldDelete = false;
46301d05cddcSAtari911
46311d05cddcSAtari911                    // Age-based
46321d05cddcSAtari911                    if ($cleanupType === 'age' && $cutoffDate && $date < $cutoffDate) {
46331d05cddcSAtari911                        $shouldDelete = true;
46341d05cddcSAtari911                    }
46351d05cddcSAtari911
46361d05cddcSAtari911                    // Range-based
46371d05cddcSAtari911                    if ($cleanupType === 'range' && $rangeStart && $rangeEnd) {
46381d05cddcSAtari911                        if ($date >= $rangeStart && $date <= $rangeEnd) {
46391d05cddcSAtari911                            $shouldDelete = true;
46401d05cddcSAtari911                        }
46411d05cddcSAtari911                    }
46421d05cddcSAtari911
46431d05cddcSAtari911                    // Status-based
46441d05cddcSAtari911                    if ($cleanupType === 'status') {
46451d05cddcSAtari911                        $isTask = isset($event['isTask']) && $event['isTask'];
46461d05cddcSAtari911                        $isCompleted = isset($event['completed']) && $event['completed'];
46471d05cddcSAtari911                        $isPast = $date < date('Y-m-d');
46481d05cddcSAtari911
46491d05cddcSAtari911                        if ($deleteCompleted && $isTask && $isCompleted) {
46501d05cddcSAtari911                            $shouldDelete = true;
46511d05cddcSAtari911                        }
46521d05cddcSAtari911                        if ($deletePast && !$isTask && $isPast) {
46531d05cddcSAtari911                            $shouldDelete = true;
46541d05cddcSAtari911                        }
46551d05cddcSAtari911                    }
46561d05cddcSAtari911
46571d05cddcSAtari911                    if ($shouldDelete) {
46581d05cddcSAtari911                        $eventsToDelete[] = [
46591d05cddcSAtari911                            'id' => $event['id'],
46601d05cddcSAtari911                            'title' => $event['title'],
46611d05cddcSAtari911                            'date' => $date,
46621d05cddcSAtari911                            'namespace' => $namespace ?: 'default',
46631d05cddcSAtari911                            'file' => $file
46641d05cddcSAtari911                        ];
46651d05cddcSAtari911                    }
46661d05cddcSAtari911                }
46671d05cddcSAtari911            }
46681d05cddcSAtari911        }
46691d05cddcSAtari911    }
4670*9ccd446eSAtari911
4671*9ccd446eSAtari911    /**
4672*9ccd446eSAtari911     * Render Themes tab for sidebar widget theme selection
4673*9ccd446eSAtari911     */
4674*9ccd446eSAtari911    private function renderThemesTab($colors = null) {
4675*9ccd446eSAtari911        global $INPUT;
4676*9ccd446eSAtari911
4677*9ccd446eSAtari911        // Use defaults if not provided
4678*9ccd446eSAtari911        if ($colors === null) {
4679*9ccd446eSAtari911            $colors = $this->getTemplateColors();
4680*9ccd446eSAtari911        }
4681*9ccd446eSAtari911
4682*9ccd446eSAtari911        // Handle theme save
4683*9ccd446eSAtari911        if ($INPUT->str('action') === 'save_theme') {
4684*9ccd446eSAtari911            $theme = $INPUT->str('theme', 'matrix');
4685*9ccd446eSAtari911            $weekStart = $INPUT->str('week_start', 'monday');
4686*9ccd446eSAtari911            $this->saveSidebarTheme($theme);
4687*9ccd446eSAtari911            $this->saveWeekStartDay($weekStart);
4688*9ccd446eSAtari911            echo '<div style="background:#d4edda; border:1px solid #c3e6cb; color:#155724; padding:12px; border-radius:4px; margin-bottom:20px;">';
4689*9ccd446eSAtari911            echo '✓ Theme and settings saved successfully! Refresh any page with the sidebar to see changes.';
4690*9ccd446eSAtari911            echo '</div>';
4691*9ccd446eSAtari911        }
4692*9ccd446eSAtari911
4693*9ccd446eSAtari911        $currentTheme = $this->getSidebarTheme();
4694*9ccd446eSAtari911        $currentWeekStart = $this->getWeekStartDay();
4695*9ccd446eSAtari911
4696*9ccd446eSAtari911        echo '<h2 style="margin:0 0 20px 0; color:' . $colors['text'] . ';">�� Sidebar Widget Settings</h2>';
4697*9ccd446eSAtari911        echo '<p style="color:' . $colors['text'] . '; margin-bottom:20px;">Customize the appearance and behavior of the sidebar calendar widget.</p>';
4698*9ccd446eSAtari911
4699*9ccd446eSAtari911        echo '<form method="post" action="?do=admin&page=calendar&tab=themes">';
4700*9ccd446eSAtari911        echo '<input type="hidden" name="action" value="save_theme">';
4701*9ccd446eSAtari911
4702*9ccd446eSAtari911        // Week Start Day Section
4703*9ccd446eSAtari911        echo '<div style="background:' . $colors['bg'] . '; border:1px solid ' . $colors['border'] . '; border-radius:6px; padding:20px; margin-bottom:30px;">';
4704*9ccd446eSAtari911        echo '<h3 style="margin:0 0 15px 0; color:' . $colors['text'] . '; font-size:16px;">�� Week Start Day</h3>';
4705*9ccd446eSAtari911        echo '<p style="color:' . $colors['text'] . '; margin-bottom:15px; font-size:13px;">Choose which day the week calendar grid starts with:</p>';
4706*9ccd446eSAtari911
4707*9ccd446eSAtari911        echo '<div style="display:flex; gap:15px;">';
4708*9ccd446eSAtari911        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;">';
4709*9ccd446eSAtari911        echo '<input type="radio" name="week_start" value="monday" ' . ($currentWeekStart === 'monday' ? 'checked' : '') . ' style="margin-right:10px; width:18px; height:18px;">';
4710*9ccd446eSAtari911        echo '<div>';
4711*9ccd446eSAtari911        echo '<div style="font-weight:bold; color:' . $colors['text'] . '; margin-bottom:3px;">Monday</div>';
4712*9ccd446eSAtari911        echo '<div style="font-size:11px; color:' . $colors['text'] . ';">Week starts on Monday (ISO standard)</div>';
4713*9ccd446eSAtari911        echo '</div>';
4714*9ccd446eSAtari911        echo '</label>';
4715*9ccd446eSAtari911
4716*9ccd446eSAtari911        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;">';
4717*9ccd446eSAtari911        echo '<input type="radio" name="week_start" value="sunday" ' . ($currentWeekStart === 'sunday' ? 'checked' : '') . ' style="margin-right:10px; width:18px; height:18px;">';
4718*9ccd446eSAtari911        echo '<div>';
4719*9ccd446eSAtari911        echo '<div style="font-weight:bold; color:' . $colors['text'] . '; margin-bottom:3px;">Sunday</div>';
4720*9ccd446eSAtari911        echo '<div style="font-size:11px; color:' . $colors['text'] . ';">Week starts on Sunday (US/Canada standard)</div>';
4721*9ccd446eSAtari911        echo '</div>';
4722*9ccd446eSAtari911        echo '</label>';
4723*9ccd446eSAtari911        echo '</div>';
4724*9ccd446eSAtari911        echo '</div>';
4725*9ccd446eSAtari911
4726*9ccd446eSAtari911        // Visual Theme Section
4727*9ccd446eSAtari911        echo '<h3 style="margin:0 0 15px 0; color:' . $colors['text'] . '; font-size:16px;">�� Visual Theme</h3>';
4728*9ccd446eSAtari911
4729*9ccd446eSAtari911        // Matrix Theme
4730*9ccd446eSAtari911        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']) . ';">';
4731*9ccd446eSAtari911        echo '<label style="display:flex; align-items:center; cursor:pointer;">';
4732*9ccd446eSAtari911        echo '<input type="radio" name="theme" value="matrix" ' . ($currentTheme === 'matrix' ? 'checked' : '') . ' style="margin-right:12px; width:20px; height:20px;">';
4733*9ccd446eSAtari911        echo '<div style="flex:1;">';
4734*9ccd446eSAtari911        echo '<div style="font-size:18px; font-weight:bold; color:#00cc07; margin-bottom:8px;">�� Matrix Edition</div>';
4735*9ccd446eSAtari911        echo '<div style="color:' . $colors['text'] . '; margin-bottom:12px;">Dark green theme with Matrix-style glow effects and neon accents</div>';
4736*9ccd446eSAtari911        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>';
4737*9ccd446eSAtari911        echo '</div>';
4738*9ccd446eSAtari911        echo '</label>';
4739*9ccd446eSAtari911        echo '</div>';
4740*9ccd446eSAtari911
4741*9ccd446eSAtari911        // Purple Theme
4742*9ccd446eSAtari911        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']) . ';">';
4743*9ccd446eSAtari911        echo '<label style="display:flex; align-items:center; cursor:pointer;">';
4744*9ccd446eSAtari911        echo '<input type="radio" name="theme" value="purple" ' . ($currentTheme === 'purple' ? 'checked' : '') . ' style="margin-right:12px; width:20px; height:20px;">';
4745*9ccd446eSAtari911        echo '<div style="flex:1;">';
4746*9ccd446eSAtari911        echo '<div style="font-size:18px; font-weight:bold; color:#9b59b6; margin-bottom:8px;">�� Purple Dream</div>';
4747*9ccd446eSAtari911        echo '<div style="color:' . $colors['text'] . '; margin-bottom:12px;">Rich purple theme with elegant violet accents and soft glow</div>';
4748*9ccd446eSAtari911        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>';
4749*9ccd446eSAtari911        echo '</div>';
4750*9ccd446eSAtari911        echo '</label>';
4751*9ccd446eSAtari911        echo '</div>';
4752*9ccd446eSAtari911
4753*9ccd446eSAtari911        // Professional Blue Theme
4754*9ccd446eSAtari911        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']) . ';">';
4755*9ccd446eSAtari911        echo '<label style="display:flex; align-items:center; cursor:pointer;">';
4756*9ccd446eSAtari911        echo '<input type="radio" name="theme" value="professional" ' . ($currentTheme === 'professional' ? 'checked' : '') . ' style="margin-right:12px; width:20px; height:20px;">';
4757*9ccd446eSAtari911        echo '<div style="flex:1;">';
4758*9ccd446eSAtari911        echo '<div style="font-size:18px; font-weight:bold; color:#4a90e2; margin-bottom:8px;">�� Professional Blue</div>';
4759*9ccd446eSAtari911        echo '<div style="color:' . $colors['text'] . '; margin-bottom:12px;">Clean blue and grey theme with modern professional styling, no glow effects</div>';
4760*9ccd446eSAtari911        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>';
4761*9ccd446eSAtari911        echo '</div>';
4762*9ccd446eSAtari911        echo '</label>';
4763*9ccd446eSAtari911        echo '</div>';
4764*9ccd446eSAtari911
4765*9ccd446eSAtari911        // Pink Bling Theme
4766*9ccd446eSAtari911        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']) . ';">';
4767*9ccd446eSAtari911        echo '<label style="display:flex; align-items:center; cursor:pointer;">';
4768*9ccd446eSAtari911        echo '<input type="radio" name="theme" value="pink" ' . ($currentTheme === 'pink' ? 'checked' : '') . ' style="margin-right:12px; width:20px; height:20px;">';
4769*9ccd446eSAtari911        echo '<div style="flex:1;">';
4770*9ccd446eSAtari911        echo '<div style="font-size:18px; font-weight:bold; color:#ff1493; margin-bottom:8px;">�� Pink Bling</div>';
4771*9ccd446eSAtari911        echo '<div style="color:' . $colors['text'] . '; margin-bottom:12px;">Glamorous hot pink theme with maximum sparkle, hearts, and diamonds ✨</div>';
4772*9ccd446eSAtari911        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>';
4773*9ccd446eSAtari911        echo '</div>';
4774*9ccd446eSAtari911        echo '</label>';
4775*9ccd446eSAtari911        echo '</div>';
4776*9ccd446eSAtari911
4777*9ccd446eSAtari911        // Wiki Default Theme
4778*9ccd446eSAtari911        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']) . ';">';
4779*9ccd446eSAtari911        echo '<label style="display:flex; align-items:center; cursor:pointer;">';
4780*9ccd446eSAtari911        echo '<input type="radio" name="theme" value="wiki" ' . ($currentTheme === 'wiki' ? 'checked' : '') . ' style="margin-right:12px; width:20px; height:20px;">';
4781*9ccd446eSAtari911        echo '<div style="flex:1;">';
4782*9ccd446eSAtari911        echo '<div style="font-size:18px; font-weight:bold; color:#2b73b7; margin-bottom:8px;">�� Wiki Default</div>';
4783*9ccd446eSAtari911        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>';
4784*9ccd446eSAtari911        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>';
4785*9ccd446eSAtari911        echo '</div>';
4786*9ccd446eSAtari911        echo '</label>';
4787*9ccd446eSAtari911        echo '</div>';
4788*9ccd446eSAtari911
4789*9ccd446eSAtari911        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>';
4790*9ccd446eSAtari911        echo '</form>';
4791*9ccd446eSAtari911    }
4792*9ccd446eSAtari911
4793*9ccd446eSAtari911    /**
4794*9ccd446eSAtari911     * Get current sidebar theme
4795*9ccd446eSAtari911     */
4796*9ccd446eSAtari911    private function getSidebarTheme() {
4797*9ccd446eSAtari911        $configFile = DOKU_INC . 'data/meta/calendar_theme.txt';
4798*9ccd446eSAtari911        if (file_exists($configFile)) {
4799*9ccd446eSAtari911            return trim(file_get_contents($configFile));
4800*9ccd446eSAtari911        }
4801*9ccd446eSAtari911        return 'matrix'; // Default
4802*9ccd446eSAtari911    }
4803*9ccd446eSAtari911
4804*9ccd446eSAtari911    /**
4805*9ccd446eSAtari911     * Save sidebar theme
4806*9ccd446eSAtari911     */
4807*9ccd446eSAtari911    private function saveSidebarTheme($theme) {
4808*9ccd446eSAtari911        $configFile = DOKU_INC . 'data/meta/calendar_theme.txt';
4809*9ccd446eSAtari911        $validThemes = ['matrix', 'purple', 'professional', 'pink', 'wiki'];
4810*9ccd446eSAtari911
4811*9ccd446eSAtari911        if (in_array($theme, $validThemes)) {
4812*9ccd446eSAtari911            file_put_contents($configFile, $theme);
4813*9ccd446eSAtari911            return true;
4814*9ccd446eSAtari911        }
4815*9ccd446eSAtari911        return false;
4816*9ccd446eSAtari911    }
4817*9ccd446eSAtari911
4818*9ccd446eSAtari911    /**
4819*9ccd446eSAtari911     * Get week start day
4820*9ccd446eSAtari911     */
4821*9ccd446eSAtari911    private function getWeekStartDay() {
4822*9ccd446eSAtari911        $configFile = DOKU_INC . 'data/meta/calendar_week_start.txt';
4823*9ccd446eSAtari911        if (file_exists($configFile)) {
4824*9ccd446eSAtari911            $start = trim(file_get_contents($configFile));
4825*9ccd446eSAtari911            if (in_array($start, ['monday', 'sunday'])) {
4826*9ccd446eSAtari911                return $start;
4827*9ccd446eSAtari911            }
4828*9ccd446eSAtari911        }
4829*9ccd446eSAtari911        return 'sunday'; // Default to Sunday (US/Canada standard)
4830*9ccd446eSAtari911    }
4831*9ccd446eSAtari911
4832*9ccd446eSAtari911    /**
4833*9ccd446eSAtari911     * Save week start day
4834*9ccd446eSAtari911     */
4835*9ccd446eSAtari911    private function saveWeekStartDay($weekStart) {
4836*9ccd446eSAtari911        $configFile = DOKU_INC . 'data/meta/calendar_week_start.txt';
4837*9ccd446eSAtari911        $validStarts = ['monday', 'sunday'];
4838*9ccd446eSAtari911
4839*9ccd446eSAtari911        if (in_array($weekStart, $validStarts)) {
4840*9ccd446eSAtari911            file_put_contents($configFile, $weekStart);
4841*9ccd446eSAtari911            return true;
4842*9ccd446eSAtari911        }
4843*9ccd446eSAtari911        return false;
4844*9ccd446eSAtari911    }
4845*9ccd446eSAtari911
4846*9ccd446eSAtari911    /**
4847*9ccd446eSAtari911     * Get colors from DokuWiki template's style.ini file
4848*9ccd446eSAtari911     */
4849*9ccd446eSAtari911    private function getTemplateColors() {
4850*9ccd446eSAtari911        global $conf;
4851*9ccd446eSAtari911
4852*9ccd446eSAtari911        // Get current template name
4853*9ccd446eSAtari911        $template = $conf['template'];
4854*9ccd446eSAtari911
4855*9ccd446eSAtari911        // Try multiple possible locations for style.ini
4856*9ccd446eSAtari911        $possiblePaths = [
4857*9ccd446eSAtari911            DOKU_INC . 'conf/tpl/' . $template . '/style.ini',
4858*9ccd446eSAtari911            DOKU_INC . 'lib/tpl/' . $template . '/style.ini',
4859*9ccd446eSAtari911        ];
4860*9ccd446eSAtari911
4861*9ccd446eSAtari911        $styleIni = null;
4862*9ccd446eSAtari911        foreach ($possiblePaths as $path) {
4863*9ccd446eSAtari911            if (file_exists($path)) {
4864*9ccd446eSAtari911                $styleIni = parse_ini_file($path, true);
4865*9ccd446eSAtari911                break;
4866*9ccd446eSAtari911            }
4867*9ccd446eSAtari911        }
4868*9ccd446eSAtari911
4869*9ccd446eSAtari911        if (!$styleIni || !isset($styleIni['replacements'])) {
4870*9ccd446eSAtari911            // Return defaults
4871*9ccd446eSAtari911            return [
4872*9ccd446eSAtari911                'bg' => '#fff',
4873*9ccd446eSAtari911                'bg_alt' => '#e8e8e8',
4874*9ccd446eSAtari911                'text' => '#333',
4875*9ccd446eSAtari911                'border' => '#ccc',
4876*9ccd446eSAtari911                'link' => '#2b73b7',
4877*9ccd446eSAtari911            ];
4878*9ccd446eSAtari911        }
4879*9ccd446eSAtari911
4880*9ccd446eSAtari911        $r = $styleIni['replacements'];
4881*9ccd446eSAtari911
4882*9ccd446eSAtari911        return [
4883*9ccd446eSAtari911            'bg' => isset($r['__background__']) ? $r['__background__'] : '#fff',
4884*9ccd446eSAtari911            'bg_alt' => isset($r['__background_alt__']) ? $r['__background_alt__'] : '#e8e8e8',
4885*9ccd446eSAtari911            'text' => isset($r['__text__']) ? $r['__text__'] : '#333',
4886*9ccd446eSAtari911            'border' => isset($r['__border__']) ? $r['__border__'] : '#ccc',
4887*9ccd446eSAtari911            'link' => isset($r['__link__']) ? $r['__link__'] : '#2b73b7',
4888*9ccd446eSAtari911        ];
4889*9ccd446eSAtari911    }
48901d05cddcSAtari911}
4891