feat: add StatsOverviewWidget to admin dashboard
Four-stat overview widget (users, stations, oil prediction, API errors) with 30s polling registered on the admin panel dashboard. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
84
app/Filament/Widgets/StatsOverviewWidget.php
Normal file
84
app/Filament/Widgets/StatsOverviewWidget.php
Normal file
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Widgets;
|
||||
|
||||
use App\Models\ApiLog;
|
||||
use App\Models\PricePrediction;
|
||||
use App\Models\Station;
|
||||
use App\Models\User;
|
||||
use Carbon\Carbon;
|
||||
use Filament\Widgets\StatsOverviewWidget as BaseWidget;
|
||||
use Filament\Widgets\StatsOverviewWidget\Stat;
|
||||
|
||||
class StatsOverviewWidget extends BaseWidget
|
||||
{
|
||||
protected ?string $pollingInterval = '30s';
|
||||
|
||||
protected function getStats(): array
|
||||
{
|
||||
return [
|
||||
$this->usersStat(),
|
||||
$this->stationsStat(),
|
||||
$this->oilPredictionStat(),
|
||||
$this->apiErrorsStat(),
|
||||
];
|
||||
}
|
||||
|
||||
private function usersStat(): Stat
|
||||
{
|
||||
return Stat::make('Total users', User::count())
|
||||
->icon('heroicon-o-users')
|
||||
->color('primary');
|
||||
}
|
||||
|
||||
private function stationsStat(): Stat
|
||||
{
|
||||
$count = Station::count();
|
||||
$lastSeen = Station::max('last_seen_at');
|
||||
$description = $lastSeen
|
||||
? 'Last seen '.Carbon::parse($lastSeen)->diffForHumans()
|
||||
: 'No stations yet';
|
||||
|
||||
return Stat::make('Stations in DB', number_format($count))
|
||||
->description($description)
|
||||
->icon('heroicon-o-map-pin')
|
||||
->color('success');
|
||||
}
|
||||
|
||||
private function oilPredictionStat(): Stat
|
||||
{
|
||||
$prediction = PricePrediction::latest('generated_at')->first();
|
||||
|
||||
if ($prediction === null) {
|
||||
return Stat::make('Latest oil prediction', 'None')
|
||||
->icon('heroicon-o-beaker')
|
||||
->color('gray');
|
||||
}
|
||||
|
||||
$ageHours = $prediction->generated_at->diffInHours(now());
|
||||
$color = $ageHours > 24 ? 'warning' : 'success';
|
||||
$value = ucfirst($prediction->direction->value)
|
||||
.' · '.$prediction->confidence.'%'
|
||||
.' · '.strtoupper($prediction->source->value);
|
||||
|
||||
return Stat::make('Latest oil prediction', $value)
|
||||
->description('Generated '.$prediction->generated_at->diffForHumans())
|
||||
->icon('heroicon-o-beaker')
|
||||
->color($color);
|
||||
}
|
||||
|
||||
private function apiErrorsStat(): Stat
|
||||
{
|
||||
$errors = ApiLog::where('created_at', '>=', now()->subDay())
|
||||
->where(fn ($q) => $q->where('status_code', '>=', 400)
|
||||
->orWhereNull('status_code')
|
||||
->orWhereNotNull('error'))
|
||||
->count();
|
||||
|
||||
$color = $errors > 0 ? 'danger' : 'success';
|
||||
|
||||
return Stat::make('API errors (24h)', $errors)
|
||||
->icon('heroicon-o-exclamation-triangle')
|
||||
->color($color);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Providers\Filament;
|
||||
|
||||
use App\Filament\Widgets\StatsOverviewWidget;
|
||||
use Filament\Http\Middleware\Authenticate;
|
||||
use Filament\Http\Middleware\AuthenticateSession;
|
||||
use Filament\Http\Middleware\DisableBladeIconComponents;
|
||||
@@ -39,6 +40,7 @@ class AdminPanelProvider extends PanelProvider
|
||||
->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\Filament\Widgets')
|
||||
->widgets([
|
||||
AccountWidget::class,
|
||||
StatsOverviewWidget::class,
|
||||
])
|
||||
->middleware([
|
||||
EncryptCookies::class,
|
||||
|
||||
26
tests/Feature/Admin/StatsOverviewWidgetTest.php
Normal file
26
tests/Feature/Admin/StatsOverviewWidgetTest.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
use App\Filament\Widgets\StatsOverviewWidget;
|
||||
use App\Models\ApiLog;
|
||||
use App\Models\PricePrediction;
|
||||
use App\Models\Station;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->admin = User::factory()->admin()->create();
|
||||
$this->actingAs($this->admin);
|
||||
});
|
||||
|
||||
it('renders the stats overview widget', function () {
|
||||
User::factory()->count(3)->create();
|
||||
Station::factory()->count(2)->create();
|
||||
PricePrediction::factory()->create(['generated_at' => now()->subHours(2)]);
|
||||
ApiLog::factory()->count(2)->create(['status_code' => 200, 'error' => null, 'created_at' => now()->subMinutes(30)]);
|
||||
|
||||
Livewire::test(StatsOverviewWidget::class)
|
||||
->assertOk();
|
||||
});
|
||||
Reference in New Issue
Block a user