Jake Vanderwerf
6 hours ago 3baf3d2545ba6ece6b74a64c0def59bd0774cf54
=Laid the groundwork for an improved DashboardManager.php setup. Have to put it aside so I can get the dang Northeh done though.
4 files added
7 files modified
947 ■■■■■ changed files
JVBase.php 10 ●●●●● patch | view | raw | blame | history
base/options.php 3 ●●●● patch | view | raw | blame | history
inc/integrations/Integrations.php 16 ●●●●● patch | view | raw | blame | history
inc/managers/Dashboard/DashboardManager.php 659 ●●●●● patch | view | raw | blame | history
inc/managers/Dashboard/DashboardPage.php 159 ●●●●● patch | view | raw | blame | history
inc/managers/Dashboard/Section.php 55 ●●●●● patch | view | raw | blame | history
inc/managers/Dashboard/_setup.php 8 ●●●●● patch | view | raw | blame | history
inc/managers/DashboardManager.php 11 ●●●●● patch | view | raw | blame | history
inc/managers/RoleManager.php 16 ●●●● patch | view | raw | blame | history
inc/managers/_setup.php 8 ●●●● patch | view | raw | blame | history
jvb.php 2 ●●● patch | view | raw | blame | history
JVBase.php
@@ -11,7 +11,7 @@
use JVBase\managers\LoginManager;
use JVBase\managers\MagicLinkManager;
use JVBase\managers\queue\Queue;
use JVBase\managers\DashboardManager;
use JVBase\managers\Dashboard\DashboardManager;
use JVBase\managers\DirectoryManager;
use JVBase\managers\ReferralManager;
use JVBase\managers\RoleManager;
@@ -132,9 +132,6 @@
            $this->routes['referral'] = new ReferralRoutes();
        }
        if (Site::has('dashboard')) {
            $this->managers['dash'] = new DashboardManager();
        }
        if (Site::hasIntegration('square')) {
            $this->routes['square'] = new IntegrationsSquareRoutes();
@@ -198,6 +195,11 @@
            $this->routes['invites'] = new Invitations();
        }
        if (Site::has('dashboard')) {
            $this->managers['dash'] = new DashboardManager();
        }
        $this->setupIntegrations();
        add_action('wp_footer', [$this, 'additionalActions']);
base/options.php
@@ -61,7 +61,8 @@
                'open_to_public',
                [
                    'type'  => 'true_false',
                    'label' => 'Open to Public?'
                    'label'     => 'Open to Public?',
                    'default'   => 1,
                ]
            );
inc/integrations/Integrations.php
@@ -66,7 +66,7 @@
     * Used for UI rendering in admin interfaces
     */
    public string $title;  // Human-readable service name (e.g., 'Google My Business')
    public string $icon;   // Phosphoricons icon slug
    public string $icon = '';   // Phosphoricons icon slug
    /**
     * Credentials & State
@@ -2714,6 +2714,15 @@
        return $this->title;
    }
    public static function title():string
    {
        return (new static())->getTitle();
    }
    public static function icon():string
    {
        return (new static())->getIcon();
    }
    /*********************************************************************
        RENDERING
     *********************************************************************/
