Laravel Projects Examples

Laravel Reverb Live Dashboard

When building a dashboard with Financial information - it's nice to have it updated in real-time. So let's use Laravel Reverb to update a table and a few of our charts.

How to install

  • Clone the repository with git clone
  • Copy the .env.example file to .env and edit database credentials there
  • Reverb ENV variables are set to work locally
  • Run composer install
  • Run php artisan key:generate
  • Run php artisan storage:link
  • Run php artisan migrate --seed (it has some seeded data for your testing)
  • Run php artisan reverb:start
  • Launch the main URL /. Log in with credentials [email protected] and password or register with a new user.
  • That's it: Create a new order in a new tab, and the dashboard will be updated live.

How It Works

All the data to the dashboard comes from a DashboardController Controller.

app/Http/Controllers/DashboardController.php:

use Illuminate\View\View;
use App\Services\OrdersService;
 
class DashboardController extends Controller
{
public function __invoke(OrdersService $ordersService): View
{
$totalRevenue = $ordersService->getTotalRevenue();
$thisMonthRevenue = $ordersService->getThisMonthRevenue();
$todayRevenue = $ordersService->getTodayRevenue();
$latestOrders = $ordersService->getLatestOrders(5);
$orderChartByMonth = $ordersService->orderChartByMonth();
$orderChartByDay = $ordersService->orderChartByDay();
 
return view(
'dashboard',
compact(
'totalRevenue',
'thisMonthRevenue',
'todayRevenue',
'latestOrders',
'orderChartByDay',
'orderChartByMonth'
)
);
}
}

Calculations for the data are made using the OrdersService service.

app/Services/OrdersService.php:

use App\Models\Order;
use Illuminate\Database\Eloquent\Collection;
 
class OrdersService
{
public function getTotalRevenue(): float|int
{
return Order::query()
->pluck('total')
->sum() / 100;
}
 
public function getThisMonthRevenue(): float|int
{
return Order::query()
->whereMonth('created_at', now()->month)
->pluck('total')
->sum() / 100;
}
 
public function getTodayRevenue(): float|int
{
return Order::query()
->whereDate('created_at', now()->toDateString())
->pluck('total')
->sum() / 100;
}
 
public function getLatestOrders(int $count): Collection
{
return Order::query()
->with('user:id,email')
->latest()
->take($count)
->get();
}
 
public function orderChartByDay($days = 15): array
{
$orders = Order::query()
->selectRaw('DATE(created_at) as date, SUM(total) as total')
->where('created_at', '>=', now()->subDays($days)->startOfDay())
->groupBy('date')
->get();
 
$labels = $orders->pluck('date');
$totals = $orders->pluck('total');
 
return [
'labels' => $labels,
'totals' => $totals,
];
}
 
public function orderChartByMonth($months = 12): array
{
$orders = Order::query()
// SQLite date functions
->selectRaw('strftime("%Y", created_at) as year, strftime("%m", created_at) as month, SUM(total) as total')
// MySQL date functions
// ->selectRaw('YEAR(created_at) as year, MONTH(created_at) as month, SUM(total) as total')
->where('created_at', '>=', now()->subMonthsNoOverflow($months)->startOfMonth())
->groupBy('year', 'month')
->get();
 
$labels = $orders->map(function ($order) {
return $order->year . '-' . $order->month;
});
$totals = $orders->pluck('total');
 
return [
'labels' => $labels,
'totals' => $totals,
];
}
}

Brief code for the View file to show the dashboard.

resources/views/dashboard.blade.php:

{{-- ... --}}
 
<div class="...">
Total Revenue:
</div>
<div class="...">
$ <span id="box_total_revenue">{{ Number::format($totalRevenue) }}</span>
</div>
 
{{-- ... --}}
 
<div class="...">
Revenue This Month:
</div>
<div class="...">
$ <span id="box_revenue_this_month">{{ Number::format($thisMonthRevenue) }}</span>
</div>
 
{{-- ... --}}
 
<div class="...">
Revenue Today:
</div>
<div class="...">
$ <span id="box_revenue_today">{{ Number::format($todayRevenue) }}</span>
</div>
 
{{-- ... --}}
 
@foreach($latestOrders as $order)
<tr class=" [@media(hover:hover)]:transition [@media(hover:hover)]:duration-75">
<td class="...">
{{ $order->created_at->format('M d, Y h:i A') }}
</td>
<td class="...">
{{ $order->user->email }}
</td>
<td class="...">
$ {{ Number::format($order->total) }}
</td>
</tr>
@endforeach
 
{{-- ... --}}
 
<canvas id="revenueByDay"></canvas>
 
{{-- ... --}}
 
<canvas id="revenueByMonth"></canvas>
 
{{-- ... --}}
 
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
 
<script>
const revenueByDay = document.getElementById('revenueByDay');
const revenueByMonth = document.getElementById('revenueByMonth');
 
let revenueByDayChart = new Chart(revenueByDay, {
type: 'bar',
data: {
labels: @json($orderChartByDay['labels']),
datasets: [{
data: @json($orderChartByDay['totals']),
borderWidth: 1
}]
},
options: {
plugins: {
legend: {
display: false,
}
},
scales: {
y: {
beginAtZero: true
}
}
}
});
 
