Laravel Livewire voor B2B dashboards — interactief zonder SPA
Terug naar blog

Laravel Livewire voor B2B dashboards — interactief zonder SPA

AuthorRuthger Idema
6 april 202610 min leestijd

Een B2B dashboard met real-time data, filterbare tabellen en inline formulieren — zonder een React of Vue SPA te bouwen. Livewire maakt het mogelijk vanuit één Laravel codebase.

Laravel Livewire voor B2B dashboards — interactief zonder SPA

73% van de B2B inkopers geeft aan dat een slechte digitale ervaring reden is om naar een concurrent over te stappen. Toch bouwen veel bedrijven nog dashboards die na elke actie een volledige pagina herladen.

Het alternatief is niet meteen een volledige React of Vue applicatie. Laravel Livewire geeft je realtime interactiviteit vanuit één codebase, zonder JavaScript frontend framework, zonder aparte API laag, zonder state management bibliotheek.

Dit artikel laat zien hoe wij B2B dashboards bouwen met Livewire. Met concrete code, meetbare voordelen en een eerlijke vergelijking met een SPA aanpak.

Wat je leert in dit artikel

  • Wanneer Livewire de juiste keuze is voor een B2B dashboard
  • Real-time dataTables met filtering, sortering en paginering
  • Charts die updaten zonder pagina herlaad
  • Inline formulieren en modal dialogen
  • Vergelijking met Vue en React SPA: wanneer wisselen

Wat maakt B2B dashboards anders

Een B2B dashboard heeft andere eisen dan een consumentenwebshop.

Inkopers werken erin 4-8 uur per dag. Reactietijd is geen nice-to-have, het is productiviteit. Een filteractie die 2 seconden duurt terwijl iemand 200 orders doorloopt, kost dagelijks minuten die optellen tot uren per week.

Functionaliteit is complex: orderoverzichten met tientallen kolommen, inline statuswijzigingen, exportfuncties, klantspecifieke prijzen, goedkeuringsworkflows. Dit soort functionaliteit is precies waar Livewire sterk in is.

Livewire in twee minuten

Livewire is een full-stack framework dat PHP componenten verbindt met een HTML frontend. De server beheert de state. Bij een gebruikersinteractie stuurt Livewire een AJAX verzoek naar de server, voert de PHP methode uit en werkt de DOM bij met alleen de veranderde elementen.

php
// app/Livewire/OrderTable.php
namespace App\Livewire;

use App\Models\Order;
use Livewire\Component;
use Livewire\WithPagination;

class OrderTable extends Component
{
    use WithPagination;

    public string $search = '';
    public string $status = '';
    public string $sortBy = 'created_at';
    public string $sortDir = 'desc';

    // Elke wijziging in $search reset de paginering
    public function updatingSearch(): void
    {
        $this->resetPage();
    }

    public function sortOn(string $column): void
    {
        if ($this->sortBy === $column) {
            $this->sortDir = $this->sortDir === 'asc' ? 'desc' : 'asc';
        } else {
            $this->sortBy = $column;
            $this->sortDir = 'asc';
        }
    }

    public function render()
    {
        $orders = Order::query()
            ->with(['customer', 'lines'])
            ->when($this->search, fn($q) => $q->where(function ($q) {
                $q->where('reference', 'like', "%{$this->search}%")
                  ->orWhereHas('customer', fn($q) => $q->where('name', 'like', "%{$this->search}%"));
            }))
            ->when($this->status, fn($q) => $q->where('status', $this->status))
            ->orderBy($this->sortBy, $this->sortDir)
            ->paginate(25);

        return view('livewire.order-table', compact('orders'));
    }
}

De bijbehorende Blade view bindt direct aan PHP properties via wire:model en roept PHP methoden aan via wire:click.

