xref: /plugin/calendar/admin.php (revision 2866e8271e4daef3b32eacb3a9082d02159b592b)
11d05cddcSAtari911<?php
21d05cddcSAtari911/**
31d05cddcSAtari911 * Calendar Plugin - Admin Interface
4815440faSAtari911 *
5815440faSAtari911 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
6815440faSAtari911 * @author  DokuWiki Community
7*2866e827SAtari911 * @version 7.2.6
81d05cddcSAtari911 */
91d05cddcSAtari911
101d05cddcSAtari911if(!defined('DOKU_INC')) die();
111d05cddcSAtari911
12815440faSAtari911// Load class dependencies
13815440faSAtari911require_once __DIR__ . '/classes/FileHandler.php';
14815440faSAtari911require_once __DIR__ . '/classes/EventCache.php';
15815440faSAtari911require_once __DIR__ . '/classes/RateLimiter.php';
16815440faSAtari911require_once __DIR__ . '/classes/EventManager.php';
17815440faSAtari911
181d05cddcSAtari911class admin_plugin_calendar extends DokuWiki_Admin_Plugin {
191d05cddcSAtari911
2096df7d3eSAtari911    /**
21*2866e827SAtari911     * Get the meta directory path (farm-safe)
22*2866e827SAtari911     * Uses $conf['metadir'] instead of hardcoded DOKU_INC . 'data/meta/'
23*2866e827SAtari911     */
24*2866e827SAtari911    private function metaDir() {
25*2866e827SAtari911        global $conf;
26*2866e827SAtari911        return rtrim($conf['metadir'], '/') . '/';
27*2866e827SAtari911    }
28*2866e827SAtari911
29*2866e827SAtari911    /**
30*2866e827SAtari911     * Get sync config file path (farm-safe)
31*2866e827SAtari911     * Checks per-animal metadir first, falls back to shared plugin dir
32*2866e827SAtari911     */
33*2866e827SAtari911    private function syncConfigPath() {
34*2866e827SAtari911        $perAnimal = $this->metaDir() . 'calendar/sync_config.php';
35*2866e827SAtari911        if (file_exists($perAnimal)) return $perAnimal;
36*2866e827SAtari911        return DOKU_PLUGIN . 'calendar/sync_config.php';
37*2866e827SAtari911    }
38*2866e827SAtari911
39*2866e827SAtari911    /**
4096df7d3eSAtari911     * Get the path to the sync log file (in data directory, not plugin directory)
4196df7d3eSAtari911     */
4296df7d3eSAtari911    private function getSyncLogPath() {
43*2866e827SAtari911        $dataDir = $this->metaDir() . 'calendar/';
4496df7d3eSAtari911        if (!is_dir($dataDir)) {
4596df7d3eSAtari911            @mkdir($dataDir, 0755, true);
4696df7d3eSAtari911        }
4796df7d3eSAtari911        return $dataDir . 'sync.log';
4896df7d3eSAtari911    }
4996df7d3eSAtari911
5096df7d3eSAtari911    /**
5196df7d3eSAtari911     * Get the path to the sync state file (in data directory, not plugin directory)
5296df7d3eSAtari911     */
5396df7d3eSAtari911    private function getSyncStatePath() {
54*2866e827SAtari911        $dataDir = $this->metaDir() . 'calendar/';
5596df7d3eSAtari911        if (!is_dir($dataDir)) {
5696df7d3eSAtari911            mkdir($dataDir, 0755, true);
5796df7d3eSAtari911        }
5896df7d3eSAtari911        return $dataDir . 'sync_state.json';
5996df7d3eSAtari911    }
6096df7d3eSAtari911
611d05cddcSAtari911    public function getMenuText($language) {
6222228b0eSAtari911        return $this->getLang('menu');
631d05cddcSAtari911    }
641d05cddcSAtari911
651d05cddcSAtari911    public function getMenuSort() {
661d05cddcSAtari911        return 100;
671d05cddcSAtari911    }
681d05cddcSAtari911
69da206178SAtari911    /**
70da206178SAtari911     * Return the path to the icon for the admin menu
71da206178SAtari911     * @return string path to SVG icon
72da206178SAtari911     */
73da206178SAtari911    public function getMenuIcon() {
74da206178SAtari911        return DOKU_PLUGIN . 'calendar/images/icon.svg';
75da206178SAtari911    }
76da206178SAtari911
771d05cddcSAtari911    public function forAdminOnly() {
781d05cddcSAtari911        return true;
791d05cddcSAtari911    }
801d05cddcSAtari911
817e8ea635SAtari911    /**
827e8ea635SAtari911     * Public entry point for AJAX actions routed from action.php
837e8ea635SAtari911     */
847e8ea635SAtari911    public function handleAjaxAction($action) {
857e8ea635SAtari911        // Verify admin privileges for all admin AJAX actions
867e8ea635SAtari911        if (!auth_isadmin()) {
87da206178SAtari911            echo json_encode(['success' => false, 'error' => 'Admin access required']);
887e8ea635SAtari911            return;
897e8ea635SAtari911        }
907e8ea635SAtari911
917e8ea635SAtari911        switch ($action) {
927e8ea635SAtari911            case 'cleanup_empty_namespaces': $this->handleCleanupEmptyNamespaces(); break;
937e8ea635SAtari911            case 'trim_all_past_recurring': $this->handleTrimAllPastRecurring(); break;
947e8ea635SAtari911            case 'rescan_recurring': $this->handleRescanRecurring(); break;
957e8ea635SAtari911            case 'extend_recurring': $this->handleExtendRecurring(); break;
967e8ea635SAtari911            case 'trim_recurring': $this->handleTrimRecurring(); break;
977e8ea635SAtari911            case 'pause_recurring': $this->handlePauseRecurring(); break;
987e8ea635SAtari911            case 'resume_recurring': $this->handleResumeRecurring(); break;
997e8ea635SAtari911            case 'change_start_recurring': $this->handleChangeStartRecurring(); break;
1007e8ea635SAtari911            case 'change_pattern_recurring': $this->handleChangePatternRecurring(); break;
1017e8ea635SAtari911            default:
102da206178SAtari911                echo json_encode(['success' => false, 'error' => 'Unknown admin action']);
1037e8ea635SAtari911        }
1047e8ea635SAtari911    }
1057e8ea635SAtari911
1061d05cddcSAtari911    public function handle() {
1071d05cddcSAtari911        global $INPUT;
1081d05cddcSAtari911
1091d05cddcSAtari911        $action = $INPUT->str('action');
1101d05cddcSAtari911
111*2866e827SAtari911        // CSRF protection: all POST actions require a valid security token
112*2866e827SAtari911        if ($action && !checkSecurityToken()) {
113*2866e827SAtari911            msg('Security token expired. Please try again.', -1);
114*2866e827SAtari911            return;
115*2866e827SAtari911        }
116*2866e827SAtari911
1171d05cddcSAtari911        if ($action === 'clear_cache') {
1181d05cddcSAtari911            $this->clearCache();
1191d05cddcSAtari911        } elseif ($action === 'save_config') {
1201d05cddcSAtari911            $this->saveConfig();
1211d05cddcSAtari911        } elseif ($action === 'delete_recurring_series') {
1221d05cddcSAtari911            $this->deleteRecurringSeries();
1231d05cddcSAtari911        } elseif ($action === 'edit_recurring_series') {
1241d05cddcSAtari911            $this->editRecurringSeries();
1251d05cddcSAtari911        } elseif ($action === 'move_selected_events') {
1261d05cddcSAtari911            $this->moveEvents();
1271d05cddcSAtari911        } elseif ($action === 'move_single_event') {
1281d05cddcSAtari911            $this->moveSingleEvent();
1291d05cddcSAtari911        } elseif ($action === 'delete_selected_events') {
1301d05cddcSAtari911            $this->deleteSelectedEvents();
1311d05cddcSAtari911        } elseif ($action === 'create_namespace') {
1321d05cddcSAtari911            $this->createNamespace();
1331d05cddcSAtari911        } elseif ($action === 'delete_namespace') {
1341d05cddcSAtari911            $this->deleteNamespace();
1359ccd446eSAtari911        } elseif ($action === 'rename_namespace') {
1369ccd446eSAtari911            $this->renameNamespace();
1371d05cddcSAtari911        } elseif ($action === 'run_sync') {
1381d05cddcSAtari911            $this->runSync();
1391d05cddcSAtari911        } elseif ($action === 'stop_sync') {
1401d05cddcSAtari911            $this->stopSync();
1411d05cddcSAtari911        } elseif ($action === 'upload_update') {
1421d05cddcSAtari911            $this->uploadUpdate();
1431d05cddcSAtari911        } elseif ($action === 'delete_backup') {
1441d05cddcSAtari911            $this->deleteBackup();
1451d05cddcSAtari911        } elseif ($action === 'rename_backup') {
1461d05cddcSAtari911            $this->renameBackup();
1471d05cddcSAtari911        } elseif ($action === 'restore_backup') {
1481d05cddcSAtari911            $this->restoreBackup();
1499ccd446eSAtari911        } elseif ($action === 'create_manual_backup') {
1509ccd446eSAtari911            $this->createManualBackup();
1511d05cddcSAtari911        } elseif ($action === 'export_config') {
1521d05cddcSAtari911            $this->exportConfig();
1531d05cddcSAtari911        } elseif ($action === 'import_config') {
1541d05cddcSAtari911            $this->importConfig();
1551d05cddcSAtari911        } elseif ($action === 'get_log') {
1561d05cddcSAtari911            $this->getLog();
1577e8ea635SAtari911        } elseif ($action === 'cleanup_empty_namespaces') {
1587e8ea635SAtari911            $this->handleCleanupEmptyNamespaces();
1597e8ea635SAtari911        } elseif ($action === 'trim_all_past_recurring') {
1607e8ea635SAtari911            $this->handleTrimAllPastRecurring();
1617e8ea635SAtari911        } elseif ($action === 'rescan_recurring') {
1627e8ea635SAtari911            $this->handleRescanRecurring();
1637e8ea635SAtari911        } elseif ($action === 'extend_recurring') {
1647e8ea635SAtari911            $this->handleExtendRecurring();
1657e8ea635SAtari911        } elseif ($action === 'trim_recurring') {
1667e8ea635SAtari911            $this->handleTrimRecurring();
1677e8ea635SAtari911        } elseif ($action === 'pause_recurring') {
1687e8ea635SAtari911            $this->handlePauseRecurring();
1697e8ea635SAtari911        } elseif ($action === 'resume_recurring') {
1707e8ea635SAtari911            $this->handleResumeRecurring();
1717e8ea635SAtari911        } elseif ($action === 'change_start_recurring') {
1727e8ea635SAtari911            $this->handleChangeStartRecurring();
1737e8ea635SAtari911        } elseif ($action === 'change_pattern_recurring') {
1747e8ea635SAtari911            $this->handleChangePatternRecurring();
1751d05cddcSAtari911        } elseif ($action === 'clear_log') {
1761d05cddcSAtari911            $this->clearLogFile();
1771d05cddcSAtari911        } elseif ($action === 'download_log') {
1781d05cddcSAtari911            $this->downloadLog();
1791d05cddcSAtari911        } elseif ($action === 'rescan_events') {
1801d05cddcSAtari911            $this->rescanEvents();
1811d05cddcSAtari911        } elseif ($action === 'export_all_events') {
1821d05cddcSAtari911            $this->exportAllEvents();
1831d05cddcSAtari911        } elseif ($action === 'import_all_events') {
1841d05cddcSAtari911            $this->importAllEvents();
1851d05cddcSAtari911        } elseif ($action === 'preview_cleanup') {
1861d05cddcSAtari911            $this->previewCleanup();
1871d05cddcSAtari911        } elseif ($action === 'cleanup_events') {
1881d05cddcSAtari911            $this->cleanupEvents();
1894590242dSAtari911        } elseif ($action === 'save_important_namespaces') {
1904590242dSAtari911            $this->saveImportantNamespaces();
1911d05cddcSAtari911        }
1921d05cddcSAtari911    }
1931d05cddcSAtari911
1941d05cddcSAtari911    public function html() {
1951d05cddcSAtari911        global $INPUT;
1961d05cddcSAtari911
1979ccd446eSAtari911        // Get current tab - default to 'manage' (Manage Events tab)
1989ccd446eSAtari911        $tab = $INPUT->str('tab', 'manage');
1991d05cddcSAtari911
2009ccd446eSAtari911        // Get template colors
2019ccd446eSAtari911        $colors = $this->getTemplateColors();
2029ccd446eSAtari911        $accentColor = '#00cc07'; // Keep calendar plugin accent color
2039ccd446eSAtari911
204815440faSAtari911        // Tab navigation (Manage Events, Update Plugin, Outlook Sync, Google Sync, Themes)
2059ccd446eSAtari911        echo '<div style="border-bottom:2px solid ' . $colors['border'] . '; margin:10px 0 15px 0;">';
20622228b0eSAtari911        echo '<a href="?do=admin&page=calendar&tab=manage" style="display:inline-block; padding:8px 16px; text-decoration:none; color:' . ($tab === 'manage' ? $accentColor : $colors['text']) . '; border-bottom:3px solid ' . ($tab === 'manage' ? $accentColor : 'transparent') . '; font-weight:' . ($tab === 'manage' ? 'bold' : 'normal') . ';">�� ' . $this->getLang('tab_manage') . '</a>';
20722228b0eSAtari911        echo '<a href="?do=admin&page=calendar&tab=update" style="display:inline-block; padding:8px 16px; text-decoration:none; color:' . ($tab === 'update' ? $accentColor : $colors['text']) . '; border-bottom:3px solid ' . ($tab === 'update' ? $accentColor : 'transparent') . '; font-weight:' . ($tab === 'update' ? 'bold' : 'normal') . ';">�� ' . $this->getLang('tab_update') . '</a>';
208815440faSAtari911        echo '<a href="?do=admin&page=calendar&tab=config" style="display:inline-block; padding:8px 16px; text-decoration:none; color:' . ($tab === 'config' ? $accentColor : $colors['text']) . '; border-bottom:3px solid ' . ($tab === 'config' ? $accentColor : 'transparent') . '; font-weight:' . ($tab === 'config' ? 'bold' : 'normal') . ';">�� Outlook</a>';
209815440faSAtari911        echo '<a href="?do=admin&page=calendar&tab=google" style="display:inline-block; padding:8px 16px; text-decoration:none; color:' . ($tab === 'google' ? $accentColor : $colors['text']) . '; border-bottom:3px solid ' . ($tab === 'google' ? $accentColor : 'transparent') . '; font-weight:' . ($tab === 'google' ? 'bold' : 'normal') . ';">�� Google</a>';
21022228b0eSAtari911        echo '<a href="?do=admin&page=calendar&tab=themes" style="display:inline-block; padding:8px 16px; text-decoration:none; color:' . ($tab === 'themes' ? $accentColor : $colors['text']) . '; border-bottom:3px solid ' . ($tab === 'themes' ? $accentColor : 'transparent') . '; font-weight:' . ($tab === 'themes' ? 'bold' : 'normal') . ';">�� ' . $this->getLang('tab_themes') . '</a>';
2111d05cddcSAtari911        echo '</div>';
2121d05cddcSAtari911
2131d05cddcSAtari911        // Render appropriate tab
2141d05cddcSAtari911        if ($tab === 'config') {
2159ccd446eSAtari911            $this->renderConfigTab($colors);
2161d05cddcSAtari911        } elseif ($tab === 'manage') {
2179ccd446eSAtari911            $this->renderManageTab($colors);
2189ccd446eSAtari911        } elseif ($tab === 'themes') {
2199ccd446eSAtari911            $this->renderThemesTab($colors);
220815440faSAtari911        } elseif ($tab === 'google') {
221815440faSAtari911            $this->renderGoogleSyncTab($colors);
2221d05cddcSAtari911        } else {
2239ccd446eSAtari911            $this->renderUpdateTab($colors);
2241d05cddcSAtari911        }
2251d05cddcSAtari911    }
2261d05cddcSAtari911
2279ccd446eSAtari911    private function renderConfigTab($colors = null) {
2281d05cddcSAtari911        global $INPUT;
2291d05cddcSAtari911
2309ccd446eSAtari911        // Use defaults if not provided
2319ccd446eSAtari911        if ($colors === null) {
2329ccd446eSAtari911            $colors = $this->getTemplateColors();
2339ccd446eSAtari911        }
2349ccd446eSAtari911
2351d05cddcSAtari911        // Load current config
236*2866e827SAtari911        $configFile = $this->syncConfigPath();
2371d05cddcSAtari911        $config = [];
2381d05cddcSAtari911        if (file_exists($configFile)) {
2391d05cddcSAtari911            $config = include $configFile;
2401d05cddcSAtari911        }
2411d05cddcSAtari911
2421d05cddcSAtari911        // Show message if present
2431d05cddcSAtari911        if ($INPUT->has('msg')) {
2441d05cddcSAtari911            $msg = hsc($INPUT->str('msg'));
2451d05cddcSAtari911            $type = $INPUT->str('msgtype', 'success');
2461d05cddcSAtari911            $class = ($type === 'success') ? 'msg success' : 'msg error';
2471d05cddcSAtari911            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;\">";
2481d05cddcSAtari911            echo $msg;
2491d05cddcSAtari911            echo "</div>";
2501d05cddcSAtari911        }
2511d05cddcSAtari911
252da206178SAtari911        echo '<h2 style="margin:10px 0; font-size:20px;">Outlook Sync Configuration</h2>';
2531d05cddcSAtari911
2541d05cddcSAtari911        // Import/Export buttons
2551d05cddcSAtari911        echo '<div style="display:flex; gap:10px; margin-bottom:15px;">';
256da206178SAtari911        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>';
257da206178SAtari911        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>';
2581d05cddcSAtari911        echo '<input type="file" id="importFileInput" accept=".enc" style="display:none;" onchange="importConfig(this)">';
2591d05cddcSAtari911        echo '<span id="importStatus" style="margin-left:10px; font-size:12px;"></span>';
2601d05cddcSAtari911        echo '</div>';
2611d05cddcSAtari911
2621d05cddcSAtari911        echo '<form method="post" action="?do=admin&page=calendar" style="max-width:900px;">';
263*2866e827SAtari911        echo formSecurityToken(false);
2641d05cddcSAtari911        echo '<input type="hidden" name="action" value="save_config">';
2651d05cddcSAtari911
2661d05cddcSAtari911        // Azure Credentials
2679ccd446eSAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px;">';
268da206178SAtari911        echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">Microsoft Azure App Credentials</h3>';
269da206178SAtari911        echo '<p style="color:' . $colors['text'] . '; font-size:0.85em; margin:0 0 10px 0;">Register at <a href="https://portal.azure.com" target="_blank" style="color:#00cc07;">Azure Portal</a> → App registrations</p>';
2701d05cddcSAtari911
271da206178SAtari911        echo '<label style="display:block; font-weight:bold; margin:8px 0 3px; font-size:13px;">Tenant ID</label>';
2729ccd446eSAtari911        echo '<input type="text" name="tenant_id" value="' . hsc($config['tenant_id'] ?? '') . '" placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" required style="width:100%; padding:6px; border:1px solid ' . $colors['border'] . '; border-radius:3px; font-size:13px;">';
2731d05cddcSAtari911
274da206178SAtari911        echo '<label style="display:block; font-weight:bold; margin:8px 0 3px; font-size:13px;">Client ID (Application ID)</label>';
27596df7d3eSAtari911        echo '<input type="text" name="client_id" value="' . hsc($config['client_id'] ?? '') . '" placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" required autocomplete="off" style="width:100%; padding:6px; border:1px solid ' . $colors['border'] . '; border-radius:3px; font-size:13px;">';
2761d05cddcSAtari911
277da206178SAtari911        echo '<label style="display:block; font-weight:bold; margin:8px 0 3px; font-size:13px;">Client Secret</label>';
278da206178SAtari911        echo '<input type="password" name="client_secret" value="' . hsc($config['client_secret'] ?? '') . '" placeholder="Enter client secret" required autocomplete="new-password" style="width:100%; padding:6px; border:1px solid ' . $colors['border'] . '; border-radius:3px; font-size:13px;">';
279da206178SAtari911        echo '<p style="color:#999; font-size:0.8em; margin:3px 0 0;">⚠️ Keep this secret safe!</p>';
2801d05cddcSAtari911        echo '</div>';
2811d05cddcSAtari911
2821d05cddcSAtari911        // Outlook Settings
2839ccd446eSAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px;">';
284da206178SAtari911        echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">Outlook Settings</h3>';
2851d05cddcSAtari911
2861d05cddcSAtari911        echo '<div style="display:grid; grid-template-columns:1fr 1fr; gap:10px;">';
2871d05cddcSAtari911
2881d05cddcSAtari911        echo '<div>';
289da206178SAtari911        echo '<label style="display:block; font-weight:bold; margin:0 0 3px; font-size:13px;">User Email</label>';
29096df7d3eSAtari911        echo '<input type="email" name="user_email" value="' . hsc($config['user_email'] ?? '') . '" placeholder="your.email@company.com" required autocomplete="email" style="width:100%; padding:6px; border:1px solid ' . $colors['border'] . '; border-radius:3px; font-size:13px;">';
2911d05cddcSAtari911        echo '</div>';
2921d05cddcSAtari911
2931d05cddcSAtari911        echo '<div>';
294da206178SAtari911        echo '<label style="display:block; font-weight:bold; margin:0 0 3px; font-size:13px;">Timezone</label>';
2959ccd446eSAtari911        echo '<input type="text" name="timezone" value="' . hsc($config['timezone'] ?? 'America/Los_Angeles') . '" placeholder="America/Los_Angeles" style="width:100%; padding:6px; border:1px solid ' . $colors['border'] . '; border-radius:3px; font-size:13px;">';
2961d05cddcSAtari911        echo '</div>';
2971d05cddcSAtari911
2981d05cddcSAtari911        echo '<div>';
299da206178SAtari911        echo '<label style="display:block; font-weight:bold; margin:0 0 3px; font-size:13px;">Default Category</label>';
3009ccd446eSAtari911        echo '<input type="text" name="default_category" value="' . hsc($config['default_category'] ?? 'Blue category') . '" placeholder="Blue category" style="width:100%; padding:6px; border:1px solid ' . $colors['border'] . '; border-radius:3px; font-size:13px;">';
3011d05cddcSAtari911        echo '</div>';
3021d05cddcSAtari911
3031d05cddcSAtari911        echo '<div>';
304da206178SAtari911        echo '<label style="display:block; font-weight:bold; margin:0 0 3px; font-size:13px;">Reminder (minutes)</label>';
3059ccd446eSAtari911        echo '<input type="number" name="reminder_minutes" value="' . hsc($config['reminder_minutes'] ?? 15) . '" placeholder="15" style="width:100%; padding:6px; border:1px solid ' . $colors['border'] . '; border-radius:3px; font-size:13px;">';
3061d05cddcSAtari911        echo '</div>';
3071d05cddcSAtari911
3081d05cddcSAtari911        echo '</div>'; // end grid
3091d05cddcSAtari911        echo '</div>';
3101d05cddcSAtari911
3111d05cddcSAtari911        // Sync Options
3129ccd446eSAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px;">';
313da206178SAtari911        echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">Sync Options</h3>';
3141d05cddcSAtari911
3151d05cddcSAtari911        $syncCompleted = isset($config['sync_completed_tasks']) ? $config['sync_completed_tasks'] : false;
316da206178SAtari911        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>';
3171d05cddcSAtari911
3181d05cddcSAtari911        $deleteOutlook = isset($config['delete_outlook_events']) ? $config['delete_outlook_events'] : true;
319da206178SAtari911        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>';
3201d05cddcSAtari911
3211d05cddcSAtari911        $syncAll = isset($config['sync_all_namespaces']) ? $config['sync_all_namespaces'] : true;
322da206178SAtari911        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>';
3231d05cddcSAtari911
3241d05cddcSAtari911        // Namespace selection (shown when sync_all is unchecked)
3251d05cddcSAtari911        echo '<div id="namespace_selection" style="margin-top:10px; ' . ($syncAll ? 'display:none;' : '') . '">';
326da206178SAtari911        echo '<label style="display:block; font-weight:bold; margin-bottom:5px; font-size:13px;">Select namespaces to sync:</label>';
3271d05cddcSAtari911
3281d05cddcSAtari911        // Get available namespaces
3291d05cddcSAtari911        $availableNamespaces = $this->getAllNamespaces();
3301d05cddcSAtari911        $selectedNamespaces = isset($config['sync_namespaces']) ? $config['sync_namespaces'] : [];
3311d05cddcSAtari911
3329ccd446eSAtari911        echo '<div style="max-height:150px; overflow-y:auto; border:1px solid ' . $colors['border'] . '; border-radius:3px; padding:8px; background:' . $colors['bg'] . ';">';
3331d05cddcSAtari911        echo '<label style="display:block; margin:3px 0;"><input type="checkbox" name="sync_namespaces[]" value=""> (default)</label>';
3341d05cddcSAtari911        foreach ($availableNamespaces as $ns) {
3351d05cddcSAtari911            if ($ns !== '') {
3361d05cddcSAtari911                $checked = in_array($ns, $selectedNamespaces) ? 'checked' : '';
3371d05cddcSAtari911                echo '<label style="display:block; margin:3px 0;"><input type="checkbox" name="sync_namespaces[]" value="' . hsc($ns) . '" ' . $checked . '> ' . hsc($ns) . '</label>';
3381d05cddcSAtari911            }
3391d05cddcSAtari911        }
3401d05cddcSAtari911        echo '</div>';
3411d05cddcSAtari911        echo '</div>';
3421d05cddcSAtari911
3431d05cddcSAtari911        echo '<script>
3441d05cddcSAtari911        function toggleNamespaceSelection(checkbox) {
3451d05cddcSAtari911            document.getElementById("namespace_selection").style.display = checkbox.checked ? "none" : "block";
3461d05cddcSAtari911        }
3471d05cddcSAtari911        </script>';
3481d05cddcSAtari911
3491d05cddcSAtari911        echo '</div>';
3501d05cddcSAtari911
3511d05cddcSAtari911        // Namespace and Color Mapping - Side by Side
3521d05cddcSAtari911        echo '<div style="display:grid; grid-template-columns:1fr 1fr; gap:10px; margin:10px 0;">';
3531d05cddcSAtari911
3541d05cddcSAtari911        // Namespace Mapping
3559ccd446eSAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:12px; border-left:3px solid #00cc07; border-radius:3px;">';
356da206178SAtari911        echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">Namespace → Category</h3>';
357da206178SAtari911        echo '<p style="color:' . $colors['text'] . '; font-size:0.8em; margin:0 0 5px;">One per line: namespace=Category</p>';
3589ccd446eSAtari911        echo '<textarea name="category_mapping" rows="6" style="width:100%; padding:6px; border:1px solid ' . $colors['border'] . '; border-radius:3px; font-family:monospace; font-size:12px; resize:vertical;" placeholder="work=Blue category&#10;personal=Green category">';
3591d05cddcSAtari911        if (isset($config['category_mapping']) && is_array($config['category_mapping'])) {
3601d05cddcSAtari911            foreach ($config['category_mapping'] as $ns => $cat) {
3611d05cddcSAtari911                echo hsc($ns) . '=' . hsc($cat) . "\n";
3621d05cddcSAtari911            }
3631d05cddcSAtari911        }
3641d05cddcSAtari911        echo '</textarea>';
3651d05cddcSAtari911        echo '</div>';
3661d05cddcSAtari911
3671d05cddcSAtari911        // Color Mapping with Color Picker
3689ccd446eSAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:12px; border-left:3px solid #00cc07; border-radius:3px;">';
369da206178SAtari911        echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">�� Event Color → Category</h3>';
370da206178SAtari911        echo '<p style="color:' . $colors['text'] . '; font-size:0.8em; margin:0 0 8px;">Map calendar colors to Outlook categories</p>';
3711d05cddcSAtari911
3721d05cddcSAtari911        // Define calendar colors and Outlook categories (only the main 6 colors)
3731d05cddcSAtari911        $calendarColors = [
374da206178SAtari911            '#3498db' => 'Blue',
375da206178SAtari911            '#2ecc71' => 'Green',
376da206178SAtari911            '#e74c3c' => 'Red',
377da206178SAtari911            '#f39c12' => 'Orange',
378da206178SAtari911            '#9b59b6' => 'Purple',
379da206178SAtari911            '#1abc9c' => 'Teal'
3801d05cddcSAtari911        ];
3811d05cddcSAtari911
3821d05cddcSAtari911        $outlookCategories = [
3831d05cddcSAtari911            'Blue category',
3841d05cddcSAtari911            'Green category',
3851d05cddcSAtari911            'Orange category',
3861d05cddcSAtari911            'Red category',
3871d05cddcSAtari911            'Yellow category',
3881d05cddcSAtari911            'Purple category'
3891d05cddcSAtari911        ];
3901d05cddcSAtari911
3911d05cddcSAtari911        // Load existing color mappings
3921d05cddcSAtari911        $existingMappings = isset($config['color_mapping']) && is_array($config['color_mapping'])
3931d05cddcSAtari911            ? $config['color_mapping']
3941d05cddcSAtari911            : [];
3951d05cddcSAtari911
3961d05cddcSAtari911        // Display color mapping rows
3971d05cddcSAtari911        echo '<div id="colorMappings" style="max-height:200px; overflow-y:auto;">';
3981d05cddcSAtari911
3991d05cddcSAtari911        $rowIndex = 0;
4001d05cddcSAtari911        foreach ($calendarColors as $hexColor => $colorName) {
4011d05cddcSAtari911            $selectedCategory = isset($existingMappings[$hexColor]) ? $existingMappings[$hexColor] : '';
4021d05cddcSAtari911
4031d05cddcSAtari911            echo '<div style="display:flex; gap:8px; align-items:center; margin-bottom:6px;">';
4041d05cddcSAtari911
4051d05cddcSAtari911            // Color preview box
4061d05cddcSAtari911            echo '<div style="width:24px; height:24px; background:' . $hexColor . '; border:2px solid #ddd; border-radius:3px; flex-shrink:0;"></div>';
4071d05cddcSAtari911
4081d05cddcSAtari911            // Color name
4099ccd446eSAtari911            echo '<span style="font-size:12px; min-width:90px; color:' . $colors['text'] . ';">' . $colorName . '</span>';
4101d05cddcSAtari911
4111d05cddcSAtari911            // Arrow
4121d05cddcSAtari911            echo '<span style="color:#999; font-size:12px;">→</span>';
4131d05cddcSAtari911
4141d05cddcSAtari911            // Outlook category dropdown
4159ccd446eSAtari911            echo '<select name="color_map_' . $rowIndex . '" style="flex:1; padding:4px; border:1px solid ' . $colors['border'] . '; border-radius:3px; font-size:12px;">';
416da206178SAtari911            echo '<option value="">-- None --</option>';
4171d05cddcSAtari911            foreach ($outlookCategories as $category) {
4181d05cddcSAtari911                $selected = ($selectedCategory === $category) ? 'selected' : '';
4191d05cddcSAtari911                echo '<option value="' . hsc($category) . '" ' . $selected . '>' . hsc($category) . '</option>';
4201d05cddcSAtari911            }
4211d05cddcSAtari911            echo '</select>';
4221d05cddcSAtari911
4231d05cddcSAtari911            // Hidden input for the hex color
4241d05cddcSAtari911            echo '<input type="hidden" name="color_hex_' . $rowIndex . '" value="' . $hexColor . '">';
4251d05cddcSAtari911
4261d05cddcSAtari911            echo '</div>';
4271d05cddcSAtari911            $rowIndex++;
4281d05cddcSAtari911        }
4291d05cddcSAtari911
4301d05cddcSAtari911        echo '</div>';
4311d05cddcSAtari911
4321d05cddcSAtari911        // Hidden input to track number of color mappings
4331d05cddcSAtari911        echo '<input type="hidden" name="color_mapping_count" value="' . $rowIndex . '">';
4341d05cddcSAtari911
4351d05cddcSAtari911        echo '</div>';
4361d05cddcSAtari911
4371d05cddcSAtari911        echo '</div>'; // end grid
4381d05cddcSAtari911
4391d05cddcSAtari911        // Submit button
440da206178SAtari911        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>';
4411d05cddcSAtari911        echo '</form>';
4421d05cddcSAtari911
443da206178SAtari911        // JavaScript for Import/Export
4441d05cddcSAtari911        echo '<script>
4451d05cddcSAtari911        async function exportConfig() {
4461d05cddcSAtari911            try {
447*2866e827SAtari911                const response = await fetch("?do=admin&page=calendar&action=export_config&call=ajax&sectok=" + JSINFO.sectok, {
4481d05cddcSAtari911                    method: "POST"
4491d05cddcSAtari911                });
4501d05cddcSAtari911                const data = await response.json();
4511d05cddcSAtari911
4521d05cddcSAtari911                if (data.success) {
4531d05cddcSAtari911                    // Create download link
4541d05cddcSAtari911                    const blob = new Blob([data.encrypted], {type: "application/octet-stream"});
4551d05cddcSAtari911                    const url = URL.createObjectURL(blob);
4561d05cddcSAtari911                    const a = document.createElement("a");
4571d05cddcSAtari911                    a.href = url;
4581d05cddcSAtari911                    a.download = "sync_config_" + new Date().toISOString().split("T")[0] + ".enc";
4591d05cddcSAtari911                    document.body.appendChild(a);
4601d05cddcSAtari911                    a.click();
4611d05cddcSAtari911                    document.body.removeChild(a);
4621d05cddcSAtari911                    URL.revokeObjectURL(url);
4631d05cddcSAtari911
464da206178SAtari911                    alert("✅ Config exported successfully!\\n\\n⚠️ This file contains encrypted credentials.\\nKeep it secure!");
4651d05cddcSAtari911                } else {
466da206178SAtari911                    alert("❌ Export failed: " + data.message);
4671d05cddcSAtari911                }
4681d05cddcSAtari911            } catch (error) {
469da206178SAtari911                alert("❌ Error: " + error.message);
4701d05cddcSAtari911            }
4711d05cddcSAtari911        }
4721d05cddcSAtari911
4731d05cddcSAtari911        async function importConfig(input) {
4741d05cddcSAtari911            const file = input.files[0];
4751d05cddcSAtari911            if (!file) return;
4761d05cddcSAtari911
4771d05cddcSAtari911            const status = document.getElementById("importStatus");
478da206178SAtari911            status.textContent = "⏳ Importing...";
4791d05cddcSAtari911            status.style.color = "#00cc07";
4801d05cddcSAtari911
4811d05cddcSAtari911            try {
4821d05cddcSAtari911                const encrypted = await file.text();
4831d05cddcSAtari911
4841d05cddcSAtari911                const formData = new FormData();
4851d05cddcSAtari911                formData.append("encrypted_config", encrypted);
4861d05cddcSAtari911
487*2866e827SAtari911                const response = await fetch("?do=admin&page=calendar&action=import_config&call=ajax&sectok=" + JSINFO.sectok, {
4881d05cddcSAtari911                    method: "POST",
4891d05cddcSAtari911                    body: formData
4901d05cddcSAtari911                });
4911d05cddcSAtari911                const data = await response.json();
4921d05cddcSAtari911
4931d05cddcSAtari911                if (data.success) {
494da206178SAtari911                    status.textContent = "✅ Import successful! Reloading...";
4951d05cddcSAtari911                    status.style.color = "#28a745";
4961d05cddcSAtari911                    setTimeout(() => {
4971d05cddcSAtari911                        window.location.reload();
4981d05cddcSAtari911                    }, 1500);
4991d05cddcSAtari911                } else {
500da206178SAtari911                    status.textContent = "❌ Import failed: " + data.message;
5011d05cddcSAtari911                    status.style.color = "#dc3545";
5021d05cddcSAtari911                }
5031d05cddcSAtari911            } catch (error) {
5041d05cddcSAtari911                status.textContent = "❌ Error: " + error.message;
5051d05cddcSAtari911                status.style.color = "#dc3545";
5061d05cddcSAtari911            }
5071d05cddcSAtari911
5081d05cddcSAtari911            // Reset file input
5091d05cddcSAtari911            input.value = "";
5101d05cddcSAtari911        }
5111d05cddcSAtari911        </script>';
5121d05cddcSAtari911
5131d05cddcSAtari911        // Sync Controls Section
5149ccd446eSAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:15px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:900px;">';
515da206178SAtari911        echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">�� Sync Controls</h3>';
5161d05cddcSAtari911
5171d05cddcSAtari911        // Check cron job status
5181d05cddcSAtari911        $cronStatus = $this->getCronStatus();
5191d05cddcSAtari911
5201d05cddcSAtari911        // Check log file permissions
52196df7d3eSAtari911        $logFile = $this->getSyncLogPath();
5221d05cddcSAtari911        $logWritable = is_writable($logFile) || is_writable(dirname($logFile));
5231d05cddcSAtari911
5241d05cddcSAtari911        echo '<div style="display:flex; gap:10px; align-items:center; margin-bottom:10px;">';
525da206178SAtari911        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>';
526da206178SAtari911        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>';
5271d05cddcSAtari911
5281d05cddcSAtari911        if ($cronStatus['active']) {
5299ccd446eSAtari911            echo '<span style="color:' . $colors['text'] . '; font-size:12px;">⏰ ' . hsc($cronStatus['frequency']) . '</span>';
5301d05cddcSAtari911        } else {
531da206178SAtari911            echo '<span style="color:#999; font-size:12px;">⚠️ No cron job detected</span>';
5321d05cddcSAtari911        }
5331d05cddcSAtari911
5349ccd446eSAtari911        echo '<span id="syncStatus" style="color:' . $colors['text'] . '; font-size:12px; margin-left:auto;"></span>';
5351d05cddcSAtari911        echo '</div>';
5361d05cddcSAtari911
5371d05cddcSAtari911        // Show permission warning if log not writable
5381d05cddcSAtari911        if (!$logWritable) {
5391d05cddcSAtari911            echo '<div style="background:#fff3e0; border-left:3px solid #ff9800; padding:8px; margin:8px 0; border-radius:3px;">';
540da206178SAtari911            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>';
5411d05cddcSAtari911            echo '</div>';
5421d05cddcSAtari911        }
5431d05cddcSAtari911
5441d05cddcSAtari911        // Show debug info if cron detected
5451d05cddcSAtari911        if ($cronStatus['active'] && !empty($cronStatus['full_line'])) {
54696df7d3eSAtari911            // Check if crontab has >> redirect which will cause duplicate log entries
54796df7d3eSAtari911            $hasRedirect = (strpos($cronStatus['full_line'], '>>') !== false || strpos($cronStatus['full_line'], '> ') !== false);
54896df7d3eSAtari911
54996df7d3eSAtari911            if ($hasRedirect) {
55096df7d3eSAtari911                echo '<div style="background:#fff3e0; border-left:3px solid #ff9800; padding:8px; margin:8px 0; border-radius:3px;">';
551da206178SAtari911                echo '<span style="color:#e65100; font-size:11px;">⚠️ <strong>Duplicate log entries:</strong> Your crontab has a <code>&gt;&gt;</code> redirect. The sync script logs internally, so this causes duplicate entries. Remove the redirect from your crontab.</span>';
55296df7d3eSAtari911                echo '</div>';
55396df7d3eSAtari911            }
55496df7d3eSAtari911
5551d05cddcSAtari911            echo '<details style="margin-top:5px;">';
556da206178SAtari911            echo '<summary style="cursor:pointer; color:#999; font-size:11px;">Show cron details</summary>';
5571d05cddcSAtari911            echo '<pre style="background:#f0f0f0; padding:8px; border-radius:3px; font-size:10px; margin:5px 0; overflow-x:auto;">' . hsc($cronStatus['full_line']) . '</pre>';
5581d05cddcSAtari911            echo '</details>';
5591d05cddcSAtari911        }
5601d05cddcSAtari911
5611d05cddcSAtari911        if (!$cronStatus['active']) {
562da206178SAtari911            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>';
563da206178SAtari911            echo '<p style="color:#888; font-size:10px; margin:3px 0;"><em>Note: The script logs to ' . $logFile . ' automatically. Do not use &gt;&gt; redirect.</em></p>';
5641d05cddcSAtari911        }
5651d05cddcSAtari911
5661d05cddcSAtari911        echo '</div>';
5671d05cddcSAtari911
568da206178SAtari911        // JavaScript for Run Sync Now
5691d05cddcSAtari911        echo '<script>
5701d05cddcSAtari911        let syncAbortController = null;
5711d05cddcSAtari911
5721d05cddcSAtari911        function runSyncNow() {
5731d05cddcSAtari911            const btn = document.getElementById("syncBtn");
5741d05cddcSAtari911            const stopBtn = document.getElementById("stopBtn");
5751d05cddcSAtari911            const status = document.getElementById("syncStatus");
5761d05cddcSAtari911
5771d05cddcSAtari911            btn.disabled = true;
5781d05cddcSAtari911            btn.style.display = "none";
5791d05cddcSAtari911            stopBtn.style.display = "inline-block";
580da206178SAtari911            btn.textContent = "⏳ Running...";
5811d05cddcSAtari911            btn.style.background = "#999";
582da206178SAtari911            status.textContent = "Starting sync...";
5831d05cddcSAtari911            status.style.color = "#00cc07";
5841d05cddcSAtari911
5851d05cddcSAtari911            // Create abort controller for this sync
5861d05cddcSAtari911            syncAbortController = new AbortController();
5871d05cddcSAtari911
588*2866e827SAtari911            fetch("?do=admin&page=calendar&action=run_sync&call=ajax&sectok=" + JSINFO.sectok, {
5891d05cddcSAtari911                method: "POST",
5901d05cddcSAtari911                signal: syncAbortController.signal
5911d05cddcSAtari911            })
5921d05cddcSAtari911                .then(response => response.json())
5931d05cddcSAtari911                .then(data => {
5941d05cddcSAtari911                    if (data.success) {
5951d05cddcSAtari911                        status.textContent = "✅ " + data.message;
5961d05cddcSAtari911                        status.style.color = "#28a745";
5971d05cddcSAtari911                    } else {
5981d05cddcSAtari911                        status.textContent = "❌ " + data.message;
5991d05cddcSAtari911                        status.style.color = "#dc3545";
6001d05cddcSAtari911                    }
6011d05cddcSAtari911                    btn.disabled = false;
6021d05cddcSAtari911                    btn.style.display = "inline-block";
6031d05cddcSAtari911                    stopBtn.style.display = "none";
604da206178SAtari911                    btn.textContent = "▶️ Run Sync Now";
6051d05cddcSAtari911                    btn.style.background = "#00cc07";
6061d05cddcSAtari911                    syncAbortController = null;
6071d05cddcSAtari911
6081d05cddcSAtari911                    // Clear status after 10 seconds
6091d05cddcSAtari911                    setTimeout(() => {
6101d05cddcSAtari911                        status.textContent = "";
6111d05cddcSAtari911                    }, 10000);
6121d05cddcSAtari911                })
6131d05cddcSAtari911                .catch(error => {
6141d05cddcSAtari911                    if (error.name === "AbortError") {
615da206178SAtari911                        status.textContent = "⏹️ Sync stopped by user";
6161d05cddcSAtari911                        status.style.color = "#ff9800";
6171d05cddcSAtari911                    } else {
618da206178SAtari911                        status.textContent = "❌ Error: " + error.message;
6191d05cddcSAtari911                        status.style.color = "#dc3545";
6201d05cddcSAtari911                    }
6211d05cddcSAtari911                    btn.disabled = false;
6221d05cddcSAtari911                    btn.style.display = "inline-block";
6231d05cddcSAtari911                    stopBtn.style.display = "none";
624da206178SAtari911                    btn.textContent = "▶️ Run Sync Now";
6251d05cddcSAtari911                    btn.style.background = "#00cc07";
6261d05cddcSAtari911                    syncAbortController = null;
6271d05cddcSAtari911                });
6281d05cddcSAtari911        }
6291d05cddcSAtari911
6301d05cddcSAtari911        function stopSyncNow() {
6311d05cddcSAtari911            const status = document.getElementById("syncStatus");
6321d05cddcSAtari911
633da206178SAtari911            status.textContent = "⏹️ Sending stop signal...";
6341d05cddcSAtari911            status.style.color = "#ff9800";
6351d05cddcSAtari911
6361d05cddcSAtari911            // First, send stop signal to server
637*2866e827SAtari911            fetch("?do=admin&page=calendar&action=stop_sync&call=ajax&sectok=" + JSINFO.sectok, {
6381d05cddcSAtari911                method: "POST"
6391d05cddcSAtari911            })
6401d05cddcSAtari911            .then(response => response.json())
6411d05cddcSAtari911            .then(data => {
6421d05cddcSAtari911                if (data.success) {
643da206178SAtari911                    status.textContent = "⏹️ Stop signal sent - sync will abort soon";
6441d05cddcSAtari911                    status.style.color = "#ff9800";
6451d05cddcSAtari911                } else {
6461d05cddcSAtari911                    status.textContent = "⚠️ " + data.message;
6471d05cddcSAtari911                    status.style.color = "#ff9800";
6481d05cddcSAtari911                }
6491d05cddcSAtari911            })
6501d05cddcSAtari911            .catch(error => {
651da206178SAtari911                status.textContent = "⚠️ Error sending stop signal: " + error.message;
6521d05cddcSAtari911                status.style.color = "#ff9800";
6531d05cddcSAtari911            });
6541d05cddcSAtari911
6551d05cddcSAtari911            // Also abort the fetch request
6561d05cddcSAtari911            if (syncAbortController) {
6571d05cddcSAtari911                syncAbortController.abort();
658da206178SAtari911                status.textContent = "⏹️ Stopping sync...";
6591d05cddcSAtari911                status.style.color = "#ff9800";
6601d05cddcSAtari911            }
6611d05cddcSAtari911        }
6621d05cddcSAtari911        </script>';
6631d05cddcSAtari911
6641d05cddcSAtari911        // Log Viewer Section - More Compact
6659ccd446eSAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:15px 0 10px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:900px;">';
666da206178SAtari911        echo '<h3 style="margin:0 0 5px 0; color:#00cc07; font-size:16px;">�� Live Sync Log</h3>';
667da206178SAtari911        echo '<p style="color:' . $colors['text'] . '; font-size:0.8em; margin:0 0 8px;">Location: <code style="font-size:10px;">' . $logFile . '</code> • Updates every 2 seconds</p>';
6681d05cddcSAtari911
6691d05cddcSAtari911        // Log viewer container
6701d05cddcSAtari911        echo '<div style="background:#1e1e1e; border-radius:5px; overflow:hidden; box-shadow:0 2px 4px rgba(0,0,0,0.3);">';
6711d05cddcSAtari911
6721d05cddcSAtari911        // Log header - More compact
6731d05cddcSAtari911        echo '<div style="background:#2d2d2d; padding:6px 10px; display:flex; justify-content:space-between; align-items:center; border-bottom:1px solid #444;">';
6741d05cddcSAtari911        echo '<span style="color:#00cc07; font-family:monospace; font-weight:bold; font-size:12px;">sync.log</span>';
6751d05cddcSAtari911        echo '<div>';
676da206178SAtari911        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>';
677da206178SAtari911        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>';
678da206178SAtari911        echo '<button onclick="downloadLog()" style="background:#666; color:white; border:none; padding:4px 8px; border-radius:3px; cursor:pointer; font-size:11px;">�� Download</button>';
6791d05cddcSAtari911        echo '</div>';
6801d05cddcSAtari911        echo '</div>';
6811d05cddcSAtari911
6821d05cddcSAtari911        // Log content - Reduced height to 250px
683da206178SAtari911        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>';
6841d05cddcSAtari911
6851d05cddcSAtari911        echo '</div>';
6861d05cddcSAtari911        echo '</div>';
6871d05cddcSAtari911
688da206178SAtari911        // JavaScript for log viewer
6891d05cddcSAtari911        echo '<script>
6901d05cddcSAtari911        let refreshInterval = null;
6911d05cddcSAtari911        let isPaused = false;
6921d05cddcSAtari911
6931d05cddcSAtari911        function refreshLog() {
6941d05cddcSAtari911            if (isPaused) return;
6951d05cddcSAtari911
696*2866e827SAtari911            fetch("?do=admin&page=calendar&action=get_log&call=ajax&sectok=" + JSINFO.sectok)
6971d05cddcSAtari911                .then(response => response.json())
6981d05cddcSAtari911                .then(data => {
6991d05cddcSAtari911                    const logContent = document.getElementById("logContent");
7001d05cddcSAtari911                    if (logContent) {
701da206178SAtari911                        logContent.textContent = data.log || "No log data available";
7021d05cddcSAtari911                        logContent.scrollTop = logContent.scrollHeight;
7031d05cddcSAtari911                    }
7041d05cddcSAtari911                })
7051d05cddcSAtari911                .catch(error => {
7061d05cddcSAtari911                    console.error("Error fetching log:", error);
7071d05cddcSAtari911                });
7081d05cddcSAtari911        }
7091d05cddcSAtari911
7101d05cddcSAtari911        function togglePause() {
7111d05cddcSAtari911            isPaused = !isPaused;
7121d05cddcSAtari911            const btn = document.getElementById("pauseBtn");
7131d05cddcSAtari911            if (isPaused) {
714da206178SAtari911                btn.textContent = "▶ Resume";
7151d05cddcSAtari911                btn.style.background = "#00cc07";
7161d05cddcSAtari911            } else {
717da206178SAtari911                btn.textContent = "⏸ Pause";
7181d05cddcSAtari911                btn.style.background = "#666";
7191d05cddcSAtari911                refreshLog();
7201d05cddcSAtari911            }
7211d05cddcSAtari911        }
7221d05cddcSAtari911
7231d05cddcSAtari911        function clearLog() {
724da206178SAtari911            if (!confirm("Clear the sync log file?\\n\\nThis will delete all log entries.")) {
7251d05cddcSAtari911                return;
7261d05cddcSAtari911            }
7271d05cddcSAtari911
728*2866e827SAtari911            fetch("?do=admin&page=calendar&action=clear_log&call=ajax&sectok=" + JSINFO.sectok, {
7291d05cddcSAtari911                method: "POST"
7301d05cddcSAtari911            })
7311d05cddcSAtari911                .then(response => response.json())
7321d05cddcSAtari911                .then(data => {
7331d05cddcSAtari911                    if (data.success) {
7341d05cddcSAtari911                        refreshLog();
735da206178SAtari911                        alert("Log cleared successfully");
7361d05cddcSAtari911                    } else {
737da206178SAtari911                        alert("Error clearing log: " + data.message);
7381d05cddcSAtari911                    }
7391d05cddcSAtari911                })
7401d05cddcSAtari911                .catch(error => {
741da206178SAtari911                    alert("Error: " + error.message);
7421d05cddcSAtari911                });
7431d05cddcSAtari911        }
7441d05cddcSAtari911
7451d05cddcSAtari911        function downloadLog() {
7461d05cddcSAtari911            window.location.href = "?do=admin&page=calendar&action=download_log";
7471d05cddcSAtari911        }
7481d05cddcSAtari911
7491d05cddcSAtari911        // Start auto-refresh
7501d05cddcSAtari911        refreshLog();
7511d05cddcSAtari911        refreshInterval = setInterval(refreshLog, 2000);
7521d05cddcSAtari911
7531d05cddcSAtari911        // Cleanup on page unload
7541d05cddcSAtari911        window.addEventListener("beforeunload", function() {
7551d05cddcSAtari911            if (refreshInterval) {
7561d05cddcSAtari911                clearInterval(refreshInterval);
7571d05cddcSAtari911            }
7581d05cddcSAtari911        });
7591d05cddcSAtari911        </script>';
7601d05cddcSAtari911    }
7611d05cddcSAtari911
7629ccd446eSAtari911    private function renderManageTab($colors = null) {
7631d05cddcSAtari911        global $INPUT;
7641d05cddcSAtari911
7659ccd446eSAtari911        // Use defaults if not provided
7669ccd446eSAtari911        if ($colors === null) {
7679ccd446eSAtari911            $colors = $this->getTemplateColors();
7689ccd446eSAtari911        }
7699ccd446eSAtari911
7701d05cddcSAtari911        // Show message if present
7711d05cddcSAtari911        if ($INPUT->has('msg')) {
7721d05cddcSAtari911            $msg = hsc($INPUT->str('msg'));
7731d05cddcSAtari911            $type = $INPUT->str('msgtype', 'success');
7741d05cddcSAtari911            echo "<div style=\"padding:10px; margin:10px 0; border-left:3px solid " . ($type === 'success' ? '#28a745' : '#dc3545') . "; background:" . ($type === 'success' ? '#d4edda' : '#f8d7da') . "; border-radius:3px;\">";
7751d05cddcSAtari911            echo $msg;
7761d05cddcSAtari911            echo "</div>";
7771d05cddcSAtari911        }
7781d05cddcSAtari911
77922228b0eSAtari911        echo '<h2 style="margin:10px 0; font-size:20px;">' . $this->getLang('manage_calendar_events') . '</h2>';
7801d05cddcSAtari911
7819ccd446eSAtari911        // Events Manager Section
7829ccd446eSAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:1200px;">';
78322228b0eSAtari911        echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">�� ' . $this->getLang('events_manager') . '</h3>';
78422228b0eSAtari911        echo '<p style="color:' . $colors['text'] . '; font-size:11px; margin:0 0 10px;">' . $this->getLang('events_manager_desc') . '</p>';
7851d05cddcSAtari911
7861d05cddcSAtari911        // Get event statistics
7871d05cddcSAtari911        $stats = $this->getEventStatistics();
7881d05cddcSAtari911
7891d05cddcSAtari911        // Statistics display
7909ccd446eSAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:10px; border-radius:3px; margin-bottom:10px; border:1px solid ' . $colors['border'] . ';">';
7911d05cddcSAtari911        echo '<div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(150px, 1fr)); gap:10px; font-size:12px;">';
7921d05cddcSAtari911
7931d05cddcSAtari911        echo '<div style="background:#f3e5f5; padding:8px; border-radius:3px; text-align:center;">';
7941d05cddcSAtari911        echo '<div style="font-size:24px; font-weight:bold; color:#7b1fa2;">' . $stats['total_events'] . '</div>';
79522228b0eSAtari911        echo '<div style="color:' . $colors['text'] . '; font-size:10px;">' . $this->getLang('total_events') . '</div>';
7961d05cddcSAtari911        echo '</div>';
7971d05cddcSAtari911
7981d05cddcSAtari911        echo '<div style="background:#f3e5f5; padding:8px; border-radius:3px; text-align:center;">';
7991d05cddcSAtari911        echo '<div style="font-size:24px; font-weight:bold; color:#7b1fa2;">' . $stats['total_namespaces'] . '</div>';
80022228b0eSAtari911        echo '<div style="color:' . $colors['text'] . '; font-size:10px;">' . $this->getLang('namespaces') . '</div>';
8011d05cddcSAtari911        echo '</div>';
8021d05cddcSAtari911
8031d05cddcSAtari911        echo '<div style="background:#e8f5e9; padding:8px; border-radius:3px; text-align:center;">';
8041d05cddcSAtari911        echo '<div style="font-size:24px; font-weight:bold; color:#388e3c;">' . $stats['total_files'] . '</div>';
80522228b0eSAtari911        echo '<div style="color:' . $colors['text'] . '; font-size:10px;">' . $this->getLang('json_files') . '</div>';
8061d05cddcSAtari911        echo '</div>';
8071d05cddcSAtari911
8081d05cddcSAtari911        echo '<div style="background:#fff3e0; padding:8px; border-radius:3px; text-align:center;">';
8091d05cddcSAtari911        echo '<div style="font-size:24px; font-weight:bold; color:#f57c00;">' . $stats['total_recurring'] . '</div>';
81022228b0eSAtari911        echo '<div style="color:' . $colors['text'] . '; font-size:10px;">' . $this->getLang('recurring') . '</div>';
8111d05cddcSAtari911        echo '</div>';
8121d05cddcSAtari911
8131d05cddcSAtari911        echo '</div>';
8141d05cddcSAtari911
8151d05cddcSAtari911        // Last scan time
8161d05cddcSAtari911        if (!empty($stats['last_scan'])) {
81722228b0eSAtari911            echo '<div style="margin-top:8px; color:' . $colors['text'] . '; font-size:10px;">' . $this->getLang('last_scanned') . ': ' . hsc($stats['last_scan']) . '</div>';
8181d05cddcSAtari911        }
8191d05cddcSAtari911
8201d05cddcSAtari911        echo '</div>';
8211d05cddcSAtari911
8221d05cddcSAtari911        // Action buttons
8231d05cddcSAtari911        echo '<div style="display:flex; gap:10px; flex-wrap:wrap; margin-top:10px;">';
8241d05cddcSAtari911
8251d05cddcSAtari911        // Rescan button
8261d05cddcSAtari911        echo '<form method="post" action="?do=admin&page=calendar&tab=manage" style="display:inline;">';
827*2866e827SAtari911        echo formSecurityToken(false);
8281d05cddcSAtari911        echo '<input type="hidden" name="action" value="rescan_events">';
8291d05cddcSAtari911        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;">';
83022228b0eSAtari911        echo '<span>��</span><span>' . $this->getLang('rescan_events') . '</span>';
8311d05cddcSAtari911        echo '</button>';
8321d05cddcSAtari911        echo '</form>';
8331d05cddcSAtari911
8341d05cddcSAtari911        // Export button
8351d05cddcSAtari911        echo '<form method="post" action="?do=admin&page=calendar&tab=manage" style="display:inline;">';
836*2866e827SAtari911        echo formSecurityToken(false);
8371d05cddcSAtari911        echo '<input type="hidden" name="action" value="export_all_events">';
8381d05cddcSAtari911        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;">';
83922228b0eSAtari911        echo '<span>��</span><span>' . $this->getLang('export_all_events') . '</span>';
8401d05cddcSAtari911        echo '</button>';
8411d05cddcSAtari911        echo '</form>';
8421d05cddcSAtari911
8431d05cddcSAtari911        // Import button (with file upload)
84422228b0eSAtari911        echo '<form method="post" action="?do=admin&page=calendar&tab=manage" enctype="multipart/form-data" style="display:inline;" onsubmit="return confirm(\'' . $this->getLang('import_confirm') . '\')">';
845*2866e827SAtari911        echo formSecurityToken(false);
8461d05cddcSAtari911        echo '<input type="hidden" name="action" value="import_all_events">';
8471d05cddcSAtari911        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;">';
84822228b0eSAtari911        echo '<span>��</span><span>' . $this->getLang('import_events') . '</span>';
8491d05cddcSAtari911        echo '<input type="file" name="import_file" accept=".json,.zip" required style="display:none;" onchange="this.form.submit()">';
8501d05cddcSAtari911        echo '</label>';
8511d05cddcSAtari911        echo '</form>';
8521d05cddcSAtari911
8531d05cddcSAtari911        echo '</div>';
8541d05cddcSAtari911
8551d05cddcSAtari911        // Breakdown by namespace
8561d05cddcSAtari911        if (!empty($stats['by_namespace'])) {
8571d05cddcSAtari911            echo '<details style="margin-top:12px;">';
85822228b0eSAtari911            echo '<summary style="cursor:pointer; padding:6px; background:#e9e9e9; border-radius:3px; font-size:11px; font-weight:bold;">' . $this->getLang('view_breakdown') . '</summary>';
8599ccd446eSAtari911            echo '<div style="margin-top:8px; max-height:200px; overflow-y:auto; border:1px solid ' . $colors['border'] . '; border-radius:3px;">';
8601d05cddcSAtari911            echo '<table style="width:100%; border-collapse:collapse; font-size:11px;">';
8611d05cddcSAtari911            echo '<thead style="position:sticky; top:0; background:#f5f5f5;">';
8621d05cddcSAtari911            echo '<tr>';
86322228b0eSAtari911            echo '<th style="padding:4px 6px; text-align:left; border-bottom:2px solid #ddd;">' . $this->getLang('namespace') . '</th>';
86422228b0eSAtari911            echo '<th style="padding:4px 6px; text-align:right; border-bottom:2px solid #ddd;">' . $this->getLang('events_column') . '</th>';
86522228b0eSAtari911            echo '<th style="padding:4px 6px; text-align:right; border-bottom:2px solid #ddd;">' . $this->getLang('files_column') . '</th>';
8661d05cddcSAtari911            echo '</tr></thead><tbody>';
8671d05cddcSAtari911
8681d05cddcSAtari911            foreach ($stats['by_namespace'] as $ns => $nsStats) {
8691d05cddcSAtari911                echo '<tr style="border-bottom:1px solid #eee;">';
87022228b0eSAtari911                echo '<td style="padding:4px 6px;"><code style="background:#f0f0f0; padding:1px 3px; border-radius:2px; font-size:10px;">' . hsc($ns ?: $this->getLang('default_ns')) . '</code></td>';
8711d05cddcSAtari911                echo '<td style="padding:4px 6px; text-align:right;"><strong>' . $nsStats['events'] . '</strong></td>';
8721d05cddcSAtari911                echo '<td style="padding:4px 6px; text-align:right;">' . $nsStats['files'] . '</td>';
8731d05cddcSAtari911                echo '</tr>';
8741d05cddcSAtari911            }
8751d05cddcSAtari911
8761d05cddcSAtari911            echo '</tbody></table>';
8771d05cddcSAtari911            echo '</div>';
8781d05cddcSAtari911            echo '</details>';
8791d05cddcSAtari911        }
8801d05cddcSAtari911
8811d05cddcSAtari911        echo '</div>';
8821d05cddcSAtari911
8834590242dSAtari911        // Important Namespaces Section
884*2866e827SAtari911        $configFile = $this->syncConfigPath();
8854590242dSAtari911        $importantConfig = [];
8864590242dSAtari911        if (file_exists($configFile)) {
8874590242dSAtari911            $importantConfig = include $configFile;
8884590242dSAtari911        }
8894590242dSAtari911        $importantNsValue = isset($importantConfig['important_namespaces']) ? $importantConfig['important_namespaces'] : 'important';
8904590242dSAtari911
8914590242dSAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:1200px;">';
89222228b0eSAtari911        echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">⭐ ' . $this->getLang('important_namespaces') . '</h3>';
89322228b0eSAtari911        echo '<p style="color:' . $colors['text'] . '; font-size:11px; margin:0 0 8px;">' . $this->getLang('important_ns_desc') . '</p>';
89496df7d3eSAtari911
89596df7d3eSAtari911        // Effects description
89696df7d3eSAtari911        echo '<div style="background:rgba(0,204,7,0.05); padding:8px 10px; margin:0 0 10px; border-radius:3px; font-size:10px; color:' . $colors['text'] . ';">';
89722228b0eSAtari911        echo '<strong style="color:#00cc07;">' . $this->getLang('visual_effects') . ':</strong><br>';
89822228b0eSAtari911        echo '• ' . $this->getLang('effect_grid') . '<br>';
89922228b0eSAtari911        echo '• ' . $this->getLang('effect_sidebar') . '<br>';
90022228b0eSAtari911        echo '• ' . $this->getLang('effect_widget') . '<br>';
90122228b0eSAtari911        echo '• ' . $this->getLang('effect_popup');
90296df7d3eSAtari911        echo '</div>';
90396df7d3eSAtari911
9044590242dSAtari911        echo '<form method="post" action="?do=admin&page=calendar&tab=manage" style="display:flex; gap:8px; align-items:center;">';
905*2866e827SAtari911        echo formSecurityToken(false);
9064590242dSAtari911        echo '<input type="hidden" name="action" value="save_important_namespaces">';
9074590242dSAtari911        echo '<input type="text" name="important_namespaces" value="' . hsc($importantNsValue) . '" style="flex:1; padding:6px; border:1px solid ' . $colors['border'] . '; border-radius:3px; font-size:12px;" placeholder="important,urgent,priority">';
90822228b0eSAtari911        echo '<button type="submit" style="background:#00cc07; color:white; padding:6px 16px; border:none; border-radius:3px; cursor:pointer; font-size:12px; font-weight:bold; white-space:nowrap;">' . $this->getLang('save') . '</button>';
9094590242dSAtari911        echo '</form>';
91022228b0eSAtari911        echo '<p style="color:' . $colors['text'] . '; font-size:10px; margin:4px 0 0;">' . $this->getLang('important_ns_hint') . '</p>';
9114590242dSAtari911        echo '</div>';
9124590242dSAtari911
9139ccd446eSAtari911        // Cleanup Events Section
9149ccd446eSAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:1200px;">';
91522228b0eSAtari911        echo '<h3 style="margin:0 0 6px 0; color:#00cc07; font-size:16px;">�� ' . $this->getLang('cleanup_old_events') . '</h3>';
91622228b0eSAtari911        echo '<p style="color:' . $colors['text'] . '; font-size:11px; margin:0 0 12px;">' . $this->getLang('cleanup_desc') . '</p>';
9171d05cddcSAtari911
9181d05cddcSAtari911        echo '<form method="post" action="?do=admin&page=calendar&tab=manage" id="cleanupForm">';
919*2866e827SAtari911        echo formSecurityToken(false);
9201d05cddcSAtari911        echo '<input type="hidden" name="action" value="cleanup_events">';
9211d05cddcSAtari911
9221d05cddcSAtari911        // Compact options layout
9239ccd446eSAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:10px; border:1px solid ' . $colors['border'] . '; border-radius:3px; margin-bottom:10px;">';
9241d05cddcSAtari911
9251d05cddcSAtari911        // Radio buttons in a row
9261d05cddcSAtari911        echo '<div style="display:flex; gap:20px; margin-bottom:10px; padding-bottom:8px; border-bottom:1px solid #f0f0f0;">';
9271d05cddcSAtari911        echo '<label style="cursor:pointer; font-size:12px; font-weight:600; display:flex; align-items:center; gap:4px;">';
9281d05cddcSAtari911        echo '<input type="radio" name="cleanup_type" value="age" checked onchange="updateCleanupOptions()">';
92922228b0eSAtari911        echo '<span>' . $this->getLang('by_age') . '</span>';
9301d05cddcSAtari911        echo '</label>';
9311d05cddcSAtari911        echo '<label style="cursor:pointer; font-size:12px; font-weight:600; display:flex; align-items:center; gap:4px;">';
9321d05cddcSAtari911        echo '<input type="radio" name="cleanup_type" value="status" onchange="updateCleanupOptions()">';
93322228b0eSAtari911        echo '<span>' . $this->getLang('by_status') . '</span>';
9341d05cddcSAtari911        echo '</label>';
9351d05cddcSAtari911        echo '<label style="cursor:pointer; font-size:12px; font-weight:600; display:flex; align-items:center; gap:4px;">';
9361d05cddcSAtari911        echo '<input type="radio" name="cleanup_type" value="range" onchange="updateCleanupOptions()">';
93722228b0eSAtari911        echo '<span>' . $this->getLang('by_date_range') . '</span>';
9381d05cddcSAtari911        echo '</label>';
9391d05cddcSAtari911        echo '</div>';
9401d05cddcSAtari911
9411d05cddcSAtari911        // Age options
9421d05cddcSAtari911        echo '<div id="age-options" style="padding:6px 0;">';
94322228b0eSAtari911        echo '<span style="font-size:11px; color:' . $colors['text'] . '; margin-right:8px;">' . $this->getLang('delete_older_than') . ':</span>';
9441d05cddcSAtari911        echo '<select name="age_value" style="width:50px; padding:3px 4px; font-size:11px; border:1px solid #d0d0d0; border-radius:3px; margin-right:4px;">';
9451d05cddcSAtari911        for ($i = 1; $i <= 24; $i++) {
9461d05cddcSAtari911            $sel = $i === 6 ? ' selected' : '';
9471d05cddcSAtari911            echo '<option value="' . $i . '"' . $sel . '>' . $i . '</option>';
9481d05cddcSAtari911        }
9491d05cddcSAtari911        echo '</select>';
9501d05cddcSAtari911        echo '<select name="age_unit" style="width:80px; padding:3px 4px; font-size:11px; border:1px solid #d0d0d0; border-radius:3px;">';
95122228b0eSAtari911        echo '<option value="months" selected>' . $this->getLang('months') . '</option>';
95222228b0eSAtari911        echo '<option value="years">' . $this->getLang('years') . '</option>';
9531d05cddcSAtari911        echo '</select>';
9541d05cddcSAtari911        echo '</div>';
9551d05cddcSAtari911
9561d05cddcSAtari911        // Status options
9571d05cddcSAtari911        echo '<div id="status-options" style="padding:6px 0; opacity:0.4;">';
95822228b0eSAtari911        echo '<span style="font-size:11px; color:' . $colors['text'] . '; margin-right:8px;">' . $this->getLang('delete') . ':</span>';
95922228b0eSAtari911        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;"> ' . $this->getLang('completed_tasks') . '</label>';
96022228b0eSAtari911        echo '<label style="display:inline-block; font-size:11px; cursor:pointer;"><input type="checkbox" name="delete_past" value="1" style="margin-right:3px;"> ' . $this->getLang('past_events') . '</label>';
9611d05cddcSAtari911        echo '</div>';
9621d05cddcSAtari911
9631d05cddcSAtari911        // Range options
9641d05cddcSAtari911        echo '<div id="range-options" style="padding:6px 0; opacity:0.4;">';
96522228b0eSAtari911        echo '<span style="font-size:11px; color:' . $colors['text'] . '; margin-right:8px;">' . $this->getLang('from_date') . ':</span>';
9661d05cddcSAtari911        echo '<input type="date" name="range_start" style="padding:3px 6px; font-size:11px; border:1px solid #d0d0d0; border-radius:3px; margin-right:10px;">';
96722228b0eSAtari911        echo '<span style="font-size:11px; color:' . $colors['text'] . '; margin-right:8px;">' . $this->getLang('to_date') . ':</span>';
9681d05cddcSAtari911        echo '<input type="date" name="range_end" style="padding:3px 6px; font-size:11px; border:1px solid #d0d0d0; border-radius:3px;">';
9691d05cddcSAtari911        echo '</div>';
9701d05cddcSAtari911
9711d05cddcSAtari911        echo '</div>';
9721d05cddcSAtari911
9731d05cddcSAtari911        // Namespace filter - compact
9749ccd446eSAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:8px 10px; border:1px solid ' . $colors['border'] . '; border-radius:3px; margin-bottom:10px; display:flex; align-items:center; gap:8px;">';
97522228b0eSAtari911        echo '<label style="font-size:11px; font-weight:600; white-space:nowrap; color:#555;">' . $this->getLang('namespace_filter') . ':</label>';
97622228b0eSAtari911        echo '<input type="text" name="namespace_filter" placeholder="' . $this->getLang('namespace_filter_hint') . '" style="flex:1; padding:4px 8px; font-size:11px; border:1px solid #d0d0d0; border-radius:3px;">';
9771d05cddcSAtari911        echo '</div>';
9781d05cddcSAtari911
9791d05cddcSAtari911        // Action buttons - compact row
9801d05cddcSAtari911        echo '<div style="display:flex; gap:8px; align-items:center;">';
98122228b0eSAtari911        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;">��️ ' . $this->getLang('preview') . '</button>';
98222228b0eSAtari911        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;">��️ ' . $this->getLang('delete') . '</button>';
98322228b0eSAtari911        echo '<span style="font-size:10px; color:#999;">⚠️ ' . $this->getLang('backup_auto') . '</span>';
9841d05cddcSAtari911        echo '</div>';
9851d05cddcSAtari911
9861d05cddcSAtari911        echo '</form>';
9871d05cddcSAtari911
9881d05cddcSAtari911        // Preview results area
9891d05cddcSAtari911        echo '<div id="cleanup-preview" style="margin-top:10px; display:none;"></div>';
9901d05cddcSAtari911
99122228b0eSAtari911        // Store language strings for JavaScript
99222228b0eSAtari911        $jsLang = [
99322228b0eSAtari911            'loading_preview' => $this->getLang('loading_preview'),
99422228b0eSAtari911            'no_events_match' => $this->getLang('no_events_match'),
99522228b0eSAtari911            'debug_info' => $this->getLang('debug_info'),
99622228b0eSAtari911            'error_loading' => $this->getLang('error_loading'),
99722228b0eSAtari911            'cleanup_confirm' => $this->getLang('cleanup_confirm'),
99822228b0eSAtari911        ];
99922228b0eSAtari911
10001d05cddcSAtari911        echo '<script>
100122228b0eSAtari911        var calendarLang = ' . json_encode($jsLang) . ';
100222228b0eSAtari911
10031d05cddcSAtari911        function updateCleanupOptions() {
10041d05cddcSAtari911            const type = document.querySelector(\'input[name="cleanup_type"]:checked\').value;
10051d05cddcSAtari911
10061d05cddcSAtari911            // Show selected, gray out others
10071d05cddcSAtari911            document.getElementById(\'age-options\').style.opacity = type === \'age\' ? \'1\' : \'0.4\';
10081d05cddcSAtari911            document.getElementById(\'status-options\').style.opacity = type === \'status\' ? \'1\' : \'0.4\';
10091d05cddcSAtari911            document.getElementById(\'range-options\').style.opacity = type === \'range\' ? \'1\' : \'0.4\';
10101d05cddcSAtari911
10111d05cddcSAtari911            // Enable/disable inputs
10121d05cddcSAtari911            document.querySelectorAll(\'#age-options select\').forEach(el => el.disabled = type !== \'age\');
10131d05cddcSAtari911            document.querySelectorAll(\'#status-options input\').forEach(el => el.disabled = type !== \'status\');
10141d05cddcSAtari911            document.querySelectorAll(\'#range-options input\').forEach(el => el.disabled = type !== \'range\');
10151d05cddcSAtari911        }
10161d05cddcSAtari911
10171d05cddcSAtari911        function previewCleanup() {
10181d05cddcSAtari911            const form = document.getElementById(\'cleanupForm\');
10191d05cddcSAtari911            const formData = new FormData(form);
10201d05cddcSAtari911            formData.set(\'action\', \'preview_cleanup\');
10211d05cddcSAtari911
10221d05cddcSAtari911            const preview = document.getElementById(\'cleanup-preview\');
102322228b0eSAtari911            preview.innerHTML = \'<div style="text-align:center; padding:20px; color:' . $colors['text'] . ';">\' + calendarLang.loading_preview + \'</div>\';
10241d05cddcSAtari911            preview.style.display = \'block\';
10251d05cddcSAtari911
10261d05cddcSAtari911            fetch(\'?do=admin&page=calendar&tab=manage\', {
10271d05cddcSAtari911                method: \'POST\',
10281d05cddcSAtari911                body: new URLSearchParams(formData)
10291d05cddcSAtari911            })
10301d05cddcSAtari911            .then(r => r.json())
10311d05cddcSAtari911            .then(data => {
10321d05cddcSAtari911                if (data.count === 0) {
103322228b0eSAtari911                    let html = \'<div style="background:#d4edda; border:1px solid #c3e6cb; padding:10px; border-radius:3px; font-size:12px; color:#155724;">✅ \' + calendarLang.no_events_match + \'</div>\';
10341d05cddcSAtari911
10351d05cddcSAtari911                    // Show debug info if available
10361d05cddcSAtari911                    if (data.debug) {
10379ccd446eSAtari911                        html += \'<details style="margin-top:8px; font-size:11px; color:' . $colors['text'] . ';">\';
103822228b0eSAtari911                        html += \'<summary style="cursor:pointer;">\' + calendarLang.debug_info + \'</summary>\';
10391d05cddcSAtari911                        html += \'<pre style="background:#f5f5f5; padding:6px; margin-top:4px; border-radius:3px; overflow-x:auto;">\' + JSON.stringify(data.debug, null, 2) + \'</pre>\';
10401d05cddcSAtari911                        html += \'</details>\';
10411d05cddcSAtari911                    }
10421d05cddcSAtari911
10431d05cddcSAtari911                    preview.innerHTML = html;
10441d05cddcSAtari911                } else {
10451d05cddcSAtari911                    let html = \'<div style="background:#f8d7da; border:1px solid #f5c6cb; padding:10px; border-radius:3px; font-size:12px; color:#721c24;">\';
104622228b0eSAtari911                    html += \'<strong>⚠️</strong> \' + data.count + \' event(s):<br><br>\';
10479ccd446eSAtari911                    html += \'<div style="max-height:150px; overflow-y:auto; margin-top:6px; background:' . $colors['bg'] . '; padding:6px; border-radius:3px;">\';
10481d05cddcSAtari911                    data.events.forEach(evt => {
10491d05cddcSAtari911                        html += \'<div style="padding:3px; border-bottom:1px solid #eee; font-size:11px;">\';
10501d05cddcSAtari911                        html += \'\' + evt.title + \' (\' + evt.date + \')\';
10511d05cddcSAtari911                        if (evt.namespace) html += \' <span style="background:#e3f2fd; padding:1px 4px; border-radius:2px; font-size:9px;">\' + evt.namespace + \'</span>\';
10521d05cddcSAtari911                        html += \'</div>\';
10531d05cddcSAtari911                    });
10541d05cddcSAtari911                    html += \'</div></div>\';
10551d05cddcSAtari911                    preview.innerHTML = html;
10561d05cddcSAtari911                }
10571d05cddcSAtari911            })
10581d05cddcSAtari911            .catch(err => {
105922228b0eSAtari911                preview.innerHTML = \'<div style="background:#f8d7da; border:1px solid #f5c6cb; padding:10px; border-radius:3px; font-size:12px; color:#721c24;">\' + calendarLang.error_loading + \'</div>\';
10601d05cddcSAtari911            });
10611d05cddcSAtari911        }
10621d05cddcSAtari911
10631d05cddcSAtari911        function confirmCleanup() {
106422228b0eSAtari911            return confirm(calendarLang.cleanup_confirm);
10651d05cddcSAtari911        }
10661d05cddcSAtari911
10671d05cddcSAtari911        updateCleanupOptions();
10681d05cddcSAtari911        </script>';
10691d05cddcSAtari911
10701d05cddcSAtari911        echo '</div>';
10711d05cddcSAtari911
10721d05cddcSAtari911        // Recurring Events Section
10737e8ea635SAtari911        echo '<div id="recurring-section" style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:1200px;">';
10747e8ea635SAtari911        echo '<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:8px;">';
107522228b0eSAtari911        echo '<h3 style="margin:0; color:#00cc07; font-size:16px;">�� ' . $this->getLang('recurring_events') . '</h3>';
10767e8ea635SAtari911        echo '<div style="display:flex; gap:6px;">';
107722228b0eSAtari911        echo '<button onclick="trimAllPastRecurring()" id="trim-all-past-btn" style="background:#e74c3c; color:#fff; border:none; padding:4px 12px; border-radius:3px; cursor:pointer; font-size:11px; font-weight:600; transition:all 0.15s;" onmouseover="this.style.filter=\'brightness(1.2)\'" onmouseout="this.style.filter=\'none\'">✂️ ' . $this->getLang('trim_all_past') . '</button>';
107822228b0eSAtari911        echo '<button onclick="rescanRecurringEvents()" id="rescan-recurring-btn" style="background:#00cc07; color:#fff; border:none; padding:4px 12px; border-radius:3px; cursor:pointer; font-size:11px; font-weight:600; transition:all 0.15s;" onmouseover="this.style.filter=\'brightness(1.2)\'" onmouseout="this.style.filter=\'none\'">�� ' . $this->getLang('rescan') . '</button>';
10797e8ea635SAtari911        echo '</div>';
10807e8ea635SAtari911        echo '</div>';
10811d05cddcSAtari911
10821d05cddcSAtari911        $recurringEvents = $this->findRecurringEvents();
10831d05cddcSAtari911
10847e8ea635SAtari911        echo '<div id="recurring-content">';
10857e8ea635SAtari911        $this->renderRecurringTable($recurringEvents, $colors);
10861d05cddcSAtari911        echo '</div>';
10871d05cddcSAtari911        echo '</div>';
10881d05cddcSAtari911
10891d05cddcSAtari911        // Compact Tree-based Namespace Manager
10909ccd446eSAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:1200px;">';
109122228b0eSAtari911        echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">�� ' . $this->getLang('namespace_explorer') . '</h3>';
109222228b0eSAtari911        echo '<p style="color:' . $colors['text'] . '; font-size:11px; margin:0 0 8px;">' . $this->getLang('namespace_explorer_desc') . '</p>';
10931d05cddcSAtari911
10941d05cddcSAtari911        // Search bar
10951d05cddcSAtari911        echo '<div style="margin-bottom:8px;">';
109622228b0eSAtari911        echo '<input type="text" id="searchEvents" onkeyup="filterEvents()" placeholder="�� ' . $this->getLang('search_events') . '" style="width:100%; padding:6px 10px; border:1px solid ' . $colors['border'] . '; border-radius:3px; font-size:12px;">';
10971d05cddcSAtari911        echo '</div>';
10981d05cddcSAtari911
10991d05cddcSAtari911        $eventsByNamespace = $this->getEventsByNamespace();
11001d05cddcSAtari911
11011d05cddcSAtari911        // Control bar
11021d05cddcSAtari911        echo '<form method="post" action="?do=admin&page=calendar&tab=manage" id="moveForm">';
1103*2866e827SAtari911        echo formSecurityToken(false);
11041d05cddcSAtari911        echo '<input type="hidden" name="action" value="move_selected_events" id="formAction">';
11051d05cddcSAtari911        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;">';
110622228b0eSAtari911        echo '<button type="button" onclick="selectAll()" style="background:#00cc07; color:white; border:none; padding:4px 8px; border-radius:2px; cursor:pointer; font-size:11px;">☑ ' . $this->getLang('select_all') . '</button>';
110722228b0eSAtari911        echo '<button type="button" onclick="deselectAll()" style="background:#666; color:white; border:none; padding:4px 8px; border-radius:2px; cursor:pointer; font-size:11px;">☐ ' . $this->getLang('select_none') . '</button>';
110822228b0eSAtari911        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;">��️ ' . $this->getLang('delete') . '</button>';
110922228b0eSAtari911        echo '<span style="margin-left:10px;">' . $this->getLang('move_to') . ':</span>';
111022228b0eSAtari911        echo '<input list="namespaceList" name="target_namespace" required style="padding:3px 6px; border:1px solid ' . $colors['border'] . '; border-radius:2px; font-size:11px; min-width:150px;" placeholder="' . $this->getLang('type_or_select') . '">';
11111d05cddcSAtari911        echo '<datalist id="namespaceList">';
111222228b0eSAtari911        echo '<option value="">' . $this->getLang('default_ns') . '</option>';
11131d05cddcSAtari911        foreach (array_keys($eventsByNamespace) as $ns) {
11141d05cddcSAtari911            if ($ns !== '') {
11151d05cddcSAtari911                echo '<option value="' . hsc($ns) . '">' . hsc($ns) . '</option>';
11161d05cddcSAtari911            }
11171d05cddcSAtari911        }
11181d05cddcSAtari911        echo '</datalist>';
111922228b0eSAtari911        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;">➡️ ' . $this->getLang('btn_move') . '</button>';
112022228b0eSAtari911        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;">➕ ' . $this->getLang('new_namespace') . '</button>';
112122228b0eSAtari911        echo '<button type="button" onclick="cleanupEmptyNamespaces()" id="cleanup-ns-btn" style="background:#e74c3c; color:white; border:none; padding:4px 10px; border-radius:2px; cursor:pointer; font-size:11px; font-weight:bold; margin-left:5px;">�� ' . $this->getLang('cleanup_empty') . '</button>';
112222228b0eSAtari911        echo '<span id="selectedCount" style="margin-left:auto; color:#00cc07; font-size:11px;">' . $this->getLang('zero_selected') . '</span>';
11231d05cddcSAtari911        echo '</div>';
11241d05cddcSAtari911
11257e8ea635SAtari911        // Cleanup status message - displayed prominently after control bar
11267e8ea635SAtari911        echo '<div id="cleanup-ns-status" style="font-size:12px; margin-bottom:8px; min-height:18px;"></div>';
11277e8ea635SAtari911
11281d05cddcSAtari911        echo '<div style="display:grid; grid-template-columns:1fr 1fr; gap:10px;">';
11291d05cddcSAtari911
11301d05cddcSAtari911        // Event list with checkboxes
11311d05cddcSAtari911        echo '<div>';
11329ccd446eSAtari911        echo '<div style="max-height:450px; overflow-y:auto; border:1px solid ' . $colors['border'] . '; border-radius:3px; background:' . $colors['bg'] . ';">';
11331d05cddcSAtari911
11341d05cddcSAtari911        foreach ($eventsByNamespace as $namespace => $data) {
11351d05cddcSAtari911            $nsId = 'ns_' . md5($namespace);
113696df7d3eSAtari911            $events = isset($data['events']) && is_array($data['events']) ? $data['events'] : [];
113796df7d3eSAtari911            $eventCount = count($events);
11381d05cddcSAtari911
11391d05cddcSAtari911            echo '<div style="border-bottom:1px solid #ddd;">';
11401d05cddcSAtari911
11411d05cddcSAtari911            // Namespace header - ultra compact
11421d05cddcSAtari911            echo '<div style="background:#f5f5f5; padding:3px 6px; display:flex; justify-content:space-between; align-items:center; font-size:11px;">';
11431d05cddcSAtari911            echo '<div style="display:flex; align-items:center; gap:4px;">';
11441d05cddcSAtari911            echo '<span onclick="toggleNamespace(\'' . $nsId . '\')" style="cursor:pointer; width:12px; display:inline-block; font-size:10px;"><span id="' . $nsId . '_arrow">▶</span></span>';
11451d05cddcSAtari911            echo '<input type="checkbox" onclick="toggleNamespaceSelect(\'' . $nsId . '\')" id="' . $nsId . '_check" style="margin:0; width:12px; height:12px;">';
11461d05cddcSAtari911            echo '<span onclick="toggleNamespace(\'' . $nsId . '\')" style="cursor:pointer; font-weight:600; font-size:11px;">�� ' . hsc($namespace ?: '(default)') . '</span>';
11471d05cddcSAtari911            echo '</div>';
11481d05cddcSAtari911            echo '<div style="display:flex; gap:3px; align-items:center;">';
11491d05cddcSAtari911            echo '<span style="background:#00cc07; color:white; padding:0px 4px; border-radius:6px; font-size:9px; line-height:14px;">' . $eventCount . '</span>';
11509ccd446eSAtari911            echo '<button type="button" onclick="renameNamespace(\'' . hsc($namespace) . '\')" style="background:#3498db; color:white; border:none; padding:1px 4px; border-radius:2px; cursor:pointer; font-size:9px; line-height:14px;" title="Rename namespace">✏️</button>';
11511d05cddcSAtari911            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>';
11521d05cddcSAtari911            echo '</div>';
11531d05cddcSAtari911            echo '</div>';
11541d05cddcSAtari911
11551d05cddcSAtari911            // Events - ultra compact
11561d05cddcSAtari911            echo '<div id="' . $nsId . '" style="display:none; max-height:150px; overflow-y:auto;">';
115796df7d3eSAtari911            foreach ($events as $event) {
11581d05cddcSAtari911                $eventId = $event['id'] . '|' . $namespace . '|' . $event['date'] . '|' . $event['month'];
11591d05cddcSAtari911                $checkId = 'evt_' . md5($eventId);
11601d05cddcSAtari911
11611d05cddcSAtari911                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\'">';
11621d05cddcSAtari911                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;">';
11631d05cddcSAtari911                echo '<div style="flex:1; min-width:0;">';
11641d05cddcSAtari911                echo '<div style="font-weight:500; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; font-size:10px;">' . hsc($event['title']) . '</div>';
11651d05cddcSAtari911                echo '<div style="color:#999; font-size:9px;">' . hsc($event['date']) . ($event['startTime'] ? ' • ' . hsc($event['startTime']) : '') . '</div>';
11661d05cddcSAtari911                echo '</div>';
11671d05cddcSAtari911                echo '</div>';
11681d05cddcSAtari911            }
11691d05cddcSAtari911            echo '</div>';
11701d05cddcSAtari911            echo '</div>';
11711d05cddcSAtari911        }
11721d05cddcSAtari911
11731d05cddcSAtari911        echo '</div>';
11741d05cddcSAtari911        echo '</div>';
11751d05cddcSAtari911
11761d05cddcSAtari911        // Drop zones - ultra compact
11771d05cddcSAtari911        echo '<div>';
117822228b0eSAtari911        echo '<div style="background:#00cc07; color:white; padding:3px 6px; border-radius:3px 3px 0 0; font-size:11px; font-weight:bold;">�� ' . $this->getLang('drop_target') . '</div>';
11799ccd446eSAtari911        echo '<div style="border:1px solid ' . $colors['border'] . '; border-top:none; border-radius:0 0 3px 3px; max-height:450px; overflow-y:auto; background:' . $colors['bg'] . ';">';
11801d05cddcSAtari911
11811d05cddcSAtari911        foreach (array_keys($eventsByNamespace) as $namespace) {
11829ccd446eSAtari911            echo '<div ondrop="drop(event, \'' . hsc($namespace) . '\')" ondragover="allowDrop(event)" style="padding:5px 6px; border-bottom:1px solid #eee; background:' . $colors['bg'] . '; min-height:28px;" onmouseover="this.style.background=\'#f0fff0\'" onmouseout="this.style.background=\'white\'">';
118322228b0eSAtari911            echo '<div style="font-size:11px; font-weight:600; color:#00cc07;">�� ' . hsc($namespace ?: $this->getLang('default_ns')) . '</div>';
118422228b0eSAtari911            echo '<div style="color:#999; font-size:9px; margin-top:1px;">' . $this->getLang('drop_here') . '</div>';
11851d05cddcSAtari911            echo '</div>';
11861d05cddcSAtari911        }
11871d05cddcSAtari911
11881d05cddcSAtari911        echo '</div>';
11891d05cddcSAtari911        echo '</div>';
11901d05cddcSAtari911
11911d05cddcSAtari911        echo '</div>'; // end grid
11921d05cddcSAtari911        echo '</form>';
11931d05cddcSAtari911
11941d05cddcSAtari911        echo '</div>';
11951d05cddcSAtari911
119622228b0eSAtari911        // JavaScript language strings
119722228b0eSAtari911        $jsAdminLang = [
119822228b0eSAtari911            // Namespace explorer
119922228b0eSAtari911            'x_selected' => $this->getLang('x_selected'),
120022228b0eSAtari911            'zero_selected' => $this->getLang('zero_selected'),
120122228b0eSAtari911            'cleanup_empty' => $this->getLang('cleanup_empty'),
120222228b0eSAtari911            'default_ns' => $this->getLang('default_ns'),
120322228b0eSAtari911            'no_events_selected' => $this->getLang('no_events_selected'),
120422228b0eSAtari911            'delete_confirm' => $this->getLang('delete_confirm'),
120522228b0eSAtari911            'delete_ns_confirm' => $this->getLang('delete_ns_confirm'),
120622228b0eSAtari911            'scanning' => $this->getLang('scanning'),
120722228b0eSAtari911            'cleaning' => $this->getLang('cleaning'),
120822228b0eSAtari911            'no_empty_ns' => $this->getLang('no_empty_ns'),
120922228b0eSAtari911            'found_items' => $this->getLang('found_items'),
121022228b0eSAtari911            'proceed_cleanup' => $this->getLang('proceed_cleanup'),
121122228b0eSAtari911            'enter_namespace' => $this->getLang('enter_namespace'),
121222228b0eSAtari911            'invalid_namespace' => $this->getLang('invalid_namespace'),
121322228b0eSAtari911            'rename_namespace' => $this->getLang('rename_namespace'),
121422228b0eSAtari911            'delete_recurring_confirm' => $this->getLang('delete_recurring_confirm'),
121522228b0eSAtari911            'no_past_recurring' => $this->getLang('no_past_recurring'),
121622228b0eSAtari911            'found_past_recurring' => $this->getLang('found_past_recurring'),
121722228b0eSAtari911            'counting' => $this->getLang('counting'),
121822228b0eSAtari911            'trimming' => $this->getLang('trimming'),
121922228b0eSAtari911            'trim_confirm' => $this->getLang('trim_confirm'),
122022228b0eSAtari911            'respace_confirm' => $this->getLang('respace_confirm'),
122122228b0eSAtari911            'shift_confirm' => $this->getLang('shift_confirm'),
122222228b0eSAtari911            'trim_all_past' => $this->getLang('trim_all_past'),
122322228b0eSAtari911            // Manage recurring dialog
122422228b0eSAtari911            'manage_recurring_title' => $this->getLang('manage_recurring_title'),
122522228b0eSAtari911            'occurrences' => $this->getLang('occurrences'),
122622228b0eSAtari911            'extend_series' => $this->getLang('extend_series'),
122722228b0eSAtari911            'add_occurrences' => $this->getLang('add_occurrences'),
122822228b0eSAtari911            'days_apart' => $this->getLang('days_apart'),
122922228b0eSAtari911            'btn_extend' => $this->getLang('btn_extend'),
123022228b0eSAtari911            'trim_past_events' => $this->getLang('trim_past_events'),
123122228b0eSAtari911            'remove_before' => $this->getLang('remove_before'),
123222228b0eSAtari911            'change_pattern' => $this->getLang('change_pattern'),
123322228b0eSAtari911            'respace_note' => $this->getLang('respace_note'),
123422228b0eSAtari911            'new_interval' => $this->getLang('new_interval'),
123522228b0eSAtari911            'change_start_date' => $this->getLang('change_start_date'),
123622228b0eSAtari911            'shift_note' => $this->getLang('shift_note'),
123722228b0eSAtari911            'current_label' => $this->getLang('current_label'),
123822228b0eSAtari911            'pause_series' => $this->getLang('pause_series'),
123922228b0eSAtari911            'resume_series' => $this->getLang('resume_series'),
124022228b0eSAtari911            'pause_note' => $this->getLang('pause_note'),
124122228b0eSAtari911            'resume_note' => $this->getLang('resume_note'),
124222228b0eSAtari911            'btn_pause' => $this->getLang('btn_pause'),
124322228b0eSAtari911            'btn_resume' => $this->getLang('btn_resume'),
124422228b0eSAtari911            'btn_close' => $this->getLang('btn_close'),
124522228b0eSAtari911            'btn_trim' => $this->getLang('btn_trim'),
124622228b0eSAtari911            'btn_change' => $this->getLang('btn_change'),
124722228b0eSAtari911            'btn_shift' => $this->getLang('btn_shift'),
124822228b0eSAtari911            // Interval options
124922228b0eSAtari911            'interval_daily' => $this->getLang('interval_daily'),
125022228b0eSAtari911            'interval_weekly' => $this->getLang('interval_weekly'),
125122228b0eSAtari911            'interval_biweekly' => $this->getLang('interval_biweekly'),
125222228b0eSAtari911            'interval_monthly' => $this->getLang('interval_monthly'),
125322228b0eSAtari911            'interval_quarterly' => $this->getLang('interval_quarterly'),
125422228b0eSAtari911            'interval_yearly' => $this->getLang('interval_yearly'),
125522228b0eSAtari911            // Edit recurring dialog
125622228b0eSAtari911            'edit_recurring_title' => $this->getLang('edit_recurring_title'),
125722228b0eSAtari911            'changes_apply_to' => $this->getLang('changes_apply_to'),
125822228b0eSAtari911            'field_title' => $this->getLang('field_title'),
125922228b0eSAtari911            'field_start_time' => $this->getLang('field_start_time'),
126022228b0eSAtari911            'field_end_time' => $this->getLang('field_end_time'),
126122228b0eSAtari911            'field_namespace' => $this->getLang('field_namespace'),
126222228b0eSAtari911            'field_color' => $this->getLang('field_color'),
126322228b0eSAtari911            'recurrence_pattern' => $this->getLang('recurrence_pattern'),
126422228b0eSAtari911            'every' => $this->getLang('every'),
126522228b0eSAtari911            'on_days' => $this->getLang('on_days'),
126622228b0eSAtari911            'monthly_options' => $this->getLang('monthly_options'),
126722228b0eSAtari911            'day_of_month' => $this->getLang('day_of_month'),
126822228b0eSAtari911            'ordinal_weekday' => $this->getLang('ordinal_weekday'),
126922228b0eSAtari911            'btn_save_changes' => $this->getLang('btn_save_changes'),
127022228b0eSAtari911            'btn_cancel' => $this->getLang('btn_cancel'),
127122228b0eSAtari911            // Day names
127222228b0eSAtari911            'day_names' => [$this->getLang('day_sun'), $this->getLang('day_mon'), $this->getLang('day_tue'), $this->getLang('day_wed'), $this->getLang('day_thu'), $this->getLang('day_fri'), $this->getLang('day_sat')],
127322228b0eSAtari911            'day_names_full' => [$this->getLang('day_sunday'), $this->getLang('day_monday'), $this->getLang('day_tuesday'), $this->getLang('day_wednesday'), $this->getLang('day_thursday'), $this->getLang('day_friday'), $this->getLang('day_saturday')],
127422228b0eSAtari911            // Ordinal labels
127522228b0eSAtari911            'ordinal_first' => $this->getLang('ordinal_first'),
127622228b0eSAtari911            'ordinal_second' => $this->getLang('ordinal_second'),
127722228b0eSAtari911            'ordinal_third' => $this->getLang('ordinal_third'),
127822228b0eSAtari911            'ordinal_fourth' => $this->getLang('ordinal_fourth'),
127922228b0eSAtari911            'ordinal_fifth' => $this->getLang('ordinal_fifth'),
128022228b0eSAtari911            'ordinal_last' => $this->getLang('ordinal_last'),
128122228b0eSAtari911            // Recurrence types
128222228b0eSAtari911            'rec_days' => $this->getLang('rec_days'),
128322228b0eSAtari911            'rec_weeks' => $this->getLang('rec_weeks'),
128422228b0eSAtari911            'rec_months' => $this->getLang('rec_months'),
128522228b0eSAtari911            'rec_years' => $this->getLang('rec_years'),
128622228b0eSAtari911        ];
128722228b0eSAtari911
12881d05cddcSAtari911        // JavaScript
12891d05cddcSAtari911        echo '<script>
12907e8ea635SAtari911        var adminColors = {
12917e8ea635SAtari911            text: "' . $colors['text'] . '",
12927e8ea635SAtari911            bg: "' . $colors['bg'] . '",
12937e8ea635SAtari911            border: "' . $colors['border'] . '"
12947e8ea635SAtari911        };
129522228b0eSAtari911        var adminLang = ' . json_encode($jsAdminLang) . ';
12961d05cddcSAtari911        // Table sorting functionality - defined early so onclick handlers work
12971d05cddcSAtari911        let sortDirection = {}; // Track sort direction for each column
12981d05cddcSAtari911
12997e8ea635SAtari911        function cleanupEmptyNamespaces() {
13007e8ea635SAtari911            var btn = document.getElementById("cleanup-ns-btn");
13017e8ea635SAtari911            var status = document.getElementById("cleanup-ns-status");
130222228b0eSAtari911            if (btn) { btn.textContent = "⏳ " + adminLang.scanning; btn.disabled = true; }
13037e8ea635SAtari911            if (status) { status.innerHTML = ""; }
13047e8ea635SAtari911
13057e8ea635SAtari911            // Dry run first
13067e8ea635SAtari911            fetch(DOKU_BASE + "lib/exe/ajax.php", {
13077e8ea635SAtari911                method: "POST",
13087e8ea635SAtari911                headers: {"Content-Type": "application/x-www-form-urlencoded"},
13097e8ea635SAtari911                body: "call=plugin_calendar&action=cleanup_empty_namespaces&dry_run=1&sectok=" + JSINFO.sectok
13107e8ea635SAtari911            })
13117e8ea635SAtari911            .then(function(r) { return r.json(); })
13127e8ea635SAtari911            .then(function(data) {
131322228b0eSAtari911                if (btn) { btn.textContent = "�� " + adminLang.cleanup_empty; btn.disabled = false; }
13147e8ea635SAtari911                if (!data.success) {
1315da206178SAtari911                    if (status) { status.innerHTML = "<span style=\"color:#e74c3c;\">❌ " + (data.error || "Failed") + "</span>"; }
13167e8ea635SAtari911                    return;
13177e8ea635SAtari911                }
13187e8ea635SAtari911
13197e8ea635SAtari911                var details = data.details || [];
13207e8ea635SAtari911                var totalActions = details.length;
13217e8ea635SAtari911
13227e8ea635SAtari911                if (totalActions === 0) {
132322228b0eSAtari911                    if (status) { status.innerHTML = "<span style=\"color:#00cc07;\">✅ " + adminLang.no_empty_ns + "</span>"; }
13247e8ea635SAtari911                    return;
13257e8ea635SAtari911                }
13267e8ea635SAtari911
13277e8ea635SAtari911                // Build detail list for confirm
132822228b0eSAtari911                var msg = adminLang.found_items.replace(/%d/, totalActions) + ":\\n\\n";
13297e8ea635SAtari911                for (var i = 0; i < details.length; i++) {
13307e8ea635SAtari911                    msg += "• " + details[i] + "\\n";
13317e8ea635SAtari911                }
133222228b0eSAtari911                msg += "\\n" + adminLang.proceed_cleanup;
13337e8ea635SAtari911
13347e8ea635SAtari911                if (!confirm(msg)) return;
13357e8ea635SAtari911
13367e8ea635SAtari911                // Execute
133722228b0eSAtari911                if (btn) { btn.textContent = "⏳ " + adminLang.cleaning; btn.disabled = true; }
13387e8ea635SAtari911                fetch(DOKU_BASE + "lib/exe/ajax.php", {
13397e8ea635SAtari911                    method: "POST",
13407e8ea635SAtari911                    headers: {"Content-Type": "application/x-www-form-urlencoded"},
13417e8ea635SAtari911                    body: "call=plugin_calendar&action=cleanup_empty_namespaces&sectok=" + JSINFO.sectok
13427e8ea635SAtari911                })
13437e8ea635SAtari911                .then(function(r) { return r.json(); })
13447e8ea635SAtari911                .then(function(data2) {
1345da206178SAtari911                    var msgText = data2.message || "Cleanup complete";
13467e8ea635SAtari911                    if (data2.details && data2.details.length > 0) {
13477e8ea635SAtari911                        msgText += " (" + data2.details.join(", ") + ")";
13487e8ea635SAtari911                    }
13497e8ea635SAtari911                    window.location.href = "?do=admin&page=calendar&tab=manage&msg=" + encodeURIComponent(msgText) + "&msgtype=success";
13507e8ea635SAtari911                });
13517e8ea635SAtari911            })
13527e8ea635SAtari911            .catch(function(err) {
135322228b0eSAtari911                if (btn) { btn.textContent = "�� " + adminLang.cleanup_empty; btn.disabled = false; }
135422228b0eSAtari911                if (status) { status.innerHTML = "<span style=\"color:#e74c3c;\">❌ Error: " + err + "</span>"; }
13557e8ea635SAtari911            });
13567e8ea635SAtari911        }
13577e8ea635SAtari911        function trimAllPastRecurring() {
13587e8ea635SAtari911            var btn = document.getElementById("trim-all-past-btn");
135922228b0eSAtari911            if (btn) { btn.textContent = "⏳ " + adminLang.counting; btn.disabled = true; }
13607e8ea635SAtari911
13617e8ea635SAtari911            // Step 1: dry run to get count
13627e8ea635SAtari911            fetch(DOKU_BASE + "lib/exe/ajax.php", {
13637e8ea635SAtari911                method: "POST",
13647e8ea635SAtari911                headers: {"Content-Type": "application/x-www-form-urlencoded"},
13657e8ea635SAtari911                body: "call=plugin_calendar&action=trim_all_past_recurring&dry_run=1&sectok=" + JSINFO.sectok
13667e8ea635SAtari911            })
13677e8ea635SAtari911            .then(function(r) { return r.json(); })
13687e8ea635SAtari911            .then(function(data) {
136922228b0eSAtari911                if (btn) { btn.textContent = "✂️ " + adminLang.trim_all_past; btn.disabled = false; }
13707e8ea635SAtari911                var count = data.count || 0;
13717e8ea635SAtari911                if (count === 0) {
137222228b0eSAtari911                    alert(adminLang.no_past_recurring);
13737e8ea635SAtari911                    return;
13747e8ea635SAtari911                }
137522228b0eSAtari911                if (!confirm(adminLang.found_past_recurring.replace(/%d/, count))) return;
13767e8ea635SAtari911
13777e8ea635SAtari911                // Step 2: actually delete
137822228b0eSAtari911                if (btn) { btn.textContent = "⏳ " + adminLang.trimming; btn.disabled = true; }
13797e8ea635SAtari911                fetch(DOKU_BASE + "lib/exe/ajax.php", {
13807e8ea635SAtari911                    method: "POST",
13817e8ea635SAtari911                    headers: {"Content-Type": "application/x-www-form-urlencoded"},
13827e8ea635SAtari911                    body: "call=plugin_calendar&action=trim_all_past_recurring&sectok=" + JSINFO.sectok
13837e8ea635SAtari911                })
13847e8ea635SAtari911                .then(function(r) { return r.json(); })
13857e8ea635SAtari911                .then(function(data2) {
13867e8ea635SAtari911                    if (btn) {
138722228b0eSAtari911                        btn.textContent = data2.success ? ("✅ " + (data2.count || 0)) : "❌";
13887e8ea635SAtari911                        btn.disabled = false;
13897e8ea635SAtari911                    }
139022228b0eSAtari911                    setTimeout(function() { if (btn) btn.textContent = "✂️ " + adminLang.trim_all_past; }, 3000);
13917e8ea635SAtari911                    rescanRecurringEvents();
13927e8ea635SAtari911                });
13937e8ea635SAtari911            })
13947e8ea635SAtari911            .catch(function(err) {
139522228b0eSAtari911                if (btn) { btn.textContent = "✂️ " + adminLang.trim_all_past; btn.disabled = false; }
13967e8ea635SAtari911            });
13977e8ea635SAtari911        }
13987e8ea635SAtari911
13997e8ea635SAtari911        function rescanRecurringEvents() {
14007e8ea635SAtari911            var btn = document.getElementById("rescan-recurring-btn");
14017e8ea635SAtari911            var content = document.getElementById("recurring-content");
14027e8ea635SAtari911            if (btn) { btn.textContent = "⏳ Scanning..."; btn.disabled = true; }
14037e8ea635SAtari911
14047e8ea635SAtari911            fetch(DOKU_BASE + "lib/exe/ajax.php", {
14057e8ea635SAtari911                method: "POST",
14067e8ea635SAtari911                headers: {"Content-Type": "application/x-www-form-urlencoded"},
14077e8ea635SAtari911                body: "call=plugin_calendar&action=rescan_recurring&sectok=" + JSINFO.sectok
14087e8ea635SAtari911            })
14097e8ea635SAtari911            .then(function(r) { return r.json(); })
14107e8ea635SAtari911            .then(function(data) {
14117e8ea635SAtari911                if (data.success && content) {
14127e8ea635SAtari911                    content.innerHTML = data.html;
14137e8ea635SAtari911                }
14147e8ea635SAtari911                if (btn) { btn.textContent = "�� Rescan (" + (data.count || 0) + " found)"; btn.disabled = false; }
14157e8ea635SAtari911                setTimeout(function() { if (btn) btn.textContent = "�� Rescan"; }, 3000);
14167e8ea635SAtari911            })
14177e8ea635SAtari911            .catch(function(err) {
14187e8ea635SAtari911                if (btn) { btn.textContent = "�� Rescan"; btn.disabled = false; }
14197e8ea635SAtari911                console.error("Rescan failed:", err);
14207e8ea635SAtari911            });
14217e8ea635SAtari911        }
14227e8ea635SAtari911
14237e8ea635SAtari911        function recurringAction(action, params, statusEl) {
14247e8ea635SAtari911            if (statusEl) statusEl.textContent = "⏳ Working...";
14257e8ea635SAtari911            var body = "call=plugin_calendar&action=" + action + "&sectok=" + JSINFO.sectok;
14267e8ea635SAtari911            for (var key in params) {
14277e8ea635SAtari911                body += "&" + encodeURIComponent(key) + "=" + encodeURIComponent(params[key]);
14287e8ea635SAtari911            }
14297e8ea635SAtari911            return fetch(DOKU_BASE + "lib/exe/ajax.php", {
14307e8ea635SAtari911                method: "POST",
14317e8ea635SAtari911                headers: {"Content-Type": "application/x-www-form-urlencoded"},
14327e8ea635SAtari911                body: body
14337e8ea635SAtari911            })
14347e8ea635SAtari911            .then(function(r) { return r.json(); })
14357e8ea635SAtari911            .then(function(data) {
14367e8ea635SAtari911                if (statusEl) {
14377e8ea635SAtari911                    statusEl.textContent = data.success ? ("✅ " + data.message) : ("❌ " + (data.error || "Failed"));
14387e8ea635SAtari911                    statusEl.style.color = data.success ? "#00cc07" : "#e74c3c";
14397e8ea635SAtari911                }
14407e8ea635SAtari911                return data;
14417e8ea635SAtari911            })
14427e8ea635SAtari911            .catch(function(err) {
14437e8ea635SAtari911                if (statusEl) { statusEl.textContent = "❌ Error: " + err; statusEl.style.color = "#e74c3c"; }
14447e8ea635SAtari911            });
14457e8ea635SAtari911        }
14467e8ea635SAtari911
144796df7d3eSAtari911        function manageRecurringSeries(title, namespace, count, firstDate, lastDate, pattern, hasFlag) {
14487e8ea635SAtari911            var isPaused = title.indexOf("⏸") === 0;
14497e8ea635SAtari911            var cleanTitle = title.replace(/^⏸\s*/, "");
14507e8ea635SAtari911            var safeTitle = title.replace(/\x27/g, "\\\x27");
14517e8ea635SAtari911            var todayStr = new Date().toISOString().split("T")[0];
14527e8ea635SAtari911
14537e8ea635SAtari911            var dialog = document.createElement("div");
14547e8ea635SAtari911            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;";
14557e8ea635SAtari911            dialog.addEventListener("click", function(e) { if (e.target === dialog) dialog.remove(); });
14567e8ea635SAtari911
14577e8ea635SAtari911            var h = "<div style=\"background:' . $colors['bg'] . '; padding:20px; border-radius:8px; min-width:520px; max-width:700px; max-height:90vh; overflow-y:auto; font-family:system-ui,sans-serif;\">";
145822228b0eSAtari911            h += "<h3 style=\"margin:0 0 5px; color:#00cc07;\">⚙️ " + adminLang.manage_recurring_title + "</h3>";
145922228b0eSAtari911            h += "<p style=\"margin:0 0 15px; color:' . $colors['text'] . '; font-size:13px;\"><strong>" + cleanTitle + "</strong><br>" + count + " " + adminLang.occurrences + " · " + pattern + "<br>" + firstDate + " → " + lastDate + "</p>";
14607e8ea635SAtari911            h += "<div id=\"manage-status\" style=\"font-size:12px; min-height:18px; margin-bottom:10px;\"></div>";
14617e8ea635SAtari911
14627e8ea635SAtari911            // Extend
14637e8ea635SAtari911            h += "<div style=\"border:1px solid ' . $colors['border'] . '; border-radius:4px; padding:10px; margin-bottom:10px;\">";
146422228b0eSAtari911            h += "<div style=\"font-weight:700; color:#00cc07; font-size:12px; margin-bottom:6px;\">�� " + adminLang.extend_series + "</div>";
14657e8ea635SAtari911            h += "<div style=\"display:flex; gap:8px; align-items:end;\">";
146622228b0eSAtari911            h += "<div><label style=\"font-size:11px; display:block; margin-bottom:2px;\">" + adminLang.add_occurrences + "</label>";
14677e8ea635SAtari911            h += "<input type=\"number\" id=\"manage-extend-count\" value=\"4\" min=\"1\" max=\"52\" style=\"width:60px; padding:4px; border:1px solid ' . $colors['border'] . '; border-radius:3px; font-size:12px;\"></div>";
146822228b0eSAtari911            h += "<div><label style=\"font-size:11px; display:block; margin-bottom:2px;\">" + adminLang.days_apart + "</label>";
14697e8ea635SAtari911            h += "<select id=\"manage-extend-interval\" style=\"padding:4px; border:1px solid ' . $colors['border'] . '; border-radius:3px; font-size:12px;\">";
147022228b0eSAtari911            h += "<option value=\"1\">" + adminLang.interval_daily + "</option><option value=\"7\" selected>" + adminLang.interval_weekly + "</option><option value=\"14\">" + adminLang.interval_biweekly + "</option><option value=\"30\">" + adminLang.interval_monthly + "</option><option value=\"90\">" + adminLang.interval_quarterly + "</option><option value=\"365\">" + adminLang.interval_yearly + "</option></select></div>";
147122228b0eSAtari911            h += "<button onclick=\"recurringAction(\x27extend_recurring\x27, {title:\x27" + safeTitle + "\x27, namespace:\x27" + namespace + "\x27, count:document.getElementById(\x27manage-extend-count\x27).value, interval_days:document.getElementById(\x27manage-extend-interval\x27).value}, document.getElementById(\x27manage-status\x27))\" style=\"background:#00cc07; color:#fff; border:none; padding:5px 12px; border-radius:3px; cursor:pointer; font-size:11px; font-weight:600;\">" + adminLang.btn_extend + "</button>";
14727e8ea635SAtari911            h += "</div></div>";
14737e8ea635SAtari911
14747e8ea635SAtari911            // Trim
14757e8ea635SAtari911            h += "<div style=\"border:1px solid ' . $colors['border'] . '; border-radius:4px; padding:10px; margin-bottom:10px;\">";
147622228b0eSAtari911            h += "<div style=\"font-weight:700; color:#e74c3c; font-size:12px; margin-bottom:6px;\">✂️ " + adminLang.trim_past_events + "</div>";
14777e8ea635SAtari911            h += "<div style=\"display:flex; gap:8px; align-items:end;\">";
147822228b0eSAtari911            h += "<div><label style=\"font-size:11px; display:block; margin-bottom:2px;\">" + adminLang.remove_before + "</label>";
14797e8ea635SAtari911            h += "<input type=\"date\" id=\"manage-trim-date\" value=\"" + todayStr + "\" style=\"padding:4px; border:1px solid ' . $colors['border'] . '; border-radius:3px; font-size:12px;\"></div>";
148022228b0eSAtari911            h += "<button onclick=\"if(confirm(adminLang.trim_confirm.replace(/%s/, document.getElementById(\x27manage-trim-date\x27).value))) recurringAction(\x27trim_recurring\x27, {title:\x27" + safeTitle + "\x27, namespace:\x27" + namespace + "\x27, cutoff_date:document.getElementById(\x27manage-trim-date\x27).value}, document.getElementById(\x27manage-status\x27))\" style=\"background:#e74c3c; color:#fff; border:none; padding:5px 12px; border-radius:3px; cursor:pointer; font-size:11px; font-weight:600;\">" + adminLang.btn_trim + "</button>";
14817e8ea635SAtari911            h += "</div></div>";
14827e8ea635SAtari911
14837e8ea635SAtari911            // Change Pattern
14847e8ea635SAtari911            h += "<div style=\"border:1px solid ' . $colors['border'] . '; border-radius:4px; padding:10px; margin-bottom:10px;\">";
148522228b0eSAtari911            h += "<div style=\"font-weight:700; color:#ff9800; font-size:12px; margin-bottom:6px;\">�� " + adminLang.change_pattern + "</div>";
148622228b0eSAtari911            h += "<p style=\"font-size:11px; color:' . $colors['text'] . '; margin:0 0 6px; opacity:0.7;\">" + adminLang.respace_note + "</p>";
14877e8ea635SAtari911            h += "<div style=\"display:flex; gap:8px; align-items:end;\">";
148822228b0eSAtari911            h += "<div><label style=\"font-size:11px; display:block; margin-bottom:2px;\">" + adminLang.new_interval + "</label>";
14897e8ea635SAtari911            h += "<select id=\"manage-pattern-interval\" style=\"padding:4px; border:1px solid ' . $colors['border'] . '; border-radius:3px; font-size:12px;\">";
149022228b0eSAtari911            h += "<option value=\"1\">" + adminLang.interval_daily + "</option><option value=\"7\">" + adminLang.interval_weekly + "</option><option value=\"14\">" + adminLang.interval_biweekly + "</option><option value=\"30\">" + adminLang.interval_monthly + "</option><option value=\"90\">" + adminLang.interval_quarterly + "</option><option value=\"365\">" + adminLang.interval_yearly + "</option></select></div>";
149122228b0eSAtari911            h += "<button onclick=\"if(confirm(adminLang.respace_confirm)) recurringAction(\x27change_pattern_recurring\x27, {title:\x27" + safeTitle + "\x27, namespace:\x27" + namespace + "\x27, interval_days:document.getElementById(\x27manage-pattern-interval\x27).value}, document.getElementById(\x27manage-status\x27))\" style=\"background:#ff9800; color:#fff; border:none; padding:5px 12px; border-radius:3px; cursor:pointer; font-size:11px; font-weight:600;\">" + adminLang.btn_change + "</button>";
14927e8ea635SAtari911            h += "</div></div>";
14937e8ea635SAtari911
14947e8ea635SAtari911            // Change Start Date
14957e8ea635SAtari911            h += "<div style=\"border:1px solid ' . $colors['border'] . '; border-radius:4px; padding:10px; margin-bottom:10px;\">";
149622228b0eSAtari911            h += "<div style=\"font-weight:700; color:#2196f3; font-size:12px; margin-bottom:6px;\">�� " + adminLang.change_start_date + "</div>";
149722228b0eSAtari911            h += "<p style=\"font-size:11px; color:' . $colors['text'] . '; margin:0 0 6px; opacity:0.7;\">" + adminLang.shift_note + "</p>";
14987e8ea635SAtari911            h += "<div style=\"display:flex; gap:8px; align-items:end;\">";
149922228b0eSAtari911            h += "<div><label style=\"font-size:11px; display:block; margin-bottom:2px;\">" + adminLang.current_label + " " + firstDate + "</label>";
15007e8ea635SAtari911            h += "<input type=\"date\" id=\"manage-start-date\" value=\"" + firstDate + "\" style=\"padding:4px; border:1px solid ' . $colors['border'] . '; border-radius:3px; font-size:12px;\"></div>";
150122228b0eSAtari911            h += "<button onclick=\"if(confirm(adminLang.shift_confirm)) recurringAction(\x27change_start_recurring\x27, {title:\x27" + safeTitle + "\x27, namespace:\x27" + namespace + "\x27, new_start_date:document.getElementById(\x27manage-start-date\x27).value}, document.getElementById(\x27manage-status\x27))\" style=\"background:#2196f3; color:#fff; border:none; padding:5px 12px; border-radius:3px; cursor:pointer; font-size:11px; font-weight:600;\">" + adminLang.btn_shift + "</button>";
15027e8ea635SAtari911            h += "</div></div>";
15037e8ea635SAtari911
15047e8ea635SAtari911            // Pause/Resume
15057e8ea635SAtari911            h += "<div style=\"border:1px solid ' . $colors['border'] . '; border-radius:4px; padding:10px; margin-bottom:10px;\">";
150622228b0eSAtari911            h += "<div style=\"font-weight:700; color:#9c27b0; font-size:12px; margin-bottom:6px;\">" + (isPaused ? "▶️ " + adminLang.resume_series : "⏸ " + adminLang.pause_series) + "</div>";
150722228b0eSAtari911            h += "<p style=\"font-size:11px; color:' . $colors['text'] . '; margin:0 0 6px; opacity:0.7;\">" + (isPaused ? adminLang.resume_note : adminLang.pause_note) + "</p>";
150822228b0eSAtari911            h += "<button onclick=\"recurringAction(\x27" + (isPaused ? "resume_recurring" : "pause_recurring") + "\x27, {title:\x27" + safeTitle + "\x27, namespace:\x27" + namespace + "\x27}, document.getElementById(\x27manage-status\x27))\" style=\"background:#9c27b0; color:#fff; border:none; padding:5px 12px; border-radius:3px; cursor:pointer; font-size:11px; font-weight:600;\">" + (isPaused ? "▶️ " + adminLang.btn_resume : "⏸ " + adminLang.btn_pause) + "</button>";
15097e8ea635SAtari911            h += "</div>";
15107e8ea635SAtari911
15117e8ea635SAtari911            // Close
15127e8ea635SAtari911            h += "<div style=\"text-align:right; margin-top:10px;\">";
151322228b0eSAtari911            h += "<button onclick=\"this.closest(\x27[style*=fixed]\x27).remove(); rescanRecurringEvents();\" style=\"background:#666; color:#fff; border:none; padding:8px 20px; border-radius:3px; cursor:pointer; font-weight:600;\">" + adminLang.btn_close + "</button>";
15147e8ea635SAtari911            h += "</div></div>";
15157e8ea635SAtari911
15167e8ea635SAtari911            dialog.innerHTML = h;
15177e8ea635SAtari911            document.body.appendChild(dialog);
15187e8ea635SAtari911        }
15197e8ea635SAtari911
15201d05cddcSAtari911        function sortRecurringTable(columnIndex) {
15211d05cddcSAtari911            const table = document.getElementById("recurringTable");
15221d05cddcSAtari911            const tbody = document.getElementById("recurringTableBody");
15231d05cddcSAtari911
15249ccd446eSAtari911            if (!table || !tbody) return;
15251d05cddcSAtari911
15261d05cddcSAtari911            const rows = Array.from(tbody.querySelectorAll("tr"));
15279ccd446eSAtari911            if (rows.length === 0) return;
15281d05cddcSAtari911
15291d05cddcSAtari911            // Toggle sort direction for this column
15301d05cddcSAtari911            if (!sortDirection[columnIndex]) {
15311d05cddcSAtari911                sortDirection[columnIndex] = "asc";
15321d05cddcSAtari911            } else {
15331d05cddcSAtari911                sortDirection[columnIndex] = sortDirection[columnIndex] === "asc" ? "desc" : "asc";
15341d05cddcSAtari911            }
15351d05cddcSAtari911
15361d05cddcSAtari911            const direction = sortDirection[columnIndex];
15371d05cddcSAtari911            const isNumeric = columnIndex === 4; // Count column
15381d05cddcSAtari911
15391d05cddcSAtari911            // Sort rows
15401d05cddcSAtari911            rows.sort((a, b) => {
15411d05cddcSAtari911                let aValue = a.cells[columnIndex].textContent.trim();
15421d05cddcSAtari911                let bValue = b.cells[columnIndex].textContent.trim();
15431d05cddcSAtari911
15441d05cddcSAtari911                // Extract text from code elements for namespace column
15451d05cddcSAtari911                if (columnIndex === 1) {
15461d05cddcSAtari911                    const aCode = a.cells[columnIndex].querySelector("code");
15471d05cddcSAtari911                    const bCode = b.cells[columnIndex].querySelector("code");
15481d05cddcSAtari911                    aValue = aCode ? aCode.textContent.trim() : aValue;
15491d05cddcSAtari911                    bValue = bCode ? bCode.textContent.trim() : bValue;
15501d05cddcSAtari911                }
15511d05cddcSAtari911
15521d05cddcSAtari911                // Extract number from strong elements for count column
15531d05cddcSAtari911                if (isNumeric) {
15541d05cddcSAtari911                    const aStrong = a.cells[columnIndex].querySelector("strong");
15551d05cddcSAtari911                    const bStrong = b.cells[columnIndex].querySelector("strong");
15561d05cddcSAtari911                    aValue = aStrong ? parseInt(aStrong.textContent.trim()) : 0;
15571d05cddcSAtari911                    bValue = bStrong ? parseInt(bStrong.textContent.trim()) : 0;
15581d05cddcSAtari911
15591d05cddcSAtari911                    return direction === "asc" ? aValue - bValue : bValue - aValue;
15601d05cddcSAtari911                }
15611d05cddcSAtari911
15621d05cddcSAtari911                // String comparison
15631d05cddcSAtari911                if (direction === "asc") {
15641d05cddcSAtari911                    return aValue.localeCompare(bValue);
15651d05cddcSAtari911                } else {
15661d05cddcSAtari911                    return bValue.localeCompare(aValue);
15671d05cddcSAtari911                }
15681d05cddcSAtari911            });
15691d05cddcSAtari911
15701d05cddcSAtari911            // Update arrows
15711d05cddcSAtari911            const headers = table.querySelectorAll("th");
15721d05cddcSAtari911            headers.forEach((header, index) => {
15731d05cddcSAtari911                const arrow = header.querySelector(".sort-arrow");
15741d05cddcSAtari911                if (arrow) {
15751d05cddcSAtari911                    if (index === columnIndex) {
15761d05cddcSAtari911                        arrow.textContent = direction === "asc" ? "↑" : "↓";
15771d05cddcSAtari911                        arrow.style.color = "#00cc07";
15781d05cddcSAtari911                    } else {
15791d05cddcSAtari911                        arrow.textContent = "⇅";
15801d05cddcSAtari911                        arrow.style.color = "#999";
15811d05cddcSAtari911                    }
15821d05cddcSAtari911                }
15831d05cddcSAtari911            });
15841d05cddcSAtari911
15851d05cddcSAtari911            // Rebuild tbody
15861d05cddcSAtari911            rows.forEach(row => tbody.appendChild(row));
15871d05cddcSAtari911        }
15881d05cddcSAtari911
15891d05cddcSAtari911        function filterRecurringEvents() {
15901d05cddcSAtari911            const searchInput = document.getElementById("searchRecurring");
15911d05cddcSAtari911            const filter = normalizeText(searchInput.value);
15921d05cddcSAtari911            const tbody = document.getElementById("recurringTableBody");
15931d05cddcSAtari911            const rows = tbody.getElementsByTagName("tr");
15941d05cddcSAtari911
15951d05cddcSAtari911            for (let i = 0; i < rows.length; i++) {
15961d05cddcSAtari911                const row = rows[i];
15971d05cddcSAtari911                const titleCell = row.getElementsByTagName("td")[0];
15981d05cddcSAtari911
15991d05cddcSAtari911                if (titleCell) {
16001d05cddcSAtari911                    const titleText = normalizeText(titleCell.textContent || titleCell.innerText);
16011d05cddcSAtari911
16021d05cddcSAtari911                    if (titleText.indexOf(filter) > -1) {
16031d05cddcSAtari911                        row.classList.remove("recurring-row-hidden");
16041d05cddcSAtari911                    } else {
16051d05cddcSAtari911                        row.classList.add("recurring-row-hidden");
16061d05cddcSAtari911                    }
16071d05cddcSAtari911                }
16081d05cddcSAtari911            }
16091d05cddcSAtari911        }
16101d05cddcSAtari911
16111d05cddcSAtari911        function normalizeText(text) {
16121d05cddcSAtari911            // Convert to lowercase
16131d05cddcSAtari911            text = text.toLowerCase();
16141d05cddcSAtari911
16151d05cddcSAtari911            // Remove apostrophes and quotes
16161d05cddcSAtari911            text = text.replace(/[\'\"]/g, "");
16171d05cddcSAtari911
16181d05cddcSAtari911            // Replace accented characters with regular ones
16191d05cddcSAtari911            text = text.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
16201d05cddcSAtari911
16211d05cddcSAtari911            // Remove special characters except spaces and alphanumeric
16221d05cddcSAtari911            text = text.replace(/[^a-z0-9\s]/g, "");
16231d05cddcSAtari911
16241d05cddcSAtari911            // Collapse multiple spaces
16251d05cddcSAtari911            text = text.replace(/\s+/g, " ");
16261d05cddcSAtari911
16271d05cddcSAtari911            return text.trim();
16281d05cddcSAtari911        }
16291d05cddcSAtari911
16301d05cddcSAtari911        function filterEvents() {
16311d05cddcSAtari911            const searchText = normalizeText(document.getElementById("searchEvents").value);
16321d05cddcSAtari911            const eventRows = document.querySelectorAll(".event-row");
16331d05cddcSAtari911            let visibleCount = 0;
16341d05cddcSAtari911
16351d05cddcSAtari911            eventRows.forEach(row => {
16361d05cddcSAtari911                const titleElement = row.querySelector("div div");
16371d05cddcSAtari911                const originalTitle = titleElement.getAttribute("data-original-title") || titleElement.textContent;
16381d05cddcSAtari911
16391d05cddcSAtari911                // Store original title if not already stored
16401d05cddcSAtari911                if (!titleElement.getAttribute("data-original-title")) {
16411d05cddcSAtari911                    titleElement.setAttribute("data-original-title", originalTitle);
16421d05cddcSAtari911                }
16431d05cddcSAtari911
16441d05cddcSAtari911                const normalizedTitle = normalizeText(originalTitle);
16451d05cddcSAtari911
16461d05cddcSAtari911                if (normalizedTitle.includes(searchText) || searchText === "") {
16471d05cddcSAtari911                    row.style.display = "flex";
16481d05cddcSAtari911                    visibleCount++;
16491d05cddcSAtari911                } else {
16501d05cddcSAtari911                    row.style.display = "none";
16511d05cddcSAtari911                }
16521d05cddcSAtari911            });
16531d05cddcSAtari911
16541d05cddcSAtari911            // Update namespace visibility and counts
16551d05cddcSAtari911            document.querySelectorAll("[id^=ns_]").forEach(nsDiv => {
16561d05cddcSAtari911                if (nsDiv.id.endsWith("_arrow") || nsDiv.id.endsWith("_check")) return;
16571d05cddcSAtari911
16581d05cddcSAtari911                const visibleEvents = nsDiv.querySelectorAll(".event-row[style*=\\"display: flex\\"], .event-row:not([style*=\\"display: none\\"])").length;
16591d05cddcSAtari911                const nsId = nsDiv.id;
16601d05cddcSAtari911                const arrow = document.getElementById(nsId + "_arrow");
16611d05cddcSAtari911
16621d05cddcSAtari911                // Auto-expand namespaces with matches when searching
16631d05cddcSAtari911                if (searchText && visibleEvents > 0) {
16641d05cddcSAtari911                    nsDiv.style.display = "block";
16651d05cddcSAtari911                    if (arrow) arrow.textContent = "▼";
16661d05cddcSAtari911                }
16671d05cddcSAtari911            });
16681d05cddcSAtari911        }
16691d05cddcSAtari911
16701d05cddcSAtari911        function toggleNamespace(id) {
16711d05cddcSAtari911            const elem = document.getElementById(id);
16721d05cddcSAtari911            const arrow = document.getElementById(id + "_arrow");
16731d05cddcSAtari911            if (elem.style.display === "none") {
16741d05cddcSAtari911                elem.style.display = "block";
16751d05cddcSAtari911                arrow.textContent = "▼";
16761d05cddcSAtari911            } else {
16771d05cddcSAtari911                elem.style.display = "none";
16781d05cddcSAtari911                arrow.textContent = "▶";
16791d05cddcSAtari911            }
16801d05cddcSAtari911        }
16811d05cddcSAtari911
16821d05cddcSAtari911        function toggleNamespaceSelect(nsId) {
16831d05cddcSAtari911            const checkbox = document.getElementById(nsId + "_check");
16841d05cddcSAtari911            const events = document.querySelectorAll("." + nsId + "_events");
16851d05cddcSAtari911
16861d05cddcSAtari911            // Only select visible events (not hidden by search)
16871d05cddcSAtari911            events.forEach(cb => {
16881d05cddcSAtari911                const eventRow = cb.closest(".event-row");
16891d05cddcSAtari911                if (eventRow && eventRow.style.display !== "none") {
16901d05cddcSAtari911                    cb.checked = checkbox.checked;
16911d05cddcSAtari911                }
16921d05cddcSAtari911            });
16931d05cddcSAtari911            updateCount();
16941d05cddcSAtari911        }
16951d05cddcSAtari911
16961d05cddcSAtari911        function selectAll() {
16971d05cddcSAtari911            // Only select visible events
16981d05cddcSAtari911            document.querySelectorAll(".event-checkbox").forEach(cb => {
16991d05cddcSAtari911                const eventRow = cb.closest(".event-row");
17001d05cddcSAtari911                if (eventRow && eventRow.style.display !== "none") {
17011d05cddcSAtari911                    cb.checked = true;
17021d05cddcSAtari911                }
17031d05cddcSAtari911            });
17041d05cddcSAtari911            // Update namespace checkboxes to indeterminate if partially selected
17051d05cddcSAtari911            document.querySelectorAll("input[id$=_check]").forEach(nsCheckbox => {
17061d05cddcSAtari911                const nsId = nsCheckbox.id.replace("_check", "");
17071d05cddcSAtari911                const events = document.querySelectorAll("." + nsId + "_events");
17081d05cddcSAtari911                const visibleEvents = Array.from(events).filter(cb => {
17091d05cddcSAtari911                    const row = cb.closest(".event-row");
17101d05cddcSAtari911                    return row && row.style.display !== "none";
17111d05cddcSAtari911                });
17121d05cddcSAtari911                const checkedVisible = visibleEvents.filter(cb => cb.checked);
17131d05cddcSAtari911
17141d05cddcSAtari911                if (checkedVisible.length === visibleEvents.length && visibleEvents.length > 0) {
17151d05cddcSAtari911                    nsCheckbox.checked = true;
17161d05cddcSAtari911                } else if (checkedVisible.length > 0) {
17171d05cddcSAtari911                    nsCheckbox.indeterminate = true;
17181d05cddcSAtari911                } else {
17191d05cddcSAtari911                    nsCheckbox.checked = false;
17201d05cddcSAtari911                }
17211d05cddcSAtari911            });
17221d05cddcSAtari911            updateCount();
17231d05cddcSAtari911        }
17241d05cddcSAtari911
17251d05cddcSAtari911        function deselectAll() {
17261d05cddcSAtari911            document.querySelectorAll(".event-checkbox").forEach(cb => cb.checked = false);
17271d05cddcSAtari911            document.querySelectorAll("input[id$=_check]").forEach(cb => {
17281d05cddcSAtari911                cb.checked = false;
17291d05cddcSAtari911                cb.indeterminate = false;
17301d05cddcSAtari911            });
17311d05cddcSAtari911            updateCount();
17321d05cddcSAtari911        }
17331d05cddcSAtari911
17341d05cddcSAtari911        function deleteSelected() {
17351d05cddcSAtari911            const checkedBoxes = document.querySelectorAll(".event-checkbox:checked");
17361d05cddcSAtari911            if (checkedBoxes.length === 0) {
173722228b0eSAtari911                alert(adminLang.no_events_selected);
17381d05cddcSAtari911                return;
17391d05cddcSAtari911            }
17401d05cddcSAtari911
17411d05cddcSAtari911            const count = checkedBoxes.length;
174222228b0eSAtari911            if (!confirm(adminLang.delete_confirm.replace(/%d/, count))) {
17431d05cddcSAtari911                return;
17441d05cddcSAtari911            }
17451d05cddcSAtari911
17461d05cddcSAtari911            const form = document.createElement("form");
17471d05cddcSAtari911            form.method = "POST";
17481d05cddcSAtari911            form.action = "?do=admin&page=calendar&tab=manage";
17491d05cddcSAtari911
1750*2866e827SAtari911            var sectokInput = document.createElement("input");
1751*2866e827SAtari911            sectokInput.type = "hidden";
1752*2866e827SAtari911            sectokInput.name = "sectok";
1753*2866e827SAtari911            sectokInput.value = JSINFO.sectok;
1754*2866e827SAtari911            form.appendChild(sectokInput);
1755*2866e827SAtari911
17561d05cddcSAtari911            const actionInput = document.createElement("input");
17571d05cddcSAtari911            actionInput.type = "hidden";
17581d05cddcSAtari911            actionInput.name = "action";
17591d05cddcSAtari911            actionInput.value = "delete_selected_events";
17601d05cddcSAtari911            form.appendChild(actionInput);
17611d05cddcSAtari911
17621d05cddcSAtari911            checkedBoxes.forEach(cb => {
17631d05cddcSAtari911                const eventInput = document.createElement("input");
17641d05cddcSAtari911                eventInput.type = "hidden";
17651d05cddcSAtari911                eventInput.name = "events[]";
17661d05cddcSAtari911                eventInput.value = cb.value;
17671d05cddcSAtari911                form.appendChild(eventInput);
17681d05cddcSAtari911            });
17691d05cddcSAtari911
17701d05cddcSAtari911            document.body.appendChild(form);
17711d05cddcSAtari911            form.submit();
17721d05cddcSAtari911        }
17731d05cddcSAtari911
17741d05cddcSAtari911        function createNewNamespace() {
177522228b0eSAtari911            const namespaceName = prompt(adminLang.enter_namespace);
17761d05cddcSAtari911
17771d05cddcSAtari911            if (!namespaceName) {
17781d05cddcSAtari911                return; // Cancelled
17791d05cddcSAtari911            }
17801d05cddcSAtari911
17811d05cddcSAtari911            // Validate namespace name
17821d05cddcSAtari911            if (!/^[a-zA-Z0-9_:-]+$/.test(namespaceName)) {
178322228b0eSAtari911                alert(adminLang.invalid_namespace);
17841d05cddcSAtari911                return;
17851d05cddcSAtari911            }
17861d05cddcSAtari911
17871d05cddcSAtari911            // Submit form to create namespace
17881d05cddcSAtari911            const form = document.createElement("form");
17891d05cddcSAtari911            form.method = "POST";
17901d05cddcSAtari911            form.action = "?do=admin&page=calendar&tab=manage";
17911d05cddcSAtari911
1792*2866e827SAtari911            var sectokInput = document.createElement("input");
1793*2866e827SAtari911            sectokInput.type = "hidden";
1794*2866e827SAtari911            sectokInput.name = "sectok";
1795*2866e827SAtari911            sectokInput.value = JSINFO.sectok;
1796*2866e827SAtari911            form.appendChild(sectokInput);
1797*2866e827SAtari911
17981d05cddcSAtari911            const actionInput = document.createElement("input");
17991d05cddcSAtari911            actionInput.type = "hidden";
18001d05cddcSAtari911            actionInput.name = "action";
18011d05cddcSAtari911            actionInput.value = "create_namespace";
18021d05cddcSAtari911            form.appendChild(actionInput);
18031d05cddcSAtari911
18041d05cddcSAtari911            const namespaceInput = document.createElement("input");
18051d05cddcSAtari911            namespaceInput.type = "hidden";
18061d05cddcSAtari911            namespaceInput.name = "namespace_name";
18071d05cddcSAtari911            namespaceInput.value = namespaceName;
18081d05cddcSAtari911            form.appendChild(namespaceInput);
18091d05cddcSAtari911
18101d05cddcSAtari911            document.body.appendChild(form);
18111d05cddcSAtari911            form.submit();
18121d05cddcSAtari911        }
18131d05cddcSAtari911
18141d05cddcSAtari911        function updateCount() {
18151d05cddcSAtari911            const count = document.querySelectorAll(".event-checkbox:checked").length;
181622228b0eSAtari911            document.getElementById("selectedCount").textContent = adminLang.x_selected.replace(/%d/, count);
18171d05cddcSAtari911        }
18181d05cddcSAtari911
18191d05cddcSAtari911        function deleteNamespace(namespace) {
182022228b0eSAtari911            const displayName = namespace || adminLang.default_ns;
182122228b0eSAtari911            if (!confirm(adminLang.delete_ns_confirm.replace(/%s/, displayName))) {
18221d05cddcSAtari911                return;
18231d05cddcSAtari911            }
18241d05cddcSAtari911            const form = document.createElement("form");
18251d05cddcSAtari911            form.method = "POST";
18261d05cddcSAtari911            form.action = "?do=admin&page=calendar&tab=manage";
1827*2866e827SAtari911
1828*2866e827SAtari911            var sectokInput = document.createElement("input");
1829*2866e827SAtari911            sectokInput.type = "hidden";
1830*2866e827SAtari911            sectokInput.name = "sectok";
1831*2866e827SAtari911            sectokInput.value = JSINFO.sectok;
1832*2866e827SAtari911            form.appendChild(sectokInput);
18331d05cddcSAtari911            const actionInput = document.createElement("input");
18341d05cddcSAtari911            actionInput.type = "hidden";
18351d05cddcSAtari911            actionInput.name = "action";
18361d05cddcSAtari911            actionInput.value = "delete_namespace";
18371d05cddcSAtari911            form.appendChild(actionInput);
18381d05cddcSAtari911            const nsInput = document.createElement("input");
18391d05cddcSAtari911            nsInput.type = "hidden";
18401d05cddcSAtari911            nsInput.name = "namespace";
18411d05cddcSAtari911            nsInput.value = namespace;
18421d05cddcSAtari911            form.appendChild(nsInput);
18431d05cddcSAtari911            document.body.appendChild(form);
18441d05cddcSAtari911            form.submit();
18451d05cddcSAtari911        }
18461d05cddcSAtari911
18479ccd446eSAtari911        function renameNamespace(oldNamespace) {
184822228b0eSAtari911            const displayName = oldNamespace || adminLang.default_ns;
184922228b0eSAtari911            const newName = prompt(adminLang.rename_namespace.replace(/%s/, displayName), oldNamespace);
18509ccd446eSAtari911            if (newName === null || newName === oldNamespace) {
18519ccd446eSAtari911                return; // Cancelled or no change
18529ccd446eSAtari911            }
18539ccd446eSAtari911            const form = document.createElement("form");
18549ccd446eSAtari911            form.method = "POST";
18559ccd446eSAtari911            form.action = "?do=admin&page=calendar&tab=manage";
1856*2866e827SAtari911
1857*2866e827SAtari911            var sectokInput = document.createElement("input");
1858*2866e827SAtari911            sectokInput.type = "hidden";
1859*2866e827SAtari911            sectokInput.name = "sectok";
1860*2866e827SAtari911            sectokInput.value = JSINFO.sectok;
1861*2866e827SAtari911            form.appendChild(sectokInput);
18629ccd446eSAtari911            const actionInput = document.createElement("input");
18639ccd446eSAtari911            actionInput.type = "hidden";
18649ccd446eSAtari911            actionInput.name = "action";
18659ccd446eSAtari911            actionInput.value = "rename_namespace";
18669ccd446eSAtari911            form.appendChild(actionInput);
18679ccd446eSAtari911            const oldInput = document.createElement("input");
18689ccd446eSAtari911            oldInput.type = "hidden";
18699ccd446eSAtari911            oldInput.name = "old_namespace";
18709ccd446eSAtari911            oldInput.value = oldNamespace;
18719ccd446eSAtari911            form.appendChild(oldInput);
18729ccd446eSAtari911            const newInput = document.createElement("input");
18739ccd446eSAtari911            newInput.type = "hidden";
18749ccd446eSAtari911            newInput.name = "new_namespace";
18759ccd446eSAtari911            newInput.value = newName;
18769ccd446eSAtari911            form.appendChild(newInput);
18779ccd446eSAtari911            document.body.appendChild(form);
18789ccd446eSAtari911            form.submit();
18799ccd446eSAtari911        }
18809ccd446eSAtari911
18811d05cddcSAtari911        let draggedEvent = null;
18821d05cddcSAtari911
18831d05cddcSAtari911        function dragStart(event, eventId) {
18841d05cddcSAtari911            const checkbox = event.target.closest(".event-row").querySelector(".event-checkbox");
18851d05cddcSAtari911
18861d05cddcSAtari911            // If this event is checked, drag all checked events
18871d05cddcSAtari911            const checkedBoxes = document.querySelectorAll(".event-checkbox:checked");
18881d05cddcSAtari911            if (checkbox && checkbox.checked && checkedBoxes.length > 1) {
18891d05cddcSAtari911                // Dragging multiple selected events
18901d05cddcSAtari911                draggedEvent = "MULTIPLE";
18911d05cddcSAtari911                event.dataTransfer.setData("text/plain", "MULTIPLE");
18921d05cddcSAtari911            } else {
18931d05cddcSAtari911                // Dragging single event
18941d05cddcSAtari911                draggedEvent = eventId;
18951d05cddcSAtari911                event.dataTransfer.setData("text/plain", eventId);
18961d05cddcSAtari911            }
18971d05cddcSAtari911            event.dataTransfer.effectAllowed = "move";
18981d05cddcSAtari911            event.target.style.opacity = "0.5";
18991d05cddcSAtari911        }
19001d05cddcSAtari911
19011d05cddcSAtari911        function allowDrop(event) {
19021d05cddcSAtari911            event.preventDefault();
19031d05cddcSAtari911            event.dataTransfer.dropEffect = "move";
19041d05cddcSAtari911        }
19051d05cddcSAtari911
19061d05cddcSAtari911        function drop(event, targetNamespace) {
19071d05cddcSAtari911            event.preventDefault();
19081d05cddcSAtari911
19091d05cddcSAtari911            if (draggedEvent === "MULTIPLE") {
19101d05cddcSAtari911                // Move all selected events
19111d05cddcSAtari911                const checkedBoxes = document.querySelectorAll(".event-checkbox:checked");
19121d05cddcSAtari911                if (checkedBoxes.length === 0) return;
19131d05cddcSAtari911
19141d05cddcSAtari911                const form = document.createElement("form");
19151d05cddcSAtari911                form.method = "POST";
19161d05cddcSAtari911                form.action = "?do=admin&page=calendar&tab=manage";
19171d05cddcSAtari911
1918*2866e827SAtari911            var sectokInput = document.createElement("input");
1919*2866e827SAtari911            sectokInput.type = "hidden";
1920*2866e827SAtari911            sectokInput.name = "sectok";
1921*2866e827SAtari911            sectokInput.value = JSINFO.sectok;
1922*2866e827SAtari911            form.appendChild(sectokInput);
1923*2866e827SAtari911
19241d05cddcSAtari911                const actionInput = document.createElement("input");
19251d05cddcSAtari911                actionInput.type = "hidden";
19261d05cddcSAtari911                actionInput.name = "action";
19271d05cddcSAtari911                actionInput.value = "move_selected_events";
19281d05cddcSAtari911                form.appendChild(actionInput);
19291d05cddcSAtari911
19301d05cddcSAtari911                checkedBoxes.forEach(cb => {
19311d05cddcSAtari911                    const eventInput = document.createElement("input");
19321d05cddcSAtari911                    eventInput.type = "hidden";
19331d05cddcSAtari911                    eventInput.name = "events[]";
19341d05cddcSAtari911                    eventInput.value = cb.value;
19351d05cddcSAtari911                    form.appendChild(eventInput);
19361d05cddcSAtari911                });
19371d05cddcSAtari911
19381d05cddcSAtari911                const targetInput = document.createElement("input");
19391d05cddcSAtari911                targetInput.type = "hidden";
19401d05cddcSAtari911                targetInput.name = "target_namespace";
19411d05cddcSAtari911                targetInput.value = targetNamespace;
19421d05cddcSAtari911                form.appendChild(targetInput);
19431d05cddcSAtari911
19441d05cddcSAtari911                document.body.appendChild(form);
19451d05cddcSAtari911                form.submit();
19461d05cddcSAtari911            } else {
19471d05cddcSAtari911                // Move single event
19481d05cddcSAtari911                if (!draggedEvent) return;
19491d05cddcSAtari911                const parts = draggedEvent.split("|");
19501d05cddcSAtari911                const sourceNamespace = parts[1];
19511d05cddcSAtari911                if (sourceNamespace === targetNamespace) return;
19521d05cddcSAtari911
19531d05cddcSAtari911                const form = document.createElement("form");
19541d05cddcSAtari911                form.method = "POST";
19551d05cddcSAtari911                form.action = "?do=admin&page=calendar&tab=manage";
1956*2866e827SAtari911
1957*2866e827SAtari911            var sectokInput = document.createElement("input");
1958*2866e827SAtari911            sectokInput.type = "hidden";
1959*2866e827SAtari911            sectokInput.name = "sectok";
1960*2866e827SAtari911            sectokInput.value = JSINFO.sectok;
1961*2866e827SAtari911            form.appendChild(sectokInput);
19621d05cddcSAtari911                const actionInput = document.createElement("input");
19631d05cddcSAtari911                actionInput.type = "hidden";
19641d05cddcSAtari911                actionInput.name = "action";
19651d05cddcSAtari911                actionInput.value = "move_single_event";
19661d05cddcSAtari911                form.appendChild(actionInput);
19671d05cddcSAtari911                const eventInput = document.createElement("input");
19681d05cddcSAtari911                eventInput.type = "hidden";
19691d05cddcSAtari911                eventInput.name = "event";
19701d05cddcSAtari911                eventInput.value = draggedEvent;
19711d05cddcSAtari911                form.appendChild(eventInput);
19721d05cddcSAtari911                const targetInput = document.createElement("input");
19731d05cddcSAtari911                targetInput.type = "hidden";
19741d05cddcSAtari911                targetInput.name = "target_namespace";
19751d05cddcSAtari911                targetInput.value = targetNamespace;
19761d05cddcSAtari911                form.appendChild(targetInput);
19771d05cddcSAtari911                document.body.appendChild(form);
19781d05cddcSAtari911                form.submit();
19791d05cddcSAtari911            }
19801d05cddcSAtari911        }
19811d05cddcSAtari911
198296df7d3eSAtari911        function editRecurringSeries(title, namespace, time, color, recurrenceType, recurrenceInterval, weekDays, monthlyType, monthDay, ordinalWeek, ordinalDay) {
19839ccd446eSAtari911            // Get available namespaces from the namespace explorer
19849ccd446eSAtari911            const namespaces = new Set();
19851d05cddcSAtari911
19869ccd446eSAtari911            // Method 1: Try to get from namespace explorer folder names
19879ccd446eSAtari911            document.querySelectorAll("[id^=ns_]").forEach(el => {
19889ccd446eSAtari911                const nsSpan = el.querySelector("span:nth-child(3)");
19899ccd446eSAtari911                if (nsSpan) {
19909ccd446eSAtari911                    let nsText = nsSpan.textContent.replace("�� ", "").trim();
19919ccd446eSAtari911                    if (nsText && nsText !== "(default)") {
19929ccd446eSAtari911                        namespaces.add(nsText);
19939ccd446eSAtari911                    }
19949ccd446eSAtari911                }
19959ccd446eSAtari911            });
19969ccd446eSAtari911
19979ccd446eSAtari911            // Method 2: Get from datalist if it exists
19989ccd446eSAtari911            document.querySelectorAll("#namespaceList option").forEach(opt => {
19999ccd446eSAtari911                if (opt.value && opt.value !== "") {
20009ccd446eSAtari911                    namespaces.add(opt.value);
20019ccd446eSAtari911                }
20029ccd446eSAtari911            });
20039ccd446eSAtari911
20049ccd446eSAtari911            // Convert to sorted array
20059ccd446eSAtari911            const nsArray = Array.from(namespaces).sort();
20069ccd446eSAtari911
200796df7d3eSAtari911            // Build namespace options
2008da206178SAtari911            let nsOptions = "<option value=\\"\\">(default)</option>";
20099ccd446eSAtari911            if (namespace && namespace !== "") {
2010da206178SAtari911                nsOptions += "<option value=\\"" + namespace + "\\" selected>" + namespace + " (current)</option>";
20119ccd446eSAtari911            }
20129ccd446eSAtari911            for (const ns of nsArray) {
20139ccd446eSAtari911                if (ns !== namespace) {
20149ccd446eSAtari911                    nsOptions += "<option value=\\"" + ns + "\\">" + ns + "</option>";
20151d05cddcSAtari911                }
20161d05cddcSAtari911            }
20171d05cddcSAtari911
201896df7d3eSAtari911            // Build weekday checkboxes - matching event editor style exactly
2019da206178SAtari911            const dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
202096df7d3eSAtari911            let weekDayChecks = "";
202196df7d3eSAtari911            for (let i = 0; i < 7; i++) {
202296df7d3eSAtari911                const checked = weekDays && weekDays.includes(i) ? " checked" : "";
202396df7d3eSAtari911                weekDayChecks += `<label style="display:inline-flex; align-items:center; padding:2px 6px; background:#1a1a1a; border:1px solid #333; border-radius:3px; cursor:pointer; font-size:10px;">
202496df7d3eSAtari911                    <input type="checkbox" name="weekDays" value="${i}"${checked} style="margin-right:3px; width:12px; height:12px;">
202596df7d3eSAtari911                    <span>${dayNames[i]}</span>
202696df7d3eSAtari911                </label>`;
202796df7d3eSAtari911            }
202896df7d3eSAtari911
202996df7d3eSAtari911            // Build ordinal week options
203096df7d3eSAtari911            let ordinalWeekOpts = "";
2031da206178SAtari911            const ordinalLabels = [[1,"First"], [2,"Second"], [3,"Third"], [4,"Fourth"], [5,"Fifth"], [-1,"Last"]];
203296df7d3eSAtari911            for (const [val, label] of ordinalLabels) {
203396df7d3eSAtari911                const selected = val === ordinalWeek ? " selected" : "";
203496df7d3eSAtari911                ordinalWeekOpts += `<option value="${val}"${selected}>${label}</option>`;
203596df7d3eSAtari911            }
203696df7d3eSAtari911
203796df7d3eSAtari911            // Build ordinal day options - full day names like event editor
2038da206178SAtari911            const fullDayNames = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
203996df7d3eSAtari911            let ordinalDayOpts = "";
204096df7d3eSAtari911            for (let i = 0; i < 7; i++) {
204196df7d3eSAtari911                const selected = i === ordinalDay ? " selected" : "";
204296df7d3eSAtari911                ordinalDayOpts += `<option value="${i}"${selected}>${fullDayNames[i]}</option>`;
204396df7d3eSAtari911            }
204496df7d3eSAtari911
20451d05cddcSAtari911            // Show edit dialog for recurring events
20461d05cddcSAtari911            const dialog = document.createElement("div");
204796df7d3eSAtari911            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; padding:20px; box-sizing:border-box;";
20481d05cddcSAtari911
20491d05cddcSAtari911            // Close on clicking background
20501d05cddcSAtari911            dialog.addEventListener("click", function(e) {
20511d05cddcSAtari911                if (e.target === dialog) {
20521d05cddcSAtari911                    dialog.remove();
20531d05cddcSAtari911                }
20541d05cddcSAtari911            });
20551d05cddcSAtari911
205696df7d3eSAtari911            const monthlyDayChecked = monthlyType !== "ordinalWeekday" ? "checked" : "";
205796df7d3eSAtari911            const monthlyOrdinalChecked = monthlyType === "ordinalWeekday" ? "checked" : "";
205896df7d3eSAtari911            const weeklyDisplay = recurrenceType === "weekly" ? "block" : "none";
205996df7d3eSAtari911            const monthlyDisplay = recurrenceType === "monthly" ? "block" : "none";
206096df7d3eSAtari911
206196df7d3eSAtari911            // Get recurrence type selection - matching event editor labels
2062da206178SAtari911            const recTypes = [["daily","Day(s)"], ["weekly","Week(s)"], ["monthly","Month(s)"], ["yearly","Year(s)"]];
206396df7d3eSAtari911            let recTypeOptions = "";
206496df7d3eSAtari911            for (const [val, label] of recTypes) {
206596df7d3eSAtari911                const selected = val === recurrenceType ? " selected" : "";
206696df7d3eSAtari911                recTypeOptions += `<option value="${val}"${selected}>${label}</option>`;
206796df7d3eSAtari911            }
206896df7d3eSAtari911
206996df7d3eSAtari911            // Input/select base style matching event editor
207096df7d3eSAtari911            const inputStyle = "width:100%; padding:6px 8px; border:2px solid #444; border-radius:4px; font-size:12px; box-sizing:border-box; background:#2a2a2a; color:#eee;";
207196df7d3eSAtari911            const inputSmallStyle = "padding:4px 6px; border:2px solid #444; border-radius:4px; font-size:11px; background:#2a2a2a; color:#eee;";
207296df7d3eSAtari911            const labelStyle = "display:block; font-size:10px; font-weight:500; margin-bottom:4px; color:#888;";
207396df7d3eSAtari911
20741d05cddcSAtari911            dialog.innerHTML = `
207596df7d3eSAtari911                <div style="background:#1e1e1e; padding:0; border-radius:8px; width:100%; max-width:450px; max-height:calc(100vh - 40px); overflow:hidden; display:flex; flex-direction:column; border:1px solid #00cc07; box-shadow:0 8px 32px rgba(0,0,0,0.4);">
20761d05cddcSAtari911
207796df7d3eSAtari911                    <!-- Header - matching event editor -->
207896df7d3eSAtari911                    <div style="display:flex; align-items:center; justify-content:space-between; padding:10px 14px; background:#2c3e50; color:white; flex-shrink:0;">
2079da206178SAtari911                        <h3 style="margin:0; font-size:15px; font-weight:600;">✏️ Edit Recurring Event</h3>
208096df7d3eSAtari911                        <button type="button" onclick="closeEditDialog()" style="background:rgba(255,255,255,0.2); border:none; color:white; font-size:22px; width:28px; height:28px; border-radius:50%; cursor:pointer; display:flex; align-items:center; justify-content:center; line-height:1; padding:0;">×</button>
20811d05cddcSAtari911                    </div>
20821d05cddcSAtari911
208396df7d3eSAtari911                    <!-- Form body - matching event editor -->
208496df7d3eSAtari911                    <form id="editRecurringForm" style="padding:10px 12px; overflow-y:auto; flex:1; display:flex; flex-direction:column; gap:8px;">
208596df7d3eSAtari911
2086da206178SAtari911                        <p style="margin:0 0 4px; color:#888; font-size:11px;">Changes apply to ALL occurrences of: <strong style="color:#00cc07;">${title}</strong></p>
208796df7d3eSAtari911
208896df7d3eSAtari911                        <!-- Title -->
20891d05cddcSAtari911                        <div>
2090da206178SAtari911                            <label style="${labelStyle}">�� TITLE</label>
209196df7d3eSAtari911                            <input type="text" name="new_title" value="${title}" style="${inputStyle}" required>
209296df7d3eSAtari911                        </div>
209396df7d3eSAtari911
209496df7d3eSAtari911                        <!-- Time Row -->
209596df7d3eSAtari911                        <div style="display:grid; grid-template-columns:1fr 1fr; gap:8px;">
209696df7d3eSAtari911                            <div>
2097da206178SAtari911                                <label style="${labelStyle}">�� START TIME</label>
209896df7d3eSAtari911                                <input type="time" name="start_time" value="${time || \'\'}" style="${inputStyle}">
20991d05cddcSAtari911                            </div>
21001d05cddcSAtari911                            <div>
2101da206178SAtari911                                <label style="${labelStyle}">�� END TIME</label>
210296df7d3eSAtari911                                <input type="time" name="end_time" style="${inputStyle}">
21031d05cddcSAtari911                            </div>
21041d05cddcSAtari911                        </div>
21051d05cddcSAtari911
210696df7d3eSAtari911                        <!-- Recurrence Pattern Box - matching event editor exactly -->
210796df7d3eSAtari911                        <div style="border:1px solid #333; border-radius:4px; padding:8px; margin:4px 0; background:rgba(0,0,0,0.2);">
210896df7d3eSAtari911
210996df7d3eSAtari911                            <!-- Repeat every [N] [period] -->
211096df7d3eSAtari911                            <div style="display:flex; gap:8px; align-items:flex-end; margin-bottom:6px;">
211196df7d3eSAtari911                                <div style="flex:0 0 auto;">
2112da206178SAtari911                                    <label style="${labelStyle}">Repeat every</label>
211396df7d3eSAtari911                                    <input type="number" name="recurrence_interval" value="${recurrenceInterval || 1}" min="1" max="99" style="width:50px; ${inputSmallStyle}">
211496df7d3eSAtari911                                </div>
211596df7d3eSAtari911                                <div style="flex:1;">
211696df7d3eSAtari911                                    <label style="${labelStyle}">&nbsp;</label>
211796df7d3eSAtari911                                    <select name="recurrence_type" id="editRecType" onchange="toggleEditRecOptions()" style="width:100%; ${inputSmallStyle}">
211896df7d3eSAtari911                                        ${recTypeOptions}
21191d05cddcSAtari911                                    </select>
21201d05cddcSAtari911                                </div>
212196df7d3eSAtari911                            </div>
21221d05cddcSAtari911
212396df7d3eSAtari911                            <!-- Weekly options - day checkboxes -->
212496df7d3eSAtari911                            <div id="editWeeklyOptions" style="display:${weeklyDisplay}; margin-bottom:6px;">
2125da206178SAtari911                                <label style="${labelStyle}">On these days:</label>
212696df7d3eSAtari911                                <div style="display:flex; flex-wrap:wrap; gap:2px;">
212796df7d3eSAtari911                                    ${weekDayChecks}
212896df7d3eSAtari911                                </div>
212996df7d3eSAtari911                            </div>
213096df7d3eSAtari911
213196df7d3eSAtari911                            <!-- Monthly options -->
213296df7d3eSAtari911                            <div id="editMonthlyOptions" style="display:${monthlyDisplay}; margin-bottom:6px;">
2133da206178SAtari911                                <label style="${labelStyle}">Repeat on:</label>
213496df7d3eSAtari911
213596df7d3eSAtari911                                <!-- Radio: Day of month vs Ordinal weekday -->
213696df7d3eSAtari911                                <div style="margin-bottom:6px;">
213796df7d3eSAtari911                                    <label style="display:inline-flex; align-items:center; margin-right:12px; cursor:pointer; font-size:11px; color:#ccc;">
213896df7d3eSAtari911                                        <input type="radio" name="monthly_type" value="dayOfMonth" ${monthlyDayChecked} onchange="toggleEditMonthlyType()" style="margin-right:4px;">
2139da206178SAtari911                                        Day of month
214096df7d3eSAtari911                                    </label>
214196df7d3eSAtari911                                    <label style="display:inline-flex; align-items:center; cursor:pointer; font-size:11px; color:#ccc;">
214296df7d3eSAtari911                                        <input type="radio" name="monthly_type" value="ordinalWeekday" ${monthlyOrdinalChecked} onchange="toggleEditMonthlyType()" style="margin-right:4px;">
2143da206178SAtari911                                        Weekday pattern
214496df7d3eSAtari911                                    </label>
214596df7d3eSAtari911                                </div>
214696df7d3eSAtari911
214796df7d3eSAtari911                                <!-- Day of month input -->
214896df7d3eSAtari911                                <div id="editMonthlyDay" style="display:${monthlyType !== "ordinalWeekday" ? "flex" : "none"}; align-items:center; gap:6px;">
2149da206178SAtari911                                    <span style="font-size:11px; color:#ccc;">Day</span>
215096df7d3eSAtari911                                    <input type="number" name="month_day" value="${monthDay || 1}" min="1" max="31" style="width:50px; ${inputSmallStyle}">
2151da206178SAtari911                                    <span style="font-size:10px; color:#666;">of each month</span>
215296df7d3eSAtari911                                </div>
215396df7d3eSAtari911
215496df7d3eSAtari911                                <!-- Ordinal weekday -->
215596df7d3eSAtari911                                <div id="editMonthlyOrdinal" style="display:${monthlyType === "ordinalWeekday" ? "flex" : "none"}; align-items:center; gap:4px; flex-wrap:wrap;">
215696df7d3eSAtari911                                    <select name="ordinal_week" style="width:auto; ${inputSmallStyle}">
215796df7d3eSAtari911                                        ${ordinalWeekOpts}
215896df7d3eSAtari911                                    </select>
215996df7d3eSAtari911                                    <select name="ordinal_day" style="width:auto; ${inputSmallStyle}">
216096df7d3eSAtari911                                        ${ordinalDayOpts}
216196df7d3eSAtari911                                    </select>
2162da206178SAtari911                                    <span style="font-size:10px; color:#666;">of each month</span>
216396df7d3eSAtari911                                </div>
216496df7d3eSAtari911                            </div>
216596df7d3eSAtari911
216696df7d3eSAtari911                            <!-- Repeat Until -->
21671d05cddcSAtari911                            <div>
2168da206178SAtari911                                <label style="${labelStyle}">Repeat Until (optional)</label>
216996df7d3eSAtari911                                <input type="date" name="recurrence_end" style="width:100%; ${inputSmallStyle}; box-sizing:border-box;">
2170da206178SAtari911                                <div style="font-size:9px; color:#666; margin-top:2px;">Leave empty to keep existing end date</div>
217196df7d3eSAtari911                            </div>
217296df7d3eSAtari911                        </div>
217396df7d3eSAtari911
217496df7d3eSAtari911                        <!-- Namespace -->
217596df7d3eSAtari911                        <div>
2176da206178SAtari911                            <label style="${labelStyle}">�� NAMESPACE</label>
217796df7d3eSAtari911                            <select name="new_namespace" style="${inputStyle}">
21781d05cddcSAtari911                                ${nsOptions}
21791d05cddcSAtari911                            </select>
21801d05cddcSAtari911                        </div>
21811d05cddcSAtari911                    </form>
218296df7d3eSAtari911
218396df7d3eSAtari911                    <!-- Footer buttons - matching event editor -->
218496df7d3eSAtari911                    <div style="display:flex; gap:8px; padding:12px 14px; background:#252525; border-top:1px solid #333; flex-shrink:0;">
2185da206178SAtari911                        <button type="button" onclick="closeEditDialog()" style="flex:1; background:#444; color:#ccc; padding:8px; border:none; border-radius:4px; cursor:pointer; font-size:12px;">Cancel</button>
2186da206178SAtari911                        <button type="button" onclick="document.getElementById(\x27editRecurringForm\x27).dispatchEvent(new Event(\x27submit\x27))" style="flex:1; background:#00cc07; color:white; padding:8px; border:none; border-radius:4px; cursor:pointer; font-weight:bold; font-size:12px; box-shadow:0 2px 4px rgba(0,0,0,0.2);">�� Save Changes</button>
218796df7d3eSAtari911                    </div>
21881d05cddcSAtari911                </div>
21891d05cddcSAtari911            `;
21901d05cddcSAtari911
21911d05cddcSAtari911            document.body.appendChild(dialog);
21921d05cddcSAtari911
219396df7d3eSAtari911            // Toggle functions for recurrence options
219496df7d3eSAtari911            window.toggleEditRecOptions = function() {
219596df7d3eSAtari911                const type = document.getElementById("editRecType").value;
219696df7d3eSAtari911                document.getElementById("editWeeklyOptions").style.display = type === "weekly" ? "block" : "none";
219796df7d3eSAtari911                document.getElementById("editMonthlyOptions").style.display = type === "monthly" ? "block" : "none";
219896df7d3eSAtari911            };
219996df7d3eSAtari911
220096df7d3eSAtari911            window.toggleEditMonthlyType = function() {
220196df7d3eSAtari911                const radio = document.querySelector("input[name=monthly_type]:checked");
220296df7d3eSAtari911                if (radio) {
220396df7d3eSAtari911                    document.getElementById("editMonthlyDay").style.display = radio.value === "dayOfMonth" ? "flex" : "none";
220496df7d3eSAtari911                    document.getElementById("editMonthlyOrdinal").style.display = radio.value === "ordinalWeekday" ? "flex" : "none";
220596df7d3eSAtari911                }
220696df7d3eSAtari911            };
220796df7d3eSAtari911
22081d05cddcSAtari911            // Add close function to window
22091d05cddcSAtari911            window.closeEditDialog = function() {
22101d05cddcSAtari911                dialog.remove();
22111d05cddcSAtari911            };
22121d05cddcSAtari911
22131d05cddcSAtari911            // Handle form submission
22141d05cddcSAtari911            dialog.querySelector("#editRecurringForm").addEventListener("submit", function(e) {
22151d05cddcSAtari911                e.preventDefault();
22161d05cddcSAtari911                const formData = new FormData(this);
22171d05cddcSAtari911
221896df7d3eSAtari911                // Collect weekDays as comma-separated string
221996df7d3eSAtari911                const weekDaysArr = [];
222096df7d3eSAtari911                document.querySelectorAll("input[name=weekDays]:checked").forEach(cb => {
222196df7d3eSAtari911                    weekDaysArr.push(cb.value);
222296df7d3eSAtari911                });
222396df7d3eSAtari911
22241d05cddcSAtari911                // Submit the edit
22251d05cddcSAtari911                const form = document.createElement("form");
22261d05cddcSAtari911                form.method = "POST";
22271d05cddcSAtari911                form.action = "?do=admin&page=calendar&tab=manage";
22281d05cddcSAtari911
2229*2866e827SAtari911            var sectokInput = document.createElement("input");
2230*2866e827SAtari911            sectokInput.type = "hidden";
2231*2866e827SAtari911            sectokInput.name = "sectok";
2232*2866e827SAtari911            sectokInput.value = JSINFO.sectok;
2233*2866e827SAtari911            form.appendChild(sectokInput);
2234*2866e827SAtari911
22351d05cddcSAtari911                const actionInput = document.createElement("input");
22361d05cddcSAtari911                actionInput.type = "hidden";
22371d05cddcSAtari911                actionInput.name = "action";
22381d05cddcSAtari911                actionInput.value = "edit_recurring_series";
22391d05cddcSAtari911                form.appendChild(actionInput);
22401d05cddcSAtari911
22411d05cddcSAtari911                const oldTitleInput = document.createElement("input");
22421d05cddcSAtari911                oldTitleInput.type = "hidden";
22431d05cddcSAtari911                oldTitleInput.name = "old_title";
22441d05cddcSAtari911                oldTitleInput.value = title;
22451d05cddcSAtari911                form.appendChild(oldTitleInput);
22461d05cddcSAtari911
22471d05cddcSAtari911                const oldNamespaceInput = document.createElement("input");
22481d05cddcSAtari911                oldNamespaceInput.type = "hidden";
22491d05cddcSAtari911                oldNamespaceInput.name = "old_namespace";
22501d05cddcSAtari911                oldNamespaceInput.value = namespace;
22511d05cddcSAtari911                form.appendChild(oldNamespaceInput);
22521d05cddcSAtari911
225396df7d3eSAtari911                // Add weekDays
225496df7d3eSAtari911                const weekDaysInput = document.createElement("input");
225596df7d3eSAtari911                weekDaysInput.type = "hidden";
225696df7d3eSAtari911                weekDaysInput.name = "week_days";
225796df7d3eSAtari911                weekDaysInput.value = weekDaysArr.join(",");
225896df7d3eSAtari911                form.appendChild(weekDaysInput);
225996df7d3eSAtari911
22601d05cddcSAtari911                // Add all form fields
22611d05cddcSAtari911                for (let [key, value] of formData.entries()) {
226296df7d3eSAtari911                    if (key === "weekDays") continue; // Skip individual checkboxes
22631d05cddcSAtari911                    const input = document.createElement("input");
22641d05cddcSAtari911                    input.type = "hidden";
22651d05cddcSAtari911                    input.name = key;
22661d05cddcSAtari911                    input.value = value;
22671d05cddcSAtari911                    form.appendChild(input);
22681d05cddcSAtari911                }
22691d05cddcSAtari911
22701d05cddcSAtari911                document.body.appendChild(form);
22711d05cddcSAtari911                form.submit();
22721d05cddcSAtari911            });
22731d05cddcSAtari911        }
22741d05cddcSAtari911
22751d05cddcSAtari911        function deleteRecurringSeries(title, namespace) {
227622228b0eSAtari911            const displayNs = namespace || adminLang.default_ns;
227722228b0eSAtari911            if (!confirm(adminLang.delete_recurring_confirm.replace(/%s/, title).replace(/%s/, displayNs))) {
22781d05cddcSAtari911                return;
22791d05cddcSAtari911            }
22801d05cddcSAtari911            const form = document.createElement("form");
22811d05cddcSAtari911            form.method = "POST";
22821d05cddcSAtari911            form.action = "?do=admin&page=calendar&tab=manage";
2283*2866e827SAtari911
2284*2866e827SAtari911            var sectokInput = document.createElement("input");
2285*2866e827SAtari911            sectokInput.type = "hidden";
2286*2866e827SAtari911            sectokInput.name = "sectok";
2287*2866e827SAtari911            sectokInput.value = JSINFO.sectok;
2288*2866e827SAtari911            form.appendChild(sectokInput);
22891d05cddcSAtari911            const actionInput = document.createElement("input");
22901d05cddcSAtari911            actionInput.type = "hidden";
22911d05cddcSAtari911            actionInput.name = "action";
22921d05cddcSAtari911            actionInput.value = "delete_recurring_series";
22931d05cddcSAtari911            form.appendChild(actionInput);
22941d05cddcSAtari911            const titleInput = document.createElement("input");
22951d05cddcSAtari911            titleInput.type = "hidden";
22961d05cddcSAtari911            titleInput.name = "event_title";
22971d05cddcSAtari911            titleInput.value = title;
22981d05cddcSAtari911            form.appendChild(titleInput);
22991d05cddcSAtari911            const namespaceInput = document.createElement("input");
23001d05cddcSAtari911            namespaceInput.type = "hidden";
23011d05cddcSAtari911            namespaceInput.name = "namespace";
23021d05cddcSAtari911            namespaceInput.value = namespace;
23031d05cddcSAtari911            form.appendChild(namespaceInput);
23041d05cddcSAtari911            document.body.appendChild(form);
23051d05cddcSAtari911            form.submit();
23061d05cddcSAtari911        }
23071d05cddcSAtari911
23081d05cddcSAtari911        document.addEventListener("dragend", function(e) {
23091d05cddcSAtari911            if (e.target.draggable) {
23101d05cddcSAtari911                e.target.style.opacity = "1";
23111d05cddcSAtari911            }
23121d05cddcSAtari911        });
23131d05cddcSAtari911        </script>';
23141d05cddcSAtari911    }
23151d05cddcSAtari911
23169ccd446eSAtari911    private function renderUpdateTab($colors = null) {
23171d05cddcSAtari911        global $INPUT;
23181d05cddcSAtari911
23199ccd446eSAtari911        // Use defaults if not provided
23209ccd446eSAtari911        if ($colors === null) {
23219ccd446eSAtari911            $colors = $this->getTemplateColors();
23229ccd446eSAtari911        }
23231d05cddcSAtari911
2324da206178SAtari911        echo '<h2 style="margin:10px 0; font-size:20px;">�� Update Plugin</h2>';
23251d05cddcSAtari911
23261d05cddcSAtari911        // Show message if present
23271d05cddcSAtari911        if ($INPUT->has('msg')) {
23281d05cddcSAtari911            $msg = hsc($INPUT->str('msg'));
23291d05cddcSAtari911            $type = $INPUT->str('msgtype', 'success');
23301d05cddcSAtari911            $class = ($type === 'success') ? 'msg success' : 'msg error';
23319ccd446eSAtari911            echo "<div class=\"$class\" style=\"padding:10px; margin:10px 0; border-left:3px solid " . ($type === 'success' ? '#28a745' : '#dc3545') . "; background:" . ($type === 'success' ? '#d4edda' : '#f8d7da') . "; border-radius:3px; max-width:1200px;\">";
23321d05cddcSAtari911            echo $msg;
23331d05cddcSAtari911            echo "</div>";
23341d05cddcSAtari911        }
23351d05cddcSAtari911
23369ccd446eSAtari911        // Show current version FIRST (MOVED TO TOP)
23371d05cddcSAtari911        $pluginInfo = DOKU_PLUGIN . 'calendar/plugin.info.txt';
23381d05cddcSAtari911        $info = ['version' => 'Unknown', 'date' => 'Unknown', 'name' => 'Calendar Plugin', 'author' => 'Unknown', 'email' => '', 'desc' => ''];
23391d05cddcSAtari911        if (file_exists($pluginInfo)) {
23401d05cddcSAtari911            $info = array_merge($info, confToHash($pluginInfo));
23411d05cddcSAtari911        }
23421d05cddcSAtari911
23439ccd446eSAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:1200px;">';
2344da206178SAtari911        echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">�� Current Version</h3>';
23451d05cddcSAtari911        echo '<div style="font-size:12px; line-height:1.6;">';
2346da206178SAtari911        echo '<div style="margin:4px 0;"><strong>Version:</strong> ' . hsc($info['version']) . ' (' . hsc($info['date']) . ')</div>';
2347da206178SAtari911        echo '<div style="margin:4px 0;"><strong>Author:</strong> ' . hsc($info['author']) . ($info['email'] ? ' &lt;' . hsc($info['email']) . '&gt;' : '') . '</div>';
23481d05cddcSAtari911        if ($info['desc']) {
2349da206178SAtari911            echo '<div style="margin:4px 0;"><strong>Description:</strong> ' . hsc($info['desc']) . '</div>';
23501d05cddcSAtari911        }
2351da206178SAtari911        echo '<div style="margin:4px 0;"><strong>Location:</strong> <code style="background:#f0f0f0; padding:2px 4px; border-radius:2px;">' . DOKU_PLUGIN . 'calendar/</code></div>';
23521d05cddcSAtari911        echo '</div>';
23531d05cddcSAtari911
23541d05cddcSAtari911        // Check permissions
23551d05cddcSAtari911        $pluginDir = DOKU_PLUGIN . 'calendar/';
23561d05cddcSAtari911        $pluginWritable = is_writable($pluginDir);
23571d05cddcSAtari911        $parentWritable = is_writable(DOKU_PLUGIN);
23581d05cddcSAtari911
23599ccd446eSAtari911        echo '<div style="margin-top:8px; padding-top:8px; border-top:1px solid ' . $colors['border'] . ';">';
23601d05cddcSAtari911        if ($pluginWritable && $parentWritable) {
2361da206178SAtari911            echo '<p style="margin:5px 0; font-size:13px; color:#28a745;"><strong>✅ Permissions:</strong> OK - ready to update</p>';
23621d05cddcSAtari911        } else {
2363da206178SAtari911            echo '<p style="margin:5px 0; font-size:13px; color:#dc3545;"><strong>❌ Permissions:</strong> Issues detected</p>';
23641d05cddcSAtari911            if (!$pluginWritable) {
2365da206178SAtari911                echo '<p style="margin:2px 0 2px 20px; font-size:12px; color:#dc3545;">Plugin directory not writable</p>';
23661d05cddcSAtari911            }
23671d05cddcSAtari911            if (!$parentWritable) {
2368da206178SAtari911                echo '<p style="margin:2px 0 2px 20px; font-size:12px; color:#dc3545;">Parent directory not writable</p>';
23691d05cddcSAtari911            }
2370da206178SAtari911            echo '<p style="margin:5px 0; font-size:12px; color:' . $colors['text'] . ';">Fix with: <code style="background:#f0f0f0; padding:2px 4px; border-radius:2px;">chmod -R 755 ' . DOKU_PLUGIN . 'calendar/</code></p>';
2371da206178SAtari911            echo '<p style="margin:2px 0; font-size:12px; color:' . $colors['text'] . ';">Or: <code style="background:#f0f0f0; padding:2px 4px; border-radius:2px;">chown -R www-data:www-data ' . DOKU_PLUGIN . 'calendar/</code></p>';
23721d05cddcSAtari911        }
23731d05cddcSAtari911        echo '</div>';
23741d05cddcSAtari911
23751d05cddcSAtari911        echo '</div>';
23761d05cddcSAtari911
23779ccd446eSAtari911        // Combined upload and notes section (SIDE BY SIDE)
23789ccd446eSAtari911        echo '<div style="display:flex; gap:15px; max-width:1200px; margin:10px 0;">';
23791d05cddcSAtari911
23809ccd446eSAtari911        // Left side - Upload form (60% width)
23819ccd446eSAtari911        echo '<div style="flex:1; min-width:0; background:' . $colors['bg'] . '; padding:12px; border-left:3px solid #00cc07; border-radius:3px;">';
2382da206178SAtari911        echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">�� Upload New Version</h3>';
2383da206178SAtari911        echo '<p style="color:' . $colors['text'] . '; font-size:13px; margin:0 0 10px;">Upload a calendar plugin ZIP file to update. Your configuration will be preserved.</p>';
23841d05cddcSAtari911
23851d05cddcSAtari911        echo '<form method="post" action="?do=admin&page=calendar&tab=update" enctype="multipart/form-data" id="uploadForm">';
2386*2866e827SAtari911        echo formSecurityToken(false);
23871d05cddcSAtari911        echo '<input type="hidden" name="action" value="upload_update">';
23881d05cddcSAtari911        echo '<div style="margin:10px 0;">';
23899ccd446eSAtari911        echo '<input type="file" name="plugin_zip" accept=".zip" required style="padding:8px; border:1px solid ' . $colors['border'] . '; border-radius:3px; font-size:13px; width:100%;">';
23901d05cddcSAtari911        echo '</div>';
23911d05cddcSAtari911        echo '<div style="margin:10px 0;">';
23921d05cddcSAtari911        echo '<label style="display:flex; align-items:center; gap:8px; font-size:13px;">';
23931d05cddcSAtari911        echo '<input type="checkbox" name="backup_first" value="1" checked>';
2394da206178SAtari911        echo '<span>Create backup before updating (Recommended)</span>';
23951d05cddcSAtari911        echo '</label>';
23961d05cddcSAtari911        echo '</div>';
23979ccd446eSAtari911
23989ccd446eSAtari911        // Buttons side by side
23999ccd446eSAtari911        echo '<div style="display:flex; gap:10px; align-items:center; flex-wrap:wrap;">';
2400da206178SAtari911        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>';
24011d05cddcSAtari911        echo '</form>';
24029ccd446eSAtari911
24039ccd446eSAtari911        // Clear Cache button (next to Upload button)
24049ccd446eSAtari911        echo '<form method="post" action="?do=admin&page=calendar&tab=update" style="display:inline; margin:0;">';
2405*2866e827SAtari911        echo formSecurityToken(false);
24069ccd446eSAtari911        echo '<input type="hidden" name="action" value="clear_cache">';
24079ccd446eSAtari911        echo '<input type="hidden" name="tab" value="update">';
2408da206178SAtari911        echo '<button type="submit" onclick="return confirm(\'Clear all DokuWiki cache? This will refresh all plugin files.\')" style="background:#ff9800; color:white; padding:10px 20px; border:none; border-radius:3px; cursor:pointer; font-size:14px; font-weight:bold;">��️ Clear Cache</button>';
24099ccd446eSAtari911        echo '</form>';
24101d05cddcSAtari911        echo '</div>';
24111d05cddcSAtari911
2412da206178SAtari911        echo '<p style="margin:8px 0 0 0; font-size:12px; color:' . $colors['text'] . ';">Clear the DokuWiki cache if changes aren\'t appearing or after updating the plugin.</p>';
24139ccd446eSAtari911        echo '</div>';
24149ccd446eSAtari911
24159ccd446eSAtari911        // Right side - Important Notes (40% width)
24169ccd446eSAtari911        echo '<div style="flex:0 0 350px; min-width:0; background:#fff3e0; border-left:3px solid #ff9800; padding:12px; border-radius:3px;">';
2417da206178SAtari911        echo '<h4 style="margin:0 0 5px 0; color:#e65100; font-size:14px;">⚠️ Important Notes</h4>';
24189ccd446eSAtari911        echo '<ul style="margin:5px 0; padding-left:20px; font-size:12px; color:#e65100; line-height:1.6;">';
2419da206178SAtari911        echo '<li>This will replace all plugin files</li>';
2420da206178SAtari911        echo '<li>Configuration files (sync_config.php) will be preserved</li>';
2421da206178SAtari911        echo '<li>Event data will not be affected</li>';
2422da206178SAtari911        echo '<li>Backup will be saved to: <code style="font-size:10px;">calendar.backup.vX.X.X.YYYY-MM-DD_HH-MM-SS.zip</code></li>';
2423da206178SAtari911        echo '<li>Make sure the ZIP file is a valid calendar plugin</li>';
24241d05cddcSAtari911        echo '</ul>';
24251d05cddcSAtari911        echo '</div>';
24261d05cddcSAtari911
24279ccd446eSAtari911        echo '</div>'; // End flex container
24289ccd446eSAtari911
24299ccd446eSAtari911        // Changelog section - Timeline viewer
24307e8ea635SAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:1200px;">';
2431da206178SAtari911        echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">�� Version History</h3>';
24329ccd446eSAtari911
24339ccd446eSAtari911        $changelogFile = DOKU_PLUGIN . 'calendar/CHANGELOG.md';
24349ccd446eSAtari911        if (file_exists($changelogFile)) {
24359ccd446eSAtari911            $changelog = file_get_contents($changelogFile);
24369ccd446eSAtari911
24379ccd446eSAtari911            // Parse ALL versions into structured data
24389ccd446eSAtari911            $lines = explode("\n", $changelog);
24399ccd446eSAtari911            $versions = [];
24409ccd446eSAtari911            $currentVersion = null;
24417e8ea635SAtari911            $currentSubsection = '';
24429ccd446eSAtari911
24439ccd446eSAtari911            foreach ($lines as $line) {
24447e8ea635SAtari911                $trimmed = trim($line);
24459ccd446eSAtari911
24469ccd446eSAtari911                // Version header (## Version X.X.X or ## Version X.X.X (date) - title)
24477e8ea635SAtari911                if (preg_match('/^## Version (.+?)(?:\s*\(([^)]+)\))?\s*(?:-\s*(.+))?$/', $trimmed, $matches)) {
24489ccd446eSAtari911                    if ($currentVersion !== null) {
24499ccd446eSAtari911                        $versions[] = $currentVersion;
24509ccd446eSAtari911                    }
24519ccd446eSAtari911                    $currentVersion = [
24529ccd446eSAtari911                        'number' => trim($matches[1]),
24539ccd446eSAtari911                        'date' => isset($matches[2]) ? trim($matches[2]) : '',
24549ccd446eSAtari911                        'title' => isset($matches[3]) ? trim($matches[3]) : '',
24559ccd446eSAtari911                        'items' => []
24569ccd446eSAtari911                    ];
24577e8ea635SAtari911                    $currentSubsection = '';
24589ccd446eSAtari911                }
24597e8ea635SAtari911                // Subsection header (### Something)
24607e8ea635SAtari911                elseif ($currentVersion !== null && preg_match('/^### (.+)$/', $trimmed, $matches)) {
24617e8ea635SAtari911                    $currentSubsection = trim($matches[1]);
24629ccd446eSAtari911                    $currentVersion['items'][] = [
24637e8ea635SAtari911                        'type' => 'section',
24647e8ea635SAtari911                        'desc' => $currentSubsection
24657e8ea635SAtari911                    ];
24667e8ea635SAtari911                }
24677e8ea635SAtari911                // Formatted item (- **Type:** description)
24687e8ea635SAtari911                elseif ($currentVersion !== null && preg_match('/^- \*\*(.+?):\*\*\s*(.+)$/', $trimmed, $matches)) {
24697e8ea635SAtari911                    $currentVersion['items'][] = [
24707e8ea635SAtari911                        'type' => trim($matches[1]),
24717e8ea635SAtari911                        'desc' => trim($matches[2])
24727e8ea635SAtari911                    ];
24737e8ea635SAtari911                }
24747e8ea635SAtari911                // Plain bullet item (- something)
24757e8ea635SAtari911                elseif ($currentVersion !== null && preg_match('/^- (.+)$/', $trimmed, $matches)) {
24767e8ea635SAtari911                    $currentVersion['items'][] = [
24777e8ea635SAtari911                        'type' => $currentSubsection ?: 'Changed',
24787e8ea635SAtari911                        'desc' => trim($matches[1])
24799ccd446eSAtari911                    ];
24809ccd446eSAtari911                }
24819ccd446eSAtari911            }
24827e8ea635SAtari911            // Don't forget last version
24839ccd446eSAtari911            if ($currentVersion !== null) {
24849ccd446eSAtari911                $versions[] = $currentVersion;
24859ccd446eSAtari911            }
24869ccd446eSAtari911
24879ccd446eSAtari911            $totalVersions = count($versions);
24889ccd446eSAtari911            $uniqueId = 'changelog_' . substr(md5(microtime()), 0, 6);
24899ccd446eSAtari911
24907e8ea635SAtari911            // Find the index of the currently running version
24917e8ea635SAtari911            $runningVersion = trim($info['version']);
24927e8ea635SAtari911            $runningIndex = 0;
24937e8ea635SAtari911            foreach ($versions as $idx => $ver) {
24947e8ea635SAtari911                if (trim($ver['number']) === $runningVersion) {
24957e8ea635SAtari911                    $runningIndex = $idx;
24967e8ea635SAtari911                    break;
24977e8ea635SAtari911                }
24987e8ea635SAtari911            }
24997e8ea635SAtari911
25009ccd446eSAtari911            if ($totalVersions > 0) {
25019ccd446eSAtari911                // Timeline navigation bar
25029ccd446eSAtari911                echo '<div id="' . $uniqueId . '_wrap" style="position:relative;">';
25039ccd446eSAtari911
25049ccd446eSAtari911                // Nav controls
25059ccd446eSAtari911                echo '<div style="display:flex; align-items:center; gap:8px; margin-bottom:10px;">';
25067e8ea635SAtari911                echo '<button id="' . $uniqueId . '_prev" onclick="changelogNav(\'' . $uniqueId . '\', -1)" style="background:none; border:1px solid ' . $colors['border'] . '; color:' . $colors['text'] . '; width:32px; height:32px; border-radius:50%; cursor:pointer; font-size:16px; display:flex; align-items:center; justify-content:center; transition:all 0.15s;" onmouseover="this.style.borderColor=\'#00cc07\'; this.style.color=\'#00cc07\'" onmouseout="this.style.borderColor=\'' . $colors['border'] . '\'; this.style.color=\'' . $colors['text'] . '\'">‹</button>';
25077e8ea635SAtari911                echo '<div style="flex:1; text-align:center; display:flex; align-items:center; justify-content:center; gap:10px;">';
25089ccd446eSAtari911                echo '<span id="' . $uniqueId . '_counter" style="font-size:11px; color:' . $colors['text'] . '; opacity:0.7;">1 of ' . $totalVersions . '</span>';
2509da206178SAtari911                echo '<button id="' . $uniqueId . '_current" onclick="changelogJumpTo(\'' . $uniqueId . '\', ' . $runningIndex . ')" style="background:#00cc07; border:none; color:#fff; padding:3px 10px; border-radius:3px; cursor:pointer; font-size:10px; font-weight:600; letter-spacing:0.3px; transition:all 0.15s;" onmouseover="this.style.filter=\'brightness(1.2)\'" onmouseout="this.style.filter=\'none\'">Current Release</button>';
25109ccd446eSAtari911                echo '</div>';
25117e8ea635SAtari911                echo '<button id="' . $uniqueId . '_next" onclick="changelogNav(\'' . $uniqueId . '\', 1)" style="background:none; border:1px solid ' . $colors['border'] . '; color:' . $colors['text'] . '; width:32px; height:32px; border-radius:50%; cursor:pointer; font-size:16px; display:flex; align-items:center; justify-content:center; transition:all 0.15s;" onmouseover="this.style.borderColor=\'#00cc07\'; this.style.color=\'#00cc07\'" onmouseout="this.style.borderColor=\'' . $colors['border'] . '\'; this.style.color=\'' . $colors['text'] . '\'">›</button>';
25129ccd446eSAtari911                echo '</div>';
25139ccd446eSAtari911
25149ccd446eSAtari911                // Version cards (one per version, only first visible)
25159ccd446eSAtari911                foreach ($versions as $i => $ver) {
25169ccd446eSAtari911                    $display = ($i === 0) ? 'block' : 'none';
25177e8ea635SAtari911                    $isRunning = (trim($ver['number']) === $runningVersion);
25187e8ea635SAtari911                    $cardBorder = $isRunning ? '2px solid #00cc07' : '1px solid ' . $colors['border'];
25197e8ea635SAtari911                    echo '<div class="' . $uniqueId . '_card" id="' . $uniqueId . '_card_' . $i . '" style="display:' . $display . '; padding:10px; background:' . $colors['bg'] . '; border:' . $cardBorder . '; border-left:3px solid #00cc07; border-radius:4px; transition:opacity 0.2s;">';
25209ccd446eSAtari911
25219ccd446eSAtari911                    // Version header
25229ccd446eSAtari911                    echo '<div style="display:flex; align-items:baseline; gap:8px; margin-bottom:8px;">';
25237e8ea635SAtari911                    echo '<span style="font-weight:bold; color:#00cc07; font-size:14px;">v' . hsc($ver['number']) . '</span>';
25247e8ea635SAtari911                    if ($isRunning) {
2525da206178SAtari911                        echo '<span style="background:#00cc07; color:#fff; padding:1px 6px; border-radius:3px; font-size:9px; font-weight:700; letter-spacing:0.3px;">RUNNING</span>';
25267e8ea635SAtari911                    }
25279ccd446eSAtari911                    if ($ver['date']) {
25289ccd446eSAtari911                        echo '<span style="font-size:11px; color:' . $colors['text'] . '; opacity:0.6;">' . hsc($ver['date']) . '</span>';
25299ccd446eSAtari911                    }
25309ccd446eSAtari911                    echo '</div>';
25319ccd446eSAtari911                    if ($ver['title']) {
25329ccd446eSAtari911                        echo '<div style="font-size:12px; font-weight:600; color:' . $colors['text'] . '; margin-bottom:8px;">' . hsc($ver['title']) . '</div>';
25339ccd446eSAtari911                    }
25349ccd446eSAtari911
25359ccd446eSAtari911                    // Change items
25369ccd446eSAtari911                    if (!empty($ver['items'])) {
25379ccd446eSAtari911                        echo '<div style="font-size:12px; line-height:1.7;">';
25389ccd446eSAtari911                        foreach ($ver['items'] as $item) {
25397e8ea635SAtari911                            if ($item['type'] === 'section') {
25407e8ea635SAtari911                                echo '<div style="margin:6px 0 2px 0; font-weight:700; color:#00cc07; font-size:11px; letter-spacing:0.3px;">' . hsc($item['desc']) . '</div>';
25417e8ea635SAtari911                                continue;
25427e8ea635SAtari911                            }
25439ccd446eSAtari911                            $color = '#666'; $icon = '•';
25449ccd446eSAtari911                            $t = $item['type'];
25457e8ea635SAtari911                            if ($t === 'Added' || $t === 'New') { $color = '#28a745'; $icon = '✨'; }
25467e8ea635SAtari911                            elseif ($t === 'Fixed' || $t === 'Fix' || $t === 'Bug Fix') { $color = '#dc3545'; $icon = '��'; }
25477e8ea635SAtari911                            elseif ($t === 'Changed' || $t === 'Change') { $color = '#00cc07'; $icon = '��'; }
25487e8ea635SAtari911                            elseif ($t === 'Improved' || $t === 'Enhancement') { $color = '#ff9800'; $icon = '⚡'; }
25499ccd446eSAtari911                            elseif ($t === 'Removed') { $color = '#e91e63'; $icon = '��️'; }
25509ccd446eSAtari911                            elseif ($t === 'Development' || $t === 'Refactored') { $color = '#6c757d'; $icon = '��️'; }
25519ccd446eSAtari911                            elseif ($t === 'Result') { $color = '#2196f3'; $icon = '✅'; }
25527e8ea635SAtari911                            else { $color = $colors['text']; $icon = '•'; }
25539ccd446eSAtari911
25549ccd446eSAtari911                            echo '<div style="margin:2px 0; padding-left:4px;">';
25559ccd446eSAtari911                            echo '<span style="color:' . $color . '; font-weight:600;">' . $icon . ' ' . hsc($item['type']) . ':</span> ';
25569ccd446eSAtari911                            echo '<span style="color:' . $colors['text'] . ';">' . hsc($item['desc']) . '</span>';
25579ccd446eSAtari911                            echo '</div>';
25589ccd446eSAtari911                        }
25599ccd446eSAtari911                        echo '</div>';
25609ccd446eSAtari911                    } else {
2561da206178SAtari911                        echo '<div style="font-size:11px; color:' . $colors['text'] . '; opacity:0.5; font-style:italic;">No detailed changes recorded</div>';
25629ccd446eSAtari911                    }
25639ccd446eSAtari911
25649ccd446eSAtari911                    echo '</div>';
25659ccd446eSAtari911                }
25669ccd446eSAtari911
25679ccd446eSAtari911                echo '</div>'; // wrap
25689ccd446eSAtari911
25699ccd446eSAtari911                // JavaScript for navigation
25709ccd446eSAtari911                echo '<script>
25719ccd446eSAtari911                (function() {
25729ccd446eSAtari911                    var id = "' . $uniqueId . '";
25739ccd446eSAtari911                    var total = ' . $totalVersions . ';
25749ccd446eSAtari911                    var current = 0;
25759ccd446eSAtari911
25767e8ea635SAtari911                    function showCard(idx) {
25779ccd446eSAtari911                        // Hide current
25789ccd446eSAtari911                        var curCard = document.getElementById(id + "_card_" + current);
25799ccd446eSAtari911                        if (curCard) curCard.style.display = "none";
25809ccd446eSAtari911
25817e8ea635SAtari911                        // Show target
25827e8ea635SAtari911                        current = idx;
25839ccd446eSAtari911                        var nextCard = document.getElementById(id + "_card_" + current);
25849ccd446eSAtari911                        if (nextCard) nextCard.style.display = "block";
25859ccd446eSAtari911
25869ccd446eSAtari911                        // Update counter
25879ccd446eSAtari911                        var counter = document.getElementById(id + "_counter");
25889ccd446eSAtari911                        if (counter) counter.textContent = (current + 1) + " of " + total;
25899ccd446eSAtari911
25909ccd446eSAtari911                        // Update button states
25919ccd446eSAtari911                        var prevBtn = document.getElementById(id + "_prev");
25929ccd446eSAtari911                        var nextBtn = document.getElementById(id + "_next");
25939ccd446eSAtari911                        if (prevBtn) prevBtn.style.opacity = (current === 0) ? "0.3" : "1";
25949ccd446eSAtari911                        if (nextBtn) nextBtn.style.opacity = (current === total - 1) ? "0.3" : "1";
25957e8ea635SAtari911                    }
25967e8ea635SAtari911
25977e8ea635SAtari911                    window.changelogNav = function(uid, dir) {
25987e8ea635SAtari911                        if (uid !== id) return;
25997e8ea635SAtari911                        var next = current + dir;
26007e8ea635SAtari911                        if (next < 0 || next >= total) return;
26017e8ea635SAtari911                        showCard(next);
26027e8ea635SAtari911                    };
26037e8ea635SAtari911
26047e8ea635SAtari911                    window.changelogJumpTo = function(uid, idx) {
26057e8ea635SAtari911                        if (uid !== id) return;
26067e8ea635SAtari911                        if (idx < 0 || idx >= total) return;
26077e8ea635SAtari911                        showCard(idx);
26089ccd446eSAtari911                    };
26099ccd446eSAtari911
26109ccd446eSAtari911                    // Initialize button states
26119ccd446eSAtari911                    var prevBtn = document.getElementById(id + "_prev");
26129ccd446eSAtari911                    if (prevBtn) prevBtn.style.opacity = "0.3";
26139ccd446eSAtari911                })();
26149ccd446eSAtari911                </script>';
26159ccd446eSAtari911
26169ccd446eSAtari911            } else {
2617da206178SAtari911                echo '<p style="color:#999; font-size:13px; font-style:italic;">No versions found in changelog</p>';
26189ccd446eSAtari911            }
26199ccd446eSAtari911        } else {
2620da206178SAtari911            echo '<p style="color:#999; font-size:13px; font-style:italic;">Changelog not available</p>';
26219ccd446eSAtari911        }
26229ccd446eSAtari911
26239ccd446eSAtari911        echo '</div>';
26249ccd446eSAtari911
26259ccd446eSAtari911        // Backup list or manual backup section
26261d05cddcSAtari911        $backupDir = DOKU_PLUGIN;
26271d05cddcSAtari911        $backups = glob($backupDir . 'calendar*.zip');
26281d05cddcSAtari911
26291d05cddcSAtari911        // Filter to only show files that look like backups (not the uploaded plugin files)
26301d05cddcSAtari911        $backups = array_filter($backups, function($file) {
26311d05cddcSAtari911            $name = basename($file);
26321d05cddcSAtari911            // Include files that start with "calendar" but exclude files that are just "calendar.zip" (uploaded plugin)
26331d05cddcSAtari911            return $name !== 'calendar.zip';
26341d05cddcSAtari911        });
26351d05cddcSAtari911
26369ccd446eSAtari911        // Always show backup section (even if no backups yet)
26379ccd446eSAtari911        echo '<div id="backupSection" style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:900px;">';
26389ccd446eSAtari911        echo '<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:8px;">';
2639da206178SAtari911        echo '<h3 style="margin:0; color:#00cc07; font-size:16px;">�� Backups</h3>';
26409ccd446eSAtari911
26419ccd446eSAtari911        // Manual backup button
26429ccd446eSAtari911        echo '<form method="post" action="?do=admin&page=calendar&tab=update" style="margin:0;">';
2643*2866e827SAtari911        echo formSecurityToken(false);
26449ccd446eSAtari911        echo '<input type="hidden" name="action" value="create_manual_backup">';
2645da206178SAtari911        echo '<button type="submit" onclick="return confirm(\'Create a backup of the current plugin version?\')" style="background:#00cc07; color:white; padding:6px 12px; border:none; border-radius:3px; cursor:pointer; font-size:12px; font-weight:bold;">�� Create Backup Now</button>';
26469ccd446eSAtari911        echo '</form>';
26479ccd446eSAtari911        echo '</div>';
26489ccd446eSAtari911
264996df7d3eSAtari911        // Restore instructions note
265096df7d3eSAtari911        echo '<div style="background:#1a2d1a; border:1px solid #00cc07; border-radius:3px; padding:8px 12px; margin-bottom:10px;">';
2651da206178SAtari911        echo '<p style="margin:0; color:#00cc07; font-size:12px;"><strong>�� Restore:</strong> Click the �� Restore button to reinstall from a backup. This uses DokuWiki\'s Extension Manager for safe installation. Alternatively, download the ZIP and upload via <strong>Admin → Extension Manager → Install</strong>.</p>';
265296df7d3eSAtari911        echo '</div>';
265396df7d3eSAtari911
26541d05cddcSAtari911        if (!empty($backups)) {
26551d05cddcSAtari911            rsort($backups); // Newest first
265696df7d3eSAtari911
265796df7d3eSAtari911            // Bulk action bar
265896df7d3eSAtari911            echo '<div id="bulkActionBar" style="display:flex; align-items:center; gap:10px; margin-bottom:8px; padding:6px 10px; background:#333; border-radius:3px;">';
265996df7d3eSAtari911            echo '<label style="display:flex; align-items:center; gap:5px; color:#ccc; font-size:12px; cursor:pointer;">';
266096df7d3eSAtari911            echo '<input type="checkbox" id="selectAllBackups" onchange="toggleAllBackups(this)" style="width:16px; height:16px;">';
2661da206178SAtari911            echo 'Select All</label>';
2662da206178SAtari911            echo '<span id="selectedCount" style="color:#888; font-size:11px;">(0 selected)</span>';
2663da206178SAtari911            echo '<button onclick="deleteSelectedBackups()" id="bulkDeleteBtn" style="background:#e74c3c; color:white; border:none; padding:4px 10px; border-radius:3px; cursor:pointer; font-size:11px; margin-left:auto; display:none;">��️ Delete Selected</button>';
266496df7d3eSAtari911            echo '</div>';
266596df7d3eSAtari911
26669ccd446eSAtari911            echo '<div style="max-height:200px; overflow-y:auto; border:1px solid ' . $colors['border'] . '; border-radius:3px; background:' . $colors['bg'] . ';">';
26679ccd446eSAtari911            echo '<table id="backupTable" style="width:100%; border-collapse:collapse; font-size:12px;">';
26681d05cddcSAtari911            echo '<thead style="position:sticky; top:0; background:#e9e9e9;">';
26691d05cddcSAtari911            echo '<tr>';
267096df7d3eSAtari911            echo '<th style="padding:6px; text-align:center; border-bottom:2px solid ' . $colors['border'] . '; width:30px;"></th>';
2671da206178SAtari911            echo '<th style="padding:6px; text-align:left; border-bottom:2px solid ' . $colors['border'] . ';">Backup File</th>';
2672da206178SAtari911            echo '<th style="padding:6px; text-align:left; border-bottom:2px solid ' . $colors['border'] . ';">Size</th>';
2673da206178SAtari911            echo '<th style="padding:6px; text-align:left; border-bottom:2px solid ' . $colors['border'] . ';">Actions</th>';
26741d05cddcSAtari911            echo '</tr></thead><tbody>';
26751d05cddcSAtari911
26761d05cddcSAtari911            foreach ($backups as $backup) {
26771d05cddcSAtari911                $filename = basename($backup);
26781d05cddcSAtari911                $size = $this->formatBytes(filesize($backup));
267996df7d3eSAtari911                echo '<tr style="border-bottom:1px solid #eee;" data-filename="' . hsc($filename) . '">';
268096df7d3eSAtari911                echo '<td style="padding:6px; text-align:center;"><input type="checkbox" class="backup-checkbox" value="' . hsc($filename) . '" onchange="updateSelectedCount()" style="width:16px; height:16px;"></td>';
26811d05cddcSAtari911                echo '<td style="padding:6px;"><code style="font-size:11px;">' . hsc($filename) . '</code></td>';
26821d05cddcSAtari911                echo '<td style="padding:6px;">' . $size . '</td>';
26831d05cddcSAtari911                echo '<td style="padding:6px; white-space:nowrap;">';
2684da206178SAtari911                echo '<a href="' . DOKU_BASE . 'lib/plugins/' . hsc($filename) . '" download style="color:#00cc07; text-decoration:none; font-size:11px; margin-right:10px;">�� Download</a>';
2685da206178SAtari911                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>';
2686da206178SAtari911                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;">✏️ Rename</button>';
26871d05cddcSAtari911                echo '</td>';
26881d05cddcSAtari911                echo '</tr>';
26891d05cddcSAtari911            }
26901d05cddcSAtari911
26911d05cddcSAtari911            echo '</tbody></table>';
26921d05cddcSAtari911            echo '</div>';
26939ccd446eSAtari911        } else {
2694da206178SAtari911            echo '<p style="color:' . $colors['text'] . '; font-size:13px; margin:8px 0;">No backups yet. Click "Create Backup Now" to create your first backup.</p>';
26951d05cddcSAtari911        }
26969ccd446eSAtari911        echo '</div>';
26971d05cddcSAtari911
26981d05cddcSAtari911        echo '<script>
26991d05cddcSAtari911        function confirmUpload() {
27001d05cddcSAtari911            const fileInput = document.querySelector(\'input[name="plugin_zip"]\');
27011d05cddcSAtari911            if (!fileInput.files[0]) {
2702da206178SAtari911                alert("Please select a ZIP file");
27031d05cddcSAtari911                return false;
27041d05cddcSAtari911            }
27051d05cddcSAtari911
27061d05cddcSAtari911            const fileName = fileInput.files[0].name;
27071d05cddcSAtari911            if (!fileName.endsWith(".zip")) {
2708da206178SAtari911                alert("Please select a ZIP file");
27091d05cddcSAtari911                return false;
27101d05cddcSAtari911            }
27111d05cddcSAtari911
2712da206178SAtari911            return confirm("Upload and install: " + fileName + "?\\n\\nThis will replace all plugin files.\\nYour configuration and data will be preserved.\\n\\nContinue?");
27131d05cddcSAtari911        }
27141d05cddcSAtari911
271596df7d3eSAtari911        // Toggle all backup checkboxes
271696df7d3eSAtari911        function toggleAllBackups(selectAllCheckbox) {
271796df7d3eSAtari911            const checkboxes = document.querySelectorAll(\'.backup-checkbox\');
271896df7d3eSAtari911            checkboxes.forEach(cb => cb.checked = selectAllCheckbox.checked);
271996df7d3eSAtari911            updateSelectedCount();
272096df7d3eSAtari911        }
272196df7d3eSAtari911
272296df7d3eSAtari911        // Update the selected count display
272396df7d3eSAtari911        function updateSelectedCount() {
272496df7d3eSAtari911            const checkboxes = document.querySelectorAll(\'.backup-checkbox:checked\');
272596df7d3eSAtari911            const count = checkboxes.length;
272696df7d3eSAtari911            const countSpan = document.getElementById(\'selectedCount\');
272796df7d3eSAtari911            const bulkDeleteBtn = document.getElementById(\'bulkDeleteBtn\');
272896df7d3eSAtari911            const selectAllCheckbox = document.getElementById(\'selectAllBackups\');
272996df7d3eSAtari911            const totalCheckboxes = document.querySelectorAll(\'.backup-checkbox\').length;
273096df7d3eSAtari911
2731da206178SAtari911            if (countSpan) countSpan.textContent = \'(\' + count + \' selected)\';
273296df7d3eSAtari911            if (bulkDeleteBtn) bulkDeleteBtn.style.display = count > 0 ? \'block\' : \'none\';
273396df7d3eSAtari911            if (selectAllCheckbox) selectAllCheckbox.checked = (count === totalCheckboxes && count > 0);
273496df7d3eSAtari911        }
273596df7d3eSAtari911
273696df7d3eSAtari911        // Delete selected backups
273796df7d3eSAtari911        function deleteSelectedBackups() {
273896df7d3eSAtari911            const checkboxes = document.querySelectorAll(\'.backup-checkbox:checked\');
273996df7d3eSAtari911            const filenames = Array.from(checkboxes).map(cb => cb.value);
274096df7d3eSAtari911
274196df7d3eSAtari911            if (filenames.length === 0) {
2742da206178SAtari911                alert(\'No backups selected\');
27431d05cddcSAtari911                return;
27441d05cddcSAtari911            }
27451d05cddcSAtari911
2746da206178SAtari911            if (!confirm(\'Delete \' + filenames.length + \' selected backup(s)?\\n\\n\' + filenames.join(\'\\n\') + \'\\n\\nThis cannot be undone!\')) {
274796df7d3eSAtari911                return;
274896df7d3eSAtari911            }
274996df7d3eSAtari911
275096df7d3eSAtari911            // Delete each backup sequentially
275196df7d3eSAtari911            let deleted = 0;
275296df7d3eSAtari911            let errors = [];
275396df7d3eSAtari911
275496df7d3eSAtari911            function deleteNext(index) {
275596df7d3eSAtari911                if (index >= filenames.length) {
275696df7d3eSAtari911                    // All done
275796df7d3eSAtari911                    if (errors.length > 0) {
2758da206178SAtari911                        alert(\'Deleted \' + deleted + \' backups. Errors: \' + errors.join(\', \'));
275996df7d3eSAtari911                    }
276096df7d3eSAtari911                    updateSelectedCount();
276196df7d3eSAtari911
276296df7d3eSAtari911                    // Check if table is now empty
276396df7d3eSAtari911                    const tbody = document.querySelector(\'#backupTable tbody\');
276496df7d3eSAtari911                    if (tbody && tbody.children.length === 0) {
276596df7d3eSAtari911                        location.reload();
276696df7d3eSAtari911                    }
276796df7d3eSAtari911                    return;
276896df7d3eSAtari911                }
276996df7d3eSAtari911
277096df7d3eSAtari911                const filename = filenames[index];
27719ccd446eSAtari911                const formData = new FormData();
27729ccd446eSAtari911                formData.append(\'action\', \'delete_backup\');
27739ccd446eSAtari911                formData.append(\'backup_file\', filename);
27741d05cddcSAtari911
27759ccd446eSAtari911                fetch(\'?do=admin&page=calendar&tab=update\', {
27769ccd446eSAtari911                    method: \'POST\',
27779ccd446eSAtari911                    body: formData
27789ccd446eSAtari911                })
27799ccd446eSAtari911                .then(response => response.text())
27809ccd446eSAtari911                .then(data => {
27819ccd446eSAtari911                    // Remove the row from the table
278296df7d3eSAtari911                    const row = document.querySelector(\'tr[data-filename="\' + filename + \'"]\');
278396df7d3eSAtari911                    if (row) {
278496df7d3eSAtari911                        row.style.transition = \'opacity 0.2s\';
27859ccd446eSAtari911                        row.style.opacity = \'0\';
278696df7d3eSAtari911                        setTimeout(() => row.remove(), 200);
27879ccd446eSAtari911                    }
278896df7d3eSAtari911                    deleted++;
278996df7d3eSAtari911                    deleteNext(index + 1);
27909ccd446eSAtari911                })
27919ccd446eSAtari911                .catch(error => {
279296df7d3eSAtari911                    errors.push(filename);
279396df7d3eSAtari911                    deleteNext(index + 1);
27949ccd446eSAtari911                });
27951d05cddcSAtari911            }
27961d05cddcSAtari911
279796df7d3eSAtari911            deleteNext(0);
279896df7d3eSAtari911        }
279996df7d3eSAtari911
28001d05cddcSAtari911        function restoreBackup(filename) {
2801da206178SAtari911            if (!confirm("Restore from backup: " + filename + "?\\n\\nThis will use DokuWiki\'s Extension Manager to reinstall the plugin from the backup.\\nYour current plugin files will be replaced.\\n\\nContinue?")) {
28021d05cddcSAtari911                return;
28031d05cddcSAtari911            }
28041d05cddcSAtari911
28051d05cddcSAtari911            const form = document.createElement("form");
28061d05cddcSAtari911            form.method = "POST";
28071d05cddcSAtari911            form.action = "?do=admin&page=calendar&tab=update";
28081d05cddcSAtari911
2809*2866e827SAtari911            var sectokInput = document.createElement("input");
2810*2866e827SAtari911            sectokInput.type = "hidden";
2811*2866e827SAtari911            sectokInput.name = "sectok";
2812*2866e827SAtari911            sectokInput.value = JSINFO.sectok;
2813*2866e827SAtari911            form.appendChild(sectokInput);
2814*2866e827SAtari911
28151d05cddcSAtari911            const actionInput = document.createElement("input");
28161d05cddcSAtari911            actionInput.type = "hidden";
28171d05cddcSAtari911            actionInput.name = "action";
28181d05cddcSAtari911            actionInput.value = "restore_backup";
28191d05cddcSAtari911            form.appendChild(actionInput);
28201d05cddcSAtari911
28211d05cddcSAtari911            const filenameInput = document.createElement("input");
28221d05cddcSAtari911            filenameInput.type = "hidden";
28231d05cddcSAtari911            filenameInput.name = "backup_file";
28241d05cddcSAtari911            filenameInput.value = filename;
28251d05cddcSAtari911            form.appendChild(filenameInput);
28261d05cddcSAtari911
28271d05cddcSAtari911            document.body.appendChild(form);
28281d05cddcSAtari911            form.submit();
28291d05cddcSAtari911        }
28301d05cddcSAtari911
28311d05cddcSAtari911        function renameBackup(filename) {
2832da206178SAtari911            const newName = prompt("Enter new backup name (without .zip extension):\\n\\nCurrent: " + filename.replace(/\\.zip$/, ""), filename.replace(/\\.zip$/, ""));
2833da206178SAtari911            if (!newName || newName === filename.replace(/\\.zip$/, "")) {
28341d05cddcSAtari911                return;
28351d05cddcSAtari911            }
28361d05cddcSAtari911
28371d05cddcSAtari911            // Add .zip if not present
28381d05cddcSAtari911            const newFilename = newName.endsWith(".zip") ? newName : newName + ".zip";
28391d05cddcSAtari911
28401d05cddcSAtari911            // Basic validation
28411d05cddcSAtari911            if (!/^[a-zA-Z0-9._-]+$/.test(newFilename.replace(/\\.zip$/, ""))) {
2842da206178SAtari911                alert("Invalid filename. Use only letters, numbers, dots, dashes, and underscores.");
28431d05cddcSAtari911                return;
28441d05cddcSAtari911            }
28451d05cddcSAtari911
28461d05cddcSAtari911            const form = document.createElement("form");
28471d05cddcSAtari911            form.method = "POST";
28481d05cddcSAtari911            form.action = "?do=admin&page=calendar&tab=update";
28491d05cddcSAtari911
2850*2866e827SAtari911            var sectokInput = document.createElement("input");
2851*2866e827SAtari911            sectokInput.type = "hidden";
2852*2866e827SAtari911            sectokInput.name = "sectok";
2853*2866e827SAtari911            sectokInput.value = JSINFO.sectok;
2854*2866e827SAtari911            form.appendChild(sectokInput);
2855*2866e827SAtari911
28561d05cddcSAtari911            const actionInput = document.createElement("input");
28571d05cddcSAtari911            actionInput.type = "hidden";
28581d05cddcSAtari911            actionInput.name = "action";
28591d05cddcSAtari911            actionInput.value = "rename_backup";
28601d05cddcSAtari911            form.appendChild(actionInput);
28611d05cddcSAtari911
28621d05cddcSAtari911            const oldNameInput = document.createElement("input");
28631d05cddcSAtari911            oldNameInput.type = "hidden";
28641d05cddcSAtari911            oldNameInput.name = "old_name";
28651d05cddcSAtari911            oldNameInput.value = filename;
28661d05cddcSAtari911            form.appendChild(oldNameInput);
28671d05cddcSAtari911
28681d05cddcSAtari911            const newNameInput = document.createElement("input");
28691d05cddcSAtari911            newNameInput.type = "hidden";
28701d05cddcSAtari911            newNameInput.name = "new_name";
28711d05cddcSAtari911            newNameInput.value = newFilename;
28721d05cddcSAtari911            form.appendChild(newNameInput);
28731d05cddcSAtari911
28741d05cddcSAtari911            document.body.appendChild(form);
28751d05cddcSAtari911            form.submit();
28761d05cddcSAtari911        }
28771d05cddcSAtari911        </script>';
28781d05cddcSAtari911    }
28791d05cddcSAtari911
28801d05cddcSAtari911    private function saveConfig() {
28811d05cddcSAtari911        global $INPUT;
28821d05cddcSAtari911
28831d05cddcSAtari911        // Load existing config to preserve all settings
2884*2866e827SAtari911        $configFile = $this->syncConfigPath();
28851d05cddcSAtari911        $existingConfig = [];
28861d05cddcSAtari911        if (file_exists($configFile)) {
28871d05cddcSAtari911            $existingConfig = include $configFile;
28881d05cddcSAtari911        }
28891d05cddcSAtari911
28901d05cddcSAtari911        // Update only the fields from the form - preserve everything else
28911d05cddcSAtari911        $config = $existingConfig;
28921d05cddcSAtari911
28931d05cddcSAtari911        // Update basic fields
28941d05cddcSAtari911        $config['tenant_id'] = $INPUT->str('tenant_id');
28951d05cddcSAtari911        $config['client_id'] = $INPUT->str('client_id');
28961d05cddcSAtari911        $config['client_secret'] = $INPUT->str('client_secret');
28971d05cddcSAtari911        $config['user_email'] = $INPUT->str('user_email');
28981d05cddcSAtari911        $config['timezone'] = $INPUT->str('timezone', 'America/Los_Angeles');
28991d05cddcSAtari911        $config['default_category'] = $INPUT->str('default_category', 'Blue category');
29001d05cddcSAtari911        $config['reminder_minutes'] = $INPUT->int('reminder_minutes', 15);
29011d05cddcSAtari911        $config['sync_completed_tasks'] = $INPUT->bool('sync_completed_tasks');
29021d05cddcSAtari911        $config['delete_outlook_events'] = $INPUT->bool('delete_outlook_events');
29031d05cddcSAtari911        $config['sync_all_namespaces'] = $INPUT->bool('sync_all_namespaces');
29041d05cddcSAtari911        $config['sync_namespaces'] = $INPUT->arr('sync_namespaces');
29054590242dSAtari911        // important_namespaces is managed from the Manage tab, preserve existing value
29064590242dSAtari911        if (!isset($config['important_namespaces'])) {
29074590242dSAtari911            $config['important_namespaces'] = 'important';
29084590242dSAtari911        }
29091d05cddcSAtari911
29101d05cddcSAtari911        // Parse category mapping
29111d05cddcSAtari911        $config['category_mapping'] = [];
29121d05cddcSAtari911        $mappingText = $INPUT->str('category_mapping');
29131d05cddcSAtari911        if ($mappingText) {
29141d05cddcSAtari911            $lines = explode("\n", $mappingText);
29151d05cddcSAtari911            foreach ($lines as $line) {
29161d05cddcSAtari911                $line = trim($line);
29171d05cddcSAtari911                if (empty($line)) continue;
29181d05cddcSAtari911                $parts = explode('=', $line, 2);
29191d05cddcSAtari911                if (count($parts) === 2) {
29201d05cddcSAtari911                    $config['category_mapping'][trim($parts[0])] = trim($parts[1]);
29211d05cddcSAtari911                }
29221d05cddcSAtari911            }
29231d05cddcSAtari911        }
29241d05cddcSAtari911
29251d05cddcSAtari911        // Parse color mapping from dropdown selections
29261d05cddcSAtari911        $config['color_mapping'] = [];
29271d05cddcSAtari911        $colorMappingCount = $INPUT->int('color_mapping_count', 0);
29281d05cddcSAtari911        for ($i = 0; $i < $colorMappingCount; $i++) {
29291d05cddcSAtari911            $hexColor = $INPUT->str('color_hex_' . $i);
29301d05cddcSAtari911            $category = $INPUT->str('color_map_' . $i);
29311d05cddcSAtari911
29321d05cddcSAtari911            if (!empty($hexColor) && !empty($category)) {
29331d05cddcSAtari911                $config['color_mapping'][$hexColor] = $category;
29341d05cddcSAtari911            }
29351d05cddcSAtari911        }
29361d05cddcSAtari911
29371d05cddcSAtari911        // Build file content using return format
29381d05cddcSAtari911        $content = "<?php\n";
29391d05cddcSAtari911        $content .= "/**\n";
29401d05cddcSAtari911        $content .= " * DokuWiki Calendar → Outlook Sync - Configuration\n";
29411d05cddcSAtari911        $content .= " * \n";
29421d05cddcSAtari911        $content .= " * SECURITY: Add this file to .gitignore!\n";
29431d05cddcSAtari911        $content .= " * Never commit credentials to version control.\n";
29441d05cddcSAtari911        $content .= " */\n\n";
29451d05cddcSAtari911        $content .= "return " . var_export($config, true) . ";\n";
29461d05cddcSAtari911
29471d05cddcSAtari911        // Save file
29481d05cddcSAtari911        if (file_put_contents($configFile, $content)) {
2949da206178SAtari911            $this->redirect('Configuration saved successfully!', 'success');
29501d05cddcSAtari911        } else {
2951da206178SAtari911            $this->redirect('Error: Could not save configuration file', 'error');
29521d05cddcSAtari911        }
29531d05cddcSAtari911    }
29541d05cddcSAtari911
29551d05cddcSAtari911    private function clearCache() {
2956*2866e827SAtari911        // Clear DokuWiki cache (farm-safe)
2957*2866e827SAtari911        global $conf;
2958*2866e827SAtari911        $cacheDir = $conf['cachedir'];
29591d05cddcSAtari911
29601d05cddcSAtari911        if (is_dir($cacheDir)) {
29611d05cddcSAtari911            $this->recursiveDelete($cacheDir, false);
2962da206178SAtari911            $this->redirect('Cache cleared successfully!', 'success', 'update');
29631d05cddcSAtari911        } else {
2964da206178SAtari911            $this->redirect('Cache directory not found', 'error', 'update');
29651d05cddcSAtari911        }
29661d05cddcSAtari911    }
29671d05cddcSAtari911
29681d05cddcSAtari911    private function recursiveDelete($dir, $deleteRoot = true) {
29691d05cddcSAtari911        if (!is_dir($dir)) return;
29701d05cddcSAtari911
29711d05cddcSAtari911        $files = array_diff(scandir($dir), array('.', '..'));
29721d05cddcSAtari911        foreach ($files as $file) {
29731d05cddcSAtari911            $path = $dir . '/' . $file;
29741d05cddcSAtari911            if (is_dir($path)) {
29751d05cddcSAtari911                $this->recursiveDelete($path, true);
29761d05cddcSAtari911            } else {
29771d05cddcSAtari911                @unlink($path);
29781d05cddcSAtari911            }
29791d05cddcSAtari911        }
29801d05cddcSAtari911
29811d05cddcSAtari911        if ($deleteRoot) {
29821d05cddcSAtari911            @rmdir($dir);
29831d05cddcSAtari911        }
29841d05cddcSAtari911    }
29851d05cddcSAtari911
29861d05cddcSAtari911    private function findRecurringEvents() {
2987*2866e827SAtari911        $dataDir = $this->metaDir();
29881d05cddcSAtari911        $recurring = [];
29891d05cddcSAtari911        $allEvents = []; // Track all events to detect patterns
29907e8ea635SAtari911        $flaggedSeries = []; // Track events with recurring flag by recurringId
29911d05cddcSAtari911
29927e8ea635SAtari911        // Helper to process events from a calendar directory
29937e8ea635SAtari911        $processCalendarDir = function($calDir, $fallbackNamespace) use (&$allEvents, &$flaggedSeries) {
29947e8ea635SAtari911            if (!is_dir($calDir)) return;
29957e8ea635SAtari911
29967e8ea635SAtari911            foreach (glob($calDir . '/*.json') as $file) {
2997815440faSAtari911                $data = CalendarFileHandler::readJson($file);
29987e8ea635SAtari911                if (!$data || !is_array($data)) continue;
29991d05cddcSAtari911
30001d05cddcSAtari911                foreach ($data as $dateKey => $events) {
300196df7d3eSAtari911                    // Skip non-date keys (like "mapping" or other metadata)
300296df7d3eSAtari911                    if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $dateKey)) continue;
300396df7d3eSAtari911
30047e8ea635SAtari911                    if (!is_array($events)) continue;
30051d05cddcSAtari911                    foreach ($events as $event) {
30067e8ea635SAtari911                        if (!isset($event['title']) || empty(trim($event['title']))) continue;
30071d05cddcSAtari911
30087e8ea635SAtari911                        $ns = isset($event['namespace']) ? $event['namespace'] : $fallbackNamespace;
30097e8ea635SAtari911
30107e8ea635SAtari911                        // If event has recurring flag, group by recurringId
30117e8ea635SAtari911                        if (!empty($event['recurring']) && !empty($event['recurringId'])) {
30127e8ea635SAtari911                            $rid = $event['recurringId'];
30137e8ea635SAtari911                            if (!isset($flaggedSeries[$rid])) {
30147e8ea635SAtari911                                $flaggedSeries[$rid] = [
30151d05cddcSAtari911                                    'title' => $event['title'],
30167e8ea635SAtari911                                    'namespace' => $ns,
30171d05cddcSAtari911                                    'dates' => [],
301896df7d3eSAtari911                                    'events' => [],
301996df7d3eSAtari911                                    // Capture recurrence metadata from first event
302096df7d3eSAtari911                                    'recurrenceType' => $event['recurrenceType'] ?? null,
302196df7d3eSAtari911                                    'recurrenceInterval' => $event['recurrenceInterval'] ?? 1,
302296df7d3eSAtari911                                    'weekDays' => $event['weekDays'] ?? [],
302396df7d3eSAtari911                                    'monthlyType' => $event['monthlyType'] ?? null,
302496df7d3eSAtari911                                    'monthDay' => $event['monthDay'] ?? null,
302596df7d3eSAtari911                                    'ordinalWeek' => $event['ordinalWeek'] ?? null,
302696df7d3eSAtari911                                    'ordinalDay' => $event['ordinalDay'] ?? null,
302796df7d3eSAtari911                                    'time' => $event['time'] ?? null,
302896df7d3eSAtari911                                    'endTime' => $event['endTime'] ?? null,
302996df7d3eSAtari911                                    'color' => $event['color'] ?? null
30301d05cddcSAtari911                                ];
30311d05cddcSAtari911                            }
30327e8ea635SAtari911                            $flaggedSeries[$rid]['dates'][] = $dateKey;
30337e8ea635SAtari911                            $flaggedSeries[$rid]['events'][] = $event;
30341d05cddcSAtari911                        }
30351d05cddcSAtari911
30367e8ea635SAtari911                        // Also group by title+namespace for pattern detection
30377e8ea635SAtari911                        $groupKey = strtolower(trim($event['title'])) . '|' . $ns;
30381d05cddcSAtari911
30391d05cddcSAtari911                        if (!isset($allEvents[$groupKey])) {
30401d05cddcSAtari911                            $allEvents[$groupKey] = [
30411d05cddcSAtari911                                'title' => $event['title'],
30427e8ea635SAtari911                                'namespace' => $ns,
30431d05cddcSAtari911                                'dates' => [],
30447e8ea635SAtari911                                'events' => [],
304596df7d3eSAtari911                                'hasFlag' => false,
304696df7d3eSAtari911                                'time' => $event['time'] ?? null,
304796df7d3eSAtari911                                'color' => $event['color'] ?? null
30481d05cddcSAtari911                            ];
30491d05cddcSAtari911                        }
30501d05cddcSAtari911                        $allEvents[$groupKey]['dates'][] = $dateKey;
30511d05cddcSAtari911                        $allEvents[$groupKey]['events'][] = $event;
30527e8ea635SAtari911                        if (!empty($event['recurring'])) {
30537e8ea635SAtari911                            $allEvents[$groupKey]['hasFlag'] = true;
30541d05cddcSAtari911                        }
30551d05cddcSAtari911                    }
30561d05cddcSAtari911                }
30571d05cddcSAtari911            }
30587e8ea635SAtari911        };
30597e8ea635SAtari911
30607e8ea635SAtari911        // Check root calendar directory (blank/default namespace)
30617e8ea635SAtari911        $processCalendarDir($dataDir . 'calendar', '');
30627e8ea635SAtari911
30637e8ea635SAtari911        // Scan all namespace directories (including nested)
30647e8ea635SAtari911        $this->scanNamespaceDirs($dataDir, $processCalendarDir);
30657e8ea635SAtari911
30667e8ea635SAtari911        // Deduplicate: remove from allEvents groups that are fully covered by flaggedSeries
30677e8ea635SAtari911        $flaggedTitleNs = [];
30687e8ea635SAtari911        foreach ($flaggedSeries as $rid => $series) {
30697e8ea635SAtari911            $key = strtolower(trim($series['title'])) . '|' . $series['namespace'];
30707e8ea635SAtari911            $flaggedTitleNs[$key] = $rid;
30717e8ea635SAtari911        }
30721d05cddcSAtari911
30737e8ea635SAtari911        // Build results from flaggedSeries first (known recurring)
30747e8ea635SAtari911        $seen = [];
30757e8ea635SAtari911        foreach ($flaggedSeries as $rid => $series) {
30767e8ea635SAtari911            sort($series['dates']);
30777e8ea635SAtari911            $dedupDates = array_unique($series['dates']);
30787e8ea635SAtari911
307996df7d3eSAtari911            // Use stored recurrence metadata if available, otherwise detect pattern
308096df7d3eSAtari911            $pattern = $this->formatRecurrencePattern($series);
308196df7d3eSAtari911            if (!$pattern) {
30827e8ea635SAtari911                $pattern = $this->detectRecurrencePattern($dedupDates);
308396df7d3eSAtari911            }
30847e8ea635SAtari911
30857e8ea635SAtari911            $recurring[] = [
30867e8ea635SAtari911                'baseId' => $rid,
30877e8ea635SAtari911                'title' => $series['title'],
30887e8ea635SAtari911                'namespace' => $series['namespace'],
30897e8ea635SAtari911                'pattern' => $pattern,
30907e8ea635SAtari911                'count' => count($dedupDates),
30917e8ea635SAtari911                'firstDate' => $dedupDates[0],
309296df7d3eSAtari911                'lastDate' => end($dedupDates),
309396df7d3eSAtari911                'hasFlag' => true,
309496df7d3eSAtari911                'time' => $series['time'],
309596df7d3eSAtari911                'endTime' => $series['endTime'],
309696df7d3eSAtari911                'color' => $series['color'],
309796df7d3eSAtari911                'recurrenceType' => $series['recurrenceType'],
309896df7d3eSAtari911                'recurrenceInterval' => $series['recurrenceInterval'],
309996df7d3eSAtari911                'weekDays' => $series['weekDays'],
310096df7d3eSAtari911                'monthlyType' => $series['monthlyType'],
310196df7d3eSAtari911                'monthDay' => $series['monthDay'],
310296df7d3eSAtari911                'ordinalWeek' => $series['ordinalWeek'],
310396df7d3eSAtari911                'ordinalDay' => $series['ordinalDay']
31047e8ea635SAtari911            ];
31057e8ea635SAtari911            $seen[strtolower(trim($series['title'])) . '|' . $series['namespace']] = true;
31067e8ea635SAtari911        }
31077e8ea635SAtari911
31087e8ea635SAtari911        // Add pattern-detected recurring (3+ occurrences, not already in flaggedSeries)
31091d05cddcSAtari911        foreach ($allEvents as $groupKey => $group) {
31107e8ea635SAtari911            if (isset($seen[$groupKey])) continue;
31111d05cddcSAtari911
31127e8ea635SAtari911            $dedupDates = array_unique($group['dates']);
31137e8ea635SAtari911            sort($dedupDates);
31141d05cddcSAtari911
31157e8ea635SAtari911            if (count($dedupDates) < 3) continue;
31161d05cddcSAtari911
31177e8ea635SAtari911            $pattern = $this->detectRecurrencePattern($dedupDates);
31187e8ea635SAtari911
31191d05cddcSAtari911            $baseId = isset($group['events'][0]['recurringId'])
31201d05cddcSAtari911                ? $group['events'][0]['recurringId']
31211d05cddcSAtari911                : md5($group['title'] . $group['namespace']);
31221d05cddcSAtari911
31231d05cddcSAtari911            $recurring[] = [
31241d05cddcSAtari911                'baseId' => $baseId,
31251d05cddcSAtari911                'title' => $group['title'],
31261d05cddcSAtari911                'namespace' => $group['namespace'],
31271d05cddcSAtari911                'pattern' => $pattern,
31287e8ea635SAtari911                'count' => count($dedupDates),
31297e8ea635SAtari911                'firstDate' => $dedupDates[0],
313096df7d3eSAtari911                'lastDate' => end($dedupDates),
313196df7d3eSAtari911                'hasFlag' => $group['hasFlag'],
313296df7d3eSAtari911                'time' => $group['time'],
313396df7d3eSAtari911                'color' => $group['color'],
313496df7d3eSAtari911                'recurrenceType' => null,
313596df7d3eSAtari911                'recurrenceInterval' => null,
313696df7d3eSAtari911                'weekDays' => null,
313796df7d3eSAtari911                'monthlyType' => null,
313896df7d3eSAtari911                'monthDay' => null,
313996df7d3eSAtari911                'ordinalWeek' => null,
314096df7d3eSAtari911                'ordinalDay' => null
31411d05cddcSAtari911            ];
31421d05cddcSAtari911        }
31437e8ea635SAtari911
31447e8ea635SAtari911        // Sort by title
31457e8ea635SAtari911        usort($recurring, function($a, $b) {
31467e8ea635SAtari911            return strcasecmp($a['title'], $b['title']);
31477e8ea635SAtari911        });
31487e8ea635SAtari911
31497e8ea635SAtari911        return $recurring;
31507e8ea635SAtari911    }
31517e8ea635SAtari911
31527e8ea635SAtari911    /**
315396df7d3eSAtari911     * Format a human-readable recurrence pattern from stored metadata
315496df7d3eSAtari911     */
315596df7d3eSAtari911    private function formatRecurrencePattern($series) {
315696df7d3eSAtari911        $type = $series['recurrenceType'] ?? null;
315796df7d3eSAtari911        $interval = $series['recurrenceInterval'] ?? 1;
315896df7d3eSAtari911
315996df7d3eSAtari911        if (!$type) return null;
316096df7d3eSAtari911
316196df7d3eSAtari911        $result = '';
316296df7d3eSAtari911
316396df7d3eSAtari911        switch ($type) {
316496df7d3eSAtari911            case 'daily':
316596df7d3eSAtari911                if ($interval == 1) {
3166da206178SAtari911                    $result = 'Daily';
316796df7d3eSAtari911                } else {
3168da206178SAtari911                    $result = "Every $interval days";
316996df7d3eSAtari911                }
317096df7d3eSAtari911                break;
317196df7d3eSAtari911
317296df7d3eSAtari911            case 'weekly':
317396df7d3eSAtari911                $weekDays = $series['weekDays'] ?? [];
3174da206178SAtari911                $dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
317596df7d3eSAtari911
317696df7d3eSAtari911                if ($interval == 1) {
3177da206178SAtari911                    $result = 'Weekly';
317896df7d3eSAtari911                } elseif ($interval == 2) {
3179da206178SAtari911                    $result = 'Bi-weekly';
318096df7d3eSAtari911                } else {
3181da206178SAtari911                    $result = "Every $interval weeks";
318296df7d3eSAtari911                }
318396df7d3eSAtari911
318496df7d3eSAtari911                if (!empty($weekDays) && count($weekDays) < 7) {
318596df7d3eSAtari911                    $dayLabels = array_map(function($d) use ($dayNames) {
318696df7d3eSAtari911                        return $dayNames[$d] ?? '';
318796df7d3eSAtari911                    }, $weekDays);
318896df7d3eSAtari911                    $result .= ' (' . implode(', ', $dayLabels) . ')';
318996df7d3eSAtari911                }
319096df7d3eSAtari911                break;
319196df7d3eSAtari911
319296df7d3eSAtari911            case 'monthly':
319396df7d3eSAtari911                $monthlyType = $series['monthlyType'] ?? 'dayOfMonth';
319496df7d3eSAtari911
319596df7d3eSAtari911                if ($interval == 1) {
3196da206178SAtari911                    $prefix = 'Monthly';
319796df7d3eSAtari911                } elseif ($interval == 3) {
3198da206178SAtari911                    $prefix = 'Quarterly';
319996df7d3eSAtari911                } elseif ($interval == 6) {
3200da206178SAtari911                    $prefix = 'Semi-annual';
320196df7d3eSAtari911                } else {
3202da206178SAtari911                    $prefix = "Every $interval months";
320396df7d3eSAtari911                }
320496df7d3eSAtari911
320596df7d3eSAtari911                if ($monthlyType === 'dayOfMonth') {
320696df7d3eSAtari911                    $day = $series['monthDay'] ?? '?';
3207da206178SAtari911                    $result = "$prefix (day $day)";
320896df7d3eSAtari911                } else {
3209da206178SAtari911                    $ordinalNames = [1 => '1st', 2 => '2nd', 3 => '3rd', 4 => '4th', 5 => '5th', -1 => 'Last'];
3210da206178SAtari911                    $dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
321196df7d3eSAtari911                    $ordinal = $ordinalNames[$series['ordinalWeek']] ?? '';
321296df7d3eSAtari911                    $dayName = $dayNames[$series['ordinalDay']] ?? '';
3213da206178SAtari911                    $result = "$prefix ($ordinal $dayName)";
321496df7d3eSAtari911                }
321596df7d3eSAtari911                break;
321696df7d3eSAtari911
321796df7d3eSAtari911            case 'yearly':
321896df7d3eSAtari911                if ($interval == 1) {
3219da206178SAtari911                    $result = 'Yearly';
322096df7d3eSAtari911                } else {
3221da206178SAtari911                    $result = "Every $interval years";
322296df7d3eSAtari911                }
322396df7d3eSAtari911                break;
322496df7d3eSAtari911
322596df7d3eSAtari911            default:
322696df7d3eSAtari911                $result = ucfirst($type);
322796df7d3eSAtari911        }
322896df7d3eSAtari911
322996df7d3eSAtari911        return $result;
323096df7d3eSAtari911    }
323196df7d3eSAtari911
323296df7d3eSAtari911    /**
32337e8ea635SAtari911     * Recursively scan namespace directories for calendar data
32347e8ea635SAtari911     */
32357e8ea635SAtari911    private function scanNamespaceDirs($baseDir, $callback) {
32367e8ea635SAtari911        foreach (glob($baseDir . '*', GLOB_ONLYDIR) as $nsDir) {
32377e8ea635SAtari911            $namespace = basename($nsDir);
32387e8ea635SAtari911
32397e8ea635SAtari911            // Skip the root 'calendar' dir (already processed)
32407e8ea635SAtari911            if ($namespace === 'calendar') continue;
32417e8ea635SAtari911
32427e8ea635SAtari911            $calendarDir = $nsDir . '/calendar';
32437e8ea635SAtari911            if (is_dir($calendarDir)) {
32447e8ea635SAtari911                // Derive namespace from path relative to meta dir
3245*2866e827SAtari911                $metaDir = $this->metaDir();
32467e8ea635SAtari911                $relPath = str_replace($metaDir, '', $nsDir);
32477e8ea635SAtari911                $ns = str_replace('/', ':', trim($relPath, '/'));
32487e8ea635SAtari911                $callback($calendarDir, $ns);
32497e8ea635SAtari911            }
32507e8ea635SAtari911
32517e8ea635SAtari911            // Recurse into subdirectories for nested namespaces
32527e8ea635SAtari911            $this->scanNamespaceDirs($nsDir . '/', $callback);
32537e8ea635SAtari911        }
32541d05cddcSAtari911    }
32551d05cddcSAtari911
32567e8ea635SAtari911    /**
32577e8ea635SAtari911     * Detect recurrence pattern from sorted dates using median interval
32587e8ea635SAtari911     */
32597e8ea635SAtari911    private function detectRecurrencePattern($dates) {
32607e8ea635SAtari911        if (count($dates) < 2) return 'Single';
32617e8ea635SAtari911
32627e8ea635SAtari911        // Calculate all intervals between consecutive dates
32637e8ea635SAtari911        $intervals = [];
32647e8ea635SAtari911        for ($i = 1; $i < count($dates); $i++) {
32657e8ea635SAtari911            try {
32667e8ea635SAtari911                $d1 = new DateTime($dates[$i - 1]);
32677e8ea635SAtari911                $d2 = new DateTime($dates[$i]);
32687e8ea635SAtari911                $intervals[] = $d1->diff($d2)->days;
32697e8ea635SAtari911            } catch (Exception $e) {
32707e8ea635SAtari911                continue;
32717e8ea635SAtari911            }
32727e8ea635SAtari911        }
32737e8ea635SAtari911
32747e8ea635SAtari911        if (empty($intervals)) return 'Custom';
32757e8ea635SAtari911
327696df7d3eSAtari911        // Check if all intervals are the same (or very close)
327796df7d3eSAtari911        $uniqueIntervals = array_unique($intervals);
327896df7d3eSAtari911        $isConsistent = (count($uniqueIntervals) === 1) ||
327996df7d3eSAtari911                        (max($intervals) - min($intervals) <= 1); // Allow 1 day variance
328096df7d3eSAtari911
32817e8ea635SAtari911        // Use median interval (more robust than first pair)
32827e8ea635SAtari911        sort($intervals);
32837e8ea635SAtari911        $mid = floor(count($intervals) / 2);
32847e8ea635SAtari911        $median = (count($intervals) % 2 === 0)
32857e8ea635SAtari911            ? ($intervals[$mid - 1] + $intervals[$mid]) / 2
32867e8ea635SAtari911            : $intervals[$mid];
32877e8ea635SAtari911
328896df7d3eSAtari911        // Check for specific day-based patterns first
32897e8ea635SAtari911        if ($median <= 1) return 'Daily';
329096df7d3eSAtari911
329196df7d3eSAtari911        // Check for every N days (2-6 days)
329296df7d3eSAtari911        if ($median >= 2 && $median <= 6 && $isConsistent) {
329396df7d3eSAtari911            return 'Every ' . round($median) . ' days';
329496df7d3eSAtari911        }
329596df7d3eSAtari911
329696df7d3eSAtari911        // Weekly patterns
32977e8ea635SAtari911        if ($median >= 6 && $median <= 8) return 'Weekly';
329896df7d3eSAtari911
329996df7d3eSAtari911        // Check for every N weeks
33007e8ea635SAtari911        if ($median >= 13 && $median <= 16) return 'Bi-weekly';
330196df7d3eSAtari911        if ($median >= 20 && $median <= 23) return 'Every 3 weeks';
330296df7d3eSAtari911
330396df7d3eSAtari911        // Monthly patterns
33047e8ea635SAtari911        if ($median >= 27 && $median <= 32) return 'Monthly';
330596df7d3eSAtari911
330696df7d3eSAtari911        // Check for every N months by looking at month differences
330796df7d3eSAtari911        if ($median >= 55 && $median <= 65) return 'Every 2 months';
33087e8ea635SAtari911        if ($median >= 89 && $median <= 93) return 'Quarterly';
330996df7d3eSAtari911        if ($median >= 115 && $median <= 125) return 'Every 4 months';
331096df7d3eSAtari911        if ($median >= 175 && $median <= 190) return 'Semi-annual';
331196df7d3eSAtari911
331296df7d3eSAtari911        // Yearly
33137e8ea635SAtari911        if ($median >= 363 && $median <= 368) return 'Yearly';
33147e8ea635SAtari911
331596df7d3eSAtari911        // For other intervals, calculate weeks if appropriate
331696df7d3eSAtari911        if ($median >= 7 && $median < 28) {
331796df7d3eSAtari911            $weeks = round($median / 7);
331896df7d3eSAtari911            if (abs($median - ($weeks * 7)) <= 1) {
331996df7d3eSAtari911                return "Every $weeks weeks";
332096df7d3eSAtari911            }
332196df7d3eSAtari911        }
332296df7d3eSAtari911
332396df7d3eSAtari911        // For monthly-ish intervals
332496df7d3eSAtari911        if ($median >= 28 && $median < 365) {
332596df7d3eSAtari911            $months = round($median / 30);
332696df7d3eSAtari911            if ($months >= 2 && abs($median - ($months * 30)) <= 3) {
332796df7d3eSAtari911                return "Every $months months";
332896df7d3eSAtari911            }
332996df7d3eSAtari911        }
333096df7d3eSAtari911
33317e8ea635SAtari911        return 'Every ~' . round($median) . ' days';
33327e8ea635SAtari911    }
33337e8ea635SAtari911
33347e8ea635SAtari911    /**
33357e8ea635SAtari911     * Render the recurring events table HTML
33367e8ea635SAtari911     */
33377e8ea635SAtari911    private function renderRecurringTable($recurringEvents, $colors) {
33387e8ea635SAtari911        if (empty($recurringEvents)) {
333922228b0eSAtari911            echo '<p style="color:' . $colors['text'] . '; font-size:13px; margin:5px 0;">' . $this->getLang('no_recurring_found') . '</p>';
33407e8ea635SAtari911            return;
33417e8ea635SAtari911        }
33427e8ea635SAtari911
33437e8ea635SAtari911        // Search bar
33447e8ea635SAtari911        echo '<div style="margin-bottom:8px;">';
334522228b0eSAtari911        echo '<input type="text" id="searchRecurring" onkeyup="filterRecurringEvents()" placeholder="�� ' . $this->getLang('search_recurring') . '" style="width:100%; padding:6px 10px; border:1px solid ' . $colors['border'] . '; border-radius:3px; font-size:12px;">';
33467e8ea635SAtari911        echo '</div>';
33477e8ea635SAtari911
33487e8ea635SAtari911        echo '<style>
33497e8ea635SAtari911            .sort-arrow {
33507e8ea635SAtari911                color: #999;
33517e8ea635SAtari911                font-size: 10px;
33527e8ea635SAtari911                margin-left: 3px;
33537e8ea635SAtari911                display: inline-block;
33547e8ea635SAtari911            }
33557e8ea635SAtari911            #recurringTable th:hover {
33567e8ea635SAtari911                background: #ddd;
33577e8ea635SAtari911            }
33587e8ea635SAtari911            #recurringTable th:hover .sort-arrow {
33597e8ea635SAtari911                color: #00cc07;
33607e8ea635SAtari911            }
33617e8ea635SAtari911            .recurring-row-hidden {
33627e8ea635SAtari911                display: none;
33637e8ea635SAtari911            }
336496df7d3eSAtari911            .pattern-badge {
336596df7d3eSAtari911                display: inline-block;
336696df7d3eSAtari911                padding: 1px 4px;
336796df7d3eSAtari911                border-radius: 3px;
336896df7d3eSAtari911                font-size: 9px;
336996df7d3eSAtari911                font-weight: bold;
337096df7d3eSAtari911            }
337196df7d3eSAtari911            .pattern-daily { background: #e3f2fd; color: #1565c0; }
337296df7d3eSAtari911            .pattern-weekly { background: #e8f5e9; color: #2e7d32; }
337396df7d3eSAtari911            .pattern-monthly { background: #fff3e0; color: #ef6c00; }
337496df7d3eSAtari911            .pattern-yearly { background: #fce4ec; color: #c2185b; }
337596df7d3eSAtari911            .pattern-custom { background: #f3e5f5; color: #7b1fa2; }
33767e8ea635SAtari911        </style>';
33777e8ea635SAtari911        echo '<div style="max-height:250px; overflow-y:auto; border:1px solid ' . $colors['border'] . '; border-radius:3px;">';
33787e8ea635SAtari911        echo '<table id="recurringTable" style="width:100%; border-collapse:collapse; font-size:11px;">';
33797e8ea635SAtari911        echo '<thead style="position:sticky; top:0; background:#e9e9e9;">';
33807e8ea635SAtari911        echo '<tr>';
338122228b0eSAtari911        echo '<th onclick="sortRecurringTable(0)" style="padding:4px 6px; text-align:left; border-bottom:2px solid #ddd; cursor:pointer; user-select:none;">' . $this->getLang('col_title') . ' <span class="sort-arrow">⇅</span></th>';
338222228b0eSAtari911        echo '<th onclick="sortRecurringTable(1)" style="padding:4px 6px; text-align:left; border-bottom:2px solid #ddd; cursor:pointer; user-select:none;">' . $this->getLang('col_namespace') . ' <span class="sort-arrow">⇅</span></th>';
338322228b0eSAtari911        echo '<th onclick="sortRecurringTable(2)" style="padding:4px 6px; text-align:left; border-bottom:2px solid #ddd; cursor:pointer; user-select:none;">' . $this->getLang('col_pattern') . ' <span class="sort-arrow">⇅</span></th>';
338422228b0eSAtari911        echo '<th onclick="sortRecurringTable(3)" style="padding:4px 6px; text-align:left; border-bottom:2px solid #ddd; cursor:pointer; user-select:none;">' . $this->getLang('col_range') . ' <span class="sort-arrow">⇅</span></th>';
338522228b0eSAtari911        echo '<th onclick="sortRecurringTable(4)" style="padding:4px 6px; text-align:left; border-bottom:2px solid #ddd; cursor:pointer; user-select:none;">' . $this->getLang('col_count') . ' <span class="sort-arrow">⇅</span></th>';
338622228b0eSAtari911        echo '<th onclick="sortRecurringTable(5)" style="padding:4px 6px; text-align:left; border-bottom:2px solid #ddd; cursor:pointer; user-select:none;">' . $this->getLang('col_source') . ' <span class="sort-arrow">⇅</span></th>';
338722228b0eSAtari911        echo '<th style="padding:4px 6px; text-align:left; border-bottom:2px solid #ddd;">' . $this->getLang('col_actions') . '</th>';
33887e8ea635SAtari911        echo '</tr></thead><tbody id="recurringTableBody">';
33897e8ea635SAtari911
339022228b0eSAtari911        // Pattern translations
339122228b0eSAtari911        $patternTranslations = [
339222228b0eSAtari911            'daily' => $this->getLang('pattern_daily'),
339322228b0eSAtari911            'weekly' => $this->getLang('pattern_weekly'),
339422228b0eSAtari911            'monthly' => $this->getLang('pattern_monthly'),
339522228b0eSAtari911            'yearly' => $this->getLang('pattern_yearly'),
339622228b0eSAtari911        ];
339722228b0eSAtari911
33987e8ea635SAtari911        foreach ($recurringEvents as $series) {
339922228b0eSAtari911            $sourceLabel = $series['hasFlag'] ? '��️ ' . $this->getLang('source_flagged') : '�� ' . $this->getLang('source_detected');
34007e8ea635SAtari911            $sourceColor = $series['hasFlag'] ? '#00cc07' : '#ff9800';
340196df7d3eSAtari911
340222228b0eSAtari911            // Determine pattern badge class and translate pattern
340396df7d3eSAtari911            $pattern = strtolower($series['pattern']);
340422228b0eSAtari911            $displayPattern = $series['pattern'];
340522228b0eSAtari911
340696df7d3eSAtari911            if (strpos($pattern, 'daily') !== false || strpos($pattern, 'day') !== false) {
340796df7d3eSAtari911                $patternClass = 'pattern-daily';
340822228b0eSAtari911                $displayPattern = $this->getLang('pattern_daily');
340996df7d3eSAtari911            } elseif (strpos($pattern, 'weekly') !== false || strpos($pattern, 'week') !== false) {
341096df7d3eSAtari911                $patternClass = 'pattern-weekly';
341122228b0eSAtari911                $displayPattern = $this->getLang('pattern_weekly');
341296df7d3eSAtari911            } elseif (strpos($pattern, 'monthly') !== false || strpos($pattern, 'month') !== false ||
341396df7d3eSAtari911                      strpos($pattern, 'quarterly') !== false || strpos($pattern, 'semi') !== false) {
341496df7d3eSAtari911                $patternClass = 'pattern-monthly';
341522228b0eSAtari911                $displayPattern = $this->getLang('pattern_monthly');
341696df7d3eSAtari911            } elseif (strpos($pattern, 'yearly') !== false || strpos($pattern, 'year') !== false) {
341796df7d3eSAtari911                $patternClass = 'pattern-yearly';
341822228b0eSAtari911                $displayPattern = $this->getLang('pattern_yearly');
341996df7d3eSAtari911            } else {
342096df7d3eSAtari911                $patternClass = 'pattern-custom';
342122228b0eSAtari911                $displayPattern = $this->getLang('pattern_custom');
342296df7d3eSAtari911            }
342396df7d3eSAtari911
342496df7d3eSAtari911            // Format date range
342596df7d3eSAtari911            $firstDate = date('M j, Y', strtotime($series['firstDate']));
342696df7d3eSAtari911            $lastDate = isset($series['lastDate']) ? date('M j, Y', strtotime($series['lastDate'])) : $firstDate;
342796df7d3eSAtari911            $dateRange = ($firstDate === $lastDate) ? $firstDate : "$firstDate$lastDate";
342896df7d3eSAtari911
34297e8ea635SAtari911            echo '<tr style="border-bottom:1px solid #eee;">';
34307e8ea635SAtari911            echo '<td style="padding:4px 6px;">' . hsc($series['title']) . '</td>';
343122228b0eSAtari911            echo '<td style="padding:4px 6px;"><code style="background:#f0f0f0; padding:1px 3px; border-radius:2px; font-size:10px;">' . hsc($series['namespace'] ?: $this->getLang('default_ns')) . '</code></td>';
343222228b0eSAtari911            echo '<td style="padding:4px 6px;"><span class="pattern-badge ' . $patternClass . '">' . hsc($displayPattern) . '</span></td>';
343396df7d3eSAtari911            echo '<td style="padding:4px 6px; font-size:10px;">' . $dateRange . '</td>';
34347e8ea635SAtari911            echo '<td style="padding:4px 6px;"><strong>' . $series['count'] . '</strong></td>';
34357e8ea635SAtari911            echo '<td style="padding:4px 6px;"><span style="color:' . $sourceColor . '; font-size:10px;">' . $sourceLabel . '</span></td>';
34367e8ea635SAtari911            echo '<td style="padding:4px 6px; white-space:nowrap;">';
343796df7d3eSAtari911
343896df7d3eSAtari911            // Prepare JS data - include recurrence metadata
34397e8ea635SAtari911            $jsTitle = hsc(addslashes($series['title']));
34407e8ea635SAtari911            $jsNs = hsc($series['namespace']);
34417e8ea635SAtari911            $jsCount = $series['count'];
34427e8ea635SAtari911            $jsFirst = hsc($series['firstDate']);
344396df7d3eSAtari911            $jsLast = hsc($series['lastDate'] ?? $series['firstDate']);
34447e8ea635SAtari911            $jsPattern = hsc($series['pattern']);
34457e8ea635SAtari911            $jsHasFlag = $series['hasFlag'] ? 'true' : 'false';
344696df7d3eSAtari911            $jsTime = hsc($series['time'] ?? '');
344796df7d3eSAtari911            $jsEndTime = hsc($series['endTime'] ?? '');
344896df7d3eSAtari911            $jsColor = hsc($series['color'] ?? '');
344996df7d3eSAtari911
345096df7d3eSAtari911            // Recurrence metadata for edit dialog
345196df7d3eSAtari911            $jsRecurrenceType = hsc($series['recurrenceType'] ?? '');
345296df7d3eSAtari911            $jsRecurrenceInterval = intval($series['recurrenceInterval'] ?? 1);
345396df7d3eSAtari911            $jsWeekDays = json_encode($series['weekDays'] ?? []);
345496df7d3eSAtari911            $jsMonthlyType = hsc($series['monthlyType'] ?? '');
345596df7d3eSAtari911            $jsMonthDay = intval($series['monthDay'] ?? 0);
345696df7d3eSAtari911            $jsOrdinalWeek = intval($series['ordinalWeek'] ?? 1);
345796df7d3eSAtari911            $jsOrdinalDay = intval($series['ordinalDay'] ?? 0);
345896df7d3eSAtari911
345922228b0eSAtari911            echo '<button onclick="editRecurringSeries(\'' . $jsTitle . '\', \'' . $jsNs . '\', \'' . $jsTime . '\', \'' . $jsColor . '\', \'' . $jsRecurrenceType . '\', ' . $jsRecurrenceInterval . ', ' . $jsWeekDays . ', \'' . $jsMonthlyType . '\', ' . $jsMonthDay . ', ' . $jsOrdinalWeek . ', ' . $jsOrdinalDay . ')" style="background:#00cc07; color:white; border:none; padding:2px 6px; border-radius:2px; cursor:pointer; font-size:10px; margin-right:2px;" title="' . $this->getLang('tooltip_edit') . '">' . $this->getLang('btn_edit') . '</button>';
346022228b0eSAtari911            echo '<button onclick="manageRecurringSeries(\'' . $jsTitle . '\', \'' . $jsNs . '\', ' . $jsCount . ', \'' . $jsFirst . '\', \'' . $jsLast . '\', \'' . $jsPattern . '\', ' . $jsHasFlag . ')" style="background:#ff9800; color:white; border:none; padding:2px 6px; border-radius:2px; cursor:pointer; font-size:10px; margin-right:2px;" title="' . $this->getLang('tooltip_manage') . '">' . $this->getLang('btn_manage') . '</button>';
346122228b0eSAtari911            echo '<button onclick="deleteRecurringSeries(\'' . $jsTitle . '\', \'' . $jsNs . '\')" style="background:#e74c3c; color:white; border:none; padding:2px 6px; border-radius:2px; cursor:pointer; font-size:10px;" title="' . $this->getLang('tooltip_delete_all') . '">' . $this->getLang('btn_del') . '</button>';
34627e8ea635SAtari911            echo '</td>';
34637e8ea635SAtari911            echo '</tr>';
34647e8ea635SAtari911        }
34657e8ea635SAtari911
34667e8ea635SAtari911        echo '</tbody></table>';
34677e8ea635SAtari911        echo '</div>';
346822228b0eSAtari911        echo '<p style="color:' . $colors['text'] . '; font-size:10px; margin:5px 0 0;">' . sprintf($this->getLang('total_series'), count($recurringEvents)) . '</p>';
34697e8ea635SAtari911    }
34707e8ea635SAtari911
34717e8ea635SAtari911    /**
34727e8ea635SAtari911     * AJAX handler: rescan recurring events and return HTML
34737e8ea635SAtari911     */
34747e8ea635SAtari911    private function handleCleanupEmptyNamespaces() {
34757e8ea635SAtari911        global $INPUT;
34767e8ea635SAtari911        $dryRun = $INPUT->bool('dry_run', false);
34777e8ea635SAtari911
3478*2866e827SAtari911        $metaDir = $this->metaDir();
34797e8ea635SAtari911        $details = [];
34807e8ea635SAtari911        $removedDirs = 0;
34817e8ea635SAtari911        $removedCalDirs = 0;
34827e8ea635SAtari911
34837e8ea635SAtari911        // 1. Find all calendar/ subdirectories anywhere under data/meta/
34847e8ea635SAtari911        $allCalDirs = [];
34857e8ea635SAtari911        $this->findAllCalendarDirsRecursive($metaDir, $allCalDirs);
34867e8ea635SAtari911
34877e8ea635SAtari911        // 2. Check each calendar dir for empty JSON files
34887e8ea635SAtari911        foreach ($allCalDirs as $calDir) {
34897e8ea635SAtari911            $jsonFiles = glob($calDir . '/*.json');
34907e8ea635SAtari911            $hasEvents = false;
34917e8ea635SAtari911
34927e8ea635SAtari911            foreach ($jsonFiles as $jsonFile) {
3493815440faSAtari911                $data = CalendarFileHandler::readJson($jsonFile);
34947e8ea635SAtari911                if ($data && is_array($data)) {
34957e8ea635SAtari911                    // Check if any date key has actual events
34967e8ea635SAtari911                    foreach ($data as $dateKey => $events) {
34977e8ea635SAtari911                        if (is_array($events) && !empty($events)) {
34987e8ea635SAtari911                            $hasEvents = true;
34997e8ea635SAtari911                            break 2;
35007e8ea635SAtari911                        }
35017e8ea635SAtari911                    }
35027e8ea635SAtari911                    // JSON file has data but all dates are empty — remove it
35037e8ea635SAtari911                    if (!$dryRun) unlink($jsonFile);
35047e8ea635SAtari911                }
35057e8ea635SAtari911            }
35067e8ea635SAtari911
35077e8ea635SAtari911            // Re-check after cleaning empty JSON files
35087e8ea635SAtari911            if (!$dryRun) {
35097e8ea635SAtari911                $jsonFiles = glob($calDir . '/*.json');
35107e8ea635SAtari911            }
35117e8ea635SAtari911
35127e8ea635SAtari911            // Derive display name from path
35137e8ea635SAtari911            $relPath = str_replace($metaDir, '', $calDir);
35147e8ea635SAtari911            $relPath = rtrim(str_replace('/calendar', '', $relPath), '/');
35157e8ea635SAtari911            $displayName = $relPath ?: '(root)';
35167e8ea635SAtari911
35177e8ea635SAtari911            if ($displayName === '(root)') continue; // Never remove root calendar dir
35187e8ea635SAtari911
35197e8ea635SAtari911            if (!$hasEvents || empty($jsonFiles)) {
35207e8ea635SAtari911                $removedCalDirs++;
35217e8ea635SAtari911                $details[] = "Remove empty calendar folder: " . $displayName . "/calendar/ (0 events)";
35227e8ea635SAtari911
35237e8ea635SAtari911                if (!$dryRun) {
35247e8ea635SAtari911                    // Remove all remaining files in calendar dir
35257e8ea635SAtari911                    foreach (glob($calDir . '/*') as $f) {
35267e8ea635SAtari911                        if (is_file($f)) unlink($f);
35277e8ea635SAtari911                    }
35287e8ea635SAtari911                    @rmdir($calDir);
35297e8ea635SAtari911
35307e8ea635SAtari911                    // Check if parent namespace dir is now empty too
35317e8ea635SAtari911                    $parentDir = dirname($calDir);
35327e8ea635SAtari911                    if ($parentDir !== $metaDir && is_dir($parentDir)) {
35337e8ea635SAtari911                        $remaining = array_diff(scandir($parentDir), ['.', '..']);
35347e8ea635SAtari911                        if (empty($remaining)) {
35357e8ea635SAtari911                            @rmdir($parentDir);
35367e8ea635SAtari911                            $removedDirs++;
35377e8ea635SAtari911                            $details[] = "Removed empty namespace directory: " . $displayName . "/";
35387e8ea635SAtari911                        }
35397e8ea635SAtari911                    }
35407e8ea635SAtari911                }
35417e8ea635SAtari911            }
35427e8ea635SAtari911        }
35437e8ea635SAtari911
35447e8ea635SAtari911        // 3. Also scan for namespace dirs that have a calendar/ subdir with 0 json files
35457e8ea635SAtari911        //    (already covered above, but also check for namespace dirs without calendar/ at all
35467e8ea635SAtari911        //    that are tracked in the event system)
35477e8ea635SAtari911
35487e8ea635SAtari911        $total = $removedCalDirs + $removedDirs;
35497e8ea635SAtari911        $message = $dryRun
35507e8ea635SAtari911            ? "Found $total item(s) to clean up"
35517e8ea635SAtari911            : "Cleaned up $removedCalDirs empty calendar folder(s)" . ($removedDirs > 0 ? " and $removedDirs empty namespace directory(ies)" : "");
35527e8ea635SAtari911
35537e8ea635SAtari911        if (!$dryRun) $this->clearStatsCache();
35547e8ea635SAtari911
35557e8ea635SAtari911        echo json_encode([
35567e8ea635SAtari911            'success' => true,
35577e8ea635SAtari911            'count' => $total,
35587e8ea635SAtari911            'message' => $message,
35597e8ea635SAtari911            'details' => $details
35607e8ea635SAtari911        ]);
35617e8ea635SAtari911    }
35627e8ea635SAtari911
35637e8ea635SAtari911    /**
35647e8ea635SAtari911     * Recursively find all 'calendar' directories under a base path
35657e8ea635SAtari911     */
35667e8ea635SAtari911    private function findAllCalendarDirsRecursive($baseDir, &$results) {
35677e8ea635SAtari911        $entries = glob($baseDir . '*', GLOB_ONLYDIR);
35687e8ea635SAtari911        if (!$entries) return;
35697e8ea635SAtari911
35707e8ea635SAtari911        foreach ($entries as $dir) {
35717e8ea635SAtari911            $name = basename($dir);
35727e8ea635SAtari911            if ($name === 'calendar') {
35737e8ea635SAtari911                $results[] = $dir;
35747e8ea635SAtari911            } else {
35757e8ea635SAtari911                // Check for calendar subdir
35767e8ea635SAtari911                if (is_dir($dir . '/calendar')) {
35777e8ea635SAtari911                    $results[] = $dir . '/calendar';
35787e8ea635SAtari911                }
35797e8ea635SAtari911                // Recurse into subdirectories for nested namespaces
35807e8ea635SAtari911                $this->findAllCalendarDirsRecursive($dir . '/', $results);
35817e8ea635SAtari911            }
35827e8ea635SAtari911        }
35837e8ea635SAtari911    }
35847e8ea635SAtari911
35857e8ea635SAtari911    private function handleTrimAllPastRecurring() {
35867e8ea635SAtari911        global $INPUT;
35877e8ea635SAtari911        $dryRun = $INPUT->bool('dry_run', false);
35887e8ea635SAtari911        $today = date('Y-m-d');
3589*2866e827SAtari911        $dataDir = $this->metaDir();
35907e8ea635SAtari911        $calendarDirs = [];
35917e8ea635SAtari911
35927e8ea635SAtari911        if (is_dir($dataDir . 'calendar')) {
35937e8ea635SAtari911            $calendarDirs[] = $dataDir . 'calendar';
35947e8ea635SAtari911        }
35957e8ea635SAtari911        $this->findCalendarDirs($dataDir, $calendarDirs);
35967e8ea635SAtari911
35977e8ea635SAtari911        $removed = 0;
35987e8ea635SAtari911
35997e8ea635SAtari911        foreach ($calendarDirs as $calDir) {
36007e8ea635SAtari911            foreach (glob($calDir . '/*.json') as $file) {
3601815440faSAtari911                $data = CalendarFileHandler::readJson($file);
36027e8ea635SAtari911                if (!$data || !is_array($data)) continue;
36037e8ea635SAtari911
36047e8ea635SAtari911                $modified = false;
36057e8ea635SAtari911                foreach ($data as $dateKey => &$dayEvents) {
360696df7d3eSAtari911                    // Skip non-date keys (like "mapping" or other metadata)
360796df7d3eSAtari911                    if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $dateKey)) continue;
360896df7d3eSAtari911
36097e8ea635SAtari911                    if ($dateKey >= $today) continue;
36107e8ea635SAtari911                    if (!is_array($dayEvents)) continue;
36117e8ea635SAtari911
36127e8ea635SAtari911                    $filtered = [];
36137e8ea635SAtari911                    foreach ($dayEvents as $event) {
36147e8ea635SAtari911                        if (!empty($event['recurring']) || !empty($event['recurringId'])) {
36157e8ea635SAtari911                            $removed++;
36167e8ea635SAtari911                            if (!$dryRun) $modified = true;
36177e8ea635SAtari911                        } else {
36187e8ea635SAtari911                            $filtered[] = $event;
36197e8ea635SAtari911                        }
36207e8ea635SAtari911                    }
36217e8ea635SAtari911                    if (!$dryRun) $dayEvents = $filtered;
36227e8ea635SAtari911                }
36237e8ea635SAtari911                unset($dayEvents);
36247e8ea635SAtari911
36257e8ea635SAtari911                if (!$dryRun && $modified) {
36267e8ea635SAtari911                    foreach ($data as $dk => $evts) {
36277e8ea635SAtari911                        if (empty($evts)) unset($data[$dk]);
36287e8ea635SAtari911                    }
36297e8ea635SAtari911                    if (empty($data)) {
36307e8ea635SAtari911                        unlink($file);
36317e8ea635SAtari911                    } else {
3632815440faSAtari911                        CalendarFileHandler::writeJson($file, $data);
36337e8ea635SAtari911                    }
36347e8ea635SAtari911                }
36357e8ea635SAtari911            }
36367e8ea635SAtari911        }
36377e8ea635SAtari911
36387e8ea635SAtari911        if (!$dryRun) $this->clearStatsCache();
3639da206178SAtari911        echo json_encode(['success' => true, 'count' => $removed, 'message' => "Removed $removed past recurring occurrences"]);
36407e8ea635SAtari911    }
36417e8ea635SAtari911
36427e8ea635SAtari911    private function handleRescanRecurring() {
36437e8ea635SAtari911        $colors = $this->getTemplateColors();
36447e8ea635SAtari911        $recurringEvents = $this->findRecurringEvents();
36457e8ea635SAtari911
36467e8ea635SAtari911        ob_start();
36477e8ea635SAtari911        $this->renderRecurringTable($recurringEvents, $colors);
36487e8ea635SAtari911        $html = ob_get_clean();
36497e8ea635SAtari911
36507e8ea635SAtari911        echo json_encode([
36517e8ea635SAtari911            'success' => true,
36527e8ea635SAtari911            'html' => $html,
36537e8ea635SAtari911            'count' => count($recurringEvents)
36547e8ea635SAtari911        ]);
36557e8ea635SAtari911    }
36567e8ea635SAtari911
36577e8ea635SAtari911    /**
36587e8ea635SAtari911     * Helper: find all events matching a title in a namespace's calendar dir
36597e8ea635SAtari911     */
36607e8ea635SAtari911    private function getRecurringSeriesEvents($title, $namespace) {
3661*2866e827SAtari911        $dataDir = $this->metaDir();
36627e8ea635SAtari911        if ($namespace !== '') {
36637e8ea635SAtari911            $dataDir .= str_replace(':', '/', $namespace) . '/';
36647e8ea635SAtari911        }
36657e8ea635SAtari911        $dataDir .= 'calendar/';
36667e8ea635SAtari911
36677e8ea635SAtari911        $events = []; // ['date' => dateKey, 'file' => filepath, 'event' => eventData, 'index' => idx]
36687e8ea635SAtari911
36697e8ea635SAtari911        if (!is_dir($dataDir)) return $events;
36707e8ea635SAtari911
36717e8ea635SAtari911        foreach (glob($dataDir . '*.json') as $file) {
3672815440faSAtari911            $data = CalendarFileHandler::readJson($file);
36737e8ea635SAtari911            if (!$data || !is_array($data)) continue;
36747e8ea635SAtari911
36757e8ea635SAtari911            foreach ($data as $dateKey => $dayEvents) {
367696df7d3eSAtari911                // Skip non-date keys (like "mapping" or other metadata)
367796df7d3eSAtari911                if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $dateKey)) continue;
367896df7d3eSAtari911
36797e8ea635SAtari911                if (!is_array($dayEvents)) continue;
36807e8ea635SAtari911                foreach ($dayEvents as $idx => $event) {
368196df7d3eSAtari911                    if (!isset($event['title'])) continue;
36827e8ea635SAtari911                    if (strtolower(trim($event['title'])) === strtolower(trim($title))) {
36837e8ea635SAtari911                        $events[] = [
36847e8ea635SAtari911                            'date' => $dateKey,
36857e8ea635SAtari911                            'file' => $file,
36867e8ea635SAtari911                            'event' => $event,
36877e8ea635SAtari911                            'index' => $idx
36887e8ea635SAtari911                        ];
36897e8ea635SAtari911                    }
36907e8ea635SAtari911                }
36917e8ea635SAtari911            }
36927e8ea635SAtari911        }
36937e8ea635SAtari911
36947e8ea635SAtari911        // Sort by date
36957e8ea635SAtari911        usort($events, function($a, $b) {
36967e8ea635SAtari911            return strcmp($a['date'], $b['date']);
36977e8ea635SAtari911        });
36987e8ea635SAtari911
36997e8ea635SAtari911        return $events;
37007e8ea635SAtari911    }
37017e8ea635SAtari911
37027e8ea635SAtari911    /**
37037e8ea635SAtari911     * Extend series: add more future occurrences
37047e8ea635SAtari911     */
37057e8ea635SAtari911    private function handleExtendRecurring() {
37067e8ea635SAtari911        global $INPUT;
37077e8ea635SAtari911        $title = $INPUT->str('title');
37087e8ea635SAtari911        $namespace = $INPUT->str('namespace');
37097e8ea635SAtari911        $count = $INPUT->int('count', 4);
37107e8ea635SAtari911        $intervalDays = $INPUT->int('interval_days', 7);
37117e8ea635SAtari911
37127e8ea635SAtari911        $events = $this->getRecurringSeriesEvents($title, $namespace);
37137e8ea635SAtari911        if (empty($events)) {
37147e8ea635SAtari911            echo json_encode(['success' => false, 'error' => 'Series not found']);
37157e8ea635SAtari911            return;
37167e8ea635SAtari911        }
37177e8ea635SAtari911
37187e8ea635SAtari911        // Use last event as template
37197e8ea635SAtari911        $lastEvent = end($events);
37207e8ea635SAtari911        $lastDate = new DateTime($lastEvent['date']);
37217e8ea635SAtari911        $template = $lastEvent['event'];
37227e8ea635SAtari911
3723*2866e827SAtari911        $dataDir = $this->metaDir();
37247e8ea635SAtari911        if ($namespace !== '') {
37257e8ea635SAtari911            $dataDir .= str_replace(':', '/', $namespace) . '/';
37267e8ea635SAtari911        }
37277e8ea635SAtari911        $dataDir .= 'calendar/';
37287e8ea635SAtari911
37297e8ea635SAtari911        if (!is_dir($dataDir)) mkdir($dataDir, 0755, true);
37307e8ea635SAtari911
37317e8ea635SAtari911        $added = 0;
37327e8ea635SAtari911        $baseId = isset($template['recurringId']) ? $template['recurringId'] : md5($title . $namespace);
37337e8ea635SAtari911        $maxExistingIdx = 0;
37347e8ea635SAtari911        foreach ($events as $e) {
37357e8ea635SAtari911            if (isset($e['event']['id']) && preg_match('/-(\d+)$/', $e['event']['id'], $m)) {
37367e8ea635SAtari911                $maxExistingIdx = max($maxExistingIdx, (int)$m[1]);
37377e8ea635SAtari911            }
37387e8ea635SAtari911        }
37397e8ea635SAtari911
37407e8ea635SAtari911        for ($i = 1; $i <= $count; $i++) {
37417e8ea635SAtari911            $newDate = clone $lastDate;
37427e8ea635SAtari911            $newDate->modify('+' . ($i * $intervalDays) . ' days');
37437e8ea635SAtari911            $dateKey = $newDate->format('Y-m-d');
37447e8ea635SAtari911            list($year, $month) = explode('-', $dateKey);
37457e8ea635SAtari911
37467e8ea635SAtari911            $file = $dataDir . sprintf('%04d-%02d.json', $year, $month);
3747815440faSAtari911            $fileData = file_exists($file) ? CalendarFileHandler::readJson($file) : [];
37487e8ea635SAtari911            if (!is_array($fileData)) $fileData = [];
37497e8ea635SAtari911
37507e8ea635SAtari911            if (!isset($fileData[$dateKey])) $fileData[$dateKey] = [];
37517e8ea635SAtari911
37527e8ea635SAtari911            $newEvent = $template;
37537e8ea635SAtari911            $newEvent['id'] = $baseId . '-' . ($maxExistingIdx + $i);
37547e8ea635SAtari911            $newEvent['recurring'] = true;
37557e8ea635SAtari911            $newEvent['recurringId'] = $baseId;
37567e8ea635SAtari911            $newEvent['created'] = date('Y-m-d H:i:s');
37577e8ea635SAtari911            unset($newEvent['completed']);
37587e8ea635SAtari911            $newEvent['completed'] = false;
37597e8ea635SAtari911
37607e8ea635SAtari911            $fileData[$dateKey][] = $newEvent;
3761815440faSAtari911            CalendarFileHandler::writeJson($file, $fileData);
37627e8ea635SAtari911            $added++;
37637e8ea635SAtari911        }
37647e8ea635SAtari911
37657e8ea635SAtari911        $this->clearStatsCache();
3766da206178SAtari911        echo json_encode(['success' => true, 'message' => "Added $added new occurrences"]);
37677e8ea635SAtari911    }
37687e8ea635SAtari911
37697e8ea635SAtari911    /**
37707e8ea635SAtari911     * Trim series: remove past occurrences before a cutoff date
37717e8ea635SAtari911     */
37727e8ea635SAtari911    private function handleTrimRecurring() {
37737e8ea635SAtari911        global $INPUT;
37747e8ea635SAtari911        $title = $INPUT->str('title');
37757e8ea635SAtari911        $namespace = $INPUT->str('namespace');
37767e8ea635SAtari911        $cutoffDate = $INPUT->str('cutoff_date', date('Y-m-d'));
37777e8ea635SAtari911
37787e8ea635SAtari911        $events = $this->getRecurringSeriesEvents($title, $namespace);
37797e8ea635SAtari911        $removed = 0;
37807e8ea635SAtari911
37817e8ea635SAtari911        foreach ($events as $entry) {
37827e8ea635SAtari911            if ($entry['date'] < $cutoffDate) {
37837e8ea635SAtari911                // Remove this event from its file
3784815440faSAtari911                $data = CalendarFileHandler::readJson($entry['file']);
37857e8ea635SAtari911                if (!$data || !isset($data[$entry['date']])) continue;
37867e8ea635SAtari911
37877e8ea635SAtari911                // Find and remove by matching title
37887e8ea635SAtari911                foreach ($data[$entry['date']] as $k => $evt) {
37897e8ea635SAtari911                    if (strtolower(trim($evt['title'])) === strtolower(trim($title))) {
37907e8ea635SAtari911                        unset($data[$entry['date']][$k]);
37917e8ea635SAtari911                        $data[$entry['date']] = array_values($data[$entry['date']]);
37927e8ea635SAtari911                        $removed++;
37937e8ea635SAtari911                        break;
37947e8ea635SAtari911                    }
37957e8ea635SAtari911                }
37967e8ea635SAtari911
37977e8ea635SAtari911                // Clean up empty dates
37987e8ea635SAtari911                if (empty($data[$entry['date']])) unset($data[$entry['date']]);
37997e8ea635SAtari911
38007e8ea635SAtari911                if (empty($data)) {
38017e8ea635SAtari911                    unlink($entry['file']);
38027e8ea635SAtari911                } else {
3803815440faSAtari911                    CalendarFileHandler::writeJson($entry['file'], $data);
38047e8ea635SAtari911                }
38057e8ea635SAtari911            }
38067e8ea635SAtari911        }
38077e8ea635SAtari911
38087e8ea635SAtari911        $this->clearStatsCache();
3809da206178SAtari911        echo json_encode(['success' => true, 'message' => "Removed $removed past occurrences before $cutoffDate"]);
38107e8ea635SAtari911    }
38117e8ea635SAtari911
38127e8ea635SAtari911    /**
38137e8ea635SAtari911     * Pause series: mark all future occurrences as paused
38147e8ea635SAtari911     */
38157e8ea635SAtari911    private function handlePauseRecurring() {
38167e8ea635SAtari911        global $INPUT;
38177e8ea635SAtari911        $title = $INPUT->str('title');
38187e8ea635SAtari911        $namespace = $INPUT->str('namespace');
38197e8ea635SAtari911        $today = date('Y-m-d');
38207e8ea635SAtari911
38217e8ea635SAtari911        $events = $this->getRecurringSeriesEvents($title, $namespace);
38227e8ea635SAtari911        $paused = 0;
38237e8ea635SAtari911
38247e8ea635SAtari911        foreach ($events as $entry) {
38257e8ea635SAtari911            if ($entry['date'] >= $today) {
3826815440faSAtari911                $data = CalendarFileHandler::readJson($entry['file']);
38277e8ea635SAtari911                if (!$data || !isset($data[$entry['date']])) continue;
38287e8ea635SAtari911
38297e8ea635SAtari911                foreach ($data[$entry['date']] as $k => &$evt) {
38307e8ea635SAtari911                    if (strtolower(trim($evt['title'])) === strtolower(trim($title))) {
38317e8ea635SAtari911                        $evt['paused'] = true;
38327e8ea635SAtari911                        $evt['title'] = '⏸ ' . preg_replace('/^⏸\s*/', '', $evt['title']);
38337e8ea635SAtari911                        $paused++;
38347e8ea635SAtari911                        break;
38357e8ea635SAtari911                    }
38367e8ea635SAtari911                }
38377e8ea635SAtari911                unset($evt);
38387e8ea635SAtari911
3839815440faSAtari911                CalendarFileHandler::writeJson($entry['file'], $data);
38407e8ea635SAtari911            }
38417e8ea635SAtari911        }
38427e8ea635SAtari911
38437e8ea635SAtari911        $this->clearStatsCache();
3844da206178SAtari911        echo json_encode(['success' => true, 'message' => "Paused $paused future occurrences"]);
38457e8ea635SAtari911    }
38467e8ea635SAtari911
38477e8ea635SAtari911    /**
38487e8ea635SAtari911     * Resume series: unmark paused occurrences
38497e8ea635SAtari911     */
38507e8ea635SAtari911    private function handleResumeRecurring() {
38517e8ea635SAtari911        global $INPUT;
38527e8ea635SAtari911        $title = $INPUT->str('title');
38537e8ea635SAtari911        $namespace = $INPUT->str('namespace');
38547e8ea635SAtari911
38557e8ea635SAtari911        // Search for both paused and non-paused versions
3856*2866e827SAtari911        $dataDir = $this->metaDir();
38577e8ea635SAtari911        if ($namespace !== '') {
38587e8ea635SAtari911            $dataDir .= str_replace(':', '/', $namespace) . '/';
38597e8ea635SAtari911        }
38607e8ea635SAtari911        $dataDir .= 'calendar/';
38617e8ea635SAtari911
38627e8ea635SAtari911        $resumed = 0;
38637e8ea635SAtari911        $cleanTitle = preg_replace('/^⏸\s*/', '', $title);
38647e8ea635SAtari911
38657e8ea635SAtari911        if (!is_dir($dataDir)) {
38667e8ea635SAtari911            echo json_encode(['success' => false, 'error' => 'Directory not found']);
38677e8ea635SAtari911            return;
38687e8ea635SAtari911        }
38697e8ea635SAtari911
38707e8ea635SAtari911        foreach (glob($dataDir . '*.json') as $file) {
3871815440faSAtari911            $data = CalendarFileHandler::readJson($file);
38727e8ea635SAtari911            if (!$data) continue;
38737e8ea635SAtari911
38747e8ea635SAtari911            $modified = false;
38757e8ea635SAtari911            foreach ($data as $dateKey => &$dayEvents) {
387696df7d3eSAtari911                // Skip non-date keys (like "mapping" or other metadata)
387796df7d3eSAtari911                if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $dateKey)) continue;
387896df7d3eSAtari911                if (!is_array($dayEvents)) continue;
387996df7d3eSAtari911
38807e8ea635SAtari911                foreach ($dayEvents as $k => &$evt) {
388196df7d3eSAtari911                    if (!isset($evt['title'])) continue;
38827e8ea635SAtari911                    $evtCleanTitle = preg_replace('/^⏸\s*/', '', $evt['title']);
38837e8ea635SAtari911                    if (strtolower(trim($evtCleanTitle)) === strtolower(trim($cleanTitle)) &&
38847e8ea635SAtari911                        (!empty($evt['paused']) || strpos($evt['title'], '⏸') === 0)) {
38857e8ea635SAtari911                        $evt['paused'] = false;
38867e8ea635SAtari911                        $evt['title'] = $cleanTitle;
38877e8ea635SAtari911                        $resumed++;
38887e8ea635SAtari911                        $modified = true;
38897e8ea635SAtari911                    }
38907e8ea635SAtari911                }
38917e8ea635SAtari911                unset($evt);
38927e8ea635SAtari911            }
38937e8ea635SAtari911            unset($dayEvents);
38947e8ea635SAtari911
38957e8ea635SAtari911            if ($modified) {
3896815440faSAtari911                CalendarFileHandler::writeJson($file, $data);
38977e8ea635SAtari911            }
38987e8ea635SAtari911        }
38997e8ea635SAtari911
39007e8ea635SAtari911        $this->clearStatsCache();
3901da206178SAtari911        echo json_encode(['success' => true, 'message' => "Resumed $resumed occurrences"]);
39027e8ea635SAtari911    }
39037e8ea635SAtari911
39047e8ea635SAtari911    /**
39057e8ea635SAtari911     * Change start date: shift all occurrences by an offset
39067e8ea635SAtari911     */
39077e8ea635SAtari911    private function handleChangeStartRecurring() {
39087e8ea635SAtari911        global $INPUT;
39097e8ea635SAtari911        $title = $INPUT->str('title');
39107e8ea635SAtari911        $namespace = $INPUT->str('namespace');
39117e8ea635SAtari911        $newStartDate = $INPUT->str('new_start_date');
39127e8ea635SAtari911
39137e8ea635SAtari911        if (empty($newStartDate)) {
39147e8ea635SAtari911            echo json_encode(['success' => false, 'error' => 'No start date provided']);
39157e8ea635SAtari911            return;
39167e8ea635SAtari911        }
39177e8ea635SAtari911
39187e8ea635SAtari911        $events = $this->getRecurringSeriesEvents($title, $namespace);
39197e8ea635SAtari911        if (empty($events)) {
39207e8ea635SAtari911            echo json_encode(['success' => false, 'error' => 'Series not found']);
39217e8ea635SAtari911            return;
39227e8ea635SAtari911        }
39237e8ea635SAtari911
39247e8ea635SAtari911        // Calculate offset from old first date to new first date
39257e8ea635SAtari911        $oldFirst = new DateTime($events[0]['date']);
39267e8ea635SAtari911        $newFirst = new DateTime($newStartDate);
39277e8ea635SAtari911        $offsetDays = (int)$oldFirst->diff($newFirst)->format('%r%a');
39287e8ea635SAtari911
39297e8ea635SAtari911        if ($offsetDays === 0) {
3930da206178SAtari911            echo json_encode(['success' => true, 'message' => 'Start date unchanged']);
39317e8ea635SAtari911            return;
39327e8ea635SAtari911        }
39337e8ea635SAtari911
3934*2866e827SAtari911        $dataDir = $this->metaDir();
39357e8ea635SAtari911        if ($namespace !== '') {
39367e8ea635SAtari911            $dataDir .= str_replace(':', '/', $namespace) . '/';
39377e8ea635SAtari911        }
39387e8ea635SAtari911        $dataDir .= 'calendar/';
39397e8ea635SAtari911
39407e8ea635SAtari911        // Collect all events to move
39417e8ea635SAtari911        $toMove = [];
39427e8ea635SAtari911        foreach ($events as $entry) {
39437e8ea635SAtari911            $oldDate = new DateTime($entry['date']);
39447e8ea635SAtari911            $newDate = clone $oldDate;
39457e8ea635SAtari911            $newDate->modify(($offsetDays > 0 ? '+' : '') . $offsetDays . ' days');
39467e8ea635SAtari911
39477e8ea635SAtari911            $toMove[] = [
39487e8ea635SAtari911                'oldDate' => $entry['date'],
39497e8ea635SAtari911                'newDate' => $newDate->format('Y-m-d'),
39507e8ea635SAtari911                'event' => $entry['event'],
39517e8ea635SAtari911                'file' => $entry['file']
39527e8ea635SAtari911            ];
39537e8ea635SAtari911        }
39547e8ea635SAtari911
39557e8ea635SAtari911        // Remove all from old positions
39567e8ea635SAtari911        foreach ($toMove as $move) {
3957815440faSAtari911            $data = CalendarFileHandler::readJson($move['file']);
39587e8ea635SAtari911            if (!$data || !isset($data[$move['oldDate']])) continue;
39597e8ea635SAtari911
39607e8ea635SAtari911            foreach ($data[$move['oldDate']] as $k => $evt) {
39617e8ea635SAtari911                if (strtolower(trim($evt['title'])) === strtolower(trim($title))) {
39627e8ea635SAtari911                    unset($data[$move['oldDate']][$k]);
39637e8ea635SAtari911                    $data[$move['oldDate']] = array_values($data[$move['oldDate']]);
39647e8ea635SAtari911                    break;
39657e8ea635SAtari911                }
39667e8ea635SAtari911            }
39677e8ea635SAtari911            if (empty($data[$move['oldDate']])) unset($data[$move['oldDate']]);
39687e8ea635SAtari911            if (empty($data)) {
39697e8ea635SAtari911                unlink($move['file']);
39707e8ea635SAtari911            } else {
3971815440faSAtari911                CalendarFileHandler::writeJson($move['file'], $data);
39727e8ea635SAtari911            }
39737e8ea635SAtari911        }
39747e8ea635SAtari911
39757e8ea635SAtari911        // Add to new positions
39767e8ea635SAtari911        $moved = 0;
39777e8ea635SAtari911        foreach ($toMove as $move) {
39787e8ea635SAtari911            list($year, $month) = explode('-', $move['newDate']);
39797e8ea635SAtari911            $file = $dataDir . sprintf('%04d-%02d.json', $year, $month);
3980815440faSAtari911            $data = file_exists($file) ? CalendarFileHandler::readJson($file) : [];
39817e8ea635SAtari911            if (!is_array($data)) $data = [];
39827e8ea635SAtari911
39837e8ea635SAtari911            if (!isset($data[$move['newDate']])) $data[$move['newDate']] = [];
39847e8ea635SAtari911            $data[$move['newDate']][] = $move['event'];
3985815440faSAtari911            CalendarFileHandler::writeJson($file, $data);
39867e8ea635SAtari911            $moved++;
39877e8ea635SAtari911        }
39887e8ea635SAtari911
3989da206178SAtari911        $dir = $offsetDays > 0 ? 'forward' : 'back';
39907e8ea635SAtari911        $this->clearStatsCache();
3991da206178SAtari911        echo json_encode(['success' => true, 'message' => "Shifted $moved occurrences $dir by " . abs($offsetDays) . " days"]);
39927e8ea635SAtari911    }
39937e8ea635SAtari911
39947e8ea635SAtari911    /**
39957e8ea635SAtari911     * Change pattern: re-space all future events with a new interval
39967e8ea635SAtari911     */
39977e8ea635SAtari911    private function handleChangePatternRecurring() {
39987e8ea635SAtari911        global $INPUT;
39997e8ea635SAtari911        $title = $INPUT->str('title');
40007e8ea635SAtari911        $namespace = $INPUT->str('namespace');
40017e8ea635SAtari911        $newIntervalDays = $INPUT->int('interval_days', 7);
40027e8ea635SAtari911
40037e8ea635SAtari911        $events = $this->getRecurringSeriesEvents($title, $namespace);
40047e8ea635SAtari911        $today = date('Y-m-d');
40057e8ea635SAtari911
40067e8ea635SAtari911        // Split into past and future
40077e8ea635SAtari911        $pastEvents = [];
40087e8ea635SAtari911        $futureEvents = [];
40097e8ea635SAtari911        foreach ($events as $e) {
40107e8ea635SAtari911            if ($e['date'] < $today) {
40117e8ea635SAtari911                $pastEvents[] = $e;
40127e8ea635SAtari911            } else {
40137e8ea635SAtari911                $futureEvents[] = $e;
40147e8ea635SAtari911            }
40157e8ea635SAtari911        }
40167e8ea635SAtari911
40177e8ea635SAtari911        if (empty($futureEvents)) {
4018da206178SAtari911            echo json_encode(['success' => false, 'error' => 'No future occurrences to respace']);
40197e8ea635SAtari911            return;
40207e8ea635SAtari911        }
40217e8ea635SAtari911
4022*2866e827SAtari911        $dataDir = $this->metaDir();
40237e8ea635SAtari911        if ($namespace !== '') {
40247e8ea635SAtari911            $dataDir .= str_replace(':', '/', $namespace) . '/';
40257e8ea635SAtari911        }
40267e8ea635SAtari911        $dataDir .= 'calendar/';
40277e8ea635SAtari911
40287e8ea635SAtari911        // Use first future event as anchor
40297e8ea635SAtari911        $anchorDate = new DateTime($futureEvents[0]['date']);
40307e8ea635SAtari911
40317e8ea635SAtari911        // Remove all future events from files
40327e8ea635SAtari911        foreach ($futureEvents as $entry) {
4033815440faSAtari911            $data = CalendarFileHandler::readJson($entry['file']);
40347e8ea635SAtari911            if (!$data || !isset($data[$entry['date']])) continue;
40357e8ea635SAtari911
40367e8ea635SAtari911            foreach ($data[$entry['date']] as $k => $evt) {
40377e8ea635SAtari911                if (strtolower(trim($evt['title'])) === strtolower(trim($title))) {
40387e8ea635SAtari911                    unset($data[$entry['date']][$k]);
40397e8ea635SAtari911                    $data[$entry['date']] = array_values($data[$entry['date']]);
40407e8ea635SAtari911                    break;
40417e8ea635SAtari911                }
40427e8ea635SAtari911            }
40437e8ea635SAtari911            if (empty($data[$entry['date']])) unset($data[$entry['date']]);
40447e8ea635SAtari911            if (empty($data)) {
40457e8ea635SAtari911                unlink($entry['file']);
40467e8ea635SAtari911            } else {
4047815440faSAtari911                CalendarFileHandler::writeJson($entry['file'], $data);
40487e8ea635SAtari911            }
40497e8ea635SAtari911        }
40507e8ea635SAtari911
40517e8ea635SAtari911        // Re-create with new spacing
40527e8ea635SAtari911        $template = $futureEvents[0]['event'];
40537e8ea635SAtari911        $baseId = isset($template['recurringId']) ? $template['recurringId'] : md5($title . $namespace);
40547e8ea635SAtari911        $count = count($futureEvents);
40557e8ea635SAtari911        $created = 0;
40567e8ea635SAtari911
40577e8ea635SAtari911        for ($i = 0; $i < $count; $i++) {
40587e8ea635SAtari911            $newDate = clone $anchorDate;
40597e8ea635SAtari911            $newDate->modify('+' . ($i * $newIntervalDays) . ' days');
40607e8ea635SAtari911            $dateKey = $newDate->format('Y-m-d');
40617e8ea635SAtari911            list($year, $month) = explode('-', $dateKey);
40627e8ea635SAtari911
40637e8ea635SAtari911            $file = $dataDir . sprintf('%04d-%02d.json', $year, $month);
4064815440faSAtari911            $fileData = file_exists($file) ? CalendarFileHandler::readJson($file) : [];
40657e8ea635SAtari911            if (!is_array($fileData)) $fileData = [];
40667e8ea635SAtari911
40677e8ea635SAtari911            if (!isset($fileData[$dateKey])) $fileData[$dateKey] = [];
40687e8ea635SAtari911
40697e8ea635SAtari911            $newEvent = $template;
40707e8ea635SAtari911            $newEvent['id'] = $baseId . '-respace-' . $i;
40717e8ea635SAtari911            $newEvent['recurring'] = true;
40727e8ea635SAtari911            $newEvent['recurringId'] = $baseId;
40737e8ea635SAtari911
40747e8ea635SAtari911            $fileData[$dateKey][] = $newEvent;
4075815440faSAtari911            CalendarFileHandler::writeJson($file, $fileData);
40767e8ea635SAtari911            $created++;
40777e8ea635SAtari911        }
40787e8ea635SAtari911
40797e8ea635SAtari911        $this->clearStatsCache();
40807e8ea635SAtari911        $patternName = $this->intervalToPattern($newIntervalDays);
4081da206178SAtari911        echo json_encode(['success' => true, 'message' => "Respaced $created future occurrences to $patternName ($newIntervalDays days)"]);
40827e8ea635SAtari911    }
40837e8ea635SAtari911
40847e8ea635SAtari911    private function intervalToPattern($days) {
4085da206178SAtari911        if ($days == 1) return 'Daily';
4086da206178SAtari911        if ($days == 7) return 'Weekly';
4087da206178SAtari911        if ($days == 14) return 'Bi-weekly';
4088da206178SAtari911        if ($days >= 28 && $days <= 31) return 'Monthly';
4089da206178SAtari911        if ($days >= 89 && $days <= 93) return 'Quarterly';
4090da206178SAtari911        if ($days >= 363 && $days <= 368) return 'Yearly';
4091da206178SAtari911        return "Every $days days";
40921d05cddcSAtari911    }
40931d05cddcSAtari911
40941d05cddcSAtari911    private function getEventsByNamespace() {
4095*2866e827SAtari911        $dataDir = $this->metaDir();
40961d05cddcSAtari911        $result = [];
40971d05cddcSAtari911
40981d05cddcSAtari911        // Check root calendar directory first (blank/default namespace)
40991d05cddcSAtari911        $rootCalendarDir = $dataDir . 'calendar';
41001d05cddcSAtari911        if (is_dir($rootCalendarDir)) {
41011d05cddcSAtari911            $hasFiles = false;
41021d05cddcSAtari911            $events = [];
41031d05cddcSAtari911
41041d05cddcSAtari911            foreach (glob($rootCalendarDir . '/*.json') as $file) {
41051d05cddcSAtari911                $hasFiles = true;
41061d05cddcSAtari911                $month = basename($file, '.json');
4107815440faSAtari911                $data = CalendarFileHandler::readJson($file);
41081d05cddcSAtari911                if (!$data) continue;
41091d05cddcSAtari911
41101d05cddcSAtari911                foreach ($data as $dateKey => $eventList) {
411196df7d3eSAtari911                    // Skip non-date keys (like "mapping" or other metadata)
411296df7d3eSAtari911                    // Date keys should be in YYYY-MM-DD format
411396df7d3eSAtari911                    if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $dateKey)) continue;
411496df7d3eSAtari911
411596df7d3eSAtari911                    // Skip if eventList is not an array (corrupted data)
411696df7d3eSAtari911                    if (!is_array($eventList)) continue;
411796df7d3eSAtari911
41181d05cddcSAtari911                    foreach ($eventList as $event) {
411996df7d3eSAtari911                        // Skip if event is not an array
412096df7d3eSAtari911                        if (!is_array($event)) continue;
412196df7d3eSAtari911
412296df7d3eSAtari911                        // Skip if event doesn't have required fields
412396df7d3eSAtari911                        if (empty($event['id']) || empty($event['title'])) continue;
412496df7d3eSAtari911
41251d05cddcSAtari911                        $events[] = [
41261d05cddcSAtari911                            'id' => $event['id'],
41271d05cddcSAtari911                            'title' => $event['title'],
41281d05cddcSAtari911                            'date' => $dateKey,
41291d05cddcSAtari911                            'startTime' => $event['startTime'] ?? null,
41301d05cddcSAtari911                            'month' => $month
41311d05cddcSAtari911                        ];
41321d05cddcSAtari911                    }
41331d05cddcSAtari911                }
41341d05cddcSAtari911            }
41351d05cddcSAtari911
41361d05cddcSAtari911            // Add if it has JSON files (even if empty)
41371d05cddcSAtari911            if ($hasFiles) {
41381d05cddcSAtari911                $result[''] = ['events' => $events];
41391d05cddcSAtari911            }
41401d05cddcSAtari911        }
41411d05cddcSAtari911
41421d05cddcSAtari911        // Recursively scan all namespace directories including sub-namespaces
41431d05cddcSAtari911        $this->scanNamespaceRecursive($dataDir, '', $result);
41441d05cddcSAtari911
41451d05cddcSAtari911        // Sort namespaces, but keep '' (default) first
41461d05cddcSAtari911        uksort($result, function($a, $b) {
41471d05cddcSAtari911            if ($a === '') return -1;
41481d05cddcSAtari911            if ($b === '') return 1;
41491d05cddcSAtari911            return strcmp($a, $b);
41501d05cddcSAtari911        });
41511d05cddcSAtari911
41521d05cddcSAtari911        return $result;
41531d05cddcSAtari911    }
41541d05cddcSAtari911
41551d05cddcSAtari911    private function scanNamespaceRecursive($baseDir, $parentNamespace, &$result) {
41561d05cddcSAtari911        foreach (glob($baseDir . '*', GLOB_ONLYDIR) as $nsDir) {
41571d05cddcSAtari911            $dirName = basename($nsDir);
41581d05cddcSAtari911
41591d05cddcSAtari911            // Skip the root 'calendar' dir
41601d05cddcSAtari911            if ($dirName === 'calendar' && empty($parentNamespace)) continue;
41611d05cddcSAtari911
41621d05cddcSAtari911            // Build namespace path
41631d05cddcSAtari911            $namespace = empty($parentNamespace) ? $dirName : $parentNamespace . ':' . $dirName;
41641d05cddcSAtari911
41651d05cddcSAtari911            // Check for calendar directory
41661d05cddcSAtari911            $calendarDir = $nsDir . '/calendar';
41671d05cddcSAtari911            if (is_dir($calendarDir)) {
41681d05cddcSAtari911                $hasFiles = false;
41691d05cddcSAtari911                $events = [];
41701d05cddcSAtari911
41711d05cddcSAtari911                // Scan all calendar files
41721d05cddcSAtari911                foreach (glob($calendarDir . '/*.json') as $file) {
41731d05cddcSAtari911                    $hasFiles = true;
41741d05cddcSAtari911                    $month = basename($file, '.json');
4175815440faSAtari911                    $data = CalendarFileHandler::readJson($file);
41761d05cddcSAtari911                    if (!$data) continue;
41771d05cddcSAtari911
41781d05cddcSAtari911                    foreach ($data as $dateKey => $eventList) {
417996df7d3eSAtari911                        // Skip non-date keys (like "mapping" or other metadata)
418096df7d3eSAtari911                        // Date keys should be in YYYY-MM-DD format
418196df7d3eSAtari911                        if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $dateKey)) continue;
418296df7d3eSAtari911
418396df7d3eSAtari911                        // Skip if eventList is not an array (corrupted data)
418496df7d3eSAtari911                        if (!is_array($eventList)) continue;
418596df7d3eSAtari911
41861d05cddcSAtari911                        foreach ($eventList as $event) {
418796df7d3eSAtari911                            // Skip if event is not an array
418896df7d3eSAtari911                            if (!is_array($event)) continue;
418996df7d3eSAtari911
419096df7d3eSAtari911                            // Skip if event doesn't have required fields
419196df7d3eSAtari911                            if (empty($event['id']) || empty($event['title'])) continue;
419296df7d3eSAtari911
41931d05cddcSAtari911                            $events[] = [
41941d05cddcSAtari911                                'id' => $event['id'],
41951d05cddcSAtari911                                'title' => $event['title'],
41961d05cddcSAtari911                                'date' => $dateKey,
41971d05cddcSAtari911                                'startTime' => $event['startTime'] ?? null,
41981d05cddcSAtari911                                'month' => $month
41991d05cddcSAtari911                            ];
42001d05cddcSAtari911                        }
42011d05cddcSAtari911                    }
42021d05cddcSAtari911                }
42031d05cddcSAtari911
42041d05cddcSAtari911                // Add namespace if it has JSON files (even if empty)
42051d05cddcSAtari911                if ($hasFiles) {
42061d05cddcSAtari911                    $result[$namespace] = ['events' => $events];
42071d05cddcSAtari911                }
42081d05cddcSAtari911            }
42091d05cddcSAtari911
42101d05cddcSAtari911            // Recursively scan sub-directories
42111d05cddcSAtari911            $this->scanNamespaceRecursive($nsDir . '/', $namespace, $result);
42121d05cddcSAtari911        }
42131d05cddcSAtari911    }
42141d05cddcSAtari911
42151d05cddcSAtari911    private function getAllNamespaces() {
4216*2866e827SAtari911        $dataDir = $this->metaDir();
42171d05cddcSAtari911        $namespaces = [];
42181d05cddcSAtari911
42191d05cddcSAtari911        // Check root calendar directory first
42201d05cddcSAtari911        $rootCalendarDir = $dataDir . 'calendar';
42211d05cddcSAtari911        if (is_dir($rootCalendarDir)) {
42221d05cddcSAtari911            $namespaces[] = '';  // Blank/default namespace
42231d05cddcSAtari911        }
42241d05cddcSAtari911
42251d05cddcSAtari911        // Check all other namespace directories
42261d05cddcSAtari911        foreach (glob($dataDir . '*', GLOB_ONLYDIR) as $nsDir) {
42271d05cddcSAtari911            $namespace = basename($nsDir);
42281d05cddcSAtari911
42291d05cddcSAtari911            // Skip the root 'calendar' dir (already added as '')
42301d05cddcSAtari911            if ($namespace === 'calendar') continue;
42311d05cddcSAtari911
42321d05cddcSAtari911            $calendarDir = $nsDir . '/calendar';
42331d05cddcSAtari911            if (is_dir($calendarDir)) {
42341d05cddcSAtari911                $namespaces[] = $namespace;
42351d05cddcSAtari911            }
42361d05cddcSAtari911        }
42371d05cddcSAtari911
42381d05cddcSAtari911        return $namespaces;
42391d05cddcSAtari911    }
42401d05cddcSAtari911
42411d05cddcSAtari911    private function searchEvents($search, $filterNamespace) {
4242*2866e827SAtari911        $dataDir = $this->metaDir();
42431d05cddcSAtari911        $results = [];
42441d05cddcSAtari911
42451d05cddcSAtari911        $search = strtolower(trim($search));
42461d05cddcSAtari911
42471d05cddcSAtari911        foreach (glob($dataDir . '*', GLOB_ONLYDIR) as $nsDir) {
42481d05cddcSAtari911            $namespace = basename($nsDir);
42491d05cddcSAtari911            $calendarDir = $nsDir . '/calendar';
42501d05cddcSAtari911
42511d05cddcSAtari911            if (!is_dir($calendarDir)) continue;
42521d05cddcSAtari911            if ($filterNamespace !== '' && $namespace !== $filterNamespace) continue;
42531d05cddcSAtari911
42541d05cddcSAtari911            foreach (glob($calendarDir . '/*.json') as $file) {
42551d05cddcSAtari911                $month = basename($file, '.json');
4256815440faSAtari911                $data = CalendarFileHandler::readJson($file);
42571d05cddcSAtari911                if (!$data) continue;
42581d05cddcSAtari911
42591d05cddcSAtari911                foreach ($data as $dateKey => $events) {
426096df7d3eSAtari911                    // Skip non-date keys (like "mapping" or other metadata)
426196df7d3eSAtari911                    if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $dateKey)) continue;
426296df7d3eSAtari911                    if (!is_array($events)) continue;
426396df7d3eSAtari911
42641d05cddcSAtari911                    foreach ($events as $event) {
426596df7d3eSAtari911                        if (!isset($event['title']) || !isset($event['id'])) continue;
42661d05cddcSAtari911                        if ($search === '' || strpos(strtolower($event['title']), $search) !== false) {
42671d05cddcSAtari911                            $results[] = [
42681d05cddcSAtari911                                'id' => $event['id'],
42691d05cddcSAtari911                                'title' => $event['title'],
42701d05cddcSAtari911                                'date' => $dateKey,
42711d05cddcSAtari911                                'startTime' => $event['startTime'] ?? null,
42721d05cddcSAtari911                                'namespace' => $event['namespace'] ?? '',
42731d05cddcSAtari911                                'month' => $month
42741d05cddcSAtari911                            ];
42751d05cddcSAtari911                        }
42761d05cddcSAtari911                    }
42771d05cddcSAtari911                }
42781d05cddcSAtari911            }
42791d05cddcSAtari911        }
42801d05cddcSAtari911
42811d05cddcSAtari911        return $results;
42821d05cddcSAtari911    }
42831d05cddcSAtari911
42841d05cddcSAtari911    private function deleteRecurringSeries() {
42851d05cddcSAtari911        global $INPUT;
42861d05cddcSAtari911
42871d05cddcSAtari911        $eventTitle = $INPUT->str('event_title');
42881d05cddcSAtari911        $namespace = $INPUT->str('namespace');
42891d05cddcSAtari911
42907e8ea635SAtari911        // Collect ALL calendar directories
4291*2866e827SAtari911        $dataDir = $this->metaDir();
42927e8ea635SAtari911        $calendarDirs = [];
42937e8ea635SAtari911        if (is_dir($dataDir . 'calendar')) {
42947e8ea635SAtari911            $calendarDirs[] = $dataDir . 'calendar';
42951d05cddcSAtari911        }
42967e8ea635SAtari911        $this->findCalendarDirs($dataDir, $calendarDirs);
42971d05cddcSAtari911
42981d05cddcSAtari911        $count = 0;
42991d05cddcSAtari911
43007e8ea635SAtari911        foreach ($calendarDirs as $calDir) {
43017e8ea635SAtari911            foreach (glob($calDir . '/*.json') as $file) {
4302815440faSAtari911                $data = CalendarFileHandler::readJson($file);
43037e8ea635SAtari911                if (!$data || !is_array($data)) continue;
43041d05cddcSAtari911
43051d05cddcSAtari911                $modified = false;
43061d05cddcSAtari911                foreach ($data as $dateKey => $events) {
430796df7d3eSAtari911                    // Skip non-date keys (like "mapping" or other metadata)
430896df7d3eSAtari911                    if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $dateKey)) continue;
430996df7d3eSAtari911                    if (!is_array($events)) continue;
431096df7d3eSAtari911
43111d05cddcSAtari911                    $filtered = [];
43121d05cddcSAtari911                    foreach ($events as $event) {
431396df7d3eSAtari911                        if (!isset($event['title'])) {
431496df7d3eSAtari911                            $filtered[] = $event;
431596df7d3eSAtari911                            continue;
431696df7d3eSAtari911                        }
43177e8ea635SAtari911                        $eventNs = isset($event['namespace']) ? $event['namespace'] : '';
43187e8ea635SAtari911                        // Match by title AND namespace field
43197e8ea635SAtari911                        if (strtolower(trim($event['title'])) === strtolower(trim($eventTitle)) &&
43207e8ea635SAtari911                            strtolower(trim($eventNs)) === strtolower(trim($namespace))) {
43211d05cddcSAtari911                            $count++;
43221d05cddcSAtari911                            $modified = true;
43231d05cddcSAtari911                        } else {
43241d05cddcSAtari911                            $filtered[] = $event;
43251d05cddcSAtari911                        }
43261d05cddcSAtari911                    }
43271d05cddcSAtari911                    $data[$dateKey] = $filtered;
43281d05cddcSAtari911                }
43291d05cddcSAtari911
43301d05cddcSAtari911                if ($modified) {
43319ccd446eSAtari911                    foreach ($data as $dk => $evts) {
43329ccd446eSAtari911                        if (empty($evts)) unset($data[$dk]);
43339ccd446eSAtari911                    }
43349ccd446eSAtari911
43359ccd446eSAtari911                    if (empty($data)) {
43369ccd446eSAtari911                        unlink($file);
43379ccd446eSAtari911                    } else {
4338815440faSAtari911                        CalendarFileHandler::writeJson($file, $data);
43391d05cddcSAtari911                    }
43401d05cddcSAtari911                }
43411d05cddcSAtari911            }
43429ccd446eSAtari911        }
43431d05cddcSAtari911
43449ccd446eSAtari911        $this->clearStatsCache();
4345da206178SAtari911        $this->redirect("Deleted $count occurrences of recurring event: " . $eventTitle, 'success', 'manage');
43461d05cddcSAtari911    }
43471d05cddcSAtari911
43481d05cddcSAtari911    private function editRecurringSeries() {
43491d05cddcSAtari911        global $INPUT;
43501d05cddcSAtari911
43511d05cddcSAtari911        $oldTitle = $INPUT->str('old_title');
43521d05cddcSAtari911        $oldNamespace = $INPUT->str('old_namespace');
43531d05cddcSAtari911        $newTitle = $INPUT->str('new_title');
43541d05cddcSAtari911        $startTime = $INPUT->str('start_time');
43551d05cddcSAtari911        $endTime = $INPUT->str('end_time');
43561d05cddcSAtari911        $newNamespace = $INPUT->str('new_namespace');
43571d05cddcSAtari911
435896df7d3eSAtari911        // New recurrence parameters
435996df7d3eSAtari911        $recurrenceType = $INPUT->str('recurrence_type', '');
436096df7d3eSAtari911        $recurrenceInterval = $INPUT->int('recurrence_interval', 0);
436196df7d3eSAtari911        $weekDaysStr = $INPUT->str('week_days', '');
436296df7d3eSAtari911        $weekDays = $weekDaysStr ? array_map('intval', explode(',', $weekDaysStr)) : [];
436396df7d3eSAtari911        $monthlyType = $INPUT->str('monthly_type', '');
436496df7d3eSAtari911        $monthDay = $INPUT->int('month_day', 0);
436596df7d3eSAtari911        $ordinalWeek = $INPUT->int('ordinal_week', 0);
436696df7d3eSAtari911        $ordinalDay = $INPUT->int('ordinal_day', 0);
436796df7d3eSAtari911
43681d05cddcSAtari911        // Use old namespace if new namespace is empty (keep current)
43691d05cddcSAtari911        if (empty($newNamespace) && !isset($_POST['new_namespace'])) {
43701d05cddcSAtari911            $newNamespace = $oldNamespace;
43711d05cddcSAtari911        }
43721d05cddcSAtari911
43737e8ea635SAtari911        // Collect ALL calendar directories to search
4374*2866e827SAtari911        $dataDir = $this->metaDir();
43757e8ea635SAtari911        $calendarDirs = [];
43767e8ea635SAtari911
43777e8ea635SAtari911        // Root calendar dir
43787e8ea635SAtari911        if (is_dir($dataDir . 'calendar')) {
43797e8ea635SAtari911            $calendarDirs[] = $dataDir . 'calendar';
43801d05cddcSAtari911        }
43811d05cddcSAtari911
43827e8ea635SAtari911        // All namespace dirs
43837e8ea635SAtari911        $this->findCalendarDirs($dataDir, $calendarDirs);
43841d05cddcSAtari911
43857e8ea635SAtari911        $count = 0;
43867e8ea635SAtari911
438796df7d3eSAtari911        // Pass 1: Rename title, update time, update namespace field and recurrence metadata in ALL matching events
43887e8ea635SAtari911        foreach ($calendarDirs as $calDir) {
43897e8ea635SAtari911            if (is_string($calDir)) {
43907e8ea635SAtari911                $dir = $calDir;
43917e8ea635SAtari911            } else {
43927e8ea635SAtari911                $dir = $calDir['dir'];
43937e8ea635SAtari911            }
43947e8ea635SAtari911
43957e8ea635SAtari911            foreach (glob($dir . '/*.json') as $file) {
4396815440faSAtari911                $data = CalendarFileHandler::readJson($file);
43977e8ea635SAtari911                if (!$data || !is_array($data)) continue;
43981d05cddcSAtari911
43991d05cddcSAtari911                $modified = false;
44007e8ea635SAtari911                foreach ($data as $dateKey => &$dayEvents) {
440196df7d3eSAtari911                    // Skip non-date keys (like "mapping" or other metadata)
440296df7d3eSAtari911                    if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $dateKey)) continue;
44037e8ea635SAtari911                    if (!is_array($dayEvents)) continue;
440496df7d3eSAtari911
44057e8ea635SAtari911                    foreach ($dayEvents as $key => &$event) {
440696df7d3eSAtari911                        if (!isset($event['title'])) continue;
44077e8ea635SAtari911                        // Match by old title (case-insensitive) AND namespace field
44087e8ea635SAtari911                        $eventNs = isset($event['namespace']) ? $event['namespace'] : '';
44097e8ea635SAtari911                        if (strtolower(trim($event['title'])) !== strtolower(trim($oldTitle))) continue;
44107e8ea635SAtari911                        if (strtolower(trim($eventNs)) !== strtolower(trim($oldNamespace))) continue;
44117e8ea635SAtari911
44127e8ea635SAtari911                        // Update title
44137e8ea635SAtari911                        $event['title'] = $newTitle;
44141d05cddcSAtari911
44151d05cddcSAtari911                        // Update start time if provided
44161d05cddcSAtari911                        if (!empty($startTime)) {
44177e8ea635SAtari911                            $event['time'] = $startTime;
44181d05cddcSAtari911                        }
44191d05cddcSAtari911
44201d05cddcSAtari911                        // Update end time if provided
44211d05cddcSAtari911                        if (!empty($endTime)) {
44227e8ea635SAtari911                            $event['endTime'] = $endTime;
44231d05cddcSAtari911                        }
44241d05cddcSAtari911
44257e8ea635SAtari911                        // Update namespace field
44267e8ea635SAtari911                        $event['namespace'] = $newNamespace;
44271d05cddcSAtari911
442896df7d3eSAtari911                        // Update recurrence metadata if provided
442996df7d3eSAtari911                        if (!empty($recurrenceType)) {
443096df7d3eSAtari911                            $event['recurrenceType'] = $recurrenceType;
443196df7d3eSAtari911                        }
443296df7d3eSAtari911                        if ($recurrenceInterval > 0) {
443396df7d3eSAtari911                            $event['recurrenceInterval'] = $recurrenceInterval;
443496df7d3eSAtari911                        }
443596df7d3eSAtari911                        if (!empty($weekDays)) {
443696df7d3eSAtari911                            $event['weekDays'] = $weekDays;
443796df7d3eSAtari911                        }
443896df7d3eSAtari911                        if (!empty($monthlyType)) {
443996df7d3eSAtari911                            $event['monthlyType'] = $monthlyType;
444096df7d3eSAtari911                            if ($monthlyType === 'dayOfMonth' && $monthDay > 0) {
444196df7d3eSAtari911                                $event['monthDay'] = $monthDay;
444296df7d3eSAtari911                                unset($event['ordinalWeek']);
444396df7d3eSAtari911                                unset($event['ordinalDay']);
444496df7d3eSAtari911                            } elseif ($monthlyType === 'ordinalWeekday') {
444596df7d3eSAtari911                                $event['ordinalWeek'] = $ordinalWeek;
444696df7d3eSAtari911                                $event['ordinalDay'] = $ordinalDay;
444796df7d3eSAtari911                                unset($event['monthDay']);
444896df7d3eSAtari911                            }
444996df7d3eSAtari911                        }
445096df7d3eSAtari911
44511d05cddcSAtari911                        $count++;
44521d05cddcSAtari911                        $modified = true;
44531d05cddcSAtari911                    }
44547e8ea635SAtari911                    unset($event);
44551d05cddcSAtari911                }
44567e8ea635SAtari911                unset($dayEvents);
44571d05cddcSAtari911
44581d05cddcSAtari911                if ($modified) {
4459815440faSAtari911                    CalendarFileHandler::writeJson($file, $data);
44601d05cddcSAtari911                }
44611d05cddcSAtari911            }
44627e8ea635SAtari911        }
44631d05cddcSAtari911
446496df7d3eSAtari911        // Pass 2: Handle recurrence pattern changes - reschedule future events
446596df7d3eSAtari911        $needsReschedule = !empty($recurrenceType) && $recurrenceInterval > 0;
446696df7d3eSAtari911
446796df7d3eSAtari911        if ($needsReschedule && $count > 0) {
446896df7d3eSAtari911            // Get all events with the NEW title
44697e8ea635SAtari911            $allEvents = $this->getRecurringSeriesEvents($newTitle, $newNamespace);
44701d05cddcSAtari911
44717e8ea635SAtari911            if (count($allEvents) > 1) {
447296df7d3eSAtari911                // Sort by date
447396df7d3eSAtari911                usort($allEvents, function($a, $b) {
447496df7d3eSAtari911                    return strcmp($a['date'], $b['date']);
447596df7d3eSAtari911                });
44761d05cddcSAtari911
447796df7d3eSAtari911                $firstDate = new DateTime($allEvents[0]['date']);
447896df7d3eSAtari911                $today = new DateTime();
447996df7d3eSAtari911                $today->setTime(0, 0, 0);
448096df7d3eSAtari911
448196df7d3eSAtari911                // Find the anchor date - either first date or first future date
448296df7d3eSAtari911                $anchorDate = $firstDate;
448396df7d3eSAtari911                $anchorIndex = 0;
448496df7d3eSAtari911                for ($i = 0; $i < count($allEvents); $i++) {
448596df7d3eSAtari911                    $eventDate = new DateTime($allEvents[$i]['date']);
448696df7d3eSAtari911                    if ($eventDate >= $today) {
448796df7d3eSAtari911                        $anchorDate = $eventDate;
448896df7d3eSAtari911                        $anchorIndex = $i;
448996df7d3eSAtari911                        break;
449096df7d3eSAtari911                    }
449196df7d3eSAtari911                }
449296df7d3eSAtari911
449396df7d3eSAtari911                // Get template from anchor event
449496df7d3eSAtari911                $template = $allEvents[$anchorIndex]['event'];
449596df7d3eSAtari911
449696df7d3eSAtari911                // Remove all future events (we'll recreate them)
449796df7d3eSAtari911                for ($i = $anchorIndex + 1; $i < count($allEvents); $i++) {
44987e8ea635SAtari911                    $entry = $allEvents[$i];
4499815440faSAtari911                    $data = CalendarFileHandler::readJson($entry['file']);
45007e8ea635SAtari911                    if (!$data || !isset($data[$entry['date']])) continue;
45017e8ea635SAtari911
45027e8ea635SAtari911                    foreach ($data[$entry['date']] as $k => $evt) {
45037e8ea635SAtari911                        if (strtolower(trim($evt['title'])) === strtolower(trim($newTitle))) {
45047e8ea635SAtari911                            unset($data[$entry['date']][$k]);
45057e8ea635SAtari911                            $data[$entry['date']] = array_values($data[$entry['date']]);
45067e8ea635SAtari911                            break;
45071d05cddcSAtari911                        }
45081d05cddcSAtari911                    }
45097e8ea635SAtari911                    if (empty($data[$entry['date']])) unset($data[$entry['date']]);
45107e8ea635SAtari911                    if (empty($data)) {
45117e8ea635SAtari911                        unlink($entry['file']);
45127e8ea635SAtari911                    } else {
4513815440faSAtari911                        CalendarFileHandler::writeJson($entry['file'], $data);
45141d05cddcSAtari911                    }
45151d05cddcSAtari911                }
45161d05cddcSAtari911
451796df7d3eSAtari911                // Recreate with new pattern
45187e8ea635SAtari911                $targetDir = ($newNamespace === '')
4519*2866e827SAtari911                    ? $this->metaDir() . 'calendar'
4520*2866e827SAtari911                    : $this->metaDir() . str_replace(':', '/', $newNamespace) . '/calendar';
45217e8ea635SAtari911                if (!is_dir($targetDir)) mkdir($targetDir, 0755, true);
45221d05cddcSAtari911
45237e8ea635SAtari911                $baseId = isset($template['recurringId']) ? $template['recurringId'] : md5($newTitle . $newNamespace);
45241d05cddcSAtari911
452596df7d3eSAtari911                // Calculate how many future events we need (use same count as before)
452696df7d3eSAtari911                $futureCount = count($allEvents) - $anchorIndex - 1;
452796df7d3eSAtari911                if ($futureCount < 1) $futureCount = 12; // Default to 12 future occurrences
452896df7d3eSAtari911
452996df7d3eSAtari911                // Generate new dates based on recurrence pattern
453096df7d3eSAtari911                $newDates = $this->generateRecurrenceDates(
453196df7d3eSAtari911                    $anchorDate->format('Y-m-d'),
453296df7d3eSAtari911                    $recurrenceType,
453396df7d3eSAtari911                    $recurrenceInterval,
453496df7d3eSAtari911                    $weekDays,
453596df7d3eSAtari911                    $monthlyType,
453696df7d3eSAtari911                    $monthDay,
453796df7d3eSAtari911                    $ordinalWeek,
453896df7d3eSAtari911                    $ordinalDay,
453996df7d3eSAtari911                    $futureCount
454096df7d3eSAtari911                );
454196df7d3eSAtari911
454296df7d3eSAtari911                // Create events for new dates (skip first since it's the anchor)
454396df7d3eSAtari911                for ($i = 1; $i < count($newDates); $i++) {
454496df7d3eSAtari911                    $dateKey = $newDates[$i];
45457e8ea635SAtari911                    list($year, $month) = explode('-', $dateKey);
45461d05cddcSAtari911
45477e8ea635SAtari911                    $file = $targetDir . '/' . sprintf('%04d-%02d.json', $year, $month);
4548815440faSAtari911                    $fileData = file_exists($file) ? CalendarFileHandler::readJson($file) : [];
45497e8ea635SAtari911                    if (!is_array($fileData)) $fileData = [];
45507e8ea635SAtari911                    if (!isset($fileData[$dateKey])) $fileData[$dateKey] = [];
45511d05cddcSAtari911
45527e8ea635SAtari911                    $newEvent = $template;
45537e8ea635SAtari911                    $newEvent['id'] = $baseId . '-respace-' . $i;
455496df7d3eSAtari911                    $newEvent['recurrenceType'] = $recurrenceType;
455596df7d3eSAtari911                    $newEvent['recurrenceInterval'] = $recurrenceInterval;
455696df7d3eSAtari911                    if (!empty($weekDays)) $newEvent['weekDays'] = $weekDays;
455796df7d3eSAtari911                    if (!empty($monthlyType)) $newEvent['monthlyType'] = $monthlyType;
455896df7d3eSAtari911                    if ($monthlyType === 'dayOfMonth' && $monthDay > 0) $newEvent['monthDay'] = $monthDay;
455996df7d3eSAtari911                    if ($monthlyType === 'ordinalWeekday') {
456096df7d3eSAtari911                        $newEvent['ordinalWeek'] = $ordinalWeek;
456196df7d3eSAtari911                        $newEvent['ordinalDay'] = $ordinalDay;
456296df7d3eSAtari911                    }
456396df7d3eSAtari911
45647e8ea635SAtari911                    $fileData[$dateKey][] = $newEvent;
4565815440faSAtari911                    CalendarFileHandler::writeJson($file, $fileData);
45661d05cddcSAtari911                }
45671d05cddcSAtari911            }
45681d05cddcSAtari911        }
45691d05cddcSAtari911
45701d05cddcSAtari911        $changes = [];
45711d05cddcSAtari911        if ($oldTitle !== $newTitle) $changes[] = "title";
45721d05cddcSAtari911        if (!empty($startTime) || !empty($endTime)) $changes[] = "time";
457396df7d3eSAtari911        if (!empty($recurrenceType)) $changes[] = "pattern";
45741d05cddcSAtari911        if ($newNamespace !== $oldNamespace) $changes[] = "namespace";
45751d05cddcSAtari911
45761d05cddcSAtari911        $changeStr = !empty($changes) ? " (" . implode(", ", $changes) . ")" : "";
45779ccd446eSAtari911        $this->clearStatsCache();
4578da206178SAtari911        $this->redirect("Updated $count occurrences of recurring event$changeStr", 'success', 'manage');
45791d05cddcSAtari911    }
45801d05cddcSAtari911
45817e8ea635SAtari911    /**
458296df7d3eSAtari911     * Generate dates for a recurrence pattern
458396df7d3eSAtari911     */
458496df7d3eSAtari911    private function generateRecurrenceDates($startDate, $type, $interval, $weekDays, $monthlyType, $monthDay, $ordinalWeek, $ordinalDay, $count) {
458596df7d3eSAtari911        $dates = [$startDate];
458696df7d3eSAtari911        $currentDate = new DateTime($startDate);
458796df7d3eSAtari911        $maxIterations = $count * 100; // Safety limit
458896df7d3eSAtari911        $iterations = 0;
458996df7d3eSAtari911
459096df7d3eSAtari911        while (count($dates) < $count + 1 && $iterations < $maxIterations) {
459196df7d3eSAtari911            $iterations++;
459296df7d3eSAtari911            $currentDate->modify('+1 day');
459396df7d3eSAtari911            $shouldInclude = false;
459496df7d3eSAtari911
459596df7d3eSAtari911            switch ($type) {
459696df7d3eSAtari911                case 'daily':
459796df7d3eSAtari911                    $daysSinceStart = (new DateTime($startDate))->diff($currentDate)->days;
459896df7d3eSAtari911                    $shouldInclude = ($daysSinceStart % $interval === 0);
459996df7d3eSAtari911                    break;
460096df7d3eSAtari911
460196df7d3eSAtari911                case 'weekly':
460296df7d3eSAtari911                    $daysSinceStart = (new DateTime($startDate))->diff($currentDate)->days;
460396df7d3eSAtari911                    $weeksSinceStart = floor($daysSinceStart / 7);
460496df7d3eSAtari911                    $isCorrectWeek = ($weeksSinceStart % $interval === 0);
460596df7d3eSAtari911                    $currentDayOfWeek = (int)$currentDate->format('w');
460696df7d3eSAtari911                    $isDaySelected = empty($weekDays) || in_array($currentDayOfWeek, $weekDays);
460796df7d3eSAtari911                    $shouldInclude = $isCorrectWeek && $isDaySelected;
460896df7d3eSAtari911                    break;
460996df7d3eSAtari911
461096df7d3eSAtari911                case 'monthly':
461196df7d3eSAtari911                    $startDT = new DateTime($startDate);
461296df7d3eSAtari911                    $monthsSinceStart = (($currentDate->format('Y') - $startDT->format('Y')) * 12) +
461396df7d3eSAtari911                                        ($currentDate->format('n') - $startDT->format('n'));
461496df7d3eSAtari911                    $isCorrectMonth = ($monthsSinceStart > 0 && $monthsSinceStart % $interval === 0);
461596df7d3eSAtari911
461696df7d3eSAtari911                    if (!$isCorrectMonth) break;
461796df7d3eSAtari911
461896df7d3eSAtari911                    if ($monthlyType === 'dayOfMonth' || empty($monthlyType)) {
461996df7d3eSAtari911                        $targetDay = $monthDay ?: (int)$startDT->format('j');
462096df7d3eSAtari911                        $currentDay = (int)$currentDate->format('j');
462196df7d3eSAtari911                        $daysInMonth = (int)$currentDate->format('t');
462296df7d3eSAtari911                        $effectiveTargetDay = min($targetDay, $daysInMonth);
462396df7d3eSAtari911                        $shouldInclude = ($currentDay === $effectiveTargetDay);
462496df7d3eSAtari911                    } else {
462596df7d3eSAtari911                        $shouldInclude = $this->isOrdinalWeekdayAdmin($currentDate, $ordinalWeek, $ordinalDay);
462696df7d3eSAtari911                    }
462796df7d3eSAtari911                    break;
462896df7d3eSAtari911
462996df7d3eSAtari911                case 'yearly':
463096df7d3eSAtari911                    $startDT = new DateTime($startDate);
463196df7d3eSAtari911                    $yearsSinceStart = (int)$currentDate->format('Y') - (int)$startDT->format('Y');
463296df7d3eSAtari911                    $isCorrectYear = ($yearsSinceStart > 0 && $yearsSinceStart % $interval === 0);
463396df7d3eSAtari911                    $sameMonthDay = ($currentDate->format('m-d') === $startDT->format('m-d'));
463496df7d3eSAtari911                    $shouldInclude = $isCorrectYear && $sameMonthDay;
463596df7d3eSAtari911                    break;
463696df7d3eSAtari911            }
463796df7d3eSAtari911
463896df7d3eSAtari911            if ($shouldInclude) {
463996df7d3eSAtari911                $dates[] = $currentDate->format('Y-m-d');
464096df7d3eSAtari911            }
464196df7d3eSAtari911        }
464296df7d3eSAtari911
464396df7d3eSAtari911        return $dates;
464496df7d3eSAtari911    }
464596df7d3eSAtari911
464696df7d3eSAtari911    /**
464796df7d3eSAtari911     * Check if a date is the Nth occurrence of a weekday in its month (admin version)
464896df7d3eSAtari911     */
464996df7d3eSAtari911    private function isOrdinalWeekdayAdmin($date, $ordinalWeek, $targetDayOfWeek) {
465096df7d3eSAtari911        $currentDayOfWeek = (int)$date->format('w');
465196df7d3eSAtari911        if ($currentDayOfWeek !== $targetDayOfWeek) return false;
465296df7d3eSAtari911
465396df7d3eSAtari911        $dayOfMonth = (int)$date->format('j');
465496df7d3eSAtari911        $daysInMonth = (int)$date->format('t');
465596df7d3eSAtari911
465696df7d3eSAtari911        if ($ordinalWeek === -1) {
465796df7d3eSAtari911            $daysRemaining = $daysInMonth - $dayOfMonth;
465896df7d3eSAtari911            return $daysRemaining < 7;
465996df7d3eSAtari911        } else {
466096df7d3eSAtari911            $weekNumber = ceil($dayOfMonth / 7);
466196df7d3eSAtari911            return $weekNumber === $ordinalWeek;
466296df7d3eSAtari911        }
466396df7d3eSAtari911    }
466496df7d3eSAtari911
466596df7d3eSAtari911    /**
46667e8ea635SAtari911     * Find all calendar directories recursively
46677e8ea635SAtari911     */
46687e8ea635SAtari911    private function findCalendarDirs($baseDir, &$dirs) {
46697e8ea635SAtari911        foreach (glob($baseDir . '*', GLOB_ONLYDIR) as $nsDir) {
46707e8ea635SAtari911            $name = basename($nsDir);
46717e8ea635SAtari911            if ($name === 'calendar') continue; // Skip root calendar (added separately)
46727e8ea635SAtari911
46737e8ea635SAtari911            $calDir = $nsDir . '/calendar';
46747e8ea635SAtari911            if (is_dir($calDir)) {
46757e8ea635SAtari911                $dirs[] = $calDir;
46767e8ea635SAtari911            }
46777e8ea635SAtari911
46787e8ea635SAtari911            // Recurse
46797e8ea635SAtari911            $this->findCalendarDirs($nsDir . '/', $dirs);
46807e8ea635SAtari911        }
46817e8ea635SAtari911    }
46827e8ea635SAtari911
46831d05cddcSAtari911    private function moveEvents() {
46841d05cddcSAtari911        global $INPUT;
46851d05cddcSAtari911
46861d05cddcSAtari911        $events = $INPUT->arr('events');
46871d05cddcSAtari911        $targetNamespace = $INPUT->str('target_namespace');
46881d05cddcSAtari911
46891d05cddcSAtari911        if (empty($events)) {
4690da206178SAtari911            $this->redirect('No events selected', 'error', 'manage');
46911d05cddcSAtari911        }
46921d05cddcSAtari911
46931d05cddcSAtari911        $moved = 0;
46941d05cddcSAtari911
46951d05cddcSAtari911        foreach ($events as $eventData) {
46961d05cddcSAtari911            list($id, $namespace, $date, $month) = explode('|', $eventData);
46971d05cddcSAtari911
46981d05cddcSAtari911            // Determine old file path
46991d05cddcSAtari911            if ($namespace === '') {
4700*2866e827SAtari911                $oldFile = $this->metaDir() . 'calendar/' . $month . '.json';
47011d05cddcSAtari911            } else {
4702*2866e827SAtari911                $oldFile = $this->metaDir() . $namespace . '/calendar/' . $month . '.json';
47031d05cddcSAtari911            }
47041d05cddcSAtari911
47051d05cddcSAtari911            if (!file_exists($oldFile)) continue;
47061d05cddcSAtari911
4707815440faSAtari911            $oldData = CalendarFileHandler::readJson($oldFile);
47081d05cddcSAtari911            if (!$oldData) continue;
47091d05cddcSAtari911
47101d05cddcSAtari911            // Find and remove event from old file
47111d05cddcSAtari911            $event = null;
47129ccd446eSAtari911            if (isset($oldData[$date])) {
47131d05cddcSAtari911                foreach ($oldData[$date] as $key => $evt) {
47141d05cddcSAtari911                    if ($evt['id'] === $id) {
47151d05cddcSAtari911                        $event = $evt;
47161d05cddcSAtari911                        unset($oldData[$date][$key]);
47171d05cddcSAtari911                        $oldData[$date] = array_values($oldData[$date]);
47181d05cddcSAtari911                        break;
47191d05cddcSAtari911                    }
47201d05cddcSAtari911                }
47211d05cddcSAtari911
47229ccd446eSAtari911                // Remove empty date arrays
47239ccd446eSAtari911                if (empty($oldData[$date])) {
47249ccd446eSAtari911                    unset($oldData[$date]);
47259ccd446eSAtari911                }
47269ccd446eSAtari911            }
47279ccd446eSAtari911
47281d05cddcSAtari911            if (!$event) continue;
47291d05cddcSAtari911
47301d05cddcSAtari911            // Save old file
4731815440faSAtari911            CalendarFileHandler::writeJson($oldFile, $oldData);
47321d05cddcSAtari911
47331d05cddcSAtari911            // Update event namespace
47341d05cddcSAtari911            $event['namespace'] = $targetNamespace;
47351d05cddcSAtari911
47361d05cddcSAtari911            // Determine new file path
47371d05cddcSAtari911            if ($targetNamespace === '') {
4738*2866e827SAtari911                $newFile = $this->metaDir() . 'calendar/' . $month . '.json';
47391d05cddcSAtari911                $newDir = dirname($newFile);
47401d05cddcSAtari911            } else {
4741*2866e827SAtari911                $newFile = $this->metaDir() . $targetNamespace . '/calendar/' . $month . '.json';
47421d05cddcSAtari911                $newDir = dirname($newFile);
47431d05cddcSAtari911            }
47441d05cddcSAtari911
47451d05cddcSAtari911            if (!is_dir($newDir)) {
47461d05cddcSAtari911                mkdir($newDir, 0755, true);
47471d05cddcSAtari911            }
47481d05cddcSAtari911
47491d05cddcSAtari911            $newData = [];
47501d05cddcSAtari911            if (file_exists($newFile)) {
4751815440faSAtari911                $newData = CalendarFileHandler::readJson($newFile) ?: [];
47521d05cddcSAtari911            }
47531d05cddcSAtari911
47541d05cddcSAtari911            if (!isset($newData[$date])) {
47551d05cddcSAtari911                $newData[$date] = [];
47561d05cddcSAtari911            }
47571d05cddcSAtari911            $newData[$date][] = $event;
47581d05cddcSAtari911
4759815440faSAtari911            CalendarFileHandler::writeJson($newFile, $newData);
47601d05cddcSAtari911            $moved++;
47611d05cddcSAtari911        }
47621d05cddcSAtari911
4763da206178SAtari911        $displayTarget = $targetNamespace ?: '(default)';
47649ccd446eSAtari911        $this->clearStatsCache();
4765da206178SAtari911        $this->redirect("Moved $moved event(s) to namespace: " . $displayTarget, 'success', 'manage');
47661d05cddcSAtari911    }
47671d05cddcSAtari911
47681d05cddcSAtari911    private function moveSingleEvent() {
47691d05cddcSAtari911        global $INPUT;
47701d05cddcSAtari911
47711d05cddcSAtari911        $eventData = $INPUT->str('event');
47721d05cddcSAtari911        $targetNamespace = $INPUT->str('target_namespace');
47731d05cddcSAtari911
47741d05cddcSAtari911        list($id, $namespace, $date, $month) = explode('|', $eventData);
47751d05cddcSAtari911
47761d05cddcSAtari911        // Determine old file path
47771d05cddcSAtari911        if ($namespace === '') {
4778*2866e827SAtari911            $oldFile = $this->metaDir() . 'calendar/' . $month . '.json';
47791d05cddcSAtari911        } else {
4780*2866e827SAtari911            $oldFile = $this->metaDir() . $namespace . '/calendar/' . $month . '.json';
47811d05cddcSAtari911        }
47821d05cddcSAtari911
47831d05cddcSAtari911        if (!file_exists($oldFile)) {
4784da206178SAtari911            $this->redirect('Event file not found', 'error', 'manage');
47851d05cddcSAtari911        }
47861d05cddcSAtari911
4787815440faSAtari911        $oldData = CalendarFileHandler::readJson($oldFile);
47881d05cddcSAtari911        if (!$oldData) {
4789da206178SAtari911            $this->redirect('Could not read event file', 'error', 'manage');
47901d05cddcSAtari911        }
47911d05cddcSAtari911
47921d05cddcSAtari911        // Find and remove event from old file
47931d05cddcSAtari911        $event = null;
47949ccd446eSAtari911        if (isset($oldData[$date])) {
47951d05cddcSAtari911            foreach ($oldData[$date] as $key => $evt) {
47961d05cddcSAtari911                if ($evt['id'] === $id) {
47971d05cddcSAtari911                    $event = $evt;
47981d05cddcSAtari911                    unset($oldData[$date][$key]);
47991d05cddcSAtari911                    $oldData[$date] = array_values($oldData[$date]);
48001d05cddcSAtari911                    break;
48011d05cddcSAtari911                }
48021d05cddcSAtari911            }
48031d05cddcSAtari911
48049ccd446eSAtari911            // Remove empty date arrays
48059ccd446eSAtari911            if (empty($oldData[$date])) {
48069ccd446eSAtari911                unset($oldData[$date]);
48079ccd446eSAtari911            }
48089ccd446eSAtari911        }
48099ccd446eSAtari911
48101d05cddcSAtari911        if (!$event) {
4811da206178SAtari911            $this->redirect('Event not found', 'error', 'manage');
48121d05cddcSAtari911        }
48131d05cddcSAtari911
48149ccd446eSAtari911        // Save old file (or delete if empty)
48159ccd446eSAtari911        if (empty($oldData)) {
48169ccd446eSAtari911            unlink($oldFile);
48179ccd446eSAtari911        } else {
4818815440faSAtari911            CalendarFileHandler::writeJson($oldFile, $oldData);
48199ccd446eSAtari911        }
48201d05cddcSAtari911
48211d05cddcSAtari911        // Update event namespace
48221d05cddcSAtari911        $event['namespace'] = $targetNamespace;
48231d05cddcSAtari911
48241d05cddcSAtari911        // Determine new file path
48251d05cddcSAtari911        if ($targetNamespace === '') {
4826*2866e827SAtari911            $newFile = $this->metaDir() . 'calendar/' . $month . '.json';
48271d05cddcSAtari911            $newDir = dirname($newFile);
48281d05cddcSAtari911        } else {
4829*2866e827SAtari911            $newFile = $this->metaDir() . $targetNamespace . '/calendar/' . $month . '.json';
48301d05cddcSAtari911            $newDir = dirname($newFile);
48311d05cddcSAtari911        }
48321d05cddcSAtari911
48331d05cddcSAtari911        if (!is_dir($newDir)) {
48341d05cddcSAtari911            mkdir($newDir, 0755, true);
48351d05cddcSAtari911        }
48361d05cddcSAtari911
48371d05cddcSAtari911        $newData = [];
48381d05cddcSAtari911        if (file_exists($newFile)) {
4839815440faSAtari911            $newData = CalendarFileHandler::readJson($newFile) ?: [];
48401d05cddcSAtari911        }
48411d05cddcSAtari911
48421d05cddcSAtari911        if (!isset($newData[$date])) {
48431d05cddcSAtari911            $newData[$date] = [];
48441d05cddcSAtari911        }
48451d05cddcSAtari911        $newData[$date][] = $event;
48461d05cddcSAtari911
4847815440faSAtari911        CalendarFileHandler::writeJson($newFile, $newData);
48481d05cddcSAtari911
4849da206178SAtari911        $displayTarget = $targetNamespace ?: '(default)';
48509ccd446eSAtari911        $this->clearStatsCache();
4851da206178SAtari911        $this->redirect('Moved "' . $event['title'] . '" to ' . $displayTarget, 'success', 'manage');
48521d05cddcSAtari911    }
48531d05cddcSAtari911
48541d05cddcSAtari911    private function createNamespace() {
48551d05cddcSAtari911        global $INPUT;
48561d05cddcSAtari911
48571d05cddcSAtari911        $namespaceName = $INPUT->str('namespace_name');
48581d05cddcSAtari911
48591d05cddcSAtari911        // Validate namespace name
48601d05cddcSAtari911        if (empty($namespaceName)) {
4861da206178SAtari911            $this->redirect('Namespace name cannot be empty', 'error', 'manage');
48621d05cddcSAtari911        }
48631d05cddcSAtari911
48641d05cddcSAtari911        if (!preg_match('/^[a-zA-Z0-9_:-]+$/', $namespaceName)) {
4865da206178SAtari911            $this->redirect('Invalid namespace name. Use only letters, numbers, underscore, hyphen, and colon.', 'error', 'manage');
48661d05cddcSAtari911        }
48671d05cddcSAtari911
48681d05cddcSAtari911        // Convert namespace to directory path
48691d05cddcSAtari911        $namespacePath = str_replace(':', '/', $namespaceName);
4870*2866e827SAtari911        $calendarDir = $this->metaDir() . $namespacePath . '/calendar';
48711d05cddcSAtari911
48721d05cddcSAtari911        // Check if already exists
48731d05cddcSAtari911        if (is_dir($calendarDir)) {
48741d05cddcSAtari911            // Check if it has any JSON files
48751d05cddcSAtari911            $hasFiles = !empty(glob($calendarDir . '/*.json'));
48761d05cddcSAtari911            if ($hasFiles) {
4877da206178SAtari911                $this->redirect("Namespace '$namespaceName' already exists with events", 'info', 'manage');
48781d05cddcSAtari911            }
48791d05cddcSAtari911            // If directory exists but empty, continue to create placeholder
48801d05cddcSAtari911        }
48811d05cddcSAtari911
48821d05cddcSAtari911        // Create the directory
48831d05cddcSAtari911        if (!is_dir($calendarDir)) {
48841d05cddcSAtari911            if (!mkdir($calendarDir, 0755, true)) {
4885da206178SAtari911                $this->redirect("Failed to create namespace directory", 'error', 'manage');
48861d05cddcSAtari911            }
48871d05cddcSAtari911        }
48881d05cddcSAtari911
48891d05cddcSAtari911        // Create a placeholder JSON file with an empty structure for current month
48901d05cddcSAtari911        // This ensures the namespace appears in the list immediately
48911d05cddcSAtari911        $currentMonth = date('Y-m');
48921d05cddcSAtari911        $placeholderFile = $calendarDir . '/' . $currentMonth . '.json';
48931d05cddcSAtari911
48941d05cddcSAtari911        if (!file_exists($placeholderFile)) {
4895815440faSAtari911            CalendarFileHandler::writeJson($placeholderFile, []);
48961d05cddcSAtari911        }
48971d05cddcSAtari911
4898da206178SAtari911        $this->redirect("Created namespace: $namespaceName", 'success', 'manage');
48991d05cddcSAtari911    }
49001d05cddcSAtari911
49011d05cddcSAtari911    private function deleteNamespace() {
49021d05cddcSAtari911        global $INPUT;
49031d05cddcSAtari911
49041d05cddcSAtari911        $namespace = $INPUT->str('namespace');
49051d05cddcSAtari911
49067e8ea635SAtari911        // Validate namespace name to prevent path traversal
49077e8ea635SAtari911        if ($namespace !== '' && !preg_match('/^[a-zA-Z0-9_:-]+$/', $namespace)) {
4908da206178SAtari911            $this->redirect('Invalid namespace name. Use only letters, numbers, underscore, hyphen, and colon.', 'error', 'manage');
49097e8ea635SAtari911            return;
49107e8ea635SAtari911        }
49117e8ea635SAtari911
49127e8ea635SAtari911        // Additional safety: ensure no path traversal sequences
49137e8ea635SAtari911        if (strpos($namespace, '..') !== false || strpos($namespace, '/') !== false || strpos($namespace, '\\') !== false) {
4914da206178SAtari911            $this->redirect('Invalid namespace: path traversal not allowed', 'error', 'manage');
49157e8ea635SAtari911            return;
49167e8ea635SAtari911        }
49177e8ea635SAtari911
49181d05cddcSAtari911        // Convert namespace to directory path (e.g., "work:projects" → "work/projects")
49191d05cddcSAtari911        $namespacePath = str_replace(':', '/', $namespace);
49201d05cddcSAtari911
49211d05cddcSAtari911        // Determine calendar directory
49221d05cddcSAtari911        if ($namespace === '') {
4923*2866e827SAtari911            $calendarDir = $this->metaDir() . 'calendar';
49241d05cddcSAtari911            $namespaceDir = null; // Don't delete root
49251d05cddcSAtari911        } else {
4926*2866e827SAtari911            $calendarDir = $this->metaDir() . $namespacePath . '/calendar';
4927*2866e827SAtari911            $namespaceDir = $this->metaDir() . $namespacePath;
49281d05cddcSAtari911        }
49291d05cddcSAtari911
49301d05cddcSAtari911        // Check if directory exists
49311d05cddcSAtari911        if (!is_dir($calendarDir)) {
49321d05cddcSAtari911            // Maybe it was never created or already deleted
4933da206178SAtari911            $this->redirect("Namespace directory not found: $calendarDir", 'error', 'manage');
49341d05cddcSAtari911            return;
49351d05cddcSAtari911        }
49361d05cddcSAtari911
49371d05cddcSAtari911        $filesDeleted = 0;
49381d05cddcSAtari911        $eventsDeleted = 0;
49391d05cddcSAtari911
49401d05cddcSAtari911        // Delete all calendar JSON files (including empty ones)
49411d05cddcSAtari911        foreach (glob($calendarDir . '/*.json') as $file) {
4942815440faSAtari911            $data = CalendarFileHandler::readJson($file);
49431d05cddcSAtari911            if ($data) {
49441d05cddcSAtari911                foreach ($data as $events) {
494596df7d3eSAtari911                    if (is_array($events)) {
49461d05cddcSAtari911                        $eventsDeleted += count($events);
49471d05cddcSAtari911                    }
49481d05cddcSAtari911                }
494996df7d3eSAtari911            }
49501d05cddcSAtari911            unlink($file);
49511d05cddcSAtari911            $filesDeleted++;
49521d05cddcSAtari911        }
49531d05cddcSAtari911
49541d05cddcSAtari911        // Delete any other files in calendar directory
49551d05cddcSAtari911        foreach (glob($calendarDir . '/*') as $file) {
49561d05cddcSAtari911            if (is_file($file)) {
49571d05cddcSAtari911                unlink($file);
49581d05cddcSAtari911            }
49591d05cddcSAtari911        }
49601d05cddcSAtari911
49611d05cddcSAtari911        // Remove the calendar directory
49621d05cddcSAtari911        if ($namespace !== '') {
49631d05cddcSAtari911            @rmdir($calendarDir);
49641d05cddcSAtari911
49651d05cddcSAtari911            // Try to remove parent directories if they're empty
49661d05cddcSAtari911            // This handles nested namespaces like work:projects:alpha
49671d05cddcSAtari911            $currentDir = dirname($calendarDir);
4968*2866e827SAtari911            $metaDir = rtrim($this->metaDir(), '/');
49691d05cddcSAtari911
49701d05cddcSAtari911            while ($currentDir !== $metaDir && $currentDir !== dirname($metaDir)) {
49711d05cddcSAtari911                if (is_dir($currentDir)) {
49721d05cddcSAtari911                    // Check if directory is empty
49731d05cddcSAtari911                    $contents = scandir($currentDir);
49741d05cddcSAtari911                    $isEmpty = count($contents) === 2; // Only . and ..
49751d05cddcSAtari911
49761d05cddcSAtari911                    if ($isEmpty) {
49771d05cddcSAtari911                        @rmdir($currentDir);
49781d05cddcSAtari911                        $currentDir = dirname($currentDir);
49791d05cddcSAtari911                    } else {
49801d05cddcSAtari911                        break; // Directory not empty, stop
49811d05cddcSAtari911                    }
49821d05cddcSAtari911                } else {
49831d05cddcSAtari911                    break;
49841d05cddcSAtari911                }
49851d05cddcSAtari911            }
49861d05cddcSAtari911        }
49871d05cddcSAtari911
4988da206178SAtari911        $displayName = $namespace ?: '(default)';
49899ccd446eSAtari911        $this->clearStatsCache();
4990da206178SAtari911        $this->redirect("Deleted namespace '$displayName': $eventsDeleted events in $filesDeleted files", 'success', 'manage');
49911d05cddcSAtari911    }
49921d05cddcSAtari911
49939ccd446eSAtari911    private function renameNamespace() {
49949ccd446eSAtari911        global $INPUT;
49959ccd446eSAtari911
49969ccd446eSAtari911        $oldNamespace = $INPUT->str('old_namespace');
49979ccd446eSAtari911        $newNamespace = $INPUT->str('new_namespace');
49989ccd446eSAtari911
49997e8ea635SAtari911        // Validate namespace names to prevent path traversal
50007e8ea635SAtari911        if ($oldNamespace !== '' && !preg_match('/^[a-zA-Z0-9_:-]+$/', $oldNamespace)) {
5001da206178SAtari911            $this->redirect('Invalid old namespace name. Use only letters, numbers, underscore, hyphen, and colon.', 'error', 'manage');
50027e8ea635SAtari911            return;
50037e8ea635SAtari911        }
50047e8ea635SAtari911
50057e8ea635SAtari911        if ($newNamespace !== '' && !preg_match('/^[a-zA-Z0-9_:-]+$/', $newNamespace)) {
5006da206178SAtari911            $this->redirect('Invalid new namespace name. Use only letters, numbers, underscore, hyphen, and colon.', 'error', 'manage');
50077e8ea635SAtari911            return;
50087e8ea635SAtari911        }
50097e8ea635SAtari911
50107e8ea635SAtari911        // Additional safety: ensure no path traversal sequences
50117e8ea635SAtari911        if (strpos($oldNamespace, '..') !== false || strpos($oldNamespace, '/') !== false || strpos($oldNamespace, '\\') !== false ||
50127e8ea635SAtari911            strpos($newNamespace, '..') !== false || strpos($newNamespace, '/') !== false || strpos($newNamespace, '\\') !== false) {
5013da206178SAtari911            $this->redirect('Invalid namespace: path traversal not allowed', 'error', 'manage');
50147e8ea635SAtari911            return;
50157e8ea635SAtari911        }
50167e8ea635SAtari911
50179ccd446eSAtari911        // Validate new namespace name
50189ccd446eSAtari911        if ($newNamespace === '') {
5019da206178SAtari911            $this->redirect("Cannot rename to empty namespace", 'error', 'manage');
50209ccd446eSAtari911            return;
50219ccd446eSAtari911        }
50229ccd446eSAtari911
50239ccd446eSAtari911        // Convert namespaces to directory paths
50249ccd446eSAtari911        $oldPath = str_replace(':', '/', $oldNamespace);
50259ccd446eSAtari911        $newPath = str_replace(':', '/', $newNamespace);
50269ccd446eSAtari911
50279ccd446eSAtari911        // Determine source and destination directories
50289ccd446eSAtari911        if ($oldNamespace === '') {
5029*2866e827SAtari911            $sourceDir = $this->metaDir() . 'calendar';
50309ccd446eSAtari911        } else {
5031*2866e827SAtari911            $sourceDir = $this->metaDir() . $oldPath . '/calendar';
50329ccd446eSAtari911        }
50339ccd446eSAtari911
50349ccd446eSAtari911        if ($newNamespace === '') {
5035*2866e827SAtari911            $targetDir = $this->metaDir() . 'calendar';
50369ccd446eSAtari911        } else {
5037*2866e827SAtari911            $targetDir = $this->metaDir() . $newPath . '/calendar';
50389ccd446eSAtari911        }
50399ccd446eSAtari911
50409ccd446eSAtari911        // Check if source exists
50419ccd446eSAtari911        if (!is_dir($sourceDir)) {
5042da206178SAtari911            $this->redirect("Source namespace not found: $oldNamespace", 'error', 'manage');
50439ccd446eSAtari911            return;
50449ccd446eSAtari911        }
50459ccd446eSAtari911
50469ccd446eSAtari911        // Check if target already exists
50479ccd446eSAtari911        if (is_dir($targetDir)) {
5048da206178SAtari911            $this->redirect("Target namespace already exists: $newNamespace", 'error', 'manage');
50499ccd446eSAtari911            return;
50509ccd446eSAtari911        }
50519ccd446eSAtari911
50529ccd446eSAtari911        // Create target directory
50539ccd446eSAtari911        if (!file_exists(dirname($targetDir))) {
50549ccd446eSAtari911            mkdir(dirname($targetDir), 0755, true);
50559ccd446eSAtari911        }
50569ccd446eSAtari911
50579ccd446eSAtari911        // Rename directory
50589ccd446eSAtari911        if (!rename($sourceDir, $targetDir)) {
5059da206178SAtari911            $this->redirect("Failed to rename namespace", 'error', 'manage');
50609ccd446eSAtari911            return;
50619ccd446eSAtari911        }
50629ccd446eSAtari911
50639ccd446eSAtari911        // Update event namespace field in all JSON files
50649ccd446eSAtari911        $eventsUpdated = 0;
50659ccd446eSAtari911        foreach (glob($targetDir . '/*.json') as $file) {
5066815440faSAtari911            $data = CalendarFileHandler::readJson($file);
50679ccd446eSAtari911            if ($data) {
50689ccd446eSAtari911                foreach ($data as $date => &$events) {
50699ccd446eSAtari911                    foreach ($events as &$event) {
50709ccd446eSAtari911                        if (isset($event['namespace']) && $event['namespace'] === $oldNamespace) {
50719ccd446eSAtari911                            $event['namespace'] = $newNamespace;
50729ccd446eSAtari911                            $eventsUpdated++;
50739ccd446eSAtari911                        }
50749ccd446eSAtari911                    }
50759ccd446eSAtari911                }
5076815440faSAtari911                CalendarFileHandler::writeJson($file, $data);
50779ccd446eSAtari911            }
50789ccd446eSAtari911        }
50799ccd446eSAtari911
50809ccd446eSAtari911        // Clean up old directory structure if empty
50819ccd446eSAtari911        if ($oldNamespace !== '') {
50829ccd446eSAtari911            $currentDir = dirname($sourceDir);
5083*2866e827SAtari911            $metaDir = rtrim($this->metaDir(), '/');
50849ccd446eSAtari911
50859ccd446eSAtari911            while ($currentDir !== $metaDir && $currentDir !== dirname($metaDir)) {
50869ccd446eSAtari911                if (is_dir($currentDir)) {
50879ccd446eSAtari911                    $contents = scandir($currentDir);
50889ccd446eSAtari911                    $isEmpty = count($contents) === 2; // Only . and ..
50899ccd446eSAtari911
50909ccd446eSAtari911                    if ($isEmpty) {
50919ccd446eSAtari911                        @rmdir($currentDir);
50929ccd446eSAtari911                        $currentDir = dirname($currentDir);
50939ccd446eSAtari911                    } else {
50949ccd446eSAtari911                        break;
50959ccd446eSAtari911                    }
50969ccd446eSAtari911                } else {
50979ccd446eSAtari911                    break;
50989ccd446eSAtari911                }
50999ccd446eSAtari911            }
51009ccd446eSAtari911        }
51019ccd446eSAtari911
51029ccd446eSAtari911        $this->clearStatsCache();
5103da206178SAtari911        $this->redirect("Renamed namespace from '$oldNamespace' to '$newNamespace' ($eventsUpdated events updated)", 'success', 'manage');
51049ccd446eSAtari911    }
51059ccd446eSAtari911
51061d05cddcSAtari911    private function deleteSelectedEvents() {
51071d05cddcSAtari911        global $INPUT;
51081d05cddcSAtari911
51091d05cddcSAtari911        $events = $INPUT->arr('events');
51101d05cddcSAtari911
51111d05cddcSAtari911        if (empty($events)) {
5112da206178SAtari911            $this->redirect('No events selected', 'error', 'manage');
51131d05cddcSAtari911        }
51141d05cddcSAtari911
51151d05cddcSAtari911        $deletedCount = 0;
51161d05cddcSAtari911
51171d05cddcSAtari911        foreach ($events as $eventData) {
51181d05cddcSAtari911            list($id, $namespace, $date, $month) = explode('|', $eventData);
51191d05cddcSAtari911
51201d05cddcSAtari911            // Determine file path
51211d05cddcSAtari911            if ($namespace === '') {
5122*2866e827SAtari911                $file = $this->metaDir() . 'calendar/' . $month . '.json';
51231d05cddcSAtari911            } else {
5124*2866e827SAtari911                $file = $this->metaDir() . $namespace . '/calendar/' . $month . '.json';
51251d05cddcSAtari911            }
51261d05cddcSAtari911
51271d05cddcSAtari911            if (!file_exists($file)) continue;
51281d05cddcSAtari911
5129815440faSAtari911            $data = CalendarFileHandler::readJson($file);
51301d05cddcSAtari911            if (!$data) continue;
51311d05cddcSAtari911
51321d05cddcSAtari911            // Find and remove event
51331d05cddcSAtari911            if (isset($data[$date])) {
51341d05cddcSAtari911                foreach ($data[$date] as $key => $evt) {
51351d05cddcSAtari911                    if ($evt['id'] === $id) {
51361d05cddcSAtari911                        unset($data[$date][$key]);
51371d05cddcSAtari911                        $data[$date] = array_values($data[$date]);
51381d05cddcSAtari911                        $deletedCount++;
51391d05cddcSAtari911                        break;
51401d05cddcSAtari911                    }
51411d05cddcSAtari911                }
51421d05cddcSAtari911
51431d05cddcSAtari911                // Remove empty date arrays
51441d05cddcSAtari911                if (empty($data[$date])) {
51451d05cddcSAtari911                    unset($data[$date]);
51461d05cddcSAtari911                }
51471d05cddcSAtari911
51481d05cddcSAtari911                // Save file
5149815440faSAtari911                CalendarFileHandler::writeJson($file, $data);
51501d05cddcSAtari911            }
51511d05cddcSAtari911        }
51521d05cddcSAtari911
51539ccd446eSAtari911        $this->clearStatsCache();
5154da206178SAtari911        $this->redirect("Deleted $deletedCount event(s)", 'success', 'manage');
51551d05cddcSAtari911    }
51561d05cddcSAtari911
51579ccd446eSAtari911    /**
51589ccd446eSAtari911     * Clear the event statistics cache so counts refresh after mutations
51599ccd446eSAtari911     */
51604590242dSAtari911    private function saveImportantNamespaces() {
51614590242dSAtari911        global $INPUT;
51624590242dSAtari911
5163*2866e827SAtari911        $configFile = $this->syncConfigPath();
51644590242dSAtari911        $config = [];
51654590242dSAtari911        if (file_exists($configFile)) {
51664590242dSAtari911            $config = include $configFile;
51674590242dSAtari911        }
51684590242dSAtari911
51694590242dSAtari911        $config['important_namespaces'] = $INPUT->str('important_namespaces', 'important');
51704590242dSAtari911
51714590242dSAtari911        $content = "<?php\nreturn " . var_export($config, true) . ";\n";
51724590242dSAtari911        if (file_put_contents($configFile, $content)) {
5173da206178SAtari911            $this->redirect('Important namespaces saved', 'success', 'manage');
51744590242dSAtari911        } else {
5175da206178SAtari911            $this->redirect('Error: Could not save configuration', 'error', 'manage');
51764590242dSAtari911        }
51774590242dSAtari911    }
51784590242dSAtari911
51799ccd446eSAtari911    private function clearStatsCache() {
5180*2866e827SAtari911        $cacheFile = $this->metaDir() . 'calendar/.event_stats_cache';
51819ccd446eSAtari911        if (file_exists($cacheFile)) {
51829ccd446eSAtari911            unlink($cacheFile);
51839ccd446eSAtari911        }
51849ccd446eSAtari911    }
51859ccd446eSAtari911
51861d05cddcSAtari911    private function getCronStatus() {
51871d05cddcSAtari911        // Try to read root's crontab first, then current user
51881d05cddcSAtari911        $output = [];
51891d05cddcSAtari911        exec('sudo crontab -l 2>/dev/null', $output);
51901d05cddcSAtari911
51911d05cddcSAtari911        // If sudo doesn't work, try current user
51921d05cddcSAtari911        if (empty($output)) {
51931d05cddcSAtari911            exec('crontab -l 2>/dev/null', $output);
51941d05cddcSAtari911        }
51951d05cddcSAtari911
51961d05cddcSAtari911        // Also check system crontab files
51971d05cddcSAtari911        if (empty($output)) {
51981d05cddcSAtari911            $cronFiles = [
51991d05cddcSAtari911                '/etc/crontab',
52001d05cddcSAtari911                '/etc/cron.d/calendar',
52011d05cddcSAtari911                '/var/spool/cron/root',
52021d05cddcSAtari911                '/var/spool/cron/crontabs/root'
52031d05cddcSAtari911            ];
52041d05cddcSAtari911
52051d05cddcSAtari911            foreach ($cronFiles as $file) {
52061d05cddcSAtari911                if (file_exists($file) && is_readable($file)) {
52071d05cddcSAtari911                    $content = file_get_contents($file);
52081d05cddcSAtari911                    $output = explode("\n", $content);
52091d05cddcSAtari911                    break;
52101d05cddcSAtari911                }
52111d05cddcSAtari911            }
52121d05cddcSAtari911        }
52131d05cddcSAtari911
52141d05cddcSAtari911        // Look for sync_outlook.php in the cron entries
52151d05cddcSAtari911        foreach ($output as $line) {
52161d05cddcSAtari911            $line = trim($line);
52171d05cddcSAtari911
52181d05cddcSAtari911            // Skip empty lines and comments
52191d05cddcSAtari911            if (empty($line) || $line[0] === '#') continue;
52201d05cddcSAtari911
52211d05cddcSAtari911            // Check if line contains sync_outlook.php
52221d05cddcSAtari911            if (strpos($line, 'sync_outlook.php') !== false) {
52231d05cddcSAtari911                // Parse cron expression
52241d05cddcSAtari911                // Format: minute hour day month weekday [user] command
52251d05cddcSAtari911                $parts = preg_split('/\s+/', $line, 7);
52261d05cddcSAtari911
52271d05cddcSAtari911                if (count($parts) >= 5) {
52281d05cddcSAtari911                    // Determine if this has a user field (system crontab format)
52291d05cddcSAtari911                    $hasUser = (count($parts) >= 6 && !preg_match('/^[\/\*]/', $parts[5]));
52301d05cddcSAtari911                    $offset = $hasUser ? 1 : 0;
52311d05cddcSAtari911
52321d05cddcSAtari911                    $frequency = $this->parseCronExpression($parts[0], $parts[1], $parts[2], $parts[3], $parts[4]);
52331d05cddcSAtari911                    return [
52341d05cddcSAtari911                        'active' => true,
52351d05cddcSAtari911                        'frequency' => $frequency,
52361d05cddcSAtari911                        'expression' => implode(' ', array_slice($parts, 0, 5)),
52371d05cddcSAtari911                        'full_line' => $line
52381d05cddcSAtari911                    ];
52391d05cddcSAtari911                }
52401d05cddcSAtari911            }
52411d05cddcSAtari911        }
52421d05cddcSAtari911
52431d05cddcSAtari911        return ['active' => false, 'frequency' => '', 'expression' => '', 'full_line' => ''];
52441d05cddcSAtari911    }
52451d05cddcSAtari911
52461d05cddcSAtari911    private function parseCronExpression($minute, $hour, $day, $month, $weekday) {
52471d05cddcSAtari911        // Parse minute field
52481d05cddcSAtari911        if ($minute === '*') {
5249da206178SAtari911            return 'Runs every minute';
52501d05cddcSAtari911        } elseif (strpos($minute, '*/') === 0) {
5251da206178SAtari911            $interval = substr($minute, 2);
52521d05cddcSAtari911            if ($interval == 1) {
5253da206178SAtari911                return 'Runs every minute';
5254da206178SAtari911            } elseif ($interval == 5) {
5255da206178SAtari911                return 'Runs every 5 minutes';
5256da206178SAtari911            } elseif ($interval == 8) {
5257da206178SAtari911                return 'Runs every 8 minutes';
5258da206178SAtari911            } elseif ($interval == 10) {
5259da206178SAtari911                return 'Runs every 10 minutes';
5260da206178SAtari911            } elseif ($interval == 15) {
5261da206178SAtari911                return 'Runs every 15 minutes';
5262da206178SAtari911            } elseif ($interval == 30) {
5263da206178SAtari911                return 'Runs every 30 minutes';
52641d05cddcSAtari911            } else {
5265da206178SAtari911                return "Runs every $interval minutes";
52661d05cddcSAtari911            }
52671d05cddcSAtari911        }
52681d05cddcSAtari911
52691d05cddcSAtari911        // Parse hour field
52701d05cddcSAtari911        if ($hour === '*' && $minute !== '*') {
5271da206178SAtari911            return 'Runs hourly';
52721d05cddcSAtari911        } elseif (strpos($hour, '*/') === 0 && $minute !== '*') {
5273da206178SAtari911            $interval = substr($hour, 2);
52741d05cddcSAtari911            if ($interval == 1) {
5275da206178SAtari911                return 'Runs every hour';
52761d05cddcSAtari911            } else {
5277da206178SAtari911                return "Runs every $interval hours";
52781d05cddcSAtari911            }
52791d05cddcSAtari911        }
52801d05cddcSAtari911
52811d05cddcSAtari911        // Parse day field
52821d05cddcSAtari911        if ($day === '*' && $hour !== '*' && $minute !== '*') {
5283da206178SAtari911            return 'Runs daily';
52841d05cddcSAtari911        }
52851d05cddcSAtari911
52861d05cddcSAtari911        // Default
5287da206178SAtari911        return 'Custom schedule';
52881d05cddcSAtari911    }
52891d05cddcSAtari911
52901d05cddcSAtari911    private function runSync() {
52911d05cddcSAtari911        global $INPUT;
52921d05cddcSAtari911
52931d05cddcSAtari911        if ($INPUT->str('call') === 'ajax') {
52941d05cddcSAtari911            header('Content-Type: application/json');
52951d05cddcSAtari911
52961d05cddcSAtari911            $syncScript = DOKU_PLUGIN . 'calendar/sync_outlook.php';
5297*2866e827SAtari911            $abortFile = $this->metaDir() . 'calendar/.sync_abort';
52981d05cddcSAtari911
52991d05cddcSAtari911            // Remove any existing abort flag
53001d05cddcSAtari911            if (file_exists($abortFile)) {
53011d05cddcSAtari911                @unlink($abortFile);
53021d05cddcSAtari911            }
53031d05cddcSAtari911
53041d05cddcSAtari911            if (!file_exists($syncScript)) {
5305da206178SAtari911                echo json_encode(['success' => false, 'message' => 'Sync script not found at: ' . $syncScript]);
53061d05cddcSAtari911                exit;
53071d05cddcSAtari911            }
53081d05cddcSAtari911
530996df7d3eSAtari911            // Get log file from data directory (writable)
531096df7d3eSAtari911            $logFile = $this->getSyncLogPath();
531196df7d3eSAtari911            $logDir = dirname($logFile);
531296df7d3eSAtari911
531396df7d3eSAtari911            // Ensure log directory exists
531496df7d3eSAtari911            if (!is_dir($logDir)) {
531596df7d3eSAtari911                if (!@mkdir($logDir, 0755, true)) {
5316da206178SAtari911                    echo json_encode(['success' => false, 'message' => 'Cannot create log directory: ' . $logDir]);
531796df7d3eSAtari911                    exit;
531896df7d3eSAtari911                }
531996df7d3eSAtari911            }
53201d05cddcSAtari911
53211d05cddcSAtari911            // Ensure log file exists and is writable
53221d05cddcSAtari911            if (!file_exists($logFile)) {
532396df7d3eSAtari911                if (!@touch($logFile)) {
5324da206178SAtari911                    echo json_encode(['success' => false, 'message' => 'Cannot create log file: ' . $logFile]);
532596df7d3eSAtari911                    exit;
532696df7d3eSAtari911                }
53271d05cddcSAtari911                @chmod($logFile, 0666);
53281d05cddcSAtari911            }
53291d05cddcSAtari911
533096df7d3eSAtari911            // Check if we can write to the log
533196df7d3eSAtari911            if (!is_writable($logFile)) {
5332da206178SAtari911                echo json_encode(['success' => false, 'message' => 'Log file not writable: ' . $logFile . ' - Run: chmod 666 ' . $logFile]);
533396df7d3eSAtari911                exit;
533496df7d3eSAtari911            }
533596df7d3eSAtari911
533696df7d3eSAtari911            // Find PHP binary
533796df7d3eSAtari911            $phpPath = $this->findPhpBinary();
533896df7d3eSAtari911            if (!$phpPath) {
5339da206178SAtari911                echo json_encode(['success' => false, 'message' => 'Cannot find PHP binary']);
534096df7d3eSAtari911                exit;
534196df7d3eSAtari911            }
534296df7d3eSAtari911
534396df7d3eSAtari911            // Get plugin directory for cd command
534496df7d3eSAtari911            $pluginDir = DOKU_PLUGIN . 'calendar';
534596df7d3eSAtari911
534696df7d3eSAtari911            // Build command - NO --verbose flag because the script logs internally
534796df7d3eSAtari911            // The script writes directly to the log file, so we don't need to capture stdout
534896df7d3eSAtari911            $command = sprintf(
534996df7d3eSAtari911                'cd %s && %s sync_outlook.php 2>&1',
535096df7d3eSAtari911                escapeshellarg($pluginDir),
535196df7d3eSAtari911                $phpPath
535296df7d3eSAtari911            );
535396df7d3eSAtari911
535496df7d3eSAtari911            // Log that we're starting
53551d05cddcSAtari911            $tz = new DateTimeZone('America/Los_Angeles');
53561d05cddcSAtari911            $now = new DateTime('now', $tz);
53571d05cddcSAtari911            $timestamp = $now->format('Y-m-d H:i:s');
53581d05cddcSAtari911            @file_put_contents($logFile, "[$timestamp] [ADMIN] Manual sync triggered via admin panel\n", FILE_APPEND);
535996df7d3eSAtari911            @file_put_contents($logFile, "[$timestamp] [ADMIN] Command: $command\n", FILE_APPEND);
53601d05cddcSAtari911
536196df7d3eSAtari911            // Execute sync
53621d05cddcSAtari911            $output = [];
53631d05cddcSAtari911            $returnCode = 0;
53641d05cddcSAtari911            exec($command, $output, $returnCode);
53651d05cddcSAtari911
536696df7d3eSAtari911            // Only log output if there was an error (the script logs its own progress)
536796df7d3eSAtari911            if ($returnCode !== 0 && !empty($output)) {
536896df7d3eSAtari911                @file_put_contents($logFile, "[$timestamp] [ADMIN] Error output:\n" . implode("\n", $output) . "\n", FILE_APPEND);
53691d05cddcSAtari911            }
53701d05cddcSAtari911
537196df7d3eSAtari911            // Check results
537296df7d3eSAtari911            if ($returnCode === 0) {
53731d05cddcSAtari911                echo json_encode([
53741d05cddcSAtari911                    'success' => true,
5375da206178SAtari911                    'message' => 'Sync completed! Check log for details.'
53761d05cddcSAtari911                ]);
53771d05cddcSAtari911            } else {
5378da206178SAtari911                $errorMsg = 'Sync failed (exit code: ' . $returnCode . ')';
53791d05cddcSAtari911                if (!empty($output)) {
538096df7d3eSAtari911                    $lastLines = array_slice($output, -3);
538196df7d3eSAtari911                    $errorMsg .= ' - ' . implode(' | ', $lastLines);
53821d05cddcSAtari911                }
53831d05cddcSAtari911                echo json_encode([
53841d05cddcSAtari911                    'success' => false,
53851d05cddcSAtari911                    'message' => $errorMsg
53861d05cddcSAtari911                ]);
53871d05cddcSAtari911            }
53881d05cddcSAtari911            exit;
53891d05cddcSAtari911        }
53901d05cddcSAtari911    }
53911d05cddcSAtari911
53921d05cddcSAtari911    private function stopSync() {
53931d05cddcSAtari911        global $INPUT;
53941d05cddcSAtari911
53951d05cddcSAtari911        if ($INPUT->str('call') === 'ajax') {
53961d05cddcSAtari911            header('Content-Type: application/json');
53971d05cddcSAtari911
5398*2866e827SAtari911            $abortFile = $this->metaDir() . 'calendar/.sync_abort';
53991d05cddcSAtari911
54001d05cddcSAtari911            // Create abort flag file
54011d05cddcSAtari911            if (file_put_contents($abortFile, date('Y-m-d H:i:s')) !== false) {
54021d05cddcSAtari911                echo json_encode([
54031d05cddcSAtari911                    'success' => true,
5404da206178SAtari911                    'message' => 'Stop signal sent to sync process'
54051d05cddcSAtari911                ]);
54061d05cddcSAtari911            } else {
54071d05cddcSAtari911                echo json_encode([
54081d05cddcSAtari911                    'success' => false,
5409da206178SAtari911                    'message' => 'Failed to create abort flag'
54101d05cddcSAtari911                ]);
54111d05cddcSAtari911            }
54121d05cddcSAtari911            exit;
54131d05cddcSAtari911        }
54141d05cddcSAtari911    }
54151d05cddcSAtari911
54161d05cddcSAtari911    private function uploadUpdate() {
54171d05cddcSAtari911        if (!isset($_FILES['plugin_zip']) || $_FILES['plugin_zip']['error'] !== UPLOAD_ERR_OK) {
5418da206178SAtari911            $this->redirect('Upload failed: ' . ($_FILES['plugin_zip']['error'] ?? 'No file uploaded'), 'error', 'update');
54191d05cddcSAtari911            return;
54201d05cddcSAtari911        }
54211d05cddcSAtari911
54221d05cddcSAtari911        $uploadedFile = $_FILES['plugin_zip']['tmp_name'];
54231d05cddcSAtari911        $pluginDir = DOKU_PLUGIN . 'calendar/';
54241d05cddcSAtari911        $backupFirst = isset($_POST['backup_first']);
54251d05cddcSAtari911
54261d05cddcSAtari911        // Check if plugin directory is writable
54271d05cddcSAtari911        if (!is_writable($pluginDir)) {
5428da206178SAtari911            $this->redirect('Plugin directory is not writable. Please check permissions: ' . $pluginDir, 'error', 'update');
54291d05cddcSAtari911            return;
54301d05cddcSAtari911        }
54311d05cddcSAtari911
54321d05cddcSAtari911        // Check if parent directory is writable (for backup and temp files)
54331d05cddcSAtari911        if (!is_writable(DOKU_PLUGIN)) {
5434da206178SAtari911            $this->redirect('Plugin parent directory is not writable. Please check permissions: ' . DOKU_PLUGIN, 'error', 'update');
54351d05cddcSAtari911            return;
54361d05cddcSAtari911        }
54371d05cddcSAtari911
54381d05cddcSAtari911        // Verify it's a ZIP file
54391d05cddcSAtari911        $finfo = finfo_open(FILEINFO_MIME_TYPE);
54401d05cddcSAtari911        $mimeType = finfo_file($finfo, $uploadedFile);
54411d05cddcSAtari911        finfo_close($finfo);
54421d05cddcSAtari911
54431d05cddcSAtari911        if ($mimeType !== 'application/zip' && $mimeType !== 'application/x-zip-compressed') {
5444da206178SAtari911            $this->redirect('Invalid file type. Please upload a ZIP file.', 'error', 'update');
54451d05cddcSAtari911            return;
54461d05cddcSAtari911        }
54471d05cddcSAtari911
54481d05cddcSAtari911        // Create backup if requested
54491d05cddcSAtari911        if ($backupFirst) {
54501d05cddcSAtari911            // Get current version
54511d05cddcSAtari911            $pluginInfo = $pluginDir . 'plugin.info.txt';
54521d05cddcSAtari911            $version = 'unknown';
54531d05cddcSAtari911            if (file_exists($pluginInfo)) {
54541d05cddcSAtari911                $info = confToHash($pluginInfo);
54551d05cddcSAtari911                $version = $info['version'] ?? ($info['date'] ?? 'unknown');
54561d05cddcSAtari911            }
54571d05cddcSAtari911
54581d05cddcSAtari911            $backupName = 'calendar.backup.v' . $version . '.' . date('Y-m-d_H-i-s') . '.zip';
54591d05cddcSAtari911            $backupPath = DOKU_PLUGIN . $backupName;
54601d05cddcSAtari911
54611d05cddcSAtari911            try {
54621d05cddcSAtari911                $zip = new ZipArchive();
54631d05cddcSAtari911                if ($zip->open($backupPath, ZipArchive::CREATE) === TRUE) {
54649ccd446eSAtari911                    $fileCount = $this->addDirectoryToZip($zip, $pluginDir, 'calendar/');
54651d05cddcSAtari911                    $zip->close();
54669ccd446eSAtari911
54679ccd446eSAtari911                    // Verify backup was created and has content
54689ccd446eSAtari911                    if (!file_exists($backupPath)) {
5469da206178SAtari911                        $this->redirect('Backup file was not created', 'error', 'update');
54709ccd446eSAtari911                        return;
54719ccd446eSAtari911                    }
54729ccd446eSAtari911
54739ccd446eSAtari911                    $backupSize = filesize($backupPath);
54749ccd446eSAtari911                    if ($backupSize < 1000) { // Backup should be at least 1KB
54759ccd446eSAtari911                        @unlink($backupPath);
5476da206178SAtari911                        $this->redirect('Backup file is too small (' . $backupSize . ' bytes). Only ' . $fileCount . ' files were added. Backup aborted.', 'error', 'update');
54779ccd446eSAtari911                        return;
54789ccd446eSAtari911                    }
54799ccd446eSAtari911
54809ccd446eSAtari911                    if ($fileCount < 10) { // Should have at least 10 files
54819ccd446eSAtari911                        @unlink($backupPath);
5482da206178SAtari911                        $this->redirect('Backup incomplete: Only ' . $fileCount . ' files were added (expected 30+). Backup aborted.', 'error', 'update');
54839ccd446eSAtari911                        return;
54849ccd446eSAtari911                    }
54851d05cddcSAtari911                } else {
5486da206178SAtari911                    $this->redirect('Failed to create backup ZIP file', 'error', 'update');
54871d05cddcSAtari911                    return;
54881d05cddcSAtari911                }
54891d05cddcSAtari911            } catch (Exception $e) {
54909ccd446eSAtari911                if (file_exists($backupPath)) {
54919ccd446eSAtari911                    @unlink($backupPath);
54929ccd446eSAtari911                }
5493da206178SAtari911                $this->redirect('Backup failed: ' . $e->getMessage(), 'error', 'update');
54941d05cddcSAtari911                return;
54951d05cddcSAtari911            }
54961d05cddcSAtari911        }
54971d05cddcSAtari911
54981d05cddcSAtari911        // Extract uploaded ZIP
54991d05cddcSAtari911        $zip = new ZipArchive();
55001d05cddcSAtari911        if ($zip->open($uploadedFile) !== TRUE) {
5501da206178SAtari911            $this->redirect('Failed to open ZIP file', 'error', 'update');
55021d05cddcSAtari911            return;
55031d05cddcSAtari911        }
55041d05cddcSAtari911
55051d05cddcSAtari911        // Check if ZIP contains calendar folder
55061d05cddcSAtari911        $hasCalendarFolder = false;
55071d05cddcSAtari911        for ($i = 0; $i < $zip->numFiles; $i++) {
55081d05cddcSAtari911            $filename = $zip->getNameIndex($i);
55091d05cddcSAtari911            if (strpos($filename, 'calendar/') === 0) {
55101d05cddcSAtari911                $hasCalendarFolder = true;
55111d05cddcSAtari911                break;
55121d05cddcSAtari911            }
55131d05cddcSAtari911        }
55141d05cddcSAtari911
55151d05cddcSAtari911        // Extract to temp directory first
55161d05cddcSAtari911        $tempDir = DOKU_PLUGIN . 'calendar_update_temp/';
55171d05cddcSAtari911        if (is_dir($tempDir)) {
55181d05cddcSAtari911            $this->deleteDirectory($tempDir);
55191d05cddcSAtari911        }
55201d05cddcSAtari911        mkdir($tempDir);
55211d05cddcSAtari911
55221d05cddcSAtari911        $zip->extractTo($tempDir);
55231d05cddcSAtari911        $zip->close();
55241d05cddcSAtari911
55251d05cddcSAtari911        // Determine source directory
55261d05cddcSAtari911        if ($hasCalendarFolder) {
55271d05cddcSAtari911            $sourceDir = $tempDir . 'calendar/';
55281d05cddcSAtari911        } else {
55291d05cddcSAtari911            $sourceDir = $tempDir;
55301d05cddcSAtari911        }
55311d05cddcSAtari911
553296df7d3eSAtari911        // Preserve configuration files (sync_state.json and sync.log are now in data/meta/calendar/)
553396df7d3eSAtari911        $preserveFiles = ['sync_config.php'];
55341d05cddcSAtari911        $preserved = [];
55351d05cddcSAtari911        foreach ($preserveFiles as $file) {
55361d05cddcSAtari911            $oldFile = $pluginDir . $file;
55371d05cddcSAtari911            if (file_exists($oldFile)) {
55381d05cddcSAtari911                $preserved[$file] = file_get_contents($oldFile);
55391d05cddcSAtari911            }
55401d05cddcSAtari911        }
55411d05cddcSAtari911
55421d05cddcSAtari911        // Delete old plugin files (except data files)
55431d05cddcSAtari911        $this->deleteDirectoryContents($pluginDir, $preserveFiles);
55441d05cddcSAtari911
55451d05cddcSAtari911        // Copy new files
55461d05cddcSAtari911        $this->recursiveCopy($sourceDir, $pluginDir);
55471d05cddcSAtari911
55481d05cddcSAtari911        // Restore preserved files
55491d05cddcSAtari911        foreach ($preserved as $file => $content) {
55501d05cddcSAtari911            file_put_contents($pluginDir . $file, $content);
55511d05cddcSAtari911        }
55521d05cddcSAtari911
55531d05cddcSAtari911        // Update version and date in plugin.info.txt
55541d05cddcSAtari911        $pluginInfo = $pluginDir . 'plugin.info.txt';
55551d05cddcSAtari911        if (file_exists($pluginInfo)) {
55561d05cddcSAtari911            $info = confToHash($pluginInfo);
55571d05cddcSAtari911
55581d05cddcSAtari911            // Get new version from uploaded plugin
55591d05cddcSAtari911            $newVersion = $info['version'] ?? 'unknown';
55601d05cddcSAtari911
55611d05cddcSAtari911            // Update date to current
55621d05cddcSAtari911            $info['date'] = date('Y-m-d');
55631d05cddcSAtari911
55641d05cddcSAtari911            // Write updated info back
55651d05cddcSAtari911            $lines = [];
55661d05cddcSAtari911            foreach ($info as $key => $value) {
55671d05cddcSAtari911                $lines[] = str_pad($key, 8) . ' ' . $value;
55681d05cddcSAtari911            }
55691d05cddcSAtari911            file_put_contents($pluginInfo, implode("\n", $lines) . "\n");
55701d05cddcSAtari911        }
55711d05cddcSAtari911
55721d05cddcSAtari911        // Cleanup temp directory
55731d05cddcSAtari911        $this->deleteDirectory($tempDir);
55741d05cddcSAtari911
5575da206178SAtari911        $message = 'Plugin updated successfully!';
55761d05cddcSAtari911        if ($backupFirst) {
5577da206178SAtari911            $message .= ' Backup saved as: ' . $backupName;
55781d05cddcSAtari911        }
55791d05cddcSAtari911        $this->redirect($message, 'success', 'update');
55801d05cddcSAtari911    }
55811d05cddcSAtari911
55821d05cddcSAtari911    private function deleteBackup() {
55831d05cddcSAtari911        global $INPUT;
55841d05cddcSAtari911
55851d05cddcSAtari911        $filename = $INPUT->str('backup_file');
55861d05cddcSAtari911
55871d05cddcSAtari911        if (empty($filename)) {
5588da206178SAtari911            $this->redirect('No backup file specified', 'error', 'update');
55891d05cddcSAtari911            return;
55901d05cddcSAtari911        }
55911d05cddcSAtari911
55921d05cddcSAtari911        // Security: only allow files starting with "calendar" and ending with .zip, no directory traversal
55931d05cddcSAtari911        if (!preg_match('/^calendar[a-zA-Z0-9._-]*\.zip$/', $filename)) {
5594da206178SAtari911            $this->redirect('Invalid backup filename', 'error', 'update');
55951d05cddcSAtari911            return;
55961d05cddcSAtari911        }
55971d05cddcSAtari911
55981d05cddcSAtari911        $backupPath = DOKU_PLUGIN . $filename;
55991d05cddcSAtari911
56001d05cddcSAtari911        if (!file_exists($backupPath)) {
5601da206178SAtari911            $this->redirect('Backup file not found', 'error', 'update');
56021d05cddcSAtari911            return;
56031d05cddcSAtari911        }
56041d05cddcSAtari911
56051d05cddcSAtari911        if (@unlink($backupPath)) {
5606da206178SAtari911            $this->redirect('Backup deleted: ' . $filename, 'success', 'update');
56071d05cddcSAtari911        } else {
5608da206178SAtari911            $this->redirect('Failed to delete backup. Check file permissions.', 'error', 'update');
56091d05cddcSAtari911        }
56101d05cddcSAtari911    }
56111d05cddcSAtari911
56121d05cddcSAtari911    private function renameBackup() {
56131d05cddcSAtari911        global $INPUT;
56141d05cddcSAtari911
56151d05cddcSAtari911        $oldName = $INPUT->str('old_name');
56161d05cddcSAtari911        $newName = $INPUT->str('new_name');
56171d05cddcSAtari911
56181d05cddcSAtari911        if (empty($oldName) || empty($newName)) {
5619da206178SAtari911            $this->redirect('Missing filename(s)', 'error', 'update');
56201d05cddcSAtari911            return;
56211d05cddcSAtari911        }
56221d05cddcSAtari911
56231d05cddcSAtari911        // Security: validate filenames
56241d05cddcSAtari911        if (!preg_match('/^[a-zA-Z0-9._-]+\.zip$/', $oldName) || !preg_match('/^[a-zA-Z0-9._-]+\.zip$/', $newName)) {
5625da206178SAtari911            $this->redirect('Invalid filename format', 'error', 'update');
56261d05cddcSAtari911            return;
56271d05cddcSAtari911        }
56281d05cddcSAtari911
56291d05cddcSAtari911        $oldPath = DOKU_PLUGIN . $oldName;
56301d05cddcSAtari911        $newPath = DOKU_PLUGIN . $newName;
56311d05cddcSAtari911
56321d05cddcSAtari911        if (!file_exists($oldPath)) {
5633da206178SAtari911            $this->redirect('Backup file not found', 'error', 'update');
56341d05cddcSAtari911            return;
56351d05cddcSAtari911        }
56361d05cddcSAtari911
56371d05cddcSAtari911        if (file_exists($newPath)) {
5638da206178SAtari911            $this->redirect('A file with the new name already exists', 'error', 'update');
56391d05cddcSAtari911            return;
56401d05cddcSAtari911        }
56411d05cddcSAtari911
56421d05cddcSAtari911        if (@rename($oldPath, $newPath)) {
5643da206178SAtari911            $this->redirect('Backup renamed: ' . $oldName . ' → ' . $newName, 'success', 'update');
56441d05cddcSAtari911        } else {
5645da206178SAtari911            $this->redirect('Failed to rename backup. Check file permissions.', 'error', 'update');
56461d05cddcSAtari911        }
56471d05cddcSAtari911    }
56481d05cddcSAtari911
564996df7d3eSAtari911    /**
565096df7d3eSAtari911     * Restore a backup using DokuWiki's extension manager
565196df7d3eSAtari911     * This ensures proper permissions and follows DokuWiki's standard installation process
565296df7d3eSAtari911     */
56531d05cddcSAtari911    private function restoreBackup() {
56541d05cddcSAtari911        global $INPUT;
56551d05cddcSAtari911
56561d05cddcSAtari911        $filename = $INPUT->str('backup_file');
56571d05cddcSAtari911
56581d05cddcSAtari911        if (empty($filename)) {
5659da206178SAtari911            $this->redirect('No backup file specified', 'error', 'update');
56601d05cddcSAtari911            return;
56611d05cddcSAtari911        }
56621d05cddcSAtari911
56631d05cddcSAtari911        // Security: only allow files starting with "calendar" and ending with .zip, no directory traversal
56641d05cddcSAtari911        if (!preg_match('/^calendar[a-zA-Z0-9._-]*\.zip$/', $filename)) {
5665da206178SAtari911            $this->redirect('Invalid backup filename', 'error', 'update');
56661d05cddcSAtari911            return;
56671d05cddcSAtari911        }
56681d05cddcSAtari911
56691d05cddcSAtari911        $backupPath = DOKU_PLUGIN . $filename;
56701d05cddcSAtari911
56711d05cddcSAtari911        if (!file_exists($backupPath)) {
5672da206178SAtari911            $this->redirect('Backup file not found', 'error', 'update');
56731d05cddcSAtari911            return;
56741d05cddcSAtari911        }
56751d05cddcSAtari911
567696df7d3eSAtari911        // Try to use DokuWiki's extension manager helper
567796df7d3eSAtari911        $extensionHelper = plugin_load('helper', 'extension_extension');
567896df7d3eSAtari911
567996df7d3eSAtari911        if (!$extensionHelper) {
568096df7d3eSAtari911            // Extension manager not available - provide manual instructions
5681da206178SAtari911            $this->redirect('DokuWiki Extension Manager not available. Please install manually: Download the backup, go to Admin → Extension Manager → Install, and upload the ZIP file.', 'error', 'update');
56821d05cddcSAtari911            return;
56831d05cddcSAtari911        }
56841d05cddcSAtari911
568596df7d3eSAtari911        try {
568696df7d3eSAtari911            // Set the extension we're working with
568796df7d3eSAtari911            $extensionHelper->setExtension('calendar');
568896df7d3eSAtari911
568996df7d3eSAtari911            // Use DokuWiki's extension manager to install from the local file
569096df7d3eSAtari911            // This handles all permissions and file operations properly
569196df7d3eSAtari911            $installed = $extensionHelper->installFromLocal($backupPath, true); // true = overwrite
569296df7d3eSAtari911
569396df7d3eSAtari911            if ($installed) {
5694da206178SAtari911                $this->redirect('Plugin restored from backup: ' . $filename . ' (via Extension Manager)', 'success', 'update');
569596df7d3eSAtari911            } else {
569696df7d3eSAtari911                // Get any error message from the extension helper
569796df7d3eSAtari911                $errors = $extensionHelper->getErrors();
569896df7d3eSAtari911                $errorMsg = !empty($errors) ? implode(', ', $errors) : 'Unknown error';
5699da206178SAtari911                $this->redirect('Restore failed: ' . $errorMsg, 'error', 'update');
57001d05cddcSAtari911            }
570196df7d3eSAtari911        } catch (Exception $e) {
5702da206178SAtari911            $this->redirect('Restore failed: ' . $e->getMessage(), 'error', 'update');
57031d05cddcSAtari911        }
57041d05cddcSAtari911    }
57051d05cddcSAtari911
57069ccd446eSAtari911    private function createManualBackup() {
57079ccd446eSAtari911        $pluginDir = DOKU_PLUGIN . 'calendar/';
57089ccd446eSAtari911
57099ccd446eSAtari911        // Check if plugin directory is readable
57109ccd446eSAtari911        if (!is_readable($pluginDir)) {
5711da206178SAtari911            $this->redirect('Plugin directory is not readable. Please check permissions.', 'error', 'update');
57129ccd446eSAtari911            return;
57139ccd446eSAtari911        }
57149ccd446eSAtari911
57159ccd446eSAtari911        // Check if parent directory is writable (for saving backup)
57169ccd446eSAtari911        if (!is_writable(DOKU_PLUGIN)) {
5717da206178SAtari911            $this->redirect('Plugin parent directory is not writable. Cannot save backup.', 'error', 'update');
57189ccd446eSAtari911            return;
57199ccd446eSAtari911        }
57209ccd446eSAtari911
57219ccd446eSAtari911        // Get current version
57229ccd446eSAtari911        $pluginInfo = $pluginDir . 'plugin.info.txt';
57239ccd446eSAtari911        $version = 'unknown';
57249ccd446eSAtari911        if (file_exists($pluginInfo)) {
57259ccd446eSAtari911            $info = confToHash($pluginInfo);
57269ccd446eSAtari911            $version = $info['version'] ?? ($info['date'] ?? 'unknown');
57279ccd446eSAtari911        }
57289ccd446eSAtari911
57299ccd446eSAtari911        $backupName = 'calendar.backup.v' . $version . '.manual.' . date('Y-m-d_H-i-s') . '.zip';
57309ccd446eSAtari911        $backupPath = DOKU_PLUGIN . $backupName;
57319ccd446eSAtari911
57329ccd446eSAtari911        try {
57339ccd446eSAtari911            $zip = new ZipArchive();
57349ccd446eSAtari911            if ($zip->open($backupPath, ZipArchive::CREATE) === TRUE) {
57359ccd446eSAtari911                $fileCount = $this->addDirectoryToZip($zip, $pluginDir, 'calendar/');
57369ccd446eSAtari911                $zip->close();
57379ccd446eSAtari911
57389ccd446eSAtari911                // Verify backup was created and has content
57399ccd446eSAtari911                if (!file_exists($backupPath)) {
5740da206178SAtari911                    $this->redirect('Backup file was not created', 'error', 'update');
57419ccd446eSAtari911                    return;
57429ccd446eSAtari911                }
57439ccd446eSAtari911
57449ccd446eSAtari911                $backupSize = filesize($backupPath);
57459ccd446eSAtari911                if ($backupSize < 1000) { // Backup should be at least 1KB
57469ccd446eSAtari911                    @unlink($backupPath);
5747da206178SAtari911                    $this->redirect('Backup file is too small (' . $this->formatBytes($backupSize) . '). Only ' . $fileCount . ' files were added. Backup failed.', 'error', 'update');
57489ccd446eSAtari911                    return;
57499ccd446eSAtari911                }
57509ccd446eSAtari911
57519ccd446eSAtari911                if ($fileCount < 10) { // Should have at least 10 files
57529ccd446eSAtari911                    @unlink($backupPath);
5753da206178SAtari911                    $this->redirect('Backup incomplete: Only ' . $fileCount . ' files were added (expected 30+). Backup failed.', 'error', 'update');
57549ccd446eSAtari911                    return;
57559ccd446eSAtari911                }
57569ccd446eSAtari911
57579ccd446eSAtari911                // Success!
5758da206178SAtari911                $this->redirect('✓ Manual backup created successfully: ' . $backupName . ' (' . $this->formatBytes($backupSize) . ', ' . $fileCount . ' files)', 'success', 'update');
57599ccd446eSAtari911
57609ccd446eSAtari911            } else {
5761da206178SAtari911                $this->redirect('Failed to create backup ZIP file', 'error', 'update');
57629ccd446eSAtari911                return;
57639ccd446eSAtari911            }
57649ccd446eSAtari911        } catch (Exception $e) {
57659ccd446eSAtari911            if (file_exists($backupPath)) {
57669ccd446eSAtari911                @unlink($backupPath);
57679ccd446eSAtari911            }
5768da206178SAtari911            $this->redirect('Backup failed: ' . $e->getMessage(), 'error', 'update');
57699ccd446eSAtari911            return;
57709ccd446eSAtari911        }
57719ccd446eSAtari911    }
57729ccd446eSAtari911
57731d05cddcSAtari911    private function addDirectoryToZip($zip, $dir, $zipPath = '') {
57749ccd446eSAtari911        $fileCount = 0;
57759ccd446eSAtari911        $errors = [];
57769ccd446eSAtari911
57777e8ea635SAtari911        // Ensure dir has trailing slash
57787e8ea635SAtari911        $dir = rtrim($dir, '/') . '/';
57797e8ea635SAtari911
57809ccd446eSAtari911        if (!is_dir($dir)) {
5781da206178SAtari911            throw new Exception("Directory does not exist: $dir");
57829ccd446eSAtari911        }
57839ccd446eSAtari911
57849ccd446eSAtari911        if (!is_readable($dir)) {
5785da206178SAtari911            throw new Exception("Directory is not readable: $dir");
57869ccd446eSAtari911        }
57879ccd446eSAtari911
57881d05cddcSAtari911        try {
57897e8ea635SAtari911            // First, add all directories to preserve structure (including empty ones)
57907e8ea635SAtari911            $dirs = new RecursiveIteratorIterator(
57911d05cddcSAtari911                new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS),
57927e8ea635SAtari911                RecursiveIteratorIterator::SELF_FIRST  // Process directories before their contents
57931d05cddcSAtari911            );
57941d05cddcSAtari911
57957e8ea635SAtari911            foreach ($dirs as $item) {
57967e8ea635SAtari911                $itemPath = $item->getRealPath();
57977e8ea635SAtari911                if (!$itemPath) continue;
57989ccd446eSAtari911
57997e8ea635SAtari911                // Calculate relative path from the source directory
58007e8ea635SAtari911                $relativePath = $zipPath . substr($itemPath, strlen($dir));
58017e8ea635SAtari911
58027e8ea635SAtari911                if ($item->isDir()) {
58037e8ea635SAtari911                    // Add directory to ZIP (preserves empty directories and structure)
58047e8ea635SAtari911                    $dirInZip = rtrim($relativePath, '/') . '/';
58057e8ea635SAtari911                    $zip->addEmptyDir($dirInZip);
58067e8ea635SAtari911                } else {
58077e8ea635SAtari911                    // Add file to ZIP
58087e8ea635SAtari911                    if (is_readable($itemPath)) {
58097e8ea635SAtari911                        if ($zip->addFile($itemPath, $relativePath)) {
58109ccd446eSAtari911                            $fileCount++;
58119ccd446eSAtari911                        } else {
5812da206178SAtari911                            $errors[] = "Failed to add: " . basename($itemPath);
58139ccd446eSAtari911                        }
58149ccd446eSAtari911                    } else {
5815da206178SAtari911                        $errors[] = "Cannot read: " . basename($itemPath);
58161d05cddcSAtari911                    }
58171d05cddcSAtari911                }
58181d05cddcSAtari911            }
58199ccd446eSAtari911
58209ccd446eSAtari911            // Log any errors but don't fail if we got most files
58219ccd446eSAtari911            if (!empty($errors) && count($errors) < 5) {
58229ccd446eSAtari911                foreach ($errors as $error) {
58239ccd446eSAtari911                    error_log('Calendar plugin backup warning: ' . $error);
58249ccd446eSAtari911                }
58259ccd446eSAtari911            }
58269ccd446eSAtari911
58279ccd446eSAtari911            // If too many errors, fail
58289ccd446eSAtari911            if (count($errors) > 5) {
58299ccd446eSAtari911                throw new Exception("Too many errors adding files to backup: " . implode(', ', array_slice($errors, 0, 5)));
58309ccd446eSAtari911            }
58319ccd446eSAtari911
58321d05cddcSAtari911        } catch (Exception $e) {
58339ccd446eSAtari911            error_log('Calendar plugin backup error: ' . $e->getMessage());
58349ccd446eSAtari911            throw $e;
58351d05cddcSAtari911        }
58369ccd446eSAtari911
58379ccd446eSAtari911        return $fileCount;
58381d05cddcSAtari911    }
58391d05cddcSAtari911
58401d05cddcSAtari911    private function deleteDirectory($dir) {
58411d05cddcSAtari911        if (!is_dir($dir)) return;
58421d05cddcSAtari911
58431d05cddcSAtari911        try {
58441d05cddcSAtari911            $files = new RecursiveIteratorIterator(
58451d05cddcSAtari911                new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS),
58461d05cddcSAtari911                RecursiveIteratorIterator::CHILD_FIRST
58471d05cddcSAtari911            );
58481d05cddcSAtari911
58491d05cddcSAtari911            foreach ($files as $file) {
58501d05cddcSAtari911                if ($file->isDir()) {
58511d05cddcSAtari911                    @rmdir($file->getRealPath());
58521d05cddcSAtari911                } else {
58531d05cddcSAtari911                    @unlink($file->getRealPath());
58541d05cddcSAtari911                }
58551d05cddcSAtari911            }
58561d05cddcSAtari911
58571d05cddcSAtari911            @rmdir($dir);
58581d05cddcSAtari911        } catch (Exception $e) {
58591d05cddcSAtari911            error_log('Calendar plugin delete directory error: ' . $e->getMessage());
58601d05cddcSAtari911        }
58611d05cddcSAtari911    }
58621d05cddcSAtari911
58631d05cddcSAtari911    private function deleteDirectoryContents($dir, $preserve = []) {
58641d05cddcSAtari911        if (!is_dir($dir)) return;
58651d05cddcSAtari911
58661d05cddcSAtari911        $items = scandir($dir);
58671d05cddcSAtari911        foreach ($items as $item) {
58681d05cddcSAtari911            if ($item === '.' || $item === '..') continue;
58691d05cddcSAtari911            if (in_array($item, $preserve)) continue;
58701d05cddcSAtari911
58711d05cddcSAtari911            $path = $dir . $item;
58721d05cddcSAtari911            if (is_dir($path)) {
58731d05cddcSAtari911                $this->deleteDirectory($path);
58741d05cddcSAtari911            } else {
58751d05cddcSAtari911                unlink($path);
58761d05cddcSAtari911            }
58771d05cddcSAtari911        }
58781d05cddcSAtari911    }
58791d05cddcSAtari911
58801d05cddcSAtari911    private function recursiveCopy($src, $dst) {
58817e8ea635SAtari911        if (!is_dir($src)) {
58827e8ea635SAtari911            return false;
58837e8ea635SAtari911        }
58847e8ea635SAtari911
58851d05cddcSAtari911        $dir = opendir($src);
58867e8ea635SAtari911        if (!$dir) {
58877e8ea635SAtari911            return false;
58887e8ea635SAtari911        }
58897e8ea635SAtari911
58907e8ea635SAtari911        // Create destination directory with proper permissions (0755)
58917e8ea635SAtari911        if (!is_dir($dst)) {
58927e8ea635SAtari911            mkdir($dst, 0755, true);
58937e8ea635SAtari911        }
58941d05cddcSAtari911
58951d05cddcSAtari911        while (($file = readdir($dir)) !== false) {
58961d05cddcSAtari911            if ($file !== '.' && $file !== '..') {
58977e8ea635SAtari911                $srcPath = $src . '/' . $file;
58987e8ea635SAtari911                $dstPath = $dst . '/' . $file;
58997e8ea635SAtari911
59007e8ea635SAtari911                if (is_dir($srcPath)) {
59017e8ea635SAtari911                    // Recursively copy subdirectory
59027e8ea635SAtari911                    $this->recursiveCopy($srcPath, $dstPath);
59031d05cddcSAtari911                } else {
59047e8ea635SAtari911                    // Copy file and preserve permissions
59057e8ea635SAtari911                    if (copy($srcPath, $dstPath)) {
59067e8ea635SAtari911                        // Try to preserve file permissions from source, fallback to 0644
59077e8ea635SAtari911                        $perms = @fileperms($srcPath);
59087e8ea635SAtari911                        if ($perms !== false) {
59097e8ea635SAtari911                            @chmod($dstPath, $perms);
59107e8ea635SAtari911                        } else {
59117e8ea635SAtari911                            @chmod($dstPath, 0644);
59127e8ea635SAtari911                        }
59137e8ea635SAtari911                    }
59141d05cddcSAtari911                }
59151d05cddcSAtari911            }
59161d05cddcSAtari911        }
59171d05cddcSAtari911
59181d05cddcSAtari911        closedir($dir);
59197e8ea635SAtari911        return true;
59201d05cddcSAtari911    }
59211d05cddcSAtari911
59221d05cddcSAtari911    private function formatBytes($bytes) {
59231d05cddcSAtari911        if ($bytes >= 1073741824) {
59241d05cddcSAtari911            return number_format($bytes / 1073741824, 2) . ' GB';
59251d05cddcSAtari911        } elseif ($bytes >= 1048576) {
59261d05cddcSAtari911            return number_format($bytes / 1048576, 2) . ' MB';
59271d05cddcSAtari911        } elseif ($bytes >= 1024) {
59281d05cddcSAtari911            return number_format($bytes / 1024, 2) . ' KB';
59291d05cddcSAtari911        } else {
59301d05cddcSAtari911            return $bytes . ' bytes';
59311d05cddcSAtari911        }
59321d05cddcSAtari911    }
59331d05cddcSAtari911
59341d05cddcSAtari911    private function findPhpBinary() {
59351d05cddcSAtari911        // Try PHP_BINARY constant first (most reliable if available)
59361d05cddcSAtari911        if (defined('PHP_BINARY') && !empty(PHP_BINARY) && is_executable(PHP_BINARY)) {
593796df7d3eSAtari911            return PHP_BINARY;
59381d05cddcSAtari911        }
59391d05cddcSAtari911
59401d05cddcSAtari911        // Try common PHP binary locations
59411d05cddcSAtari911        $possiblePaths = [
59421d05cddcSAtari911            '/usr/bin/php',
59431d05cddcSAtari911            '/usr/bin/php8.1',
59441d05cddcSAtari911            '/usr/bin/php8.2',
59451d05cddcSAtari911            '/usr/bin/php8.3',
59461d05cddcSAtari911            '/usr/bin/php7.4',
59471d05cddcSAtari911            '/usr/local/bin/php',
59481d05cddcSAtari911        ];
59491d05cddcSAtari911
59501d05cddcSAtari911        foreach ($possiblePaths as $path) {
595196df7d3eSAtari911            if (is_executable($path)) {
595296df7d3eSAtari911                return $path;
59531d05cddcSAtari911            }
59541d05cddcSAtari911        }
59551d05cddcSAtari911
595696df7d3eSAtari911        // Try using 'which' to find php
595796df7d3eSAtari911        $which = trim(shell_exec('which php 2>/dev/null') ?? '');
595896df7d3eSAtari911        if (!empty($which) && is_executable($which)) {
595996df7d3eSAtari911            return $which;
596096df7d3eSAtari911        }
596196df7d3eSAtari911
59621d05cddcSAtari911        // Fallback to 'php' and hope it's in PATH
59631d05cddcSAtari911        return 'php';
59641d05cddcSAtari911    }
59651d05cddcSAtari911
59661d05cddcSAtari911    private function redirect($message, $type = 'success', $tab = null) {
59671d05cddcSAtari911        $url = '?do=admin&page=calendar';
59681d05cddcSAtari911        if ($tab) {
59691d05cddcSAtari911            $url .= '&tab=' . $tab;
59701d05cddcSAtari911        }
59711d05cddcSAtari911        $url .= '&msg=' . urlencode($message) . '&msgtype=' . $type;
59721d05cddcSAtari911        header('Location: ' . $url);
59731d05cddcSAtari911        exit;
59741d05cddcSAtari911    }
59751d05cddcSAtari911
59761d05cddcSAtari911    private function getLog() {
59771d05cddcSAtari911        global $INPUT;
59781d05cddcSAtari911
59791d05cddcSAtari911        if ($INPUT->str('call') === 'ajax') {
59801d05cddcSAtari911            header('Content-Type: application/json');
59811d05cddcSAtari911
598296df7d3eSAtari911            $logFile = $this->getSyncLogPath();
59831d05cddcSAtari911            $log = '';
59841d05cddcSAtari911
59851d05cddcSAtari911            if (file_exists($logFile)) {
59861d05cddcSAtari911                // Get last 500 lines
59871d05cddcSAtari911                $lines = file($logFile);
59881d05cddcSAtari911                if ($lines !== false) {
59891d05cddcSAtari911                    $lines = array_slice($lines, -500);
59901d05cddcSAtari911                    $log = implode('', $lines);
59911d05cddcSAtari911                }
59921d05cddcSAtari911            } else {
5993da206178SAtari911                $log = "No log file found. Sync hasn't run yet.";
59941d05cddcSAtari911            }
59951d05cddcSAtari911
59961d05cddcSAtari911            echo json_encode(['log' => $log]);
59971d05cddcSAtari911            exit;
59981d05cddcSAtari911        }
59991d05cddcSAtari911    }
60001d05cddcSAtari911
60011d05cddcSAtari911    private function exportConfig() {
60021d05cddcSAtari911        global $INPUT;
60031d05cddcSAtari911
60041d05cddcSAtari911        if ($INPUT->str('call') === 'ajax') {
60051d05cddcSAtari911            header('Content-Type: application/json');
60061d05cddcSAtari911
60071d05cddcSAtari911            try {
6008*2866e827SAtari911                $configFile = $this->syncConfigPath();
60091d05cddcSAtari911
60101d05cddcSAtari911                if (!file_exists($configFile)) {
60111d05cddcSAtari911                    echo json_encode([
60121d05cddcSAtari911                        'success' => false,
6013da206178SAtari911                        'message' => 'Config file not found'
60141d05cddcSAtari911                    ]);
60151d05cddcSAtari911                    exit;
60161d05cddcSAtari911                }
60171d05cddcSAtari911
60181d05cddcSAtari911                // Read config file
60191d05cddcSAtari911                $configContent = file_get_contents($configFile);
60201d05cddcSAtari911
60211d05cddcSAtari911                // Generate encryption key from DokuWiki secret
60221d05cddcSAtari911                $key = $this->getEncryptionKey();
60231d05cddcSAtari911
60241d05cddcSAtari911                // Encrypt config
60251d05cddcSAtari911                $encrypted = $this->encryptData($configContent, $key);
60261d05cddcSAtari911
60271d05cddcSAtari911                echo json_encode([
60281d05cddcSAtari911                    'success' => true,
60291d05cddcSAtari911                    'encrypted' => $encrypted,
6030da206178SAtari911                    'message' => 'Config exported successfully'
60311d05cddcSAtari911                ]);
60321d05cddcSAtari911                exit;
60331d05cddcSAtari911
60341d05cddcSAtari911            } catch (Exception $e) {
60351d05cddcSAtari911                echo json_encode([
60361d05cddcSAtari911                    'success' => false,
60371d05cddcSAtari911                    'message' => $e->getMessage()
60381d05cddcSAtari911                ]);
60391d05cddcSAtari911                exit;
60401d05cddcSAtari911            }
60411d05cddcSAtari911        }
60421d05cddcSAtari911    }
60431d05cddcSAtari911
60441d05cddcSAtari911    private function importConfig() {
60451d05cddcSAtari911        global $INPUT;
60461d05cddcSAtari911
60471d05cddcSAtari911        if ($INPUT->str('call') === 'ajax') {
60481d05cddcSAtari911            header('Content-Type: application/json');
60491d05cddcSAtari911
60501d05cddcSAtari911            try {
6051da206178SAtari911                $encrypted = $_POST['encrypted_config'] ?? '';
60521d05cddcSAtari911
60531d05cddcSAtari911                if (empty($encrypted)) {
60541d05cddcSAtari911                    echo json_encode([
60551d05cddcSAtari911                        'success' => false,
6056da206178SAtari911                        'message' => 'No config data provided'
60571d05cddcSAtari911                    ]);
60581d05cddcSAtari911                    exit;
60591d05cddcSAtari911                }
60601d05cddcSAtari911
60611d05cddcSAtari911                // Generate encryption key from DokuWiki secret
60621d05cddcSAtari911                $key = $this->getEncryptionKey();
60631d05cddcSAtari911
60641d05cddcSAtari911                // Decrypt config
60651d05cddcSAtari911                $configContent = $this->decryptData($encrypted, $key);
60661d05cddcSAtari911
6067da206178SAtari911                if ($configContent === false) {
60681d05cddcSAtari911                    echo json_encode([
60691d05cddcSAtari911                        'success' => false,
6070da206178SAtari911                        'message' => 'Decryption failed. Invalid key or corrupted file.'
60711d05cddcSAtari911                    ]);
60721d05cddcSAtari911                    exit;
60731d05cddcSAtari911                }
60741d05cddcSAtari911
60757e8ea635SAtari911                // Validate PHP config file structure (without using eval)
60767e8ea635SAtari911                // Check that it starts with <?php and contains a return statement with array
60777e8ea635SAtari911                if (strpos($configContent, '<?php') === false) {
60781d05cddcSAtari911                    echo json_encode([
60791d05cddcSAtari911                        'success' => false,
6080da206178SAtari911                        'message' => 'Invalid config file: missing PHP opening tag'
60817e8ea635SAtari911                    ]);
60827e8ea635SAtari911                    exit;
60837e8ea635SAtari911                }
60847e8ea635SAtari911
60857e8ea635SAtari911                // Check for dangerous patterns that shouldn't be in a config file
60867e8ea635SAtari911                $dangerousPatterns = [
60877e8ea635SAtari911                    '/\b(exec|shell_exec|system|passthru|popen|proc_open)\s*\(/i',
60887e8ea635SAtari911                    '/\b(eval|assert|create_function)\s*\(/i',
60897e8ea635SAtari911                    '/\b(file_get_contents|file_put_contents|fopen|fwrite|unlink|rmdir)\s*\(/i',
60907e8ea635SAtari911                    '/\$_(GET|POST|REQUEST|SERVER|FILES|COOKIE|SESSION)\s*\[/i',
60917e8ea635SAtari911                    '/`[^`]+`/',  // Backtick execution
60927e8ea635SAtari911                ];
60937e8ea635SAtari911
60947e8ea635SAtari911                foreach ($dangerousPatterns as $pattern) {
60957e8ea635SAtari911                    if (preg_match($pattern, $configContent)) {
60967e8ea635SAtari911                        echo json_encode([
60977e8ea635SAtari911                            'success' => false,
6098da206178SAtari911                            'message' => 'Invalid config file: contains prohibited code patterns'
60997e8ea635SAtari911                        ]);
61007e8ea635SAtari911                        exit;
61017e8ea635SAtari911                    }
61027e8ea635SAtari911                }
61037e8ea635SAtari911
61047e8ea635SAtari911                // Verify it looks like a valid config (has return array structure)
6105da206178SAtari911                // Accept both "return [" and "return array(" syntax
610622228b0eSAtari911                if (!preg_match('/return\s*(\[|array\s*\()/', $configContent)) {
61077e8ea635SAtari911                    echo json_encode([
61087e8ea635SAtari911                        'success' => false,
6109da206178SAtari911                        'message' => 'Invalid config file: must contain a return array statement'
61101d05cddcSAtari911                    ]);
61111d05cddcSAtari911                    exit;
61121d05cddcSAtari911                }
61131d05cddcSAtari911
61141d05cddcSAtari911                // Write to config file
6115*2866e827SAtari911                $configFile = $this->syncConfigPath();
61161d05cddcSAtari911
61171d05cddcSAtari911                // Backup existing config
61181d05cddcSAtari911                if (file_exists($configFile)) {
61191d05cddcSAtari911                    $backupFile = $configFile . '.backup.' . date('Y-m-d_H-i-s');
61201d05cddcSAtari911                    copy($configFile, $backupFile);
61211d05cddcSAtari911                }
61221d05cddcSAtari911
61231d05cddcSAtari911                // Write new config
61241d05cddcSAtari911                if (file_put_contents($configFile, $configContent) === false) {
61251d05cddcSAtari911                    echo json_encode([
61261d05cddcSAtari911                        'success' => false,
6127da206178SAtari911                        'message' => 'Failed to write config file'
61281d05cddcSAtari911                    ]);
61291d05cddcSAtari911                    exit;
61301d05cddcSAtari911                }
61311d05cddcSAtari911
61321d05cddcSAtari911                echo json_encode([
61331d05cddcSAtari911                    'success' => true,
6134da206178SAtari911                    'message' => 'Config imported successfully'
61351d05cddcSAtari911                ]);
61361d05cddcSAtari911                exit;
61371d05cddcSAtari911
61381d05cddcSAtari911            } catch (Exception $e) {
61391d05cddcSAtari911                echo json_encode([
61401d05cddcSAtari911                    'success' => false,
61411d05cddcSAtari911                    'message' => $e->getMessage()
61421d05cddcSAtari911                ]);
61431d05cddcSAtari911                exit;
61441d05cddcSAtari911            }
61451d05cddcSAtari911        }
61461d05cddcSAtari911    }
61471d05cddcSAtari911
61481d05cddcSAtari911    private function getEncryptionKey() {
61491d05cddcSAtari911        global $conf;
61501d05cddcSAtari911        // Use DokuWiki's secret as the base for encryption
61511d05cddcSAtari911        // This ensures the key is unique per installation
61521d05cddcSAtari911        return hash('sha256', $conf['secret'] . 'calendar_config_encryption', true);
61531d05cddcSAtari911    }
61541d05cddcSAtari911
61551d05cddcSAtari911    private function encryptData($data, $key) {
61561d05cddcSAtari911        // Use AES-256-CBC encryption
61571d05cddcSAtari911        $ivLength = openssl_cipher_iv_length('aes-256-cbc');
61581d05cddcSAtari911        $iv = openssl_random_pseudo_bytes($ivLength);
61591d05cddcSAtari911
61601d05cddcSAtari911        $encrypted = openssl_encrypt($data, 'aes-256-cbc', $key, 0, $iv);
61611d05cddcSAtari911
61621d05cddcSAtari911        // Combine IV and encrypted data, then base64 encode
61631d05cddcSAtari911        return base64_encode($iv . $encrypted);
61641d05cddcSAtari911    }
61651d05cddcSAtari911
61661d05cddcSAtari911    private function decryptData($encryptedData, $key) {
61671d05cddcSAtari911        // Decode base64
61681d05cddcSAtari911        $data = base64_decode($encryptedData);
61691d05cddcSAtari911
61701d05cddcSAtari911        if ($data === false) {
61711d05cddcSAtari911            return false;
61721d05cddcSAtari911        }
61731d05cddcSAtari911
61741d05cddcSAtari911        // Extract IV and encrypted content
61751d05cddcSAtari911        $ivLength = openssl_cipher_iv_length('aes-256-cbc');
61761d05cddcSAtari911        $iv = substr($data, 0, $ivLength);
61771d05cddcSAtari911        $encrypted = substr($data, $ivLength);
61781d05cddcSAtari911
61791d05cddcSAtari911        // Decrypt
61801d05cddcSAtari911        $decrypted = openssl_decrypt($encrypted, 'aes-256-cbc', $key, 0, $iv);
61811d05cddcSAtari911
61821d05cddcSAtari911        return $decrypted;
61831d05cddcSAtari911    }
61841d05cddcSAtari911
61851d05cddcSAtari911    private function clearLogFile() {
61861d05cddcSAtari911        global $INPUT;
61871d05cddcSAtari911
61881d05cddcSAtari911        if ($INPUT->str('call') === 'ajax') {
61891d05cddcSAtari911            header('Content-Type: application/json');
61901d05cddcSAtari911
619196df7d3eSAtari911            $logFile = $this->getSyncLogPath();
61921d05cddcSAtari911
619396df7d3eSAtari911            // Check if file exists
619496df7d3eSAtari911            if (!file_exists($logFile)) {
619596df7d3eSAtari911                // Try to create empty file
619696df7d3eSAtari911                if (@touch($logFile)) {
6197da206178SAtari911                    echo json_encode(['success' => true, 'message' => 'Log file created']);
619896df7d3eSAtari911                } else {
6199da206178SAtari911                    echo json_encode(['success' => false, 'message' => 'Log file does not exist and cannot be created: ' . $logFile]);
620096df7d3eSAtari911                }
620196df7d3eSAtari911                exit;
620296df7d3eSAtari911            }
620396df7d3eSAtari911
620496df7d3eSAtari911            // Check if writable
620596df7d3eSAtari911            if (!is_writable($logFile)) {
6206da206178SAtari911                echo json_encode(['success' => false, 'message' => 'Log file not writable. Run: sudo chmod 666 ' . $logFile]);
620796df7d3eSAtari911                exit;
620896df7d3eSAtari911            }
620996df7d3eSAtari911
621096df7d3eSAtari911            // Try to clear it
621196df7d3eSAtari911            $result = file_put_contents($logFile, '');
621296df7d3eSAtari911            if ($result !== false) {
62131d05cddcSAtari911                echo json_encode(['success' => true]);
62141d05cddcSAtari911            } else {
6215da206178SAtari911                echo json_encode(['success' => false, 'message' => 'file_put_contents failed on: ' . $logFile]);
62161d05cddcSAtari911            }
62171d05cddcSAtari911            exit;
62181d05cddcSAtari911        }
62191d05cddcSAtari911    }
62201d05cddcSAtari911
62211d05cddcSAtari911    private function downloadLog() {
622296df7d3eSAtari911        $logFile = $this->getSyncLogPath();
62231d05cddcSAtari911
62241d05cddcSAtari911        if (file_exists($logFile)) {
62251d05cddcSAtari911            header('Content-Type: text/plain');
62261d05cddcSAtari911            header('Content-Disposition: attachment; filename="calendar-sync-' . date('Y-m-d-His') . '.log"');
62271d05cddcSAtari911            readfile($logFile);
62281d05cddcSAtari911            exit;
62291d05cddcSAtari911        } else {
6230da206178SAtari911            echo 'No log file found';
62311d05cddcSAtari911            exit;
62321d05cddcSAtari911        }
62331d05cddcSAtari911    }
62341d05cddcSAtari911
62351d05cddcSAtari911    private function getEventStatistics() {
62361d05cddcSAtari911        $stats = [
62371d05cddcSAtari911            'total_events' => 0,
62381d05cddcSAtari911            'total_namespaces' => 0,
62391d05cddcSAtari911            'total_files' => 0,
62401d05cddcSAtari911            'total_recurring' => 0,
62411d05cddcSAtari911            'by_namespace' => [],
62421d05cddcSAtari911            'last_scan' => ''
62431d05cddcSAtari911        ];
62441d05cddcSAtari911
6245*2866e827SAtari911        $metaDir = $this->metaDir();
6246*2866e827SAtari911        $cacheFile = $this->metaDir() . 'calendar/.event_stats_cache';
62471d05cddcSAtari911
62481d05cddcSAtari911        // Check if we have cached stats (less than 5 minutes old)
62491d05cddcSAtari911        if (file_exists($cacheFile)) {
62501d05cddcSAtari911            $cacheData = json_decode(file_get_contents($cacheFile), true);
62511d05cddcSAtari911            if ($cacheData && (time() - $cacheData['timestamp']) < 300) {
62521d05cddcSAtari911                return $cacheData['stats'];
62531d05cddcSAtari911            }
62541d05cddcSAtari911        }
62551d05cddcSAtari911
62561d05cddcSAtari911        // Scan for events
62571d05cddcSAtari911        $this->scanDirectoryForStats($metaDir, '', $stats);
62581d05cddcSAtari911
62591d05cddcSAtari911        // Count recurring events
62601d05cddcSAtari911        $recurringEvents = $this->findRecurringEvents();
62611d05cddcSAtari911        $stats['total_recurring'] = count($recurringEvents);
62621d05cddcSAtari911
62631d05cddcSAtari911        $stats['total_namespaces'] = count($stats['by_namespace']);
62641d05cddcSAtari911        $stats['last_scan'] = date('Y-m-d H:i:s');
62651d05cddcSAtari911
62661d05cddcSAtari911        // Cache the results
62671d05cddcSAtari911        file_put_contents($cacheFile, json_encode([
62681d05cddcSAtari911            'timestamp' => time(),
62691d05cddcSAtari911            'stats' => $stats
62701d05cddcSAtari911        ]));
62711d05cddcSAtari911
62721d05cddcSAtari911        return $stats;
62731d05cddcSAtari911    }
62741d05cddcSAtari911
62751d05cddcSAtari911    private function scanDirectoryForStats($dir, $namespace, &$stats) {
62761d05cddcSAtari911        if (!is_dir($dir)) return;
62771d05cddcSAtari911
62781d05cddcSAtari911        $items = scandir($dir);
62791d05cddcSAtari911        foreach ($items as $item) {
62801d05cddcSAtari911            if ($item === '.' || $item === '..') continue;
62811d05cddcSAtari911
62821d05cddcSAtari911            $path = $dir . $item;
62831d05cddcSAtari911
62841d05cddcSAtari911            // Check if this is a calendar directory
62851d05cddcSAtari911            if ($item === 'calendar' && is_dir($path)) {
62861d05cddcSAtari911                $jsonFiles = glob($path . '/*.json');
62871d05cddcSAtari911                $eventCount = 0;
62881d05cddcSAtari911
62891d05cddcSAtari911                foreach ($jsonFiles as $file) {
62901d05cddcSAtari911                    $stats['total_files']++;
6291815440faSAtari911                    $data = CalendarFileHandler::readJson($file);
62921d05cddcSAtari911                    if ($data) {
629396df7d3eSAtari911                        foreach ($data as $dateKey => $dateEvents) {
629496df7d3eSAtari911                            // Skip non-date keys (like "mapping" or other metadata)
629596df7d3eSAtari911                            if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $dateKey)) continue;
629696df7d3eSAtari911
629796df7d3eSAtari911                            if (is_array($dateEvents)) {
629896df7d3eSAtari911                                // Only count events that have id and title
629996df7d3eSAtari911                                foreach ($dateEvents as $event) {
630096df7d3eSAtari911                                    if (is_array($event) && !empty($event['id']) && !empty($event['title'])) {
630196df7d3eSAtari911                                        $eventCount++;
630296df7d3eSAtari911                                    }
630396df7d3eSAtari911                                }
630496df7d3eSAtari911                            }
63051d05cddcSAtari911                        }
63061d05cddcSAtari911                    }
63071d05cddcSAtari911                }
63081d05cddcSAtari911
63091d05cddcSAtari911                $stats['total_events'] += $eventCount;
63101d05cddcSAtari911
63111d05cddcSAtari911                if ($eventCount > 0) {
63121d05cddcSAtari911                    $stats['by_namespace'][$namespace] = [
63131d05cddcSAtari911                        'events' => $eventCount,
63141d05cddcSAtari911                        'files' => count($jsonFiles)
63151d05cddcSAtari911                    ];
63161d05cddcSAtari911                }
63171d05cddcSAtari911            } elseif (is_dir($path)) {
63181d05cddcSAtari911                // Recurse into subdirectories
63191d05cddcSAtari911                $newNamespace = $namespace ? $namespace . ':' . $item : $item;
63201d05cddcSAtari911                $this->scanDirectoryForStats($path . '/', $newNamespace, $stats);
63211d05cddcSAtari911            }
63221d05cddcSAtari911        }
63231d05cddcSAtari911    }
63241d05cddcSAtari911
63251d05cddcSAtari911    private function rescanEvents() {
63261d05cddcSAtari911        // Clear the cache to force a rescan
63279ccd446eSAtari911        $this->clearStatsCache();
63281d05cddcSAtari911
63291d05cddcSAtari911        // Get fresh statistics
63301d05cddcSAtari911        $stats = $this->getEventStatistics();
63311d05cddcSAtari911
63321d05cddcSAtari911        // Build absolute redirect URL
63331d05cddcSAtari911        $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';
63341d05cddcSAtari911
63351d05cddcSAtari911        // Redirect with success message using absolute URL
63361d05cddcSAtari911        header('Location: ' . $redirectUrl, true, 303);
63371d05cddcSAtari911        exit;
63381d05cddcSAtari911    }
63391d05cddcSAtari911
63401d05cddcSAtari911    private function exportAllEvents() {
6341*2866e827SAtari911        $metaDir = $this->metaDir();
63421d05cddcSAtari911        $allEvents = [];
63431d05cddcSAtari911
63441d05cddcSAtari911        // Collect all events
63451d05cddcSAtari911        $this->collectAllEvents($metaDir, '', $allEvents);
63461d05cddcSAtari911
63471d05cddcSAtari911        // Create export package
63489ccd446eSAtari911        // Get current version
63499ccd446eSAtari911        $pluginInfo = DOKU_PLUGIN . 'calendar/plugin.info.txt';
63509ccd446eSAtari911        $info = file_exists($pluginInfo) ? confToHash($pluginInfo) : [];
63519ccd446eSAtari911        $currentVersion = isset($info['version']) ? trim($info['version']) : 'unknown';
63529ccd446eSAtari911
63531d05cddcSAtari911        $exportData = [
63541d05cddcSAtari911            'export_date' => date('Y-m-d H:i:s'),
63559ccd446eSAtari911            'version' => $currentVersion,
63561d05cddcSAtari911            'total_events' => 0,
63571d05cddcSAtari911            'namespaces' => []
63581d05cddcSAtari911        ];
63591d05cddcSAtari911
63601d05cddcSAtari911        foreach ($allEvents as $namespace => $files) {
63611d05cddcSAtari911            $exportData['namespaces'][$namespace] = [];
63621d05cddcSAtari911            foreach ($files as $filename => $events) {
63631d05cddcSAtari911                $exportData['namespaces'][$namespace][$filename] = $events;
63641d05cddcSAtari911                foreach ($events as $dateEvents) {
636596df7d3eSAtari911                    if (is_array($dateEvents)) {
63661d05cddcSAtari911                        $exportData['total_events'] += count($dateEvents);
63671d05cddcSAtari911                    }
63681d05cddcSAtari911                }
63691d05cddcSAtari911            }
637096df7d3eSAtari911        }
63711d05cddcSAtari911
63721d05cddcSAtari911        // Send as download
63731d05cddcSAtari911        header('Content-Type: application/json');
63741d05cddcSAtari911        header('Content-Disposition: attachment; filename="calendar-events-export-' . date('Y-m-d-His') . '.json"');
63751d05cddcSAtari911        echo json_encode($exportData, JSON_PRETTY_PRINT);
63761d05cddcSAtari911        exit;
63771d05cddcSAtari911    }
63781d05cddcSAtari911
63791d05cddcSAtari911    private function collectAllEvents($dir, $namespace, &$allEvents) {
63801d05cddcSAtari911        if (!is_dir($dir)) return;
63811d05cddcSAtari911
63821d05cddcSAtari911        $items = scandir($dir);
63831d05cddcSAtari911        foreach ($items as $item) {
63841d05cddcSAtari911            if ($item === '.' || $item === '..') continue;
63851d05cddcSAtari911
63861d05cddcSAtari911            $path = $dir . $item;
63871d05cddcSAtari911
63881d05cddcSAtari911            // Check if this is a calendar directory
63891d05cddcSAtari911            if ($item === 'calendar' && is_dir($path)) {
63901d05cddcSAtari911                $jsonFiles = glob($path . '/*.json');
63911d05cddcSAtari911
63921d05cddcSAtari911                if (!isset($allEvents[$namespace])) {
63931d05cddcSAtari911                    $allEvents[$namespace] = [];
63941d05cddcSAtari911                }
63951d05cddcSAtari911
63961d05cddcSAtari911                foreach ($jsonFiles as $file) {
63971d05cddcSAtari911                    $filename = basename($file);
6398815440faSAtari911                    $data = CalendarFileHandler::readJson($file);
63991d05cddcSAtari911                    if ($data) {
64001d05cddcSAtari911                        $allEvents[$namespace][$filename] = $data;
64011d05cddcSAtari911                    }
64021d05cddcSAtari911                }
64031d05cddcSAtari911            } elseif (is_dir($path)) {
64041d05cddcSAtari911                // Recurse into subdirectories
64051d05cddcSAtari911                $newNamespace = $namespace ? $namespace . ':' . $item : $item;
64061d05cddcSAtari911                $this->collectAllEvents($path . '/', $newNamespace, $allEvents);
64071d05cddcSAtari911            }
64081d05cddcSAtari911        }
64091d05cddcSAtari911    }
64101d05cddcSAtari911
64111d05cddcSAtari911    private function importAllEvents() {
64121d05cddcSAtari911        global $INPUT;
64131d05cddcSAtari911
64141d05cddcSAtari911        if (!isset($_FILES['import_file'])) {
64151d05cddcSAtari911            $redirectUrl = DOKU_URL . 'doku.php?do=admin&page=calendar&tab=manage&msg=' . urlencode('No file uploaded') . '&msgtype=error';
64161d05cddcSAtari911            header('Location: ' . $redirectUrl, true, 303);
64171d05cddcSAtari911            exit;
64181d05cddcSAtari911        }
64191d05cddcSAtari911
64201d05cddcSAtari911        $file = $_FILES['import_file'];
64211d05cddcSAtari911
64221d05cddcSAtari911        if ($file['error'] !== UPLOAD_ERR_OK) {
64231d05cddcSAtari911            $redirectUrl = DOKU_URL . 'doku.php?do=admin&page=calendar&tab=manage&msg=' . urlencode('Upload error: ' . $file['error']) . '&msgtype=error';
64241d05cddcSAtari911            header('Location: ' . $redirectUrl, true, 303);
64251d05cddcSAtari911            exit;
64261d05cddcSAtari911        }
64271d05cddcSAtari911
64281d05cddcSAtari911        // Read and decode the import file
64291d05cddcSAtari911        $importData = json_decode(file_get_contents($file['tmp_name']), true);
64301d05cddcSAtari911
64311d05cddcSAtari911        if (!$importData || !isset($importData['namespaces'])) {
64321d05cddcSAtari911            $redirectUrl = DOKU_URL . 'doku.php?do=admin&page=calendar&tab=manage&msg=' . urlencode('Invalid import file format') . '&msgtype=error';
64331d05cddcSAtari911            header('Location: ' . $redirectUrl, true, 303);
64341d05cddcSAtari911            exit;
64351d05cddcSAtari911        }
64361d05cddcSAtari911
64371d05cddcSAtari911        $importedCount = 0;
64381d05cddcSAtari911        $mergedCount = 0;
64391d05cddcSAtari911
64401d05cddcSAtari911        // Import events
64411d05cddcSAtari911        foreach ($importData['namespaces'] as $namespace => $files) {
6442*2866e827SAtari911            $metaDir = $this->metaDir();
64431d05cddcSAtari911            if ($namespace) {
64441d05cddcSAtari911                $metaDir .= str_replace(':', '/', $namespace) . '/';
64451d05cddcSAtari911            }
64461d05cddcSAtari911            $calendarDir = $metaDir . 'calendar/';
64471d05cddcSAtari911
64481d05cddcSAtari911            // Create directory if needed
64491d05cddcSAtari911            if (!is_dir($calendarDir)) {
64501d05cddcSAtari911                mkdir($calendarDir, 0755, true);
64511d05cddcSAtari911            }
64521d05cddcSAtari911
64531d05cddcSAtari911            foreach ($files as $filename => $events) {
64541d05cddcSAtari911                $targetFile = $calendarDir . $filename;
64551d05cddcSAtari911
64561d05cddcSAtari911                // If file exists, merge events
64571d05cddcSAtari911                if (file_exists($targetFile)) {
64581d05cddcSAtari911                    $existing = json_decode(file_get_contents($targetFile), true);
64591d05cddcSAtari911                    if ($existing) {
64601d05cddcSAtari911                        foreach ($events as $date => $dateEvents) {
64611d05cddcSAtari911                            if (!isset($existing[$date])) {
64621d05cddcSAtari911                                $existing[$date] = [];
64631d05cddcSAtari911                            }
64641d05cddcSAtari911                            foreach ($dateEvents as $event) {
64651d05cddcSAtari911                                // Check if event with same ID exists
64661d05cddcSAtari911                                $found = false;
64671d05cddcSAtari911                                foreach ($existing[$date] as $existingEvent) {
64681d05cddcSAtari911                                    if ($existingEvent['id'] === $event['id']) {
64691d05cddcSAtari911                                        $found = true;
64701d05cddcSAtari911                                        break;
64711d05cddcSAtari911                                    }
64721d05cddcSAtari911                                }
64731d05cddcSAtari911                                if (!$found) {
64741d05cddcSAtari911                                    $existing[$date][] = $event;
64751d05cddcSAtari911                                    $importedCount++;
64761d05cddcSAtari911                                } else {
64771d05cddcSAtari911                                    $mergedCount++;
64781d05cddcSAtari911                                }
64791d05cddcSAtari911                            }
64801d05cddcSAtari911                        }
6481815440faSAtari911                        CalendarFileHandler::writeJson($targetFile, $existing);
64821d05cddcSAtari911                    }
64831d05cddcSAtari911                } else {
64841d05cddcSAtari911                    // New file
6485815440faSAtari911                    CalendarFileHandler::writeJson($targetFile, $events);
64861d05cddcSAtari911                    foreach ($events as $dateEvents) {
648796df7d3eSAtari911                        if (is_array($dateEvents)) {
64881d05cddcSAtari911                            $importedCount += count($dateEvents);
64891d05cddcSAtari911                        }
64901d05cddcSAtari911                    }
64911d05cddcSAtari911                }
64921d05cddcSAtari911            }
649396df7d3eSAtari911        }
64941d05cddcSAtari911
64951d05cddcSAtari911        // Clear cache
64969ccd446eSAtari911        $this->clearStatsCache();
64971d05cddcSAtari911
6498da206178SAtari911        $message = "Import complete! Imported $importedCount new events";
64991d05cddcSAtari911        if ($mergedCount > 0) {
6500da206178SAtari911            $message .= ", skipped $mergedCount duplicates";
65011d05cddcSAtari911        }
65021d05cddcSAtari911
65031d05cddcSAtari911        $redirectUrl = DOKU_URL . 'doku.php?do=admin&page=calendar&tab=manage&msg=' . urlencode($message) . '&msgtype=success';
65041d05cddcSAtari911        header('Location: ' . $redirectUrl, true, 303);
65051d05cddcSAtari911        exit;
65061d05cddcSAtari911    }
65071d05cddcSAtari911
65081d05cddcSAtari911    private function previewCleanup() {
65091d05cddcSAtari911        global $INPUT;
65101d05cddcSAtari911
65111d05cddcSAtari911        $cleanupType = $INPUT->str('cleanup_type', 'age');
65121d05cddcSAtari911        $namespaceFilter = $INPUT->str('namespace_filter', '');
65131d05cddcSAtari911
65141d05cddcSAtari911        // Debug info
65151d05cddcSAtari911        $debug = [];
65161d05cddcSAtari911        $debug['cleanup_type'] = $cleanupType;
65171d05cddcSAtari911        $debug['namespace_filter'] = $namespaceFilter;
65181d05cddcSAtari911        $debug['age_value'] = $INPUT->int('age_value', 6);
65191d05cddcSAtari911        $debug['age_unit'] = $INPUT->str('age_unit', 'months');
65201d05cddcSAtari911        $debug['range_start'] = $INPUT->str('range_start', '');
65211d05cddcSAtari911        $debug['range_end'] = $INPUT->str('range_end', '');
65221d05cddcSAtari911        $debug['delete_completed'] = $INPUT->bool('delete_completed', false);
65231d05cddcSAtari911        $debug['delete_past'] = $INPUT->bool('delete_past', false);
65241d05cddcSAtari911
6525*2866e827SAtari911        $dataDir = $this->metaDir();
65261d05cddcSAtari911        $debug['data_dir'] = $dataDir;
65271d05cddcSAtari911        $debug['data_dir_exists'] = is_dir($dataDir);
65281d05cddcSAtari911
65291d05cddcSAtari911        $eventsToDelete = $this->findEventsToCleanup($cleanupType, $namespaceFilter);
65301d05cddcSAtari911
65311d05cddcSAtari911        // Merge with scan debug info
65321d05cddcSAtari911        if (isset($this->_cleanupDebug)) {
65331d05cddcSAtari911            $debug = array_merge($debug, $this->_cleanupDebug);
65341d05cddcSAtari911        }
65351d05cddcSAtari911
65361d05cddcSAtari911        // Return JSON for preview with debug info
65371d05cddcSAtari911        header('Content-Type: application/json');
65381d05cddcSAtari911        echo json_encode([
65391d05cddcSAtari911            'count' => count($eventsToDelete),
65401d05cddcSAtari911            'events' => array_slice($eventsToDelete, 0, 50), // Limit to 50 for preview
65411d05cddcSAtari911            'debug' => $debug
65421d05cddcSAtari911        ]);
65431d05cddcSAtari911        exit;
65441d05cddcSAtari911    }
65451d05cddcSAtari911
65461d05cddcSAtari911    private function cleanupEvents() {
65471d05cddcSAtari911        global $INPUT;
65481d05cddcSAtari911
65491d05cddcSAtari911        $cleanupType = $INPUT->str('cleanup_type', 'age');
65501d05cddcSAtari911        $namespaceFilter = $INPUT->str('namespace_filter', '');
65511d05cddcSAtari911
65521d05cddcSAtari911        // Create backup first
65531d05cddcSAtari911        $backupDir = DOKU_PLUGIN . 'calendar/backups/';
65541d05cddcSAtari911        if (!is_dir($backupDir)) {
65551d05cddcSAtari911            mkdir($backupDir, 0755, true);
65561d05cddcSAtari911        }
65571d05cddcSAtari911
65581d05cddcSAtari911        $backupFile = $backupDir . 'before-cleanup-' . date('Y-m-d-His') . '.zip';
65591d05cddcSAtari911        $this->createBackup($backupFile);
65601d05cddcSAtari911
65611d05cddcSAtari911        // Find events to delete
65621d05cddcSAtari911        $eventsToDelete = $this->findEventsToCleanup($cleanupType, $namespaceFilter);
65631d05cddcSAtari911        $deletedCount = 0;
65641d05cddcSAtari911
65651d05cddcSAtari911        // Group by file
65661d05cddcSAtari911        $fileGroups = [];
65671d05cddcSAtari911        foreach ($eventsToDelete as $evt) {
65681d05cddcSAtari911            $fileGroups[$evt['file']][] = $evt;
65691d05cddcSAtari911        }
65701d05cddcSAtari911
65711d05cddcSAtari911        // Delete from each file
65721d05cddcSAtari911        foreach ($fileGroups as $file => $events) {
65731d05cddcSAtari911            if (!file_exists($file)) continue;
65741d05cddcSAtari911
65751d05cddcSAtari911            $json = file_get_contents($file);
65761d05cddcSAtari911            $data = json_decode($json, true);
65771d05cddcSAtari911
65781d05cddcSAtari911            if (!$data) continue;
65791d05cddcSAtari911
65801d05cddcSAtari911            // Remove events
65811d05cddcSAtari911            foreach ($events as $evt) {
65821d05cddcSAtari911                if (isset($data[$evt['date']])) {
65831d05cddcSAtari911                    $data[$evt['date']] = array_filter($data[$evt['date']], function($e) use ($evt) {
65841d05cddcSAtari911                        return $e['id'] !== $evt['id'];
65851d05cddcSAtari911                    });
65861d05cddcSAtari911
65871d05cddcSAtari911                    // Remove date key if empty
65881d05cddcSAtari911                    if (empty($data[$evt['date']])) {
65891d05cddcSAtari911                        unset($data[$evt['date']]);
65901d05cddcSAtari911                    }
65911d05cddcSAtari911
65921d05cddcSAtari911                    $deletedCount++;
65931d05cddcSAtari911                }
65941d05cddcSAtari911            }
65951d05cddcSAtari911
65961d05cddcSAtari911            // Save file or delete if empty
65971d05cddcSAtari911            if (empty($data)) {
65981d05cddcSAtari911                unlink($file);
65991d05cddcSAtari911            } else {
6600815440faSAtari911                CalendarFileHandler::writeJson($file, $data);
66011d05cddcSAtari911            }
66021d05cddcSAtari911        }
66031d05cddcSAtari911
66041d05cddcSAtari911        // Clear cache
66059ccd446eSAtari911        $this->clearStatsCache();
66061d05cddcSAtari911
6607da206178SAtari911        $message = "Cleanup complete! Deleted $deletedCount event(s). Backup created: " . basename($backupFile);
66081d05cddcSAtari911        $redirectUrl = DOKU_URL . 'doku.php?do=admin&page=calendar&tab=manage&msg=' . urlencode($message) . '&msgtype=success';
66091d05cddcSAtari911        header('Location: ' . $redirectUrl, true, 303);
66101d05cddcSAtari911        exit;
66111d05cddcSAtari911    }
66121d05cddcSAtari911
66131d05cddcSAtari911    private function findEventsToCleanup($cleanupType, $namespaceFilter) {
66141d05cddcSAtari911        global $INPUT;
66151d05cddcSAtari911
66161d05cddcSAtari911        $eventsToDelete = [];
6617*2866e827SAtari911        $dataDir = $this->metaDir();
66181d05cddcSAtari911
66191d05cddcSAtari911        $debug = [];
66201d05cddcSAtari911        $debug['scanned_dirs'] = [];
66211d05cddcSAtari911        $debug['found_files'] = [];
66221d05cddcSAtari911
66231d05cddcSAtari911        // Calculate cutoff date for age-based cleanup
66241d05cddcSAtari911        $cutoffDate = null;
66251d05cddcSAtari911        if ($cleanupType === 'age') {
66261d05cddcSAtari911            $ageValue = $INPUT->int('age_value', 6);
66271d05cddcSAtari911            $ageUnit = $INPUT->str('age_unit', 'months');
66281d05cddcSAtari911
66291d05cddcSAtari911            if ($ageUnit === 'years') {
66301d05cddcSAtari911                $ageValue *= 12; // Convert to months
66311d05cddcSAtari911            }
66321d05cddcSAtari911
66331d05cddcSAtari911            $cutoffDate = date('Y-m-d', strtotime("-$ageValue months"));
66341d05cddcSAtari911            $debug['cutoff_date'] = $cutoffDate;
66351d05cddcSAtari911        }
66361d05cddcSAtari911
66371d05cddcSAtari911        // Get date range for range-based cleanup
66381d05cddcSAtari911        $rangeStart = $cleanupType === 'range' ? $INPUT->str('range_start', '') : null;
66391d05cddcSAtari911        $rangeEnd = $cleanupType === 'range' ? $INPUT->str('range_end', '') : null;
66401d05cddcSAtari911
66411d05cddcSAtari911        // Get status filters
66421d05cddcSAtari911        $deleteCompleted = $cleanupType === 'status' && $INPUT->bool('delete_completed', false);
66431d05cddcSAtari911        $deletePast = $cleanupType === 'status' && $INPUT->bool('delete_past', false);
66441d05cddcSAtari911
66451d05cddcSAtari911        // Check root calendar directory first (blank/default namespace)
66461d05cddcSAtari911        $rootCalendarDir = $dataDir . 'calendar';
66471d05cddcSAtari911        $debug['root_calendar_dir'] = $rootCalendarDir;
66481d05cddcSAtari911        $debug['root_exists'] = is_dir($rootCalendarDir);
66491d05cddcSAtari911
66501d05cddcSAtari911        if (is_dir($rootCalendarDir)) {
66511d05cddcSAtari911            if (!$namespaceFilter || $namespaceFilter === '' || $namespaceFilter === 'default') {
66521d05cddcSAtari911                $debug['scanned_dirs'][] = $rootCalendarDir;
66531d05cddcSAtari911                $files = glob($rootCalendarDir . '/*.json');
66541d05cddcSAtari911                $debug['found_files'] = array_merge($debug['found_files'], $files);
66551d05cddcSAtari911                $this->processCalendarFiles($rootCalendarDir, '', $eventsToDelete, $cleanupType, $cutoffDate, $rangeStart, $rangeEnd, $deleteCompleted, $deletePast);
66561d05cddcSAtari911            }
66571d05cddcSAtari911        }
66581d05cddcSAtari911
66591d05cddcSAtari911        // Scan all namespace directories
66601d05cddcSAtari911        $namespaceDirs = glob($dataDir . '*', GLOB_ONLYDIR);
66611d05cddcSAtari911        $debug['namespace_dirs_found'] = $namespaceDirs;
66621d05cddcSAtari911
66631d05cddcSAtari911        foreach ($namespaceDirs as $nsDir) {
66641d05cddcSAtari911            $namespace = basename($nsDir);
66651d05cddcSAtari911
66661d05cddcSAtari911            // Skip the root 'calendar' dir (already processed above)
66671d05cddcSAtari911            if ($namespace === 'calendar') continue;
66681d05cddcSAtari911
66691d05cddcSAtari911            // Check namespace filter
66701d05cddcSAtari911            if ($namespaceFilter && strpos($namespace, $namespaceFilter) === false) {
66711d05cddcSAtari911                continue;
66721d05cddcSAtari911            }
66731d05cddcSAtari911
66741d05cddcSAtari911            $calendarDir = $nsDir . '/calendar';
66751d05cddcSAtari911            $debug['checked_calendar_dirs'][] = $calendarDir;
66761d05cddcSAtari911
66771d05cddcSAtari911            if (!is_dir($calendarDir)) {
66781d05cddcSAtari911                $debug['missing_calendar_dirs'][] = $calendarDir;
66791d05cddcSAtari911                continue;
66801d05cddcSAtari911            }
66811d05cddcSAtari911
66821d05cddcSAtari911            $debug['scanned_dirs'][] = $calendarDir;
66831d05cddcSAtari911            $files = glob($calendarDir . '/*.json');
66841d05cddcSAtari911            $debug['found_files'] = array_merge($debug['found_files'], $files);
66851d05cddcSAtari911            $this->processCalendarFiles($calendarDir, $namespace, $eventsToDelete, $cleanupType, $cutoffDate, $rangeStart, $rangeEnd, $deleteCompleted, $deletePast);
66861d05cddcSAtari911        }
66871d05cddcSAtari911
66881d05cddcSAtari911        // Store debug info globally for preview
66891d05cddcSAtari911        $this->_cleanupDebug = $debug;
66901d05cddcSAtari911
66911d05cddcSAtari911        return $eventsToDelete;
66921d05cddcSAtari911    }
66931d05cddcSAtari911
66941d05cddcSAtari911    private function processCalendarFiles($calendarDir, $namespace, &$eventsToDelete, $cleanupType, $cutoffDate, $rangeStart, $rangeEnd, $deleteCompleted, $deletePast) {
66951d05cddcSAtari911        foreach (glob($calendarDir . '/*.json') as $file) {
66961d05cddcSAtari911            $json = file_get_contents($file);
66971d05cddcSAtari911            $data = json_decode($json, true);
66981d05cddcSAtari911
66991d05cddcSAtari911            if (!$data) continue;
67001d05cddcSAtari911
67011d05cddcSAtari911            foreach ($data as $date => $dateEvents) {
67021d05cddcSAtari911                foreach ($dateEvents as $event) {
67031d05cddcSAtari911                    $shouldDelete = false;
67041d05cddcSAtari911
67051d05cddcSAtari911                    // Age-based
67061d05cddcSAtari911                    if ($cleanupType === 'age' && $cutoffDate && $date < $cutoffDate) {
67071d05cddcSAtari911                        $shouldDelete = true;
67081d05cddcSAtari911                    }
67091d05cddcSAtari911
67101d05cddcSAtari911                    // Range-based
67111d05cddcSAtari911                    if ($cleanupType === 'range' && $rangeStart && $rangeEnd) {
67121d05cddcSAtari911                        if ($date >= $rangeStart && $date <= $rangeEnd) {
67131d05cddcSAtari911                            $shouldDelete = true;
67141d05cddcSAtari911                        }
67151d05cddcSAtari911                    }
67161d05cddcSAtari911
67171d05cddcSAtari911                    // Status-based
67181d05cddcSAtari911                    if ($cleanupType === 'status') {
67191d05cddcSAtari911                        $isTask = isset($event['isTask']) && $event['isTask'];
67201d05cddcSAtari911                        $isCompleted = isset($event['completed']) && $event['completed'];
67211d05cddcSAtari911                        $isPast = $date < date('Y-m-d');
67221d05cddcSAtari911
67231d05cddcSAtari911                        if ($deleteCompleted && $isTask && $isCompleted) {
67241d05cddcSAtari911                            $shouldDelete = true;
67251d05cddcSAtari911                        }
67261d05cddcSAtari911                        if ($deletePast && !$isTask && $isPast) {
67271d05cddcSAtari911                            $shouldDelete = true;
67281d05cddcSAtari911                        }
67291d05cddcSAtari911                    }
67301d05cddcSAtari911
67311d05cddcSAtari911                    if ($shouldDelete) {
67321d05cddcSAtari911                        $eventsToDelete[] = [
67331d05cddcSAtari911                            'id' => $event['id'],
67341d05cddcSAtari911                            'title' => $event['title'],
67351d05cddcSAtari911                            'date' => $date,
67361d05cddcSAtari911                            'namespace' => $namespace ?: 'default',
67371d05cddcSAtari911                            'file' => $file
67381d05cddcSAtari911                        ];
67391d05cddcSAtari911                    }
67401d05cddcSAtari911                }
67411d05cddcSAtari911            }
67421d05cddcSAtari911        }
67431d05cddcSAtari911    }
67449ccd446eSAtari911
67459ccd446eSAtari911    /**
6746815440faSAtari911     * Render Google Calendar Sync tab
6747815440faSAtari911     */
6748815440faSAtari911    private function renderGoogleSyncTab($colors = null) {
6749815440faSAtari911        global $INPUT;
6750815440faSAtari911
6751815440faSAtari911        if ($colors === null) {
6752815440faSAtari911            $colors = $this->getTemplateColors();
6753815440faSAtari911        }
6754815440faSAtari911
6755815440faSAtari911        // Load Google sync class
6756815440faSAtari911        require_once __DIR__ . '/classes/GoogleCalendarSync.php';
6757815440faSAtari911        $googleSync = new GoogleCalendarSync();
6758815440faSAtari911        $status = $googleSync->getStatus();
6759815440faSAtari911
6760815440faSAtari911        // Handle config save
6761815440faSAtari911        if ($INPUT->str('action') === 'save_google_config') {
6762815440faSAtari911            $clientId = $INPUT->str('google_client_id');
6763815440faSAtari911            $clientSecret = $INPUT->str('google_client_secret');
6764815440faSAtari911            $calendarId = $INPUT->str('google_calendar_id', 'primary');
6765815440faSAtari911
6766815440faSAtari911            if ($clientId && $clientSecret) {
6767815440faSAtari911                $googleSync->saveConfig($clientId, $clientSecret, $calendarId);
6768815440faSAtari911                echo '<div style="background:#d4edda;border:1px solid #c3e6cb;color:#155724;padding:12px;border-radius:6px;margin-bottom:20px;">✓ Google API credentials saved successfully!</div>';
6769815440faSAtari911                $status = $googleSync->getStatus(); // Refresh status
6770815440faSAtari911            }
6771815440faSAtari911        }
6772815440faSAtari911
6773815440faSAtari911        // Handle calendar selection
6774815440faSAtari911        if ($INPUT->str('action') === 'select_google_calendar') {
6775815440faSAtari911            $calendarId = $INPUT->str('selected_calendar');
6776815440faSAtari911            if ($calendarId) {
6777815440faSAtari911                $googleSync->setCalendarId($calendarId);
6778815440faSAtari911                echo '<div style="background:#d4edda;border:1px solid #c3e6cb;color:#155724;padding:12px;border-radius:6px;margin-bottom:20px;">✓ Calendar selected!</div>';
6779815440faSAtari911            }
6780815440faSAtari911        }
6781815440faSAtari911
6782815440faSAtari911        $accentColor = '#00cc07';
6783815440faSAtari911
6784815440faSAtari911        echo '<div style="max-width:800px;">';
6785815440faSAtari911        echo '<h2 style="color:' . $colors['text'] . ';margin-bottom:20px;">�� Google Calendar Sync</h2>';
6786815440faSAtari911
6787815440faSAtari911        // Status indicator
6788815440faSAtari911        $statusColor = $status['authenticated'] ? '#28a745' : ($status['configured'] ? '#ffc107' : '#dc3545');
6789815440faSAtari911        $statusText = $status['authenticated'] ? 'Connected' : ($status['configured'] ? 'Not Authenticated' : 'Not Configured');
6790815440faSAtari911        $statusIcon = $status['authenticated'] ? '✓' : ($status['configured'] ? '⚠' : '✕');
6791815440faSAtari911
6792815440faSAtari911        echo '<div style="background:' . $colors['bg'] . ';border:1px solid ' . $colors['border'] . ';border-radius:8px;padding:20px;margin-bottom:24px;">';
6793815440faSAtari911        echo '<div style="display:flex;align-items:center;gap:12px;margin-bottom:16px;">';
6794815440faSAtari911        echo '<span style="display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;background:' . $statusColor . ';color:white;border-radius:50%;font-weight:bold;">' . $statusIcon . '</span>';
6795815440faSAtari911        echo '<span style="font-size:18px;font-weight:600;color:' . $colors['text'] . ';">Status: ' . $statusText . '</span>';
6796815440faSAtari911        echo '</div>';
6797815440faSAtari911
6798815440faSAtari911        if ($status['authenticated']) {
6799815440faSAtari911            echo '<p style="color:' . $colors['textDim'] . ';margin:0;">Calendar: <strong>' . htmlspecialchars($status['calendar_id']) . '</strong></p>';
6800815440faSAtari911        }
6801815440faSAtari911        echo '</div>';
6802815440faSAtari911
6803815440faSAtari911        // Setup Instructions
6804815440faSAtari911        echo '<div style="background:' . $colors['bg'] . ';border:1px solid ' . $colors['border'] . ';border-radius:8px;padding:20px;margin-bottom:24px;">';
6805815440faSAtari911        echo '<h3 style="color:' . $colors['text'] . ';margin:0 0 16px 0;">Setup Instructions</h3>';
6806815440faSAtari911        echo '<ol style="color:' . $colors['textDim'] . ';margin:0;padding-left:20px;line-height:1.8;">';
6807815440faSAtari911        echo '<li>Go to <a href="https://console.cloud.google.com/" target="_blank" style="color:' . $accentColor . ';">Google Cloud Console</a></li>';
6808815440faSAtari911        echo '<li>Create a new project (or select existing)</li>';
6809815440faSAtari911        echo '<li>Enable the <strong>Google Calendar API</strong></li>';
6810815440faSAtari911        echo '<li>Go to Credentials → Create Credentials → OAuth 2.0 Client ID</li>';
6811815440faSAtari911        echo '<li>Application type: <strong>Web application</strong></li>';
6812815440faSAtari911        echo '<li>Add Authorized redirect URI: <code style="background:#f5f5f5;padding:2px 6px;border-radius:3px;">' . DOKU_URL . 'lib/exe/ajax.php?call=plugin_calendar&action=google_callback</code></li>';
6813815440faSAtari911        echo '<li>Copy Client ID and Client Secret below</li>';
6814815440faSAtari911        echo '</ol>';
6815815440faSAtari911        echo '</div>';
6816815440faSAtari911
6817815440faSAtari911        // Configuration Form
6818815440faSAtari911        echo '<div style="background:' . $colors['bg'] . ';border:1px solid ' . $colors['border'] . ';border-radius:8px;padding:20px;margin-bottom:24px;">';
6819815440faSAtari911        echo '<h3 style="color:' . $colors['text'] . ';margin:0 0 16px 0;">API Credentials</h3>';
6820815440faSAtari911
6821815440faSAtari911        echo '<form method="post" action="?do=admin&page=calendar&tab=google">';
6822*2866e827SAtari911        echo formSecurityToken(false);
6823815440faSAtari911        echo '<input type="hidden" name="action" value="save_google_config">';
6824815440faSAtari911
6825815440faSAtari911        echo '<div style="margin-bottom:16px;">';
6826815440faSAtari911        echo '<label style="display:block;font-weight:600;color:' . $colors['text'] . ';margin-bottom:6px;">Client ID</label>';
6827815440faSAtari911        echo '<input type="text" name="google_client_id" value="" placeholder="xxxx.apps.googleusercontent.com" style="width:100%;padding:10px 12px;border:1px solid ' . $colors['border'] . ';border-radius:6px;font-size:14px;background:' . $colors['inputBg'] . ';color:' . $colors['text'] . ';">';
6828815440faSAtari911        echo '<small style="color:' . $colors['textDim'] . ';">Leave blank to keep existing value</small>';
6829815440faSAtari911        echo '</div>';
6830815440faSAtari911
6831815440faSAtari911        echo '<div style="margin-bottom:16px;">';
6832815440faSAtari911        echo '<label style="display:block;font-weight:600;color:' . $colors['text'] . ';margin-bottom:6px;">Client Secret</label>';
6833815440faSAtari911        echo '<input type="password" name="google_client_secret" value="" placeholder="Enter client secret" style="width:100%;padding:10px 12px;border:1px solid ' . $colors['border'] . ';border-radius:6px;font-size:14px;background:' . $colors['inputBg'] . ';color:' . $colors['text'] . ';">';
6834815440faSAtari911        echo '<small style="color:' . $colors['textDim'] . ';">Leave blank to keep existing value</small>';
6835815440faSAtari911        echo '</div>';
6836815440faSAtari911
6837815440faSAtari911        echo '<button type="submit" style="background:' . $accentColor . ';color:white;border:none;padding:12px 24px;border-radius:6px;cursor:pointer;font-size:14px;font-weight:600;">Save Credentials</button>';
6838815440faSAtari911        echo '</form>';
6839815440faSAtari911        echo '</div>';
6840815440faSAtari911
6841815440faSAtari911        // Authentication Section
6842815440faSAtari911        if ($status['configured']) {
6843815440faSAtari911            echo '<div style="background:' . $colors['bg'] . ';border:1px solid ' . $colors['border'] . ';border-radius:8px;padding:20px;margin-bottom:24px;">';
6844815440faSAtari911            echo '<h3 style="color:' . $colors['text'] . ';margin:0 0 16px 0;">Authentication</h3>';
6845815440faSAtari911
6846815440faSAtari911            if ($status['authenticated']) {
6847815440faSAtari911                echo '<p style="color:#28a745;margin:0 0 16px 0;">✓ Connected to Google Calendar</p>';
6848815440faSAtari911                echo '<button onclick="googleDisconnect()" style="background:#dc3545;color:white;border:none;padding:10px 20px;border-radius:6px;cursor:pointer;font-size:14px;">Disconnect</button>';
6849815440faSAtari911            } else {
6850815440faSAtari911                echo '<p style="color:' . $colors['textDim'] . ';margin:0 0 16px 0;">Click below to authorize access to your Google Calendar.</p>';
6851815440faSAtari911                echo '<button onclick="googleConnect()" style="background:#4285f4;color:white;border:none;padding:12px 24px;border-radius:6px;cursor:pointer;font-size:14px;font-weight:600;">';
6852815440faSAtari911                echo '<span style="margin-right:8px;">��</span> Connect Google Calendar</button>';
6853815440faSAtari911            }
6854815440faSAtari911            echo '</div>';
6855815440faSAtari911        }
6856815440faSAtari911
6857815440faSAtari911        // Calendar Selection (if authenticated)
6858815440faSAtari911        if ($status['authenticated']) {
6859815440faSAtari911            echo '<div style="background:' . $colors['bg'] . ';border:1px solid ' . $colors['border'] . ';border-radius:8px;padding:20px;margin-bottom:24px;">';
6860815440faSAtari911            echo '<h3 style="color:' . $colors['text'] . ';margin:0 0 16px 0;">Select Calendar</h3>';
6861815440faSAtari911            echo '<div id="google-calendars-list">Loading calendars...</div>';
6862815440faSAtari911            echo '</div>';
6863815440faSAtari911
6864815440faSAtari911            // Import/Export Section
6865815440faSAtari911            echo '<div style="background:' . $colors['bg'] . ';border:1px solid ' . $colors['border'] . ';border-radius:8px;padding:20px;margin-bottom:24px;">';
6866815440faSAtari911            echo '<h3 style="color:' . $colors['text'] . ';margin:0 0 16px 0;">Sync Events</h3>';
6867815440faSAtari911
6868815440faSAtari911            echo '<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;">';
6869815440faSAtari911
6870815440faSAtari911            // Import section
6871815440faSAtari911            echo '<div style="padding:16px;border:1px solid ' . $colors['border'] . ';border-radius:8px;">';
6872815440faSAtari911            echo '<h4 style="color:' . $colors['text'] . ';margin:0 0 12px 0;">⬇️ Import from Google</h4>';
6873815440faSAtari911            echo '<p style="color:' . $colors['textDim'] . ';font-size:13px;margin:0 0 12px 0;">Import events from Google Calendar to DokuWiki.</p>';
6874815440faSAtari911            echo '<div style="margin-bottom:12px;">';
6875815440faSAtari911            echo '<label style="display:block;font-size:12px;color:' . $colors['textDim'] . ';margin-bottom:4px;">Namespace (optional)</label>';
6876815440faSAtari911            echo '<input type="text" id="import-namespace" placeholder="e.g. team:meetings" style="width:100%;padding:8px;border:1px solid ' . $colors['border'] . ';border-radius:4px;font-size:13px;">';
6877815440faSAtari911            echo '</div>';
6878815440faSAtari911            echo '<button onclick="googleImport()" style="background:' . $accentColor . ';color:white;border:none;padding:10px 16px;border-radius:6px;cursor:pointer;font-size:13px;width:100%;">Import Events</button>';
6879815440faSAtari911            echo '</div>';
6880815440faSAtari911
6881815440faSAtari911            // Export section
6882815440faSAtari911            echo '<div style="padding:16px;border:1px solid ' . $colors['border'] . ';border-radius:8px;">';
6883815440faSAtari911            echo '<h4 style="color:' . $colors['text'] . ';margin:0 0 12px 0;">⬆️ Export to Google</h4>';
6884815440faSAtari911            echo '<p style="color:' . $colors['textDim'] . ';font-size:13px;margin:0 0 12px 0;">Export events from DokuWiki to Google Calendar.</p>';
6885815440faSAtari911            echo '<div style="margin-bottom:12px;">';
6886815440faSAtari911            echo '<label style="display:block;font-size:12px;color:' . $colors['textDim'] . ';margin-bottom:4px;">Namespace (optional)</label>';
6887815440faSAtari911            echo '<input type="text" id="export-namespace" placeholder="e.g. team:meetings" style="width:100%;padding:8px;border:1px solid ' . $colors['border'] . ';border-radius:4px;font-size:13px;">';
6888815440faSAtari911            echo '</div>';
6889815440faSAtari911            echo '<button onclick="googleExport()" style="background:#17a2b8;color:white;border:none;padding:10px 16px;border-radius:6px;cursor:pointer;font-size:13px;width:100%;">Export Events</button>';
6890815440faSAtari911            echo '</div>';
6891815440faSAtari911
6892815440faSAtari911            echo '</div>';
6893815440faSAtari911
6894815440faSAtari911            echo '<div id="sync-result" style="margin-top:16px;"></div>';
6895815440faSAtari911            echo '</div>';
6896815440faSAtari911        }
6897815440faSAtari911
6898815440faSAtari911        echo '</div>'; // End max-width container
6899815440faSAtari911
6900815440faSAtari911        // JavaScript for Google sync
6901815440faSAtari911        echo '<script>
6902815440faSAtari911        var DOKU_BASE = "' . DOKU_BASE . '";
6903815440faSAtari911
6904815440faSAtari911        // Listen for OAuth callback
6905815440faSAtari911        window.addEventListener("message", function(e) {
6906815440faSAtari911            if (e.data && e.data.type === "google_auth_complete") {
6907815440faSAtari911                if (e.data.success) {
6908815440faSAtari911                    location.reload();
6909815440faSAtari911                }
6910815440faSAtari911            }
6911815440faSAtari911        });
6912815440faSAtari911
6913815440faSAtari911        function googleConnect() {
6914815440faSAtari911            fetch(DOKU_BASE + "lib/exe/ajax.php?call=plugin_calendar&action=google_auth_url")
6915815440faSAtari911            .then(r => r.json())
6916815440faSAtari911            .then(data => {
6917815440faSAtari911                if (data.success && data.url) {
6918815440faSAtari911                    // Open auth window
6919815440faSAtari911                    var w = 600, h = 700;
6920815440faSAtari911                    var left = (screen.width - w) / 2;
6921815440faSAtari911                    var top = (screen.height - h) / 2;
6922815440faSAtari911                    window.open(data.url, "google_auth", "width=" + w + ",height=" + h + ",left=" + left + ",top=" + top);
6923815440faSAtari911                } else {
6924815440faSAtari911                    alert("Error: " + (data.error || "Could not get auth URL"));
6925815440faSAtari911                }
6926815440faSAtari911            })
6927815440faSAtari911            .catch(err => alert("Error: " + err.message));
6928815440faSAtari911        }
6929815440faSAtari911
6930815440faSAtari911        function googleDisconnect() {
6931815440faSAtari911            if (!confirm("Disconnect from Google Calendar?")) return;
6932815440faSAtari911
6933815440faSAtari911            fetch(DOKU_BASE + "lib/exe/ajax.php?call=plugin_calendar&action=google_disconnect", {
6934815440faSAtari911                method: "POST",
6935815440faSAtari911                headers: {"Content-Type": "application/x-www-form-urlencoded"}
6936815440faSAtari911            })
6937815440faSAtari911            .then(r => r.json())
6938815440faSAtari911            .then(data => {
6939815440faSAtari911                if (data.success) {
6940815440faSAtari911                    location.reload();
6941815440faSAtari911                }
6942815440faSAtari911            });
6943815440faSAtari911        }
6944815440faSAtari911
6945815440faSAtari911        function loadGoogleCalendars() {
6946815440faSAtari911            var container = document.getElementById("google-calendars-list");
6947815440faSAtari911            if (!container) return;
6948815440faSAtari911
6949815440faSAtari911            fetch(DOKU_BASE + "lib/exe/ajax.php?call=plugin_calendar&action=google_calendars")
6950815440faSAtari911            .then(r => r.json())
6951815440faSAtari911            .then(data => {
6952815440faSAtari911                if (data.success && data.calendars) {
6953815440faSAtari911                    var html = "<form method=\"post\" action=\"?do=admin&page=calendar&tab=google\">";
6954*2866e827SAtari911                    html += "<input type=\"hidden\" name=\"sectok\" value=\"" + JSINFO.sectok + "\">";
6955815440faSAtari911                    html += "<input type=\"hidden\" name=\"action\" value=\"select_google_calendar\">";
6956815440faSAtari911                    html += "<select name=\"selected_calendar\" style=\"width:100%;padding:10px;border:1px solid #ddd;border-radius:6px;margin-bottom:12px;\">";
6957815440faSAtari911
6958815440faSAtari911                    data.calendars.forEach(function(cal) {
6959815440faSAtari911                        var selected = cal.primary ? " selected" : "";
6960815440faSAtari911                        html += "<option value=\"" + cal.id + "\"" + selected + ">" + cal.summary;
6961815440faSAtari911                        if (cal.primary) html += " (Primary)";
6962815440faSAtari911                        html += "</option>";
6963815440faSAtari911                    });
6964815440faSAtari911
6965815440faSAtari911                    html += "</select>";
6966815440faSAtari911                    html += "<button type=\"submit\" style=\"background:#6c757d;color:white;border:none;padding:10px 20px;border-radius:6px;cursor:pointer;\">Select Calendar</button>";
6967815440faSAtari911                    html += "</form>";
6968815440faSAtari911
6969815440faSAtari911                    container.innerHTML = html;
6970815440faSAtari911                } else {
6971815440faSAtari911                    container.innerHTML = "<p style=\"color:#dc3545;\">Error loading calendars: " + (data.error || "Unknown error") + "</p>";
6972815440faSAtari911                }
6973815440faSAtari911            })
6974815440faSAtari911            .catch(err => {
6975815440faSAtari911                container.innerHTML = "<p style=\"color:#dc3545;\">Error: " + err.message + "</p>";
6976815440faSAtari911            });
6977815440faSAtari911        }
6978815440faSAtari911
6979815440faSAtari911        function googleImport() {
6980815440faSAtari911            var namespace = document.getElementById("import-namespace").value;
6981815440faSAtari911            var resultDiv = document.getElementById("sync-result");
6982815440faSAtari911
6983815440faSAtari911            resultDiv.innerHTML = "<p style=\"color:#666;\">⏳ Importing events...</p>";
6984815440faSAtari911
6985815440faSAtari911            var params = new URLSearchParams({
6986815440faSAtari911                call: "plugin_calendar",
6987815440faSAtari911                action: "google_import",
6988815440faSAtari911                namespace: namespace
6989815440faSAtari911            });
6990815440faSAtari911
6991815440faSAtari911            fetch(DOKU_BASE + "lib/exe/ajax.php", {
6992815440faSAtari911                method: "POST",
6993815440faSAtari911                headers: {"Content-Type": "application/x-www-form-urlencoded"},
6994815440faSAtari911                body: params.toString()
6995815440faSAtari911            })
6996815440faSAtari911            .then(r => r.json())
6997815440faSAtari911            .then(data => {
6998815440faSAtari911                if (data.success) {
6999815440faSAtari911                    resultDiv.innerHTML = "<div style=\"background:#d4edda;border:1px solid #c3e6cb;color:#155724;padding:12px;border-radius:6px;\">✓ Imported " + data.imported + " events, " + data.skipped + " skipped (duplicates)</div>";
7000815440faSAtari911                } else {
7001815440faSAtari911                    resultDiv.innerHTML = "<div style=\"background:#f8d7da;border:1px solid #f5c6cb;color:#721c24;padding:12px;border-radius:6px;\">Error: " + data.error + "</div>";
7002815440faSAtari911                }
7003815440faSAtari911            })
7004815440faSAtari911            .catch(err => {
7005815440faSAtari911                resultDiv.innerHTML = "<div style=\"background:#f8d7da;border:1px solid #f5c6cb;color:#721c24;padding:12px;border-radius:6px;\">Error: " + err.message + "</div>";
7006815440faSAtari911            });
7007815440faSAtari911        }
7008815440faSAtari911
7009815440faSAtari911        function googleExport() {
7010815440faSAtari911            var namespace = document.getElementById("export-namespace").value;
7011815440faSAtari911            var resultDiv = document.getElementById("sync-result");
7012815440faSAtari911
7013815440faSAtari911            resultDiv.innerHTML = "<p style=\"color:#666;\">⏳ Exporting events...</p>";
7014815440faSAtari911
7015815440faSAtari911            var params = new URLSearchParams({
7016815440faSAtari911                call: "plugin_calendar",
7017815440faSAtari911                action: "google_export",
7018815440faSAtari911                namespace: namespace
7019815440faSAtari911            });
7020815440faSAtari911
7021815440faSAtari911            fetch(DOKU_BASE + "lib/exe/ajax.php", {
7022815440faSAtari911                method: "POST",
7023815440faSAtari911                headers: {"Content-Type": "application/x-www-form-urlencoded"},
7024815440faSAtari911                body: params.toString()
7025815440faSAtari911            })
7026815440faSAtari911            .then(r => r.json())
7027815440faSAtari911            .then(data => {
7028815440faSAtari911                if (data.success) {
7029815440faSAtari911                    resultDiv.innerHTML = "<div style=\"background:#d4edda;border:1px solid #c3e6cb;color:#155724;padding:12px;border-radius:6px;\">✓ Exported " + data.exported + " events, " + data.skipped + " skipped</div>";
7030815440faSAtari911                } else {
7031815440faSAtari911                    resultDiv.innerHTML = "<div style=\"background:#f8d7da;border:1px solid #f5c6cb;color:#721c24;padding:12px;border-radius:6px;\">Error: " + data.error + "</div>";
7032815440faSAtari911                }
7033815440faSAtari911            })
7034815440faSAtari911            .catch(err => {
7035815440faSAtari911                resultDiv.innerHTML = "<div style=\"background:#f8d7da;border:1px solid #f5c6cb;color:#721c24;padding:12px;border-radius:6px;\">Error: " + err.message + "</div>";
7036815440faSAtari911            });
7037815440faSAtari911        }
7038815440faSAtari911
7039815440faSAtari911        // Load calendars on page load if authenticated
7040815440faSAtari911        ' . ($status['authenticated'] ? 'loadGoogleCalendars();' : '') . '
7041815440faSAtari911        </script>';
7042815440faSAtari911    }
7043815440faSAtari911
7044815440faSAtari911    /**
70459ccd446eSAtari911     * Render Themes tab for sidebar widget theme selection
70469ccd446eSAtari911     */
70479ccd446eSAtari911    private function renderThemesTab($colors = null) {
70489ccd446eSAtari911        global $INPUT;
70499ccd446eSAtari911
70509ccd446eSAtari911        // Use defaults if not provided
70519ccd446eSAtari911        if ($colors === null) {
70529ccd446eSAtari911            $colors = $this->getTemplateColors();
70539ccd446eSAtari911        }
70549ccd446eSAtari911
70559ccd446eSAtari911        // Handle theme save
70569ccd446eSAtari911        if ($INPUT->str('action') === 'save_theme') {
70579ccd446eSAtari911            $theme = $INPUT->str('theme', 'matrix');
70589ccd446eSAtari911            $weekStart = $INPUT->str('week_start', 'monday');
705996df7d3eSAtari911            $itineraryCollapsed = $INPUT->str('itinerary_collapsed', 'no');
70609ccd446eSAtari911            $this->saveSidebarTheme($theme);
70619ccd446eSAtari911            $this->saveWeekStartDay($weekStart);
706296df7d3eSAtari911            $this->saveItineraryCollapsed($itineraryCollapsed === 'yes');
706364a96c92SAtari911            $searchDefault = $INPUT->str('search_default', 'month');
706464a96c92SAtari911            $this->saveSearchDefault($searchDefault);
70659ccd446eSAtari911            echo '<div style="background:#d4edda; border:1px solid #c3e6cb; color:#155724; padding:12px; border-radius:4px; margin-bottom:20px;">';
7066da206178SAtari911            echo '✓ Theme and settings saved successfully! Refresh any page with the sidebar to see changes.';
70679ccd446eSAtari911            echo '</div>';
70689ccd446eSAtari911        }
70699ccd446eSAtari911
70709ccd446eSAtari911        $currentTheme = $this->getSidebarTheme();
70719ccd446eSAtari911        $currentWeekStart = $this->getWeekStartDay();
707296df7d3eSAtari911        $currentItineraryCollapsed = $this->getItineraryCollapsed();
707364a96c92SAtari911        $currentSearchDefault = $this->getSearchDefault();
70749ccd446eSAtari911
7075da206178SAtari911        echo '<h2 style="margin:0 0 20px 0; color:' . $colors['text'] . ';">�� Sidebar Widget Settings</h2>';
7076da206178SAtari911        echo '<p style="color:' . $colors['text'] . '; margin-bottom:20px;">Customize the appearance and behavior of the sidebar calendar widget.</p>';
70779ccd446eSAtari911
70789ccd446eSAtari911        echo '<form method="post" action="?do=admin&page=calendar&tab=themes">';
7079*2866e827SAtari911        echo formSecurityToken(false);
70809ccd446eSAtari911        echo '<input type="hidden" name="action" value="save_theme">';
70819ccd446eSAtari911
70829ccd446eSAtari911        // Week Start Day Section
70839ccd446eSAtari911        echo '<div style="background:' . $colors['bg'] . '; border:1px solid ' . $colors['border'] . '; border-radius:6px; padding:20px; margin-bottom:30px;">';
7084da206178SAtari911        echo '<h3 style="margin:0 0 15px 0; color:' . $colors['text'] . '; font-size:16px;">�� Week Start Day</h3>';
7085da206178SAtari911        echo '<p style="color:' . $colors['text'] . '; margin-bottom:15px; font-size:13px;">Choose which day the week calendar grid starts with:</p>';
70869ccd446eSAtari911
70879ccd446eSAtari911        echo '<div style="display:flex; gap:15px;">';
70889ccd446eSAtari911        echo '<label style="flex:1; padding:12px; border:2px solid ' . ($currentWeekStart === 'monday' ? '#00cc07' : $colors['border']) . '; border-radius:4px; background:' . ($currentWeekStart === 'monday' ? 'rgba(0, 204, 7, 0.05)' : $colors['bg']) . '; cursor:pointer; display:flex; align-items:center;">';
70899ccd446eSAtari911        echo '<input type="radio" name="week_start" value="monday" ' . ($currentWeekStart === 'monday' ? 'checked' : '') . ' style="margin-right:10px; width:18px; height:18px;">';
70909ccd446eSAtari911        echo '<div>';
7091da206178SAtari911        echo '<div style="font-weight:bold; color:' . $colors['text'] . '; margin-bottom:3px;">Monday</div>';
7092da206178SAtari911        echo '<div style="font-size:11px; color:' . $colors['text'] . ';">Week starts on Monday (ISO standard)</div>';
70939ccd446eSAtari911        echo '</div>';
70949ccd446eSAtari911        echo '</label>';
70959ccd446eSAtari911
70969ccd446eSAtari911        echo '<label style="flex:1; padding:12px; border:2px solid ' . ($currentWeekStart === 'sunday' ? '#00cc07' : $colors['border']) . '; border-radius:4px; background:' . ($currentWeekStart === 'sunday' ? 'rgba(0, 204, 7, 0.05)' : $colors['bg']) . '; cursor:pointer; display:flex; align-items:center;">';
70979ccd446eSAtari911        echo '<input type="radio" name="week_start" value="sunday" ' . ($currentWeekStart === 'sunday' ? 'checked' : '') . ' style="margin-right:10px; width:18px; height:18px;">';
70989ccd446eSAtari911        echo '<div>';
7099da206178SAtari911        echo '<div style="font-weight:bold; color:' . $colors['text'] . '; margin-bottom:3px;">Sunday</div>';
7100da206178SAtari911        echo '<div style="font-size:11px; color:' . $colors['text'] . ';">Week starts on Sunday (US/Canada standard)</div>';
71019ccd446eSAtari911        echo '</div>';
71029ccd446eSAtari911        echo '</label>';
71039ccd446eSAtari911        echo '</div>';
71049ccd446eSAtari911        echo '</div>';
71059ccd446eSAtari911
710696df7d3eSAtari911        // Itinerary Default State Section
710796df7d3eSAtari911        echo '<div style="background:' . $colors['bg'] . '; border:1px solid ' . $colors['border'] . '; border-radius:6px; padding:20px; margin-bottom:30px;">';
7108da206178SAtari911        echo '<h3 style="margin:0 0 15px 0; color:' . $colors['text'] . '; font-size:16px;">�� Itinerary Section</h3>';
7109da206178SAtari911        echo '<p style="color:' . $colors['text'] . '; margin-bottom:15px; font-size:13px;">Choose whether the Today/Tomorrow/Important Events sections are expanded or collapsed by default:</p>';
711096df7d3eSAtari911
711196df7d3eSAtari911        echo '<div style="display:flex; gap:15px;">';
711296df7d3eSAtari911        echo '<label style="flex:1; padding:12px; border:2px solid ' . (!$currentItineraryCollapsed ? '#00cc07' : $colors['border']) . '; border-radius:4px; background:' . (!$currentItineraryCollapsed ? 'rgba(0, 204, 7, 0.05)' : $colors['bg']) . '; cursor:pointer; display:flex; align-items:center;">';
711396df7d3eSAtari911        echo '<input type="radio" name="itinerary_collapsed" value="no" ' . (!$currentItineraryCollapsed ? 'checked' : '') . ' style="margin-right:10px; width:18px; height:18px;">';
711496df7d3eSAtari911        echo '<div>';
7115da206178SAtari911        echo '<div style="font-weight:bold; color:' . $colors['text'] . '; margin-bottom:3px;">Expanded</div>';
7116da206178SAtari911        echo '<div style="font-size:11px; color:' . $colors['text'] . ';">Show itinerary sections by default</div>';
711796df7d3eSAtari911        echo '</div>';
711896df7d3eSAtari911        echo '</label>';
711996df7d3eSAtari911
712096df7d3eSAtari911        echo '<label style="flex:1; padding:12px; border:2px solid ' . ($currentItineraryCollapsed ? '#00cc07' : $colors['border']) . '; border-radius:4px; background:' . ($currentItineraryCollapsed ? 'rgba(0, 204, 7, 0.05)' : $colors['bg']) . '; cursor:pointer; display:flex; align-items:center;">';
712196df7d3eSAtari911        echo '<input type="radio" name="itinerary_collapsed" value="yes" ' . ($currentItineraryCollapsed ? 'checked' : '') . ' style="margin-right:10px; width:18px; height:18px;">';
712296df7d3eSAtari911        echo '<div>';
7123da206178SAtari911        echo '<div style="font-weight:bold; color:' . $colors['text'] . '; margin-bottom:3px;">Collapsed</div>';
7124da206178SAtari911        echo '<div style="font-size:11px; color:' . $colors['text'] . ';">Hide itinerary sections by default (click bar to expand)</div>';
712596df7d3eSAtari911        echo '</div>';
712696df7d3eSAtari911        echo '</label>';
712796df7d3eSAtari911        echo '</div>';
712896df7d3eSAtari911        echo '</div>';
712996df7d3eSAtari911
713064a96c92SAtari911        // Default Search Scope Section
713164a96c92SAtari911        echo '<div style="background:' . $colors['bg'] . '; border:1px solid ' . $colors['border'] . '; border-radius:6px; padding:20px; margin-bottom:30px;">';
713264a96c92SAtari911        echo '<h3 style="margin:0 0 15px 0; color:' . $colors['text'] . '; font-size:16px;">�� ' . $this->getLang('search_default_title') . '</h3>';
713364a96c92SAtari911        echo '<p style="color:' . $colors['text'] . '; margin-bottom:15px; font-size:13px;">' . $this->getLang('search_default_desc') . '</p>';
713464a96c92SAtari911
713564a96c92SAtari911        echo '<div style="display:flex; gap:15px;">';
713664a96c92SAtari911        echo '<label style="flex:1; padding:12px; border:2px solid ' . ($currentSearchDefault === 'month' ? '#00cc07' : $colors['border']) . '; border-radius:4px; background:' . ($currentSearchDefault === 'month' ? 'rgba(0, 204, 7, 0.05)' : $colors['bg']) . '; cursor:pointer; display:flex; align-items:center;">';
713764a96c92SAtari911        echo '<input type="radio" name="search_default" value="month" ' . ($currentSearchDefault === 'month' ? 'checked' : '') . ' style="margin-right:10px; width:18px; height:18px;">';
713864a96c92SAtari911        echo '<div>';
713964a96c92SAtari911        echo '<div style="font-weight:bold; color:' . $colors['text'] . '; margin-bottom:3px;">�� ' . $this->getLang('search_default_month') . '</div>';
714064a96c92SAtari911        echo '<div style="font-size:11px; color:' . $colors['text'] . ';">' . $this->getLang('search_default_month_desc') . '</div>';
714164a96c92SAtari911        echo '</div>';
714264a96c92SAtari911        echo '</label>';
714364a96c92SAtari911
714464a96c92SAtari911        echo '<label style="flex:1; padding:12px; border:2px solid ' . ($currentSearchDefault === 'all' ? '#00cc07' : $colors['border']) . '; border-radius:4px; background:' . ($currentSearchDefault === 'all' ? 'rgba(0, 204, 7, 0.05)' : $colors['bg']) . '; cursor:pointer; display:flex; align-items:center;">';
714564a96c92SAtari911        echo '<input type="radio" name="search_default" value="all" ' . ($currentSearchDefault === 'all' ? 'checked' : '') . ' style="margin-right:10px; width:18px; height:18px;">';
714664a96c92SAtari911        echo '<div>';
714764a96c92SAtari911        echo '<div style="font-weight:bold; color:' . $colors['text'] . '; margin-bottom:3px;">�� ' . $this->getLang('search_default_all') . '</div>';
714864a96c92SAtari911        echo '<div style="font-size:11px; color:' . $colors['text'] . ';">' . $this->getLang('search_default_all_desc') . '</div>';
714964a96c92SAtari911        echo '</div>';
715064a96c92SAtari911        echo '</label>';
715164a96c92SAtari911        echo '</div>';
715264a96c92SAtari911        echo '</div>';
715364a96c92SAtari911
71549ccd446eSAtari911        // Visual Theme Section
7155da206178SAtari911        echo '<h3 style="margin:0 0 15px 0; color:' . $colors['text'] . '; font-size:16px;">�� Visual Theme</h3>';
71569ccd446eSAtari911
71579ccd446eSAtari911        // Matrix Theme
71589ccd446eSAtari911        echo '<div style="border:2px solid ' . ($currentTheme === 'matrix' ? '#00cc07' : $colors['border']) . '; border-radius:6px; padding:20px; margin-bottom:20px; background:' . ($currentTheme === 'matrix' ? 'rgba(0, 204, 7, 0.05)' : $colors['bg']) . ';">';
71599ccd446eSAtari911        echo '<label style="display:flex; align-items:center; cursor:pointer;">';
71609ccd446eSAtari911        echo '<input type="radio" name="theme" value="matrix" ' . ($currentTheme === 'matrix' ? 'checked' : '') . ' style="margin-right:12px; width:20px; height:20px;">';
71619ccd446eSAtari911        echo '<div style="flex:1;">';
7162815440faSAtari911        echo '<div style="font-size:18px; font-weight:bold; color:#00cc07; margin-bottom:8px;">�� Matrix</div>';
7163da206178SAtari911        echo '<div style="color:' . $colors['text'] . '; margin-bottom:12px;">Dark green theme with Matrix-style glow effects and neon accents</div>';
7164da206178SAtari911        echo '<div style="display:inline-block; background:#242424; border:2px solid #00cc07; padding:8px 12px; border-radius:4px; font-size:11px; font-family:monospace; color:#00cc07; box-shadow:0 0 10px rgba(0, 204, 7, 0.3);">Preview: Matrix Theme</div>';
71659ccd446eSAtari911        echo '</div>';
71669ccd446eSAtari911        echo '</label>';
71679ccd446eSAtari911        echo '</div>';
71689ccd446eSAtari911
71699ccd446eSAtari911        // Purple Theme
71709ccd446eSAtari911        echo '<div style="border:2px solid ' . ($currentTheme === 'purple' ? '#9b59b6' : $colors['border']) . '; border-radius:6px; padding:20px; margin-bottom:20px; background:' . ($currentTheme === 'purple' ? 'rgba(155, 89, 182, 0.05)' : $colors['bg']) . ';">';
71719ccd446eSAtari911        echo '<label style="display:flex; align-items:center; cursor:pointer;">';
71729ccd446eSAtari911        echo '<input type="radio" name="theme" value="purple" ' . ($currentTheme === 'purple' ? 'checked' : '') . ' style="margin-right:12px; width:20px; height:20px;">';
71739ccd446eSAtari911        echo '<div style="flex:1;">';
7174da206178SAtari911        echo '<div style="font-size:18px; font-weight:bold; color:#9b59b6; margin-bottom:8px;">�� Purple Dream</div>';
7175da206178SAtari911        echo '<div style="color:' . $colors['text'] . '; margin-bottom:12px;">Rich purple theme with elegant violet accents and soft glow</div>';
7176da206178SAtari911        echo '<div style="display:inline-block; background:#2a2030; border:2px solid #9b59b6; padding:8px 12px; border-radius:4px; font-size:11px; font-family:monospace; color:#b19cd9; box-shadow:0 0 10px rgba(155, 89, 182, 0.3);">Preview: Purple Theme</div>';
71779ccd446eSAtari911        echo '</div>';
71789ccd446eSAtari911        echo '</label>';
71799ccd446eSAtari911        echo '</div>';
71809ccd446eSAtari911
71819ccd446eSAtari911        // Professional Blue Theme
71829ccd446eSAtari911        echo '<div style="border:2px solid ' . ($currentTheme === 'professional' ? '#4a90e2' : $colors['border']) . '; border-radius:6px; padding:20px; margin-bottom:20px; background:' . ($currentTheme === 'professional' ? 'rgba(74, 144, 226, 0.05)' : $colors['bg']) . ';">';
71839ccd446eSAtari911        echo '<label style="display:flex; align-items:center; cursor:pointer;">';
71849ccd446eSAtari911        echo '<input type="radio" name="theme" value="professional" ' . ($currentTheme === 'professional' ? 'checked' : '') . ' style="margin-right:12px; width:20px; height:20px;">';
71859ccd446eSAtari911        echo '<div style="flex:1;">';
7186da206178SAtari911        echo '<div style="font-size:18px; font-weight:bold; color:#4a90e2; margin-bottom:8px;">�� Professional Blue</div>';
7187da206178SAtari911        echo '<div style="color:' . $colors['text'] . '; margin-bottom:12px;">Clean blue and grey theme with modern professional styling, no glow effects</div>';
7188da206178SAtari911        echo '<div style="display:inline-block; background:#f5f7fa; border:2px solid #4a90e2; padding:8px 12px; border-radius:4px; font-size:11px; font-family:sans-serif; color:#2c3e50; box-shadow:0 2px 4px rgba(0, 0, 0, 0.1);">Preview: Professional Theme</div>';
71899ccd446eSAtari911        echo '</div>';
71909ccd446eSAtari911        echo '</label>';
71919ccd446eSAtari911        echo '</div>';
71929ccd446eSAtari911
71939ccd446eSAtari911        // Pink Bling Theme
71949ccd446eSAtari911        echo '<div style="border:2px solid ' . ($currentTheme === 'pink' ? '#ff1493' : $colors['border']) . '; border-radius:6px; padding:20px; margin-bottom:20px; background:' . ($currentTheme === 'pink' ? 'rgba(255, 20, 147, 0.05)' : $colors['bg']) . ';">';
71959ccd446eSAtari911        echo '<label style="display:flex; align-items:center; cursor:pointer;">';
71969ccd446eSAtari911        echo '<input type="radio" name="theme" value="pink" ' . ($currentTheme === 'pink' ? 'checked' : '') . ' style="margin-right:12px; width:20px; height:20px;">';
71979ccd446eSAtari911        echo '<div style="flex:1;">';
7198da206178SAtari911        echo '<div style="font-size:18px; font-weight:bold; color:#ff1493; margin-bottom:8px;">�� Pink Bling</div>';
7199da206178SAtari911        echo '<div style="color:' . $colors['text'] . '; margin-bottom:12px;">Glamorous hot pink theme with maximum sparkle, hearts, and diamonds ✨</div>';
7200da206178SAtari911        echo '<div style="display:inline-block; background:#1a0d14; border:2px solid #ff1493; padding:8px 12px; border-radius:4px; font-size:11px; font-family:monospace; color:#ff69b4; box-shadow:0 0 12px rgba(255, 20, 147, 0.6);">Preview: Pink Bling Theme ��</div>';
72019ccd446eSAtari911        echo '</div>';
72029ccd446eSAtari911        echo '</label>';
72039ccd446eSAtari911        echo '</div>';
72049ccd446eSAtari911
72059ccd446eSAtari911        // Wiki Default Theme
72069ccd446eSAtari911        echo '<div style="border:2px solid ' . ($currentTheme === 'wiki' ? '#2b73b7' : $colors['border']) . '; border-radius:6px; padding:20px; margin-bottom:20px; background:' . ($currentTheme === 'wiki' ? 'rgba(43, 115, 183, 0.05)' : $colors['bg']) . ';">';
72079ccd446eSAtari911        echo '<label style="display:flex; align-items:center; cursor:pointer;">';
72089ccd446eSAtari911        echo '<input type="radio" name="theme" value="wiki" ' . ($currentTheme === 'wiki' ? 'checked' : '') . ' style="margin-right:12px; width:20px; height:20px;">';
72099ccd446eSAtari911        echo '<div style="flex:1;">';
7210da206178SAtari911        echo '<div style="font-size:18px; font-weight:bold; color:#2b73b7; margin-bottom:8px;">�� Wiki Default</div>';
7211da206178SAtari911        echo '<div style="color:' . $colors['text'] . '; margin-bottom:12px;">Automatically matches your DokuWiki template theme using CSS variables - adapts to light and dark themes</div>';
7212da206178SAtari911        echo '<div style="display:inline-block; background:#f5f5f5; border:2px solid #ccc; padding:8px 12px; border-radius:4px; font-size:11px; font-family:sans-serif; color:' . $colors['text'] . '; box-shadow:0 1px 2px rgba(0, 0, 0, 0.1);">Preview: Matches Your Wiki Theme</div>';
72139ccd446eSAtari911        echo '</div>';
72149ccd446eSAtari911        echo '</label>';
72159ccd446eSAtari911        echo '</div>';
72169ccd446eSAtari911
7217da206178SAtari911        echo '<button type="submit" style="background:#00cc07; color:#fff; border:none; padding:12px 24px; border-radius:4px; font-size:14px; font-weight:bold; cursor:pointer; box-shadow:0 2px 4px rgba(0,0,0,0.2);">Save Settings</button>';
72189ccd446eSAtari911        echo '</form>';
72199ccd446eSAtari911    }
72209ccd446eSAtari911
72219ccd446eSAtari911    /**
72229ccd446eSAtari911     * Get current sidebar theme
72239ccd446eSAtari911     */
72249ccd446eSAtari911    private function getSidebarTheme() {
7225*2866e827SAtari911        $configFile = $this->metaDir() . 'calendar_theme.txt';
72269ccd446eSAtari911        if (file_exists($configFile)) {
72279ccd446eSAtari911            return trim(file_get_contents($configFile));
72289ccd446eSAtari911        }
72299ccd446eSAtari911        return 'matrix'; // Default
72309ccd446eSAtari911    }
72319ccd446eSAtari911
72329ccd446eSAtari911    /**
72339ccd446eSAtari911     * Save sidebar theme
72349ccd446eSAtari911     */
72359ccd446eSAtari911    private function saveSidebarTheme($theme) {
7236*2866e827SAtari911        $configFile = $this->metaDir() . 'calendar_theme.txt';
72379ccd446eSAtari911        $validThemes = ['matrix', 'purple', 'professional', 'pink', 'wiki'];
72389ccd446eSAtari911
72399ccd446eSAtari911        if (in_array($theme, $validThemes)) {
72409ccd446eSAtari911            file_put_contents($configFile, $theme);
72419ccd446eSAtari911            return true;
72429ccd446eSAtari911        }
72439ccd446eSAtari911        return false;
72449ccd446eSAtari911    }
72459ccd446eSAtari911
72469ccd446eSAtari911    /**
72479ccd446eSAtari911     * Get week start day
72489ccd446eSAtari911     */
72499ccd446eSAtari911    private function getWeekStartDay() {
7250*2866e827SAtari911        $configFile = $this->metaDir() . 'calendar_week_start.txt';
72519ccd446eSAtari911        if (file_exists($configFile)) {
72529ccd446eSAtari911            $start = trim(file_get_contents($configFile));
72539ccd446eSAtari911            if (in_array($start, ['monday', 'sunday'])) {
72549ccd446eSAtari911                return $start;
72559ccd446eSAtari911            }
72569ccd446eSAtari911        }
72579ccd446eSAtari911        return 'sunday'; // Default to Sunday (US/Canada standard)
72589ccd446eSAtari911    }
72599ccd446eSAtari911
72609ccd446eSAtari911    /**
72619ccd446eSAtari911     * Save week start day
72629ccd446eSAtari911     */
72639ccd446eSAtari911    private function saveWeekStartDay($weekStart) {
7264*2866e827SAtari911        $configFile = $this->metaDir() . 'calendar_week_start.txt';
72659ccd446eSAtari911        $validStarts = ['monday', 'sunday'];
72669ccd446eSAtari911
72679ccd446eSAtari911        if (in_array($weekStart, $validStarts)) {
72689ccd446eSAtari911            file_put_contents($configFile, $weekStart);
72699ccd446eSAtari911            return true;
72709ccd446eSAtari911        }
72719ccd446eSAtari911        return false;
72729ccd446eSAtari911    }
72739ccd446eSAtari911
72749ccd446eSAtari911    /**
727596df7d3eSAtari911     * Get itinerary collapsed default state
727696df7d3eSAtari911     */
727796df7d3eSAtari911    private function getItineraryCollapsed() {
7278*2866e827SAtari911        $configFile = $this->metaDir() . 'calendar_itinerary_collapsed.txt';
727996df7d3eSAtari911        if (file_exists($configFile)) {
728096df7d3eSAtari911            return trim(file_get_contents($configFile)) === 'yes';
728196df7d3eSAtari911        }
728296df7d3eSAtari911        return false; // Default to expanded
728396df7d3eSAtari911    }
728496df7d3eSAtari911
728596df7d3eSAtari911    /**
728696df7d3eSAtari911     * Save itinerary collapsed default state
728796df7d3eSAtari911     */
728896df7d3eSAtari911    private function saveItineraryCollapsed($collapsed) {
7289*2866e827SAtari911        $configFile = $this->metaDir() . 'calendar_itinerary_collapsed.txt';
729096df7d3eSAtari911        file_put_contents($configFile, $collapsed ? 'yes' : 'no');
729196df7d3eSAtari911        return true;
729296df7d3eSAtari911    }
729396df7d3eSAtari911
729496df7d3eSAtari911    /**
729564a96c92SAtari911     * Get default search scope (month or all)
729664a96c92SAtari911     */
729764a96c92SAtari911    private function getSearchDefault() {
7298*2866e827SAtari911        $configFile = $this->metaDir() . 'calendar_search_default.txt';
729964a96c92SAtari911        if (file_exists($configFile)) {
730064a96c92SAtari911            $value = trim(file_get_contents($configFile));
730164a96c92SAtari911            if (in_array($value, ['month', 'all'])) {
730264a96c92SAtari911                return $value;
730364a96c92SAtari911            }
730464a96c92SAtari911        }
730564a96c92SAtari911        return 'month'; // Default to month search
730664a96c92SAtari911    }
730764a96c92SAtari911
730864a96c92SAtari911    /**
730964a96c92SAtari911     * Save default search scope
731064a96c92SAtari911     */
731164a96c92SAtari911    private function saveSearchDefault($scope) {
7312*2866e827SAtari911        $configFile = $this->metaDir() . 'calendar_search_default.txt';
731364a96c92SAtari911        $validScopes = ['month', 'all'];
731464a96c92SAtari911
731564a96c92SAtari911        if (in_array($scope, $validScopes)) {
731664a96c92SAtari911            file_put_contents($configFile, $scope);
731764a96c92SAtari911            return true;
731864a96c92SAtari911        }
731964a96c92SAtari911        return false;
732064a96c92SAtari911    }
732164a96c92SAtari911
732264a96c92SAtari911    /**
73239ccd446eSAtari911     * Get colors from DokuWiki template's style.ini file
73249ccd446eSAtari911     */
73259ccd446eSAtari911    private function getTemplateColors() {
73269ccd446eSAtari911        global $conf;
73279ccd446eSAtari911
73289ccd446eSAtari911        // Get current template name
73299ccd446eSAtari911        $template = $conf['template'];
73309ccd446eSAtari911
7331*2866e827SAtari911        // Try multiple possible locations for style.ini (farm-safe)
73329ccd446eSAtari911        $possiblePaths = [
73339ccd446eSAtari911            DOKU_INC . 'lib/tpl/' . $template . '/style.ini',
73349ccd446eSAtari911        ];
7335*2866e827SAtari911        // Add farm-specific conf override path if available
7336*2866e827SAtari911        if (!empty($conf['savedir'])) {
7337*2866e827SAtari911            array_unshift($possiblePaths, $conf['savedir'] . '/tpl/' . $template . '/style.ini');
7338*2866e827SAtari911        }
7339*2866e827SAtari911        array_unshift($possiblePaths, DOKU_INC . 'conf/tpl/' . $template . '/style.ini');
73409ccd446eSAtari911
73419ccd446eSAtari911        $styleIni = null;
73429ccd446eSAtari911        foreach ($possiblePaths as $path) {
73439ccd446eSAtari911            if (file_exists($path)) {
73449ccd446eSAtari911                $styleIni = parse_ini_file($path, true);
73459ccd446eSAtari911                break;
73469ccd446eSAtari911            }
73479ccd446eSAtari911        }
73489ccd446eSAtari911
73499ccd446eSAtari911        if (!$styleIni || !isset($styleIni['replacements'])) {
73509ccd446eSAtari911            // Return defaults
73519ccd446eSAtari911            return [
73529ccd446eSAtari911                'bg' => '#fff',
73539ccd446eSAtari911                'bg_alt' => '#e8e8e8',
73549ccd446eSAtari911                'text' => '#333',
73559ccd446eSAtari911                'border' => '#ccc',
73569ccd446eSAtari911                'link' => '#2b73b7',
73579ccd446eSAtari911            ];
73589ccd446eSAtari911        }
73599ccd446eSAtari911
73609ccd446eSAtari911        $r = $styleIni['replacements'];
73619ccd446eSAtari911
73629ccd446eSAtari911        return [
73639ccd446eSAtari911            'bg' => isset($r['__background__']) ? $r['__background__'] : '#fff',
73649ccd446eSAtari911            'bg_alt' => isset($r['__background_alt__']) ? $r['__background_alt__'] : '#e8e8e8',
73659ccd446eSAtari911            'text' => isset($r['__text__']) ? $r['__text__'] : '#333',
73669ccd446eSAtari911            'border' => isset($r['__border__']) ? $r['__border__'] : '#ccc',
73679ccd446eSAtari911            'link' => isset($r['__link__']) ? $r['__link__'] : '#2b73b7',
73689ccd446eSAtari911        ];
73699ccd446eSAtari911    }
73701d05cddcSAtari911}
7371