Jake Vanderwerf
9 days ago 47e77f9fac1155c536b2b87fec552c7fcce66fa6
inc/managers/AdminPages.php
@@ -1,7 +1,9 @@
<?php
namespace JVBase\managers;
use JVBase\utility\Features;
use JVBase\registrar\Registrar;
use JVBase\base\Site;
use WP_REST_Response;
if (!defined('ABSPATH')) {
    exit; // Exit if accessed directly
@@ -30,10 +32,10 @@
            'menu_title' => 'JakeVan',
            'capability' => 'manage_options',
            'menu_slug' => BASE . 'settings',
            'icon' => jvbCSSIcon('settings'),
            'icon' => jvbCSSIcon('gear-six'),
            'position' => 0
        ];
      $this->subpages = apply_filters('jvbAdminSubpages', []);
      $this->subpages = get_option(BASE.'adminSubpage', []);
//        delete_option(BASE.'admin_actions');
//        delete_option(BASE.'admin_subpages');
//        $this->getSubpages();
@@ -43,6 +45,12 @@
        // Hook into WordPress admin
        add_action('admin_menu', [$this, 'registerAdminPages']);
        add_action('admin_enqueue_scripts', [$this, 'enqueueAdminAssets']);
      add_filter(BASE.'admin_action_filter', [$this, 'handleCacheActions'], 10, 3);
      // Handle form submissions
      add_action('admin_init', [$this, 'handleAdminPageSubmission']);
      add_action('admin_notices', [$this, 'displayAdminNotices']);
    }
    /**
@@ -179,6 +187,14 @@
            BASE.'cache',
            [$this, 'renderCachePage']
        );
      add_submenu_page(
         $this->main_page['menu_slug'],
         'Icon Management',
         'Icons',
         'manage_options',
         BASE.'icons',
         [$this, 'renderIconsPage']
      );
//        $this->getSubpages();
        // Add registered subpages
@@ -202,6 +218,11 @@
        }
    }
   public function getMainConfig():array
   {
      return $this->main_page;
   }
    /**
     * Render the main settings page
     */
@@ -270,7 +291,7 @@
    protected function renderStatusItems():void
    {
        // Get queue stats
        $queue_status = JVB()->queue()->getQueueStatus();
        $queue_status = JVB()->queue()->getStatus();
      error_log('Queue Status: '.print_r($queue_status, true));
        // Other system checks
@@ -297,11 +318,11 @@
        </li>
        <li>
            <span class="status-label">Content Types:</span>
            <span class="status-value"><?= count(JVB_CONTENT); ?> registered</span>
            <span class="status-value"><?= count(Registrar::getRegistered('post')); ?> registered</span>
        </li>
        <li>
            <span class="status-label">Taxonomies:</span>
            <span class="status-value"><?= count(JVB_TAXONOMY); ?> registered</span>
            <span class="status-value"><?= count(Registrar::getRegistered('term')); ?> registered</span>
        </li>
        <?php
    }
@@ -333,10 +354,8 @@
        global $wpdb;
        $week_ago = date('Y-m-d H:i:s', strtotime('-7 days'));
      $content_types = [];
      foreach (JVB_CONTENT as $content => $config) {
         $content_types[jvbCheckBase($content)] = $config['plural'];
      }
      $content_types = array_map(function ($type) { return jvbCheckBase($type); },
         Registrar::getRegistered('post'));
        ?>
        <table class="jvb-content-table">
@@ -415,9 +434,9 @@
            if (current_user_can($action['capability'])) {
                ?>
                <a data-action="<?=$action['slug']?>" class="jvb-action">
                    <?= jvbIcon($action['icon']); ?>
                    <?= jvbDashIcon($action['icon']); ?>
                    <span class="jvb-link-title"><?= esc_html($action['label'])?></span>
                    <span class="loader"><?=jvbIcon('arrows-clockwise')?><?=jvbIcon('check')?></span>
                    <span class="loader"><?=jvbDashIcon('arrows-clockwise')?><?=jvbDashIcon('check')?></span>
                </a>
                <?php
            }
