- Add `/stats/live` endpoint returning station count and latest price timestamp with 5-minute cache - Transform StationCard into expandable component with click/keyboard interaction showing full details - Display brand label, badges (24h/Supermarket/Motorway), fuel types, amenities, opening hours, and price delta vs average - Add brand filter dropdown to StationList with dynamic brand extraction from results - Calculate and display price comparison against filtered stations average - Redesign map markers to simpler price display; move directions link to popup alongside station details - Add "locate-me" button to SearchBar for geolocation trigger - Show "Live" indicator with station count and last-update time on homepage hero - Remove standalone directions link from marker HTML; consolidate in popup with click propagation handling - Persist `avgPence` calculation across StationList and pass to cards for delta display - Add `@iconify-json/lucide` dev dependency and register collection on app mount - Stop click propagation on card action buttons (directions, remove)
72 lines
2.6 KiB
PHP
72 lines
2.6 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Api;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\Search;
|
|
use App\Models\Station;
|
|
use App\Models\StationPrice;
|
|
use Carbon\CarbonImmutable;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Cache;
|
|
|
|
class StatsController extends Controller
|
|
{
|
|
public function live(): JsonResponse
|
|
{
|
|
$payload = Cache::remember('api:stats:live', now()->addMinutes(5), function (): array {
|
|
$stationCount = Station::query()
|
|
->where('permanent_closure', false)
|
|
->count();
|
|
|
|
$latestPriceAt = StationPrice::query()->max('recorded_at');
|
|
|
|
return [
|
|
'station_count' => $stationCount,
|
|
'latest_price_at' => $latestPriceAt ? CarbonImmutable::parse($latestPriceAt)->toIso8601String() : null,
|
|
];
|
|
});
|
|
|
|
return response()->json($payload);
|
|
}
|
|
|
|
public function searches(Request $request): JsonResponse
|
|
{
|
|
$period = $request->input('period', 'week');
|
|
$days = $period === 'month' ? 30 : 7;
|
|
|
|
$stats = Search::query()
|
|
->where('searched_at', '>=', now()->subDays($days))
|
|
->selectRaw('
|
|
COUNT(*) as total_searches,
|
|
COUNT(DISTINCT ip_hash) as unique_searchers,
|
|
AVG(results_count) as avg_results,
|
|
AVG(lowest_pence) as avg_lowest_pence,
|
|
AVG(highest_pence) as avg_highest_pence,
|
|
AVG(avg_pence) as avg_avg_pence
|
|
')
|
|
->first();
|
|
|
|
$totalSearches = (int) $stats->total_searches;
|
|
$uniqueSearchers = (int) $stats->unique_searchers;
|
|
$avgResults = $stats->avg_results !== null ? round((float) $stats->avg_results, 1) : 0.0;
|
|
$avgLowestPrice = $stats->avg_lowest_pence !== null ? round((float) $stats->avg_lowest_pence / 100, 1) : 0.0;
|
|
$avgHighestPrice = $stats->avg_highest_pence !== null ? round((float) $stats->avg_highest_pence / 100, 1) : 0.0;
|
|
$avgPrice = $stats->avg_avg_pence !== null ? round((float) $stats->avg_avg_pence / 100, 1) : 0.0;
|
|
|
|
$periodLabel = $period === 'month' ? 'month' : 'week';
|
|
|
|
return response()->json([
|
|
'total_searches' => $totalSearches,
|
|
'unique_searchers' => $uniqueSearchers,
|
|
'avg_results' => $avgResults,
|
|
'avg_lowest_price' => $avgLowestPrice,
|
|
'avg_highest_price' => $avgHighestPrice,
|
|
'avg_price' => $avgPrice,
|
|
'period' => $periodLabel,
|
|
'message' => "Helped {$uniqueSearchers} drivers find cheaper fuel this {$periodLabel} so far!",
|
|
]);
|
|
}
|
|
}
|