html
<!-- resources/views/livewire/order-table.blade.php -->
<div>
    <div class="flex gap-4 mb-6">
        <input wire:model.live.debounce.300ms="search"
               placeholder="Zoek op referentie of klant..."
               class="border rounded px-3 py-2 w-80" />

        <select wire:model.live="status" class="border rounded px-3 py-2">
            <option value="">Alle statussen</option>
            <option value="pending">In behandeling</option>
            <option value="processing">Verwerking</option>
            <option value="shipped">Verzonden</option>
            <option value="completed">Afgerond</option>
        </select>
    </div>

    <table class="w-full">
        <thead>
            <tr>
                <th wire:click="sortOn('reference')" class="cursor-pointer text-left px-4 py-2">
                    Referentie
                    @if($sortBy === 'reference')
                        {{ $sortDir === 'asc' ? '↑' : '↓' }}
                    @endif
                </th>
                <th class="text-left px-4 py-2">Klant</th>
                <th wire:click="sortOn('total')" class="cursor-pointer text-right px-4 py-2">
                    Totaal
                </th>
                <th class="text-left px-4 py-2">Status</th>
            </tr>
        </thead>
        <tbody>
            @foreach($orders as $order)
                <tr class="border-t">
                    <td class="px-4 py-2">{{ $order->reference }}</td>
                    <td class="px-4 py-2">{{ $order->customer->name }}</td>
                    <td class="px-4 py-2 text-right">€ {{ number_format($order->total, 2, ',', '.') }}</td>
                    <td class="px-4 py-2">
                        <livewire:order-status-badge :order="$order" :key="$order->id" />
                    </td>
                </tr>
            @endforeach
        </tbody>
    </table>

    {{ $orders->links() }}
</div>

Het resultaat: een filterbare, sorteerbare tabel zonder één regel JavaScript te schrijven.

Real-time data met polling en broadcasting

B2B dashboards hebben vaak data die continu verandert. Orderstatus updates, voorraadniveaus, openstaande offertes.

Livewire ondersteunt twee patronen: polling en broadcasting.

Polling: simpel maar voldoende voor veel gevallen

php
// Elke 5 seconden de component verversen
public function render()
{
    return view('livewire.dashboard-stats', [
        'stats' => $this->getStats(),
    ])->with('polling', true);
}
html
<!-- In de view: polling elke 5 seconden -->
<div wire:poll.5s>
    <div class="grid grid-cols-4 gap-4">
        <div class="bg-white rounded shadow p-4">
            <p class="text-sm text-gray-500">Open orders</p>
            <p class="text-2xl font-bold">{{ $stats['open_orders'] }}</p>
        </div>
        <div class="bg-white rounded shadow p-4">
            <p class="text-sm text-gray-500">Omzet vandaag</p>
            <p class="text-2xl font-bold">€ {{ number_format($stats['revenue_today'], 0, ',', '.') }}</p>
        </div>
    </div>
</div>

Broadcasting: direct bij wijziging

Voor critieke updates — een nieuwe grote order, een betalingsfout — gebruik je Laravel Echo met broadcasting.

php
// app/Events/OrderPlaced.php
class OrderPlaced implements ShouldBroadcast
{
    public function __construct(public Order $order) {}

    public function broadcastOn(): array
    {
        return [new PrivateChannel("dashboard.{$this->order->customer->account_manager_id}")];
    }
}

// app/Livewire/RecentOrders.php
protected $listeners = ['echo-private:dashboard.{userId},OrderPlaced' => 'refresh'];

Een nieuwe order verschijnt direct in het dashboard van de accountmanager, zonder dat hij hoeft te verversen.

Charts die updaten

Voor grafieken combineren wij Livewire met Alpine.js en Chart.js. Alpine.js wordt meegeleverd met Livewire 3 en vraagt geen aparte installatie.

php
// app/Livewire/RevenueChart.php
class RevenueChart extends Component
{
    public string $period = '30d';

    public function getChartDataProperty(): array
    {
        $days = match($this->period) {
            '7d' => 7,
            '30d' => 30,
            '90d' => 90,
            default => 30,
        };

        return Order::query()
            ->where('created_at', '>=', now()->subDays($days))
            ->where('status', 'completed')
            ->selectRaw('DATE(created_at) as date, SUM(total) as revenue')
            ->groupBy('date')
            ->orderBy('date')
            ->get()
            ->map(fn($row) => [
                'date'    => $row->date,
                'revenue' => (float) $row->revenue,
            ])
            ->toArray();
    }

    public function render()
    {
        return view('livewire.revenue-chart', [
            'chartData' => $this->chartData,
        ]);
    }
}
html
<!-- resources/views/livewire/revenue-chart.blade.php -->
<div
    x-data="{
        chart: null,
        init() {
            this.renderChart(@js($chartData));
        },
        renderChart(data) {
            if (this.chart) this.chart.destroy();
            this.chart = new Chart(this.$refs.canvas, {
                type: 'line',
                data: {
                    labels: data.map(d => d.date),
                    datasets: [{
                        label: 'Omzet',
                        data: data.map(d => d.revenue),
                        borderColor: '#6366f1',
                        fill: false,
                    }]
                }
            });
        }
    }"
    x-on:livewire:updated="renderChart(@js($chartData))"
