Zum Hauptinhalt springen
Schneespur
Dokumentation durchblättern

Module entwickeln

Portal & eigene Dashboards

Das Kundenportal als Referenzmuster für rollenspezifische Dashboards — und wie ein Modul eigene, abgesicherte Bereiche mit Guard, Middleware, Layout und Routen aufbaut.

Schneespur bringt mit dem Kundenportal einen eigenen, abgesicherten Bereich mit, der unabhängig vom Admin-Bereich läuft. Dieses Portal ist zugleich das Referenzmuster, an dem Sie sich orientieren, wenn ein Modul einen eigenen rollenspezifischen Bereich braucht — etwa ein Dashboard für die Buchhaltung oder das Lager.

Das Muster des Kundenportals

Das Kundenportal ist ein getrennter, authentifizierter Bereich unter /portal mit eigenem Guard, eigener Middleware, eigenem Layout und eigenen Controllern. Es ist bewusst vom Admin-Bereich abgekoppelt — ein Kunde meldet sich an einem anderen Eingang an und sieht eine andere Oberfläche als ein Mitarbeiter.

/portal/login          → PortalAuthController (customer guard)
/portal/               → PortalDashboardController
/portal/jobs           → PortalJobController
/portal/profile        → PortalProfileController
/portal/notifications  → PortalNotificationController
/portal/reports        → PortalPdfController

Die tragenden Bestandteile:

  • Guard: customer — getrennt vom web-Guard, damit Kunden- und Mitarbeiter-Sessions sich nicht vermischen.
  • Middleware: EnsureCustomer — prüft die Kunden-Authentifizierung, prüft das Flag portal_enabled und setzt die Sprache des Kunden (customers.locale, sofern in der LocaleRegistry registriert).
  • Layout: layouts/portal.blade.php — unabhängig vom Admin-Layout.
  • Model: Customer — fungiert für diesen Guard als „Benutzer”.
  • Session: die Standard-Session von Laravel, gebunden an den customer-Guard.

Warum ein eigener Guard: Kunden sind keine Mitarbeiter. Ein getrennter Guard hält die beiden Authentifizierungswege sauber auseinander — ein angemeldeter Kunde erhält dadurch nie versehentlich Zugriff auf Admin-Routen, und umgekehrt.

Portal-Schalter je Kunde

Welche Inhalte das Portal pro Kunde zeigt, steuern vier Flags. So sieht jeder Kunde nur das, was für ihn freigegeben ist — etwa ohne Fahrernamen, wenn das aus Datenschutzgründen gewünscht ist:

EinstellungTypWirkung
portal_enabledboolPortal-Zugang aktivieren/deaktivieren
portal_show_gpsboolGPS-Tracks in den Einsatzdetails zeigen
portal_show_photosboolEinsatzfotos zeigen
portal_show_driver_nameboolFahrernamen zeigen (Datenschutz)

Ein eigenes rollenspezifisches Dashboard bauen

Wenn Sie einen eigenständigen Dashboard-Bereich brauchen — etwa /buchhaltung für die Buchhaltung oder /lager für das Lager — folgen Sie demselben Muster wie das Portal. Die folgenden Schritte zeigen den Weg an einem Buchhaltungs-Dashboard.

Schritt 1: Rolle festlegen

Variante A — die vorhandene Berechtigungslogik mit einer eigenen Rolle nutzen. Das ist der einfachere Weg, wenn Ihre Nutzer ohnehin normale Anwender mit einer Sonderrolle sind:

// In your ServiceProvider boot()
$templates = app(RoleTemplateRegistry::class);
$templates->registerTemplate(
    slug: 'accountant',
    name: 'Buchhaltung',
    description: 'Zugriff auf das Buchhaltungs-Dashboard',
    permissions: ['dashboard.view', 'documents.view', 'reports.view'],
    module: 'accounting-dashboard',
);

Variante B — ein vollständig getrennter Authentifizierungsbereich (wie das Kundenportal). Dafür legen Sie einen eigenen Guard und eine eigene Middleware an. Diesen Weg wählen Sie nur, wenn die Nutzergruppe wirklich getrennt von den normalen Anwendern sein soll.

