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]
andpassword
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>