xref: /plugin/calendar/admin.php (revision 1d05cddc261a22328c4671319b0963b94fa1a7e9)
1*1d05cddcSAtari911<?php
2*1d05cddcSAtari911/**
3*1d05cddcSAtari911 * Calendar Plugin - Admin Interface
4*1d05cddcSAtari911 * Clean rewrite - Configuration only
5*1d05cddcSAtari911 * Version: 3.3
6*1d05cddcSAtari911 */
7*1d05cddcSAtari911
8*1d05cddcSAtari911if(!defined('DOKU_INC')) die();
9*1d05cddcSAtari911
10*1d05cddcSAtari911class admin_plugin_calendar extends DokuWiki_Admin_Plugin {
11*1d05cddcSAtari911
12*1d05cddcSAtari911    public function getMenuText($language) {
13*1d05cddcSAtari911        return 'Calendar Management';
14*1d05cddcSAtari911    }
15*1d05cddcSAtari911
16*1d05cddcSAtari911    public function getMenuSort() {
17*1d05cddcSAtari911        return 100;
18*1d05cddcSAtari911    }
19*1d05cddcSAtari911
20*1d05cddcSAtari911    public function forAdminOnly() {
21*1d05cddcSAtari911        return true;
22*1d05cddcSAtari911    }
23*1d05cddcSAtari911
24*1d05cddcSAtari911    public function handle() {
25*1d05cddcSAtari911        global $INPUT;
26*1d05cddcSAtari911
27*1d05cddcSAtari911        $action = $INPUT->str('action');
28*1d05cddcSAtari911
29*1d05cddcSAtari911        if ($action === 'clear_cache') {
30*1d05cddcSAtari911            $this->clearCache();
31*1d05cddcSAtari911        } elseif ($action === 'save_config') {
32*1d05cddcSAtari911            $this->saveConfig();
33*1d05cddcSAtari911        } elseif ($action === 'delete_recurring_series') {
34*1d05cddcSAtari911            $this->deleteRecurringSeries();
35*1d05cddcSAtari911        } elseif ($action === 'edit_recurring_series') {
36*1d05cddcSAtari911            $this->editRecurringSeries();
37*1d05cddcSAtari911        } elseif ($action === 'move_events') {
38*1d05cddcSAtari911            $this->moveEvents();
39*1d05cddcSAtari911        } elseif ($action === 'move_selected_events') {
40*1d05cddcSAtari911            $this->moveEvents();
41*1d05cddcSAtari911        } elseif ($action === 'move_single_event') {
42*1d05cddcSAtari911            $this->moveSingleEvent();
43*1d05cddcSAtari911        } elseif ($action === 'delete_selected_events') {
44*1d05cddcSAtari911            $this->deleteSelectedEvents();
45*1d05cddcSAtari911        } elseif ($action === 'create_namespace') {
46*1d05cddcSAtari911            $this->createNamespace();
47*1d05cddcSAtari911        } elseif ($action === 'delete_namespace') {
48*1d05cddcSAtari911            $this->deleteNamespace();
49*1d05cddcSAtari911        } elseif ($action === 'run_sync') {
50*1d05cddcSAtari911            $this->runSync();
51*1d05cddcSAtari911        } elseif ($action === 'stop_sync') {
52*1d05cddcSAtari911            $this->stopSync();
53*1d05cddcSAtari911        } elseif ($action === 'upload_update') {
54*1d05cddcSAtari911            $this->uploadUpdate();
55*1d05cddcSAtari911        } elseif ($action === 'delete_backup') {
56*1d05cddcSAtari911            $this->deleteBackup();
57*1d05cddcSAtari911        } elseif ($action === 'rename_backup') {
58*1d05cddcSAtari911            $this->renameBackup();
59*1d05cddcSAtari911        } elseif ($action === 'restore_backup') {
60*1d05cddcSAtari911            $this->restoreBackup();
61*1d05cddcSAtari911        } elseif ($action === 'export_config') {
62*1d05cddcSAtari911            $this->exportConfig();
63*1d05cddcSAtari911        } elseif ($action === 'import_config') {
64*1d05cddcSAtari911            $this->importConfig();
65*1d05cddcSAtari911        } elseif ($action === 'get_log') {
66*1d05cddcSAtari911            $this->getLog();
67*1d05cddcSAtari911        } elseif ($action === 'clear_log') {
68*1d05cddcSAtari911            $this->clearLogFile();
69*1d05cddcSAtari911        } elseif ($action === 'download_log') {
70*1d05cddcSAtari911            $this->downloadLog();
71*1d05cddcSAtari911        } elseif ($action === 'rescan_events') {
72*1d05cddcSAtari911            $this->rescanEvents();
73*1d05cddcSAtari911        } elseif ($action === 'export_all_events') {
74*1d05cddcSAtari911            $this->exportAllEvents();
75*1d05cddcSAtari911        } elseif ($action === 'import_all_events') {
76*1d05cddcSAtari911            $this->importAllEvents();
77*1d05cddcSAtari911        } elseif ($action === 'preview_cleanup') {
78*1d05cddcSAtari911            $this->previewCleanup();
79*1d05cddcSAtari911        } elseif ($action === 'cleanup_events') {
80*1d05cddcSAtari911            $this->cleanupEvents();
81*1d05cddcSAtari911        }
82*1d05cddcSAtari911    }
83*1d05cddcSAtari911
84*1d05cddcSAtari911    public function html() {
85*1d05cddcSAtari911        global $INPUT;
86*1d05cddcSAtari911
87*1d05cddcSAtari911        // Get current tab - default to 'update' (Update Plugin tab)
88*1d05cddcSAtari911        $tab = $INPUT->str('tab', 'update');
89*1d05cddcSAtari911
90*1d05cddcSAtari911        // Tab navigation
91*1d05cddcSAtari911        echo '<div style="border-bottom:2px solid #ddd; margin:10px 0 15px 0;">';
92*1d05cddcSAtari911        echo '<a href="?do=admin&page=calendar&tab=update" style="display:inline-block; padding:8px 16px; text-decoration:none; color:' . ($tab === 'update' ? '#00cc07' : '#333') . '; border-bottom:3px solid ' . ($tab === 'update' ? '#00cc07' : 'transparent') . '; font-weight:' . ($tab === 'update' ? 'bold' : 'normal') . ';">�� Update Plugin</a>';
93*1d05cddcSAtari911        echo '<a href="?do=admin&page=calendar&tab=config" style="display:inline-block; padding:8px 16px; text-decoration:none; color:' . ($tab === 'config' ? '#00cc07' : '#333') . '; border-bottom:3px solid ' . ($tab === 'config' ? '#00cc07' : 'transparent') . '; font-weight:' . ($tab === 'config' ? 'bold' : 'normal') . ';">⚙️ Outlook Sync</a>';
94*1d05cddcSAtari911        echo '<a href="?do=admin&page=calendar&tab=manage" style="display:inline-block; padding:8px 16px; text-decoration:none; color:' . ($tab === 'manage' ? '#00cc07' : '#333') . '; border-bottom:3px solid ' . ($tab === 'manage' ? '#00cc07' : 'transparent') . '; font-weight:' . ($tab === 'manage' ? 'bold' : 'normal') . ';">�� Manage Events</a>';
95*1d05cddcSAtari911        echo '</div>';
96*1d05cddcSAtari911
97*1d05cddcSAtari911        // Render appropriate tab
98*1d05cddcSAtari911        if ($tab === 'config') {
99*1d05cddcSAtari911            $this->renderConfigTab();
100*1d05cddcSAtari911        } elseif ($tab === 'manage') {
101*1d05cddcSAtari911            $this->renderManageTab();
102*1d05cddcSAtari911        } else {
103*1d05cddcSAtari911            $this->renderUpdateTab();
104*1d05cddcSAtari911        }
105*1d05cddcSAtari911    }
106*1d05cddcSAtari911
107*1d05cddcSAtari911    private function renderConfigTab() {
108*1d05cddcSAtari911        global $INPUT;
109*1d05cddcSAtari911
110*1d05cddcSAtari911        // Load current config
111*1d05cddcSAtari911        $configFile = DOKU_PLUGIN . 'calendar/sync_config.php';
112*1d05cddcSAtari911        $config = [];
113*1d05cddcSAtari911        if (file_exists($configFile)) {
114*1d05cddcSAtari911            $config = include $configFile;
115*1d05cddcSAtari911        }
116*1d05cddcSAtari911
117*1d05cddcSAtari911        // Show message if present
118*1d05cddcSAtari911        if ($INPUT->has('msg')) {
119*1d05cddcSAtari911            $msg = hsc($INPUT->str('msg'));
120*1d05cddcSAtari911            $type = $INPUT->str('msgtype', 'success');
121*1d05cddcSAtari911            $class = ($type === 'success') ? 'msg success' : 'msg error';
122*1d05cddcSAtari911            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;\">";
123*1d05cddcSAtari911            echo $msg;
124*1d05cddcSAtari911            echo "</div>";
125*1d05cddcSAtari911        }
126*1d05cddcSAtari911
127*1d05cddcSAtari911        echo '<h2 style="margin:10px 0; font-size:20px;">Outlook Sync Configuration</h2>';
128*1d05cddcSAtari911
129*1d05cddcSAtari911        // Import/Export buttons
130*1d05cddcSAtari911        echo '<div style="display:flex; gap:10px; margin-bottom:15px;">';
131*1d05cddcSAtari911        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>';
132*1d05cddcSAtari911        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>';
133*1d05cddcSAtari911        echo '<input type="file" id="importFileInput" accept=".enc" style="display:none;" onchange="importConfig(this)">';
134*1d05cddcSAtari911        echo '<span id="importStatus" style="margin-left:10px; font-size:12px;"></span>';
135*1d05cddcSAtari911        echo '</div>';
136*1d05cddcSAtari911
137*1d05cddcSAtari911        echo '<form method="post" action="?do=admin&page=calendar" style="max-width:900px;">';
138*1d05cddcSAtari911        echo '<input type="hidden" name="action" value="save_config">';
139*1d05cddcSAtari911
140*1d05cddcSAtari911        // Azure Credentials
141*1d05cddcSAtari911        echo '<div style="background:#f9f9f9; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px;">';
142*1d05cddcSAtari911        echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">Microsoft Azure App Credentials</h3>';
143*1d05cddcSAtari911        echo '<p style="color:#666; 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>';
144*1d05cddcSAtari911
145*1d05cddcSAtari911        echo '<label style="display:block; font-weight:bold; margin:8px 0 3px; font-size:13px;">Tenant ID</label>';
146*1d05cddcSAtari911        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 #ddd; border-radius:3px; font-size:13px;">';
147*1d05cddcSAtari911
148*1d05cddcSAtari911        echo '<label style="display:block; font-weight:bold; margin:8px 0 3px; font-size:13px;">Client ID (Application ID)</label>';
149*1d05cddcSAtari911        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 #ddd; border-radius:3px; font-size:13px;">';
150*1d05cddcSAtari911
151*1d05cddcSAtari911        echo '<label style="display:block; font-weight:bold; margin:8px 0 3px; font-size:13px;">Client Secret</label>';
152*1d05cddcSAtari911        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 #ddd; border-radius:3px; font-size:13px;">';
153*1d05cddcSAtari911        echo '<p style="color:#999; font-size:0.8em; margin:3px 0 0;">⚠️ Keep this secret safe!</p>';
154*1d05cddcSAtari911        echo '</div>';
155*1d05cddcSAtari911
156*1d05cddcSAtari911        // Outlook Settings
157*1d05cddcSAtari911        echo '<div style="background:#f9f9f9; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px;">';
158*1d05cddcSAtari911        echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">Outlook Settings</h3>';
159*1d05cddcSAtari911
160*1d05cddcSAtari911        echo '<div style="display:grid; grid-template-columns:1fr 1fr; gap:10px;">';
161*1d05cddcSAtari911
162*1d05cddcSAtari911        echo '<div>';
163*1d05cddcSAtari911        echo '<label style="display:block; font-weight:bold; margin:0 0 3px; font-size:13px;">User Email</label>';
164*1d05cddcSAtari911        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 #ddd; border-radius:3px; font-size:13px;">';
165*1d05cddcSAtari911        echo '</div>';
166*1d05cddcSAtari911
167*1d05cddcSAtari911        echo '<div>';
168*1d05cddcSAtari911        echo '<label style="display:block; font-weight:bold; margin:0 0 3px; font-size:13px;">Timezone</label>';
169*1d05cddcSAtari911        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 #ddd; border-radius:3px; font-size:13px;">';
170*1d05cddcSAtari911        echo '</div>';
171*1d05cddcSAtari911
172*1d05cddcSAtari911        echo '<div>';
173*1d05cddcSAtari911        echo '<label style="display:block; font-weight:bold; margin:0 0 3px; font-size:13px;">Default Category</label>';
174*1d05cddcSAtari911        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 #ddd; border-radius:3px; font-size:13px;">';
175*1d05cddcSAtari911        echo '</div>';
176*1d05cddcSAtari911
177*1d05cddcSAtari911        echo '<div>';
178*1d05cddcSAtari911        echo '<label style="display:block; font-weight:bold; margin:0 0 3px; font-size:13px;">Reminder (minutes)</label>';
179*1d05cddcSAtari911        echo '<input type="number" name="reminder_minutes" value="' . hsc($config['reminder_minutes'] ?? 15) . '" placeholder="15" style="width:100%; padding:6px; border:1px solid #ddd; border-radius:3px; font-size:13px;">';
180*1d05cddcSAtari911        echo '</div>';
181*1d05cddcSAtari911
182*1d05cddcSAtari911        echo '</div>'; // end grid
183*1d05cddcSAtari911        echo '</div>';
184*1d05cddcSAtari911
185*1d05cddcSAtari911        // Important Namespaces for Sidebar Widget
186*1d05cddcSAtari911        echo '<div style="background:#f9f9f9; padding:12px; margin:10px 0; border-left:3px solid #9b59b6; border-radius:3px;">';
187*1d05cddcSAtari911        echo '<h3 style="margin:0 0 8px 0; color:#9b59b6; font-size:16px;">�� Important Namespaces (Sidebar Widget)</h3>';
188*1d05cddcSAtari911        echo '<p style="color:#666; font-size:11px; margin:0 0 8px;">Events from these namespaces will be highlighted in purple in the sidebar widget</p>';
189*1d05cddcSAtari911        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 #ddd; border-radius:3px; font-size:12px;" placeholder="important,urgent,priority">';
190*1d05cddcSAtari911        echo '<p style="color:#666; font-size:10px; margin:4px 0 0;">Comma-separated list of namespace names</p>';
191*1d05cddcSAtari911        echo '</div>';
192*1d05cddcSAtari911
193*1d05cddcSAtari911        // Sync Options
194*1d05cddcSAtari911        echo '<div style="background:#f9f9f9; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px;">';
195*1d05cddcSAtari911        echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">Sync Options</h3>';
196*1d05cddcSAtari911
197*1d05cddcSAtari911        $syncCompleted = isset($config['sync_completed_tasks']) ? $config['sync_completed_tasks'] : false;
198*1d05cddcSAtari911        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>';
199*1d05cddcSAtari911
200*1d05cddcSAtari911        $deleteOutlook = isset($config['delete_outlook_events']) ? $config['delete_outlook_events'] : true;
201*1d05cddcSAtari911        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>';
202*1d05cddcSAtari911
203*1d05cddcSAtari911        $syncAll = isset($config['sync_all_namespaces']) ? $config['sync_all_namespaces'] : true;
204*1d05cddcSAtari911        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>';
205*1d05cddcSAtari911
206*1d05cddcSAtari911        // Namespace selection (shown when sync_all is unchecked)
207*1d05cddcSAtari911        echo '<div id="namespace_selection" style="margin-top:10px; ' . ($syncAll ? 'display:none;' : '') . '">';
208*1d05cddcSAtari911        echo '<label style="display:block; font-weight:bold; margin-bottom:5px; font-size:13px;">Select namespaces to sync:</label>';
209*1d05cddcSAtari911
210*1d05cddcSAtari911        // Get available namespaces
211*1d05cddcSAtari911        $availableNamespaces = $this->getAllNamespaces();
212*1d05cddcSAtari911        $selectedNamespaces = isset($config['sync_namespaces']) ? $config['sync_namespaces'] : [];
213*1d05cddcSAtari911
214*1d05cddcSAtari911        echo '<div style="max-height:150px; overflow-y:auto; border:1px solid #ddd; border-radius:3px; padding:8px; background:white;">';
215*1d05cddcSAtari911        echo '<label style="display:block; margin:3px 0;"><input type="checkbox" name="sync_namespaces[]" value=""> (default)</label>';
216*1d05cddcSAtari911        foreach ($availableNamespaces as $ns) {
217*1d05cddcSAtari911            if ($ns !== '') {
218*1d05cddcSAtari911                $checked = in_array($ns, $selectedNamespaces) ? 'checked' : '';
219*1d05cddcSAtari911                echo '<label style="display:block; margin:3px 0;"><input type="checkbox" name="sync_namespaces[]" value="' . hsc($ns) . '" ' . $checked . '> ' . hsc($ns) . '</label>';
220*1d05cddcSAtari911            }
221*1d05cddcSAtari911        }
222*1d05cddcSAtari911        echo '</div>';
223*1d05cddcSAtari911        echo '</div>';
224*1d05cddcSAtari911
225*1d05cddcSAtari911        echo '<script>
226*1d05cddcSAtari911        function toggleNamespaceSelection(checkbox) {
227*1d05cddcSAtari911            document.getElementById("namespace_selection").style.display = checkbox.checked ? "none" : "block";
228*1d05cddcSAtari911        }
229*1d05cddcSAtari911        </script>';
230*1d05cddcSAtari911
231*1d05cddcSAtari911        echo '</div>';
232*1d05cddcSAtari911
233*1d05cddcSAtari911        // Namespace and Color Mapping - Side by Side
234*1d05cddcSAtari911        echo '<div style="display:grid; grid-template-columns:1fr 1fr; gap:10px; margin:10px 0;">';
235*1d05cddcSAtari911
236*1d05cddcSAtari911        // Namespace Mapping
237*1d05cddcSAtari911        echo '<div style="background:#f9f9f9; padding:12px; border-left:3px solid #00cc07; border-radius:3px;">';
238*1d05cddcSAtari911        echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">Namespace → Category</h3>';
239*1d05cddcSAtari911        echo '<p style="color:#666; font-size:0.8em; margin:0 0 5px;">One per line: namespace=Category</p>';
240*1d05cddcSAtari911        echo '<textarea name="category_mapping" rows="6" style="width:100%; padding:6px; border:1px solid #ddd; border-radius:3px; font-family:monospace; font-size:12px; resize:vertical;" placeholder="work=Blue category&#10;personal=Green category">';
241*1d05cddcSAtari911        if (isset($config['category_mapping']) && is_array($config['category_mapping'])) {
242*1d05cddcSAtari911            foreach ($config['category_mapping'] as $ns => $cat) {
243*1d05cddcSAtari911                echo hsc($ns) . '=' . hsc($cat) . "\n";
244*1d05cddcSAtari911            }
245*1d05cddcSAtari911        }
246*1d05cddcSAtari911        echo '</textarea>';
247*1d05cddcSAtari911        echo '</div>';
248*1d05cddcSAtari911
249*1d05cddcSAtari911        // Color Mapping with Color Picker
250*1d05cddcSAtari911        echo '<div style="background:#f9f9f9; padding:12px; border-left:3px solid #00cc07; border-radius:3px;">';
251*1d05cddcSAtari911        echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">�� Event Color → Category</h3>';
252*1d05cddcSAtari911        echo '<p style="color:#666; font-size:0.8em; margin:0 0 8px;">Map calendar colors to Outlook categories</p>';
253*1d05cddcSAtari911
254*1d05cddcSAtari911        // Define calendar colors and Outlook categories (only the main 6 colors)
255*1d05cddcSAtari911        $calendarColors = [
256*1d05cddcSAtari911            '#3498db' => 'Blue',
257*1d05cddcSAtari911            '#2ecc71' => 'Green',
258*1d05cddcSAtari911            '#e74c3c' => 'Red',
259*1d05cddcSAtari911            '#f39c12' => 'Orange',
260*1d05cddcSAtari911            '#9b59b6' => 'Purple',
261*1d05cddcSAtari911            '#1abc9c' => 'Teal'
262*1d05cddcSAtari911        ];
263*1d05cddcSAtari911
264*1d05cddcSAtari911        $outlookCategories = [
265*1d05cddcSAtari911            'Blue category',
266*1d05cddcSAtari911            'Green category',
267*1d05cddcSAtari911            'Orange category',
268*1d05cddcSAtari911            'Red category',
269*1d05cddcSAtari911            'Yellow category',
270*1d05cddcSAtari911            'Purple category'
271*1d05cddcSAtari911        ];
272*1d05cddcSAtari911
273*1d05cddcSAtari911        // Load existing color mappings
274*1d05cddcSAtari911        $existingMappings = isset($config['color_mapping']) && is_array($config['color_mapping'])
275*1d05cddcSAtari911            ? $config['color_mapping']
276*1d05cddcSAtari911            : [];
277*1d05cddcSAtari911
278*1d05cddcSAtari911        // Display color mapping rows
279*1d05cddcSAtari911        echo '<div id="colorMappings" style="max-height:200px; overflow-y:auto;">';
280*1d05cddcSAtari911
281*1d05cddcSAtari911        $rowIndex = 0;
282*1d05cddcSAtari911        foreach ($calendarColors as $hexColor => $colorName) {
283*1d05cddcSAtari911            $selectedCategory = isset($existingMappings[$hexColor]) ? $existingMappings[$hexColor] : '';
284*1d05cddcSAtari911
285*1d05cddcSAtari911            echo '<div style="display:flex; gap:8px; align-items:center; margin-bottom:6px;">';
286*1d05cddcSAtari911
287*1d05cddcSAtari911            // Color preview box
288*1d05cddcSAtari911            echo '<div style="width:24px; height:24px; background:' . $hexColor . '; border:2px solid #ddd; border-radius:3px; flex-shrink:0;"></div>';
289*1d05cddcSAtari911
290*1d05cddcSAtari911            // Color name
291*1d05cddcSAtari911            echo '<span style="font-size:12px; min-width:90px; color:#666;">' . $colorName . '</span>';
292*1d05cddcSAtari911
293*1d05cddcSAtari911            // Arrow
294*1d05cddcSAtari911            echo '<span style="color:#999; font-size:12px;">→</span>';
295*1d05cddcSAtari911
296*1d05cddcSAtari911            // Outlook category dropdown
297*1d05cddcSAtari911            echo '<select name="color_map_' . $rowIndex . '" style="flex:1; padding:4px; border:1px solid #ddd; border-radius:3px; font-size:12px;">';
298*1d05cddcSAtari911            echo '<option value="">-- None --</option>';
299*1d05cddcSAtari911            foreach ($outlookCategories as $category) {
300*1d05cddcSAtari911                $selected = ($selectedCategory === $category) ? 'selected' : '';
301*1d05cddcSAtari911                echo '<option value="' . hsc($category) . '" ' . $selected . '>' . hsc($category) . '</option>';
302*1d05cddcSAtari911            }
303*1d05cddcSAtari911            echo '</select>';
304*1d05cddcSAtari911
305*1d05cddcSAtari911            // Hidden input for the hex color
306*1d05cddcSAtari911            echo '<input type="hidden" name="color_hex_' . $rowIndex . '" value="' . $hexColor . '">';
307*1d05cddcSAtari911
308*1d05cddcSAtari911            echo '</div>';
309*1d05cddcSAtari911            $rowIndex++;
310*1d05cddcSAtari911        }
311*1d05cddcSAtari911
312*1d05cddcSAtari911        echo '</div>';
313*1d05cddcSAtari911
314*1d05cddcSAtari911        // Hidden input to track number of color mappings
315*1d05cddcSAtari911        echo '<input type="hidden" name="color_mapping_count" value="' . $rowIndex . '">';
316*1d05cddcSAtari911
317*1d05cddcSAtari911        echo '</div>';
318*1d05cddcSAtari911
319*1d05cddcSAtari911        echo '</div>'; // end grid
320*1d05cddcSAtari911
321*1d05cddcSAtari911        // Submit button
322*1d05cddcSAtari911        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>';
323*1d05cddcSAtari911        echo '</form>';
324*1d05cddcSAtari911
325*1d05cddcSAtari911        // JavaScript for Import/Export
326*1d05cddcSAtari911        echo '<script>
327*1d05cddcSAtari911        async function exportConfig() {
328*1d05cddcSAtari911            try {
329*1d05cddcSAtari911                const response = await fetch("?do=admin&page=calendar&action=export_config&call=ajax", {
330*1d05cddcSAtari911                    method: "POST"
331*1d05cddcSAtari911                });
332*1d05cddcSAtari911                const data = await response.json();
333*1d05cddcSAtari911
334*1d05cddcSAtari911                if (data.success) {
335*1d05cddcSAtari911                    // Create download link
336*1d05cddcSAtari911                    const blob = new Blob([data.encrypted], {type: "application/octet-stream"});
337*1d05cddcSAtari911                    const url = URL.createObjectURL(blob);
338*1d05cddcSAtari911                    const a = document.createElement("a");
339*1d05cddcSAtari911                    a.href = url;
340*1d05cddcSAtari911                    a.download = "sync_config_" + new Date().toISOString().split("T")[0] + ".enc";
341*1d05cddcSAtari911                    document.body.appendChild(a);
342*1d05cddcSAtari911                    a.click();
343*1d05cddcSAtari911                    document.body.removeChild(a);
344*1d05cddcSAtari911                    URL.revokeObjectURL(url);
345*1d05cddcSAtari911
346*1d05cddcSAtari911                    alert("✅ Config exported successfully!\\n\\n⚠️ This file contains encrypted credentials.\\nKeep it secure!");
347*1d05cddcSAtari911                } else {
348*1d05cddcSAtari911                    alert("❌ Export failed: " + data.message);
349*1d05cddcSAtari911                }
350*1d05cddcSAtari911            } catch (error) {
351*1d05cddcSAtari911                alert("❌ Error: " + error.message);
352*1d05cddcSAtari911            }
353*1d05cddcSAtari911        }
354*1d05cddcSAtari911
355*1d05cddcSAtari911        async function importConfig(input) {
356*1d05cddcSAtari911            const file = input.files[0];
357*1d05cddcSAtari911            if (!file) return;
358*1d05cddcSAtari911
359*1d05cddcSAtari911            const status = document.getElementById("importStatus");
360*1d05cddcSAtari911            status.textContent = "⏳ Importing...";
361*1d05cddcSAtari911            status.style.color = "#00cc07";
362*1d05cddcSAtari911
363*1d05cddcSAtari911            try {
364*1d05cddcSAtari911                const encrypted = await file.text();
365*1d05cddcSAtari911
366*1d05cddcSAtari911                const formData = new FormData();
367*1d05cddcSAtari911                formData.append("encrypted_config", encrypted);
368*1d05cddcSAtari911
369*1d05cddcSAtari911                const response = await fetch("?do=admin&page=calendar&action=import_config&call=ajax", {
370*1d05cddcSAtari911                    method: "POST",
371*1d05cddcSAtari911                    body: formData
372*1d05cddcSAtari911                });
373*1d05cddcSAtari911                const data = await response.json();
374*1d05cddcSAtari911
375*1d05cddcSAtari911                if (data.success) {
376*1d05cddcSAtari911                    status.textContent = "✅ Import successful! Reloading...";
377*1d05cddcSAtari911                    status.style.color = "#28a745";
378*1d05cddcSAtari911                    setTimeout(() => {
379*1d05cddcSAtari911                        window.location.reload();
380*1d05cddcSAtari911                    }, 1500);
381*1d05cddcSAtari911                } else {
382*1d05cddcSAtari911                    status.textContent = "❌ Import failed: " + data.message;
383*1d05cddcSAtari911                    status.style.color = "#dc3545";
384*1d05cddcSAtari911                }
385*1d05cddcSAtari911            } catch (error) {
386*1d05cddcSAtari911                status.textContent = "❌ Error: " + error.message;
387*1d05cddcSAtari911                status.style.color = "#dc3545";
388*1d05cddcSAtari911            }
389*1d05cddcSAtari911
390*1d05cddcSAtari911            // Reset file input
391*1d05cddcSAtari911            input.value = "";
392*1d05cddcSAtari911        }
393*1d05cddcSAtari911        </script>';
394*1d05cddcSAtari911
395*1d05cddcSAtari911        // Sync Controls Section
396*1d05cddcSAtari911        echo '<div style="background:#f9f9f9; padding:12px; margin:15px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:900px;">';
397*1d05cddcSAtari911        echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">�� Sync Controls</h3>';
398*1d05cddcSAtari911
399*1d05cddcSAtari911        // Check cron job status
400*1d05cddcSAtari911        $cronStatus = $this->getCronStatus();
401*1d05cddcSAtari911
402*1d05cddcSAtari911        // Check log file permissions
403*1d05cddcSAtari911        $logFile = DOKU_PLUGIN . 'calendar/sync.log';
404*1d05cddcSAtari911        $logWritable = is_writable($logFile) || is_writable(dirname($logFile));
405*1d05cddcSAtari911
406*1d05cddcSAtari911        echo '<div style="display:flex; gap:10px; align-items:center; margin-bottom:10px;">';
407*1d05cddcSAtari911        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>';
408*1d05cddcSAtari911        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>';
409*1d05cddcSAtari911
410*1d05cddcSAtari911        if ($cronStatus['active']) {
411*1d05cddcSAtari911            echo '<span style="color:#666; font-size:12px;">⏰ ' . hsc($cronStatus['frequency']) . '</span>';
412*1d05cddcSAtari911        } else {
413*1d05cddcSAtari911            echo '<span style="color:#999; font-size:12px;">⚠️ No cron job detected</span>';
414*1d05cddcSAtari911        }
415*1d05cddcSAtari911
416*1d05cddcSAtari911        echo '<span id="syncStatus" style="color:#666; font-size:12px; margin-left:auto;"></span>';
417*1d05cddcSAtari911        echo '</div>';
418*1d05cddcSAtari911
419*1d05cddcSAtari911        // Show permission warning if log not writable
420*1d05cddcSAtari911        if (!$logWritable) {
421*1d05cddcSAtari911            echo '<div style="background:#fff3e0; border-left:3px solid #ff9800; padding:8px; margin:8px 0; border-radius:3px;">';
422*1d05cddcSAtari911            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>';
423*1d05cddcSAtari911            echo '</div>';
424*1d05cddcSAtari911        }
425*1d05cddcSAtari911
426*1d05cddcSAtari911        // Show debug info if cron detected
427*1d05cddcSAtari911        if ($cronStatus['active'] && !empty($cronStatus['full_line'])) {
428*1d05cddcSAtari911            echo '<details style="margin-top:5px;">';
429*1d05cddcSAtari911            echo '<summary style="cursor:pointer; color:#999; font-size:11px;">Show cron details</summary>';
430*1d05cddcSAtari911            echo '<pre style="background:#f0f0f0; padding:8px; border-radius:3px; font-size:10px; margin:5px 0; overflow-x:auto;">' . hsc($cronStatus['full_line']) . '</pre>';
431*1d05cddcSAtari911            echo '</details>';
432*1d05cddcSAtari911        }
433*1d05cddcSAtari911
434*1d05cddcSAtari911        if (!$cronStatus['active']) {
435*1d05cddcSAtari911            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>';
436*1d05cddcSAtari911        }
437*1d05cddcSAtari911
438*1d05cddcSAtari911        echo '</div>';
439*1d05cddcSAtari911
440*1d05cddcSAtari911        // JavaScript for Run Sync Now
441*1d05cddcSAtari911        echo '<script>
442*1d05cddcSAtari911        let syncAbortController = null;
443*1d05cddcSAtari911
444*1d05cddcSAtari911        function runSyncNow() {
445*1d05cddcSAtari911            const btn = document.getElementById("syncBtn");
446*1d05cddcSAtari911            const stopBtn = document.getElementById("stopBtn");
447*1d05cddcSAtari911            const status = document.getElementById("syncStatus");
448*1d05cddcSAtari911
449*1d05cddcSAtari911            btn.disabled = true;
450*1d05cddcSAtari911            btn.style.display = "none";
451*1d05cddcSAtari911            stopBtn.style.display = "inline-block";
452*1d05cddcSAtari911            btn.textContent = "⏳ Running...";
453*1d05cddcSAtari911            btn.style.background = "#999";
454*1d05cddcSAtari911            status.textContent = "Starting sync...";
455*1d05cddcSAtari911            status.style.color = "#00cc07";
456*1d05cddcSAtari911
457*1d05cddcSAtari911            // Create abort controller for this sync
458*1d05cddcSAtari911            syncAbortController = new AbortController();
459*1d05cddcSAtari911
460*1d05cddcSAtari911            fetch("?do=admin&page=calendar&action=run_sync&call=ajax", {
461*1d05cddcSAtari911                method: "POST",
462*1d05cddcSAtari911                signal: syncAbortController.signal
463*1d05cddcSAtari911            })
464*1d05cddcSAtari911                .then(response => response.json())
465*1d05cddcSAtari911                .then(data => {
466*1d05cddcSAtari911                    if (data.success) {
467*1d05cddcSAtari911                        status.textContent = "✅ " + data.message;
468*1d05cddcSAtari911                        status.style.color = "#28a745";
469*1d05cddcSAtari911                    } else {
470*1d05cddcSAtari911                        status.textContent = "❌ " + data.message;
471*1d05cddcSAtari911                        status.style.color = "#dc3545";
472*1d05cddcSAtari911                    }
473*1d05cddcSAtari911                    btn.disabled = false;
474*1d05cddcSAtari911                    btn.style.display = "inline-block";
475*1d05cddcSAtari911                    stopBtn.style.display = "none";
476*1d05cddcSAtari911                    btn.textContent = "▶️ Run Sync Now";
477*1d05cddcSAtari911                    btn.style.background = "#00cc07";
478*1d05cddcSAtari911                    syncAbortController = null;
479*1d05cddcSAtari911
480*1d05cddcSAtari911                    // Clear status after 10 seconds
481*1d05cddcSAtari911                    setTimeout(() => {
482*1d05cddcSAtari911                        status.textContent = "";
483*1d05cddcSAtari911                    }, 10000);
484*1d05cddcSAtari911                })
485*1d05cddcSAtari911                .catch(error => {
486*1d05cddcSAtari911                    if (error.name === "AbortError") {
487*1d05cddcSAtari911                        status.textContent = "⏹️ Sync stopped by user";
488*1d05cddcSAtari911                        status.style.color = "#ff9800";
489*1d05cddcSAtari911                    } else {
490*1d05cddcSAtari911                        status.textContent = "❌ Error: " + error.message;
491*1d05cddcSAtari911                        status.style.color = "#dc3545";
492*1d05cddcSAtari911                    }
493*1d05cddcSAtari911                    btn.disabled = false;
494*1d05cddcSAtari911                    btn.style.display = "inline-block";
495*1d05cddcSAtari911                    stopBtn.style.display = "none";
496*1d05cddcSAtari911                    btn.textContent = "▶️ Run Sync Now";
497*1d05cddcSAtari911                    btn.style.background = "#00cc07";
498*1d05cddcSAtari911                    syncAbortController = null;
499*1d05cddcSAtari911                });
500*1d05cddcSAtari911        }
501*1d05cddcSAtari911
502*1d05cddcSAtari911        function stopSyncNow() {
503*1d05cddcSAtari911            const status = document.getElementById("syncStatus");
504*1d05cddcSAtari911
505*1d05cddcSAtari911            status.textContent = "⏹️ Sending stop signal...";
506*1d05cddcSAtari911            status.style.color = "#ff9800";
507*1d05cddcSAtari911
508*1d05cddcSAtari911            // First, send stop signal to server
509*1d05cddcSAtari911            fetch("?do=admin&page=calendar&action=stop_sync&call=ajax", {
510*1d05cddcSAtari911                method: "POST"
511*1d05cddcSAtari911            })
512*1d05cddcSAtari911            .then(response => response.json())
513*1d05cddcSAtari911            .then(data => {
514*1d05cddcSAtari911                if (data.success) {
515*1d05cddcSAtari911                    status.textContent = "⏹️ Stop signal sent - sync will abort soon";
516*1d05cddcSAtari911                    status.style.color = "#ff9800";
517*1d05cddcSAtari911                } else {
518*1d05cddcSAtari911                    status.textContent = "⚠️ " + data.message;
519*1d05cddcSAtari911                    status.style.color = "#ff9800";
520*1d05cddcSAtari911                }
521*1d05cddcSAtari911            })
522*1d05cddcSAtari911            .catch(error => {
523*1d05cddcSAtari911                status.textContent = "⚠️ Error sending stop signal: " + error.message;
524*1d05cddcSAtari911                status.style.color = "#ff9800";
525*1d05cddcSAtari911            });
526*1d05cddcSAtari911
527*1d05cddcSAtari911            // Also abort the fetch request
528*1d05cddcSAtari911            if (syncAbortController) {
529*1d05cddcSAtari911                syncAbortController.abort();
530*1d05cddcSAtari911                status.textContent = "⏹️ Stopping sync...";
531*1d05cddcSAtari911                status.style.color = "#ff9800";
532*1d05cddcSAtari911            }
533*1d05cddcSAtari911        }
534*1d05cddcSAtari911        </script>';
535*1d05cddcSAtari911
536*1d05cddcSAtari911        // Log Viewer Section - More Compact
537*1d05cddcSAtari911        echo '<div style="background:#f9f9f9; padding:12px; margin:15px 0 10px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:900px;">';
538*1d05cddcSAtari911        echo '<h3 style="margin:0 0 5px 0; color:#00cc07; font-size:16px;">�� Live Sync Log</h3>';
539*1d05cddcSAtari911        echo '<p style="color:#666; font-size:0.8em; margin:0 0 8px;">Updates every 2 seconds</p>';
540*1d05cddcSAtari911
541*1d05cddcSAtari911        // Log viewer container
542*1d05cddcSAtari911        echo '<div style="background:#1e1e1e; border-radius:5px; overflow:hidden; box-shadow:0 2px 4px rgba(0,0,0,0.3);">';
543*1d05cddcSAtari911
544*1d05cddcSAtari911        // Log header - More compact
545*1d05cddcSAtari911        echo '<div style="background:#2d2d2d; padding:6px 10px; display:flex; justify-content:space-between; align-items:center; border-bottom:1px solid #444;">';
546*1d05cddcSAtari911        echo '<span style="color:#00cc07; font-family:monospace; font-weight:bold; font-size:12px;">sync.log</span>';
547*1d05cddcSAtari911        echo '<div>';
548*1d05cddcSAtari911        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>';
549*1d05cddcSAtari911        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>';
550*1d05cddcSAtari911        echo '<button onclick="downloadLog()" style="background:#666; color:white; border:none; padding:4px 8px; border-radius:3px; cursor:pointer; font-size:11px;">�� Download</button>';
551*1d05cddcSAtari911        echo '</div>';
552*1d05cddcSAtari911        echo '</div>';
553*1d05cddcSAtari911
554*1d05cddcSAtari911        // Log content - Reduced height to 250px
555*1d05cddcSAtari911        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>';
556*1d05cddcSAtari911
557*1d05cddcSAtari911        echo '</div>';
558*1d05cddcSAtari911        echo '</div>';
559*1d05cddcSAtari911
560*1d05cddcSAtari911        // JavaScript for log viewer
561*1d05cddcSAtari911        echo '<script>
562*1d05cddcSAtari911        let refreshInterval = null;
563*1d05cddcSAtari911        let isPaused = false;
564*1d05cddcSAtari911
565*1d05cddcSAtari911        function refreshLog() {
566*1d05cddcSAtari911            if (isPaused) return;
567*1d05cddcSAtari911
568*1d05cddcSAtari911            fetch("?do=admin&page=calendar&action=get_log&call=ajax")
569*1d05cddcSAtari911                .then(response => response.json())
570*1d05cddcSAtari911                .then(data => {
571*1d05cddcSAtari911                    const logContent = document.getElementById("logContent");
572*1d05cddcSAtari911                    if (logContent) {
573*1d05cddcSAtari911                        logContent.textContent = data.log || "No log data available";
574*1d05cddcSAtari911                        logContent.scrollTop = logContent.scrollHeight;
575*1d05cddcSAtari911                    }
576*1d05cddcSAtari911                })
577*1d05cddcSAtari911                .catch(error => {
578*1d05cddcSAtari911                    console.error("Error fetching log:", error);
579*1d05cddcSAtari911                });
580*1d05cddcSAtari911        }
581*1d05cddcSAtari911
582*1d05cddcSAtari911        function togglePause() {
583*1d05cddcSAtari911            isPaused = !isPaused;
584*1d05cddcSAtari911            const btn = document.getElementById("pauseBtn");
585*1d05cddcSAtari911            if (isPaused) {
586*1d05cddcSAtari911                btn.textContent = "▶ Resume";
587*1d05cddcSAtari911                btn.style.background = "#00cc07";
588*1d05cddcSAtari911            } else {
589*1d05cddcSAtari911                btn.textContent = "⏸ Pause";
590*1d05cddcSAtari911                btn.style.background = "#666";
591*1d05cddcSAtari911                refreshLog();
592*1d05cddcSAtari911            }
593*1d05cddcSAtari911        }
594*1d05cddcSAtari911
595*1d05cddcSAtari911        function clearLog() {
596*1d05cddcSAtari911            if (!confirm("Clear the sync log file?\\n\\nThis will delete all log entries.")) {
597*1d05cddcSAtari911                return;
598*1d05cddcSAtari911            }
599*1d05cddcSAtari911
600*1d05cddcSAtari911            fetch("?do=admin&page=calendar&action=clear_log&call=ajax", {
601*1d05cddcSAtari911                method: "POST"
602*1d05cddcSAtari911            })
603*1d05cddcSAtari911                .then(response => response.json())
604*1d05cddcSAtari911                .then(data => {
605*1d05cddcSAtari911                    if (data.success) {
606*1d05cddcSAtari911                        refreshLog();
607*1d05cddcSAtari911                        alert("Log cleared successfully");
608*1d05cddcSAtari911                    } else {
609*1d05cddcSAtari911                        alert("Error clearing log: " + data.message);
610*1d05cddcSAtari911                    }
611*1d05cddcSAtari911                })
612*1d05cddcSAtari911                .catch(error => {
613*1d05cddcSAtari911                    alert("Error: " + error.message);
614*1d05cddcSAtari911                });
615*1d05cddcSAtari911        }
616*1d05cddcSAtari911
617*1d05cddcSAtari911        function downloadLog() {
618*1d05cddcSAtari911            window.location.href = "?do=admin&page=calendar&action=download_log";
619*1d05cddcSAtari911        }
620*1d05cddcSAtari911
621*1d05cddcSAtari911        // Start auto-refresh
622*1d05cddcSAtari911        refreshLog();
623*1d05cddcSAtari911        refreshInterval = setInterval(refreshLog, 2000);
624*1d05cddcSAtari911
625*1d05cddcSAtari911        // Cleanup on page unload
626*1d05cddcSAtari911        window.addEventListener("beforeunload", function() {
627*1d05cddcSAtari911            if (refreshInterval) {
628*1d05cddcSAtari911                clearInterval(refreshInterval);
629*1d05cddcSAtari911            }
630*1d05cddcSAtari911        });
631*1d05cddcSAtari911        </script>';
632*1d05cddcSAtari911    }
633*1d05cddcSAtari911
634*1d05cddcSAtari911    private function renderManageTab() {
635*1d05cddcSAtari911        global $INPUT;
636*1d05cddcSAtari911
637*1d05cddcSAtari911        // Show message if present
638*1d05cddcSAtari911        if ($INPUT->has('msg')) {
639*1d05cddcSAtari911            $msg = hsc($INPUT->str('msg'));
640*1d05cddcSAtari911            $type = $INPUT->str('msgtype', 'success');
641*1d05cddcSAtari911            echo "<div style=\"padding:10px; margin:10px 0; border-left:3px solid " . ($type === 'success' ? '#28a745' : '#dc3545') . "; background:" . ($type === 'success' ? '#d4edda' : '#f8d7da') . "; border-radius:3px;\">";
642*1d05cddcSAtari911            echo $msg;
643*1d05cddcSAtari911            echo "</div>";
644*1d05cddcSAtari911        }
645*1d05cddcSAtari911
646*1d05cddcSAtari911        echo '<h2 style="margin:10px 0; font-size:20px;">Manage Calendar Events</h2>';
647*1d05cddcSAtari911
648*1d05cddcSAtari911        // Events Manager Section - NEW!
649*1d05cddcSAtari911        echo '<div style="background:#f9f9f9; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:1200px;">';
650*1d05cddcSAtari911        echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">�� Events Manager</h3>';
651*1d05cddcSAtari911        echo '<p style="color:#666; font-size:11px; margin:0 0 10px;">Scan, export, and import all calendar events across all namespaces.</p>';
652*1d05cddcSAtari911
653*1d05cddcSAtari911        // Get event statistics
654*1d05cddcSAtari911        $stats = $this->getEventStatistics();
655*1d05cddcSAtari911
656*1d05cddcSAtari911        // Statistics display
657*1d05cddcSAtari911        echo '<div style="background:white; padding:10px; border-radius:3px; margin-bottom:10px; border:1px solid #ddd;">';
658*1d05cddcSAtari911        echo '<div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(150px, 1fr)); gap:10px; font-size:12px;">';
659*1d05cddcSAtari911
660*1d05cddcSAtari911        echo '<div style="background:#f3e5f5; padding:8px; border-radius:3px; text-align:center;">';
661*1d05cddcSAtari911        echo '<div style="font-size:24px; font-weight:bold; color:#7b1fa2;">' . $stats['total_events'] . '</div>';
662*1d05cddcSAtari911        echo '<div style="color:#666; font-size:10px;">Total Events</div>';
663*1d05cddcSAtari911        echo '</div>';
664*1d05cddcSAtari911
665*1d05cddcSAtari911        echo '<div style="background:#f3e5f5; padding:8px; border-radius:3px; text-align:center;">';
666*1d05cddcSAtari911        echo '<div style="font-size:24px; font-weight:bold; color:#7b1fa2;">' . $stats['total_namespaces'] . '</div>';
667*1d05cddcSAtari911        echo '<div style="color:#666; font-size:10px;">Namespaces</div>';
668*1d05cddcSAtari911        echo '</div>';
669*1d05cddcSAtari911
670*1d05cddcSAtari911        echo '<div style="background:#e8f5e9; padding:8px; border-radius:3px; text-align:center;">';
671*1d05cddcSAtari911        echo '<div style="font-size:24px; font-weight:bold; color:#388e3c;">' . $stats['total_files'] . '</div>';
672*1d05cddcSAtari911        echo '<div style="color:#666; font-size:10px;">JSON Files</div>';
673*1d05cddcSAtari911        echo '</div>';
674*1d05cddcSAtari911
675*1d05cddcSAtari911        echo '<div style="background:#fff3e0; padding:8px; border-radius:3px; text-align:center;">';
676*1d05cddcSAtari911        echo '<div style="font-size:24px; font-weight:bold; color:#f57c00;">' . $stats['total_recurring'] . '</div>';
677*1d05cddcSAtari911        echo '<div style="color:#666; font-size:10px;">Recurring</div>';
678*1d05cddcSAtari911        echo '</div>';
679*1d05cddcSAtari911
680*1d05cddcSAtari911        echo '</div>';
681*1d05cddcSAtari911
682*1d05cddcSAtari911        // Last scan time
683*1d05cddcSAtari911        if (!empty($stats['last_scan'])) {
684*1d05cddcSAtari911            echo '<div style="margin-top:8px; color:#666; font-size:10px;">Last scanned: ' . hsc($stats['last_scan']) . '</div>';
685*1d05cddcSAtari911        }
686*1d05cddcSAtari911
687*1d05cddcSAtari911        echo '</div>';
688*1d05cddcSAtari911
689*1d05cddcSAtari911        // Action buttons
690*1d05cddcSAtari911        echo '<div style="display:flex; gap:10px; flex-wrap:wrap; margin-top:10px;">';
691*1d05cddcSAtari911
692*1d05cddcSAtari911        // Rescan button
693*1d05cddcSAtari911        echo '<form method="post" action="?do=admin&page=calendar&tab=manage" style="display:inline;">';
694*1d05cddcSAtari911        echo '<input type="hidden" name="action" value="rescan_events">';
695*1d05cddcSAtari911        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;">';
696*1d05cddcSAtari911        echo '<span>��</span><span>Re-scan Events</span>';
697*1d05cddcSAtari911        echo '</button>';
698*1d05cddcSAtari911        echo '</form>';
699*1d05cddcSAtari911
700*1d05cddcSAtari911        // Export button
701*1d05cddcSAtari911        echo '<form method="post" action="?do=admin&page=calendar&tab=manage" style="display:inline;">';
702*1d05cddcSAtari911        echo '<input type="hidden" name="action" value="export_all_events">';
703*1d05cddcSAtari911        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;">';
704*1d05cddcSAtari911        echo '<span>��</span><span>Export All Events</span>';
705*1d05cddcSAtari911        echo '</button>';
706*1d05cddcSAtari911        echo '</form>';
707*1d05cddcSAtari911
708*1d05cddcSAtari911        // Import button (with file upload)
709*1d05cddcSAtari911        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?\')">';
710*1d05cddcSAtari911        echo '<input type="hidden" name="action" value="import_all_events">';
711*1d05cddcSAtari911        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;">';
712*1d05cddcSAtari911        echo '<span>��</span><span>Import Events</span>';
713*1d05cddcSAtari911        echo '<input type="file" name="import_file" accept=".json,.zip" required style="display:none;" onchange="this.form.submit()">';
714*1d05cddcSAtari911        echo '</label>';
715*1d05cddcSAtari911        echo '</form>';
716*1d05cddcSAtari911
717*1d05cddcSAtari911        echo '</div>';
718*1d05cddcSAtari911
719*1d05cddcSAtari911        // Breakdown by namespace
720*1d05cddcSAtari911        if (!empty($stats['by_namespace'])) {
721*1d05cddcSAtari911            echo '<details style="margin-top:12px;">';
722*1d05cddcSAtari911            echo '<summary style="cursor:pointer; padding:6px; background:#e9e9e9; border-radius:3px; font-size:11px; font-weight:bold;">View Breakdown by Namespace</summary>';
723*1d05cddcSAtari911            echo '<div style="margin-top:8px; max-height:200px; overflow-y:auto; border:1px solid #ddd; border-radius:3px;">';
724*1d05cddcSAtari911            echo '<table style="width:100%; border-collapse:collapse; font-size:11px;">';
725*1d05cddcSAtari911            echo '<thead style="position:sticky; top:0; background:#f5f5f5;">';
726*1d05cddcSAtari911            echo '<tr>';
727*1d05cddcSAtari911            echo '<th style="padding:4px 6px; text-align:left; border-bottom:2px solid #ddd;">Namespace</th>';
728*1d05cddcSAtari911            echo '<th style="padding:4px 6px; text-align:right; border-bottom:2px solid #ddd;">Events</th>';
729*1d05cddcSAtari911            echo '<th style="padding:4px 6px; text-align:right; border-bottom:2px solid #ddd;">Files</th>';
730*1d05cddcSAtari911            echo '</tr></thead><tbody>';
731*1d05cddcSAtari911
732*1d05cddcSAtari911            foreach ($stats['by_namespace'] as $ns => $nsStats) {
733*1d05cddcSAtari911                echo '<tr style="border-bottom:1px solid #eee;">';
734*1d05cddcSAtari911                echo '<td style="padding:4px 6px;"><code style="background:#f0f0f0; padding:1px 3px; border-radius:2px; font-size:10px;">' . hsc($ns ?: '(default)') . '</code></td>';
735*1d05cddcSAtari911                echo '<td style="padding:4px 6px; text-align:right;"><strong>' . $nsStats['events'] . '</strong></td>';
736*1d05cddcSAtari911                echo '<td style="padding:4px 6px; text-align:right;">' . $nsStats['files'] . '</td>';
737*1d05cddcSAtari911                echo '</tr>';
738*1d05cddcSAtari911            }
739*1d05cddcSAtari911
740*1d05cddcSAtari911            echo '</tbody></table>';
741*1d05cddcSAtari911            echo '</div>';
742*1d05cddcSAtari911            echo '</details>';
743*1d05cddcSAtari911        }
744*1d05cddcSAtari911
745*1d05cddcSAtari911        echo '</div>';
746*1d05cddcSAtari911
747*1d05cddcSAtari911        // Cleanup Events Section - Redesigned for compact, sleek look
748*1d05cddcSAtari911        echo '<div style="background:#f9f9f9; padding:12px; margin:10px 0; border-left:3px solid #ff9800; border-radius:3px; max-width:1200px;">';
749*1d05cddcSAtari911        echo '<h3 style="margin:0 0 6px 0; color:#f57c00; font-size:16px;">�� Cleanup Old Events</h3>';
750*1d05cddcSAtari911        echo '<p style="color:#666; font-size:11px; margin:0 0 12px;">Delete events based on criteria below. Automatic backup created before deletion.</p>';
751*1d05cddcSAtari911
752*1d05cddcSAtari911        echo '<form method="post" action="?do=admin&page=calendar&tab=manage" id="cleanupForm">';
753*1d05cddcSAtari911        echo '<input type="hidden" name="action" value="cleanup_events">';
754*1d05cddcSAtari911
755*1d05cddcSAtari911        // Compact options layout
756*1d05cddcSAtari911        echo '<div style="background:white; padding:10px; border:1px solid #e0e0e0; border-radius:3px; margin-bottom:10px;">';
757*1d05cddcSAtari911
758*1d05cddcSAtari911        // Radio buttons in a row
759*1d05cddcSAtari911        echo '<div style="display:flex; gap:20px; margin-bottom:10px; padding-bottom:8px; border-bottom:1px solid #f0f0f0;">';
760*1d05cddcSAtari911        echo '<label style="cursor:pointer; font-size:12px; font-weight:600; display:flex; align-items:center; gap:4px;">';
761*1d05cddcSAtari911        echo '<input type="radio" name="cleanup_type" value="age" checked onchange="updateCleanupOptions()">';
762*1d05cddcSAtari911        echo '<span>By Age</span>';
763*1d05cddcSAtari911        echo '</label>';
764*1d05cddcSAtari911        echo '<label style="cursor:pointer; font-size:12px; font-weight:600; display:flex; align-items:center; gap:4px;">';
765*1d05cddcSAtari911        echo '<input type="radio" name="cleanup_type" value="status" onchange="updateCleanupOptions()">';
766*1d05cddcSAtari911        echo '<span>By Status</span>';
767*1d05cddcSAtari911        echo '</label>';
768*1d05cddcSAtari911        echo '<label style="cursor:pointer; font-size:12px; font-weight:600; display:flex; align-items:center; gap:4px;">';
769*1d05cddcSAtari911        echo '<input type="radio" name="cleanup_type" value="range" onchange="updateCleanupOptions()">';
770*1d05cddcSAtari911        echo '<span>By Date Range</span>';
771*1d05cddcSAtari911        echo '</label>';
772*1d05cddcSAtari911        echo '</div>';
773*1d05cddcSAtari911
774*1d05cddcSAtari911        // Age options
775*1d05cddcSAtari911        echo '<div id="age-options" style="padding:6px 0;">';
776*1d05cddcSAtari911        echo '<span style="font-size:11px; color:#666; margin-right:8px;">Delete events older than:</span>';
777*1d05cddcSAtari911        echo '<select name="age_value" style="width:50px; padding:3px 4px; font-size:11px; border:1px solid #d0d0d0; border-radius:3px; margin-right:4px;">';
778*1d05cddcSAtari911        for ($i = 1; $i <= 24; $i++) {
779*1d05cddcSAtari911            $sel = $i === 6 ? ' selected' : '';
780*1d05cddcSAtari911            echo '<option value="' . $i . '"' . $sel . '>' . $i . '</option>';
781*1d05cddcSAtari911        }
782*1d05cddcSAtari911        echo '</select>';
783*1d05cddcSAtari911        echo '<select name="age_unit" style="width:80px; padding:3px 4px; font-size:11px; border:1px solid #d0d0d0; border-radius:3px;">';
784*1d05cddcSAtari911        echo '<option value="months" selected>months</option>';
785*1d05cddcSAtari911        echo '<option value="years">years</option>';
786*1d05cddcSAtari911        echo '</select>';
787*1d05cddcSAtari911        echo '</div>';
788*1d05cddcSAtari911
789*1d05cddcSAtari911        // Status options
790*1d05cddcSAtari911        echo '<div id="status-options" style="padding:6px 0; opacity:0.4;">';
791*1d05cddcSAtari911        echo '<span style="font-size:11px; color:#666; margin-right:8px;">Delete:</span>';
792*1d05cddcSAtari911        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>';
793*1d05cddcSAtari911        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>';
794*1d05cddcSAtari911        echo '</div>';
795*1d05cddcSAtari911
796*1d05cddcSAtari911        // Range options
797*1d05cddcSAtari911        echo '<div id="range-options" style="padding:6px 0; opacity:0.4;">';
798*1d05cddcSAtari911        echo '<span style="font-size:11px; color:#666; margin-right:8px;">From:</span>';
799*1d05cddcSAtari911        echo '<input type="date" name="range_start" style="padding:3px 6px; font-size:11px; border:1px solid #d0d0d0; border-radius:3px; margin-right:10px;">';
800*1d05cddcSAtari911        echo '<span style="font-size:11px; color:#666; margin-right:8px;">To:</span>';
801*1d05cddcSAtari911        echo '<input type="date" name="range_end" style="padding:3px 6px; font-size:11px; border:1px solid #d0d0d0; border-radius:3px;">';
802*1d05cddcSAtari911        echo '</div>';
803*1d05cddcSAtari911
804*1d05cddcSAtari911        echo '</div>';
805*1d05cddcSAtari911
806*1d05cddcSAtari911        // Namespace filter - compact
807*1d05cddcSAtari911        echo '<div style="background:white; padding:8px 10px; border:1px solid #e0e0e0; border-radius:3px; margin-bottom:10px; display:flex; align-items:center; gap:8px;">';
808*1d05cddcSAtari911        echo '<label style="font-size:11px; font-weight:600; white-space:nowrap; color:#555;">Namespace:</label>';
809*1d05cddcSAtari911        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;">';
810*1d05cddcSAtari911        echo '</div>';
811*1d05cddcSAtari911
812*1d05cddcSAtari911        // Action buttons - compact row
813*1d05cddcSAtari911        echo '<div style="display:flex; gap:8px; align-items:center;">';
814*1d05cddcSAtari911        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>';
815*1d05cddcSAtari911        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>';
816*1d05cddcSAtari911        echo '<span style="font-size:10px; color:#999;">⚠️ Backup created automatically</span>';
817*1d05cddcSAtari911        echo '</div>';
818*1d05cddcSAtari911
819*1d05cddcSAtari911        echo '</form>';
820*1d05cddcSAtari911
821*1d05cddcSAtari911        // Preview results area
822*1d05cddcSAtari911        echo '<div id="cleanup-preview" style="margin-top:10px; display:none;"></div>';
823*1d05cddcSAtari911
824*1d05cddcSAtari911        echo '<script>
825*1d05cddcSAtari911        function updateCleanupOptions() {
826*1d05cddcSAtari911            const type = document.querySelector(\'input[name="cleanup_type"]:checked\').value;
827*1d05cddcSAtari911
828*1d05cddcSAtari911            // Show selected, gray out others
829*1d05cddcSAtari911            document.getElementById(\'age-options\').style.opacity = type === \'age\' ? \'1\' : \'0.4\';
830*1d05cddcSAtari911            document.getElementById(\'status-options\').style.opacity = type === \'status\' ? \'1\' : \'0.4\';
831*1d05cddcSAtari911            document.getElementById(\'range-options\').style.opacity = type === \'range\' ? \'1\' : \'0.4\';
832*1d05cddcSAtari911
833*1d05cddcSAtari911            // Enable/disable inputs
834*1d05cddcSAtari911            document.querySelectorAll(\'#age-options select\').forEach(el => el.disabled = type !== \'age\');
835*1d05cddcSAtari911            document.querySelectorAll(\'#status-options input\').forEach(el => el.disabled = type !== \'status\');
836*1d05cddcSAtari911            document.querySelectorAll(\'#range-options input\').forEach(el => el.disabled = type !== \'range\');
837*1d05cddcSAtari911        }
838*1d05cddcSAtari911
839*1d05cddcSAtari911        function previewCleanup() {
840*1d05cddcSAtari911            const form = document.getElementById(\'cleanupForm\');
841*1d05cddcSAtari911            const formData = new FormData(form);
842*1d05cddcSAtari911            formData.set(\'action\', \'preview_cleanup\');
843*1d05cddcSAtari911
844*1d05cddcSAtari911            const preview = document.getElementById(\'cleanup-preview\');
845*1d05cddcSAtari911            preview.innerHTML = \'<div style="text-align:center; padding:20px; color:#666;">Loading preview...</div>\';
846*1d05cddcSAtari911            preview.style.display = \'block\';
847*1d05cddcSAtari911
848*1d05cddcSAtari911            fetch(\'?do=admin&page=calendar&tab=manage\', {
849*1d05cddcSAtari911                method: \'POST\',
850*1d05cddcSAtari911                body: new URLSearchParams(formData)
851*1d05cddcSAtari911            })
852*1d05cddcSAtari911            .then(r => r.json())
853*1d05cddcSAtari911            .then(data => {
854*1d05cddcSAtari911                if (data.count === 0) {
855*1d05cddcSAtari911                    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>\';
856*1d05cddcSAtari911
857*1d05cddcSAtari911                    // Show debug info if available
858*1d05cddcSAtari911                    if (data.debug) {
859*1d05cddcSAtari911                        html += \'<details style="margin-top:8px; font-size:11px; color:#666;">\';
860*1d05cddcSAtari911                        html += \'<summary style="cursor:pointer;">Debug Info</summary>\';
861*1d05cddcSAtari911                        html += \'<pre style="background:#f5f5f5; padding:6px; margin-top:4px; border-radius:3px; overflow-x:auto;">\' + JSON.stringify(data.debug, null, 2) + \'</pre>\';
862*1d05cddcSAtari911                        html += \'</details>\';
863*1d05cddcSAtari911                    }
864*1d05cddcSAtari911
865*1d05cddcSAtari911                    preview.innerHTML = html;
866*1d05cddcSAtari911                } else {
867*1d05cddcSAtari911                    let html = \'<div style="background:#f8d7da; border:1px solid #f5c6cb; padding:10px; border-radius:3px; font-size:12px; color:#721c24;">\';
868*1d05cddcSAtari911                    html += \'<strong>⚠️ Warning:</strong> The following \' + data.count + \' event(s) would be deleted:<br><br>\';
869*1d05cddcSAtari911                    html += \'<div style="max-height:150px; overflow-y:auto; margin-top:6px; background:white; padding:6px; border-radius:3px;">\';
870*1d05cddcSAtari911                    data.events.forEach(evt => {
871*1d05cddcSAtari911                        html += \'<div style="padding:3px; border-bottom:1px solid #eee; font-size:11px;">\';
872*1d05cddcSAtari911                        html += \'\' + evt.title + \' (\' + evt.date + \')\';
873*1d05cddcSAtari911                        if (evt.namespace) html += \' <span style="background:#e3f2fd; padding:1px 4px; border-radius:2px; font-size:9px;">\' + evt.namespace + \'</span>\';
874*1d05cddcSAtari911                        html += \'</div>\';
875*1d05cddcSAtari911                    });
876*1d05cddcSAtari911                    html += \'</div></div>\';
877*1d05cddcSAtari911                    preview.innerHTML = html;
878*1d05cddcSAtari911                }
879*1d05cddcSAtari911            })
880*1d05cddcSAtari911            .catch(err => {
881*1d05cddcSAtari911                preview.innerHTML = \'<div style="background:#f8d7da; border:1px solid #f5c6cb; padding:10px; border-radius:3px; font-size:12px; color:#721c24;">Error loading preview</div>\';
882*1d05cddcSAtari911            });
883*1d05cddcSAtari911        }
884*1d05cddcSAtari911
885*1d05cddcSAtari911        function confirmCleanup() {
886*1d05cddcSAtari911            return confirm(\'Are you sure you want to delete these events? A backup will be created first, but this action cannot be easily undone.\');
887*1d05cddcSAtari911        }
888*1d05cddcSAtari911
889*1d05cddcSAtari911        updateCleanupOptions();
890*1d05cddcSAtari911        </script>';
891*1d05cddcSAtari911
892*1d05cddcSAtari911        echo '</div>';
893*1d05cddcSAtari911
894*1d05cddcSAtari911        // Recurring Events Section
895*1d05cddcSAtari911        echo '<div style="background:#f9f9f9; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:1200px;">';
896*1d05cddcSAtari911        echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">�� Recurring Events</h3>';
897*1d05cddcSAtari911
898*1d05cddcSAtari911        $recurringEvents = $this->findRecurringEvents();
899*1d05cddcSAtari911
900*1d05cddcSAtari911        if (empty($recurringEvents)) {
901*1d05cddcSAtari911            echo '<p style="color:#666; font-size:13px; margin:5px 0;">No recurring events found.</p>';
902*1d05cddcSAtari911        } else {
903*1d05cddcSAtari911            // Search bar
904*1d05cddcSAtari911            echo '<div style="margin-bottom:8px;">';
905*1d05cddcSAtari911            echo '<input type="text" id="searchRecurring" onkeyup="filterRecurringEvents()" placeholder="�� Search recurring events..." style="width:100%; padding:6px 10px; border:1px solid #ddd; border-radius:3px; font-size:12px;">';
906*1d05cddcSAtari911            echo '</div>';
907*1d05cddcSAtari911
908*1d05cddcSAtari911            echo '<style>
909*1d05cddcSAtari911                .sort-arrow {
910*1d05cddcSAtari911                    color: #999;
911*1d05cddcSAtari911                    font-size: 10px;
912*1d05cddcSAtari911                    margin-left: 3px;
913*1d05cddcSAtari911                    display: inline-block;
914*1d05cddcSAtari911                }
915*1d05cddcSAtari911                #recurringTable th:hover {
916*1d05cddcSAtari911                    background: #ddd;
917*1d05cddcSAtari911                }
918*1d05cddcSAtari911                #recurringTable th:hover .sort-arrow {
919*1d05cddcSAtari911                    color: #00cc07;
920*1d05cddcSAtari911                }
921*1d05cddcSAtari911                .recurring-row-hidden {
922*1d05cddcSAtari911                    display: none;
923*1d05cddcSAtari911                }
924*1d05cddcSAtari911            </style>';
925*1d05cddcSAtari911            echo '<div style="max-height:250px; overflow-y:auto; border:1px solid #ddd; border-radius:3px;">';
926*1d05cddcSAtari911            echo '<table id="recurringTable" style="width:100%; border-collapse:collapse; font-size:11px;">';
927*1d05cddcSAtari911            echo '<thead style="position:sticky; top:0; background:#e9e9e9;">';
928*1d05cddcSAtari911            echo '<tr>';
929*1d05cddcSAtari911            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>';
930*1d05cddcSAtari911            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>';
931*1d05cddcSAtari911            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>';
932*1d05cddcSAtari911            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>';
933*1d05cddcSAtari911            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>';
934*1d05cddcSAtari911            echo '<th style="padding:4px 6px; text-align:left; border-bottom:2px solid #ddd;">Actions</th>';
935*1d05cddcSAtari911            echo '</tr></thead><tbody id="recurringTableBody">';
936*1d05cddcSAtari911
937*1d05cddcSAtari911            foreach ($recurringEvents as $series) {
938*1d05cddcSAtari911                echo '<tr style="border-bottom:1px solid #eee;">';
939*1d05cddcSAtari911                echo '<td style="padding:4px 6px;">' . hsc($series['title']) . '</td>';
940*1d05cddcSAtari911                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>';
941*1d05cddcSAtari911                echo '<td style="padding:4px 6px;">' . hsc($series['pattern']) . '</td>';
942*1d05cddcSAtari911                echo '<td style="padding:4px 6px;">' . hsc($series['firstDate']) . '</td>';
943*1d05cddcSAtari911                echo '<td style="padding:4px 6px;"><strong>' . $series['count'] . '</strong></td>';
944*1d05cddcSAtari911                echo '<td style="padding:4px 6px; white-space:nowrap;">';
945*1d05cddcSAtari911                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>';
946*1d05cddcSAtari911                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>';
947*1d05cddcSAtari911                echo '</td>';
948*1d05cddcSAtari911                echo '</tr>';
949*1d05cddcSAtari911            }
950*1d05cddcSAtari911
951*1d05cddcSAtari911            echo '</tbody></table>';
952*1d05cddcSAtari911            echo '</div>';
953*1d05cddcSAtari911            echo '<p style="color:#666; font-size:10px; margin:5px 0 0;">Total: ' . count($recurringEvents) . ' series</p>';
954*1d05cddcSAtari911        }
955*1d05cddcSAtari911        echo '</div>';
956*1d05cddcSAtari911
957*1d05cddcSAtari911        // Compact Tree-based Namespace Manager
958*1d05cddcSAtari911        echo '<div style="background:#f9f9f9; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:1200px;">';
959*1d05cddcSAtari911        echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">�� Namespace Explorer</h3>';
960*1d05cddcSAtari911        echo '<p style="color:#666; font-size:11px; margin:0 0 8px;">Select events and move between namespaces. Drag & drop also supported.</p>';
961*1d05cddcSAtari911
962*1d05cddcSAtari911        // Search bar
963*1d05cddcSAtari911        echo '<div style="margin-bottom:8px;">';
964*1d05cddcSAtari911        echo '<input type="text" id="searchEvents" onkeyup="filterEvents()" placeholder="�� Search events by title..." style="width:100%; padding:6px 10px; border:1px solid #ddd; border-radius:3px; font-size:12px;">';
965*1d05cddcSAtari911        echo '</div>';
966*1d05cddcSAtari911
967*1d05cddcSAtari911        $eventsByNamespace = $this->getEventsByNamespace();
968*1d05cddcSAtari911
969*1d05cddcSAtari911        // Control bar
970*1d05cddcSAtari911        echo '<form method="post" action="?do=admin&page=calendar&tab=manage" id="moveForm">';
971*1d05cddcSAtari911        echo '<input type="hidden" name="action" value="move_selected_events" id="formAction">';
972*1d05cddcSAtari911        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;">';
973*1d05cddcSAtari911        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>';
974*1d05cddcSAtari911        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>';
975*1d05cddcSAtari911        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>';
976*1d05cddcSAtari911        echo '<span style="margin-left:10px;">Move to:</span>';
977*1d05cddcSAtari911        echo '<input list="namespaceList" name="target_namespace" required style="padding:3px 6px; border:1px solid #ddd; border-radius:2px; font-size:11px; min-width:150px;" placeholder="Type or select...">';
978*1d05cddcSAtari911        echo '<datalist id="namespaceList">';
979*1d05cddcSAtari911        echo '<option value="">(default)</option>';
980*1d05cddcSAtari911        foreach (array_keys($eventsByNamespace) as $ns) {
981*1d05cddcSAtari911            if ($ns !== '') {
982*1d05cddcSAtari911                echo '<option value="' . hsc($ns) . '">' . hsc($ns) . '</option>';
983*1d05cddcSAtari911            }
984*1d05cddcSAtari911        }
985*1d05cddcSAtari911        echo '</datalist>';
986*1d05cddcSAtari911        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>';
987*1d05cddcSAtari911        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>';
988*1d05cddcSAtari911        echo '<span id="selectedCount" style="margin-left:auto; color:#00cc07; font-size:11px;">0 selected</span>';
989*1d05cddcSAtari911        echo '</div>';
990*1d05cddcSAtari911
991*1d05cddcSAtari911        echo '<div style="display:grid; grid-template-columns:1fr 1fr; gap:10px;">';
992*1d05cddcSAtari911
993*1d05cddcSAtari911        // Event list with checkboxes
994*1d05cddcSAtari911        echo '<div>';
995*1d05cddcSAtari911        echo '<div style="max-height:450px; overflow-y:auto; border:1px solid #ddd; border-radius:3px; background:white;">';
996*1d05cddcSAtari911
997*1d05cddcSAtari911        foreach ($eventsByNamespace as $namespace => $data) {
998*1d05cddcSAtari911            $nsId = 'ns_' . md5($namespace);
999*1d05cddcSAtari911            $eventCount = count($data['events']);
1000*1d05cddcSAtari911
1001*1d05cddcSAtari911            echo '<div style="border-bottom:1px solid #ddd;">';
1002*1d05cddcSAtari911
1003*1d05cddcSAtari911            // Namespace header - ultra compact
1004*1d05cddcSAtari911            echo '<div style="background:#f5f5f5; padding:3px 6px; display:flex; justify-content:space-between; align-items:center; font-size:11px;">';
1005*1d05cddcSAtari911            echo '<div style="display:flex; align-items:center; gap:4px;">';
1006*1d05cddcSAtari911            echo '<span onclick="toggleNamespace(\'' . $nsId . '\')" style="cursor:pointer; width:12px; display:inline-block; font-size:10px;"><span id="' . $nsId . '_arrow">▶</span></span>';
1007*1d05cddcSAtari911            echo '<input type="checkbox" onclick="toggleNamespaceSelect(\'' . $nsId . '\')" id="' . $nsId . '_check" style="margin:0; width:12px; height:12px;">';
1008*1d05cddcSAtari911            echo '<span onclick="toggleNamespace(\'' . $nsId . '\')" style="cursor:pointer; font-weight:600; font-size:11px;">�� ' . hsc($namespace ?: '(default)') . '</span>';
1009*1d05cddcSAtari911            echo '</div>';
1010*1d05cddcSAtari911            echo '<div style="display:flex; gap:3px; align-items:center;">';
1011*1d05cddcSAtari911            echo '<span style="background:#00cc07; color:white; padding:0px 4px; border-radius:6px; font-size:9px; line-height:14px;">' . $eventCount . '</span>';
1012*1d05cddcSAtari911            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>';
1013*1d05cddcSAtari911            echo '</div>';
1014*1d05cddcSAtari911            echo '</div>';
1015*1d05cddcSAtari911
1016*1d05cddcSAtari911            // Events - ultra compact
1017*1d05cddcSAtari911            echo '<div id="' . $nsId . '" style="display:none; max-height:150px; overflow-y:auto;">';
1018*1d05cddcSAtari911            foreach ($data['events'] as $event) {
1019*1d05cddcSAtari911                $eventId = $event['id'] . '|' . $namespace . '|' . $event['date'] . '|' . $event['month'];
1020*1d05cddcSAtari911                $checkId = 'evt_' . md5($eventId);
1021*1d05cddcSAtari911
1022*1d05cddcSAtari911                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\'">';
1023*1d05cddcSAtari911                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;">';
1024*1d05cddcSAtari911                echo '<div style="flex:1; min-width:0;">';
1025*1d05cddcSAtari911                echo '<div style="font-weight:500; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; font-size:10px;">' . hsc($event['title']) . '</div>';
1026*1d05cddcSAtari911                echo '<div style="color:#999; font-size:9px;">' . hsc($event['date']) . ($event['startTime'] ? ' • ' . hsc($event['startTime']) : '') . '</div>';
1027*1d05cddcSAtari911                echo '</div>';
1028*1d05cddcSAtari911                echo '</div>';
1029*1d05cddcSAtari911            }
1030*1d05cddcSAtari911            echo '</div>';
1031*1d05cddcSAtari911            echo '</div>';
1032*1d05cddcSAtari911        }
1033*1d05cddcSAtari911
1034*1d05cddcSAtari911        echo '</div>';
1035*1d05cddcSAtari911        echo '</div>';
1036*1d05cddcSAtari911
1037*1d05cddcSAtari911        // Drop zones - ultra compact
1038*1d05cddcSAtari911        echo '<div>';
1039*1d05cddcSAtari911        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>';
1040*1d05cddcSAtari911        echo '<div style="border:1px solid #ddd; border-top:none; border-radius:0 0 3px 3px; max-height:450px; overflow-y:auto; background:white;">';
1041*1d05cddcSAtari911
1042*1d05cddcSAtari911        foreach (array_keys($eventsByNamespace) as $namespace) {
1043*1d05cddcSAtari911            echo '<div ondrop="drop(event, \'' . hsc($namespace) . '\')" ondragover="allowDrop(event)" style="padding:5px 6px; border-bottom:1px solid #eee; background:white; min-height:28px;" onmouseover="this.style.background=\'#f0fff0\'" onmouseout="this.style.background=\'white\'">';
1044*1d05cddcSAtari911            echo '<div style="font-size:11px; font-weight:600; color:#00cc07;">�� ' . hsc($namespace ?: '(default)') . '</div>';
1045*1d05cddcSAtari911            echo '<div style="color:#999; font-size:9px; margin-top:1px;">Drop here</div>';
1046*1d05cddcSAtari911            echo '</div>';
1047*1d05cddcSAtari911        }
1048*1d05cddcSAtari911
1049*1d05cddcSAtari911        echo '</div>';
1050*1d05cddcSAtari911        echo '</div>';
1051*1d05cddcSAtari911
1052*1d05cddcSAtari911        echo '</div>'; // end grid
1053*1d05cddcSAtari911        echo '</form>';
1054*1d05cddcSAtari911
1055*1d05cddcSAtari911        echo '</div>';
1056*1d05cddcSAtari911
1057*1d05cddcSAtari911        // JavaScript
1058*1d05cddcSAtari911        echo '<script>
1059*1d05cddcSAtari911        // Table sorting functionality - defined early so onclick handlers work
1060*1d05cddcSAtari911        let sortDirection = {}; // Track sort direction for each column
1061*1d05cddcSAtari911
1062*1d05cddcSAtari911        function sortRecurringTable(columnIndex) {
1063*1d05cddcSAtari911            console.log("sortRecurringTable called with column:", columnIndex);
1064*1d05cddcSAtari911            const table = document.getElementById("recurringTable");
1065*1d05cddcSAtari911            const tbody = document.getElementById("recurringTableBody");
1066*1d05cddcSAtari911            console.log("Table:", table, "Tbody:", tbody);
1067*1d05cddcSAtari911
1068*1d05cddcSAtari911            if (!table || !tbody) {
1069*1d05cddcSAtari911                console.error("Table or tbody not found!");
1070*1d05cddcSAtari911                return;
1071*1d05cddcSAtari911            }
1072*1d05cddcSAtari911
1073*1d05cddcSAtari911            const rows = Array.from(tbody.querySelectorAll("tr"));
1074*1d05cddcSAtari911            console.log("Rows found:", rows.length);
1075*1d05cddcSAtari911
1076*1d05cddcSAtari911            if (rows.length === 0) {
1077*1d05cddcSAtari911                console.warn("No rows to sort");
1078*1d05cddcSAtari911                return;
1079*1d05cddcSAtari911            }
1080*1d05cddcSAtari911
1081*1d05cddcSAtari911            // Toggle sort direction for this column
1082*1d05cddcSAtari911            if (!sortDirection[columnIndex]) {
1083*1d05cddcSAtari911                sortDirection[columnIndex] = "asc";
1084*1d05cddcSAtari911            } else {
1085*1d05cddcSAtari911                sortDirection[columnIndex] = sortDirection[columnIndex] === "asc" ? "desc" : "asc";
1086*1d05cddcSAtari911            }
1087*1d05cddcSAtari911
1088*1d05cddcSAtari911            const direction = sortDirection[columnIndex];
1089*1d05cddcSAtari911            console.log("Sorting column", columnIndex, "in", direction, "order");
1090*1d05cddcSAtari911            const isNumeric = columnIndex === 4; // Count column
1091*1d05cddcSAtari911
1092*1d05cddcSAtari911            // Sort rows
1093*1d05cddcSAtari911            rows.sort((a, b) => {
1094*1d05cddcSAtari911                let aValue = a.cells[columnIndex].textContent.trim();
1095*1d05cddcSAtari911                let bValue = b.cells[columnIndex].textContent.trim();
1096*1d05cddcSAtari911
1097*1d05cddcSAtari911                // Extract text from code elements for namespace column
1098*1d05cddcSAtari911                if (columnIndex === 1) {
1099*1d05cddcSAtari911                    const aCode = a.cells[columnIndex].querySelector("code");
1100*1d05cddcSAtari911                    const bCode = b.cells[columnIndex].querySelector("code");
1101*1d05cddcSAtari911                    aValue = aCode ? aCode.textContent.trim() : aValue;
1102*1d05cddcSAtari911                    bValue = bCode ? bCode.textContent.trim() : bValue;
1103*1d05cddcSAtari911                }
1104*1d05cddcSAtari911
1105*1d05cddcSAtari911                // Extract number from strong elements for count column
1106*1d05cddcSAtari911                if (isNumeric) {
1107*1d05cddcSAtari911                    const aStrong = a.cells[columnIndex].querySelector("strong");
1108*1d05cddcSAtari911                    const bStrong = b.cells[columnIndex].querySelector("strong");
1109*1d05cddcSAtari911                    aValue = aStrong ? parseInt(aStrong.textContent.trim()) : 0;
1110*1d05cddcSAtari911                    bValue = bStrong ? parseInt(bStrong.textContent.trim()) : 0;
1111*1d05cddcSAtari911
1112*1d05cddcSAtari911                    return direction === "asc" ? aValue - bValue : bValue - aValue;
1113*1d05cddcSAtari911                }
1114*1d05cddcSAtari911
1115*1d05cddcSAtari911                // String comparison
1116*1d05cddcSAtari911                if (direction === "asc") {
1117*1d05cddcSAtari911                    return aValue.localeCompare(bValue);
1118*1d05cddcSAtari911                } else {
1119*1d05cddcSAtari911                    return bValue.localeCompare(aValue);
1120*1d05cddcSAtari911                }
1121*1d05cddcSAtari911            });
1122*1d05cddcSAtari911
1123*1d05cddcSAtari911            // Update arrows
1124*1d05cddcSAtari911            const headers = table.querySelectorAll("th");
1125*1d05cddcSAtari911            headers.forEach((header, index) => {
1126*1d05cddcSAtari911                const arrow = header.querySelector(".sort-arrow");
1127*1d05cddcSAtari911                if (arrow) {
1128*1d05cddcSAtari911                    if (index === columnIndex) {
1129*1d05cddcSAtari911                        arrow.textContent = direction === "asc" ? "↑" : "↓";
1130*1d05cddcSAtari911                        arrow.style.color = "#00cc07";
1131*1d05cddcSAtari911                    } else {
1132*1d05cddcSAtari911                        arrow.textContent = "⇅";
1133*1d05cddcSAtari911                        arrow.style.color = "#999";
1134*1d05cddcSAtari911                    }
1135*1d05cddcSAtari911                }
1136*1d05cddcSAtari911            });
1137*1d05cddcSAtari911
1138*1d05cddcSAtari911            // Rebuild tbody
1139*1d05cddcSAtari911            rows.forEach(row => tbody.appendChild(row));
1140*1d05cddcSAtari911        }
1141*1d05cddcSAtari911
1142*1d05cddcSAtari911        function filterRecurringEvents() {
1143*1d05cddcSAtari911            const searchInput = document.getElementById("searchRecurring");
1144*1d05cddcSAtari911            const filter = normalizeText(searchInput.value);
1145*1d05cddcSAtari911            const tbody = document.getElementById("recurringTableBody");
1146*1d05cddcSAtari911            const rows = tbody.getElementsByTagName("tr");
1147*1d05cddcSAtari911
1148*1d05cddcSAtari911            for (let i = 0; i < rows.length; i++) {
1149*1d05cddcSAtari911                const row = rows[i];
1150*1d05cddcSAtari911                const titleCell = row.getElementsByTagName("td")[0];
1151*1d05cddcSAtari911
1152*1d05cddcSAtari911                if (titleCell) {
1153*1d05cddcSAtari911                    const titleText = normalizeText(titleCell.textContent || titleCell.innerText);
1154*1d05cddcSAtari911
1155*1d05cddcSAtari911                    if (titleText.indexOf(filter) > -1) {
1156*1d05cddcSAtari911                        row.classList.remove("recurring-row-hidden");
1157*1d05cddcSAtari911                    } else {
1158*1d05cddcSAtari911                        row.classList.add("recurring-row-hidden");
1159*1d05cddcSAtari911                    }
1160*1d05cddcSAtari911                }
1161*1d05cddcSAtari911            }
1162*1d05cddcSAtari911        }
1163*1d05cddcSAtari911
1164*1d05cddcSAtari911        function normalizeText(text) {
1165*1d05cddcSAtari911            // Convert to lowercase
1166*1d05cddcSAtari911            text = text.toLowerCase();
1167*1d05cddcSAtari911
1168*1d05cddcSAtari911            // Remove apostrophes and quotes
1169*1d05cddcSAtari911            text = text.replace(/[\'\"]/g, "");
1170*1d05cddcSAtari911
1171*1d05cddcSAtari911            // Replace accented characters with regular ones
1172*1d05cddcSAtari911            text = text.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
1173*1d05cddcSAtari911
1174*1d05cddcSAtari911            // Remove special characters except spaces and alphanumeric
1175*1d05cddcSAtari911            text = text.replace(/[^a-z0-9\s]/g, "");
1176*1d05cddcSAtari911
1177*1d05cddcSAtari911            // Collapse multiple spaces
1178*1d05cddcSAtari911            text = text.replace(/\s+/g, " ");
1179*1d05cddcSAtari911
1180*1d05cddcSAtari911            return text.trim();
1181*1d05cddcSAtari911        }
1182*1d05cddcSAtari911
1183*1d05cddcSAtari911        function filterEvents() {
1184*1d05cddcSAtari911            const searchText = normalizeText(document.getElementById("searchEvents").value);
1185*1d05cddcSAtari911            const eventRows = document.querySelectorAll(".event-row");
1186*1d05cddcSAtari911            let visibleCount = 0;
1187*1d05cddcSAtari911
1188*1d05cddcSAtari911            eventRows.forEach(row => {
1189*1d05cddcSAtari911                const titleElement = row.querySelector("div div");
1190*1d05cddcSAtari911                const originalTitle = titleElement.getAttribute("data-original-title") || titleElement.textContent;
1191*1d05cddcSAtari911
1192*1d05cddcSAtari911                // Store original title if not already stored
1193*1d05cddcSAtari911                if (!titleElement.getAttribute("data-original-title")) {
1194*1d05cddcSAtari911                    titleElement.setAttribute("data-original-title", originalTitle);
1195*1d05cddcSAtari911                }
1196*1d05cddcSAtari911
1197*1d05cddcSAtari911                const normalizedTitle = normalizeText(originalTitle);
1198*1d05cddcSAtari911
1199*1d05cddcSAtari911                if (normalizedTitle.includes(searchText) || searchText === "") {
1200*1d05cddcSAtari911                    row.style.display = "flex";
1201*1d05cddcSAtari911                    visibleCount++;
1202*1d05cddcSAtari911                } else {
1203*1d05cddcSAtari911                    row.style.display = "none";
1204*1d05cddcSAtari911                }
1205*1d05cddcSAtari911            });
1206*1d05cddcSAtari911
1207*1d05cddcSAtari911            // Update namespace visibility and counts
1208*1d05cddcSAtari911            document.querySelectorAll("[id^=ns_]").forEach(nsDiv => {
1209*1d05cddcSAtari911                if (nsDiv.id.endsWith("_arrow") || nsDiv.id.endsWith("_check")) return;
1210*1d05cddcSAtari911
1211*1d05cddcSAtari911                const visibleEvents = nsDiv.querySelectorAll(".event-row[style*=\\"display: flex\\"], .event-row:not([style*=\\"display: none\\"])").length;
1212*1d05cddcSAtari911                const nsId = nsDiv.id;
1213*1d05cddcSAtari911                const arrow = document.getElementById(nsId + "_arrow");
1214*1d05cddcSAtari911
1215*1d05cddcSAtari911                // Auto-expand namespaces with matches when searching
1216*1d05cddcSAtari911                if (searchText && visibleEvents > 0) {
1217*1d05cddcSAtari911                    nsDiv.style.display = "block";
1218*1d05cddcSAtari911                    if (arrow) arrow.textContent = "▼";
1219*1d05cddcSAtari911                }
1220*1d05cddcSAtari911            });
1221*1d05cddcSAtari911        }
1222*1d05cddcSAtari911
1223*1d05cddcSAtari911        function toggleNamespace(id) {
1224*1d05cddcSAtari911            const elem = document.getElementById(id);
1225*1d05cddcSAtari911            const arrow = document.getElementById(id + "_arrow");
1226*1d05cddcSAtari911            if (elem.style.display === "none") {
1227*1d05cddcSAtari911                elem.style.display = "block";
1228*1d05cddcSAtari911                arrow.textContent = "▼";
1229*1d05cddcSAtari911            } else {
1230*1d05cddcSAtari911                elem.style.display = "none";
1231*1d05cddcSAtari911                arrow.textContent = "▶";
1232*1d05cddcSAtari911            }
1233*1d05cddcSAtari911        }
1234*1d05cddcSAtari911
1235*1d05cddcSAtari911        function toggleNamespaceSelect(nsId) {
1236*1d05cddcSAtari911            const checkbox = document.getElementById(nsId + "_check");
1237*1d05cddcSAtari911            const events = document.querySelectorAll("." + nsId + "_events");
1238*1d05cddcSAtari911
1239*1d05cddcSAtari911            // Only select visible events (not hidden by search)
1240*1d05cddcSAtari911            events.forEach(cb => {
1241*1d05cddcSAtari911                const eventRow = cb.closest(".event-row");
1242*1d05cddcSAtari911                if (eventRow && eventRow.style.display !== "none") {
1243*1d05cddcSAtari911                    cb.checked = checkbox.checked;
1244*1d05cddcSAtari911                }
1245*1d05cddcSAtari911            });
1246*1d05cddcSAtari911            updateCount();
1247*1d05cddcSAtari911        }
1248*1d05cddcSAtari911
1249*1d05cddcSAtari911        function selectAll() {
1250*1d05cddcSAtari911            // Only select visible events
1251*1d05cddcSAtari911            document.querySelectorAll(".event-checkbox").forEach(cb => {
1252*1d05cddcSAtari911                const eventRow = cb.closest(".event-row");
1253*1d05cddcSAtari911                if (eventRow && eventRow.style.display !== "none") {
1254*1d05cddcSAtari911                    cb.checked = true;
1255*1d05cddcSAtari911                }
1256*1d05cddcSAtari911            });
1257*1d05cddcSAtari911            // Update namespace checkboxes to indeterminate if partially selected
1258*1d05cddcSAtari911            document.querySelectorAll("input[id$=_check]").forEach(nsCheckbox => {
1259*1d05cddcSAtari911                const nsId = nsCheckbox.id.replace("_check", "");
1260*1d05cddcSAtari911                const events = document.querySelectorAll("." + nsId + "_events");
1261*1d05cddcSAtari911                const visibleEvents = Array.from(events).filter(cb => {
1262*1d05cddcSAtari911                    const row = cb.closest(".event-row");
1263*1d05cddcSAtari911                    return row && row.style.display !== "none";
1264*1d05cddcSAtari911                });
1265*1d05cddcSAtari911                const checkedVisible = visibleEvents.filter(cb => cb.checked);
1266*1d05cddcSAtari911
1267*1d05cddcSAtari911                if (checkedVisible.length === visibleEvents.length && visibleEvents.length > 0) {
1268*1d05cddcSAtari911                    nsCheckbox.checked = true;
1269*1d05cddcSAtari911                } else if (checkedVisible.length > 0) {
1270*1d05cddcSAtari911                    nsCheckbox.indeterminate = true;
1271*1d05cddcSAtari911                } else {
1272*1d05cddcSAtari911                    nsCheckbox.checked = false;
1273*1d05cddcSAtari911                }
1274*1d05cddcSAtari911            });
1275*1d05cddcSAtari911            updateCount();
1276*1d05cddcSAtari911        }
1277*1d05cddcSAtari911
1278*1d05cddcSAtari911        function deselectAll() {
1279*1d05cddcSAtari911            document.querySelectorAll(".event-checkbox").forEach(cb => cb.checked = false);
1280*1d05cddcSAtari911            document.querySelectorAll("input[id$=_check]").forEach(cb => {
1281*1d05cddcSAtari911                cb.checked = false;
1282*1d05cddcSAtari911                cb.indeterminate = false;
1283*1d05cddcSAtari911            });
1284*1d05cddcSAtari911            updateCount();
1285*1d05cddcSAtari911        }
1286*1d05cddcSAtari911
1287*1d05cddcSAtari911        function deleteSelected() {
1288*1d05cddcSAtari911            const checkedBoxes = document.querySelectorAll(".event-checkbox:checked");
1289*1d05cddcSAtari911            if (checkedBoxes.length === 0) {
1290*1d05cddcSAtari911                alert("No events selected");
1291*1d05cddcSAtari911                return;
1292*1d05cddcSAtari911            }
1293*1d05cddcSAtari911
1294*1d05cddcSAtari911            const count = checkedBoxes.length;
1295*1d05cddcSAtari911            if (!confirm(`Delete ${count} selected event(s)?\\n\\nThis cannot be undone!`)) {
1296*1d05cddcSAtari911                return;
1297*1d05cddcSAtari911            }
1298*1d05cddcSAtari911
1299*1d05cddcSAtari911            const form = document.createElement("form");
1300*1d05cddcSAtari911            form.method = "POST";
1301*1d05cddcSAtari911            form.action = "?do=admin&page=calendar&tab=manage";
1302*1d05cddcSAtari911
1303*1d05cddcSAtari911            const actionInput = document.createElement("input");
1304*1d05cddcSAtari911            actionInput.type = "hidden";
1305*1d05cddcSAtari911            actionInput.name = "action";
1306*1d05cddcSAtari911            actionInput.value = "delete_selected_events";
1307*1d05cddcSAtari911            form.appendChild(actionInput);
1308*1d05cddcSAtari911
1309*1d05cddcSAtari911            checkedBoxes.forEach(cb => {
1310*1d05cddcSAtari911                const eventInput = document.createElement("input");
1311*1d05cddcSAtari911                eventInput.type = "hidden";
1312*1d05cddcSAtari911                eventInput.name = "events[]";
1313*1d05cddcSAtari911                eventInput.value = cb.value;
1314*1d05cddcSAtari911                form.appendChild(eventInput);
1315*1d05cddcSAtari911            });
1316*1d05cddcSAtari911
1317*1d05cddcSAtari911            document.body.appendChild(form);
1318*1d05cddcSAtari911            form.submit();
1319*1d05cddcSAtari911        }
1320*1d05cddcSAtari911
1321*1d05cddcSAtari911        function createNewNamespace() {
1322*1d05cddcSAtari911            const namespaceName = prompt("Enter new namespace name:\\n\\nExamples:\\n- work\\n- personal\\n- projects:alpha\\n- aspen:travel:2025");
1323*1d05cddcSAtari911
1324*1d05cddcSAtari911            if (!namespaceName) {
1325*1d05cddcSAtari911                return; // Cancelled
1326*1d05cddcSAtari911            }
1327*1d05cddcSAtari911
1328*1d05cddcSAtari911            // Validate namespace name
1329*1d05cddcSAtari911            if (!/^[a-zA-Z0-9_:-]+$/.test(namespaceName)) {
1330*1d05cddcSAtari911                alert("Invalid namespace name.\\n\\nUse only letters, numbers, underscore, hyphen, and colon.\\nExample: work:projects:alpha");
1331*1d05cddcSAtari911                return;
1332*1d05cddcSAtari911            }
1333*1d05cddcSAtari911
1334*1d05cddcSAtari911            // Submit form to create namespace
1335*1d05cddcSAtari911            const form = document.createElement("form");
1336*1d05cddcSAtari911            form.method = "POST";
1337*1d05cddcSAtari911            form.action = "?do=admin&page=calendar&tab=manage";
1338*1d05cddcSAtari911
1339*1d05cddcSAtari911            const actionInput = document.createElement("input");
1340*1d05cddcSAtari911            actionInput.type = "hidden";
1341*1d05cddcSAtari911            actionInput.name = "action";
1342*1d05cddcSAtari911            actionInput.value = "create_namespace";
1343*1d05cddcSAtari911            form.appendChild(actionInput);
1344*1d05cddcSAtari911
1345*1d05cddcSAtari911            const namespaceInput = document.createElement("input");
1346*1d05cddcSAtari911            namespaceInput.type = "hidden";
1347*1d05cddcSAtari911            namespaceInput.name = "namespace_name";
1348*1d05cddcSAtari911            namespaceInput.value = namespaceName;
1349*1d05cddcSAtari911            form.appendChild(namespaceInput);
1350*1d05cddcSAtari911
1351*1d05cddcSAtari911            document.body.appendChild(form);
1352*1d05cddcSAtari911            form.submit();
1353*1d05cddcSAtari911        }
1354*1d05cddcSAtari911
1355*1d05cddcSAtari911        function updateCount() {
1356*1d05cddcSAtari911            const count = document.querySelectorAll(".event-checkbox:checked").length;
1357*1d05cddcSAtari911            document.getElementById("selectedCount").textContent = count + " selected";
1358*1d05cddcSAtari911        }
1359*1d05cddcSAtari911
1360*1d05cddcSAtari911        function deleteNamespace(namespace) {
1361*1d05cddcSAtari911            const displayName = namespace || "(default)";
1362*1d05cddcSAtari911            if (!confirm("Delete ENTIRE namespace: " + displayName + "?\\n\\nThis will delete ALL events in this namespace!\\n\\nThis cannot be undone!")) {
1363*1d05cddcSAtari911                return;
1364*1d05cddcSAtari911            }
1365*1d05cddcSAtari911            const form = document.createElement("form");
1366*1d05cddcSAtari911            form.method = "POST";
1367*1d05cddcSAtari911            form.action = "?do=admin&page=calendar&tab=manage";
1368*1d05cddcSAtari911            const actionInput = document.createElement("input");
1369*1d05cddcSAtari911            actionInput.type = "hidden";
1370*1d05cddcSAtari911            actionInput.name = "action";
1371*1d05cddcSAtari911            actionInput.value = "delete_namespace";
1372*1d05cddcSAtari911            form.appendChild(actionInput);
1373*1d05cddcSAtari911            const nsInput = document.createElement("input");
1374*1d05cddcSAtari911            nsInput.type = "hidden";
1375*1d05cddcSAtari911            nsInput.name = "namespace";
1376*1d05cddcSAtari911            nsInput.value = namespace;
1377*1d05cddcSAtari911            form.appendChild(nsInput);
1378*1d05cddcSAtari911            document.body.appendChild(form);
1379*1d05cddcSAtari911            form.submit();
1380*1d05cddcSAtari911        }
1381*1d05cddcSAtari911
1382*1d05cddcSAtari911        let draggedEvent = null;
1383*1d05cddcSAtari911
1384*1d05cddcSAtari911        function dragStart(event, eventId) {
1385*1d05cddcSAtari911            const checkbox = event.target.closest(".event-row").querySelector(".event-checkbox");
1386*1d05cddcSAtari911
1387*1d05cddcSAtari911            // If this event is checked, drag all checked events
1388*1d05cddcSAtari911            const checkedBoxes = document.querySelectorAll(".event-checkbox:checked");
1389*1d05cddcSAtari911            if (checkbox && checkbox.checked && checkedBoxes.length > 1) {
1390*1d05cddcSAtari911                // Dragging multiple selected events
1391*1d05cddcSAtari911                draggedEvent = "MULTIPLE";
1392*1d05cddcSAtari911                event.dataTransfer.setData("text/plain", "MULTIPLE");
1393*1d05cddcSAtari911            } else {
1394*1d05cddcSAtari911                // Dragging single event
1395*1d05cddcSAtari911                draggedEvent = eventId;
1396*1d05cddcSAtari911                event.dataTransfer.setData("text/plain", eventId);
1397*1d05cddcSAtari911            }
1398*1d05cddcSAtari911            event.dataTransfer.effectAllowed = "move";
1399*1d05cddcSAtari911            event.target.style.opacity = "0.5";
1400*1d05cddcSAtari911        }
1401*1d05cddcSAtari911
1402*1d05cddcSAtari911        function allowDrop(event) {
1403*1d05cddcSAtari911            event.preventDefault();
1404*1d05cddcSAtari911            event.dataTransfer.dropEffect = "move";
1405*1d05cddcSAtari911        }
1406*1d05cddcSAtari911
1407*1d05cddcSAtari911        function drop(event, targetNamespace) {
1408*1d05cddcSAtari911            event.preventDefault();
1409*1d05cddcSAtari911
1410*1d05cddcSAtari911            if (draggedEvent === "MULTIPLE") {
1411*1d05cddcSAtari911                // Move all selected events
1412*1d05cddcSAtari911                const checkedBoxes = document.querySelectorAll(".event-checkbox:checked");
1413*1d05cddcSAtari911                if (checkedBoxes.length === 0) return;
1414*1d05cddcSAtari911
1415*1d05cddcSAtari911                const form = document.createElement("form");
1416*1d05cddcSAtari911                form.method = "POST";
1417*1d05cddcSAtari911                form.action = "?do=admin&page=calendar&tab=manage";
1418*1d05cddcSAtari911
1419*1d05cddcSAtari911                const actionInput = document.createElement("input");
1420*1d05cddcSAtari911                actionInput.type = "hidden";
1421*1d05cddcSAtari911                actionInput.name = "action";
1422*1d05cddcSAtari911                actionInput.value = "move_selected_events";
1423*1d05cddcSAtari911                form.appendChild(actionInput);
1424*1d05cddcSAtari911
1425*1d05cddcSAtari911                checkedBoxes.forEach(cb => {
1426*1d05cddcSAtari911                    const eventInput = document.createElement("input");
1427*1d05cddcSAtari911                    eventInput.type = "hidden";
1428*1d05cddcSAtari911                    eventInput.name = "events[]";
1429*1d05cddcSAtari911                    eventInput.value = cb.value;
1430*1d05cddcSAtari911                    form.appendChild(eventInput);
1431*1d05cddcSAtari911                });
1432*1d05cddcSAtari911
1433*1d05cddcSAtari911                const targetInput = document.createElement("input");
1434*1d05cddcSAtari911                targetInput.type = "hidden";
1435*1d05cddcSAtari911                targetInput.name = "target_namespace";
1436*1d05cddcSAtari911                targetInput.value = targetNamespace;
1437*1d05cddcSAtari911                form.appendChild(targetInput);
1438*1d05cddcSAtari911
1439*1d05cddcSAtari911                document.body.appendChild(form);
1440*1d05cddcSAtari911                form.submit();
1441*1d05cddcSAtari911            } else {
1442*1d05cddcSAtari911                // Move single event
1443*1d05cddcSAtari911                if (!draggedEvent) return;
1444*1d05cddcSAtari911                const parts = draggedEvent.split("|");
1445*1d05cddcSAtari911                const sourceNamespace = parts[1];
1446*1d05cddcSAtari911                if (sourceNamespace === targetNamespace) return;
1447*1d05cddcSAtari911
1448*1d05cddcSAtari911                const form = document.createElement("form");
1449*1d05cddcSAtari911                form.method = "POST";
1450*1d05cddcSAtari911                form.action = "?do=admin&page=calendar&tab=manage";
1451*1d05cddcSAtari911                const actionInput = document.createElement("input");
1452*1d05cddcSAtari911                actionInput.type = "hidden";
1453*1d05cddcSAtari911                actionInput.name = "action";
1454*1d05cddcSAtari911                actionInput.value = "move_single_event";
1455*1d05cddcSAtari911                form.appendChild(actionInput);
1456*1d05cddcSAtari911                const eventInput = document.createElement("input");
1457*1d05cddcSAtari911                eventInput.type = "hidden";
1458*1d05cddcSAtari911                eventInput.name = "event";
1459*1d05cddcSAtari911                eventInput.value = draggedEvent;
1460*1d05cddcSAtari911                form.appendChild(eventInput);
1461*1d05cddcSAtari911                const targetInput = document.createElement("input");
1462*1d05cddcSAtari911                targetInput.type = "hidden";
1463*1d05cddcSAtari911                targetInput.name = "target_namespace";
1464*1d05cddcSAtari911                targetInput.value = targetNamespace;
1465*1d05cddcSAtari911                form.appendChild(targetInput);
1466*1d05cddcSAtari911                document.body.appendChild(form);
1467*1d05cddcSAtari911                form.submit();
1468*1d05cddcSAtari911            }
1469*1d05cddcSAtari911        }
1470*1d05cddcSAtari911
1471*1d05cddcSAtari911        function editRecurringSeries(title, namespace) {
1472*1d05cddcSAtari911            // Get available namespaces
1473*1d05cddcSAtari911            const namespaces = Array.from(document.querySelectorAll("[id^=ns_]"))
1474*1d05cddcSAtari911                .map(el => {
1475*1d05cddcSAtari911                    const match = el.id.match(/^ns_[a-f0-9]+$/);
1476*1d05cddcSAtari911                    if (!match) return null;
1477*1d05cddcSAtari911                    const nsSpan = el.querySelector("span:nth-child(3)");
1478*1d05cddcSAtari911                    if (!nsSpan) return null;
1479*1d05cddcSAtari911                    return nsSpan.textContent.replace("�� ", "").replace("(default)", "").trim();
1480*1d05cddcSAtari911                })
1481*1d05cddcSAtari911                .filter((ns, idx, arr) => ns && arr.indexOf(ns) === idx);
1482*1d05cddcSAtari911
1483*1d05cddcSAtari911            let nsOptions = `<option value="">Keep current (${namespace || "(default)"})</option>`;
1484*1d05cddcSAtari911            nsOptions += `<option value="">(default)</option>`;
1485*1d05cddcSAtari911            for (const ns of namespaces) {
1486*1d05cddcSAtari911                if (ns && ns !== "(default)" && ns !== namespace) {
1487*1d05cddcSAtari911                    nsOptions += `<option value="${ns}">${ns}</option>`;
1488*1d05cddcSAtari911                }
1489*1d05cddcSAtari911            }
1490*1d05cddcSAtari911
1491*1d05cddcSAtari911            // Show edit dialog for recurring events
1492*1d05cddcSAtari911            const dialog = document.createElement("div");
1493*1d05cddcSAtari911            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;";
1494*1d05cddcSAtari911
1495*1d05cddcSAtari911            // Close on clicking background
1496*1d05cddcSAtari911            dialog.addEventListener("click", function(e) {
1497*1d05cddcSAtari911                if (e.target === dialog) {
1498*1d05cddcSAtari911                    dialog.remove();
1499*1d05cddcSAtari911                }
1500*1d05cddcSAtari911            });
1501*1d05cddcSAtari911
1502*1d05cddcSAtari911            dialog.innerHTML = `
1503*1d05cddcSAtari911                <div style="background:white; padding:20px; border-radius:8px; min-width:500px; max-width:700px; max-height:90vh; overflow-y:auto;">
1504*1d05cddcSAtari911                    <h3 style="margin:0 0 15px; color:#00cc07;">Edit Recurring Event</h3>
1505*1d05cddcSAtari911                    <p style="margin:0 0 15px; color:#666; font-size:13px;">Changes will apply to ALL occurrences of: <strong>${title}</strong></p>
1506*1d05cddcSAtari911
1507*1d05cddcSAtari911                    <form id="editRecurringForm" style="display:flex; flex-direction:column; gap:12px;">
1508*1d05cddcSAtari911                        <div>
1509*1d05cddcSAtari911                            <label style="display:block; font-weight:bold; margin-bottom:4px; font-size:13px;">New Title:</label>
1510*1d05cddcSAtari911                            <input type="text" name="new_title" value="${title}" style="width:100%; padding:8px; border:1px solid #ddd; border-radius:3px;" required>
1511*1d05cddcSAtari911                        </div>
1512*1d05cddcSAtari911
1513*1d05cddcSAtari911                        <div style="display:grid; grid-template-columns:1fr 1fr; gap:10px;">
1514*1d05cddcSAtari911                            <div>
1515*1d05cddcSAtari911                                <label style="display:block; font-weight:bold; margin-bottom:4px; font-size:13px;">Start Time:</label>
1516*1d05cddcSAtari911                                <input type="time" name="start_time" style="width:100%; padding:8px; border:1px solid #ddd; border-radius:3px;">
1517*1d05cddcSAtari911                                <small style="color:#999; font-size:11px;">Leave blank to keep current</small>
1518*1d05cddcSAtari911                            </div>
1519*1d05cddcSAtari911                            <div>
1520*1d05cddcSAtari911                                <label style="display:block; font-weight:bold; margin-bottom:4px; font-size:13px;">End Time:</label>
1521*1d05cddcSAtari911                                <input type="time" name="end_time" style="width:100%; padding:8px; border:1px solid #ddd; border-radius:3px;">
1522*1d05cddcSAtari911                                <small style="color:#999; font-size:11px;">Leave blank to keep current</small>
1523*1d05cddcSAtari911                            </div>
1524*1d05cddcSAtari911                        </div>
1525*1d05cddcSAtari911
1526*1d05cddcSAtari911                        <div>
1527*1d05cddcSAtari911                            <label style="display:block; font-weight:bold; margin-bottom:4px; font-size:13px;">Interval (days between occurrences):</label>
1528*1d05cddcSAtari911                            <select name="interval" style="width:100%; padding:8px; border:1px solid #ddd; border-radius:3px;">
1529*1d05cddcSAtari911                                <option value="">Keep current interval</option>
1530*1d05cddcSAtari911                                <option value="1">Daily (1 day)</option>
1531*1d05cddcSAtari911                                <option value="7">Weekly (7 days)</option>
1532*1d05cddcSAtari911                                <option value="14">Bi-weekly (14 days)</option>
1533*1d05cddcSAtari911                                <option value="30">Monthly (30 days)</option>
1534*1d05cddcSAtari911                                <option value="365">Yearly (365 days)</option>
1535*1d05cddcSAtari911                            </select>
1536*1d05cddcSAtari911                        </div>
1537*1d05cddcSAtari911
1538*1d05cddcSAtari911                        <div>
1539*1d05cddcSAtari911                            <label style="display:block; font-weight:bold; margin-bottom:4px; font-size:13px;">Move to Namespace:</label>
1540*1d05cddcSAtari911                            <select name="new_namespace" style="width:100%; padding:8px; border:1px solid #ddd; border-radius:3px;">
1541*1d05cddcSAtari911                                ${nsOptions}
1542*1d05cddcSAtari911                            </select>
1543*1d05cddcSAtari911                        </div>
1544*1d05cddcSAtari911
1545*1d05cddcSAtari911                        <div style="display:flex; gap:10px; margin-top:10px;">
1546*1d05cddcSAtari911                            <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>
1547*1d05cddcSAtari911                            <button type="button" onclick="closeEditDialog()" style="flex:1; background:#999; color:white; padding:10px; border:none; border-radius:3px; cursor:pointer;">Cancel</button>
1548*1d05cddcSAtari911                        </div>
1549*1d05cddcSAtari911                    </form>
1550*1d05cddcSAtari911                </div>
1551*1d05cddcSAtari911            `;
1552*1d05cddcSAtari911
1553*1d05cddcSAtari911            document.body.appendChild(dialog);
1554*1d05cddcSAtari911
1555*1d05cddcSAtari911            // Add close function to window
1556*1d05cddcSAtari911            window.closeEditDialog = function() {
1557*1d05cddcSAtari911                dialog.remove();
1558*1d05cddcSAtari911            };
1559*1d05cddcSAtari911
1560*1d05cddcSAtari911            // Handle form submission
1561*1d05cddcSAtari911            dialog.querySelector("#editRecurringForm").addEventListener("submit", function(e) {
1562*1d05cddcSAtari911                e.preventDefault();
1563*1d05cddcSAtari911                const formData = new FormData(this);
1564*1d05cddcSAtari911
1565*1d05cddcSAtari911                // Submit the edit
1566*1d05cddcSAtari911                const form = document.createElement("form");
1567*1d05cddcSAtari911                form.method = "POST";
1568*1d05cddcSAtari911                form.action = "?do=admin&page=calendar&tab=manage";
1569*1d05cddcSAtari911
1570*1d05cddcSAtari911                const actionInput = document.createElement("input");
1571*1d05cddcSAtari911                actionInput.type = "hidden";
1572*1d05cddcSAtari911                actionInput.name = "action";
1573*1d05cddcSAtari911                actionInput.value = "edit_recurring_series";
1574*1d05cddcSAtari911                form.appendChild(actionInput);
1575*1d05cddcSAtari911
1576*1d05cddcSAtari911                const oldTitleInput = document.createElement("input");
1577*1d05cddcSAtari911                oldTitleInput.type = "hidden";
1578*1d05cddcSAtari911                oldTitleInput.name = "old_title";
1579*1d05cddcSAtari911                oldTitleInput.value = title;
1580*1d05cddcSAtari911                form.appendChild(oldTitleInput);
1581*1d05cddcSAtari911
1582*1d05cddcSAtari911                const oldNamespaceInput = document.createElement("input");
1583*1d05cddcSAtari911                oldNamespaceInput.type = "hidden";
1584*1d05cddcSAtari911                oldNamespaceInput.name = "old_namespace";
1585*1d05cddcSAtari911                oldNamespaceInput.value = namespace;
1586*1d05cddcSAtari911                form.appendChild(oldNamespaceInput);
1587*1d05cddcSAtari911
1588*1d05cddcSAtari911                // Add all form fields
1589*1d05cddcSAtari911                for (let [key, value] of formData.entries()) {
1590*1d05cddcSAtari911                    const input = document.createElement("input");
1591*1d05cddcSAtari911                    input.type = "hidden";
1592*1d05cddcSAtari911                    input.name = key;
1593*1d05cddcSAtari911                    input.value = value;
1594*1d05cddcSAtari911                    form.appendChild(input);
1595*1d05cddcSAtari911                }
1596*1d05cddcSAtari911
1597*1d05cddcSAtari911                document.body.appendChild(form);
1598*1d05cddcSAtari911                form.submit();
1599*1d05cddcSAtari911            });
1600*1d05cddcSAtari911        }
1601*1d05cddcSAtari911
1602*1d05cddcSAtari911        function deleteRecurringSeries(title, namespace) {
1603*1d05cddcSAtari911            const displayNs = namespace || "(default)";
1604*1d05cddcSAtari911            if (!confirm("Delete ALL occurrences of: " + title + " (" + displayNs + ")?\\n\\nThis cannot be undone!")) {
1605*1d05cddcSAtari911                return;
1606*1d05cddcSAtari911            }
1607*1d05cddcSAtari911            const form = document.createElement("form");
1608*1d05cddcSAtari911            form.method = "POST";
1609*1d05cddcSAtari911            form.action = "?do=admin&page=calendar&tab=manage";
1610*1d05cddcSAtari911            const actionInput = document.createElement("input");
1611*1d05cddcSAtari911            actionInput.type = "hidden";
1612*1d05cddcSAtari911            actionInput.name = "action";
1613*1d05cddcSAtari911            actionInput.value = "delete_recurring_series";
1614*1d05cddcSAtari911            form.appendChild(actionInput);
1615*1d05cddcSAtari911            const titleInput = document.createElement("input");
1616*1d05cddcSAtari911            titleInput.type = "hidden";
1617*1d05cddcSAtari911            titleInput.name = "event_title";
1618*1d05cddcSAtari911            titleInput.value = title;
1619*1d05cddcSAtari911            form.appendChild(titleInput);
1620*1d05cddcSAtari911            const namespaceInput = document.createElement("input");
1621*1d05cddcSAtari911            namespaceInput.type = "hidden";
1622*1d05cddcSAtari911            namespaceInput.name = "namespace";
1623*1d05cddcSAtari911            namespaceInput.value = namespace;
1624*1d05cddcSAtari911            form.appendChild(namespaceInput);
1625*1d05cddcSAtari911            document.body.appendChild(form);
1626*1d05cddcSAtari911            form.submit();
1627*1d05cddcSAtari911        }
1628*1d05cddcSAtari911
1629*1d05cddcSAtari911        document.addEventListener("dragend", function(e) {
1630*1d05cddcSAtari911            if (e.target.draggable) {
1631*1d05cddcSAtari911                e.target.style.opacity = "1";
1632*1d05cddcSAtari911            }
1633*1d05cddcSAtari911        });
1634*1d05cddcSAtari911        </script>';
1635*1d05cddcSAtari911    }
1636*1d05cddcSAtari911
1637*1d05cddcSAtari911    private function renderUpdateTab() {
1638*1d05cddcSAtari911        global $INPUT;
1639*1d05cddcSAtari911
1640*1d05cddcSAtari911        echo '<h2 style="margin:10px 0; font-size:20px;">�� Update Plugin</h2>';
1641*1d05cddcSAtari911
1642*1d05cddcSAtari911        // Clear Cache button
1643*1d05cddcSAtari911        echo '<div style="margin-bottom:15px;">';
1644*1d05cddcSAtari911        echo '<form method="post" action="?do=admin&page=calendar&tab=update" style="display:inline; margin:0;">';
1645*1d05cddcSAtari911        echo '<input type="hidden" name="action" value="clear_cache">';
1646*1d05cddcSAtari911        echo '<input type="hidden" name="tab" value="update">';
1647*1d05cddcSAtari911        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; box-shadow:0 2px 4px rgba(0,0,0,0.1);">��️ Clear Cache</button>';
1648*1d05cddcSAtari911        echo '</form>';
1649*1d05cddcSAtari911        echo '<p style="margin:8px 0 0 0; font-size:12px; color:#666;">Clear the DokuWiki cache if changes aren\'t appearing or after updating the plugin.</p>';
1650*1d05cddcSAtari911        echo '</div>';
1651*1d05cddcSAtari911
1652*1d05cddcSAtari911        // Show message if present
1653*1d05cddcSAtari911        if ($INPUT->has('msg')) {
1654*1d05cddcSAtari911            $msg = hsc($INPUT->str('msg'));
1655*1d05cddcSAtari911            $type = $INPUT->str('msgtype', 'success');
1656*1d05cddcSAtari911            $class = ($type === 'success') ? 'msg success' : 'msg error';
1657*1d05cddcSAtari911            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:900px;\">";
1658*1d05cddcSAtari911            echo $msg;
1659*1d05cddcSAtari911            echo "</div>";
1660*1d05cddcSAtari911        }
1661*1d05cddcSAtari911
1662*1d05cddcSAtari911        // Show current version
1663*1d05cddcSAtari911        $pluginInfo = DOKU_PLUGIN . 'calendar/plugin.info.txt';
1664*1d05cddcSAtari911        $info = ['version' => 'Unknown', 'date' => 'Unknown', 'name' => 'Calendar Plugin', 'author' => 'Unknown', 'email' => '', 'desc' => ''];
1665*1d05cddcSAtari911        if (file_exists($pluginInfo)) {
1666*1d05cddcSAtari911            $info = array_merge($info, confToHash($pluginInfo));
1667*1d05cddcSAtari911        }
1668*1d05cddcSAtari911
1669*1d05cddcSAtari911        echo '<div style="background:#f9f9f9; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:900px;">';
1670*1d05cddcSAtari911        echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">' . hsc($info['name']) . '</h3>';
1671*1d05cddcSAtari911        echo '<div style="font-size:12px; line-height:1.6;">';
1672*1d05cddcSAtari911        echo '<div style="margin:4px 0;"><strong>Version:</strong> ' . hsc($info['version']) . ' (' . hsc($info['date']) . ')</div>';
1673*1d05cddcSAtari911        echo '<div style="margin:4px 0;"><strong>Author:</strong> ' . hsc($info['author']) . ($info['email'] ? ' &lt;' . hsc($info['email']) . '&gt;' : '') . '</div>';
1674*1d05cddcSAtari911        if ($info['desc']) {
1675*1d05cddcSAtari911            echo '<div style="margin:4px 0;"><strong>Description:</strong> ' . hsc($info['desc']) . '</div>';
1676*1d05cddcSAtari911        }
1677*1d05cddcSAtari911        echo '<div style="margin:4px 0;"><strong>Location:</strong> <code style="background:#f0f0f0; padding:2px 4px; border-radius:2px;">' . DOKU_PLUGIN . 'calendar/</code></div>';
1678*1d05cddcSAtari911        echo '</div>';
1679*1d05cddcSAtari911
1680*1d05cddcSAtari911        // Check permissions
1681*1d05cddcSAtari911        $pluginDir = DOKU_PLUGIN . 'calendar/';
1682*1d05cddcSAtari911        $pluginWritable = is_writable($pluginDir);
1683*1d05cddcSAtari911        $parentWritable = is_writable(DOKU_PLUGIN);
1684*1d05cddcSAtari911
1685*1d05cddcSAtari911        echo '<div style="margin-top:8px; padding-top:8px; border-top:1px solid #ddd;">';
1686*1d05cddcSAtari911        if ($pluginWritable && $parentWritable) {
1687*1d05cddcSAtari911            echo '<p style="margin:5px 0; font-size:13px; color:#28a745;"><strong>✅ Permissions:</strong> OK - ready to update</p>';
1688*1d05cddcSAtari911        } else {
1689*1d05cddcSAtari911            echo '<p style="margin:5px 0; font-size:13px; color:#dc3545;"><strong>❌ Permissions:</strong> Issues detected</p>';
1690*1d05cddcSAtari911            if (!$pluginWritable) {
1691*1d05cddcSAtari911                echo '<p style="margin:2px 0 2px 20px; font-size:12px; color:#dc3545;">Plugin directory not writable</p>';
1692*1d05cddcSAtari911            }
1693*1d05cddcSAtari911            if (!$parentWritable) {
1694*1d05cddcSAtari911                echo '<p style="margin:2px 0 2px 20px; font-size:12px; color:#dc3545;">Parent directory not writable</p>';
1695*1d05cddcSAtari911            }
1696*1d05cddcSAtari911            echo '<p style="margin:5px 0; font-size:12px; color:#666;">Fix with: <code style="background:#f0f0f0; padding:2px 4px; border-radius:2px;">chmod -R 755 ' . DOKU_PLUGIN . 'calendar/</code></p>';
1697*1d05cddcSAtari911            echo '<p style="margin:2px 0; font-size:12px; color:#666;">Or: <code style="background:#f0f0f0; padding:2px 4px; border-radius:2px;">chown -R www-data:www-data ' . DOKU_PLUGIN . 'calendar/</code></p>';
1698*1d05cddcSAtari911        }
1699*1d05cddcSAtari911        echo '</div>';
1700*1d05cddcSAtari911
1701*1d05cddcSAtari911        echo '</div>';
1702*1d05cddcSAtari911
1703*1d05cddcSAtari911        // Changelog section
1704*1d05cddcSAtari911        echo '<div style="background:#f9f9f9; padding:12px; margin:10px 0; border-left:3px solid #7b1fa2; border-radius:3px; max-width:900px;">';
1705*1d05cddcSAtari911        echo '<h3 style="margin:0 0 8px 0; color:#7b1fa2; font-size:16px;">�� Recent Changes</h3>';
1706*1d05cddcSAtari911
1707*1d05cddcSAtari911        $changelogFile = DOKU_PLUGIN . 'calendar/CHANGELOG.md';
1708*1d05cddcSAtari911        if (file_exists($changelogFile)) {
1709*1d05cddcSAtari911            $changelog = file_get_contents($changelogFile);
1710*1d05cddcSAtari911
1711*1d05cddcSAtari911            // Parse markdown and show last 10 versions
1712*1d05cddcSAtari911            $lines = explode("\n", $changelog);
1713*1d05cddcSAtari911            $versionsShown = 0;
1714*1d05cddcSAtari911            $maxVersions = 10;
1715*1d05cddcSAtari911            $inVersion = false;
1716*1d05cddcSAtari911            $changelogHtml = '<div style="font-size:12px; line-height:1.7; max-height:100px; overflow-y:auto; padding-right:10px;">';
1717*1d05cddcSAtari911
1718*1d05cddcSAtari911            foreach ($lines as $line) {
1719*1d05cddcSAtari911                $line = trim($line);
1720*1d05cddcSAtari911
1721*1d05cddcSAtari911                // Version header (## Version X.X.X)
1722*1d05cddcSAtari911                if (preg_match('/^## Version (.+)$/', $line, $matches)) {
1723*1d05cddcSAtari911                    if ($versionsShown >= $maxVersions) break;
1724*1d05cddcSAtari911                    $versionsShown++;
1725*1d05cddcSAtari911                    $inVersion = true;
1726*1d05cddcSAtari911                    $changelogHtml .= '<div style="margin-top:' . ($versionsShown > 1 ? '16px' : '0') . '; padding:8px; background:#fff; border-radius:3px; border-left:3px solid #00cc07;">';
1727*1d05cddcSAtari911                    $changelogHtml .= '<div style="font-weight:bold; color:#00cc07; margin-bottom:6px;">��️ ' . hsc($matches[1]) . '</div>';
1728*1d05cddcSAtari911                }
1729*1d05cddcSAtari911                // List items (- **Added:** text)
1730*1d05cddcSAtari911                elseif (preg_match('/^- \*\*(.+?):\*\* (.+)$/', $line, $matches)) {
1731*1d05cddcSAtari911                    $type = $matches[1];
1732*1d05cddcSAtari911                    $description = $matches[2];
1733*1d05cddcSAtari911
1734*1d05cddcSAtari911                    // Color-code by type
1735*1d05cddcSAtari911                    $color = '#666';
1736*1d05cddcSAtari911                    $icon = '•';
1737*1d05cddcSAtari911                    if ($type === 'Added') { $color = '#28a745'; $icon = '✨'; }
1738*1d05cddcSAtari911                    elseif ($type === 'Fixed') { $color = '#dc3545'; $icon = '��'; }
1739*1d05cddcSAtari911                    elseif ($type === 'Changed') { $color = '#7b1fa2'; $icon = '��'; }
1740*1d05cddcSAtari911                    elseif ($type === 'Improved') { $color = '#ff9800'; $icon = '⚡'; }
1741*1d05cddcSAtari911                    elseif ($type === 'Development') { $color = '#6c757d'; $icon = '��️'; }
1742*1d05cddcSAtari911
1743*1d05cddcSAtari911                    $changelogHtml .= '<div style="margin:3px 0 3px 10px; color:' . $color . ';">';
1744*1d05cddcSAtari911                    $changelogHtml .= '<strong>' . $icon . ' ' . hsc($type) . ':</strong> <span style="color:#333;">' . hsc($description) . '</span>';
1745*1d05cddcSAtari911                    $changelogHtml .= '</div>';
1746*1d05cddcSAtari911                }
1747*1d05cddcSAtari911                // Close version block on empty line after items
1748*1d05cddcSAtari911                elseif ($inVersion && $line === '' && $versionsShown > 0) {
1749*1d05cddcSAtari911                    $changelogHtml .= '</div>';
1750*1d05cddcSAtari911                    $inVersion = false;
1751*1d05cddcSAtari911                }
1752*1d05cddcSAtari911            }
1753*1d05cddcSAtari911
1754*1d05cddcSAtari911            // Close last version if still open
1755*1d05cddcSAtari911            if ($inVersion) {
1756*1d05cddcSAtari911                $changelogHtml .= '</div>';
1757*1d05cddcSAtari911            }
1758*1d05cddcSAtari911
1759*1d05cddcSAtari911            $changelogHtml .= '</div>';
1760*1d05cddcSAtari911
1761*1d05cddcSAtari911            echo $changelogHtml;
1762*1d05cddcSAtari911        } else {
1763*1d05cddcSAtari911            echo '<p style="color:#999; font-size:13px; font-style:italic;">Changelog not available</p>';
1764*1d05cddcSAtari911        }
1765*1d05cddcSAtari911
1766*1d05cddcSAtari911        echo '</div>';
1767*1d05cddcSAtari911
1768*1d05cddcSAtari911        // Upload form
1769*1d05cddcSAtari911        echo '<div style="background:#f9f9f9; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:900px;">';
1770*1d05cddcSAtari911        echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">Upload New Version</h3>';
1771*1d05cddcSAtari911        echo '<p style="color:#666; font-size:13px; margin:0 0 10px;">Upload a calendar plugin ZIP file to update. Your configuration will be preserved.</p>';
1772*1d05cddcSAtari911
1773*1d05cddcSAtari911        echo '<form method="post" action="?do=admin&page=calendar&tab=update" enctype="multipart/form-data" id="uploadForm">';
1774*1d05cddcSAtari911        echo '<input type="hidden" name="action" value="upload_update">';
1775*1d05cddcSAtari911        echo '<div style="margin:10px 0;">';
1776*1d05cddcSAtari911        echo '<input type="file" name="plugin_zip" accept=".zip" required style="padding:8px; border:1px solid #ddd; border-radius:3px; font-size:13px;">';
1777*1d05cddcSAtari911        echo '</div>';
1778*1d05cddcSAtari911        echo '<div style="margin:10px 0;">';
1779*1d05cddcSAtari911        echo '<label style="display:flex; align-items:center; gap:8px; font-size:13px;">';
1780*1d05cddcSAtari911        echo '<input type="checkbox" name="backup_first" value="1" checked>';
1781*1d05cddcSAtari911        echo '<span>Create backup before updating (Recommended)</span>';
1782*1d05cddcSAtari911        echo '</label>';
1783*1d05cddcSAtari911        echo '</div>';
1784*1d05cddcSAtari911        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>';
1785*1d05cddcSAtari911        echo '</form>';
1786*1d05cddcSAtari911        echo '</div>';
1787*1d05cddcSAtari911
1788*1d05cddcSAtari911        // Warning box
1789*1d05cddcSAtari911        echo '<div style="background:#fff3e0; border-left:3px solid #ff9800; padding:12px; margin:10px 0; border-radius:3px; max-width:900px;">';
1790*1d05cddcSAtari911        echo '<h4 style="margin:0 0 5px 0; color:#e65100; font-size:14px;">⚠️ Important Notes</h4>';
1791*1d05cddcSAtari911        echo '<ul style="margin:5px 0; padding-left:20px; font-size:12px; color:#e65100;">';
1792*1d05cddcSAtari911        echo '<li>This will replace all plugin files</li>';
1793*1d05cddcSAtari911        echo '<li>Configuration files (sync_config.php) will be preserved</li>';
1794*1d05cddcSAtari911        echo '<li>Event data will not be affected</li>';
1795*1d05cddcSAtari911        echo '<li>Backup will be saved to: <code>calendar.backup.vX.X.X.YYYY-MM-DD_HH-MM-SS.zip</code></li>';
1796*1d05cddcSAtari911        echo '<li>Make sure the ZIP file is a valid calendar plugin</li>';
1797*1d05cddcSAtari911        echo '</ul>';
1798*1d05cddcSAtari911        echo '</div>';
1799*1d05cddcSAtari911
1800*1d05cddcSAtari911        // Backup list
1801*1d05cddcSAtari911        $backupDir = DOKU_PLUGIN;
1802*1d05cddcSAtari911        $backups = glob($backupDir . 'calendar*.zip');
1803*1d05cddcSAtari911
1804*1d05cddcSAtari911        // Filter to only show files that look like backups (not the uploaded plugin files)
1805*1d05cddcSAtari911        $backups = array_filter($backups, function($file) {
1806*1d05cddcSAtari911            $name = basename($file);
1807*1d05cddcSAtari911            // Include files that start with "calendar" but exclude files that are just "calendar.zip" (uploaded plugin)
1808*1d05cddcSAtari911            return $name !== 'calendar.zip';
1809*1d05cddcSAtari911        });
1810*1d05cddcSAtari911
1811*1d05cddcSAtari911        if (!empty($backups)) {
1812*1d05cddcSAtari911            rsort($backups); // Newest first
1813*1d05cddcSAtari911            echo '<div style="background:#f9f9f9; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:900px;">';
1814*1d05cddcSAtari911            echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">�� Available Backups</h3>';
1815*1d05cddcSAtari911            echo '<div style="max-height:200px; overflow-y:auto; border:1px solid #ddd; border-radius:3px; background:white;">';
1816*1d05cddcSAtari911            echo '<table style="width:100%; border-collapse:collapse; font-size:12px;">';
1817*1d05cddcSAtari911            echo '<thead style="position:sticky; top:0; background:#e9e9e9;">';
1818*1d05cddcSAtari911            echo '<tr>';
1819*1d05cddcSAtari911            echo '<th style="padding:6px; text-align:left; border-bottom:2px solid #ddd;">Backup File</th>';
1820*1d05cddcSAtari911            echo '<th style="padding:6px; text-align:left; border-bottom:2px solid #ddd;">Size</th>';
1821*1d05cddcSAtari911            echo '<th style="padding:6px; text-align:left; border-bottom:2px solid #ddd;">Actions</th>';
1822*1d05cddcSAtari911            echo '</tr></thead><tbody>';
1823*1d05cddcSAtari911
1824*1d05cddcSAtari911            foreach ($backups as $backup) {
1825*1d05cddcSAtari911                $filename = basename($backup);
1826*1d05cddcSAtari911                $size = $this->formatBytes(filesize($backup));
1827*1d05cddcSAtari911                echo '<tr style="border-bottom:1px solid #eee;">';
1828*1d05cddcSAtari911                echo '<td style="padding:6px;"><code style="font-size:11px;">' . hsc($filename) . '</code></td>';
1829*1d05cddcSAtari911                echo '<td style="padding:6px;">' . $size . '</td>';
1830*1d05cddcSAtari911                echo '<td style="padding:6px; white-space:nowrap;">';
1831*1d05cddcSAtari911                echo '<a href="' . DOKU_BASE . 'lib/plugins/' . hsc($filename) . '" download style="color:#00cc07; text-decoration:none; font-size:11px; margin-right:10px;">�� Download</a>';
1832*1d05cddcSAtari911                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>';
1833*1d05cddcSAtari911                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>';
1834*1d05cddcSAtari911                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>';
1835*1d05cddcSAtari911                echo '</td>';
1836*1d05cddcSAtari911                echo '</tr>';
1837*1d05cddcSAtari911            }
1838*1d05cddcSAtari911
1839*1d05cddcSAtari911            echo '</tbody></table>';
1840*1d05cddcSAtari911            echo '</div>';
1841*1d05cddcSAtari911            echo '</div>';
1842*1d05cddcSAtari911        }
1843*1d05cddcSAtari911
1844*1d05cddcSAtari911        echo '<script>
1845*1d05cddcSAtari911        function confirmUpload() {
1846*1d05cddcSAtari911            const fileInput = document.querySelector(\'input[name="plugin_zip"]\');
1847*1d05cddcSAtari911            if (!fileInput.files[0]) {
1848*1d05cddcSAtari911                alert("Please select a ZIP file");
1849*1d05cddcSAtari911                return false;
1850*1d05cddcSAtari911            }
1851*1d05cddcSAtari911
1852*1d05cddcSAtari911            const fileName = fileInput.files[0].name;
1853*1d05cddcSAtari911            if (!fileName.endsWith(".zip")) {
1854*1d05cddcSAtari911                alert("Please select a ZIP file");
1855*1d05cddcSAtari911                return false;
1856*1d05cddcSAtari911            }
1857*1d05cddcSAtari911
1858*1d05cddcSAtari911            return confirm("Upload and install: " + fileName + "?\\n\\nThis will replace all plugin files.\\nYour configuration and data will be preserved.\\n\\nContinue?");
1859*1d05cddcSAtari911        }
1860*1d05cddcSAtari911
1861*1d05cddcSAtari911        function deleteBackup(filename) {
1862*1d05cddcSAtari911            if (!confirm("Delete backup: " + filename + "?\\n\\nThis cannot be undone!")) {
1863*1d05cddcSAtari911                return;
1864*1d05cddcSAtari911            }
1865*1d05cddcSAtari911
1866*1d05cddcSAtari911            const form = document.createElement("form");
1867*1d05cddcSAtari911            form.method = "POST";
1868*1d05cddcSAtari911            form.action = "?do=admin&page=calendar&tab=update";
1869*1d05cddcSAtari911
1870*1d05cddcSAtari911            const actionInput = document.createElement("input");
1871*1d05cddcSAtari911            actionInput.type = "hidden";
1872*1d05cddcSAtari911            actionInput.name = "action";
1873*1d05cddcSAtari911            actionInput.value = "delete_backup";
1874*1d05cddcSAtari911            form.appendChild(actionInput);
1875*1d05cddcSAtari911
1876*1d05cddcSAtari911            const filenameInput = document.createElement("input");
1877*1d05cddcSAtari911            filenameInput.type = "hidden";
1878*1d05cddcSAtari911            filenameInput.name = "backup_file";
1879*1d05cddcSAtari911            filenameInput.value = filename;
1880*1d05cddcSAtari911            form.appendChild(filenameInput);
1881*1d05cddcSAtari911
1882*1d05cddcSAtari911            document.body.appendChild(form);
1883*1d05cddcSAtari911            form.submit();
1884*1d05cddcSAtari911        }
1885*1d05cddcSAtari911
1886*1d05cddcSAtari911        function restoreBackup(filename) {
1887*1d05cddcSAtari911            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?")) {
1888*1d05cddcSAtari911                return;
1889*1d05cddcSAtari911            }
1890*1d05cddcSAtari911
1891*1d05cddcSAtari911            const form = document.createElement("form");
1892*1d05cddcSAtari911            form.method = "POST";
1893*1d05cddcSAtari911            form.action = "?do=admin&page=calendar&tab=update";
1894*1d05cddcSAtari911
1895*1d05cddcSAtari911            const actionInput = document.createElement("input");
1896*1d05cddcSAtari911            actionInput.type = "hidden";
1897*1d05cddcSAtari911            actionInput.name = "action";
1898*1d05cddcSAtari911            actionInput.value = "restore_backup";
1899*1d05cddcSAtari911            form.appendChild(actionInput);
1900*1d05cddcSAtari911
1901*1d05cddcSAtari911            const filenameInput = document.createElement("input");
1902*1d05cddcSAtari911            filenameInput.type = "hidden";
1903*1d05cddcSAtari911            filenameInput.name = "backup_file";
1904*1d05cddcSAtari911            filenameInput.value = filename;
1905*1d05cddcSAtari911            form.appendChild(filenameInput);
1906*1d05cddcSAtari911
1907*1d05cddcSAtari911            document.body.appendChild(form);
1908*1d05cddcSAtari911            form.submit();
1909*1d05cddcSAtari911        }
1910*1d05cddcSAtari911
1911*1d05cddcSAtari911        function renameBackup(filename) {
1912*1d05cddcSAtari911            const newName = prompt("Enter new backup name (without .zip extension):\\n\\nCurrent: " + filename.replace(/\\.zip$/, ""), filename.replace(/\\.zip$/, ""));
1913*1d05cddcSAtari911            if (!newName || newName === filename.replace(/\\.zip$/, "")) {
1914*1d05cddcSAtari911                return;
1915*1d05cddcSAtari911            }
1916*1d05cddcSAtari911
1917*1d05cddcSAtari911            // Add .zip if not present
1918*1d05cddcSAtari911            const newFilename = newName.endsWith(".zip") ? newName : newName + ".zip";
1919*1d05cddcSAtari911
1920*1d05cddcSAtari911            // Basic validation
1921*1d05cddcSAtari911            if (!/^[a-zA-Z0-9._-]+$/.test(newFilename.replace(/\\.zip$/, ""))) {
1922*1d05cddcSAtari911                alert("Invalid filename. Use only letters, numbers, dots, dashes, and underscores.");
1923*1d05cddcSAtari911                return;
1924*1d05cddcSAtari911            }
1925*1d05cddcSAtari911
1926*1d05cddcSAtari911            const form = document.createElement("form");
1927*1d05cddcSAtari911            form.method = "POST";
1928*1d05cddcSAtari911            form.action = "?do=admin&page=calendar&tab=update";
1929*1d05cddcSAtari911
1930*1d05cddcSAtari911            const actionInput = document.createElement("input");
1931*1d05cddcSAtari911            actionInput.type = "hidden";
1932*1d05cddcSAtari911            actionInput.name = "action";
1933*1d05cddcSAtari911            actionInput.value = "rename_backup";
1934*1d05cddcSAtari911            form.appendChild(actionInput);
1935*1d05cddcSAtari911
1936*1d05cddcSAtari911            const oldNameInput = document.createElement("input");
1937*1d05cddcSAtari911            oldNameInput.type = "hidden";
1938*1d05cddcSAtari911            oldNameInput.name = "old_name";
1939*1d05cddcSAtari911            oldNameInput.value = filename;
1940*1d05cddcSAtari911            form.appendChild(oldNameInput);
1941*1d05cddcSAtari911
1942*1d05cddcSAtari911            const newNameInput = document.createElement("input");
1943*1d05cddcSAtari911            newNameInput.type = "hidden";
1944*1d05cddcSAtari911            newNameInput.name = "new_name";
1945*1d05cddcSAtari911            newNameInput.value = newFilename;
1946*1d05cddcSAtari911            form.appendChild(newNameInput);
1947*1d05cddcSAtari911
1948*1d05cddcSAtari911            document.body.appendChild(form);
1949*1d05cddcSAtari911            form.submit();
1950*1d05cddcSAtari911        }
1951*1d05cddcSAtari911        </script>';
1952*1d05cddcSAtari911    }
1953*1d05cddcSAtari911
1954*1d05cddcSAtari911    private function saveConfig() {
1955*1d05cddcSAtari911        global $INPUT;
1956*1d05cddcSAtari911
1957*1d05cddcSAtari911        // Load existing config to preserve all settings
1958*1d05cddcSAtari911        $configFile = DOKU_PLUGIN . 'calendar/sync_config.php';
1959*1d05cddcSAtari911        $existingConfig = [];
1960*1d05cddcSAtari911        if (file_exists($configFile)) {
1961*1d05cddcSAtari911            $existingConfig = include $configFile;
1962*1d05cddcSAtari911        }
1963*1d05cddcSAtari911
1964*1d05cddcSAtari911        // Update only the fields from the form - preserve everything else
1965*1d05cddcSAtari911        $config = $existingConfig;
1966*1d05cddcSAtari911
1967*1d05cddcSAtari911        // Update basic fields
1968*1d05cddcSAtari911        $config['tenant_id'] = $INPUT->str('tenant_id');
1969*1d05cddcSAtari911        $config['client_id'] = $INPUT->str('client_id');
1970*1d05cddcSAtari911        $config['client_secret'] = $INPUT->str('client_secret');
1971*1d05cddcSAtari911        $config['user_email'] = $INPUT->str('user_email');
1972*1d05cddcSAtari911        $config['timezone'] = $INPUT->str('timezone', 'America/Los_Angeles');
1973*1d05cddcSAtari911        $config['default_category'] = $INPUT->str('default_category', 'Blue category');
1974*1d05cddcSAtari911        $config['reminder_minutes'] = $INPUT->int('reminder_minutes', 15);
1975*1d05cddcSAtari911        $config['sync_completed_tasks'] = $INPUT->bool('sync_completed_tasks');
1976*1d05cddcSAtari911        $config['delete_outlook_events'] = $INPUT->bool('delete_outlook_events');
1977*1d05cddcSAtari911        $config['sync_all_namespaces'] = $INPUT->bool('sync_all_namespaces');
1978*1d05cddcSAtari911        $config['sync_namespaces'] = $INPUT->arr('sync_namespaces');
1979*1d05cddcSAtari911        $config['important_namespaces'] = $INPUT->str('important_namespaces', 'important');
1980*1d05cddcSAtari911
1981*1d05cddcSAtari911        // Parse category mapping
1982*1d05cddcSAtari911        $config['category_mapping'] = [];
1983*1d05cddcSAtari911        $mappingText = $INPUT->str('category_mapping');
1984*1d05cddcSAtari911        if ($mappingText) {
1985*1d05cddcSAtari911            $lines = explode("\n", $mappingText);
1986*1d05cddcSAtari911            foreach ($lines as $line) {
1987*1d05cddcSAtari911                $line = trim($line);
1988*1d05cddcSAtari911                if (empty($line)) continue;
1989*1d05cddcSAtari911                $parts = explode('=', $line, 2);
1990*1d05cddcSAtari911                if (count($parts) === 2) {
1991*1d05cddcSAtari911                    $config['category_mapping'][trim($parts[0])] = trim($parts[1]);
1992*1d05cddcSAtari911                }
1993*1d05cddcSAtari911            }
1994*1d05cddcSAtari911        }
1995*1d05cddcSAtari911
1996*1d05cddcSAtari911        // Parse color mapping from dropdown selections
1997*1d05cddcSAtari911        $config['color_mapping'] = [];
1998*1d05cddcSAtari911        $colorMappingCount = $INPUT->int('color_mapping_count', 0);
1999*1d05cddcSAtari911        for ($i = 0; $i < $colorMappingCount; $i++) {
2000*1d05cddcSAtari911            $hexColor = $INPUT->str('color_hex_' . $i);
2001*1d05cddcSAtari911            $category = $INPUT->str('color_map_' . $i);
2002*1d05cddcSAtari911
2003*1d05cddcSAtari911            if (!empty($hexColor) && !empty($category)) {
2004*1d05cddcSAtari911                $config['color_mapping'][$hexColor] = $category;
2005*1d05cddcSAtari911            }
2006*1d05cddcSAtari911        }
2007*1d05cddcSAtari911
2008*1d05cddcSAtari911        // Build file content using return format
2009*1d05cddcSAtari911        $content = "<?php\n";
2010*1d05cddcSAtari911        $content .= "/**\n";
2011*1d05cddcSAtari911        $content .= " * DokuWiki Calendar → Outlook Sync - Configuration\n";
2012*1d05cddcSAtari911        $content .= " * \n";
2013*1d05cddcSAtari911        $content .= " * SECURITY: Add this file to .gitignore!\n";
2014*1d05cddcSAtari911        $content .= " * Never commit credentials to version control.\n";
2015*1d05cddcSAtari911        $content .= " */\n\n";
2016*1d05cddcSAtari911        $content .= "return " . var_export($config, true) . ";\n";
2017*1d05cddcSAtari911
2018*1d05cddcSAtari911        // Save file
2019*1d05cddcSAtari911        if (file_put_contents($configFile, $content)) {
2020*1d05cddcSAtari911            $this->redirect('Configuration saved successfully!', 'success');
2021*1d05cddcSAtari911        } else {
2022*1d05cddcSAtari911            $this->redirect('Error: Could not save configuration file', 'error');
2023*1d05cddcSAtari911        }
2024*1d05cddcSAtari911    }
2025*1d05cddcSAtari911
2026*1d05cddcSAtari911    private function clearCache() {
2027*1d05cddcSAtari911        // Clear DokuWiki cache
2028*1d05cddcSAtari911        $cacheDir = DOKU_INC . 'data/cache';
2029*1d05cddcSAtari911
2030*1d05cddcSAtari911        if (is_dir($cacheDir)) {
2031*1d05cddcSAtari911            $this->recursiveDelete($cacheDir, false);
2032*1d05cddcSAtari911            $this->redirect('Cache cleared successfully!', 'success', 'update');
2033*1d05cddcSAtari911        } else {
2034*1d05cddcSAtari911            $this->redirect('Cache directory not found', 'error', 'update');
2035*1d05cddcSAtari911        }
2036*1d05cddcSAtari911    }
2037*1d05cddcSAtari911
2038*1d05cddcSAtari911    private function recursiveDelete($dir, $deleteRoot = true) {
2039*1d05cddcSAtari911        if (!is_dir($dir)) return;
2040*1d05cddcSAtari911
2041*1d05cddcSAtari911        $files = array_diff(scandir($dir), array('.', '..'));
2042*1d05cddcSAtari911        foreach ($files as $file) {
2043*1d05cddcSAtari911            $path = $dir . '/' . $file;
2044*1d05cddcSAtari911            if (is_dir($path)) {
2045*1d05cddcSAtari911                $this->recursiveDelete($path, true);
2046*1d05cddcSAtari911            } else {
2047*1d05cddcSAtari911                @unlink($path);
2048*1d05cddcSAtari911            }
2049*1d05cddcSAtari911        }
2050*1d05cddcSAtari911
2051*1d05cddcSAtari911        if ($deleteRoot) {
2052*1d05cddcSAtari911            @rmdir($dir);
2053*1d05cddcSAtari911        }
2054*1d05cddcSAtari911    }
2055*1d05cddcSAtari911
2056*1d05cddcSAtari911    private function findRecurringEvents() {
2057*1d05cddcSAtari911        $dataDir = DOKU_INC . 'data/meta/';
2058*1d05cddcSAtari911        $recurring = [];
2059*1d05cddcSAtari911        $allEvents = []; // Track all events to detect patterns
2060*1d05cddcSAtari911
2061*1d05cddcSAtari911        // Check root calendar directory first (blank/default namespace)
2062*1d05cddcSAtari911        $rootCalendarDir = $dataDir . 'calendar';
2063*1d05cddcSAtari911        if (is_dir($rootCalendarDir)) {
2064*1d05cddcSAtari911            foreach (glob($rootCalendarDir . '/*.json') as $file) {
2065*1d05cddcSAtari911                $data = json_decode(file_get_contents($file), true);
2066*1d05cddcSAtari911                if (!$data) continue;
2067*1d05cddcSAtari911
2068*1d05cddcSAtari911                foreach ($data as $dateKey => $events) {
2069*1d05cddcSAtari911                    foreach ($events as $event) {
2070*1d05cddcSAtari911                        // Group by title + namespace (events with same title are likely recurring)
2071*1d05cddcSAtari911                        $groupKey = strtolower(trim($event['title'])) . '_';
2072*1d05cddcSAtari911
2073*1d05cddcSAtari911                        if (!isset($allEvents[$groupKey])) {
2074*1d05cddcSAtari911                            $allEvents[$groupKey] = [
2075*1d05cddcSAtari911                                'title' => $event['title'],
2076*1d05cddcSAtari911                                'namespace' => '',
2077*1d05cddcSAtari911                                'dates' => [],
2078*1d05cddcSAtari911                                'events' => []
2079*1d05cddcSAtari911                            ];
2080*1d05cddcSAtari911                        }
2081*1d05cddcSAtari911                        $allEvents[$groupKey]['dates'][] = $dateKey;
2082*1d05cddcSAtari911                        $allEvents[$groupKey]['events'][] = $event;
2083*1d05cddcSAtari911                    }
2084*1d05cddcSAtari911                }
2085*1d05cddcSAtari911            }
2086*1d05cddcSAtari911        }
2087*1d05cddcSAtari911
2088*1d05cddcSAtari911        // Scan all namespace directories
2089*1d05cddcSAtari911        foreach (glob($dataDir . '*', GLOB_ONLYDIR) as $nsDir) {
2090*1d05cddcSAtari911            $namespace = basename($nsDir);
2091*1d05cddcSAtari911
2092*1d05cddcSAtari911            // Skip the root 'calendar' dir (already processed above)
2093*1d05cddcSAtari911            if ($namespace === 'calendar') continue;
2094*1d05cddcSAtari911
2095*1d05cddcSAtari911            $calendarDir = $nsDir . '/calendar';
2096*1d05cddcSAtari911
2097*1d05cddcSAtari911            if (!is_dir($calendarDir)) continue;
2098*1d05cddcSAtari911
2099*1d05cddcSAtari911            // Scan all calendar files
2100*1d05cddcSAtari911            foreach (glob($calendarDir . '/*.json') as $file) {
2101*1d05cddcSAtari911                $data = json_decode(file_get_contents($file), true);
2102*1d05cddcSAtari911                if (!$data) continue;
2103*1d05cddcSAtari911
2104*1d05cddcSAtari911                foreach ($data as $dateKey => $events) {
2105*1d05cddcSAtari911                    foreach ($events as $event) {
2106*1d05cddcSAtari911                        $groupKey = strtolower(trim($event['title'])) . '_' . ($event['namespace'] ?? '');
2107*1d05cddcSAtari911
2108*1d05cddcSAtari911                        if (!isset($allEvents[$groupKey])) {
2109*1d05cddcSAtari911                            $allEvents[$groupKey] = [
2110*1d05cddcSAtari911                                'title' => $event['title'],
2111*1d05cddcSAtari911                                'namespace' => $event['namespace'] ?? '',
2112*1d05cddcSAtari911                                'dates' => [],
2113*1d05cddcSAtari911                                'events' => []
2114*1d05cddcSAtari911                            ];
2115*1d05cddcSAtari911                        }
2116*1d05cddcSAtari911                        $allEvents[$groupKey]['dates'][] = $dateKey;
2117*1d05cddcSAtari911                        $allEvents[$groupKey]['events'][] = $event;
2118*1d05cddcSAtari911                    }
2119*1d05cddcSAtari911                }
2120*1d05cddcSAtari911            }
2121*1d05cddcSAtari911        }
2122*1d05cddcSAtari911
2123*1d05cddcSAtari911        // Analyze patterns - only include if 3+ occurrences
2124*1d05cddcSAtari911        foreach ($allEvents as $groupKey => $group) {
2125*1d05cddcSAtari911            if (count($group['dates']) >= 3) {
2126*1d05cddcSAtari911                // Sort dates
2127*1d05cddcSAtari911                sort($group['dates']);
2128*1d05cddcSAtari911
2129*1d05cddcSAtari911                // Calculate interval between first and second occurrence
2130*1d05cddcSAtari911                $date1 = new DateTime($group['dates'][0]);
2131*1d05cddcSAtari911                $date2 = new DateTime($group['dates'][1]);
2132*1d05cddcSAtari911                $interval = $date1->diff($date2);
2133*1d05cddcSAtari911
2134*1d05cddcSAtari911                // Determine pattern
2135*1d05cddcSAtari911                $pattern = 'Custom';
2136*1d05cddcSAtari911                if ($interval->days == 1) {
2137*1d05cddcSAtari911                    $pattern = 'Daily';
2138*1d05cddcSAtari911                } elseif ($interval->days == 7) {
2139*1d05cddcSAtari911                    $pattern = 'Weekly';
2140*1d05cddcSAtari911                } elseif ($interval->days >= 14 && $interval->days <= 16) {
2141*1d05cddcSAtari911                    $pattern = 'Bi-weekly';
2142*1d05cddcSAtari911                } elseif ($interval->days >= 28 && $interval->days <= 31) {
2143*1d05cddcSAtari911                    $pattern = 'Monthly';
2144*1d05cddcSAtari911                } elseif ($interval->days >= 365 && $interval->days <= 366) {
2145*1d05cddcSAtari911                    $pattern = 'Yearly';
2146*1d05cddcSAtari911                }
2147*1d05cddcSAtari911
2148*1d05cddcSAtari911                // Use first event's ID or create a synthetic one
2149*1d05cddcSAtari911                $baseId = isset($group['events'][0]['recurringId'])
2150*1d05cddcSAtari911                    ? $group['events'][0]['recurringId']
2151*1d05cddcSAtari911                    : md5($group['title'] . $group['namespace']);
2152*1d05cddcSAtari911
2153*1d05cddcSAtari911                $recurring[] = [
2154*1d05cddcSAtari911                    'baseId' => $baseId,
2155*1d05cddcSAtari911                    'title' => $group['title'],
2156*1d05cddcSAtari911                    'namespace' => $group['namespace'],
2157*1d05cddcSAtari911                    'pattern' => $pattern,
2158*1d05cddcSAtari911                    'count' => count($group['dates']),
2159*1d05cddcSAtari911                    'firstDate' => $group['dates'][0],
2160*1d05cddcSAtari911                    'interval' => $interval->days
2161*1d05cddcSAtari911                ];
2162*1d05cddcSAtari911            }
2163*1d05cddcSAtari911        }
2164*1d05cddcSAtari911
2165*1d05cddcSAtari911        return $recurring;
2166*1d05cddcSAtari911    }
2167*1d05cddcSAtari911
2168*1d05cddcSAtari911    private function getEventsByNamespace() {
2169*1d05cddcSAtari911        $dataDir = DOKU_INC . 'data/meta/';
2170*1d05cddcSAtari911        $result = [];
2171*1d05cddcSAtari911
2172*1d05cddcSAtari911        // Check root calendar directory first (blank/default namespace)
2173*1d05cddcSAtari911        $rootCalendarDir = $dataDir . 'calendar';
2174*1d05cddcSAtari911        if (is_dir($rootCalendarDir)) {
2175*1d05cddcSAtari911            $hasFiles = false;
2176*1d05cddcSAtari911            $events = [];
2177*1d05cddcSAtari911
2178*1d05cddcSAtari911            foreach (glob($rootCalendarDir . '/*.json') as $file) {
2179*1d05cddcSAtari911                $hasFiles = true;
2180*1d05cddcSAtari911                $month = basename($file, '.json');
2181*1d05cddcSAtari911                $data = json_decode(file_get_contents($file), true);
2182*1d05cddcSAtari911                if (!$data) continue;
2183*1d05cddcSAtari911
2184*1d05cddcSAtari911                foreach ($data as $dateKey => $eventList) {
2185*1d05cddcSAtari911                    foreach ($eventList as $event) {
2186*1d05cddcSAtari911                        $events[] = [
2187*1d05cddcSAtari911                            'id' => $event['id'],
2188*1d05cddcSAtari911                            'title' => $event['title'],
2189*1d05cddcSAtari911                            'date' => $dateKey,
2190*1d05cddcSAtari911                            'startTime' => $event['startTime'] ?? null,
2191*1d05cddcSAtari911                            'month' => $month
2192*1d05cddcSAtari911                        ];
2193*1d05cddcSAtari911                    }
2194*1d05cddcSAtari911                }
2195*1d05cddcSAtari911            }
2196*1d05cddcSAtari911
2197*1d05cddcSAtari911            // Add if it has JSON files (even if empty)
2198*1d05cddcSAtari911            if ($hasFiles) {
2199*1d05cddcSAtari911                $result[''] = ['events' => $events];
2200*1d05cddcSAtari911            }
2201*1d05cddcSAtari911        }
2202*1d05cddcSAtari911
2203*1d05cddcSAtari911        // Recursively scan all namespace directories including sub-namespaces
2204*1d05cddcSAtari911        $this->scanNamespaceRecursive($dataDir, '', $result);
2205*1d05cddcSAtari911
2206*1d05cddcSAtari911        // Sort namespaces, but keep '' (default) first
2207*1d05cddcSAtari911        uksort($result, function($a, $b) {
2208*1d05cddcSAtari911            if ($a === '') return -1;
2209*1d05cddcSAtari911            if ($b === '') return 1;
2210*1d05cddcSAtari911            return strcmp($a, $b);
2211*1d05cddcSAtari911        });
2212*1d05cddcSAtari911
2213*1d05cddcSAtari911        return $result;
2214*1d05cddcSAtari911    }
2215*1d05cddcSAtari911
2216*1d05cddcSAtari911    private function scanNamespaceRecursive($baseDir, $parentNamespace, &$result) {
2217*1d05cddcSAtari911        foreach (glob($baseDir . '*', GLOB_ONLYDIR) as $nsDir) {
2218*1d05cddcSAtari911            $dirName = basename($nsDir);
2219*1d05cddcSAtari911
2220*1d05cddcSAtari911            // Skip the root 'calendar' dir
2221*1d05cddcSAtari911            if ($dirName === 'calendar' && empty($parentNamespace)) continue;
2222*1d05cddcSAtari911
2223*1d05cddcSAtari911            // Build namespace path
2224*1d05cddcSAtari911            $namespace = empty($parentNamespace) ? $dirName : $parentNamespace . ':' . $dirName;
2225*1d05cddcSAtari911
2226*1d05cddcSAtari911            // Check for calendar directory
2227*1d05cddcSAtari911            $calendarDir = $nsDir . '/calendar';
2228*1d05cddcSAtari911            if (is_dir($calendarDir)) {
2229*1d05cddcSAtari911                $hasFiles = false;
2230*1d05cddcSAtari911                $events = [];
2231*1d05cddcSAtari911
2232*1d05cddcSAtari911                // Scan all calendar files
2233*1d05cddcSAtari911                foreach (glob($calendarDir . '/*.json') as $file) {
2234*1d05cddcSAtari911                    $hasFiles = true;
2235*1d05cddcSAtari911                    $month = basename($file, '.json');
2236*1d05cddcSAtari911                    $data = json_decode(file_get_contents($file), true);
2237*1d05cddcSAtari911                    if (!$data) continue;
2238*1d05cddcSAtari911
2239*1d05cddcSAtari911                    foreach ($data as $dateKey => $eventList) {
2240*1d05cddcSAtari911                        foreach ($eventList as $event) {
2241*1d05cddcSAtari911                            $events[] = [
2242*1d05cddcSAtari911                                'id' => $event['id'],
2243*1d05cddcSAtari911                                'title' => $event['title'],
2244*1d05cddcSAtari911                                'date' => $dateKey,
2245*1d05cddcSAtari911                                'startTime' => $event['startTime'] ?? null,
2246*1d05cddcSAtari911                                'month' => $month
2247*1d05cddcSAtari911                            ];
2248*1d05cddcSAtari911                        }
2249*1d05cddcSAtari911                    }
2250*1d05cddcSAtari911                }
2251*1d05cddcSAtari911
2252*1d05cddcSAtari911                // Add namespace if it has JSON files (even if empty)
2253*1d05cddcSAtari911                if ($hasFiles) {
2254*1d05cddcSAtari911                    $result[$namespace] = ['events' => $events];
2255*1d05cddcSAtari911                }
2256*1d05cddcSAtari911            }
2257*1d05cddcSAtari911
2258*1d05cddcSAtari911            // Recursively scan sub-directories
2259*1d05cddcSAtari911            $this->scanNamespaceRecursive($nsDir . '/', $namespace, $result);
2260*1d05cddcSAtari911        }
2261*1d05cddcSAtari911    }
2262*1d05cddcSAtari911
2263*1d05cddcSAtari911    private function getAllNamespaces() {
2264*1d05cddcSAtari911        $dataDir = DOKU_INC . 'data/meta/';
2265*1d05cddcSAtari911        $namespaces = [];
2266*1d05cddcSAtari911
2267*1d05cddcSAtari911        // Check root calendar directory first
2268*1d05cddcSAtari911        $rootCalendarDir = $dataDir . 'calendar';
2269*1d05cddcSAtari911        if (is_dir($rootCalendarDir)) {
2270*1d05cddcSAtari911            $namespaces[] = '';  // Blank/default namespace
2271*1d05cddcSAtari911        }
2272*1d05cddcSAtari911
2273*1d05cddcSAtari911        // Check all other namespace directories
2274*1d05cddcSAtari911        foreach (glob($dataDir . '*', GLOB_ONLYDIR) as $nsDir) {
2275*1d05cddcSAtari911            $namespace = basename($nsDir);
2276*1d05cddcSAtari911
2277*1d05cddcSAtari911            // Skip the root 'calendar' dir (already added as '')
2278*1d05cddcSAtari911            if ($namespace === 'calendar') continue;
2279*1d05cddcSAtari911
2280*1d05cddcSAtari911            $calendarDir = $nsDir . '/calendar';
2281*1d05cddcSAtari911            if (is_dir($calendarDir)) {
2282*1d05cddcSAtari911                $namespaces[] = $namespace;
2283*1d05cddcSAtari911            }
2284*1d05cddcSAtari911        }
2285*1d05cddcSAtari911
2286*1d05cddcSAtari911        return $namespaces;
2287*1d05cddcSAtari911    }
2288*1d05cddcSAtari911
2289*1d05cddcSAtari911    private function searchEvents($search, $filterNamespace) {
2290*1d05cddcSAtari911        $dataDir = DOKU_INC . 'data/meta/';
2291*1d05cddcSAtari911        $results = [];
2292*1d05cddcSAtari911
2293*1d05cddcSAtari911        $search = strtolower(trim($search));
2294*1d05cddcSAtari911
2295*1d05cddcSAtari911        foreach (glob($dataDir . '*', GLOB_ONLYDIR) as $nsDir) {
2296*1d05cddcSAtari911            $namespace = basename($nsDir);
2297*1d05cddcSAtari911            $calendarDir = $nsDir . '/calendar';
2298*1d05cddcSAtari911
2299*1d05cddcSAtari911            if (!is_dir($calendarDir)) continue;
2300*1d05cddcSAtari911            if ($filterNamespace !== '' && $namespace !== $filterNamespace) continue;
2301*1d05cddcSAtari911
2302*1d05cddcSAtari911            foreach (glob($calendarDir . '/*.json') as $file) {
2303*1d05cddcSAtari911                $month = basename($file, '.json');
2304*1d05cddcSAtari911                $data = json_decode(file_get_contents($file), true);
2305*1d05cddcSAtari911                if (!$data) continue;
2306*1d05cddcSAtari911
2307*1d05cddcSAtari911                foreach ($data as $dateKey => $events) {
2308*1d05cddcSAtari911                    foreach ($events as $event) {
2309*1d05cddcSAtari911                        if ($search === '' || strpos(strtolower($event['title']), $search) !== false) {
2310*1d05cddcSAtari911                            $results[] = [
2311*1d05cddcSAtari911                                'id' => $event['id'],
2312*1d05cddcSAtari911                                'title' => $event['title'],
2313*1d05cddcSAtari911                                'date' => $dateKey,
2314*1d05cddcSAtari911                                'startTime' => $event['startTime'] ?? null,
2315*1d05cddcSAtari911                                'namespace' => $event['namespace'] ?? '',
2316*1d05cddcSAtari911                                'month' => $month
2317*1d05cddcSAtari911                            ];
2318*1d05cddcSAtari911                        }
2319*1d05cddcSAtari911                    }
2320*1d05cddcSAtari911                }
2321*1d05cddcSAtari911            }
2322*1d05cddcSAtari911        }
2323*1d05cddcSAtari911
2324*1d05cddcSAtari911        return $results;
2325*1d05cddcSAtari911    }
2326*1d05cddcSAtari911
2327*1d05cddcSAtari911    private function deleteRecurringSeries() {
2328*1d05cddcSAtari911        global $INPUT;
2329*1d05cddcSAtari911
2330*1d05cddcSAtari911        $eventTitle = $INPUT->str('event_title');
2331*1d05cddcSAtari911        $namespace = $INPUT->str('namespace');
2332*1d05cddcSAtari911
2333*1d05cddcSAtari911        // Determine calendar directory
2334*1d05cddcSAtari911        if ($namespace === '') {
2335*1d05cddcSAtari911            $dataDir = DOKU_INC . 'data/meta/calendar';
2336*1d05cddcSAtari911        } else {
2337*1d05cddcSAtari911            $dataDir = DOKU_INC . 'data/meta/' . $namespace . '/calendar';
2338*1d05cddcSAtari911        }
2339*1d05cddcSAtari911
2340*1d05cddcSAtari911        $count = 0;
2341*1d05cddcSAtari911
2342*1d05cddcSAtari911        if (is_dir($dataDir)) {
2343*1d05cddcSAtari911            foreach (glob($dataDir . '/*.json') as $file) {
2344*1d05cddcSAtari911                $data = json_decode(file_get_contents($file), true);
2345*1d05cddcSAtari911                if (!$data) continue;
2346*1d05cddcSAtari911
2347*1d05cddcSAtari911                $modified = false;
2348*1d05cddcSAtari911                foreach ($data as $dateKey => $events) {
2349*1d05cddcSAtari911                    $filtered = [];
2350*1d05cddcSAtari911                    foreach ($events as $event) {
2351*1d05cddcSAtari911                        // Match by title (case-insensitive)
2352*1d05cddcSAtari911                        if (strtolower(trim($event['title'])) === strtolower(trim($eventTitle))) {
2353*1d05cddcSAtari911                            $count++;
2354*1d05cddcSAtari911                            $modified = true;
2355*1d05cddcSAtari911                        } else {
2356*1d05cddcSAtari911                            $filtered[] = $event;
2357*1d05cddcSAtari911                        }
2358*1d05cddcSAtari911                    }
2359*1d05cddcSAtari911                    $data[$dateKey] = $filtered;
2360*1d05cddcSAtari911                }
2361*1d05cddcSAtari911
2362*1d05cddcSAtari911                if ($modified) {
2363*1d05cddcSAtari911                    file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT));
2364*1d05cddcSAtari911                }
2365*1d05cddcSAtari911            }
2366*1d05cddcSAtari911        }
2367*1d05cddcSAtari911
2368*1d05cddcSAtari911        $this->redirect("Deleted $count occurrences of recurring event: " . $eventTitle, 'success', 'manage');
2369*1d05cddcSAtari911    }
2370*1d05cddcSAtari911
2371*1d05cddcSAtari911    private function editRecurringSeries() {
2372*1d05cddcSAtari911        global $INPUT;
2373*1d05cddcSAtari911
2374*1d05cddcSAtari911        $oldTitle = $INPUT->str('old_title');
2375*1d05cddcSAtari911        $oldNamespace = $INPUT->str('old_namespace');
2376*1d05cddcSAtari911        $newTitle = $INPUT->str('new_title');
2377*1d05cddcSAtari911        $startTime = $INPUT->str('start_time');
2378*1d05cddcSAtari911        $endTime = $INPUT->str('end_time');
2379*1d05cddcSAtari911        $interval = $INPUT->int('interval', 0);
2380*1d05cddcSAtari911        $newNamespace = $INPUT->str('new_namespace');
2381*1d05cddcSAtari911
2382*1d05cddcSAtari911        // Use old namespace if new namespace is empty (keep current)
2383*1d05cddcSAtari911        if (empty($newNamespace) && !isset($_POST['new_namespace'])) {
2384*1d05cddcSAtari911            $newNamespace = $oldNamespace;
2385*1d05cddcSAtari911        }
2386*1d05cddcSAtari911
2387*1d05cddcSAtari911        // Determine old calendar directory
2388*1d05cddcSAtari911        if ($oldNamespace === '') {
2389*1d05cddcSAtari911            $oldDataDir = DOKU_INC . 'data/meta/calendar';
2390*1d05cddcSAtari911        } else {
2391*1d05cddcSAtari911            $oldDataDir = DOKU_INC . 'data/meta/' . $oldNamespace . '/calendar';
2392*1d05cddcSAtari911        }
2393*1d05cddcSAtari911
2394*1d05cddcSAtari911        $count = 0;
2395*1d05cddcSAtari911        $eventsToMove = [];
2396*1d05cddcSAtari911
2397*1d05cddcSAtari911        if (is_dir($oldDataDir)) {
2398*1d05cddcSAtari911            foreach (glob($oldDataDir . '/*.json') as $file) {
2399*1d05cddcSAtari911                $data = json_decode(file_get_contents($file), true);
2400*1d05cddcSAtari911                if (!$data) continue;
2401*1d05cddcSAtari911
2402*1d05cddcSAtari911                $modified = false;
2403*1d05cddcSAtari911                foreach ($data as $dateKey => $events) {
2404*1d05cddcSAtari911                    foreach ($events as $key => $event) {
2405*1d05cddcSAtari911                        // Match by old title (case-insensitive)
2406*1d05cddcSAtari911                        if (strtolower(trim($event['title'])) === strtolower(trim($oldTitle))) {
2407*1d05cddcSAtari911                            // Update the title
2408*1d05cddcSAtari911                            $data[$dateKey][$key]['title'] = $newTitle;
2409*1d05cddcSAtari911
2410*1d05cddcSAtari911                            // Update start time if provided
2411*1d05cddcSAtari911                            if (!empty($startTime)) {
2412*1d05cddcSAtari911                                $data[$dateKey][$key]['start'] = $startTime;
2413*1d05cddcSAtari911                            }
2414*1d05cddcSAtari911
2415*1d05cddcSAtari911                            // Update end time if provided
2416*1d05cddcSAtari911                            if (!empty($endTime)) {
2417*1d05cddcSAtari911                                $data[$dateKey][$key]['end'] = $endTime;
2418*1d05cddcSAtari911                            }
2419*1d05cddcSAtari911
2420*1d05cddcSAtari911                            // Update namespace
2421*1d05cddcSAtari911                            $data[$dateKey][$key]['namespace'] = $newNamespace;
2422*1d05cddcSAtari911
2423*1d05cddcSAtari911                            // If changing interval, calculate new date
2424*1d05cddcSAtari911                            if ($interval > 0 && $count > 0) {
2425*1d05cddcSAtari911                                // Get the first event date as base
2426*1d05cddcSAtari911                                if (empty($firstEventDate)) {
2427*1d05cddcSAtari911                                    $firstEventDate = $dateKey;
2428*1d05cddcSAtari911                                }
2429*1d05cddcSAtari911
2430*1d05cddcSAtari911                                // Calculate new date based on interval
2431*1d05cddcSAtari911                                $newDate = date('Y-m-d', strtotime($firstEventDate . ' +' . ($count * $interval) . ' days'));
2432*1d05cddcSAtari911
2433*1d05cddcSAtari911                                // Store for moving
2434*1d05cddcSAtari911                                $eventsToMove[] = [
2435*1d05cddcSAtari911                                    'oldDate' => $dateKey,
2436*1d05cddcSAtari911                                    'newDate' => $newDate,
2437*1d05cddcSAtari911                                    'event' => $data[$dateKey][$key],
2438*1d05cddcSAtari911                                    'key' => $key
2439*1d05cddcSAtari911                                ];
2440*1d05cddcSAtari911                            }
2441*1d05cddcSAtari911
2442*1d05cddcSAtari911                            $count++;
2443*1d05cddcSAtari911                            $modified = true;
2444*1d05cddcSAtari911                        }
2445*1d05cddcSAtari911                    }
2446*1d05cddcSAtari911                }
2447*1d05cddcSAtari911
2448*1d05cddcSAtari911                if ($modified) {
2449*1d05cddcSAtari911                    file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT));
2450*1d05cddcSAtari911                }
2451*1d05cddcSAtari911            }
2452*1d05cddcSAtari911
2453*1d05cddcSAtari911            // Handle interval changes by moving events to new dates
2454*1d05cddcSAtari911            if (!empty($eventsToMove)) {
2455*1d05cddcSAtari911                // Remove from old dates first
2456*1d05cddcSAtari911                foreach (glob($oldDataDir . '/*.json') as $file) {
2457*1d05cddcSAtari911                    $data = json_decode(file_get_contents($file), true);
2458*1d05cddcSAtari911                    if (!$data) continue;
2459*1d05cddcSAtari911
2460*1d05cddcSAtari911                    $modified = false;
2461*1d05cddcSAtari911                    foreach ($eventsToMove as $moveData) {
2462*1d05cddcSAtari911                        $oldMonth = substr($moveData['oldDate'], 0, 7);
2463*1d05cddcSAtari911                        $fileMonth = basename($file, '.json');
2464*1d05cddcSAtari911
2465*1d05cddcSAtari911                        if ($oldMonth === $fileMonth && isset($data[$moveData['oldDate']])) {
2466*1d05cddcSAtari911                            foreach ($data[$moveData['oldDate']] as $k => $evt) {
2467*1d05cddcSAtari911                                if ($evt['id'] === $moveData['event']['id']) {
2468*1d05cddcSAtari911                                    unset($data[$moveData['oldDate']][$k]);
2469*1d05cddcSAtari911                                    $data[$moveData['oldDate']] = array_values($data[$moveData['oldDate']]);
2470*1d05cddcSAtari911                                    $modified = true;
2471*1d05cddcSAtari911                                }
2472*1d05cddcSAtari911                            }
2473*1d05cddcSAtari911                        }
2474*1d05cddcSAtari911                    }
2475*1d05cddcSAtari911
2476*1d05cddcSAtari911                    if ($modified) {
2477*1d05cddcSAtari911                        file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT));
2478*1d05cddcSAtari911                    }
2479*1d05cddcSAtari911                }
2480*1d05cddcSAtari911
2481*1d05cddcSAtari911                // Add to new dates
2482*1d05cddcSAtari911                foreach ($eventsToMove as $moveData) {
2483*1d05cddcSAtari911                    $newMonth = substr($moveData['newDate'], 0, 7);
2484*1d05cddcSAtari911                    $targetDir = ($newNamespace === '') ? DOKU_INC . 'data/meta/calendar' : DOKU_INC . 'data/meta/' . $newNamespace . '/calendar';
2485*1d05cddcSAtari911
2486*1d05cddcSAtari911                    if (!is_dir($targetDir)) {
2487*1d05cddcSAtari911                        mkdir($targetDir, 0755, true);
2488*1d05cddcSAtari911                    }
2489*1d05cddcSAtari911
2490*1d05cddcSAtari911                    $targetFile = $targetDir . '/' . $newMonth . '.json';
2491*1d05cddcSAtari911                    $targetData = file_exists($targetFile) ? json_decode(file_get_contents($targetFile), true) : [];
2492*1d05cddcSAtari911
2493*1d05cddcSAtari911                    if (!isset($targetData[$moveData['newDate']])) {
2494*1d05cddcSAtari911                        $targetData[$moveData['newDate']] = [];
2495*1d05cddcSAtari911                    }
2496*1d05cddcSAtari911
2497*1d05cddcSAtari911                    $targetData[$moveData['newDate']][] = $moveData['event'];
2498*1d05cddcSAtari911                    file_put_contents($targetFile, json_encode($targetData, JSON_PRETTY_PRINT));
2499*1d05cddcSAtari911                }
2500*1d05cddcSAtari911            }
2501*1d05cddcSAtari911
2502*1d05cddcSAtari911            // Handle namespace change without interval change
2503*1d05cddcSAtari911            if ($newNamespace !== $oldNamespace && empty($eventsToMove)) {
2504*1d05cddcSAtari911                foreach (glob($oldDataDir . '/*.json') as $file) {
2505*1d05cddcSAtari911                    $data = json_decode(file_get_contents($file), true);
2506*1d05cddcSAtari911                    if (!$data) continue;
2507*1d05cddcSAtari911
2508*1d05cddcSAtari911                    $month = basename($file, '.json');
2509*1d05cddcSAtari911                    $targetDir = ($newNamespace === '') ? DOKU_INC . 'data/meta/calendar' : DOKU_INC . 'data/meta/' . $newNamespace . '/calendar';
2510*1d05cddcSAtari911
2511*1d05cddcSAtari911                    if (!is_dir($targetDir)) {
2512*1d05cddcSAtari911                        mkdir($targetDir, 0755, true);
2513*1d05cddcSAtari911                    }
2514*1d05cddcSAtari911
2515*1d05cddcSAtari911                    $targetFile = $targetDir . '/' . $month . '.json';
2516*1d05cddcSAtari911                    $targetData = file_exists($targetFile) ? json_decode(file_get_contents($targetFile), true) : [];
2517*1d05cddcSAtari911
2518*1d05cddcSAtari911                    $modified = false;
2519*1d05cddcSAtari911                    foreach ($data as $dateKey => $events) {
2520*1d05cddcSAtari911                        foreach ($events as $k => $event) {
2521*1d05cddcSAtari911                            if (isset($event['namespace']) && $event['namespace'] === $newNamespace &&
2522*1d05cddcSAtari911                                strtolower(trim($event['title'])) === strtolower(trim($newTitle))) {
2523*1d05cddcSAtari911                                // Move this event
2524*1d05cddcSAtari911                                if (!isset($targetData[$dateKey])) {
2525*1d05cddcSAtari911                                    $targetData[$dateKey] = [];
2526*1d05cddcSAtari911                                }
2527*1d05cddcSAtari911                                $targetData[$dateKey][] = $event;
2528*1d05cddcSAtari911                                unset($data[$dateKey][$k]);
2529*1d05cddcSAtari911                                $data[$dateKey] = array_values($data[$dateKey]);
2530*1d05cddcSAtari911                                $modified = true;
2531*1d05cddcSAtari911                            }
2532*1d05cddcSAtari911                        }
2533*1d05cddcSAtari911                    }
2534*1d05cddcSAtari911
2535*1d05cddcSAtari911                    if ($modified) {
2536*1d05cddcSAtari911                        file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT));
2537*1d05cddcSAtari911                        file_put_contents($targetFile, json_encode($targetData, JSON_PRETTY_PRINT));
2538*1d05cddcSAtari911                    }
2539*1d05cddcSAtari911                }
2540*1d05cddcSAtari911            }
2541*1d05cddcSAtari911        }
2542*1d05cddcSAtari911
2543*1d05cddcSAtari911        $changes = [];
2544*1d05cddcSAtari911        if ($oldTitle !== $newTitle) $changes[] = "title";
2545*1d05cddcSAtari911        if (!empty($startTime) || !empty($endTime)) $changes[] = "time";
2546*1d05cddcSAtari911        if ($interval > 0) $changes[] = "interval";
2547*1d05cddcSAtari911        if ($newNamespace !== $oldNamespace) $changes[] = "namespace";
2548*1d05cddcSAtari911
2549*1d05cddcSAtari911        $changeStr = !empty($changes) ? " (" . implode(", ", $changes) . ")" : "";
2550*1d05cddcSAtari911        $this->redirect("Updated $count occurrences of recurring event$changeStr", 'success', 'manage');
2551*1d05cddcSAtari911    }
2552*1d05cddcSAtari911
2553*1d05cddcSAtari911    private function moveEvents() {
2554*1d05cddcSAtari911        global $INPUT;
2555*1d05cddcSAtari911
2556*1d05cddcSAtari911        $events = $INPUT->arr('events');
2557*1d05cddcSAtari911        $targetNamespace = $INPUT->str('target_namespace');
2558*1d05cddcSAtari911
2559*1d05cddcSAtari911        if (empty($events)) {
2560*1d05cddcSAtari911            $this->redirect('No events selected', 'error', 'manage');
2561*1d05cddcSAtari911        }
2562*1d05cddcSAtari911
2563*1d05cddcSAtari911        $moved = 0;
2564*1d05cddcSAtari911
2565*1d05cddcSAtari911        foreach ($events as $eventData) {
2566*1d05cddcSAtari911            list($id, $namespace, $date, $month) = explode('|', $eventData);
2567*1d05cddcSAtari911
2568*1d05cddcSAtari911            // Determine old file path
2569*1d05cddcSAtari911            if ($namespace === '') {
2570*1d05cddcSAtari911                $oldFile = DOKU_INC . 'data/meta/calendar/' . $month . '.json';
2571*1d05cddcSAtari911            } else {
2572*1d05cddcSAtari911                $oldFile = DOKU_INC . 'data/meta/' . $namespace . '/calendar/' . $month . '.json';
2573*1d05cddcSAtari911            }
2574*1d05cddcSAtari911
2575*1d05cddcSAtari911            if (!file_exists($oldFile)) continue;
2576*1d05cddcSAtari911
2577*1d05cddcSAtari911            $oldData = json_decode(file_get_contents($oldFile), true);
2578*1d05cddcSAtari911            if (!$oldData) continue;
2579*1d05cddcSAtari911
2580*1d05cddcSAtari911            // Find and remove event from old file
2581*1d05cddcSAtari911            $event = null;
2582*1d05cddcSAtari911            foreach ($oldData[$date] as $key => $evt) {
2583*1d05cddcSAtari911                if ($evt['id'] === $id) {
2584*1d05cddcSAtari911                    $event = $evt;
2585*1d05cddcSAtari911                    unset($oldData[$date][$key]);
2586*1d05cddcSAtari911                    $oldData[$date] = array_values($oldData[$date]);
2587*1d05cddcSAtari911                    break;
2588*1d05cddcSAtari911                }
2589*1d05cddcSAtari911            }
2590*1d05cddcSAtari911
2591*1d05cddcSAtari911            if (!$event) continue;
2592*1d05cddcSAtari911
2593*1d05cddcSAtari911            // Save old file
2594*1d05cddcSAtari911            file_put_contents($oldFile, json_encode($oldData, JSON_PRETTY_PRINT));
2595*1d05cddcSAtari911
2596*1d05cddcSAtari911            // Update event namespace
2597*1d05cddcSAtari911            $event['namespace'] = $targetNamespace;
2598*1d05cddcSAtari911
2599*1d05cddcSAtari911            // Determine new file path
2600*1d05cddcSAtari911            if ($targetNamespace === '') {
2601*1d05cddcSAtari911                $newFile = DOKU_INC . 'data/meta/calendar/' . $month . '.json';
2602*1d05cddcSAtari911                $newDir = dirname($newFile);
2603*1d05cddcSAtari911            } else {
2604*1d05cddcSAtari911                $newFile = DOKU_INC . 'data/meta/' . $targetNamespace . '/calendar/' . $month . '.json';
2605*1d05cddcSAtari911                $newDir = dirname($newFile);
2606*1d05cddcSAtari911            }
2607*1d05cddcSAtari911
2608*1d05cddcSAtari911            if (!is_dir($newDir)) {
2609*1d05cddcSAtari911                mkdir($newDir, 0755, true);
2610*1d05cddcSAtari911            }
2611*1d05cddcSAtari911
2612*1d05cddcSAtari911            $newData = [];
2613*1d05cddcSAtari911            if (file_exists($newFile)) {
2614*1d05cddcSAtari911                $newData = json_decode(file_get_contents($newFile), true) ?: [];
2615*1d05cddcSAtari911            }
2616*1d05cddcSAtari911
2617*1d05cddcSAtari911            if (!isset($newData[$date])) {
2618*1d05cddcSAtari911                $newData[$date] = [];
2619*1d05cddcSAtari911            }
2620*1d05cddcSAtari911            $newData[$date][] = $event;
2621*1d05cddcSAtari911
2622*1d05cddcSAtari911            file_put_contents($newFile, json_encode($newData, JSON_PRETTY_PRINT));
2623*1d05cddcSAtari911            $moved++;
2624*1d05cddcSAtari911        }
2625*1d05cddcSAtari911
2626*1d05cddcSAtari911        $displayTarget = $targetNamespace ?: '(default)';
2627*1d05cddcSAtari911        $this->redirect("Moved $moved event(s) to namespace: " . $displayTarget, 'success', 'manage');
2628*1d05cddcSAtari911    }
2629*1d05cddcSAtari911
2630*1d05cddcSAtari911    private function moveSingleEvent() {
2631*1d05cddcSAtari911        global $INPUT;
2632*1d05cddcSAtari911
2633*1d05cddcSAtari911        $eventData = $INPUT->str('event');
2634*1d05cddcSAtari911        $targetNamespace = $INPUT->str('target_namespace');
2635*1d05cddcSAtari911
2636*1d05cddcSAtari911        list($id, $namespace, $date, $month) = explode('|', $eventData);
2637*1d05cddcSAtari911
2638*1d05cddcSAtari911        // Determine old file path
2639*1d05cddcSAtari911        if ($namespace === '') {
2640*1d05cddcSAtari911            $oldFile = DOKU_INC . 'data/meta/calendar/' . $month . '.json';
2641*1d05cddcSAtari911        } else {
2642*1d05cddcSAtari911            $oldFile = DOKU_INC . 'data/meta/' . $namespace . '/calendar/' . $month . '.json';
2643*1d05cddcSAtari911        }
2644*1d05cddcSAtari911
2645*1d05cddcSAtari911        if (!file_exists($oldFile)) {
2646*1d05cddcSAtari911            $this->redirect('Event file not found', 'error', 'manage');
2647*1d05cddcSAtari911        }
2648*1d05cddcSAtari911
2649*1d05cddcSAtari911        $oldData = json_decode(file_get_contents($oldFile), true);
2650*1d05cddcSAtari911        if (!$oldData) {
2651*1d05cddcSAtari911            $this->redirect('Could not read event file', 'error', 'manage');
2652*1d05cddcSAtari911        }
2653*1d05cddcSAtari911
2654*1d05cddcSAtari911        // Find and remove event from old file
2655*1d05cddcSAtari911        $event = null;
2656*1d05cddcSAtari911        foreach ($oldData[$date] as $key => $evt) {
2657*1d05cddcSAtari911            if ($evt['id'] === $id) {
2658*1d05cddcSAtari911                $event = $evt;
2659*1d05cddcSAtari911                unset($oldData[$date][$key]);
2660*1d05cddcSAtari911                $oldData[$date] = array_values($oldData[$date]);
2661*1d05cddcSAtari911                break;
2662*1d05cddcSAtari911            }
2663*1d05cddcSAtari911        }
2664*1d05cddcSAtari911
2665*1d05cddcSAtari911        if (!$event) {
2666*1d05cddcSAtari911            $this->redirect('Event not found', 'error', 'manage');
2667*1d05cddcSAtari911        }
2668*1d05cddcSAtari911
2669*1d05cddcSAtari911        // Save old file
2670*1d05cddcSAtari911        file_put_contents($oldFile, json_encode($oldData, JSON_PRETTY_PRINT));
2671*1d05cddcSAtari911
2672*1d05cddcSAtari911        // Update event namespace
2673*1d05cddcSAtari911        $event['namespace'] = $targetNamespace;
2674*1d05cddcSAtari911
2675*1d05cddcSAtari911        // Determine new file path
2676*1d05cddcSAtari911        if ($targetNamespace === '') {
2677*1d05cddcSAtari911            $newFile = DOKU_INC . 'data/meta/calendar/' . $month . '.json';
2678*1d05cddcSAtari911            $newDir = dirname($newFile);
2679*1d05cddcSAtari911        } else {
2680*1d05cddcSAtari911            $newFile = DOKU_INC . 'data/meta/' . $targetNamespace . '/calendar/' . $month . '.json';
2681*1d05cddcSAtari911            $newDir = dirname($newFile);
2682*1d05cddcSAtari911        }
2683*1d05cddcSAtari911
2684*1d05cddcSAtari911        if (!is_dir($newDir)) {
2685*1d05cddcSAtari911            mkdir($newDir, 0755, true);
2686*1d05cddcSAtari911        }
2687*1d05cddcSAtari911
2688*1d05cddcSAtari911        $newData = [];
2689*1d05cddcSAtari911        if (file_exists($newFile)) {
2690*1d05cddcSAtari911            $newData = json_decode(file_get_contents($newFile), true) ?: [];
2691*1d05cddcSAtari911        }
2692*1d05cddcSAtari911
2693*1d05cddcSAtari911        if (!isset($newData[$date])) {
2694*1d05cddcSAtari911            $newData[$date] = [];
2695*1d05cddcSAtari911        }
2696*1d05cddcSAtari911        $newData[$date][] = $event;
2697*1d05cddcSAtari911
2698*1d05cddcSAtari911        file_put_contents($newFile, json_encode($newData, JSON_PRETTY_PRINT));
2699*1d05cddcSAtari911
2700*1d05cddcSAtari911        $displayTarget = $targetNamespace ?: '(default)';
2701*1d05cddcSAtari911        $this->redirect('Moved "' . $event['title'] . '" to ' . $displayTarget, 'success', 'manage');
2702*1d05cddcSAtari911    }
2703*1d05cddcSAtari911
2704*1d05cddcSAtari911    private function createNamespace() {
2705*1d05cddcSAtari911        global $INPUT;
2706*1d05cddcSAtari911
2707*1d05cddcSAtari911        $namespaceName = $INPUT->str('namespace_name');
2708*1d05cddcSAtari911
2709*1d05cddcSAtari911        // Validate namespace name
2710*1d05cddcSAtari911        if (empty($namespaceName)) {
2711*1d05cddcSAtari911            $this->redirect('Namespace name cannot be empty', 'error', 'manage');
2712*1d05cddcSAtari911        }
2713*1d05cddcSAtari911
2714*1d05cddcSAtari911        if (!preg_match('/^[a-zA-Z0-9_:-]+$/', $namespaceName)) {
2715*1d05cddcSAtari911            $this->redirect('Invalid namespace name. Use only letters, numbers, underscore, hyphen, and colon.', 'error', 'manage');
2716*1d05cddcSAtari911        }
2717*1d05cddcSAtari911
2718*1d05cddcSAtari911        // Convert namespace to directory path
2719*1d05cddcSAtari911        $namespacePath = str_replace(':', '/', $namespaceName);
2720*1d05cddcSAtari911        $calendarDir = DOKU_INC . 'data/meta/' . $namespacePath . '/calendar';
2721*1d05cddcSAtari911
2722*1d05cddcSAtari911        // Check if already exists
2723*1d05cddcSAtari911        if (is_dir($calendarDir)) {
2724*1d05cddcSAtari911            // Check if it has any JSON files
2725*1d05cddcSAtari911            $hasFiles = !empty(glob($calendarDir . '/*.json'));
2726*1d05cddcSAtari911            if ($hasFiles) {
2727*1d05cddcSAtari911                $this->redirect("Namespace '$namespaceName' already exists with events", 'info', 'manage');
2728*1d05cddcSAtari911            }
2729*1d05cddcSAtari911            // If directory exists but empty, continue to create placeholder
2730*1d05cddcSAtari911        }
2731*1d05cddcSAtari911
2732*1d05cddcSAtari911        // Create the directory
2733*1d05cddcSAtari911        if (!is_dir($calendarDir)) {
2734*1d05cddcSAtari911            if (!mkdir($calendarDir, 0755, true)) {
2735*1d05cddcSAtari911                $this->redirect("Failed to create namespace directory", 'error', 'manage');
2736*1d05cddcSAtari911            }
2737*1d05cddcSAtari911        }
2738*1d05cddcSAtari911
2739*1d05cddcSAtari911        // Create a placeholder JSON file with an empty structure for current month
2740*1d05cddcSAtari911        // This ensures the namespace appears in the list immediately
2741*1d05cddcSAtari911        $currentMonth = date('Y-m');
2742*1d05cddcSAtari911        $placeholderFile = $calendarDir . '/' . $currentMonth . '.json';
2743*1d05cddcSAtari911
2744*1d05cddcSAtari911        if (!file_exists($placeholderFile)) {
2745*1d05cddcSAtari911            file_put_contents($placeholderFile, json_encode([], JSON_PRETTY_PRINT));
2746*1d05cddcSAtari911        }
2747*1d05cddcSAtari911
2748*1d05cddcSAtari911        $this->redirect("Created namespace: $namespaceName", 'success', 'manage');
2749*1d05cddcSAtari911    }
2750*1d05cddcSAtari911
2751*1d05cddcSAtari911    private function deleteNamespace() {
2752*1d05cddcSAtari911        global $INPUT;
2753*1d05cddcSAtari911
2754*1d05cddcSAtari911        $namespace = $INPUT->str('namespace');
2755*1d05cddcSAtari911
2756*1d05cddcSAtari911        // Convert namespace to directory path (e.g., "work:projects" → "work/projects")
2757*1d05cddcSAtari911        $namespacePath = str_replace(':', '/', $namespace);
2758*1d05cddcSAtari911
2759*1d05cddcSAtari911        // Determine calendar directory
2760*1d05cddcSAtari911        if ($namespace === '') {
2761*1d05cddcSAtari911            $calendarDir = DOKU_INC . 'data/meta/calendar';
2762*1d05cddcSAtari911            $namespaceDir = null; // Don't delete root
2763*1d05cddcSAtari911        } else {
2764*1d05cddcSAtari911            $calendarDir = DOKU_INC . 'data/meta/' . $namespacePath . '/calendar';
2765*1d05cddcSAtari911            $namespaceDir = DOKU_INC . 'data/meta/' . $namespacePath;
2766*1d05cddcSAtari911        }
2767*1d05cddcSAtari911
2768*1d05cddcSAtari911        // Check if directory exists
2769*1d05cddcSAtari911        if (!is_dir($calendarDir)) {
2770*1d05cddcSAtari911            // Maybe it was never created or already deleted
2771*1d05cddcSAtari911            $this->redirect("Namespace directory not found: $calendarDir", 'error', 'manage');
2772*1d05cddcSAtari911            return;
2773*1d05cddcSAtari911        }
2774*1d05cddcSAtari911
2775*1d05cddcSAtari911        $filesDeleted = 0;
2776*1d05cddcSAtari911        $eventsDeleted = 0;
2777*1d05cddcSAtari911
2778*1d05cddcSAtari911        // Delete all calendar JSON files (including empty ones)
2779*1d05cddcSAtari911        foreach (glob($calendarDir . '/*.json') as $file) {
2780*1d05cddcSAtari911            $data = json_decode(file_get_contents($file), true);
2781*1d05cddcSAtari911            if ($data) {
2782*1d05cddcSAtari911                foreach ($data as $events) {
2783*1d05cddcSAtari911                    $eventsDeleted += count($events);
2784*1d05cddcSAtari911                }
2785*1d05cddcSAtari911            }
2786*1d05cddcSAtari911            unlink($file);
2787*1d05cddcSAtari911            $filesDeleted++;
2788*1d05cddcSAtari911        }
2789*1d05cddcSAtari911
2790*1d05cddcSAtari911        // Delete any other files in calendar directory
2791*1d05cddcSAtari911        foreach (glob($calendarDir . '/*') as $file) {
2792*1d05cddcSAtari911            if (is_file($file)) {
2793*1d05cddcSAtari911                unlink($file);
2794*1d05cddcSAtari911            }
2795*1d05cddcSAtari911        }
2796*1d05cddcSAtari911
2797*1d05cddcSAtari911        // Remove the calendar directory
2798*1d05cddcSAtari911        if ($namespace !== '') {
2799*1d05cddcSAtari911            @rmdir($calendarDir);
2800*1d05cddcSAtari911
2801*1d05cddcSAtari911            // Try to remove parent directories if they're empty
2802*1d05cddcSAtari911            // This handles nested namespaces like work:projects:alpha
2803*1d05cddcSAtari911            $currentDir = dirname($calendarDir);
2804*1d05cddcSAtari911            $metaDir = DOKU_INC . 'data/meta';
2805*1d05cddcSAtari911
2806*1d05cddcSAtari911            while ($currentDir !== $metaDir && $currentDir !== dirname($metaDir)) {
2807*1d05cddcSAtari911                if (is_dir($currentDir)) {
2808*1d05cddcSAtari911                    // Check if directory is empty
2809*1d05cddcSAtari911                    $contents = scandir($currentDir);
2810*1d05cddcSAtari911                    $isEmpty = count($contents) === 2; // Only . and ..
2811*1d05cddcSAtari911
2812*1d05cddcSAtari911                    if ($isEmpty) {
2813*1d05cddcSAtari911                        @rmdir($currentDir);
2814*1d05cddcSAtari911                        $currentDir = dirname($currentDir);
2815*1d05cddcSAtari911                    } else {
2816*1d05cddcSAtari911                        break; // Directory not empty, stop
2817*1d05cddcSAtari911                    }
2818*1d05cddcSAtari911                } else {
2819*1d05cddcSAtari911                    break;
2820*1d05cddcSAtari911                }
2821*1d05cddcSAtari911            }
2822*1d05cddcSAtari911        }
2823*1d05cddcSAtari911
2824*1d05cddcSAtari911        $displayName = $namespace ?: '(default)';
2825*1d05cddcSAtari911        $this->redirect("Deleted namespace '$displayName': $eventsDeleted events in $filesDeleted files", 'success', 'manage');
2826*1d05cddcSAtari911    }
2827*1d05cddcSAtari911
2828*1d05cddcSAtari911    private function deleteSelectedEvents() {
2829*1d05cddcSAtari911        global $INPUT;
2830*1d05cddcSAtari911
2831*1d05cddcSAtari911        $events = $INPUT->arr('events');
2832*1d05cddcSAtari911
2833*1d05cddcSAtari911        if (empty($events)) {
2834*1d05cddcSAtari911            $this->redirect('No events selected', 'error', 'manage');
2835*1d05cddcSAtari911        }
2836*1d05cddcSAtari911
2837*1d05cddcSAtari911        $deletedCount = 0;
2838*1d05cddcSAtari911
2839*1d05cddcSAtari911        foreach ($events as $eventData) {
2840*1d05cddcSAtari911            list($id, $namespace, $date, $month) = explode('|', $eventData);
2841*1d05cddcSAtari911
2842*1d05cddcSAtari911            // Determine file path
2843*1d05cddcSAtari911            if ($namespace === '') {
2844*1d05cddcSAtari911                $file = DOKU_INC . 'data/meta/calendar/' . $month . '.json';
2845*1d05cddcSAtari911            } else {
2846*1d05cddcSAtari911                $file = DOKU_INC . 'data/meta/' . $namespace . '/calendar/' . $month . '.json';
2847*1d05cddcSAtari911            }
2848*1d05cddcSAtari911
2849*1d05cddcSAtari911            if (!file_exists($file)) continue;
2850*1d05cddcSAtari911
2851*1d05cddcSAtari911            $data = json_decode(file_get_contents($file), true);
2852*1d05cddcSAtari911            if (!$data) continue;
2853*1d05cddcSAtari911
2854*1d05cddcSAtari911            // Find and remove event
2855*1d05cddcSAtari911            if (isset($data[$date])) {
2856*1d05cddcSAtari911                foreach ($data[$date] as $key => $evt) {
2857*1d05cddcSAtari911                    if ($evt['id'] === $id) {
2858*1d05cddcSAtari911                        unset($data[$date][$key]);
2859*1d05cddcSAtari911                        $data[$date] = array_values($data[$date]);
2860*1d05cddcSAtari911                        $deletedCount++;
2861*1d05cddcSAtari911                        break;
2862*1d05cddcSAtari911                    }
2863*1d05cddcSAtari911                }
2864*1d05cddcSAtari911
2865*1d05cddcSAtari911                // Remove empty date arrays
2866*1d05cddcSAtari911                if (empty($data[$date])) {
2867*1d05cddcSAtari911                    unset($data[$date]);
2868*1d05cddcSAtari911                }
2869*1d05cddcSAtari911
2870*1d05cddcSAtari911                // Save file
2871*1d05cddcSAtari911                file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT));
2872*1d05cddcSAtari911            }
2873*1d05cddcSAtari911        }
2874*1d05cddcSAtari911
2875*1d05cddcSAtari911        $this->redirect("Deleted $deletedCount event(s)", 'success', 'manage');
2876*1d05cddcSAtari911    }
2877*1d05cddcSAtari911
2878*1d05cddcSAtari911    private function getCronStatus() {
2879*1d05cddcSAtari911        // Try to read root's crontab first, then current user
2880*1d05cddcSAtari911        $output = [];
2881*1d05cddcSAtari911        exec('sudo crontab -l 2>/dev/null', $output);
2882*1d05cddcSAtari911
2883*1d05cddcSAtari911        // If sudo doesn't work, try current user
2884*1d05cddcSAtari911        if (empty($output)) {
2885*1d05cddcSAtari911            exec('crontab -l 2>/dev/null', $output);
2886*1d05cddcSAtari911        }
2887*1d05cddcSAtari911
2888*1d05cddcSAtari911        // Also check system crontab files
2889*1d05cddcSAtari911        if (empty($output)) {
2890*1d05cddcSAtari911            $cronFiles = [
2891*1d05cddcSAtari911                '/etc/crontab',
2892*1d05cddcSAtari911                '/etc/cron.d/calendar',
2893*1d05cddcSAtari911                '/var/spool/cron/root',
2894*1d05cddcSAtari911                '/var/spool/cron/crontabs/root'
2895*1d05cddcSAtari911            ];
2896*1d05cddcSAtari911
2897*1d05cddcSAtari911            foreach ($cronFiles as $file) {
2898*1d05cddcSAtari911                if (file_exists($file) && is_readable($file)) {
2899*1d05cddcSAtari911                    $content = file_get_contents($file);
2900*1d05cddcSAtari911                    $output = explode("\n", $content);
2901*1d05cddcSAtari911                    break;
2902*1d05cddcSAtari911                }
2903*1d05cddcSAtari911            }
2904*1d05cddcSAtari911        }
2905*1d05cddcSAtari911
2906*1d05cddcSAtari911        // Look for sync_outlook.php in the cron entries
2907*1d05cddcSAtari911        foreach ($output as $line) {
2908*1d05cddcSAtari911            $line = trim($line);
2909*1d05cddcSAtari911
2910*1d05cddcSAtari911            // Skip empty lines and comments
2911*1d05cddcSAtari911            if (empty($line) || $line[0] === '#') continue;
2912*1d05cddcSAtari911
2913*1d05cddcSAtari911            // Check if line contains sync_outlook.php
2914*1d05cddcSAtari911            if (strpos($line, 'sync_outlook.php') !== false) {
2915*1d05cddcSAtari911                // Parse cron expression
2916*1d05cddcSAtari911                // Format: minute hour day month weekday [user] command
2917*1d05cddcSAtari911                $parts = preg_split('/\s+/', $line, 7);
2918*1d05cddcSAtari911
2919*1d05cddcSAtari911                if (count($parts) >= 5) {
2920*1d05cddcSAtari911                    // Determine if this has a user field (system crontab format)
2921*1d05cddcSAtari911                    $hasUser = (count($parts) >= 6 && !preg_match('/^[\/\*]/', $parts[5]));
2922*1d05cddcSAtari911                    $offset = $hasUser ? 1 : 0;
2923*1d05cddcSAtari911
2924*1d05cddcSAtari911                    $frequency = $this->parseCronExpression($parts[0], $parts[1], $parts[2], $parts[3], $parts[4]);
2925*1d05cddcSAtari911                    return [
2926*1d05cddcSAtari911                        'active' => true,
2927*1d05cddcSAtari911                        'frequency' => $frequency,
2928*1d05cddcSAtari911                        'expression' => implode(' ', array_slice($parts, 0, 5)),
2929*1d05cddcSAtari911                        'full_line' => $line
2930*1d05cddcSAtari911                    ];
2931*1d05cddcSAtari911                }
2932*1d05cddcSAtari911            }
2933*1d05cddcSAtari911        }
2934*1d05cddcSAtari911
2935*1d05cddcSAtari911        return ['active' => false, 'frequency' => '', 'expression' => '', 'full_line' => ''];
2936*1d05cddcSAtari911    }
2937*1d05cddcSAtari911
2938*1d05cddcSAtari911    private function parseCronExpression($minute, $hour, $day, $month, $weekday) {
2939*1d05cddcSAtari911        // Parse minute field
2940*1d05cddcSAtari911        if ($minute === '*') {
2941*1d05cddcSAtari911            return 'Runs every minute';
2942*1d05cddcSAtari911        } elseif (strpos($minute, '*/') === 0) {
2943*1d05cddcSAtari911            $interval = substr($minute, 2);
2944*1d05cddcSAtari911            if ($interval == 1) {
2945*1d05cddcSAtari911                return 'Runs every minute';
2946*1d05cddcSAtari911            } elseif ($interval == 5) {
2947*1d05cddcSAtari911                return 'Runs every 5 minutes';
2948*1d05cddcSAtari911            } elseif ($interval == 8) {
2949*1d05cddcSAtari911                return 'Runs every 8 minutes';
2950*1d05cddcSAtari911            } elseif ($interval == 10) {
2951*1d05cddcSAtari911                return 'Runs every 10 minutes';
2952*1d05cddcSAtari911            } elseif ($interval == 15) {
2953*1d05cddcSAtari911                return 'Runs every 15 minutes';
2954*1d05cddcSAtari911            } elseif ($interval == 30) {
2955*1d05cddcSAtari911                return 'Runs every 30 minutes';
2956*1d05cddcSAtari911            } else {
2957*1d05cddcSAtari911                return "Runs every $interval minutes";
2958*1d05cddcSAtari911            }
2959*1d05cddcSAtari911        }
2960*1d05cddcSAtari911
2961*1d05cddcSAtari911        // Parse hour field
2962*1d05cddcSAtari911        if ($hour === '*' && $minute !== '*') {
2963*1d05cddcSAtari911            return 'Runs hourly';
2964*1d05cddcSAtari911        } elseif (strpos($hour, '*/') === 0 && $minute !== '*') {
2965*1d05cddcSAtari911            $interval = substr($hour, 2);
2966*1d05cddcSAtari911            if ($interval == 1) {
2967*1d05cddcSAtari911                return 'Runs every hour';
2968*1d05cddcSAtari911            } else {
2969*1d05cddcSAtari911                return "Runs every $interval hours";
2970*1d05cddcSAtari911            }
2971*1d05cddcSAtari911        }
2972*1d05cddcSAtari911
2973*1d05cddcSAtari911        // Parse day field
2974*1d05cddcSAtari911        if ($day === '*' && $hour !== '*' && $minute !== '*') {
2975*1d05cddcSAtari911            return 'Runs daily';
2976*1d05cddcSAtari911        }
2977*1d05cddcSAtari911
2978*1d05cddcSAtari911        // Default
2979*1d05cddcSAtari911        return 'Custom schedule';
2980*1d05cddcSAtari911    }
2981*1d05cddcSAtari911
2982*1d05cddcSAtari911    private function runSync() {
2983*1d05cddcSAtari911        global $INPUT;
2984*1d05cddcSAtari911
2985*1d05cddcSAtari911        if ($INPUT->str('call') === 'ajax') {
2986*1d05cddcSAtari911            header('Content-Type: application/json');
2987*1d05cddcSAtari911
2988*1d05cddcSAtari911            $syncScript = DOKU_PLUGIN . 'calendar/sync_outlook.php';
2989*1d05cddcSAtari911            $abortFile = DOKU_PLUGIN . 'calendar/.sync_abort';
2990*1d05cddcSAtari911
2991*1d05cddcSAtari911            // Remove any existing abort flag
2992*1d05cddcSAtari911            if (file_exists($abortFile)) {
2993*1d05cddcSAtari911                @unlink($abortFile);
2994*1d05cddcSAtari911            }
2995*1d05cddcSAtari911
2996*1d05cddcSAtari911            if (!file_exists($syncScript)) {
2997*1d05cddcSAtari911                echo json_encode(['success' => false, 'message' => 'Sync script not found at: ' . $syncScript]);
2998*1d05cddcSAtari911                exit;
2999*1d05cddcSAtari911            }
3000*1d05cddcSAtari911
3001*1d05cddcSAtari911            // Change to plugin directory
3002*1d05cddcSAtari911            $pluginDir = DOKU_PLUGIN . 'calendar';
3003*1d05cddcSAtari911            $logFile = $pluginDir . '/sync.log';
3004*1d05cddcSAtari911
3005*1d05cddcSAtari911            // Ensure log file exists and is writable
3006*1d05cddcSAtari911            if (!file_exists($logFile)) {
3007*1d05cddcSAtari911                @touch($logFile);
3008*1d05cddcSAtari911                @chmod($logFile, 0666);
3009*1d05cddcSAtari911            }
3010*1d05cddcSAtari911
3011*1d05cddcSAtari911            // Try to log the execution (but don't fail if we can't)
3012*1d05cddcSAtari911            if (is_writable($logFile)) {
3013*1d05cddcSAtari911                $tz = new DateTimeZone('America/Los_Angeles');
3014*1d05cddcSAtari911                $now = new DateTime('now', $tz);
3015*1d05cddcSAtari911                $timestamp = $now->format('Y-m-d H:i:s');
3016*1d05cddcSAtari911                @file_put_contents($logFile, "[$timestamp] [ADMIN] Manual sync triggered via admin panel\n", FILE_APPEND);
3017*1d05cddcSAtari911            }
3018*1d05cddcSAtari911
3019*1d05cddcSAtari911            // Find PHP binary - try multiple methods
3020*1d05cddcSAtari911            $phpPath = $this->findPhpBinary();
3021*1d05cddcSAtari911
3022*1d05cddcSAtari911            // Build command
3023*1d05cddcSAtari911            $command = sprintf(
3024*1d05cddcSAtari911                'cd %s && %s %s 2>&1',
3025*1d05cddcSAtari911                escapeshellarg($pluginDir),
3026*1d05cddcSAtari911                $phpPath,
3027*1d05cddcSAtari911                escapeshellarg(basename($syncScript))
3028*1d05cddcSAtari911            );
3029*1d05cddcSAtari911
3030*1d05cddcSAtari911            // Execute and capture output
3031*1d05cddcSAtari911            $output = [];
3032*1d05cddcSAtari911            $returnCode = 0;
3033*1d05cddcSAtari911            exec($command, $output, $returnCode);
3034*1d05cddcSAtari911
3035*1d05cddcSAtari911            // Check if sync completed
3036*1d05cddcSAtari911            $lastLines = array_slice($output, -5);
3037*1d05cddcSAtari911            $completed = false;
3038*1d05cddcSAtari911            foreach ($lastLines as $line) {
3039*1d05cddcSAtari911                if (strpos($line, 'Sync Complete') !== false || strpos($line, 'Created:') !== false) {
3040*1d05cddcSAtari911                    $completed = true;
3041*1d05cddcSAtari911                    break;
3042*1d05cddcSAtari911                }
3043*1d05cddcSAtari911            }
3044*1d05cddcSAtari911
3045*1d05cddcSAtari911            if ($returnCode === 0 && $completed) {
3046*1d05cddcSAtari911                echo json_encode([
3047*1d05cddcSAtari911                    'success' => true,
3048*1d05cddcSAtari911                    'message' => 'Sync completed successfully! Check log below.'
3049*1d05cddcSAtari911                ]);
3050*1d05cddcSAtari911            } elseif ($returnCode === 0) {
3051*1d05cddcSAtari911                echo json_encode([
3052*1d05cddcSAtari911                    'success' => true,
3053*1d05cddcSAtari911                    'message' => 'Sync started. Check log below for progress.'
3054*1d05cddcSAtari911                ]);
3055*1d05cddcSAtari911            } else {
3056*1d05cddcSAtari911                // Include output for debugging
3057*1d05cddcSAtari911                $errorMsg = 'Sync failed with error code: ' . $returnCode;
3058*1d05cddcSAtari911                if (!empty($output)) {
3059*1d05cddcSAtari911                    $errorMsg .= ' | ' . implode(' | ', array_slice($output, -3));
3060*1d05cddcSAtari911                }
3061*1d05cddcSAtari911                echo json_encode([
3062*1d05cddcSAtari911                    'success' => false,
3063*1d05cddcSAtari911                    'message' => $errorMsg
3064*1d05cddcSAtari911                ]);
3065*1d05cddcSAtari911            }
3066*1d05cddcSAtari911            exit;
3067*1d05cddcSAtari911        }
3068*1d05cddcSAtari911    }
3069*1d05cddcSAtari911
3070*1d05cddcSAtari911    private function stopSync() {
3071*1d05cddcSAtari911        global $INPUT;
3072*1d05cddcSAtari911
3073*1d05cddcSAtari911        if ($INPUT->str('call') === 'ajax') {
3074*1d05cddcSAtari911            header('Content-Type: application/json');
3075*1d05cddcSAtari911
3076*1d05cddcSAtari911            $abortFile = DOKU_PLUGIN . 'calendar/.sync_abort';
3077*1d05cddcSAtari911
3078*1d05cddcSAtari911            // Create abort flag file
3079*1d05cddcSAtari911            if (file_put_contents($abortFile, date('Y-m-d H:i:s')) !== false) {
3080*1d05cddcSAtari911                echo json_encode([
3081*1d05cddcSAtari911                    'success' => true,
3082*1d05cddcSAtari911                    'message' => 'Stop signal sent to sync process'
3083*1d05cddcSAtari911                ]);
3084*1d05cddcSAtari911            } else {
3085*1d05cddcSAtari911                echo json_encode([
3086*1d05cddcSAtari911                    'success' => false,
3087*1d05cddcSAtari911                    'message' => 'Failed to create abort flag'
3088*1d05cddcSAtari911                ]);
3089*1d05cddcSAtari911            }
3090*1d05cddcSAtari911            exit;
3091*1d05cddcSAtari911        }
3092*1d05cddcSAtari911    }
3093*1d05cddcSAtari911
3094*1d05cddcSAtari911    private function uploadUpdate() {
3095*1d05cddcSAtari911        if (!isset($_FILES['plugin_zip']) || $_FILES['plugin_zip']['error'] !== UPLOAD_ERR_OK) {
3096*1d05cddcSAtari911            $this->redirect('Upload failed: ' . ($_FILES['plugin_zip']['error'] ?? 'No file uploaded'), 'error', 'update');
3097*1d05cddcSAtari911            return;
3098*1d05cddcSAtari911        }
3099*1d05cddcSAtari911
3100*1d05cddcSAtari911        $uploadedFile = $_FILES['plugin_zip']['tmp_name'];
3101*1d05cddcSAtari911        $pluginDir = DOKU_PLUGIN . 'calendar/';
3102*1d05cddcSAtari911        $backupFirst = isset($_POST['backup_first']);
3103*1d05cddcSAtari911
3104*1d05cddcSAtari911        // Check if plugin directory is writable
3105*1d05cddcSAtari911        if (!is_writable($pluginDir)) {
3106*1d05cddcSAtari911            $this->redirect('Plugin directory is not writable. Please check permissions: ' . $pluginDir, 'error', 'update');
3107*1d05cddcSAtari911            return;
3108*1d05cddcSAtari911        }
3109*1d05cddcSAtari911
3110*1d05cddcSAtari911        // Check if parent directory is writable (for backup and temp files)
3111*1d05cddcSAtari911        if (!is_writable(DOKU_PLUGIN)) {
3112*1d05cddcSAtari911            $this->redirect('Plugin parent directory is not writable. Please check permissions: ' . DOKU_PLUGIN, 'error', 'update');
3113*1d05cddcSAtari911            return;
3114*1d05cddcSAtari911        }
3115*1d05cddcSAtari911
3116*1d05cddcSAtari911        // Verify it's a ZIP file
3117*1d05cddcSAtari911        $finfo = finfo_open(FILEINFO_MIME_TYPE);
3118*1d05cddcSAtari911        $mimeType = finfo_file($finfo, $uploadedFile);
3119*1d05cddcSAtari911        finfo_close($finfo);
3120*1d05cddcSAtari911
3121*1d05cddcSAtari911        if ($mimeType !== 'application/zip' && $mimeType !== 'application/x-zip-compressed') {
3122*1d05cddcSAtari911            $this->redirect('Invalid file type. Please upload a ZIP file.', 'error', 'update');
3123*1d05cddcSAtari911            return;
3124*1d05cddcSAtari911        }
3125*1d05cddcSAtari911
3126*1d05cddcSAtari911        // Create backup if requested
3127*1d05cddcSAtari911        if ($backupFirst) {
3128*1d05cddcSAtari911            // Get current version
3129*1d05cddcSAtari911            $pluginInfo = $pluginDir . 'plugin.info.txt';
3130*1d05cddcSAtari911            $version = 'unknown';
3131*1d05cddcSAtari911            if (file_exists($pluginInfo)) {
3132*1d05cddcSAtari911                $info = confToHash($pluginInfo);
3133*1d05cddcSAtari911                $version = $info['version'] ?? ($info['date'] ?? 'unknown');
3134*1d05cddcSAtari911            }
3135*1d05cddcSAtari911
3136*1d05cddcSAtari911            $backupName = 'calendar.backup.v' . $version . '.' . date('Y-m-d_H-i-s') . '.zip';
3137*1d05cddcSAtari911            $backupPath = DOKU_PLUGIN . $backupName;
3138*1d05cddcSAtari911
3139*1d05cddcSAtari911            try {
3140*1d05cddcSAtari911                $zip = new ZipArchive();
3141*1d05cddcSAtari911                if ($zip->open($backupPath, ZipArchive::CREATE) === TRUE) {
3142*1d05cddcSAtari911                    $this->addDirectoryToZip($zip, $pluginDir, 'calendar/');
3143*1d05cddcSAtari911                    $zip->close();
3144*1d05cddcSAtari911                } else {
3145*1d05cddcSAtari911                    $this->redirect('Failed to create backup ZIP file', 'error', 'update');
3146*1d05cddcSAtari911                    return;
3147*1d05cddcSAtari911                }
3148*1d05cddcSAtari911            } catch (Exception $e) {
3149*1d05cddcSAtari911                $this->redirect('Backup failed: ' . $e->getMessage(), 'error', 'update');
3150*1d05cddcSAtari911                return;
3151*1d05cddcSAtari911            }
3152*1d05cddcSAtari911        }
3153*1d05cddcSAtari911
3154*1d05cddcSAtari911        // Extract uploaded ZIP
3155*1d05cddcSAtari911        $zip = new ZipArchive();
3156*1d05cddcSAtari911        if ($zip->open($uploadedFile) !== TRUE) {
3157*1d05cddcSAtari911            $this->redirect('Failed to open ZIP file', 'error', 'update');
3158*1d05cddcSAtari911            return;
3159*1d05cddcSAtari911        }
3160*1d05cddcSAtari911
3161*1d05cddcSAtari911        // Check if ZIP contains calendar folder
3162*1d05cddcSAtari911        $hasCalendarFolder = false;
3163*1d05cddcSAtari911        for ($i = 0; $i < $zip->numFiles; $i++) {
3164*1d05cddcSAtari911            $filename = $zip->getNameIndex($i);
3165*1d05cddcSAtari911            if (strpos($filename, 'calendar/') === 0) {
3166*1d05cddcSAtari911                $hasCalendarFolder = true;
3167*1d05cddcSAtari911                break;
3168*1d05cddcSAtari911            }
3169*1d05cddcSAtari911        }
3170*1d05cddcSAtari911
3171*1d05cddcSAtari911        // Extract to temp directory first
3172*1d05cddcSAtari911        $tempDir = DOKU_PLUGIN . 'calendar_update_temp/';
3173*1d05cddcSAtari911        if (is_dir($tempDir)) {
3174*1d05cddcSAtari911            $this->deleteDirectory($tempDir);
3175*1d05cddcSAtari911        }
3176*1d05cddcSAtari911        mkdir($tempDir);
3177*1d05cddcSAtari911
3178*1d05cddcSAtari911        $zip->extractTo($tempDir);
3179*1d05cddcSAtari911        $zip->close();
3180*1d05cddcSAtari911
3181*1d05cddcSAtari911        // Determine source directory
3182*1d05cddcSAtari911        if ($hasCalendarFolder) {
3183*1d05cddcSAtari911            $sourceDir = $tempDir . 'calendar/';
3184*1d05cddcSAtari911        } else {
3185*1d05cddcSAtari911            $sourceDir = $tempDir;
3186*1d05cddcSAtari911        }
3187*1d05cddcSAtari911
3188*1d05cddcSAtari911        // Preserve configuration files
3189*1d05cddcSAtari911        $preserveFiles = ['sync_config.php', 'sync_state.json', 'sync.log'];
3190*1d05cddcSAtari911        $preserved = [];
3191*1d05cddcSAtari911        foreach ($preserveFiles as $file) {
3192*1d05cddcSAtari911            $oldFile = $pluginDir . $file;
3193*1d05cddcSAtari911            if (file_exists($oldFile)) {
3194*1d05cddcSAtari911                $preserved[$file] = file_get_contents($oldFile);
3195*1d05cddcSAtari911            }
3196*1d05cddcSAtari911        }
3197*1d05cddcSAtari911
3198*1d05cddcSAtari911        // Delete old plugin files (except data files)
3199*1d05cddcSAtari911        $this->deleteDirectoryContents($pluginDir, $preserveFiles);
3200*1d05cddcSAtari911
3201*1d05cddcSAtari911        // Copy new files
3202*1d05cddcSAtari911        $this->recursiveCopy($sourceDir, $pluginDir);
3203*1d05cddcSAtari911
3204*1d05cddcSAtari911        // Restore preserved files
3205*1d05cddcSAtari911        foreach ($preserved as $file => $content) {
3206*1d05cddcSAtari911            file_put_contents($pluginDir . $file, $content);
3207*1d05cddcSAtari911        }
3208*1d05cddcSAtari911
3209*1d05cddcSAtari911        // Update version and date in plugin.info.txt
3210*1d05cddcSAtari911        $pluginInfo = $pluginDir . 'plugin.info.txt';
3211*1d05cddcSAtari911        if (file_exists($pluginInfo)) {
3212*1d05cddcSAtari911            $info = confToHash($pluginInfo);
3213*1d05cddcSAtari911
3214*1d05cddcSAtari911            // Get new version from uploaded plugin
3215*1d05cddcSAtari911            $newVersion = $info['version'] ?? 'unknown';
3216*1d05cddcSAtari911
3217*1d05cddcSAtari911            // Update date to current
3218*1d05cddcSAtari911            $info['date'] = date('Y-m-d');
3219*1d05cddcSAtari911
3220*1d05cddcSAtari911            // Write updated info back
3221*1d05cddcSAtari911            $lines = [];
3222*1d05cddcSAtari911            foreach ($info as $key => $value) {
3223*1d05cddcSAtari911                $lines[] = str_pad($key, 8) . ' ' . $value;
3224*1d05cddcSAtari911            }
3225*1d05cddcSAtari911            file_put_contents($pluginInfo, implode("\n", $lines) . "\n");
3226*1d05cddcSAtari911        }
3227*1d05cddcSAtari911
3228*1d05cddcSAtari911        // Cleanup temp directory
3229*1d05cddcSAtari911        $this->deleteDirectory($tempDir);
3230*1d05cddcSAtari911
3231*1d05cddcSAtari911        $message = 'Plugin updated successfully!';
3232*1d05cddcSAtari911        if ($backupFirst) {
3233*1d05cddcSAtari911            $message .= ' Backup saved as: ' . $backupName;
3234*1d05cddcSAtari911        }
3235*1d05cddcSAtari911        $this->redirect($message, 'success', 'update');
3236*1d05cddcSAtari911    }
3237*1d05cddcSAtari911
3238*1d05cddcSAtari911    private function deleteBackup() {
3239*1d05cddcSAtari911        global $INPUT;
3240*1d05cddcSAtari911
3241*1d05cddcSAtari911        $filename = $INPUT->str('backup_file');
3242*1d05cddcSAtari911
3243*1d05cddcSAtari911        if (empty($filename)) {
3244*1d05cddcSAtari911            $this->redirect('No backup file specified', 'error', 'update');
3245*1d05cddcSAtari911            return;
3246*1d05cddcSAtari911        }
3247*1d05cddcSAtari911
3248*1d05cddcSAtari911        // Security: only allow files starting with "calendar" and ending with .zip, no directory traversal
3249*1d05cddcSAtari911        if (!preg_match('/^calendar[a-zA-Z0-9._-]*\.zip$/', $filename)) {
3250*1d05cddcSAtari911            $this->redirect('Invalid backup filename', 'error', 'update');
3251*1d05cddcSAtari911            return;
3252*1d05cddcSAtari911        }
3253*1d05cddcSAtari911
3254*1d05cddcSAtari911        $backupPath = DOKU_PLUGIN . $filename;
3255*1d05cddcSAtari911
3256*1d05cddcSAtari911        if (!file_exists($backupPath)) {
3257*1d05cddcSAtari911            $this->redirect('Backup file not found', 'error', 'update');
3258*1d05cddcSAtari911            return;
3259*1d05cddcSAtari911        }
3260*1d05cddcSAtari911
3261*1d05cddcSAtari911        if (@unlink($backupPath)) {
3262*1d05cddcSAtari911            $this->redirect('Backup deleted: ' . $filename, 'success', 'update');
3263*1d05cddcSAtari911        } else {
3264*1d05cddcSAtari911            $this->redirect('Failed to delete backup. Check file permissions.', 'error', 'update');
3265*1d05cddcSAtari911        }
3266*1d05cddcSAtari911    }
3267*1d05cddcSAtari911
3268*1d05cddcSAtari911    private function renameBackup() {
3269*1d05cddcSAtari911        global $INPUT;
3270*1d05cddcSAtari911
3271*1d05cddcSAtari911        $oldName = $INPUT->str('old_name');
3272*1d05cddcSAtari911        $newName = $INPUT->str('new_name');
3273*1d05cddcSAtari911
3274*1d05cddcSAtari911        if (empty($oldName) || empty($newName)) {
3275*1d05cddcSAtari911            $this->redirect('Missing filename(s)', 'error', 'update');
3276*1d05cddcSAtari911            return;
3277*1d05cddcSAtari911        }
3278*1d05cddcSAtari911
3279*1d05cddcSAtari911        // Security: validate filenames
3280*1d05cddcSAtari911        if (!preg_match('/^[a-zA-Z0-9._-]+\.zip$/', $oldName) || !preg_match('/^[a-zA-Z0-9._-]+\.zip$/', $newName)) {
3281*1d05cddcSAtari911            $this->redirect('Invalid filename format', 'error', 'update');
3282*1d05cddcSAtari911            return;
3283*1d05cddcSAtari911        }
3284*1d05cddcSAtari911
3285*1d05cddcSAtari911        $oldPath = DOKU_PLUGIN . $oldName;
3286*1d05cddcSAtari911        $newPath = DOKU_PLUGIN . $newName;
3287*1d05cddcSAtari911
3288*1d05cddcSAtari911        if (!file_exists($oldPath)) {
3289*1d05cddcSAtari911            $this->redirect('Backup file not found', 'error', 'update');
3290*1d05cddcSAtari911            return;
3291*1d05cddcSAtari911        }
3292*1d05cddcSAtari911
3293*1d05cddcSAtari911        if (file_exists($newPath)) {
3294*1d05cddcSAtari911            $this->redirect('A file with the new name already exists', 'error', 'update');
3295*1d05cddcSAtari911            return;
3296*1d05cddcSAtari911        }
3297*1d05cddcSAtari911
3298*1d05cddcSAtari911        if (@rename($oldPath, $newPath)) {
3299*1d05cddcSAtari911            $this->redirect('Backup renamed: ' . $oldName . ' → ' . $newName, 'success', 'update');
3300*1d05cddcSAtari911        } else {
3301*1d05cddcSAtari911            $this->redirect('Failed to rename backup. Check file permissions.', 'error', 'update');
3302*1d05cddcSAtari911        }
3303*1d05cddcSAtari911    }
3304*1d05cddcSAtari911
3305*1d05cddcSAtari911    private function restoreBackup() {
3306*1d05cddcSAtari911        global $INPUT;
3307*1d05cddcSAtari911
3308*1d05cddcSAtari911        $filename = $INPUT->str('backup_file');
3309*1d05cddcSAtari911
3310*1d05cddcSAtari911        if (empty($filename)) {
3311*1d05cddcSAtari911            $this->redirect('No backup file specified', 'error', 'update');
3312*1d05cddcSAtari911            return;
3313*1d05cddcSAtari911        }
3314*1d05cddcSAtari911
3315*1d05cddcSAtari911        // Security: only allow files starting with "calendar" and ending with .zip, no directory traversal
3316*1d05cddcSAtari911        if (!preg_match('/^calendar[a-zA-Z0-9._-]*\.zip$/', $filename)) {
3317*1d05cddcSAtari911            $this->redirect('Invalid backup filename', 'error', 'update');
3318*1d05cddcSAtari911            return;
3319*1d05cddcSAtari911        }
3320*1d05cddcSAtari911
3321*1d05cddcSAtari911        $backupPath = DOKU_PLUGIN . $filename;
3322*1d05cddcSAtari911        $pluginDir = DOKU_PLUGIN . 'calendar/';
3323*1d05cddcSAtari911
3324*1d05cddcSAtari911        if (!file_exists($backupPath)) {
3325*1d05cddcSAtari911            $this->redirect('Backup file not found', 'error', 'update');
3326*1d05cddcSAtari911            return;
3327*1d05cddcSAtari911        }
3328*1d05cddcSAtari911
3329*1d05cddcSAtari911        // Check if plugin directory is writable
3330*1d05cddcSAtari911        if (!is_writable($pluginDir)) {
3331*1d05cddcSAtari911            $this->redirect('Plugin directory is not writable. Please check permissions.', 'error', 'update');
3332*1d05cddcSAtari911            return;
3333*1d05cddcSAtari911        }
3334*1d05cddcSAtari911
3335*1d05cddcSAtari911        // Extract backup to temp directory
3336*1d05cddcSAtari911        $tempDir = DOKU_PLUGIN . 'calendar_restore_temp/';
3337*1d05cddcSAtari911        if (is_dir($tempDir)) {
3338*1d05cddcSAtari911            $this->deleteDirectory($tempDir);
3339*1d05cddcSAtari911        }
3340*1d05cddcSAtari911        mkdir($tempDir);
3341*1d05cddcSAtari911
3342*1d05cddcSAtari911        $zip = new ZipArchive();
3343*1d05cddcSAtari911        if ($zip->open($backupPath) !== TRUE) {
3344*1d05cddcSAtari911            $this->redirect('Failed to open backup ZIP file', 'error', 'update');
3345*1d05cddcSAtari911            return;
3346*1d05cddcSAtari911        }
3347*1d05cddcSAtari911
3348*1d05cddcSAtari911        $zip->extractTo($tempDir);
3349*1d05cddcSAtari911        $zip->close();
3350*1d05cddcSAtari911
3351*1d05cddcSAtari911        // The backup contains a "calendar/" folder
3352*1d05cddcSAtari911        $sourceDir = $tempDir . 'calendar/';
3353*1d05cddcSAtari911
3354*1d05cddcSAtari911        if (!is_dir($sourceDir)) {
3355*1d05cddcSAtari911            $this->deleteDirectory($tempDir);
3356*1d05cddcSAtari911            $this->redirect('Invalid backup structure', 'error', 'update');
3357*1d05cddcSAtari911            return;
3358*1d05cddcSAtari911        }
3359*1d05cddcSAtari911
3360*1d05cddcSAtari911        // Delete current plugin directory contents
3361*1d05cddcSAtari911        $this->deleteDirectoryContents($pluginDir, []);
3362*1d05cddcSAtari911
3363*1d05cddcSAtari911        // Copy backup files to plugin directory
3364*1d05cddcSAtari911        $this->recursiveCopy($sourceDir, $pluginDir);
3365*1d05cddcSAtari911
3366*1d05cddcSAtari911        // Cleanup temp directory
3367*1d05cddcSAtari911        $this->deleteDirectory($tempDir);
3368*1d05cddcSAtari911
3369*1d05cddcSAtari911        $this->redirect('Plugin restored from backup: ' . $filename, 'success', 'update');
3370*1d05cddcSAtari911    }
3371*1d05cddcSAtari911
3372*1d05cddcSAtari911    private function addDirectoryToZip($zip, $dir, $zipPath = '') {
3373*1d05cddcSAtari911        try {
3374*1d05cddcSAtari911            $files = new RecursiveIteratorIterator(
3375*1d05cddcSAtari911                new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS),
3376*1d05cddcSAtari911                RecursiveIteratorIterator::LEAVES_ONLY
3377*1d05cddcSAtari911            );
3378*1d05cddcSAtari911
3379*1d05cddcSAtari911            foreach ($files as $file) {
3380*1d05cddcSAtari911                if (!$file->isDir()) {
3381*1d05cddcSAtari911                    $filePath = $file->getRealPath();
3382*1d05cddcSAtari911                    if ($filePath && is_readable($filePath)) {
3383*1d05cddcSAtari911                        $relativePath = $zipPath . substr($filePath, strlen($dir));
3384*1d05cddcSAtari911                        $zip->addFile($filePath, $relativePath);
3385*1d05cddcSAtari911                    }
3386*1d05cddcSAtari911                }
3387*1d05cddcSAtari911            }
3388*1d05cddcSAtari911        } catch (Exception $e) {
3389*1d05cddcSAtari911            // Log error but continue - some files might not be readable
3390*1d05cddcSAtari911            error_log('Calendar plugin backup warning: ' . $e->getMessage());
3391*1d05cddcSAtari911        }
3392*1d05cddcSAtari911    }
3393*1d05cddcSAtari911
3394*1d05cddcSAtari911    private function deleteDirectory($dir) {
3395*1d05cddcSAtari911        if (!is_dir($dir)) return;
3396*1d05cddcSAtari911
3397*1d05cddcSAtari911        try {
3398*1d05cddcSAtari911            $files = new RecursiveIteratorIterator(
3399*1d05cddcSAtari911                new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS),
3400*1d05cddcSAtari911                RecursiveIteratorIterator::CHILD_FIRST
3401*1d05cddcSAtari911            );
3402*1d05cddcSAtari911
3403*1d05cddcSAtari911            foreach ($files as $file) {
3404*1d05cddcSAtari911                if ($file->isDir()) {
3405*1d05cddcSAtari911                    @rmdir($file->getRealPath());
3406*1d05cddcSAtari911                } else {
3407*1d05cddcSAtari911                    @unlink($file->getRealPath());
3408*1d05cddcSAtari911                }
3409*1d05cddcSAtari911            }
3410*1d05cddcSAtari911
3411*1d05cddcSAtari911            @rmdir($dir);
3412*1d05cddcSAtari911        } catch (Exception $e) {
3413*1d05cddcSAtari911            error_log('Calendar plugin delete directory error: ' . $e->getMessage());
3414*1d05cddcSAtari911        }
3415*1d05cddcSAtari911    }
3416*1d05cddcSAtari911
3417*1d05cddcSAtari911    private function deleteDirectoryContents($dir, $preserve = []) {
3418*1d05cddcSAtari911        if (!is_dir($dir)) return;
3419*1d05cddcSAtari911
3420*1d05cddcSAtari911        $items = scandir($dir);
3421*1d05cddcSAtari911        foreach ($items as $item) {
3422*1d05cddcSAtari911            if ($item === '.' || $item === '..') continue;
3423*1d05cddcSAtari911            if (in_array($item, $preserve)) continue;
3424*1d05cddcSAtari911
3425*1d05cddcSAtari911            $path = $dir . $item;
3426*1d05cddcSAtari911            if (is_dir($path)) {
3427*1d05cddcSAtari911                $this->deleteDirectory($path);
3428*1d05cddcSAtari911            } else {
3429*1d05cddcSAtari911                unlink($path);
3430*1d05cddcSAtari911            }
3431*1d05cddcSAtari911        }
3432*1d05cddcSAtari911    }
3433*1d05cddcSAtari911
3434*1d05cddcSAtari911    private function recursiveCopy($src, $dst) {
3435*1d05cddcSAtari911        $dir = opendir($src);
3436*1d05cddcSAtari911        @mkdir($dst);
3437*1d05cddcSAtari911
3438*1d05cddcSAtari911        while (($file = readdir($dir)) !== false) {
3439*1d05cddcSAtari911            if ($file !== '.' && $file !== '..') {
3440*1d05cddcSAtari911                if (is_dir($src . '/' . $file)) {
3441*1d05cddcSAtari911                    $this->recursiveCopy($src . '/' . $file, $dst . '/' . $file);
3442*1d05cddcSAtari911                } else {
3443*1d05cddcSAtari911                    copy($src . '/' . $file, $dst . '/' . $file);
3444*1d05cddcSAtari911                }
3445*1d05cddcSAtari911            }
3446*1d05cddcSAtari911        }
3447*1d05cddcSAtari911
3448*1d05cddcSAtari911        closedir($dir);
3449*1d05cddcSAtari911    }
3450*1d05cddcSAtari911
3451*1d05cddcSAtari911    private function formatBytes($bytes) {
3452*1d05cddcSAtari911        if ($bytes >= 1073741824) {
3453*1d05cddcSAtari911            return number_format($bytes / 1073741824, 2) . ' GB';
3454*1d05cddcSAtari911        } elseif ($bytes >= 1048576) {
3455*1d05cddcSAtari911            return number_format($bytes / 1048576, 2) . ' MB';
3456*1d05cddcSAtari911        } elseif ($bytes >= 1024) {
3457*1d05cddcSAtari911            return number_format($bytes / 1024, 2) . ' KB';
3458*1d05cddcSAtari911        } else {
3459*1d05cddcSAtari911            return $bytes . ' bytes';
3460*1d05cddcSAtari911        }
3461*1d05cddcSAtari911    }
3462*1d05cddcSAtari911
3463*1d05cddcSAtari911    private function findPhpBinary() {
3464*1d05cddcSAtari911        // Try PHP_BINARY constant first (most reliable if available)
3465*1d05cddcSAtari911        if (defined('PHP_BINARY') && !empty(PHP_BINARY) && is_executable(PHP_BINARY)) {
3466*1d05cddcSAtari911            return escapeshellarg(PHP_BINARY);
3467*1d05cddcSAtari911        }
3468*1d05cddcSAtari911
3469*1d05cddcSAtari911        // Try common PHP binary locations
3470*1d05cddcSAtari911        $possiblePaths = [
3471*1d05cddcSAtari911            '/usr/bin/php',
3472*1d05cddcSAtari911            '/usr/bin/php8.1',
3473*1d05cddcSAtari911            '/usr/bin/php8.2',
3474*1d05cddcSAtari911            '/usr/bin/php8.3',
3475*1d05cddcSAtari911            '/usr/bin/php7.4',
3476*1d05cddcSAtari911            '/usr/local/bin/php',
3477*1d05cddcSAtari911            'php' // Last resort - rely on PATH
3478*1d05cddcSAtari911        ];
3479*1d05cddcSAtari911
3480*1d05cddcSAtari911        foreach ($possiblePaths as $path) {
3481*1d05cddcSAtari911            // Test if this PHP binary works
3482*1d05cddcSAtari911            $testOutput = [];
3483*1d05cddcSAtari911            $testReturn = 0;
3484*1d05cddcSAtari911            exec($path . ' -v 2>&1', $testOutput, $testReturn);
3485*1d05cddcSAtari911
3486*1d05cddcSAtari911            if ($testReturn === 0) {
3487*1d05cddcSAtari911                return ($path === 'php') ? 'php' : escapeshellarg($path);
3488*1d05cddcSAtari911            }
3489*1d05cddcSAtari911        }
3490*1d05cddcSAtari911
3491*1d05cddcSAtari911        // Fallback to 'php' and hope it's in PATH
3492*1d05cddcSAtari911        return 'php';
3493*1d05cddcSAtari911    }
3494*1d05cddcSAtari911
3495*1d05cddcSAtari911    private function redirect($message, $type = 'success', $tab = null) {
3496*1d05cddcSAtari911        $url = '?do=admin&page=calendar';
3497*1d05cddcSAtari911        if ($tab) {
3498*1d05cddcSAtari911            $url .= '&tab=' . $tab;
3499*1d05cddcSAtari911        }
3500*1d05cddcSAtari911        $url .= '&msg=' . urlencode($message) . '&msgtype=' . $type;
3501*1d05cddcSAtari911        header('Location: ' . $url);
3502*1d05cddcSAtari911        exit;
3503*1d05cddcSAtari911    }
3504*1d05cddcSAtari911
3505*1d05cddcSAtari911    private function getLog() {
3506*1d05cddcSAtari911        global $INPUT;
3507*1d05cddcSAtari911
3508*1d05cddcSAtari911        if ($INPUT->str('call') === 'ajax') {
3509*1d05cddcSAtari911            header('Content-Type: application/json');
3510*1d05cddcSAtari911
3511*1d05cddcSAtari911            $logFile = DOKU_PLUGIN . 'calendar/sync.log';
3512*1d05cddcSAtari911            $log = '';
3513*1d05cddcSAtari911
3514*1d05cddcSAtari911            if (file_exists($logFile)) {
3515*1d05cddcSAtari911                // Get last 500 lines
3516*1d05cddcSAtari911                $lines = file($logFile);
3517*1d05cddcSAtari911                if ($lines !== false) {
3518*1d05cddcSAtari911                    $lines = array_slice($lines, -500);
3519*1d05cddcSAtari911                    $log = implode('', $lines);
3520*1d05cddcSAtari911                }
3521*1d05cddcSAtari911            } else {
3522*1d05cddcSAtari911                $log = "No log file found. Sync hasn't run yet.";
3523*1d05cddcSAtari911            }
3524*1d05cddcSAtari911
3525*1d05cddcSAtari911            echo json_encode(['log' => $log]);
3526*1d05cddcSAtari911            exit;
3527*1d05cddcSAtari911        }
3528*1d05cddcSAtari911    }
3529*1d05cddcSAtari911
3530*1d05cddcSAtari911    private function exportConfig() {
3531*1d05cddcSAtari911        global $INPUT;
3532*1d05cddcSAtari911
3533*1d05cddcSAtari911        if ($INPUT->str('call') === 'ajax') {
3534*1d05cddcSAtari911            header('Content-Type: application/json');
3535*1d05cddcSAtari911
3536*1d05cddcSAtari911            try {
3537*1d05cddcSAtari911                $configFile = DOKU_PLUGIN . 'calendar/sync_config.php';
3538*1d05cddcSAtari911
3539*1d05cddcSAtari911                if (!file_exists($configFile)) {
3540*1d05cddcSAtari911                    echo json_encode([
3541*1d05cddcSAtari911                        'success' => false,
3542*1d05cddcSAtari911                        'message' => 'Config file not found'
3543*1d05cddcSAtari911                    ]);
3544*1d05cddcSAtari911                    exit;
3545*1d05cddcSAtari911                }
3546*1d05cddcSAtari911
3547*1d05cddcSAtari911                // Read config file
3548*1d05cddcSAtari911                $configContent = file_get_contents($configFile);
3549*1d05cddcSAtari911
3550*1d05cddcSAtari911                // Generate encryption key from DokuWiki secret
3551*1d05cddcSAtari911                $key = $this->getEncryptionKey();
3552*1d05cddcSAtari911
3553*1d05cddcSAtari911                // Encrypt config
3554*1d05cddcSAtari911                $encrypted = $this->encryptData($configContent, $key);
3555*1d05cddcSAtari911
3556*1d05cddcSAtari911                echo json_encode([
3557*1d05cddcSAtari911                    'success' => true,
3558*1d05cddcSAtari911                    'encrypted' => $encrypted,
3559*1d05cddcSAtari911                    'message' => 'Config exported successfully'
3560*1d05cddcSAtari911                ]);
3561*1d05cddcSAtari911                exit;
3562*1d05cddcSAtari911
3563*1d05cddcSAtari911            } catch (Exception $e) {
3564*1d05cddcSAtari911                echo json_encode([
3565*1d05cddcSAtari911                    'success' => false,
3566*1d05cddcSAtari911                    'message' => $e->getMessage()
3567*1d05cddcSAtari911                ]);
3568*1d05cddcSAtari911                exit;
3569*1d05cddcSAtari911            }
3570*1d05cddcSAtari911        }
3571*1d05cddcSAtari911    }
3572*1d05cddcSAtari911
3573*1d05cddcSAtari911    private function importConfig() {
3574*1d05cddcSAtari911        global $INPUT;
3575*1d05cddcSAtari911
3576*1d05cddcSAtari911        if ($INPUT->str('call') === 'ajax') {
3577*1d05cddcSAtari911            header('Content-Type: application/json');
3578*1d05cddcSAtari911
3579*1d05cddcSAtari911            try {
3580*1d05cddcSAtari911                $encrypted = $_POST['encrypted_config'] ?? '';
3581*1d05cddcSAtari911
3582*1d05cddcSAtari911                if (empty($encrypted)) {
3583*1d05cddcSAtari911                    echo json_encode([
3584*1d05cddcSAtari911                        'success' => false,
3585*1d05cddcSAtari911                        'message' => 'No config data provided'
3586*1d05cddcSAtari911                    ]);
3587*1d05cddcSAtari911                    exit;
3588*1d05cddcSAtari911                }
3589*1d05cddcSAtari911
3590*1d05cddcSAtari911                // Generate encryption key from DokuWiki secret
3591*1d05cddcSAtari911                $key = $this->getEncryptionKey();
3592*1d05cddcSAtari911
3593*1d05cddcSAtari911                // Decrypt config
3594*1d05cddcSAtari911                $configContent = $this->decryptData($encrypted, $key);
3595*1d05cddcSAtari911
3596*1d05cddcSAtari911                if ($configContent === false) {
3597*1d05cddcSAtari911                    echo json_encode([
3598*1d05cddcSAtari911                        'success' => false,
3599*1d05cddcSAtari911                        'message' => 'Decryption failed. Invalid key or corrupted file.'
3600*1d05cddcSAtari911                    ]);
3601*1d05cddcSAtari911                    exit;
3602*1d05cddcSAtari911                }
3603*1d05cddcSAtari911
3604*1d05cddcSAtari911                // Validate PHP syntax
3605*1d05cddcSAtari911                $valid = @eval('?>' . $configContent);
3606*1d05cddcSAtari911                if ($valid === false) {
3607*1d05cddcSAtari911                    echo json_encode([
3608*1d05cddcSAtari911                        'success' => false,
3609*1d05cddcSAtari911                        'message' => 'Invalid config file format'
3610*1d05cddcSAtari911                    ]);
3611*1d05cddcSAtari911                    exit;
3612*1d05cddcSAtari911                }
3613*1d05cddcSAtari911
3614*1d05cddcSAtari911                // Write to config file
3615*1d05cddcSAtari911                $configFile = DOKU_PLUGIN . 'calendar/sync_config.php';
3616*1d05cddcSAtari911
3617*1d05cddcSAtari911                // Backup existing config
3618*1d05cddcSAtari911                if (file_exists($configFile)) {
3619*1d05cddcSAtari911                    $backupFile = $configFile . '.backup.' . date('Y-m-d_H-i-s');
3620*1d05cddcSAtari911                    copy($configFile, $backupFile);
3621*1d05cddcSAtari911                }
3622*1d05cddcSAtari911
3623*1d05cddcSAtari911                // Write new config
3624*1d05cddcSAtari911                if (file_put_contents($configFile, $configContent) === false) {
3625*1d05cddcSAtari911                    echo json_encode([
3626*1d05cddcSAtari911                        'success' => false,
3627*1d05cddcSAtari911                        'message' => 'Failed to write config file'
3628*1d05cddcSAtari911                    ]);
3629*1d05cddcSAtari911                    exit;
3630*1d05cddcSAtari911                }
3631*1d05cddcSAtari911
3632*1d05cddcSAtari911                echo json_encode([
3633*1d05cddcSAtari911                    'success' => true,
3634*1d05cddcSAtari911                    'message' => 'Config imported successfully'
3635*1d05cddcSAtari911                ]);
3636*1d05cddcSAtari911                exit;
3637*1d05cddcSAtari911
3638*1d05cddcSAtari911            } catch (Exception $e) {
3639*1d05cddcSAtari911                echo json_encode([
3640*1d05cddcSAtari911                    'success' => false,
3641*1d05cddcSAtari911                    'message' => $e->getMessage()
3642*1d05cddcSAtari911                ]);
3643*1d05cddcSAtari911                exit;
3644*1d05cddcSAtari911            }
3645*1d05cddcSAtari911        }
3646*1d05cddcSAtari911    }
3647*1d05cddcSAtari911
3648*1d05cddcSAtari911    private function getEncryptionKey() {
3649*1d05cddcSAtari911        global $conf;
3650*1d05cddcSAtari911        // Use DokuWiki's secret as the base for encryption
3651*1d05cddcSAtari911        // This ensures the key is unique per installation
3652*1d05cddcSAtari911        return hash('sha256', $conf['secret'] . 'calendar_config_encryption', true);
3653*1d05cddcSAtari911    }
3654*1d05cddcSAtari911
3655*1d05cddcSAtari911    private function encryptData($data, $key) {
3656*1d05cddcSAtari911        // Use AES-256-CBC encryption
3657*1d05cddcSAtari911        $ivLength = openssl_cipher_iv_length('aes-256-cbc');
3658*1d05cddcSAtari911        $iv = openssl_random_pseudo_bytes($ivLength);
3659*1d05cddcSAtari911
3660*1d05cddcSAtari911        $encrypted = openssl_encrypt($data, 'aes-256-cbc', $key, 0, $iv);
3661*1d05cddcSAtari911
3662*1d05cddcSAtari911        // Combine IV and encrypted data, then base64 encode
3663*1d05cddcSAtari911        return base64_encode($iv . $encrypted);
3664*1d05cddcSAtari911    }
3665*1d05cddcSAtari911
3666*1d05cddcSAtari911    private function decryptData($encryptedData, $key) {
3667*1d05cddcSAtari911        // Decode base64
3668*1d05cddcSAtari911        $data = base64_decode($encryptedData);
3669*1d05cddcSAtari911
3670*1d05cddcSAtari911        if ($data === false) {
3671*1d05cddcSAtari911            return false;
3672*1d05cddcSAtari911        }
3673*1d05cddcSAtari911
3674*1d05cddcSAtari911        // Extract IV and encrypted content
3675*1d05cddcSAtari911        $ivLength = openssl_cipher_iv_length('aes-256-cbc');
3676*1d05cddcSAtari911        $iv = substr($data, 0, $ivLength);
3677*1d05cddcSAtari911        $encrypted = substr($data, $ivLength);
3678*1d05cddcSAtari911
3679*1d05cddcSAtari911        // Decrypt
3680*1d05cddcSAtari911        $decrypted = openssl_decrypt($encrypted, 'aes-256-cbc', $key, 0, $iv);
3681*1d05cddcSAtari911
3682*1d05cddcSAtari911        return $decrypted;
3683*1d05cddcSAtari911    }
3684*1d05cddcSAtari911
3685*1d05cddcSAtari911    private function clearLogFile() {
3686*1d05cddcSAtari911        global $INPUT;
3687*1d05cddcSAtari911
3688*1d05cddcSAtari911        if ($INPUT->str('call') === 'ajax') {
3689*1d05cddcSAtari911            header('Content-Type: application/json');
3690*1d05cddcSAtari911
3691*1d05cddcSAtari911            $logFile = DOKU_PLUGIN . 'calendar/sync.log';
3692*1d05cddcSAtari911
3693*1d05cddcSAtari911            if (file_exists($logFile)) {
3694*1d05cddcSAtari911                if (file_put_contents($logFile, '')) {
3695*1d05cddcSAtari911                    echo json_encode(['success' => true]);
3696*1d05cddcSAtari911                } else {
3697*1d05cddcSAtari911                    echo json_encode(['success' => false, 'message' => 'Could not clear log file']);
3698*1d05cddcSAtari911                }
3699*1d05cddcSAtari911            } else {
3700*1d05cddcSAtari911                echo json_encode(['success' => true, 'message' => 'No log file to clear']);
3701*1d05cddcSAtari911            }
3702*1d05cddcSAtari911            exit;
3703*1d05cddcSAtari911        }
3704*1d05cddcSAtari911    }
3705*1d05cddcSAtari911
3706*1d05cddcSAtari911    private function downloadLog() {
3707*1d05cddcSAtari911        $logFile = DOKU_PLUGIN . 'calendar/sync.log';
3708*1d05cddcSAtari911
3709*1d05cddcSAtari911        if (file_exists($logFile)) {
3710*1d05cddcSAtari911            header('Content-Type: text/plain');
3711*1d05cddcSAtari911            header('Content-Disposition: attachment; filename="calendar-sync-' . date('Y-m-d-His') . '.log"');
3712*1d05cddcSAtari911            readfile($logFile);
3713*1d05cddcSAtari911            exit;
3714*1d05cddcSAtari911        } else {
3715*1d05cddcSAtari911            echo 'No log file found';
3716*1d05cddcSAtari911            exit;
3717*1d05cddcSAtari911        }
3718*1d05cddcSAtari911    }
3719*1d05cddcSAtari911
3720*1d05cddcSAtari911    private function getEventStatistics() {
3721*1d05cddcSAtari911        $stats = [
3722*1d05cddcSAtari911            'total_events' => 0,
3723*1d05cddcSAtari911            'total_namespaces' => 0,
3724*1d05cddcSAtari911            'total_files' => 0,
3725*1d05cddcSAtari911            'total_recurring' => 0,
3726*1d05cddcSAtari911            'by_namespace' => [],
3727*1d05cddcSAtari911            'last_scan' => ''
3728*1d05cddcSAtari911        ];
3729*1d05cddcSAtari911
3730*1d05cddcSAtari911        $metaDir = DOKU_INC . 'data/meta/';
3731*1d05cddcSAtari911        $cacheFile = DOKU_PLUGIN . 'calendar/.event_stats_cache';
3732*1d05cddcSAtari911
3733*1d05cddcSAtari911        // Check if we have cached stats (less than 5 minutes old)
3734*1d05cddcSAtari911        if (file_exists($cacheFile)) {
3735*1d05cddcSAtari911            $cacheData = json_decode(file_get_contents($cacheFile), true);
3736*1d05cddcSAtari911            if ($cacheData && (time() - $cacheData['timestamp']) < 300) {
3737*1d05cddcSAtari911                return $cacheData['stats'];
3738*1d05cddcSAtari911            }
3739*1d05cddcSAtari911        }
3740*1d05cddcSAtari911
3741*1d05cddcSAtari911        // Scan for events
3742*1d05cddcSAtari911        $this->scanDirectoryForStats($metaDir, '', $stats);
3743*1d05cddcSAtari911
3744*1d05cddcSAtari911        // Count recurring events
3745*1d05cddcSAtari911        $recurringEvents = $this->findRecurringEvents();
3746*1d05cddcSAtari911        $stats['total_recurring'] = count($recurringEvents);
3747*1d05cddcSAtari911
3748*1d05cddcSAtari911        $stats['total_namespaces'] = count($stats['by_namespace']);
3749*1d05cddcSAtari911        $stats['last_scan'] = date('Y-m-d H:i:s');
3750*1d05cddcSAtari911
3751*1d05cddcSAtari911        // Cache the results
3752*1d05cddcSAtari911        file_put_contents($cacheFile, json_encode([
3753*1d05cddcSAtari911            'timestamp' => time(),
3754*1d05cddcSAtari911            'stats' => $stats
3755*1d05cddcSAtari911        ]));
3756*1d05cddcSAtari911
3757*1d05cddcSAtari911        return $stats;
3758*1d05cddcSAtari911    }
3759*1d05cddcSAtari911
3760*1d05cddcSAtari911    private function scanDirectoryForStats($dir, $namespace, &$stats) {
3761*1d05cddcSAtari911        if (!is_dir($dir)) return;
3762*1d05cddcSAtari911
3763*1d05cddcSAtari911        $items = scandir($dir);
3764*1d05cddcSAtari911        foreach ($items as $item) {
3765*1d05cddcSAtari911            if ($item === '.' || $item === '..') continue;
3766*1d05cddcSAtari911
3767*1d05cddcSAtari911            $path = $dir . $item;
3768*1d05cddcSAtari911
3769*1d05cddcSAtari911            // Check if this is a calendar directory
3770*1d05cddcSAtari911            if ($item === 'calendar' && is_dir($path)) {
3771*1d05cddcSAtari911                $jsonFiles = glob($path . '/*.json');
3772*1d05cddcSAtari911                $eventCount = 0;
3773*1d05cddcSAtari911
3774*1d05cddcSAtari911                foreach ($jsonFiles as $file) {
3775*1d05cddcSAtari911                    $stats['total_files']++;
3776*1d05cddcSAtari911                    $data = json_decode(file_get_contents($file), true);
3777*1d05cddcSAtari911                    if ($data) {
3778*1d05cddcSAtari911                        foreach ($data as $dateEvents) {
3779*1d05cddcSAtari911                            $eventCount += count($dateEvents);
3780*1d05cddcSAtari911                        }
3781*1d05cddcSAtari911                    }
3782*1d05cddcSAtari911                }
3783*1d05cddcSAtari911
3784*1d05cddcSAtari911                $stats['total_events'] += $eventCount;
3785*1d05cddcSAtari911
3786*1d05cddcSAtari911                if ($eventCount > 0) {
3787*1d05cddcSAtari911                    $stats['by_namespace'][$namespace] = [
3788*1d05cddcSAtari911                        'events' => $eventCount,
3789*1d05cddcSAtari911                        'files' => count($jsonFiles)
3790*1d05cddcSAtari911                    ];
3791*1d05cddcSAtari911                }
3792*1d05cddcSAtari911            } elseif (is_dir($path)) {
3793*1d05cddcSAtari911                // Recurse into subdirectories
3794*1d05cddcSAtari911                $newNamespace = $namespace ? $namespace . ':' . $item : $item;
3795*1d05cddcSAtari911                $this->scanDirectoryForStats($path . '/', $newNamespace, $stats);
3796*1d05cddcSAtari911            }
3797*1d05cddcSAtari911        }
3798*1d05cddcSAtari911    }
3799*1d05cddcSAtari911
3800*1d05cddcSAtari911    private function rescanEvents() {
3801*1d05cddcSAtari911        // Clear the cache to force a rescan
3802*1d05cddcSAtari911        $cacheFile = DOKU_PLUGIN . 'calendar/.event_stats_cache';
3803*1d05cddcSAtari911        if (file_exists($cacheFile)) {
3804*1d05cddcSAtari911            unlink($cacheFile);
3805*1d05cddcSAtari911        }
3806*1d05cddcSAtari911
3807*1d05cddcSAtari911        // Get fresh statistics
3808*1d05cddcSAtari911        $stats = $this->getEventStatistics();
3809*1d05cddcSAtari911
3810*1d05cddcSAtari911        // Build absolute redirect URL
3811*1d05cddcSAtari911        $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';
3812*1d05cddcSAtari911
3813*1d05cddcSAtari911        // Redirect with success message using absolute URL
3814*1d05cddcSAtari911        header('Location: ' . $redirectUrl, true, 303);
3815*1d05cddcSAtari911        exit;
3816*1d05cddcSAtari911    }
3817*1d05cddcSAtari911
3818*1d05cddcSAtari911    private function exportAllEvents() {
3819*1d05cddcSAtari911        $metaDir = DOKU_INC . 'data/meta/';
3820*1d05cddcSAtari911        $allEvents = [];
3821*1d05cddcSAtari911
3822*1d05cddcSAtari911        // Collect all events
3823*1d05cddcSAtari911        $this->collectAllEvents($metaDir, '', $allEvents);
3824*1d05cddcSAtari911
3825*1d05cddcSAtari911        // Create export package
3826*1d05cddcSAtari911        $exportData = [
3827*1d05cddcSAtari911            'export_date' => date('Y-m-d H:i:s'),
3828*1d05cddcSAtari911            'version' => '3.4.6',
3829*1d05cddcSAtari911            'total_events' => 0,
3830*1d05cddcSAtari911            'namespaces' => []
3831*1d05cddcSAtari911        ];
3832*1d05cddcSAtari911
3833*1d05cddcSAtari911        foreach ($allEvents as $namespace => $files) {
3834*1d05cddcSAtari911            $exportData['namespaces'][$namespace] = [];
3835*1d05cddcSAtari911            foreach ($files as $filename => $events) {
3836*1d05cddcSAtari911                $exportData['namespaces'][$namespace][$filename] = $events;
3837*1d05cddcSAtari911                foreach ($events as $dateEvents) {
3838*1d05cddcSAtari911                    $exportData['total_events'] += count($dateEvents);
3839*1d05cddcSAtari911                }
3840*1d05cddcSAtari911            }
3841*1d05cddcSAtari911        }
3842*1d05cddcSAtari911
3843*1d05cddcSAtari911        // Send as download
3844*1d05cddcSAtari911        header('Content-Type: application/json');
3845*1d05cddcSAtari911        header('Content-Disposition: attachment; filename="calendar-events-export-' . date('Y-m-d-His') . '.json"');
3846*1d05cddcSAtari911        echo json_encode($exportData, JSON_PRETTY_PRINT);
3847*1d05cddcSAtari911        exit;
3848*1d05cddcSAtari911    }
3849*1d05cddcSAtari911
3850*1d05cddcSAtari911    private function collectAllEvents($dir, $namespace, &$allEvents) {
3851*1d05cddcSAtari911        if (!is_dir($dir)) return;
3852*1d05cddcSAtari911
3853*1d05cddcSAtari911        $items = scandir($dir);
3854*1d05cddcSAtari911        foreach ($items as $item) {
3855*1d05cddcSAtari911            if ($item === '.' || $item === '..') continue;
3856*1d05cddcSAtari911
3857*1d05cddcSAtari911            $path = $dir . $item;
3858*1d05cddcSAtari911
3859*1d05cddcSAtari911            // Check if this is a calendar directory
3860*1d05cddcSAtari911            if ($item === 'calendar' && is_dir($path)) {
3861*1d05cddcSAtari911                $jsonFiles = glob($path . '/*.json');
3862*1d05cddcSAtari911
3863*1d05cddcSAtari911                if (!isset($allEvents[$namespace])) {
3864*1d05cddcSAtari911                    $allEvents[$namespace] = [];
3865*1d05cddcSAtari911                }
3866*1d05cddcSAtari911
3867*1d05cddcSAtari911                foreach ($jsonFiles as $file) {
3868*1d05cddcSAtari911                    $filename = basename($file);
3869*1d05cddcSAtari911                    $data = json_decode(file_get_contents($file), true);
3870*1d05cddcSAtari911                    if ($data) {
3871*1d05cddcSAtari911                        $allEvents[$namespace][$filename] = $data;
3872*1d05cddcSAtari911                    }
3873*1d05cddcSAtari911                }
3874*1d05cddcSAtari911            } elseif (is_dir($path)) {
3875*1d05cddcSAtari911                // Recurse into subdirectories
3876*1d05cddcSAtari911                $newNamespace = $namespace ? $namespace . ':' . $item : $item;
3877*1d05cddcSAtari911                $this->collectAllEvents($path . '/', $newNamespace, $allEvents);
3878*1d05cddcSAtari911            }
3879*1d05cddcSAtari911        }
3880*1d05cddcSAtari911    }
3881*1d05cddcSAtari911
3882*1d05cddcSAtari911    private function importAllEvents() {
3883*1d05cddcSAtari911        global $INPUT;
3884*1d05cddcSAtari911
3885*1d05cddcSAtari911        if (!isset($_FILES['import_file'])) {
3886*1d05cddcSAtari911            $redirectUrl = DOKU_URL . 'doku.php?do=admin&page=calendar&tab=manage&msg=' . urlencode('No file uploaded') . '&msgtype=error';
3887*1d05cddcSAtari911            header('Location: ' . $redirectUrl, true, 303);
3888*1d05cddcSAtari911            exit;
3889*1d05cddcSAtari911        }
3890*1d05cddcSAtari911
3891*1d05cddcSAtari911        $file = $_FILES['import_file'];
3892*1d05cddcSAtari911
3893*1d05cddcSAtari911        if ($file['error'] !== UPLOAD_ERR_OK) {
3894*1d05cddcSAtari911            $redirectUrl = DOKU_URL . 'doku.php?do=admin&page=calendar&tab=manage&msg=' . urlencode('Upload error: ' . $file['error']) . '&msgtype=error';
3895*1d05cddcSAtari911            header('Location: ' . $redirectUrl, true, 303);
3896*1d05cddcSAtari911            exit;
3897*1d05cddcSAtari911        }
3898*1d05cddcSAtari911
3899*1d05cddcSAtari911        // Read and decode the import file
3900*1d05cddcSAtari911        $importData = json_decode(file_get_contents($file['tmp_name']), true);
3901*1d05cddcSAtari911
3902*1d05cddcSAtari911        if (!$importData || !isset($importData['namespaces'])) {
3903*1d05cddcSAtari911            $redirectUrl = DOKU_URL . 'doku.php?do=admin&page=calendar&tab=manage&msg=' . urlencode('Invalid import file format') . '&msgtype=error';
3904*1d05cddcSAtari911            header('Location: ' . $redirectUrl, true, 303);
3905*1d05cddcSAtari911            exit;
3906*1d05cddcSAtari911        }
3907*1d05cddcSAtari911
3908*1d05cddcSAtari911        $importedCount = 0;
3909*1d05cddcSAtari911        $mergedCount = 0;
3910*1d05cddcSAtari911
3911*1d05cddcSAtari911        // Import events
3912*1d05cddcSAtari911        foreach ($importData['namespaces'] as $namespace => $files) {
3913*1d05cddcSAtari911            $metaDir = DOKU_INC . 'data/meta/';
3914*1d05cddcSAtari911            if ($namespace) {
3915*1d05cddcSAtari911                $metaDir .= str_replace(':', '/', $namespace) . '/';
3916*1d05cddcSAtari911            }
3917*1d05cddcSAtari911            $calendarDir = $metaDir . 'calendar/';
3918*1d05cddcSAtari911
3919*1d05cddcSAtari911            // Create directory if needed
3920*1d05cddcSAtari911            if (!is_dir($calendarDir)) {
3921*1d05cddcSAtari911                mkdir($calendarDir, 0755, true);
3922*1d05cddcSAtari911            }
3923*1d05cddcSAtari911
3924*1d05cddcSAtari911            foreach ($files as $filename => $events) {
3925*1d05cddcSAtari911                $targetFile = $calendarDir . $filename;
3926*1d05cddcSAtari911
3927*1d05cddcSAtari911                // If file exists, merge events
3928*1d05cddcSAtari911                if (file_exists($targetFile)) {
3929*1d05cddcSAtari911                    $existing = json_decode(file_get_contents($targetFile), true);
3930*1d05cddcSAtari911                    if ($existing) {
3931*1d05cddcSAtari911                        foreach ($events as $date => $dateEvents) {
3932*1d05cddcSAtari911                            if (!isset($existing[$date])) {
3933*1d05cddcSAtari911                                $existing[$date] = [];
3934*1d05cddcSAtari911                            }
3935*1d05cddcSAtari911                            foreach ($dateEvents as $event) {
3936*1d05cddcSAtari911                                // Check if event with same ID exists
3937*1d05cddcSAtari911                                $found = false;
3938*1d05cddcSAtari911                                foreach ($existing[$date] as $existingEvent) {
3939*1d05cddcSAtari911                                    if ($existingEvent['id'] === $event['id']) {
3940*1d05cddcSAtari911                                        $found = true;
3941*1d05cddcSAtari911                                        break;
3942*1d05cddcSAtari911                                    }
3943*1d05cddcSAtari911                                }
3944*1d05cddcSAtari911                                if (!$found) {
3945*1d05cddcSAtari911                                    $existing[$date][] = $event;
3946*1d05cddcSAtari911                                    $importedCount++;
3947*1d05cddcSAtari911                                } else {
3948*1d05cddcSAtari911                                    $mergedCount++;
3949*1d05cddcSAtari911                                }
3950*1d05cddcSAtari911                            }
3951*1d05cddcSAtari911                        }
3952*1d05cddcSAtari911                        file_put_contents($targetFile, json_encode($existing, JSON_PRETTY_PRINT));
3953*1d05cddcSAtari911                    }
3954*1d05cddcSAtari911                } else {
3955*1d05cddcSAtari911                    // New file
3956*1d05cddcSAtari911                    file_put_contents($targetFile, json_encode($events, JSON_PRETTY_PRINT));
3957*1d05cddcSAtari911                    foreach ($events as $dateEvents) {
3958*1d05cddcSAtari911                        $importedCount += count($dateEvents);
3959*1d05cddcSAtari911                    }
3960*1d05cddcSAtari911                }
3961*1d05cddcSAtari911            }
3962*1d05cddcSAtari911        }
3963*1d05cddcSAtari911
3964*1d05cddcSAtari911        // Clear cache
3965*1d05cddcSAtari911        $cacheFile = DOKU_PLUGIN . 'calendar/.event_stats_cache';
3966*1d05cddcSAtari911        if (file_exists($cacheFile)) {
3967*1d05cddcSAtari911            unlink($cacheFile);
3968*1d05cddcSAtari911        }
3969*1d05cddcSAtari911
3970*1d05cddcSAtari911        $message = "Import complete! Imported $importedCount new events";
3971*1d05cddcSAtari911        if ($mergedCount > 0) {
3972*1d05cddcSAtari911            $message .= ", skipped $mergedCount duplicates";
3973*1d05cddcSAtari911        }
3974*1d05cddcSAtari911
3975*1d05cddcSAtari911        $redirectUrl = DOKU_URL . 'doku.php?do=admin&page=calendar&tab=manage&msg=' . urlencode($message) . '&msgtype=success';
3976*1d05cddcSAtari911        header('Location: ' . $redirectUrl, true, 303);
3977*1d05cddcSAtari911        exit;
3978*1d05cddcSAtari911    }
3979*1d05cddcSAtari911
3980*1d05cddcSAtari911    private function previewCleanup() {
3981*1d05cddcSAtari911        global $INPUT;
3982*1d05cddcSAtari911
3983*1d05cddcSAtari911        $cleanupType = $INPUT->str('cleanup_type', 'age');
3984*1d05cddcSAtari911        $namespaceFilter = $INPUT->str('namespace_filter', '');
3985*1d05cddcSAtari911
3986*1d05cddcSAtari911        // Debug info
3987*1d05cddcSAtari911        $debug = [];
3988*1d05cddcSAtari911        $debug['cleanup_type'] = $cleanupType;
3989*1d05cddcSAtari911        $debug['namespace_filter'] = $namespaceFilter;
3990*1d05cddcSAtari911        $debug['age_value'] = $INPUT->int('age_value', 6);
3991*1d05cddcSAtari911        $debug['age_unit'] = $INPUT->str('age_unit', 'months');
3992*1d05cddcSAtari911        $debug['range_start'] = $INPUT->str('range_start', '');
3993*1d05cddcSAtari911        $debug['range_end'] = $INPUT->str('range_end', '');
3994*1d05cddcSAtari911        $debug['delete_completed'] = $INPUT->bool('delete_completed', false);
3995*1d05cddcSAtari911        $debug['delete_past'] = $INPUT->bool('delete_past', false);
3996*1d05cddcSAtari911
3997*1d05cddcSAtari911        $dataDir = DOKU_INC . 'data/meta/';
3998*1d05cddcSAtari911        $debug['data_dir'] = $dataDir;
3999*1d05cddcSAtari911        $debug['data_dir_exists'] = is_dir($dataDir);
4000*1d05cddcSAtari911
4001*1d05cddcSAtari911        $eventsToDelete = $this->findEventsToCleanup($cleanupType, $namespaceFilter);
4002*1d05cddcSAtari911
4003*1d05cddcSAtari911        // Merge with scan debug info
4004*1d05cddcSAtari911        if (isset($this->_cleanupDebug)) {
4005*1d05cddcSAtari911            $debug = array_merge($debug, $this->_cleanupDebug);
4006*1d05cddcSAtari911        }
4007*1d05cddcSAtari911
4008*1d05cddcSAtari911        // Return JSON for preview with debug info
4009*1d05cddcSAtari911        header('Content-Type: application/json');
4010*1d05cddcSAtari911        echo json_encode([
4011*1d05cddcSAtari911            'count' => count($eventsToDelete),
4012*1d05cddcSAtari911            'events' => array_slice($eventsToDelete, 0, 50), // Limit to 50 for preview
4013*1d05cddcSAtari911            'debug' => $debug
4014*1d05cddcSAtari911        ]);
4015*1d05cddcSAtari911        exit;
4016*1d05cddcSAtari911    }
4017*1d05cddcSAtari911
4018*1d05cddcSAtari911    private function cleanupEvents() {
4019*1d05cddcSAtari911        global $INPUT;
4020*1d05cddcSAtari911
4021*1d05cddcSAtari911        $cleanupType = $INPUT->str('cleanup_type', 'age');
4022*1d05cddcSAtari911        $namespaceFilter = $INPUT->str('namespace_filter', '');
4023*1d05cddcSAtari911
4024*1d05cddcSAtari911        // Create backup first
4025*1d05cddcSAtari911        $backupDir = DOKU_PLUGIN . 'calendar/backups/';
4026*1d05cddcSAtari911        if (!is_dir($backupDir)) {
4027*1d05cddcSAtari911            mkdir($backupDir, 0755, true);
4028*1d05cddcSAtari911        }
4029*1d05cddcSAtari911
4030*1d05cddcSAtari911        $backupFile = $backupDir . 'before-cleanup-' . date('Y-m-d-His') . '.zip';
4031*1d05cddcSAtari911        $this->createBackup($backupFile);
4032*1d05cddcSAtari911
4033*1d05cddcSAtari911        // Find events to delete
4034*1d05cddcSAtari911        $eventsToDelete = $this->findEventsToCleanup($cleanupType, $namespaceFilter);
4035*1d05cddcSAtari911        $deletedCount = 0;
4036*1d05cddcSAtari911
4037*1d05cddcSAtari911        // Group by file
4038*1d05cddcSAtari911        $fileGroups = [];
4039*1d05cddcSAtari911        foreach ($eventsToDelete as $evt) {
4040*1d05cddcSAtari911            $fileGroups[$evt['file']][] = $evt;
4041*1d05cddcSAtari911        }
4042*1d05cddcSAtari911
4043*1d05cddcSAtari911        // Delete from each file
4044*1d05cddcSAtari911        foreach ($fileGroups as $file => $events) {
4045*1d05cddcSAtari911            if (!file_exists($file)) continue;
4046*1d05cddcSAtari911
4047*1d05cddcSAtari911            $json = file_get_contents($file);
4048*1d05cddcSAtari911            $data = json_decode($json, true);
4049*1d05cddcSAtari911
4050*1d05cddcSAtari911            if (!$data) continue;
4051*1d05cddcSAtari911
4052*1d05cddcSAtari911            // Remove events
4053*1d05cddcSAtari911            foreach ($events as $evt) {
4054*1d05cddcSAtari911                if (isset($data[$evt['date']])) {
4055*1d05cddcSAtari911                    $data[$evt['date']] = array_filter($data[$evt['date']], function($e) use ($evt) {
4056*1d05cddcSAtari911                        return $e['id'] !== $evt['id'];
4057*1d05cddcSAtari911                    });
4058*1d05cddcSAtari911
4059*1d05cddcSAtari911                    // Remove date key if empty
4060*1d05cddcSAtari911                    if (empty($data[$evt['date']])) {
4061*1d05cddcSAtari911                        unset($data[$evt['date']]);
4062*1d05cddcSAtari911                    }
4063*1d05cddcSAtari911
4064*1d05cddcSAtari911                    $deletedCount++;
4065*1d05cddcSAtari911                }
4066*1d05cddcSAtari911            }
4067*1d05cddcSAtari911
4068*1d05cddcSAtari911            // Save file or delete if empty
4069*1d05cddcSAtari911            if (empty($data)) {
4070*1d05cddcSAtari911                unlink($file);
4071*1d05cddcSAtari911            } else {
4072*1d05cddcSAtari911                file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT));
4073*1d05cddcSAtari911            }
4074*1d05cddcSAtari911        }
4075*1d05cddcSAtari911
4076*1d05cddcSAtari911        // Clear cache
4077*1d05cddcSAtari911        $cacheFile = DOKU_PLUGIN . 'calendar/.event_stats_cache';
4078*1d05cddcSAtari911        if (file_exists($cacheFile)) {
4079*1d05cddcSAtari911            unlink($cacheFile);
4080*1d05cddcSAtari911        }
4081*1d05cddcSAtari911
4082*1d05cddcSAtari911        $message = "Cleanup complete! Deleted $deletedCount event(s). Backup created: " . basename($backupFile);
4083*1d05cddcSAtari911        $redirectUrl = DOKU_URL . 'doku.php?do=admin&page=calendar&tab=manage&msg=' . urlencode($message) . '&msgtype=success';
4084*1d05cddcSAtari911        header('Location: ' . $redirectUrl, true, 303);
4085*1d05cddcSAtari911        exit;
4086*1d05cddcSAtari911    }
4087*1d05cddcSAtari911
4088*1d05cddcSAtari911    private function findEventsToCleanup($cleanupType, $namespaceFilter) {
4089*1d05cddcSAtari911        global $INPUT;
4090*1d05cddcSAtari911
4091*1d05cddcSAtari911        $eventsToDelete = [];
4092*1d05cddcSAtari911        $dataDir = DOKU_INC . 'data/meta/';
4093*1d05cddcSAtari911
4094*1d05cddcSAtari911        $debug = [];
4095*1d05cddcSAtari911        $debug['scanned_dirs'] = [];
4096*1d05cddcSAtari911        $debug['found_files'] = [];
4097*1d05cddcSAtari911
4098*1d05cddcSAtari911        // Calculate cutoff date for age-based cleanup
4099*1d05cddcSAtari911        $cutoffDate = null;
4100*1d05cddcSAtari911        if ($cleanupType === 'age') {
4101*1d05cddcSAtari911            $ageValue = $INPUT->int('age_value', 6);
4102*1d05cddcSAtari911            $ageUnit = $INPUT->str('age_unit', 'months');
4103*1d05cddcSAtari911
4104*1d05cddcSAtari911            if ($ageUnit === 'years') {
4105*1d05cddcSAtari911                $ageValue *= 12; // Convert to months
4106*1d05cddcSAtari911            }
4107*1d05cddcSAtari911
4108*1d05cddcSAtari911            $cutoffDate = date('Y-m-d', strtotime("-$ageValue months"));
4109*1d05cddcSAtari911            $debug['cutoff_date'] = $cutoffDate;
4110*1d05cddcSAtari911        }
4111*1d05cddcSAtari911
4112*1d05cddcSAtari911        // Get date range for range-based cleanup
4113*1d05cddcSAtari911        $rangeStart = $cleanupType === 'range' ? $INPUT->str('range_start', '') : null;
4114*1d05cddcSAtari911        $rangeEnd = $cleanupType === 'range' ? $INPUT->str('range_end', '') : null;
4115*1d05cddcSAtari911
4116*1d05cddcSAtari911        // Get status filters
4117*1d05cddcSAtari911        $deleteCompleted = $cleanupType === 'status' && $INPUT->bool('delete_completed', false);
4118*1d05cddcSAtari911        $deletePast = $cleanupType === 'status' && $INPUT->bool('delete_past', false);
4119*1d05cddcSAtari911
4120*1d05cddcSAtari911        // Check root calendar directory first (blank/default namespace)
4121*1d05cddcSAtari911        $rootCalendarDir = $dataDir . 'calendar';
4122*1d05cddcSAtari911        $debug['root_calendar_dir'] = $rootCalendarDir;
4123*1d05cddcSAtari911        $debug['root_exists'] = is_dir($rootCalendarDir);
4124*1d05cddcSAtari911
4125*1d05cddcSAtari911        if (is_dir($rootCalendarDir)) {
4126*1d05cddcSAtari911            if (!$namespaceFilter || $namespaceFilter === '' || $namespaceFilter === 'default') {
4127*1d05cddcSAtari911                $debug['scanned_dirs'][] = $rootCalendarDir;
4128*1d05cddcSAtari911                $files = glob($rootCalendarDir . '/*.json');
4129*1d05cddcSAtari911                $debug['found_files'] = array_merge($debug['found_files'], $files);
4130*1d05cddcSAtari911                $this->processCalendarFiles($rootCalendarDir, '', $eventsToDelete, $cleanupType, $cutoffDate, $rangeStart, $rangeEnd, $deleteCompleted, $deletePast);
4131*1d05cddcSAtari911            }
4132*1d05cddcSAtari911        }
4133*1d05cddcSAtari911
4134*1d05cddcSAtari911        // Scan all namespace directories
4135*1d05cddcSAtari911        $namespaceDirs = glob($dataDir . '*', GLOB_ONLYDIR);
4136*1d05cddcSAtari911        $debug['namespace_dirs_found'] = $namespaceDirs;
4137*1d05cddcSAtari911
4138*1d05cddcSAtari911        foreach ($namespaceDirs as $nsDir) {
4139*1d05cddcSAtari911            $namespace = basename($nsDir);
4140*1d05cddcSAtari911
4141*1d05cddcSAtari911            // Skip the root 'calendar' dir (already processed above)
4142*1d05cddcSAtari911            if ($namespace === 'calendar') continue;
4143*1d05cddcSAtari911
4144*1d05cddcSAtari911            // Check namespace filter
4145*1d05cddcSAtari911            if ($namespaceFilter && strpos($namespace, $namespaceFilter) === false) {
4146*1d05cddcSAtari911                continue;
4147*1d05cddcSAtari911            }
4148*1d05cddcSAtari911
4149*1d05cddcSAtari911            $calendarDir = $nsDir . '/calendar';
4150*1d05cddcSAtari911            $debug['checked_calendar_dirs'][] = $calendarDir;
4151*1d05cddcSAtari911
4152*1d05cddcSAtari911            if (!is_dir($calendarDir)) {
4153*1d05cddcSAtari911                $debug['missing_calendar_dirs'][] = $calendarDir;
4154*1d05cddcSAtari911                continue;
4155*1d05cddcSAtari911            }
4156*1d05cddcSAtari911
4157*1d05cddcSAtari911            $debug['scanned_dirs'][] = $calendarDir;
4158*1d05cddcSAtari911            $files = glob($calendarDir . '/*.json');
4159*1d05cddcSAtari911            $debug['found_files'] = array_merge($debug['found_files'], $files);
4160*1d05cddcSAtari911            $this->processCalendarFiles($calendarDir, $namespace, $eventsToDelete, $cleanupType, $cutoffDate, $rangeStart, $rangeEnd, $deleteCompleted, $deletePast);
4161*1d05cddcSAtari911        }
4162*1d05cddcSAtari911
4163*1d05cddcSAtari911        // Store debug info globally for preview
4164*1d05cddcSAtari911        $this->_cleanupDebug = $debug;
4165*1d05cddcSAtari911
4166*1d05cddcSAtari911        return $eventsToDelete;
4167*1d05cddcSAtari911    }
4168*1d05cddcSAtari911
4169*1d05cddcSAtari911    private function processCalendarFiles($calendarDir, $namespace, &$eventsToDelete, $cleanupType, $cutoffDate, $rangeStart, $rangeEnd, $deleteCompleted, $deletePast) {
4170*1d05cddcSAtari911        foreach (glob($calendarDir . '/*.json') as $file) {
4171*1d05cddcSAtari911            $json = file_get_contents($file);
4172*1d05cddcSAtari911            $data = json_decode($json, true);
4173*1d05cddcSAtari911
4174*1d05cddcSAtari911            if (!$data) continue;
4175*1d05cddcSAtari911
4176*1d05cddcSAtari911            foreach ($data as $date => $dateEvents) {
4177*1d05cddcSAtari911                foreach ($dateEvents as $event) {
4178*1d05cddcSAtari911                    $shouldDelete = false;
4179*1d05cddcSAtari911
4180*1d05cddcSAtari911                    // Age-based
4181*1d05cddcSAtari911                    if ($cleanupType === 'age' && $cutoffDate && $date < $cutoffDate) {
4182*1d05cddcSAtari911                        $shouldDelete = true;
4183*1d05cddcSAtari911                    }
4184*1d05cddcSAtari911
4185*1d05cddcSAtari911                    // Range-based
4186*1d05cddcSAtari911                    if ($cleanupType === 'range' && $rangeStart && $rangeEnd) {
4187*1d05cddcSAtari911                        if ($date >= $rangeStart && $date <= $rangeEnd) {
4188*1d05cddcSAtari911                            $shouldDelete = true;
4189*1d05cddcSAtari911                        }
4190*1d05cddcSAtari911                    }
4191*1d05cddcSAtari911
4192*1d05cddcSAtari911                    // Status-based
4193*1d05cddcSAtari911                    if ($cleanupType === 'status') {
4194*1d05cddcSAtari911                        $isTask = isset($event['isTask']) && $event['isTask'];
4195*1d05cddcSAtari911                        $isCompleted = isset($event['completed']) && $event['completed'];
4196*1d05cddcSAtari911                        $isPast = $date < date('Y-m-d');
4197*1d05cddcSAtari911
4198*1d05cddcSAtari911                        if ($deleteCompleted && $isTask && $isCompleted) {
4199*1d05cddcSAtari911                            $shouldDelete = true;
4200*1d05cddcSAtari911                        }
4201*1d05cddcSAtari911                        if ($deletePast && !$isTask && $isPast) {
4202*1d05cddcSAtari911                            $shouldDelete = true;
4203*1d05cddcSAtari911                        }
4204*1d05cddcSAtari911                    }
4205*1d05cddcSAtari911
4206*1d05cddcSAtari911                    if ($shouldDelete) {
4207*1d05cddcSAtari911                        $eventsToDelete[] = [
4208*1d05cddcSAtari911                            'id' => $event['id'],
4209*1d05cddcSAtari911                            'title' => $event['title'],
4210*1d05cddcSAtari911                            'date' => $date,
4211*1d05cddcSAtari911                            'namespace' => $namespace ?: 'default',
4212*1d05cddcSAtari911                            'file' => $file
4213*1d05cddcSAtari911                        ];
4214*1d05cddcSAtari911                    }
4215*1d05cddcSAtari911                }
4216*1d05cddcSAtari911            }
4217*1d05cddcSAtari911        }
4218*1d05cddcSAtari911    }
4219*1d05cddcSAtari911}
4220