Schritt 2: Middleware

Die Middleware ist der Türwächter des Bereichs. Sie prüft bei jeder Anfrage, ob der angemeldete Nutzer die passende Rolle hat — andernfalls leitet sie zur Anmeldung um:

namespace Schneespur\Module\AccountingDashboard\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class EnsureAccountant
{
    public function handle(Request $request, Closure $next)
    {
        $user = $request->user();

        if (!$user || !$user->hasRole('accountant')) {
            return redirect()->route('login');
        }

        return $next($request);
    }
}

Im ServiceProvider registrieren Sie die Middleware unter einem Alias, damit Sie sie in den Routen kurz benennen können:

app('router')->aliasMiddleware('ensure.accountant', EnsureAccountant::class);

Schritt 3: Routen

Die Routen bündeln Sie unter einem gemeinsamen Präfix und schützen sie mit der eben registrierten Middleware. So gilt der Schutz für die ganze Gruppe — Sie müssen ihn nicht an jeder einzelnen Route wiederholen:

Route::middleware(['web', 'auth', 'ensure.accountant'])
    ->prefix('buchhaltung')
    ->name('accounting.')
    ->group(function () {
        Route::get('/', [AccountingDashboardController::class, 'index'])->name('dashboard');
        Route::get('/documents', [AccountingDocumentController::class, 'index'])->name('documents');
        Route::get('/invoices', [AccountingInvoiceController::class, 'index'])->name('invoices');
        Route::get('/exports', [AccountingExportController::class, 'index'])->name('exports');
    });

Schritt 4: Layout

Ein eigener Bereich verdient ein eigenes Layout — mit eigener Navigation und eigenem Erscheinungsbild, getrennt vom Admin-Layout. Die @extensionSlot-Stellen lassen Raum, damit andere Module später an definierten Punkten andocken können:

{{-- resources/views/layouts/accounting.blade.php --}}
<!DOCTYPE html>
<html lang="{{ app()->getLocale() }}">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>{{ brand() }} — Buchhaltung</title>
    @vite(['resources/css/app.css'])
    @moduleAssets
    @extensionSlot('accounting.head.after')
</head>
<body class="bg-gray-50">
    <nav class="bg-white shadow">
        {{-- Accounting-specific navigation --}}
        <div class="max-w-7xl mx-auto px-4 py-3 flex items-center justify-between">
            <span class="font-bold text-lg">{{ brand() }} Buchhaltung</span>
            <div class="flex gap-4">
                <a href="{{ route('accounting.documents') }}" class="text-gray-700 hover:text-blue-600">Dokumente</a>
                <a href="{{ route('accounting.invoices') }}" class="text-gray-700 hover:text-blue-600">Rechnungen</a>
                <a href="{{ route('accounting.exports') }}" class="text-gray-700 hover:text-blue-600">Exporte</a>
            </div>
            <form method="POST" action="{{ route('logout') }}">
                @csrf
                <button type="submit" class="text-gray-500 hover:text-red-600">Abmelden</button>
            </form>
        </div>
    </nav>

    <main class="max-w-7xl mx-auto px-4 py-8">
        @extensionSlot('accounting.content.before')
        {{ $slot }}
        @extensionSlot('accounting.content.after')
    </main>
</body>
</html>

Schritt 5: Dashboard-Controller

Der Controller liefert die Daten an die View. Halten Sie ihn schlank — er holt die Werte und reicht sie weiter:

namespace Schneespur\Module\AccountingDashboard\Http\Controllers;

use Illuminate\Http\Request;

class AccountingDashboardController
{
    public function index(Request $request)
    {
        return view('accounting-dashboard::dashboard', [
            'recentDocuments' => Document::latest()->take(10)->get(),
            'pendingInvoices' => Invoice::where('status', 'pending')->count(),
        ]);
    }
}

Schritt 6: Weiterleitung nach dem Login

