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.
Admin sidebar link
Call registerAdminMenuItem() in your module's boot():
$this->registerAdminMenuItem(
label: 'My Plugin',
route: 'ch-admin.my-plugin.index',
icon: 'extension',
order: 20,
);| Parameter | Type | Default | Description |
|---|---|---|---|
label | string | — | Text shown in the sidebar |
route | string | — | Named route for the link |
icon | string | 'ti-package' | Tabler icon class (e.g. ti-chart-bar, ti-users) |
order | int | 50 | Sort 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:
$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.phpCreate 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:
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
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:
@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>
@endsectionFull example
// 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
// 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>):
// 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).
// 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 viav-html— keep all JS out of the fragment.- Forms must have an
actionattribute pointing to your module's save endpoint; the Vue handler POSTs toform.action. - The
_tokenfield is not needed — axios sends the CSRF token via theX-CSRF-TOKENheader 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.