Skip to content

Commit

Permalink
feat: build initial dashboard page with ViewStats widget
Browse files Browse the repository at this point in the history
  • Loading branch information
alkrauss48 committed Apr 18, 2024
1 parent 52136bd commit 21aeac6
Show file tree
Hide file tree
Showing 8 changed files with 318 additions and 5 deletions.
22 changes: 22 additions & 0 deletions app/Enums/PresentationFilter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

namespace App\Enums;

enum PresentationFilter: string
{
use Traits\EnumToArray;

case INSTRUCTIONS = 'instructions';
case ADHOC = 'adhoc';

/**
* Returns the user-friendly label
*/
public function label(): string
{
return match ($this) {
self::INSTRUCTIONS => 'Instructions',
self::ADHOC => 'Adhoc',
};
}
}
48 changes: 48 additions & 0 deletions app/Enums/Traits/EnumToArray.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

namespace App\Enums\Traits;

// Inspired From: https://stackoverflow.com/a/71680007/3482221

trait EnumToArray
{
/**
* The array of keys for the enum.
*
* @return mixed[] - These will be a string array. But laravel's toArray
* doesn't specify that, so we have to use mixed.
*/
public static function names(): array
{
return collect(self::cases())
->pluck('name')
->toArray();
}

/**
* The array of values for the enum.
*
* @return mixed[]
*/
public static function values(): array
{
return collect(self::cases())
->pluck('value')
->toArray();
}

/**
* The array of values for the enum.
*
* @return array<mixed, string>
*/
public static function array(): array
{
return collect(self::cases())
->reduce(function ($carry, $row) {
$carry[$row->value] = $row->label();

return $carry;
}, []);
}
}
64 changes: 64 additions & 0 deletions app/Filament/Pages/Dashboard.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php

namespace App\Filament\Pages;

use App\Enums\PresentationFilter;
use App\Models\Presentation;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select;
use Filament\Forms\Form;
use Filament\Forms\Get;
use Filament\Pages\Dashboard as BaseDashboard;
use Filament\Pages\Dashboard\Concerns\HasFiltersForm;

class Dashboard extends BaseDashboard
{
use HasFiltersForm;

public function getColumns(): int|string|array
{
return 2;
}

public function filtersForm(Form $form): Form
{
return $form
->schema([
Section::make()
->schema([
Select::make('presentation_id')
->label('Presentation')
->searchable()
->options(
auth()->user()->isAdministrator()
? PresentationFilter::array()
: []
)->getSearchResultsUsing(function (string $search) {
return Presentation::forUser()
->where('title', 'ilike', "%{$search}%")
->limit(20)
->pluck('title', 'id')
->toArray();
})->getOptionLabelUsing(function ($value): ?string {
return Presentation::forUser()
->find(intval($value))?->title;
}),
DatePicker::make('start_date')
->label('Start Date')
->native(false)
->maxDate(fn (Get $get): ?string => $get('end_date') ?? now()->subDay())
->hintIcon('heroicon-o-information-circle', tooltip: 'Only affects "Date Range" stats')
->default(now()->subDays(8)),
DatePicker::make('end_date')
->label('End Date')
->native(false)
->minDate(fn (Get $get): ?string => $get('start_date'))
->default(now()->subDay())
->hintIcon('heroicon-o-information-circle', tooltip: 'Only affects "Date Range" stats')
->maxDate(now()->subDay()),
])
->columns(3),
]);
}
}
65 changes: 65 additions & 0 deletions app/Filament/Widgets/ViewStats.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

namespace App\Filament\Widgets;

use App\Models\AggregateView;
use App\Models\DailyView;
use Filament\Widgets\Concerns\InteractsWithPageFilters;
use Filament\Widgets\StatsOverviewWidget as BaseWidget;
use Filament\Widgets\StatsOverviewWidget\Stat;

class ViewStats extends BaseWidget
{
use InteractsWithPageFilters;

protected static ?string $pollingInterval = null;

protected function getStats(): array
{
return [
$this->aggregateViews(withinRange: true),
$this->dailyViews(),
$this->aggregateViews(),
];
}

private function dailyViews(): Stat
{
$views = DailyView::getForStat(
presentationId: $this->filters['presentation_id']
);

$totalviews = $views->count();
$uniqueviews = $views->unique(function (DailyView $item) {
return $item->presentation_id.$item->session_id;
})->count();

$percentUniqueviews = $totalviews == 0
? 0
: round(($uniqueviews / $totalviews) * 100);

return Stat::make('Total Views Today', $totalviews)
->description($percentUniqueviews."% ($uniqueviews) Unique Views");
}

private function aggregateViews(bool $withinRange = false): Stat
{
$views = AggregateView::getForStat(
presentationId: $this->filters['presentation_id'],
startDate: $withinRange ? $this->filters['start_date'] : null,
endDate: $withinRange ? $this->filters['end_date'] : null,
);

$totalviews = $views->sum('total_count');
$uniqueviews = $views->sum('unique_count');

$percentUniqueviews = $totalviews == 0
? 0
: round(($uniqueviews / $totalviews) * 100);

$heading = $withinRange ? 'Total Views in Date Range' : 'Total Lifetime Views';

return Stat::make($heading, $totalviews)
->description($percentUniqueviews."% ($uniqueviews) Unique Views");
}
}
62 changes: 62 additions & 0 deletions app/Models/AggregateView.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,74 @@

