fedi-feed-router/app/Livewire/Dashboard.php

213 lines
5.2 KiB
PHP

<?php
namespace App\Livewire;
use App\Dashboard\Stats\ApprovalRate;
use App\Dashboard\Stats\ArticlesPerFeed;
use App\Dashboard\Stats\ArticlesTrend;
use App\Dashboard\Stats\BreakdownResult;
use App\Dashboard\Stats\BreakdownStat;
use App\Dashboard\Stats\PublicationsPerChannel;
use App\Dashboard\Stats\SeriesResult;
use App\Services\DashboardStatsService;
use App\Support\DateRange;
use Illuminate\Contracts\View\View;
use Illuminate\Support\Carbon;
use InvalidArgumentException;
use Livewire\Attributes\Computed;
use Livewire\Component;
class Dashboard extends Component
{
public string $from = '';
public string $to = '';
public function mount(): void
{
$this->applyPreset('month');
}
/**
* Islands whose content depends on the global range; skipped islands never re-render on their own.
*
* @var array<int, string>
*/
private const RANGE_DEPENDENT_ISLANDS = [
'article-statistics',
'articles-trend',
'approval-rate',
'articles-per-feed',
'publications-per-channel',
];
public function applyPreset(string $preset): void
{
try {
$range = DateRange::preset($preset);
} catch (InvalidArgumentException) {
return;
}
$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);
}
}
/**
* @return array<string, string>
*/
#[Computed]
public function presets(): array
{
return DateRange::presets();
}
public bool $rangeIsValid = true;
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);
}
#[Computed]
public function articlesTrend(): SeriesResult
{
$range = $this->range();
return $range instanceof DateRange
? app(ArticlesTrend::class)->for($range)
: new SeriesResult([], []);
}
#[Computed]
public function approvalRate(): SeriesResult
{
$range = $this->range();
return $range instanceof DateRange
? app(ApprovalRate::class)->for($range)
: new SeriesResult([], []);
}
#[Computed]
public function articlesPerFeed(): BreakdownResult
{
return $this->breakdown(ArticlesPerFeed::class);
}
#[Computed]
public function publicationsPerChannel(): BreakdownResult
{
return $this->breakdown(PublicationsPerChannel::class);
}
/**
* @param class-string<BreakdownStat> $stat
*/
private function breakdown(string $stat): BreakdownResult
{
$range = $this->range();
return $range instanceof DateRange
? app($stat)->for($range)
: new BreakdownResult([]);
}
/**
* @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();
$this->refreshRangeDependentIslands();
}
}
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
{
return view('livewire.dashboard')->layout('layouts.app');
}
}