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.
// 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.
<!-- 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
// Elke 5 seconden de component verversen
public function render()
{
return view('livewire.dashboard-stats', [
'stats' => $this->getStats(),
])->with('polling', true);
}
<!-- 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.
// 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.
// 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,
]);
}
}
<!-- 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.
// 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.
| Situatie | Livewire | Vue/React SPA |
|---|---|---|
| CRUD dashboard, filterbare tabellen | Ja | Overkill |
| Real-time updates via polling | Ja | Ja |
| Complexe client-side state (drag & drop, canvas) | Beperkt | Ja |
| Offline functionaliteit nodig | Nee | Ja |
| Team heeft sterke JS expertise | Beide werken | Ja |
| Snelle time-to-market | Ja | Nee |
| Scheiden van frontend en backend teams | Nee | Ja |
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
- Te veel data ophalen per component — Elke Livewire interactie is een serverroundtrip. Laad alleen wat je toont. Gebruik
lazyloading voor zware queries. - Geen debounce op zoekvelden — Zonder
debounce.300msstuur je bij elk toetsaanslag een request. Dat belast de server onnodig. - Geneste componenten te diep — Livewire communiceert via events tussen componenten. Te veel nesting maakt de dataflow ondoorzichtig. Houd het vlak.
- Geen authorization in component methoden — Livewire methoden zijn publiek aanroepbaar. Voeg altijd
$this->authorize()toe in methoden die data muteren.
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:- Van Excel naar Laravel dashboard — migratie stap voor stap
- Laravel Sanctum vs Passport — authenticatie voor je dashboard-API
- Event-driven architectuur in Laravel — real-time data via events
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.

Geschreven door Ruthger Idema
15+ jaar ervaring in e-commerce development. Gespecialiseerd in Magento, Shopify en Laravel maatwerk.
Meer over ons team →