namespace App\Models;

use App\Enums\PresentationFilter;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;

class AggregateView extends Model
{
use HasFactory;

const UPDATED_AT = null;

/**
* Get the aggregate views based on the presentation filter, and the user's role.
*
* @return Collection<int, self>
*/
public static function getForStat(
?string $presentationId,
?string $startDate,
?string $endDate,
): Collection {
$query = self::forUser();

if ($startDate) {
$query->whereDate('created_at', '>=', $startDate);
}

if ($endDate) {
$query->whereDate('created_at', '<=', $endDate);
}

if (auth()->user()->isAdministrator()) {
if ($presentationId === PresentationFilter::INSTRUCTIONS->value) {
return $query
->whereNull('presentation_id')
->whereNull('adhoc_slug')
->get();
}

if ($presentationId === PresentationFilter::ADHOC->value) {
return $query
->whereNull('presentation_id')
->whereNotNull('adhoc_slug')
->get();
}
}

return is_null($presentationId)
? $query->get()
: $query
->where('presentation_id', intval($presentationId))
->get();
}

/**
* Scope a query to only include aggregate views for the authenticated user.
*
* @param Builder<AggregateView> $query
*/
public function scopeForUser(Builder $query): void
{
if (auth()->user()->isAdministrator()) {
return;
}

$presentationIds = auth()->user()->presentations()->pluck('id');

$query->whereIn('presentation_id', $presentationIds);
}
}
49 changes: 49 additions & 0 deletions app/Models/DailyView.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@

namespace App\Models;

use App\Enums\PresentationFilter;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Collection;

class DailyView extends Model
{
Expand Down Expand Up @@ -32,6 +35,52 @@ public static function createForAdhocPresentation(?string $slug = null): self
]);
}

/**
* Get the daily views based on the presentation filter, and the user's role.
*
* @return Collection<int, self>
*/
public static function getForStat(?string $presentationId): Collection
{
if (auth()->user()->isAdministrator()) {
if ($presentationId === PresentationFilter::INSTRUCTIONS->value) {
return self::forUser()
->whereNull('presentation_id')
->whereNull('adhoc_slug')
->get();
}

if ($presentationId === PresentationFilter::ADHOC->value) {
return self::forUser()
->whereNull('presentation_id')
->whereNotNull('adhoc_slug')
->get();
}
}

return is_null($presentationId)
? self::forUser()->get()
: self::forUser()
->where('presentation_id', intval($presentationId))
->get();
}

/**
* Scope a query to only include daily views for the authenticated user.
*
* @param Builder<DailyView> $query
*/
public function scopeForUser(Builder $query): void
{
if (auth()->user()->isAdministrator()) {
return;
}

$presentationIds = auth()->user()->presentations()->pluck('id');

$query->whereIn('presentation_id', $presentationIds);
}

/**
* The Presentation that this record belongs to
*
Expand Down
5 changes: 0 additions & 5 deletions app/Providers/Filament/AdminPanelProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
use Filament\Panel;
use Filament\PanelProvider;
use Filament\Support\Colors\Color;
use Filament\Widgets;
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
use Illuminate\Cookie\Middleware\EncryptCookies;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
Expand Down Expand Up @@ -38,10 +37,6 @@ public function panel(Panel $panel): Panel
// Pages\Dashboard::class,
])
->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\\Filament\\Widgets')
->widgets([
Widgets\AccountWidget::class,
Widgets\FilamentInfoWidget::class,
])
->navigationGroups([
'Main',
'Extras',
Expand Down
8 changes: 8 additions & 0 deletions tests/Filament/PresentationResourceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@
it('can create a record', function () {
$newData = Model::factory()->make();

// The factory generates a random historical created_at, but we don't
// want that when creating a test record in Filament.
unset($newData['created_at']);

livewire(CreateResource::class)
->fillForm([
...$newData->toArray(),
Expand Down Expand Up @@ -231,6 +235,10 @@
it('can create a record', function () {
$newData = Model::factory()->make();

// The factory generates a random historical created_at, but we don't
// want that when creating a test record in Filament.
unset($newData['created_at']);

livewire(CreateResource::class)
->fillForm([
...$newData->toArray(),
Expand Down

0 comments on commit 21aeac6

Please # to comment.