Skip to content

Commit 2e233ee

Browse files
committed
Add widgets to the dashboards
1 parent 5006f25 commit 2e233ee

File tree

9 files changed

+388
-6
lines changed

9 files changed

+388
-6
lines changed
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php
2+
3+
namespace App\Filament\Widgets;
4+
5+
use App\Models\Article;
6+
use Filament\Widgets\StatsOverviewWidget as BaseWidget;
7+
use Filament\Widgets\StatsOverviewWidget\Stat;
8+
9+
class ArticlesStatsOverview extends BaseWidget
10+
{
11+
protected ?string $pollingInterval = '30s';
12+
13+
protected function getStats(): array
14+
{
15+
$window = 30;
16+
$cacheTtlSeconds = 300; // 5 minutes
17+
18+
$total = cache()->remember(
19+
"widgets:articles:total",
20+
now()->addSeconds($cacheTtlSeconds),
21+
fn() => Article::query()->count(),
22+
);
23+
24+
$publishedTotal = cache()->remember(
25+
"widgets:articles:published:total",
26+
now()->addSeconds($cacheTtlSeconds),
27+
fn() => Article::query()->published()->count(),
28+
);
29+
30+
$publishedWindow = cache()->remember(
31+
"widgets:articles:published:{$window}",
32+
now()->addSeconds($cacheTtlSeconds),
33+
fn() => Article::query()
34+
->published()
35+
->where('submitted_at', '>=', now()->subDays($window))
36+
->count(),
37+
);
38+
39+
$awaiting = cache()->remember(
40+
"widgets:articles:awaiting",
41+
now()->addSeconds(60),
42+
fn() => Article::query()->awaitingApproval()->count(),
43+
);
44+
45+
return [
46+
Stat::make('Articles', number_format($total))
47+
->description('Total articles')
48+
->icon('heroicon-o-newspaper'),
49+
50+
Stat::make('Published', number_format($publishedTotal))
51+
->description('All-time approved')
52+
->color('success')
53+
->icon('heroicon-o-check-circle'),
54+
55+
Stat::make("Published ({$window}d)", number_format($publishedWindow))
56+
->description("Approved in last {$window} days")
57+
->color('info')
58+
->icon('heroicon-o-check-badge'),
59+
60+
Stat::make('Awaiting Approval', number_format($awaiting))
61+
->description('Submitted but not approved')
62+
->color('warning')
63+
->icon('heroicon-o-clock'),
64+
];
65+
}
66+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
namespace App\Filament\Widgets;
4+
5+
use App\Models\Article;
6+
use Carbon\Carbon;
7+
use Filament\Widgets\ChartWidget;
8+
use Flowframe\Trend\Trend;
9+
use Flowframe\Trend\TrendValue;
10+
11+
class ArticlesTrendChart extends ChartWidget
12+
{
13+
protected ?string $heading = 'Articles per Month';
14+
protected ?string $pollingInterval = '30s';
15+
16+
protected function getType(): string
17+
{
18+
return 'line';
19+
}
20+
21+
protected function getData(): array
22+
{
23+
$data = Trend::model(Article::class)
24+
->between(
25+
start: now()->startOfYear(),
26+
end: now()->endOfYear(),
27+
)
28+
->perMonth()
29+
->count();
30+
31+
return [
32+
'datasets' => [
33+
[
34+
'label' => 'Submitted',
35+
'data' => $data->map(fn(TrendValue $value) => $value->aggregate),
36+
'tension' => 0.35,
37+
],
38+
],
39+
'labels' => $data->map(fn(TrendValue $value) => Carbon::parse($value->date)->format('M Y')),
40+
];
41+
}
42+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
namespace App\Filament\Widgets;
4+
5+
use App\Models\Reply;
6+
use Filament\Widgets\StatsOverviewWidget as BaseWidget;
7+
use Filament\Widgets\StatsOverviewWidget\Stat;
8+
9+
class RepliesStatsOverview extends BaseWidget
10+
{
11+
protected ?string $pollingInterval = '30s';
12+
13+
protected function getStats(): array
14+
{
15+
$window = 30;
16+
$cacheTtlSeconds = 300;
17+
18+
$total = cache()->remember(
19+
'widgets:replies:total',
20+
now()->addSeconds($cacheTtlSeconds),
21+
fn() => Reply::query()->count(),
22+
);
23+
24+
$lastWindow = cache()->remember(
25+
"widgets:replies:new:{$window}",
26+
now()->addSeconds($cacheTtlSeconds),
27+
fn() => Reply::query()->where('created_at', '>=', now()->subDays($window))->count(),
28+
);
29+
30+
$solutions = cache()->remember(
31+
'widgets:replies:solutions',
32+
now()->addSeconds($cacheTtlSeconds),
33+
fn() => Reply::query()->isSolution()->count(),
34+
);
35+
36+
return [
37+
Stat::make('Replies', number_format($total))
38+
->description('Total replies')
39+
->icon('heroicon-o-chat-bubble-left-right'),
40+
41+
Stat::make("New ({$window}d)", number_format($lastWindow))
42+
->description("Replies in last {$window} days")
43+
->color('info')
44+
->icon('heroicon-o-chat-bubble-left-ellipsis'),
45+
46+
Stat::make('Solutions', number_format($solutions))
47+
->description('Marked as solution')
48+
->color('success')
49+
->icon('heroicon-o-sparkles'),
50+
];
51+
}
52+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
namespace App\Filament\Widgets;
4+
5+
use App\Models\Reply;
6+
use Carbon\Carbon;
7+
use Filament\Widgets\ChartWidget;
8+
use Flowframe\Trend\Trend;
9+
use Flowframe\Trend\TrendValue;
10+
11+
class RepliesTrendChart extends ChartWidget
12+
{
13+
protected ?string $heading = 'Replies per Month';
14+
protected ?string $pollingInterval = '30s';
15+
16+
protected function getType(): string
17+
{
18+
return 'line';
19+
}
20+
21+
protected function getData(): array
22+
{
23+
$data = Trend::model(Reply::class)
24+
->between(
25+
start: now()->startOfYear(),
26+
end: now()->endOfYear(),
27+
)
28+
->perMonth()
29+
->count();
30+
31+
return [
32+
'datasets' => [
33+
[
34+
'label' => 'Submitted',
35+
'data' => $data->map(fn(TrendValue $value) => $value->aggregate),
36+
'tension' => 0.35,
37+
],
38+
],
39+
'labels' => $data->map(fn(TrendValue $value) => Carbon::parse($value->date)->format('M Y')),
40+
];
41+
}
42+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
namespace App\Filament\Widgets;
4+
5+
use App\Models\User;
6+
// No form usage in Filament v4 widgets; using plain logic.
7+
use Filament\Widgets\StatsOverviewWidget as BaseWidget;
8+
use Filament\Widgets\StatsOverviewWidget\Stat;
9+
10+
class UsersStatsOverview extends BaseWidget
11+
{
12+
protected ?string $pollingInterval = '30s';
13+
14+
protected function getStats(): array
15+
{
16+
$window = 30;
17+
$cacheTtlSeconds = 300;
18+
19+
$total = cache()->remember(
20+
'widgets:users:total',
21+
now()->addSeconds($cacheTtlSeconds),
22+
fn() => User::query()->count(),
23+
);
24+
25+
$lastWindow = cache()->remember(
26+
"widgets:users:new:{$window}",
27+
now()->addSeconds($cacheTtlSeconds),
28+
fn() => User::query()->where('created_at', '>=', now()->subDays($window))->count(),
29+
);
30+
31+
$verified = cache()->remember(
32+
'widgets:users:verified',
33+
now()->addSeconds($cacheTtlSeconds),
34+
fn() => User::query()->whereNotNull('author_verified_at')->count(),
35+
);
36+
37+
return [
38+
Stat::make('Users', number_format($total))
39+
->description('Total registered')
40+
->icon('heroicon-o-users'),
41+
42+
Stat::make("New ({$window}d)", number_format($lastWindow))
43+
->description("Joined in last {$window} days")
44+
->color('info')
45+
->icon('heroicon-o-user-plus'),
46+
47+
Stat::make('Verified Authors', number_format($verified))
48+
->description('Users verified as authors')
49+
->color('success')
50+
->icon('heroicon-o-check'),
51+
];
52+
}
53+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
namespace App\Filament\Widgets;
4+
5+
use App\Models\User;
6+
use Carbon\Carbon;
7+
use Filament\Widgets\ChartWidget;
8+
use Flowframe\Trend\Trend;
9+
use Flowframe\Trend\TrendValue;
10+
11+
class UsersTrendChart extends ChartWidget
12+
{
13+
protected ?string $heading = 'New Users per Month';
14+
protected ?string $pollingInterval = '30s';
15+
16+
protected function getType(): string
17+
{
18+
return 'line';
19+
}
20+
21+
protected function getData(): array
22+
{
23+
$data = Trend::model(User::class)
24+
->between(
25+
start: now()->startOfYear(),
26+
end: now()->endOfYear(),
27+
)
28+
->perMonth()
29+
->count();
30+
31+
return [
32+
'datasets' => [
33+
[
34+
'label' => 'Submitted',
35+
'data' => $data->map(fn(TrendValue $value) => $value->aggregate),
36+
'tension' => 0.35,
37+
],
38+
],
39+
'labels' => $data->map(fn(TrendValue $value) => Carbon::parse($value->date)->format('M Y')),
40+
];
41+
}
42+
}

app/Providers/Filament/AdminPanelProvider.php

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
namespace App\Providers\Filament;
44

5+
use App\Filament\Widgets\ArticlesStatsOverview;
6+
use App\Filament\Widgets\ArticlesTrendChart;
7+
use App\Filament\Widgets\RepliesStatsOverview;
8+
use App\Filament\Widgets\RepliesTrendChart;
9+
use App\Filament\Widgets\UsersStatsOverview;
10+
use App\Filament\Widgets\UsersTrendChart;
511
use Filament\Actions\Action;
612
use Filament\Http\Middleware\Authenticate;
713
use Filament\Http\Middleware\AuthenticateSession;
@@ -10,9 +16,6 @@
1016
use Filament\Pages\Dashboard;
1117
use Filament\Panel;
1218
use Filament\PanelProvider;
13-
use Filament\Support\Colors\Color;
14-
use Filament\Widgets\AccountWidget;
15-
use Filament\Widgets\FilamentInfoWidget;
1619
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
1720
use Illuminate\Cookie\Middleware\EncryptCookies;
1821
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
@@ -37,8 +40,15 @@ public function panel(Panel $panel): Panel
3740
->pages([
3841
Dashboard::class,
3942
])
40-
->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\Filament\Widgets')
41-
->widgets([])
43+
->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\\Filament\\Widgets')
44+
->widgets([
45+
ArticlesStatsOverview::class,
46+
UsersStatsOverview::class,
47+
RepliesStatsOverview::class,
48+
ArticlesTrendChart::class,
49+
UsersTrendChart::class,
50+
RepliesTrendChart::class,
51+
])
4252
->middleware([
4353
EncryptCookies::class,
4454
AddQueuedCookiesToResponse::class,

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"codeat3/blade-simple-icons": "^7.1",
1212
"embed/embed": "^4.4",
1313
"filament/filament": "^4.0",
14+
"flowframe/laravel-trend": "^0.4.0",
1415
"guzzlehttp/guzzle": "^7.2",
1516
"guzzlehttp/psr7": "^2.7",
1617
"innocenzi/bluesky-notification-channel": "^0.2.0",

0 commit comments

Comments
 (0)