@@ -429,40 +448,41 @@
     *
     * @param string $hook Current admin page
     */
    public function enqueueAdminAssets(string $hook):void
    {
        // Check if we're on an Edmonton Ink admin page
        if (strpos($hook, BASE) === false) {
            return;
        }
   public function enqueueAdminAssets(string $hook):void
   {
      // More robust check for JVB admin pages
      if (strpos($hook, BASE) === false) {
         return;
      }
        // Enqueue admin styles
        wp_enqueue_style(
            'jvb-admin-styles',
            JVB_URL . 'assets/css/admin.css',
            [],
            '1.0.0'
        );
      // Enqueue admin styles
      wp_enqueue_style(
         'jvb-admin-styles',
         JVB_URL . 'assets/css/admin.css',
         [],
         '1.1'
      );
        // Enqueue admin scripts
        wp_enqueue_script(
            'jvb-admin-scripts',
            JVB_URL . 'assets/js/admin.js',
            [],
            '1.0.0',
            true
        );
      // Enqueue admin scripts - make sure jvb-auth is loaded first
      wp_enqueue_script(
         'jvb-admin-scripts',
         JVB_URL . 'assets/js/admin.js',
         ['jvb-auth'],
         '1.1',
         ['strategy' => 'defer', 'in_footer' => true]
      );
        wp_localize_script(
            'jvb-admin-scripts',
            'jvbSettings',
            [
                'api'    => rest_url('jvb/v1/admin-action'),
                'nonce'     => wp_create_nonce('wp_rest'),
                'action' => wp_create_nonce('itsme'),
            ]
        );
    }
      // Localize to jvb-admin-scripts as well for redundancy
      wp_localize_script(
         'jvb-auth',
         'jvbSettings',
         [
            'api'    => rest_url('jvb/v1/'),
            'nonce'  => wp_create_nonce('wp_rest'),
            'action' => wp_create_nonce('itsme'),
         ]
      );
   }
    /**
     * Create a custom SVG icon for the admin menu
@@ -472,7 +492,7 @@
     */
    protected function getIcon(string $icon = 'logo', bool $css = false): string
    {
        $svg = jvbIcon($icon, ['wrap' => false]);
        $svg = jvbDashIcon($icon, ['wrap' => false]);
        if ($css) {
            // For CSS, replace currentColor with brand color
            $svg = str_replace('currentColor', '#FF0080', $svg);
@@ -489,42 +509,346 @@
        return 'data:image/svg+xml;base64,' . base64_encode($svg);
    }
    public function renderCachePage()
    {
        $groups = get_option(BASE.'all_cache_groups', []);
   public function renderCachePage():void
   {
      $groups = Cache::getAllGroups();
        ?>
        <h1>Manage Cache</h1>
        <?php
        foreach ($groups as $group => $caches) {
            ?>
            <details>
                <summary class="row btw"><h2><?=$group?></h2></summary>
                <table>
                    <thead>
                        <tr>
                            <th scope="col"><input type="checkbox" name="select-all-<?=$group?>" id="select-all-<?=$group?>">
                                <label for="select-all-<?=$group?>">All</label></th>
                            <th scope="col">Cache Key</th>
                            <th scope="col">Actions</th>
                        </tr>
                    </thead>
                    <tbody>
                    <?php
                    foreach ($caches as $key) {
                        ?>
                        <tr>
                            <td><input type="checkbox" name="select-<?=$group?>-<?=$key?>" id="select-<?=$group?>-<?=$key?>"><label for="select-<?=$group?>-<?=$key?>"></label></td>
                            <td><?= $key ?></td>
                            <td><button type="button" data-action="flush-<?=$group?>-<?=$key?>"><?= jvbIcon('trash')?></button></td>
                        </tr>
                        <?php
                    }
                    ?>
                    </tbody>
                </table>
            </details>
            <?php
        }
    }
      // Separate by type
      $generic = [];
      $specific = [];
      foreach ($groups as $group => $data) {
         if ($this->isBoundToContentOrTaxonomy($group)) {
            $specific[$group] = $data;
         } else {
            $generic[$group] = $data;
         }
      }
      ?>
      <div class="wrap jvb-admin-wrap">
         <h1>Cache Management</h1>
         <div class="jvb-cache-actions">
            <button type="button"
                  class="button button-primary"
                  data-cache-action="flush-all">
               <?= jvbDashIcon('arrows-clockwise'); ?>
               Flush All Caches
            </button>
         </div>
         <div class="jvb-cache-section">
            <h2>Generic Caches &amp; Connections</h2>
            <table class="wp-list-table widefat fixed striped">
               <thead>
               <tr>
                  <th class="manage-column">Cache Group</th>
                  <th class="manage-column">Connected To</th>
                  <th class="manage-column">Actions</th>
               </tr>
               </thead>
               <tbody>
               <?php if (empty($generic)): ?>
                  <tr><td colspan="3">No generic caches registered</td></tr>
               <?php else: ?>
                  <?php foreach ($generic as $group => $data): ?>
                     <tr>
                        <td><strong><?= esc_html($group); ?></strong></td>
                        <td><?= $this->formatConnections($data); ?></td>
                        <td>
                           <button type="button"
                                 class="button"
                                 data-cache-action="flush-cache"
                                 data-group="<?= esc_attr($group); ?>">
                              <?= jvbDashIcon('trash'); ?> Flush
                           </button>
                        </td>
                     </tr>
                  <?php endforeach; ?>
               <?php endif; ?>
               </tbody>
            </table>
         </div>
         <details class="jvb-cache-section">
            <summary><h2>Content-Specific Caches</h2></summary>
            <table class="wp-list-table widefat fixed striped">
               <thead>
               <tr>
                  <th>Cache Group</th>
                  <th>Connected To</th>
                  <th>Actions</th>
               </tr>
               </thead>
               <tbody>
               <?php foreach ($specific as $group => $data): ?>
                  <tr>
                     <td><strong><?= esc_html($group); ?></strong></td>
                     <td><?= $this->formatConnections($data); ?></td>
                     <td>
                        <button type="button"
                              class="button"
                              data-cache-action="flush-cache"
                              data-group="<?= esc_attr($group); ?>">
                           <?= jvbDashIcon('trash'); ?> Flush
                        </button>
                     </td>
                  </tr>
               <?php endforeach; ?>
               </tbody>
            </table>
         </details>
      </div>
      <?php
   }
   protected function isBoundToContentOrTaxonomy(string $group): bool
   {
      $group = jvbNoBase($group);
      $registered = Registrar::getRegistered();
      foreach ($registered as $r) {
         if ($r === $group) {
            return true;
         }
      }
      return false;
   }
   protected function formatConnections(array $data): string
   {
      $parts = [];
      if (!empty($data['connects_to'])) {
         $targets = array_map(function($conn) {
            $flush_text = $conn['flush'] ? ' (flush all)' : '';
            return $conn['group'] . $flush_text;
         }, $data['connects_to']);
         $parts[] = '<strong>Invalidates:</strong> ' . implode(', ', $targets);
      }
      if (!empty($data['connected_from'])) {
         $sources = array_map(fn($conn) => $conn['group'], $data['connected_from']);
         $parts[] = '<strong>Invalidated by:</strong> ' . implode(', ', $sources);
      }
      return $parts ? implode('<br>', $parts) : 'No connections';
   }
   public function handleCacheActions($response, $request, $action):WP_REST_Response
   {
      if (!str_starts_with($action, 'flush-')) {
         return $response;
      }
      if ($action === 'flush-all') {
         wp_cache_flush();
         return new WP_REST_Response([
            'success' => true,
            'message' => 'All caches flushed successfully'
         ]);
      }
      if (str_starts_with($action, 'flush-cache')) {
         $group = $request->get_param('group');
         if (empty($group)) {
            return new WP_REST_Response([
               'success' => false,
               'message' => 'No cache group specified'
            ], 400);
         }
         $group = sanitize_text_field($request->get_param('group'));
         Cache::invalidateGroup($group);
         return new WP_REST_Response([
            'success' => true,
            'message' => "Cache group '{$group}' flushed successfully"
         ]);
      }
      return $response;
   }
   public function renderIconsPage():void
   {
      // Get current source from query param or default to 'icons'
      $current_source = $_GET['icon_source'] ?? 'icons';
      $current_source = sanitize_text_field($current_source);
      // Get all registered icon sources
      $all_sources = ['icons', 'forms', 'dash'];
      $icons = IconsManager::for($current_source);
      $versions = $icons->getVersionHistory();
      ?>
      <div class="wrap jvb-admin-wrap">
         <h1>Icon Management</h1>
         <!-- Source Selector -->
         <div class="jvb-icon-source-selector">
            <label for="icon-source-select">Icon Source:</label>
            <select id="icon-source-select"
                  onchange="window.location.href='<?= admin_url('admin.php?page=' . BASE . 'icons&icon_source='); ?>' + this.value">
               <?php foreach ($all_sources as $source): ?>
                  <option value="<?= esc_attr($source); ?>"
                     <?= selected($current_source, $source, false); ?>>
                     <?= esc_html(ucfirst($source)); ?>
                  </option>
               <?php endforeach; ?>
            </select>
         </div>
         <div class="jvb-icon-actions">
            <button type="button"
                  class="button button-primary"
                  data-icon-action="refresh-icons"
                  data-source="<?= esc_attr($current_source); ?>">
               <?= jvbDashIcon('arrows-clockwise'); ?>
               Force Refresh CSS
            </button>
            <button type="button"
                  class="button"
                  data-icon-action="merge-icon-versions"
                  data-source="<?= esc_attr($current_source); ?>"
                  id="merge-versions-btn"
                  disabled>
               <?= jvbDashIcon('git-merge'); ?>
               Merge Selected Versions
            </button>
         </div>
         <h2>Version History for <?= esc_html(ucfirst($current_source)); ?></h2>
         <table class="wp-list-table widefat fixed striped">
            <thead>
            <tr>
               <th class="check-column">
                  <input type="checkbox" id="select-all-versions">
                  <label for="select-all-versions" class="screen-reader-text">Select All</label>
               </th>
               <th>Date/Time</th>
               <th>Icon Count</th>
               <th>File Size</th>
               <th>Actions</th>
            </tr>
            </thead>
            <tbody>
            <?php if (empty($versions)): ?>
               <tr><td colspan="5">No version history available</td></tr>
            <?php else: ?>
               <?php foreach (array_reverse($versions) as $index => $version): ?>
                  <tr>
                     <th class="check-column">
                        <input type="checkbox"
                              name="version-select"
                              class="version-checkbox"
                              value="<?= esc_attr($version['timestamp']); ?>">
                     </th>
                     <td><?= esc_html(date('Y-m-d H:i:s', $version['timestamp'])); ?></td>
                     <td>
                        <?= esc_html($version['icon_count']); ?> icons
                        <button type="button"
                              class="button-link view-icon-list-btn"
                              data-timestamp="<?= esc_attr($version['timestamp']); ?>">
                           (view)
                        </button>
                     </td>
                     <td><?= esc_html($version['size_formatted']); ?></td>
                     <td>
                        <button type="button"
                              class="button restore-version-btn"
                              data-icon-action="restore-icon-version"
                              data-source="<?= esc_attr($current_source); ?>"
                              data-timestamp="<?= esc_attr($version['timestamp']); ?>">
                           <?= jvbDashIcon('arrow-counter-clockwise'); ?> Restore
                        </button>
                     </td>
                  </tr>
                  <tr id="icon-list-<?= esc_attr($version['timestamp']); ?>"
                     class="icon-list-row"
                     style="display: none;">
                     <td colspan="5">
                        <div class="icon-list-content">
                           <?php foreach ($version['iconList'] as $style => $icons): ?>
                              <strong><?= esc_html(ucfirst($style)); ?>:</strong>
                              <?= esc_html(implode(', ', $icons)); ?><br>
                           <?php endforeach; ?>
                        </div>
                     </td>
                  </tr>
               <?php endforeach; ?>
            <?php endif; ?>
            </tbody>
         </table>
      </div>
      <?php
   }
   public static function addSubpage(string $key, array $value):void
   {
      $option = get_option(BASE.'adminSubpage', []);
      if (empty($option) || !array_key_exists($key, $option)) {
         $option[$key] = $value;
         update_option(BASE.'adminSubpage', $option);
      }
   }
   /**
    * Handle admin page form submissions
    * Fires after WordPress admin_init
    */
   public function handleAdminPageSubmission(): void
   {
      // Only process on our admin pages
      if (!isset($_GET['page']) || strpos($_GET['page'], BASE) !== 0) {
         return;
      }
      // Check for form submission
      if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !isset($_POST['submit'])) {
         return;
      }
      // Verify nonce
      $nonce_field = BASE . 'admin_page_nonce';
      if (!isset($_POST['_wpnonce']) || !wp_verify_nonce($_POST['_wpnonce'], $nonce_field)) {
         add_action('admin_notices', function() {
            echo '<div class="notice notice-error"><p>Security check failed. Please try again.</p></div>';
         });
         return;
      }
      $page_slug = sanitize_text_field($_GET['page']);
      // Allow other classes to handle their page submissions
      $result = apply_filters('jvb_admin_page_submission', null, $page_slug, $_POST);
      // Store result in transient for after redirect
      if (is_array($result)) {
         set_transient(BASE . 'admin_notice_' . get_current_user_id(), $result, 30);
      }
      // Redirect to prevent form resubmission (POST-Redirect-GET pattern)
      wp_safe_redirect(add_query_arg(['page' => $page_slug], admin_url('admin.php')));
      exit;
   }
   /**
    * Display admin notices from form submissions
    */
   public function displayAdminNotices(): void
   {
      $notice = get_transient(BASE . 'admin_notice_' . get_current_user_id());
      if ($notice && is_array($notice)) {
         delete_transient(BASE . 'admin_notice_' . get_current_user_id());
         $type = $notice['success'] ? 'success' : 'error';
         $message = $notice['message'] ?? 'Settings saved.';
         echo '<div class="notice notice-' . esc_attr($type) . ' is-dismissible"><p>' . esc_html($message) . '</p></div>';
      }
   }
}