let revenueByMonthChart = new Chart(revenueByMonth, {
type: 'bar',
data: {
labels: @json($orderChartByMonth['labels']),
datasets: [{
data: @json($orderChartByMonth['totals']),
borderWidth: 1
}]
},
options: {
plugins: {
legend: {
display: false,
}
},
scales: {
y: {
beginAtZero: true
}
}
}
});
</script>

First, Reverb was installed using an Artisan command to use the live updating dashboard.

php artisan install:broadcasting

An Event is created for broadcasting updates. The event must have a ShouldBroadcast interface.

The broadcast channel is set in the broadcastOn() method to the order-dashboard-updates. The service is reused to get data.

This event is fired after the order is created in the OrderController.

use App\Models\Order;
use App\Services\OrdersService;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Number;
 
class OrderCreatedEvent implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
 
public string $totalRevenue;
public string $thisMonthRevenue;
public string $todayRevenue;
public array $latestOrders;
public array $orderChartByMonth;
public array $orderChartByDay;
 
public function __construct(public Order $order)
{
$ordersService = new OrdersService();
 
$this->totalRevenue = Number::format($ordersService->getTotalRevenue());
$this->thisMonthRevenue = Number::format($ordersService->getThisMonthRevenue());
$this->todayRevenue = Number::format($ordersService->getTodayRevenue());
$this->latestOrders = $ordersService->getLatestOrders(1)->map(function(Order $order) {
return [
'id' => $order->id,
'user' => $order->user->toArray(),
'total' => Number::format($order->total),
'created_at' => $order->created_at->format('M d, Y h:i A')
];
})->toArray();
$this->orderChartByMonth = $ordersService->orderChartByMonth(0);
$this->orderChartByDay = $ordersService->orderChartByDay(0);
}
 
public function broadcastOn(): array
{
return [
new PrivateChannel('order-dashboard-updates')
];
}
}

The same channel name is created in the channel route.

routes/channels.php:

Broadcast::channel('order-dashboard-updates', function () {
return true;
});

The event on the frontend is listened to using Laravel Echo. First, the channel is retrieved using echo. Then, we listen for the event.

In the event, we have all the data set in the OrderCreatedEvent. Using all the data from an event, the frontend is updated.

resources/views/dashboard.blade.php:

@php
use Illuminate\Support\Number;
@endphp
 
<x-app-layout>
// ...
 
<script>
// ...
 
window.addEventListener('DOMContentLoaded', function () {
let totalRevenue = document.getElementById('box_total_revenue');
let revenueThisMonth = document.getElementById('box_revenue_this_month');
let revenueToday = document.getElementById('box_revenue_today');
let latestOrdersTable = document.getElementById('latest-orders-table');
let tableRowTemplate = document.getElementById('table-row-template').innerHTML;
 
let channel = window.Echo.private('order-dashboard-updates');
channel.listen('OrderCreatedEvent', function (e) {
// Update the revenue widgets
totalRevenue.innerText = e.totalRevenue;
revenueThisMonth.innerText = e.thisMonthRevenue;
revenueToday.innerText = e.todayRevenue;
 
// Insert the new row at the top of the table
let newRow = tableRowTemplate
.replace('_DATE_', e.latestOrders[0].created_at)
.replace('_EMAIL_', e.latestOrders[0].user.email)
.replace('_TOTAL_', e.latestOrders[0].total);
latestOrdersTable.querySelector('tbody').insertAdjacentHTML('afterbegin', newRow);
 
setTimeout(function () {
// Remove the green background from the rows
let lines = latestOrdersTable.querySelectorAll('tbody tr');
lines.forEach(function (line) {
line.classList.remove('bg-green-100');
});
}, 2500)
// remove the last row of the table
latestOrdersTable.querySelector('tbody tr:last-child').remove();
 
if (!revenueByDayChart.data.labels.includes(e.orderChartByDay.labels[0])) {
// If there is no data for the day, add it
revenueByDayChart.data.labels.push(e.orderChartByDay.labels[0]);
revenueByDayChart.data.datasets[0].data.push(e.orderChartByDay.totals[0]);
revenueByDayChart.update();
} else {
// If there is data for the day, update it
let index = revenueByDayChart.data.labels.indexOf(e.orderChartByDay.labels[0]);
revenueByDayChart.data.datasets[0].data[index] = e.orderChartByDay.totals[0];
revenueByDayChart.update();
}
 
if (!revenueByMonthChart.data.labels.includes(e.orderChartByMonth.labels[0])) {
// If there is no data for the month, add it
revenueByMonthChart.data.labels.push(e.orderChartByMonth.labels[0]);
revenueByMonthChart.data.datasets[0].data.push(e.orderChartByMonth.totals[0]);
revenueByMonthChart.update();
} else {
// If there is data for the month, update it
let index = revenueByMonthChart.data.labels.indexOf(e.orderChartByMonth.labels[0]);
revenueByMonthChart.data.datasets[0].data[index] = e.orderChartByMonth.totals[0];
revenueByMonthChart.update();
}
});
})
</script>
</x-app-layout>