@php $panel = current_panel(); $branding = $panel?->branding ?? []; // Use the helper so logo_slim / logo_mark act as fallbacks when no favicon is set. $favicon = panel_favicon_url(); $logoDark = $branding['logo_dark'] ?? $branding['logo'] ?? null; $logoLight = $branding['logo_light'] ?? null; $user = auth()->user(); // Reseller navigation — grouped so it mirrors the owner admin the buyer // already knows, minus everything a reseller must not touch (master catalogue, // owner settings). Each row: label, icon, route name (href derived). // // TWO gates decide whether a row appears, and they are different things: // // $can('…') — PERMISSION. The reseller OWNER (reseller_admin) holds the // full set; a TEAM seat (reseller_staff) sees only what its // per-member permissions unlock. // $mod('…') — PLAN MODULE. Which modules the reseller's plan includes. // All modules are on by default and a panel with no // subscription keeps every one of them, so this only ever // subtracts when the owner deliberately built a lighter tier. // The matching route carries `panel.module:`, so a hidden // link is backed by a closed door, not just a missing entry. // // Core modules (dashboard, services, orders, clients, finance, settings) are // never gated by the plan — a panel without them is not a panel. $can = fn (string $perm) => (bool) $user?->can($perm); $mod = fn (string $key) => $panel === null || $panel->planModuleEnabled($key); $nav = fn (string $label, string $icon, string $route) => [ 'label' => __($label), 'icon' => $icon, 'route' => 'panel.'.$route, 'href' => route('panel.'.$route), ]; // Own providers is the ONE paid tier difference. It is shown even when the // plan excludes it — with a padlock, pointing at the upgrade screen — because // an upsell the reseller never sees is an upsell that never sells. $ownProvidersAllowed = $panel !== null && $panel->planAllows('own_providers'); $sections = [ __('Overview') => [ $nav('Dashboard', 'layout-dashboard', 'dashboard'), ], // Storefront stays purely operational — what a reseller tunes day to day // to sell (margins, catalogue, own supply). One-time white-label setup // (branding, landing, domains, client portal) lives in System below, // mirroring how the owner admin separates its own Catalog from System. __('Storefront') => array_values(array_filter([ ($can('reseller_prices.manage') && $mod('prices')) ? $nav('Margins & Pricing', 'percent', 'prices') : null, $can('services.pricing') ? $nav('Categories', 'folder-tree', 'categories') : null, $can('services.pricing') ? $nav('My Services', 'layers', 'services') : null, $can('own_providers.manage') ? array_merge($nav('My Providers', $ownProvidersAllowed ? 'server' : 'lock', 'providers'), ['locked' => ! $ownProvidersAllowed]) : null, ])), __('Customers') => array_values(array_filter([ $can('users.view') ? $nav('My Clients', 'users', 'clients') : null, $can('orders.view') ? $nav('Orders', 'shopping-cart', 'orders') : null, ($can('tickets.view') && module_enabled('tickets') && $mod('tickets')) ? $nav('Tickets', 'life-buoy', 'tickets') : null, ])), ...($can('crm.inbox') && $mod('crm') ? [ __('CRM') => array_values(array_filter([ $nav('Inbox', 'messages-square', 'crm.inbox'), ($can('crm.channels.manage') ? $nav('Channels', 'radio', 'crm.channels') : null), ($can('crm.ai.manage') && $mod('crm_ai') ? $nav('AI Agents', 'bot', 'crm.ai') : null), ($can('crm.automations.manage') && $mod('crm_automations') ? $nav('Automations', 'workflow', 'crm.automations') : null), ($can('crm.contacts.manage') ? $nav('Contacts', 'contact', 'crm.contacts') : null), ($can('crm.labels.manage') ? $nav('Labels', 'tags', 'crm.labels') : null), ($can('crm.labels.manage') ? $nav('Quick Replies', 'zap', 'crm.quick-replies') : null), ($can('crm.reports.view') ? $nav('CRM Reports', 'line-chart', 'crm.reports') : null), ($can('crm.settings.manage') ? $nav('CRM Settings', 'sliders', 'crm.settings') : null), ])), ] : []), // Finance moves money — owner-only (reseller_admin holds billing.view). ...($can('billing.view') ? [ __('Finance') => [ $nav('Plan & Billing', 'badge-percent', 'billing'), $nav('Add Funds', 'wallet', 'funds'), $nav('Transactions', 'receipt', 'transactions'), $nav('Support', 'headphones', 'support'), ], ] : []), ...($can('content.manage') && $mod('content') ? [ __('Content') => array_values(array_filter([ $nav('News', 'megaphone', 'news'), $nav('FAQs', 'help-circle', 'faqs'), module_enabled('blog') ? $nav('Blog Posts', 'newspaper', 'blog-posts') : null, $nav('Pages', 'file-text', 'pages'), ])), ] : []), // Profile lives ONLY in the topbar avatar menu (already links to // panel.profile) — it is not a section of settings to browse, so it is // not duplicated here in the sidebar. __('Account') => array_values(array_filter([ // Team + account Settings are owner-only (secrets / seat management). ($can('reseller_team.manage') && $mod('team')) ? $nav('Team', 'users-round', 'team') : null, $can('reseller_team.manage') ? $nav('Settings', 'settings', 'settings') : null, ($can('crm.settings.manage') && $mod('crm')) ? $nav('Integrations', 'plug', 'crm.integrations') : null, // Panel-scoped blacklist: owner-only (never delegated to a team seat). ($can('reseller_blacklist.manage') && $mod('blacklist')) ? $nav('Blacklist', 'ban', 'blacklist') : null, ])), // System: white-label / one-time setup, grouped last — same shape as the // owner admin's own System group (Settings, Appearance, Landing Page…). // Domains stays owner-only here too (never delegated to a team seat). // Branding is NOT here any more — it moved into Settings as a group // (owner admin parity), so its own account/Settings link is the only // way in now; the /panel/branding route still redirects there. __('System') => array_values(array_filter([ ($can('panels.branding') && $mod('landing')) ? $nav('Landing', 'layout-template', 'landing') : null, ($can('reseller_team.manage') && $mod('domains')) ? $nav('Domains', 'globe', 'domains') : null, ($can('panels.branding') && $mod('client_portal')) ? $nav('Client Portal', 'store', 'storefront') : null, ])), ]; // Drop any section a limited seat emptied out, so no bare heading is rendered. $sections = array_filter($sections, fn ($items) => ! empty($items)); @endphp