Skip to content

Admin Integration

v1.2 · Plan tab extension: v1.5

Modules can inject links into the admin sidebar, add tabs to the admin Settings page, and inject tabs into the plan edit modal — without touching any core view file.


Call registerAdminMenuItem() in your module's boot():

php
$this->registerAdminMenuItem(
    label: 'My Plugin',
    route: 'ch-admin.my-plugin.index',
    icon:  'extension',
    order: 20,
);
ParameterTypeDefaultDescription
labelstringText shown in the sidebar
routestringNamed route for the link
iconstring'ti-package'Tabler icon class (e.g. ti-chart-bar, ti-users)
orderint50Sort position among module items. Lower = higher up

Items are rendered between the core nav and the Settings link, sorted by order.


Admin settings tab

To add a tab to Admin → Settings:

php
$this->registerAdminSettingsLink(
    label:   'My Plugin',
    section: 'my-plugin',
    order:   20,
);

ChargePanda will look for a blade partial at:

resources/views/admin/settings/partials/my-plugin.blade.php

Create that partial and it renders inside the settings page when the tab is active. The partial receives the same $settings variable available to all settings partials.


Building admin pages

Admin pages follow the same pattern as core admin pages. Register routes in routes/web.php:

php
Route::prefix('ch-admin/my-plugin')
    ->middleware(['web', 'auth', 'role:administrator'])
    ->name('ch-admin.my-plugin.')
    ->group(function () {
        Route::get('/',        [MyPluginAdminController::class, 'index'])->name('index');
        Route::get('/{item}',  [MyPluginAdminController::class, 'show'])->name('show');
        Route::post('/',       [MyPluginAdminController::class, 'store'])->name('store');
        Route::put('/{item}',  [MyPluginAdminController::class, 'update'])->name('update');
        Route::delete('/{item}', [MyPluginAdminController::class, 'destroy'])->name('destroy');
    });

Always prefix admin routes with ch-admin/ and gate them with ['auth', 'role:administrator']. This keeps your pages consistent with the rest of the admin panel.

In your controller, extend Illuminate\Routing\Controller (not a core admin base class — modules are self-contained):

php
<?php

namespace Modules\MyPlugin\Http\Controllers\Admin;

use Illuminate\Http\Request;
use Illuminate\Routing\Controller;

class MyPluginAdminController extends Controller
{
    public function index()
    {
        $items = \Modules\MyPlugin\Models\MyItem::paginate(20);
        return view('my-plugin::admin.index', compact('items'));
    }
}

Admin views

Admin views should extend the core admin layout:

blade
@extends('admin.layouts.app', ['title' => 'My Plugin'])

@section('content')
<div class="container-xl">
    <div class="page-header">
        <div class="row align-items-center">
            <div class="col-auto">
                <h2 class="page-title">My Plugin</h2>
            </div>
        </div>
    </div>

    <div class="card">
        <div class="card-body">
            {{-- your content here --}}
        </div>
    </div>
</div>
@endsection

Full example

php
// ModuleServiceProvider.php
public function boot(): void
{
    $this->loadMigrations();
    $this->loadWebRoutes();
    $this->loadViews();

    $this->registerAdminMenuItem(
        label: 'Announcements',
        route: 'ch-admin.announcements.index',
        icon:  'ti-speakerphone',
        order: 15,
    );

    $this->registerAdminSettingsLink(
        label:   'Announcements',
        section: 'announcements',
        order:   15,
    );
}

Injecting a tab into the plan edit modal (v1.5)

Any module can add a tab to the plan edit modal in the Product Manager. The tab content is a server-rendered HTML fragment loaded via AJAX when the user clicks the tab.

Register the tab

php
// ModuleServiceProvider::boot()
$this->registerPlanTab(
    key:        'key-pool',          // unique slug for this tab
    label:      'Key Pool',          // tab header text
    contentUrl: fn () => route('ch-admin.license-keys.plan-tab.content'),
);

// contentUrl must be a Closure, not a bare route() call.
// boot() runs during artisan commands before routes are resolved —
// a closure defers the URL generation to request time.

Serve the content fragment

The contentUrl receives a GET request with ?plan_id=X. Return a plain HTML fragment (no <html>/<body>):

php
// YourPlanTabController.php
public function content(Request $request)
{
    $plan = Plan::findOrFail($request->integer('plan_id'));
    // ... load your module's data for this plan
    return view('your-module::admin.plan-tab', compact('plan', ...));
}

Forms inside the fragment

Forms inside the tab are submitted via axios by the Vue handleModuleTabSubmit handler. After a successful save the tab content is automatically refreshed. Your save endpoint must return { "success": true } (or false with a message).

php
// In your save controller action:
return response()->json(['success' => true]);

Rules for tab Blade views:

  • Bootstrap 5 classes work — the admin panel already loads BS5.
  • <script> tags are not executed when injected via v-html — keep all JS out of the fragment.
  • Forms must have an action attribute pointing to your module's save endpoint; the Vue handler POSTs to form.action.
  • The _token field is not needed — axios sends the CSRF token via the X-CSRF-TOKEN header automatically.

"Save first" placeholder

For new (unsaved) plans the Vue modal shows a placeholder automatically — your contentUrl is never called until the plan has a real database ID. You do not need to handle this case in your controller.

Released under the Commercial License.