feat: notification on filament dashboard

This commit is contained in:
2026-05-22 12:15:23 +02:00
parent 1c472e46db
commit 071d147888
9 changed files with 121 additions and 85 deletions
@@ -35,6 +35,13 @@ class UserInfolist
->label(__('admin.users.fields.account_verified_at'))
->dateTime()
->placeholder(__('admin.users.placeholders.not_verified')),
TextEntry::make('suspended_at')
->label(__('admin.users.fields.suspended_at'))
->dateTime()
->placeholder(__('admin.users.placeholders.not_suspended')),
TextEntry::make('suspendedBy.name')
->label(__('admin.users.fields.suspended_by'))
->placeholder(__('admin.users.placeholders.empty')),
ImageEntry::make('avatar_url')
->label(__('admin.users.fields.avatar'))
->disk('public')
@@ -7,6 +7,8 @@ use App\Enums\PhysicalActivityLevel;
use App\Enums\UserRole;
use App\Enums\UserSex;
use App\Enums\WeightGoal;
use App\Models\User;
use Filament\Actions\Action;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
@@ -49,6 +51,11 @@ class UsersTable
->sortable()
->placeholder(__('admin.users.placeholders.not_verified'))
->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('suspended_at')
->label(__('admin.users.fields.suspended_at'))
->dateTime()
->sortable()
->placeholder(__('admin.users.placeholders.not_suspended')),
ImageColumn::make('avatar_url')
->label(__('admin.users.fields.avatar'))
->disk('public')
@@ -144,10 +151,25 @@ class UsersTable
TernaryFilter::make('account_verified_at')
->label(__('admin.users.filters.account_verified'))
->nullable(),
TernaryFilter::make('suspended_at')
->label(__('admin.users.filters.suspended'))
->nullable(),
])
->recordActions([
ViewAction::make(),
EditAction::make(),
Action::make('suspend')
->label(__('admin.users.actions.suspend'))
->color('danger')
->requiresConfirmation()
->visible(fn (User $record): bool => ! $record->isSuspended() && $record->getKey() !== auth()->id())
->action(fn (User $record): bool => self::suspendUser($record)),
Action::make('unsuspend')
->label(__('admin.users.actions.unsuspend'))
->color('success')
->requiresConfirmation()
->visible(fn (User $record): bool => $record->isSuspended())
->action(fn (User $record): bool => self::unsuspendUser($record)),
])
->toolbarActions([
BulkActionGroup::make([
@@ -155,4 +177,30 @@ class UsersTable
]),
]);
}
private static function suspendUser(User $record): bool
{
if ($record->getKey() === auth()->id()) {
return false;
}
$record->forceFill([
'suspended_at' => now(),
'suspended_by' => auth()->id(),
'suspended_reason' => __('admin.users.actions.suspend'),
])->save();
return true;
}
private static function unsuspendUser(User $record): bool
{
$record->forceFill([
'suspended_at' => null,
'suspended_by' => null,
'suspended_reason' => null,
])->save();
return true;
}
}
@@ -1,73 +0,0 @@
<?php
namespace App\Notifications;
use App\Models\MealPosts;
use App\Models\ModerationCase;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
class ModerationThresholdReached extends Notification
{
use Queueable;
public function __construct(public ModerationCase $moderationCase) {}
public function via(object $notifiable): array
{
return ['database'];
}
public function toArray(object $notifiable): array
{
return $this->payload() + [
'title' => __('api.notifications.moderation_threshold_title'),
'body' => __('api.notifications.moderation_threshold_body', [
'type' => $this->targetLabel(),
'count' => $this->moderationCase->reports_count,
]),
'targetTitle' => $this->targetTitle(),
];
}
private function payload(): array
{
return [
'type' => 'moderation_case_threshold',
'moderationCaseId' => $this->moderationCase->getKey(),
'reportableType' => $this->targetType(),
'reportableId' => $this->moderationCase->caseable_id,
'reportsCount' => $this->moderationCase->reports_count,
];
}
private function targetType(): string
{
return match ($this->moderationCase->caseable_type) {
MealPosts::class => 'meal_post',
User::class => 'user',
default => 'unknown',
};
}
private function targetLabel(): string
{
return match ($this->moderationCase->caseable_type) {
MealPosts::class => __('api.reports.targets.meal_post'),
User::class => __('api.reports.targets.user'),
default => __('api.reports.targets.unknown'),
};
}
private function targetTitle(): string
{
$target = $this->moderationCase->caseable;
return match (true) {
$target instanceof MealPosts => $target->title,
$target instanceof User => $target->name,
default => (string) $this->moderationCase->caseable_id,
};
}
}
@@ -33,6 +33,7 @@ class DashboardPanelProvider extends PanelProvider
->path('dashboard')
->viteTheme('resources/css/filament/dashboard/theme.css')
->login()
->databaseNotifications()
->colors([
'primary' => Color::Amber,
])
+9 -2
View File
@@ -10,7 +10,7 @@ use App\Models\MealPosts;
use App\Models\ModerationCase;
use App\Models\Report;
use App\Models\User;
use App\Notifications\ModerationThresholdReached;
use Filament\Notifications\Notification as FilamentNotification;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\QueryException;
use Illuminate\Support\Facades\DB;
@@ -112,7 +112,14 @@ class ModerationReportService
User::query()
->whereIn('role', [UserRole::ADMIN->value, UserRole::MODERATOR->value])
->get()
->each(fn (User $moderator): mixed => $moderator->notify(new ModerationThresholdReached($freshCase)));
->whenNotEmpty(fn ($moderators) => FilamentNotification::make()
->title(__('api.notifications.moderation_threshold_title'))
->body(__('api.notifications.moderation_threshold_body', [
'type' => $freshCase->targetLabel(),
'count' => $freshCase->reports_count,
]))
->warning()
->sendToDatabase($moderators, isEventDispatched: true));
}
private function abortIfInvalidTarget(Model $reportable, User $reporter): void