Files
fuel-price/app/Filament/Widgets/StatsOverviewWidget.php
Ovidiu U 7101ed3550
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (8.3) (push) Has been cancelled
tests / ci (8.4) (push) Has been cancelled
tests / ci (8.5) (push) Has been cancelled
feat: add postcode resolution to /api/stations and Filament SearchResource
Extends NearbyStationsRequest to accept `postcode` (full or outcode) as an alternative to lat/lng. PostcodeService resolves it via postcodes.io and falls through to coordinates. Also adds SearchResource to the Filament admin panel for viewing logged search activity with fuel type filter and price/distance stats columns. Includes SQLite GREATEST/LEAST function polyfills in AppServiceProvider for test compatibility.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 19:10:25 +01:00

85 lines
2.5 KiB
PHP

<?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::bestFirst()->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);
}
}