2026-01-22 23:38:00 +01:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Livewire;
|
|
|
|
|
|
2026-08-12 00:32:46 +02:00
|
|
|
use App\Dashboard\Stats\ArticlesPerFeed;
|
|
|
|
|
use App\Dashboard\Stats\BreakdownResult;
|
2026-01-22 23:38:00 +01:00
|
|
|
use App\Services\DashboardStatsService;
|
2026-08-12 00:00:49 +02:00
|
|
|
use App\Support\DateRange;
|
2026-03-18 20:01:25 +01:00
|
|
|
use Illuminate\Contracts\View\View;
|
2026-08-12 00:00:49 +02:00
|
|
|
use Illuminate\Support\Carbon;
|
|
|
|
|
use InvalidArgumentException;
|
|
|
|
|
use Livewire\Attributes\Computed;
|
2026-01-22 23:38:00 +01:00
|
|
|
use Livewire\Component;
|
|
|
|
|
|
|
|
|
|
class Dashboard extends Component
|
|
|
|
|
{
|
2026-08-12 00:00:49 +02:00
|
|
|
public string $from = '';
|
|
|
|
|
|
|
|
|
|
public string $to = '';
|
2026-01-22 23:38:00 +01:00
|
|
|
|
|
|
|
|
public function mount(): void
|
|
|
|
|
{
|
2026-08-12 00:00:49 +02:00
|
|
|
$this->applyPreset('today');
|
2026-01-22 23:38:00 +01:00
|
|
|
}
|
|
|
|
|
|
2026-08-12 00:20:46 +02:00
|
|
|
/**
|
|
|
|
|
* Islands whose content depends on the global range; skipped islands never re-render on their own.
|
|
|
|
|
*
|
|
|
|
|
* @var array<int, string>
|
|
|
|
|
*/
|
2026-08-12 00:32:46 +02:00
|
|
|
private const RANGE_DEPENDENT_ISLANDS = ['article-statistics', 'articles-per-feed'];
|
2026-08-12 00:20:46 +02:00
|
|
|
|
2026-08-12 00:00:49 +02:00
|
|
|
public function applyPreset(string $preset): void
|
2026-01-22 23:38:00 +01:00
|
|
|
{
|
2026-08-12 00:00:49 +02:00
|
|
|
try {
|
|
|
|
|
$range = DateRange::preset($preset);
|
|
|
|
|
} catch (InvalidArgumentException) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-12 00:20:46 +02:00
|
|
|
$this->applyRange($range->from->toDateString(), $range->to->toDateString());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function applyRange(string $from, string $to): void
|
|
|
|
|
{
|
|
|
|
|
$this->from = $from;
|
|
|
|
|
$this->to = $to;
|
|
|
|
|
|
|
|
|
|
$this->validateRange();
|
|
|
|
|
$this->refreshRangeDependentIslands();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function refreshRangeDependentIslands(): void
|
|
|
|
|
{
|
|
|
|
|
foreach (self::RANGE_DEPENDENT_ISLANDS as $island) {
|
|
|
|
|
$this->renderIsland(name: $island);
|
|
|
|
|
}
|
2026-01-22 23:38:00 +01:00
|
|
|
}
|
|
|
|
|
|
2026-08-12 00:00:49 +02:00
|
|
|
/**
|
|
|
|
|
* @return array<string, string>
|
|
|
|
|
*/
|
2026-08-12 00:20:46 +02:00
|
|
|
#[Computed]
|
2026-08-12 00:00:49 +02:00
|
|
|
public function presets(): array
|
2026-01-22 23:38:00 +01:00
|
|
|
{
|
2026-08-12 00:00:49 +02:00
|
|
|
return DateRange::presets();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public bool $rangeIsValid = true;
|
2026-01-22 23:38:00 +01:00
|
|
|
|
2026-08-12 00:00:49 +02:00
|
|
|
public function range(): ?DateRange
|
|
|
|
|
{
|
|
|
|
|
$from = $this->parseBoundary($this->from);
|
|
|
|
|
$to = $this->parseBoundary($this->to);
|
|
|
|
|
|
|
|
|
|
if (! $from instanceof Carbon || ! $to instanceof Carbon) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
return new DateRange($from->startOfDay(), $to->endOfDay());
|
|
|
|
|
} catch (InvalidArgumentException) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function parseBoundary(string $value): ?Carbon
|
|
|
|
|
{
|
|
|
|
|
try {
|
|
|
|
|
return Carbon::parse($value);
|
|
|
|
|
} catch (\Exception) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @return array<string, mixed>
|
|
|
|
|
*/
|
|
|
|
|
#[Computed]
|
|
|
|
|
public function articleStats(): array
|
|
|
|
|
{
|
|
|
|
|
$range = $this->range();
|
|
|
|
|
|
|
|
|
|
if (! $range instanceof DateRange) {
|
|
|
|
|
return [
|
|
|
|
|
'articles_fetched' => 0,
|
|
|
|
|
'articles_published' => 0,
|
|
|
|
|
'published_percentage' => 0.0,
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return app(DashboardStatsService::class)->getStats($range);
|
|
|
|
|
}
|
2026-01-22 23:38:00 +01:00
|
|
|
|
2026-08-12 00:32:46 +02:00
|
|
|
#[Computed]
|
|
|
|
|
public function articlesPerFeed(): BreakdownResult
|
|
|
|
|
{
|
|
|
|
|
$range = $this->range();
|
|
|
|
|
|
|
|
|
|
return $range instanceof DateRange
|
|
|
|
|
? app(ArticlesPerFeed::class)->for($range)
|
|
|
|
|
: new BreakdownResult([]);
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-12 00:00:49 +02:00
|
|
|
/**
|
|
|
|
|
* @return array<string, int>
|
|
|
|
|
*/
|
|
|
|
|
#[Computed]
|
|
|
|
|
public function systemStats(): array
|
|
|
|
|
{
|
|
|
|
|
return app(DashboardStatsService::class)->getSystemStats();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function updated(string $property): void
|
|
|
|
|
{
|
|
|
|
|
if (in_array($property, ['from', 'to'], true)) {
|
|
|
|
|
$this->validateRange();
|
2026-08-12 00:20:46 +02:00
|
|
|
$this->refreshRangeDependentIslands();
|
2026-08-12 00:00:49 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function validateRange(): void
|
|
|
|
|
{
|
|
|
|
|
$this->resetErrorBag(['from', 'to']);
|
|
|
|
|
|
|
|
|
|
$from = $this->parseBoundary($this->from);
|
|
|
|
|
$to = $this->parseBoundary($this->to);
|
|
|
|
|
|
|
|
|
|
if (! $from instanceof Carbon) {
|
|
|
|
|
$this->addError('from', 'The start date is not a valid date.');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (! $to instanceof Carbon) {
|
|
|
|
|
$this->addError('to', 'The end date is not a valid date.');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($from instanceof Carbon && $to instanceof Carbon && $to->lessThan($from)) {
|
|
|
|
|
$this->addError('to', 'The end date must not be earlier than the start date.');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$this->rangeIsValid = $this->range() instanceof DateRange;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function render(): View
|
|
|
|
|
{
|
2026-08-12 00:20:46 +02:00
|
|
|
return view('livewire.dashboard')->layout('layouts.app');
|
2026-01-22 23:38:00 +01:00
|
|
|
}
|
|
|
|
|
}
|