Damit ein Nutzer nach der Anmeldung direkt in seinem Bereich landet, hängen Sie sich an das Login-Ereignis und setzen das Ziel der Weiterleitung anhand der Rolle:

// Or listen to the UserLoggedIn event
$this->app['events']->listen(UserLoggedIn::class, function (UserLoggedIn $event) {
    if ($event->user->hasRole('accountant')) {
        session(['url.intended' => route('accounting.dashboard')]);
    }
});

Das bestehende Portal erweitern

Oft braucht ein Modul gar keinen neuen Bereich, sondern soll nur das vorhandene Kundenportal ergänzen — etwa um eine Dokumentenliste. Dafür gibt es schlankere Wege.

Einen Portal-Navigationspunkt hinzufügen

Nutzen Sie die PortalNavigationRegistry (seit 1.1.3). Sie rendert den Eintrag in beide Menüs — die Desktop-Navigation und das mobile Menü — und kümmert sich um den Aktiv-Zustand:

$nav = app(\App\Services\Extension\PortalNavigationRegistry::class);

$nav->addItem(
    slug: 'documents',
    label: 'my-module::portal.nav_documents', // translation key, resolved at render
    route: 'portal.documents.index',
    order: 25,
    activePattern: 'portal.documents.*',
    condition: fn (\App\Models\Customer $c) => true, // optional per-customer visibility
);

Das condition-Closure ist optional. Es bekommt den angemeldeten Kunden übergeben und entscheidet pro Kunde, ob der Eintrag sichtbar ist — fn (\App\Models\Customer $c): bool.

Der ältere Weg — ein Blade-Partial an den Slot portal.nav.after anhängen — funktioniert weiterhin, rendert aber nur im Desktop-Header. Das mobile Menü hat keinen solchen Slot, und Sie müssten Markup und Aktiv-Zustand selbst bauen. Für Menü-Links ist die Registry der bessere Weg; den Slot portal.nav.after reservieren Sie besser für Inhalte, die keine Links sind. Mehr zur PortalNavigationRegistry unter Registries und Navigation & Dashboard.

Portal-Routen hinzufügen

Eigene Portal-Routen schützen Sie mit demselben Guard und derselben Middleware wie das Portal selbst — so gelten dieselben Zugriffsregeln:

Route::middleware(['web', 'auth:customer', EnsureCustomer::class])
    ->prefix('portal/documents')
    ->name('portal.documents.')
    ->group(function () {
        Route::get('/', [PortalDocumentController::class, 'index'])->name('index');
        Route::get('/{document}/download', [PortalDocumentController::class, 'download'])->name('download');
    });

Auf den angemeldeten Kunden zugreifen

Innerhalb der Portal-Logik holen Sie den angemeldeten Kunden über den customer-Guard:

$customer = auth('customer')->user(); // Returns Customer model

Empfehlung zur Architektur

Wenn Sie mehrere rollenspezifische Dashboards planen, lohnt sich ein gemeinsames Fundament statt mehrerer fast gleicher Bereiche. Empfehlenswert ist ein einzelnes Rahmen-Modul für Rollen-Dashboards, das die wiederkehrenden Teile bereitstellt:

  1. eine wiederverwendbare Basisklasse RoleDashboard mit URL-Präfix, Middleware, Layout-Slots und Widget-Unterstützung,
  2. jedes weitere Rollen-Dashboard registriert sich in diesem Rahmen,
  3. so vermeiden Sie doppelten Code für Authentifizierung, Layout und Widgets über mehrere Module hinweg.

Das Kundenportal zeigt dieses Muster bereits — dort allerdings fest verdrahtet. Eine generische Variante würde Rollen-Slug, URL-Präfix, Layout-Vorlage und erlaubte Berechtigungsgruppen entgegennehmen und im Gegenzug Login-Weiterleitung, Layout mit Slots, Widget-Darstellung und Navigationsaufbau übernehmen. Ein solches Rahmen-Modul ist heute nicht Teil der Auslieferung; das Portal dient bis dahin als Vorlage, die Sie nach diesem Muster nachbauen.