>
    <div class="flex gap-2 mb-4">
        <button wire:click="$set('period', '7d')" class="px-3 py-1 rounded border">7 dagen</button>
        <button wire:click="$set('period', '30d')" class="px-3 py-1 rounded border">30 dagen</button>
        <button wire:click="$set('period', '90d')" class="px-3 py-1 rounded border">90 dagen</button>
    </div>
    <canvas x-ref="canvas"></canvas>
</div>

De periodeknop triggert een Livewire update. Alpine.js vangt het livewire:updated event op en hertekent de chart met de nieuwe data.

Inline formulieren en modals

B2B gebruikers willen orders goedkeuren, statussen wijzigen en notities toevoegen zonder een nieuwe pagina te laden.

php
// app/Livewire/OrderApproval.php
class OrderApproval extends Component
{
    public bool $showModal = false;
    public ?int $orderIdToApprove = null;
    public string $approvalNote = '';

    public function openApproval(int $orderId): void
    {
        $this->orderIdToApprove = $orderId;
        $this->approvalNote = '';
        $this->showModal = true;
    }

    public function approve(): void
    {
        $this->validate(['approvalNote' => 'required|min:10']);

        $order = Order::findOrFail($this->orderIdToApprove);
        $order->update(['status' => 'approved', 'approval_note' => $this->approvalNote]);

        $this->showModal = false;
        $this->dispatch('order-approved', orderId: $order->id);
        session()->flash('message', "Order {$order->reference} goedgekeurd.");
    }
}

Livewire vs Vue/React SPA: wanneer wisselen

Livewire is niet de juiste keuze voor alles. Hier is een eerlijke afweging.

SituatieLivewireVue/React SPA
CRUD dashboard, filterbare tabellenJaOverkill
Real-time updates via pollingJaJa
Complexe client-side state (drag & drop, canvas)BeperktJa
Offline functionaliteit nodigNeeJa
Team heeft sterke JS expertiseBeide werkenJa
Snelle time-to-marketJaNee
Scheiden van frontend en backend teamsNeeJa

De vuistregel die wij hanteren: bouw met Livewire tenzij je een specifieke reden hebt voor een SPA. Die reden bestaat — maar voor de meeste B2B dashboards is hij er niet.

Veelgemaakte fouten

  1. Te veel data ophalen per component — Elke Livewire interactie is een serverroundtrip. Laad alleen wat je toont. Gebruik lazy loading voor zware queries.
  2. Geen debounce op zoekvelden — Zonder debounce.300ms stuur je bij elk toetsaanslag een request. Dat belast de server onnodig.
  3. Geneste componenten te diep — Livewire communiceert via events tussen componenten. Te veel nesting maakt de dataflow ondoorzichtig. Houd het vlak.
  4. Geen authorization in component methoden — Livewire methoden zijn publiek aanroepbaar. Voeg altijd $this->authorize() toe in methoden die data muteren.
php
public function deleteOrder(int $orderId): void
{
    // Altijd autoriseren, ook in Livewire
    $this->authorize('delete', Order::findOrFail($orderId));

    Order::destroy($orderId);
    $this->dispatch('order-deleted');
}

Conclusie

Een B2B dashboard hoeft geen SPA te zijn. Met Livewire bouw je filterbare tabellen, real-time updates en inline formulieren vanuit één Laravel codebase. Minder complexiteit, minder infrastructuur, sneller opgeleverd.

De meeste B2B dashboards die wij bouwen draaien op Livewire. De gevallen waarbij wij overstappen op een SPA zijn specifiek en bewust.

Livewire is een open-source project dat nauw samenwerkt met het Laravel ecosysteem. Combineer het met Alpine.js voor client-side interactiviteit waar nodig. Gerelateerde artikelen:

Wil je weten of Livewire past bij jouw dashboard project? Bekijk onze Laravel-diensten of neem contact op voor een vrijblijvend gesprek over de architectuur.

Ruthger Idema

Geschreven door Ruthger Idema

15+ jaar ervaring in e-commerce development. Gespecialiseerd in Magento, Shopify en Laravel maatwerk.

Meer over ons team →
Deel dit artikel:

Wil je jouw e-commerce naar het volgende niveau?

Plan een vrijblijvend gesprek met onze experts over Magento, Shopify of Laravel maatwerk.

Plan een Tech Check