@@ -3535,4 +3544,9 @@
    {
        return [];
    }
    public function getIcon():string
    {
        return $this->icon;
    }
}
inc/managers/Dashboard/DashboardManager.php
New file
@@ -0,0 +1,659 @@
<?php
namespace JVBase\managers\Dashboard;
use JetBrains\PhpStorm\NoReturn;
use JVBase\forms\TaxonomySelector;
use JVBase\base\Site;
use JVBase\managers\Cache;
use JVBase\managers\RoleManager;use JVBase\registrar\Registrar;
use JVBase\ui\Navigation;
if (!defined('ABSPATH')) {
    exit;
}
class DashboardManager {
    protected array $pages = [];
    protected array $sections = [];
    protected Cache $cache;
    public function __construct() {
        $this->cache = Cache::for('dashboard')->connect('post');
        if (JVB_TESTING) {
            $this->cache->flush();
        }
        $this->buildPages();
        $this->buildSections();
        //Since this is loaded via JVBase, it's already running on init
        $this->registerDashboard();
        $this->registerHooks();
    }
    protected function registerDashboard():void
    {
        $dash = Registrar::forPost('dash', 'Dashboard', 'Dashboards');
        $dash->setIcon('gauge')
            ->make([
                'show_in_admin_bar' => false,
                'rewrite'       => [
                    'slug'          => 'dash',
                    'with_front'    => false,
                ],
                'supports'      => [ 'title', 'editor', 'custom-fields'],
                'hierarchical'  => true
            ])->setAll([
                'system'
            ]);
    }
    protected function registerHooks():void
    {
        add_action('template_redirect', [$this, 'handleRedirects']);
        add_action('template_include', [$this, 'dashboardTemplates']);
        add_action('admin_init', [$this, 'redirectFromAdmin']);
        add_action('wp_enqueue_scripts', [$this, 'dashboardScripts'], 50);
        add_filter('the_seo_framework_sitemap_exclude_ids', [$this, 'excludeDashboard'], 8, 1);
    }
        public function handleRedirects():void
        {
            if (!is_singular(BASE.'dash') && !is_post_type_archive(BASE.'dash') && !is_404()) {
                return;
            }
            if (!is_404()) {
                if (!is_user_logged_in()) {
                    error_log('Redirecting to login - user not logged in');
                    $this->redirectToLogin();
                } elseif (!isOurPeople() && !current_user_can('manage_options')) {
                    $this->redirectToHome();
                } else {
                    $page = $this->getCurrentPageSlug();
                    if (array_key_exists($page, ['', 'dash'])) {
                        return;
                    }
                    if (!array_key_exists($page, $this->pages)) {
                        error_log('[DashboardManager]::handleRedirect could not find page for '.$page);
                        return;
                    }
                    $permission = $this->pages[$page]->getPermission();
                    if (!empty($permission) && !current_user_can($permission)) {
                        error_log('[DashboardManager]::handleRedirect User cannot manage '.$page);
                        $this->redirectToDashboard();
                    }
                }
            }
            global $wp;
            if (str_starts_with($wp->request, 'dash/') || $wp->request === 'dash') {
                error_log('404 on dashboard URL, redirecting to dashboard home');
                $this->redirectToDashboard();
            }
        }
        public function dashboardTemplates(string $template):string
        {
            if (!is_singular(BASE.'dash') && !is_post_type_archive(BASE.'dash')) {
                return $template;
            }
            $page = $this->getCurrentPage();
            if (!empty($page->getIcon())) {
                add_filter('jvbLoadingIcon', $page->getIcon());
            }
            ob_start();
            jvbInlineStyles('nav');
            jvbInlineStyles('dash');
            jvbInlineStyles('forms');
            $this->renderHeader();
            $page->render();
            $this->renderFooter();
            return ob_get_clean();
        }
            protected function renderHeader():void
            {
                $page = $this->getCurrentPage();
                ?>
                <!DOCTYPE html>
            <html <?php language_attributes(); ?>>
                <head>
                    <title><?= $page->getTitle() . ' | Dashboard' ?></title>
                    <meta charset="<?php bloginfo('charset'); ?>">
                    <meta name="viewport" content="width=device-width, initial-scale=1.0">
                    <?php
                    foreach ($this->pages as $page) {
                        if (empty($page->getPermission()) || current_user_can($page->getPermission())) {
                            echo '<link rel="preconnect" href="'.$page->getURL().'">';
                        }
                    }
                    ?>
                    <link rel="preconnect" href="<?= get_home_url()?>"/>
                    <?php wp_head(); ?>
                </head>
            <body class="dashboard<?= ' '.$this->getCurrentPageSlug()?>">
                <?php jvbAccessibility();?>
                <header>
                    <?= jvbDarkModeToggle() ?>
                    <?php
                    $function = BASE.'render_core_site_logo';
                    if (function_exists($function)) {
                        echo $function([],'');
                    } else {
                        echo render_block( [
                            'blockName' => 'core/site-logo',
                            'attrs'     => [],
                        ]);
                    }
                    ?>
                    <nav>
                        <ul>
                            <?= jvbNotificationMenu() ?>
                            <?= jvbHelpMenu() ?>
                            <li><a href="<?=wp_logout_url(get_home_url())?>" title="Logout"><?=jvbIcon('sign-out')?></a></li>
                        </ul>
                    </nav>
                </header>
                <main><section class="replace">
                <?php
            }
            protected function renderFooter():void
            {
                 ?>
                </section>
                <?= $this->outputSidebarNavigation(); ?>
                <footer class="col">
                    <?= jvbLoadingScreen() ?>
                    <?= TaxonomySelector::outputSelectorModal() ?>
        <!--            <nav class="dashboard-nav">-->
                        <?php
        //                $current_page = $this->getCurrentPageSlug();
        //                $pages = $this->getUserAllowedPages()?:[];
        //                echo '<ul>';
        //                foreach ($pages as $slug => $page) {
        //                  $slug = $this->getSlug($slug, $page);
        //                  $icon = $this->getIcon($slug, $page);
        //                  // Add data-page attribute for the navigator
        //                    $active = ($current_page == $slug) ? ' class="current"' : '';
        //                    $current = ($current_page == $slug) ? ' aria-current="page"' : '';
        //
        //
        //                  $link = ($page === 'dash') ? '/'.$page : "/dash/$slug";
        //                    printf(
        //                        '<li%s><a href="%s"%s data-page="%s" data-dash title="%s">%s<span>%s</span></a></li>',
        //                        $active,
        //                        get_home_url(null, $link),
        //                        $current,
        //                        $slug,
        //                        $page,
        //                        jvbDashIcon($icon, ['title'=> $page]),
        //                        $page
        //                    );
        //                }
        //
        //                echo '</ul>';
                        ?>
        <!--            </nav>-->
                </footer>
                <?php
                do_action('jvbRenderDashboardSettings', $this->getCurrentPageSlug());
                ?>
                <?php wp_footer(); ?>
                </body>
                </html>
                <?php
            }
            protected function outputSidebarNavigation():string
            {
                $menu = new Navigation('sidebar');
                $menuClasses = ['left'];
                $itemClasses = ['col'];
                $menu->addClass('sidebar left')->hasToggle()->defaultMenuClasses($menuClasses);
                $menu->defaultItemClasses($itemClasses);
                //Add the Main Dashboard first
                $dashboard = $menu->addItem('Dashboard', jvbDashIcon($this->pages['dash']->getIcon()))
                    ->url($this->pages['dash']->getURL());
                foreach ($this->sections as $section) {
                    $pages = array_filter($this->pages, function($page) use ($section) {
                        $canDo = empty($page->getPermission())  ||  current_user_can($page->getPermission());
                        return $page->getSection() === $section && $canDo;
                    });
                    if (empty($pages)) {
                        continue;
                    }
                    $icon = !empty($section['icon']??'') ? jvbDashIcon($section['icon']) : null;
                    $section = $menu->addItem($section['title'], $icon)
                        ->submenu($section['slug'])
                        ->defaultMenuClasses($menuClasses)
                        ->defaultItemClasses($itemClasses);
                    foreach ($pages as $page) {
                        $icon = empty($page->getIcon()) ? null : jvbDashIcon($page->getIcon());
                        $item = $section->addItem($page->getTitle(), $icon)
                            ->url($page->getURL());
                        $registrar = Registrar::getInstance($page->getSlug());
                        if ($registrar && !empty($registrar->registrar->taxonomies)) {
                            $itemMenu = $item->submenu($page->getSlug());
                            foreach ($registrar->registrar->taxonomies as $taxonomy) {
                                $taxonomy = jvbNoBase($taxonomy);
                                if (!array_key_exists($taxonomy, $this->pages)) {
                                    error_log('Could not add Taxonomy subpage for '.$taxonomy);
                                    continue;
                                }
                                $icon = empty($this->pages[$taxonomy]->getIcon()) ? null : jvbDashIcon($this->pages[$taxonomy]->getIcon());
                                $itemMenu->addItem($this->pages[$taxonomy]->getTitle(),$icon)
                                    ->url($this->pages[$taxonomy]->getURL());
                            }
                        }
                    }
                }
                $pages = $this->getUserAllowedPages()?:[];
                //Dashboard
                    //Referrals
                $dashboard = $menu->addItem('Dashboard',jvbDashIcon('door'))
                    ->url($this->baseURL);
        //          ->submenu('dashboard')
        //          ->defaultMenuClasses($menuClasses)
        //          ->defaultItemClasses($itemClasses);
                //notifications
                if (in_array('Notifications', $pages)) {
                    $menu->addItem('Notifications',jvbDashIcon('bell'))
                        ->url($this->baseURL.'/notifications');
                }
                if (in_array('Referrals', $pages)) {
                    $menu->addItem('Referrals', jvbDashIcon('hand-heart'))
                        ->url($this->baseURL.'/referrals');
                }
                if (in_array('Favourites', $pages)) {
                    $menu->addItem('Favourites', jvbDashIcon('heart'))
                        ->url($this->baseURL.'/favourites');
                }
                //Content
                    //content types
                $all = array_merge(
                    Registrar::getRegistered('post'),
                    Registrar::withFeature('is_content', 'term')
                );
                $availableContent = [];
//              $availableContent = array_filter($pages, function($page, $key) use($all) {
//                  return !is_numeric($key) && in_array($key, $all) && JVB()->roles()->checkRole($this->user, $key);
//              }, ARRAY_FILTER_USE_BOTH);
                if (!empty ($availableContent)){
                    $content = $menu->addItem('Your Content', jvbDashIcon('book-bookmark'))
                        ->submenu('content')
                        ->defaultMenuClasses($menuClasses)
                        ->defaultItemClasses($itemClasses);
                    foreach ($availableContent as $slug => $page) {
                        $registrar = Registrar::getInstance($slug);
                        $item = $content->addItem($page, $registrar->getIcon())
                            ->url($this->baseURL.'/'.$slug);
                        if ($registrar->getType() === 'post') {
                            $taxonomies = $registrar->registrar->taxonomies;
                            if (!empty ($taxonomies)) {
                                //TODO: If we add a dedicated 'create item' page, remove this from the empty check
                                $itemMenu = $item->submenu($slug);
                                foreach ($taxonomies as $s) {
                                    $taxRegistrar = Registrar::getInstance($s);
                                    if ($taxRegistrar) {
                                        $itemMenu->addItem($taxRegistrar->getPlural(), $taxRegistrar->getIcon())
                                        ->url($this->baseURL.'/'.$s);
                                    }
                                }
                            }
                        }
                    }
                }
                //Taxonomies
                //Settings
                $settings = $menu->addItem('Settings', jvbDashIcon('faders'))
                    ->submenu('settings')
                    ->defaultItemClasses($itemClasses)
                    ->defaultMenuClasses($menuClasses);
                    //SEO
                    if (in_array('SEO', $pages)) {
                        $settings->addItem('SEO', jvbDashIcon('robot'))
                            ->url($this->baseURL.'/seo');
                    }
                    //Integrations
                    if (in_array('Integrations', $pages)) {
                        $settings->addItem('Integrations', jvbDashIcon('plugs-connected'))
                            ->url($this->baseURL.'/integrations');
                    }
                //Account
                $account = $menu->addItem('Account', jvbDashIcon('user-circle'))
                    ->url($this->baseURL.'/account')
                    ->submenu('account')
                    ->defaultMenuClasses($menuClasses)
                    ->defaultItemClasses($itemClasses);
                $account->addItem('Reset Password', jvbDashIcon('password'))
                    ->url($this->baseURL.'/reset-password');
                    //name + contact
                    //reset password
                    if (in_array('notifications', $pages)) {
                        $account->addItem('Permissions', jvbDashIcon('keyhole'))
                            ->url($this->baseURL.'/permissions');
                    }
                echo $menu->render();
            }
        public function redirectFromAdmin():void
        {
            //Skip if already processing a redirect
            if (defined('DOING_AJAX') && DOING_AJAX) {
                return;
            }
            if (current_user_can('manage_options')) {
                return;
            }
            //Redirect to custom dashboard
            if (is_user_logged_in() && isOurPeople()) {
                $this->redirectToDashboard();
            }
        }
        public function dashboardScripts():void
        {
        }
        public function excludeDashboard(array $IDs):array {
            $exclude = $this->cache->remember(
                'dashboardIDs',
                function() {
                    return get_posts([
                        'post_type' => BASE.'dash',
                        'posts_per_page' => -1,
                        'fields' => 'ids',
                    ]);
                });
            if (!empty($exclude)) {
                $IDs = array_merge($IDs, $exclude);
            }
            return $IDs;
        }
    private function buildPages():void
    {
        $this->addPage('dash', 'Dashboard','door');
        $this->buildContentPages();
        $this->buildReferrals();
        $this->buildMembership();
        $this->buildFavourites();
        $this->buildKarma();
        $this->buildNotifications();
        $this->buildIntegrations();
        $this->buildSettingsPages();
        $this->buildAccountPages();
    }
        private function buildContentPages():void
        {
            $content = Registrar::getRegistered('post');
            foreach ($content as $c) {
                $registrar = Registrar::getInstance($c);
                $page = $this->addPage($registrar->getPlural(), $c, $registrar->getIcon());
                $page->setPermission(RoleManager::getPermissionName('edit', $c));
                $page->setSection('content');
                $this->pages[$c] = $page;
            }
        }
        private function buildSettingsPages():void
        {
            $seo = $this->addPage('SEO', 'seo', 'robot');
            $seo->setSection('settings');
            $seo->setPermission('manage_options');
            $this->pages['seo'] = $seo;
        }
        private function buildAccountPages():void
        {
            if (Site::has('support')) {
                $page = $this->addPage('Support', 'support', 'question');
                $page->setSection('support');
            }
            $account = $this->addPage('Account', 'account', 'user-circle');
            $account->setSection('account');
            $accountID = $account->getID();
            $password = $this->addPage('Reset Password', 'reset-password', 'password', $accountID);
            $password->setOrder(2);
            $password->setSection('account');
        }
        private function buildReferrals():void
        {
            if (Site::has('referrals')) {
                $page = $this->addPage('Referrals', 'referrals', 'hand-heart');
            }
        }
        private function buildMembership():void
        {
            $membership = Site::membership();
            if ($membership) {
                if ($membership->has('can_invite')) {
                    $page = $this->addPage('Invite', 'invite', '');
                    $page->setSection('notifications');
                }
                if ($membership->has('term_approval')) {
                    $page = $this->addPage('Approvals', 'approvals', 'check-circle');
                    $page->setSection('notifications');
                }
                if ($membership->has('forum')) {
                    $page = $this->addPage('Forum', 'forum', 'chats-teardrop');
                }
                if ($membership->has('member_content')) {
                    $page = $this->addPage('Metrics', 'metrics', 'chart-line');
                }
            }
        }
        private function buildFavourites():void
        {
            if (Site::has('favourites')) {
                $page = $this->addPage('Favourites', 'favourites', 'heart');
                //TODO: Lists, Share permissions
            }
        }
        private function buildKarma():void
        {
            if (!empty(Registrar::withFeature('karma'))) {
                $page = $this->addPage('Karmic', 'karmic', 'arrow-fat-up');
            }
        }
        private function buildNotifications():void
        {
            if (Site::has('notifications')) {
                $page = $this->addPage('Notifications', 'notifications', 'bell');
                $page->setSection('notifications');
                $page = $this->addPage('Permissions', 'permissions', 'gear-six');
                $page->setSection('notifications');
                $page->setOrder(999);
            }
        }
        private function buildIntegrations():void
        {
            if (Site::hasAnyIntegration()) {
                $page = $this->addPage('Integrations', 'integrations', 'plugs-connected');
                $page->setSection('settings');
                $parent = $page->getID();
                foreach (array_keys(Site::getIntegrations()) as $integration) {
                    $integration = match($integration) {
                        'maps' => 'JVBase\integrations\GoogleMaps',
                        'square' => 'JVBase\integrations\Square',
                        'facebook' => 'JVBase\integrations\Facebook',
                        'helcim' => 'JVBase\integrations\Helcim',
                        'instagram' => 'JVBase\integrations\Instagram',
                        'bluesky' => 'JVBase\integrations\BlueSky',
                        'gmb' => 'JVBase\integrations\GoogleMyBusiness',
                        'cloudflare' => 'JVBase\integrations\Cloudflare',
                        'umami' => 'JVBase\integrations\Umami',
                        'postmark' => 'JVBase\integrations\PostMark',
                    };
                    $title = $integration::title();
                    $icon = $integration::icon();
                    $page = $this->addPage($title, $title, $icon, $parent);
                }
            }
        }
    public function addPage(string $title, string $slug = '', string $icon = '', int $parent = 0):DashboardPage
    {
        $page = new DashboardPage($title, $slug, $icon, $parent);
        $this->pages[$page->getSlug()] = $page;
        return $page;
    }
    protected function buildSections():void
    {
        $sections = array_values(array_filter(array_unique(array_map(function ($page) {
            return $page->getSection();
        }, $this->pages))));
        foreach ($sections as $section) {
            $isLink = false;
            switch ($section) {
                case 'content':
                    $title = 'Your Content';
                    $icon = 'book-bookmark';
                    break;
                case 'settings':
                    $title = 'Settings';
                    $icon = 'faders';
                    break;
                case 'account':
                    $title = 'Account';
                    $icon = 'user-circle';
                    break;
                case 'notifications':
                    $title = 'Notifications';
                    $icon = 'bell';
                    break;
                case 'support':
                    $title = 'Support';
                    $icon = 'question';
                    break;
                default:
                    $mainPage = $this->getMainPage($section);
                    if ($mainPage) {
                        $title = $mainPage->getTitle();
                        $icon = $mainPage->getIcon();
                        $isLink = true;
                    } else {
                        error_log('[DashboardManager]::buildSections Could not create section for '.$section);
                        return;
                    }
                    break;
            }
            if (!$isLink && $this->hasMainPage($section)) {
                $isLink = true;
            }
            $this->sections[$section] = new Section($title, $section, $icon);
            if ($isLink) {
                $this->sections[$section]->setIsLink(true);
            }
        }
    }
        protected function getSectionPages(string $section):array
        {
            return array_filter($this->pages, function($page) use ($section) {
                return $page->getSection() === $section;
            });
        }
        protected function getMainPage(string $section):DashboardPage|false
        {
            $sectionPages = $this->getSectionPages($section);
            $mainPage = array_filter($sectionPages, function ($page) use ($section) {
                return $page->getSlug() === $section;
            });
            return empty($mainPage) ? false : $mainPage[0];
        }
        protected function hasMainPage(string $section):bool
        {
            return $this->getMainPage($section) !== false;
        }
    public function addSection(string $title, ?string $slug = null, string $icon = '', ?string $parent = null):void
    {
        $section = new Section($title, $slug, $icon, $parent);
        $this->sections[$section->getSlug()] = $section;
    }
    public function addPageToSection(string $slug, ?string $section = null):bool
    {
        if (!array_key_exists($slug, $this->pages)) {
            error_log('[DashboardManager]::addPageToSection Attempted to add page to section that doesn\'t exist: '.$slug);
            return false;
        }
        if (!is_null($section) && !array_key_exists($section, $this->sections)) {
            error_log('[DashboardManager]::addPageToSection Please configure section first for '.$section);
            return false;
        }
        $this->pages[$slug]->setSection($section);
        return true;
    }
    #[NoReturn]protected function redirectToLogin():void
    {
        wp_redirect(wp_login_url(get_home_url(null, '/dash')));
        exit;
    }
    #[NoReturn]protected function redirectToDashboard():void
    {
        wp_redirect(get_home_url(null, '/dash'));
        exit;
    }
    #[NoReturn]protected function redirectToHome():void
    {
        wp_redirect(get_home_url());
        exit;
    }
    protected function getCurrentPage():DashboardPage|false
    {
        $slug = $this->getCurrentPageSlug();
        if (!array_key_exists($slug, $this->pages)) {
            error_log('[DashboardManager]::getCurrentPage Could not get configuration for '.$slug);
            return false;
        }
        return $this->pages[$slug];
    }
    protected function getCurrentPageSlug():string
    {
        if (is_post_type_archive(BASE.'dash')) {
            return 'dash';
        }
        global $post;
        if (!$post) {
            return '';
        }
        return $post->post_name;
    }
}
inc/managers/Dashboard/DashboardPage.php
New file
@@ -0,0 +1,159 @@
<?php
namespace JVBase\managers\Dashboard;
use JVBase\managers\Cache;
use WP_Query;
if (!defined('ABSPATH')) {
    exit;
}
class DashboardPage {
    protected string $title;
    protected string $slug;
    protected string $icon;
    protected string $URL;
    protected int $ID;
    protected ?string $permission = null;
    protected ?string $section = null;
    protected array $scripts = [];
    protected int $order = 0;
    protected Cache $cache;
    public function __construct(string $title, string $slug = '', string $icon ='', int $parent = 0) {
        $this->cache = Cache::for('dashboard');
        $this->title = $title;
        if (empty($slug)) {
            $this->setSlug($title);
        }else {
            $this->setSlug($slug);
        }
        $this->icon = $icon;
        $this->setID($parent);
        $this->setURL();
    }
    public function setID(int $parentID):void
    {
        $this->ID = $this->cache->remember(
            $this->slug.'_ID',
            function() use ($parentID) {
                $existing = new WP_Query([
                    'post_type' => BASE.'dash',
                    'name'      => $this->slug,
                    'fields'    => 'ids',
                    'posts_per_page'=> 1,
                ]);
                if ($existing->have_posts()) {
                    return $existing->posts[0];
                }
                $args = [
                    'post_title'    => $this->title,
                    'post_name'     => $this->slug,
                    'post_type'     => BASE.'dash',
                    'post_status'   => 'publish'
                ];
                if ($parentID > 0) {
                    $args['post_parent'] = $parentID;
                }
                return wp_insert_post($args);
            }
        );
    }
    public function getID():int
    {
        return $this->ID;
    }
    public function setURL():void
    {
        $this->URL = $this->cache->remember(
            $this->slug.'_url',
            function () {
                return get_permalink($this->ID);
            }
        );
    }
    public function getURL():string
    {
        return $this->URL;
    }
    public function setTitle(string $title):void
    {
        $this->title = $title;
    }
    public function getTitle():string
    {
        return $this->title;
    }
    public function setSlug(string $slug):void
    {
        $this->slug = self::sanitizeString($slug);
    }
    public function getSlug():string
    {
        return $this->slug;
    }
    public function setIcon(string $icon):void
    {
        $this->icon = $icon;
    }
    public function getIcon():string
    {
        return $this->icon;
    }
    public function setPermission(string $permission):void
    {
        $this->permission = $permission;
    }
    public function getPermission():?string
    {
        return $this->permission;
    }
    public function setSection(string $section):void
    {
        $this->section = self::sanitizeString($section);
    }
    public function getSection():?string
    {
        return $this->section;
    }
    public function setOrder(int $order):void
    {
        $this->order = $order;
    }
    public function getOrder():int
    {
        return $this->order;
    }
    public function setRenderCallback(string $callback):void
    {
    }
    public function render():string
    {
        return $this->cache->remember(
            $this->ID,
            function () {
                return apply_filters(BASE.'render_'.$this->slug, '<h2>'.$this->title.' is not configured yet.</h2><p>Add a filter to '.BASE.'render_'.$this->slug.'</p>');
            }
        );
    }
    /*********************************************************
     * UTILITY
     ********************************************************/
    public static function sanitizeString(string $string):string
    {
        return str_replace('_', '-', strtolower(sanitize_title($string)));
    }
}
inc/managers/Dashboard/Section.php
New file
@@ -0,0 +1,55 @@
<?php
namespace JVBase\managers\Dashboard;
if (!defined('ABSPATH')) {
    exit;
}
class Section {
    protected ?string $parent = null;
    protected string $title;
    protected string $slug;
    protected string $icon = '';
    protected int $order = 0;
    protected bool $isLink = false;
    public function __construct(string $title, ?string $slug = null, string $icon = '', ?string $parent = null) {
        $this->title = $title;
        $this->slug = is_null($slug) ? DashboardPage::sanitizeString($title) : DashboardPage::sanitizeString($slug);
        $this->icon = $icon;
        $this->parent = $parent;
    }
    public function getTitle():string
    {
        return $this->title;
    }
    public function getSlug():string
    {
        return $this->slug;
    }
    public function getIcon():string
    {
        return $this->icon;
    }
    public function getParent():?string
    {
        return $this->parent;
    }
    public function setOrder(int $order):void
    {
        $this->order = $order;
    }
    public function getOrder():int
    {
        return $this->order;
    }
    public function setIsLink(bool $isLink):void
    {
        $this->isLink = $isLink;
    }
    public function getIsLink():bool
    {
        return $this->isLink;
    }
}
inc/managers/Dashboard/_setup.php
New file
@@ -0,0 +1,8 @@
<?php
if (!defined('ABSPATH')) {
    exit;
}
require_once JVB_DIR . '/inc/managers/Dashboard/DashboardManager.php';
require_once JVB_DIR . '/inc/managers/Dashboard/DashboardPage.php';
require_once JVB_DIR . '/inc/managers/Dashboard/Section.php';
inc/managers/DashboardManager.php
@@ -1,7 +1,7 @@
<?php
namespace JVBase\managers;
use JVBase\forms\TaxonomySelector;
use JetBrains\PhpStorm\NoReturn;use JVBase\forms\TaxonomySelector;
use JVBase\base\Site;
use JVBase\meta\Form;
use JVBase\registrar\Registrar;
@@ -111,7 +111,7 @@
    /**
     * Redirect all non-admin users from wp-admin to custom dashboard
     */
    public function redirectFromAdmin()
    public function redirectFromAdmin():void
    {
        // Skip if already processing a redirect
        if (defined('DOING_AJAX') && DOING_AJAX) {
@@ -133,13 +133,13 @@
        }
    }
    protected function redirectToLogin():void
    #[NoReturn]protected function redirectToLogin():void
    {
        wp_redirect(wp_login_url(get_home_url(null, '/dash')));
        exit;
    }
    protected function redirectToDashboard():void
    #[NoReturn]protected function redirectToDashboard():void
    {
        wp_redirect(get_home_url(null, '/dash'));
        exit;
@@ -191,7 +191,6 @@
        if (!is_404() && !is_user_logged_in()) {
            error_log('Redirecting to login - user not logged in');
            $this->redirectToLogin();
            return;
        }
        // If logged in but doesn't have dashboard access, redirect to home
@@ -206,7 +205,6 @@
        if (is_404() && (str_starts_with($wp->request, 'dash/') || $wp->request === 'dash')) {
            error_log('404 on dashboard URL, redirecting to dashboard home');
            $this->redirectToDashboard();
            return;
        }
        // For valid dashboard pages, check access permissions
@@ -395,7 +393,6 @@
        $this->renderHeader();
        // Pass to page handler
        $constantSlug = $this->getConstantSlug($page);
        echo apply_filters(
            'jvbDashboardPage',
            $this->renderPage($page),
inc/managers/RoleManager.php
@@ -441,9 +441,9 @@
        $content = jvbNoBase($content);
        $registrar = Registrar::getInstance($content);
        if ($registrar && $registrar->getPlural()) {
            return str_replace(' ', '_', $registrar->getPlural());
            return strtolower(str_replace(' ', '_', $registrar->getPlural()));
        }
        return str_replace(' ', '_', $content.'s');
        return strtolower(str_replace(' ', '_', $content.'s'));
    }
    public static function activate(): void
@@ -821,6 +821,18 @@
        }
        return null;
    }
    public static function getPermissionName(string $action, string $content, ?int $ID = null):?string
    {
        $plural = (new self())->getContentPlural($content);
        switch ($action) {
            case 'edit':
                if ($ID) {
                    return "edit_{$content}";
                }
                return "edit_{$plural}";
        }
        return null;
    }
    public function maybeSwitchPermissions(int $object_id, array $terms, array $tt_ids, string $taxonomy, bool $append, array $old_tt_ids):void
    {
inc/managers/_setup.php
@@ -10,11 +10,10 @@
require(JVB_DIR . '/inc/managers/CustomTable.php');
//require(JVB_DIR . '/inc/managers/CacheManager.php');
require(JVB_DIR . '/inc/managers/Cache.php');
class_alias('JVBase\managers\Cache', 'JVBase\managers\CacheManager');
require(JVB_DIR . '/inc/managers/IconsManager.php');
add_action('init', 'jvbInit', 1); // Priority 1 - very early
add_action('init', 'jvbInit', 0); // Priority 1 - very early
function jvbInit(): void
{
@@ -37,6 +36,7 @@
    }
    if (Site::has('dashboard')) {
        require(JVB_DIR . '/inc/managers/DashboardManager.php');
        require(JVB_DIR . '/inc/managers/CRUDManager.php');
        require(JVB_DIR . '/inc/managers/UploadManager.php');
    }
@@ -75,10 +75,6 @@
        require(JVB_DIR . '/inc/managers/DirectoryManager.php');
    }
    if (Site::has('dashboard')) {
        require(JVB_DIR . '/inc/managers/DashboardManager.php');
    }
    if (Site::has('referrals')) {
        require(JVB_DIR . '/inc/managers/ReferralManager.php');
    }
jvb.php
@@ -260,7 +260,7 @@
add_action('plugins_loaded', 'jvb_registrar_definitions',2);
add_action('plugins_loaded', 'jvb_field_definitions', 3);
add_action('plugins_loaded', 'jvb_options_definitions',3);
add_action('init', 'jvbLoadBase', 1);
add_action('init', 'jvbLoadBase', 2);
add_action('init', 'jvb_integration_definitions',3);
add_action('init', 'jvb_field_section_definitions', 5);
/**