Initial commit

This commit is contained in:
root
2026-05-09 17:28:23 +02:00
commit 9d73f82529
5575 changed files with 281989 additions and 0 deletions
+239
View File
@@ -0,0 +1,239 @@
<?php
declare(strict_types=1);
namespace App\Filament\Pages\Radio;
use App\Models\RadioContest;
use Filament\Actions\Action;
use Filament\Forms\Components\DateTimePicker;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
final class ContestManagement extends Page implements HasForms
{
use InteractsWithForms;
#[\Override]
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-trophy';
#[\Override]
protected static string|\UnitEnum|null $navigationGroup = 'Radio';
#[\Override]
protected static ?string $navigationLabel = null;
#[\Override]
public static function getNavigationLabel(): string
{
return __('Contests');
}
#[\Override]
protected static ?string $title = 'Competities Beheer';
#[\Override]
protected string $view = 'filament.pages.radio.contest-management';
/** @var array<string, mixed> */
public array $data = [];
public function mount(): void
{
$this->fillForm();
}
protected function fillForm(): void
{
$contests = RadioContest::orderBy('created_at', 'desc')->get();
$this->data['contests'] = $contests->toArray();
$this->data['active_contest'] = $contests->firstWhere('is_active', true)?->id;
}
public function form(Schema $schema): Schema
{
return $schema
->components([
Section::make('Nieuwe Competitie Aanmaken')
->description('Maak een nieuwe competitie aan')
->columns(1)
->headerActions([
Action::make('create_contest')
->label('Competitie Aanmaken')
->action('createContest')
->color('success'),
])
->schema([
TextInput::make('contest.title')
->label('Titel')
->required()
->placeholder('Bijv. Beste Song Suggestie'),
Textarea::make('contest.description')
->label('Beschrijving')
->rows(3)
->placeholder('Beschrijf de competitie regels...'),
TextInput::make('contest.prize')
->label('Prijs')
->required()
->placeholder('Bijv. VIP voor 1 maand'),
TextInput::make('contest.max_winners')
->label('Aantal Winnaars')
->numeric()
->default(1)
->placeholder('1'),
DateTimePicker::make('contest.starts_at')
->label('Start Datum/Tijd')
->required()
->default(now()),
DateTimePicker::make('contest.ends_at')
->label('Eind Datum/Tijd')
->required()
->default(now()->addDays(14)),
Toggle::make('contest.require_approval')
->label('Goedkeuring Vereist')
->default(false),
]),
Section::make('Actieve Competities')
->description('Beheer lopende competities')
->columns(1)
->schema([
Select::make('selected_contest')
->label('Selecteer Competitie')
->options(RadioContest::where('is_active', true)->pluck('title', 'id'))
->placeholder('Kies een competitie...')
->live(),
]),
Section::make('Acties')
->description('Competitie beheer')
->columns(2)
->schema([
Action::make('end_contest')
->label('Competitie Beëindigen')
->action('endContest')
->color('danger')
->requiresConfirmation(),
Action::make('declare_winners')
->label('Winnaars Bekend Maken')
->action('declareWinners')
->color('warning')
->requiresConfirmation(),
]),
Section::make('Afgelopen Competities')
->description('Bekijk resultaten van afgelopen competities')
->columns(1)
->schema([
Select::make('ended_contest')
->label('Selecteer Afgelopen Competitie')
->options(RadioContest::where('is_ended', true)->pluck('title', 'id'))
->placeholder('Kies een competitie...'),
]),
])
->statePath('data');
}
public function createContest(): void
{
$data = $this->data['contest'] ?? [];
$contest = RadioContest::create([
'title' => $data['title'],
'description' => $data['description'] ?? '',
'prize' => $data['prize'],
'max_winners' => $data['max_winners'] ?? 1,
'starts_at' => $data['starts_at'] ?? now(),
'ends_at' => $data['ends_at'] ?? now()->addDays(14),
'is_active' => true,
'is_ended' => false,
'winners_announced' => false,
'require_approval' => $data['require_approval'] ?? false,
]);
$this->fillForm();
Notification::make()
->success()
->title('Competitie Aangemaakt')
->body("Competitie '{$contest->title}' is succesvol aangemaakt!")
->send();
}
public function endContest(): void
{
$contestId = $this->data['selected_contest'] ?? null;
if (! $contestId) {
Notification::make()
->warning()
->title('Geen Competitie Geselecteerd')
->body('Selecteer eerst een competitie.')
->send();
return;
}
$contest = RadioContest::find($contestId);
$contest->update(['is_ended' => true, 'is_active' => false]);
$this->fillForm();
Notification::make()
->success()
->title('Competitie Beëindigd')
->body("Competitie '{$contest->title}' is beëindigd.")
->send();
}
public function declareWinners(): void
{
$contestId = $this->data['selected_contest'] ?? null;
if (! $contestId) {
Notification::make()
->warning()
->title('Geen Competitie Geselecteerd')
->body('Selecteer eerst een competitie.')
->send();
return;
}
$contest = RadioContest::find($contestId);
$entries = $contest->entries()->with('user')->get();
if ($entries->isEmpty()) {
Notification::make()
->warning()
->title('Geen Inzendingen')
->body('Er zijn nog geen inzendingen.')
->send();
return;
}
$winners = $entries->random(min($contest->max_winners, $entries->count()));
foreach ($winners as $winner) {
$winner->markAsWinner();
}
$contest->update(['winners_announced' => true]);
$this->fillForm();
Notification::make()
->success()
->title('Winnaars Bekend!')
->body(count($winners) . ' winnaars zijn geselecteerd!')
->send();
}
}
+260
View File
@@ -0,0 +1,260 @@
<?php
declare(strict_types=1);
namespace App\Filament\Pages\Radio;
use App\Models\RadioGiveaway;
use Filament\Actions\Action;
use Filament\Forms\Components\DateTimePicker;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
final class GiveawayManagement extends Page implements HasForms
{
use InteractsWithForms;
#[\Override]
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-gift';
#[\Override]
protected static string|\UnitEnum|null $navigationGroup = 'Radio';
#[\Override]
protected static ?string $navigationLabel = null;
#[\Override]
public static function getNavigationLabel(): string
{
return __('Giveaways');
}
#[\Override]
protected static ?string $title = 'Winacties Beheer';
#[\Override]
protected string $view = 'filament.pages.radio.giveaway-management';
/** @var array<string, mixed> */
public array $data = [];
public RadioGiveaway $currentGiveaway;
public function mount(): void
{
$this->fillForm();
}
protected function fillForm(): void
{
$giveaways = RadioGiveaway::orderBy('created_at', 'desc')->get();
$this->data['giveaways'] = $giveaways->toArray();
$this->data['active_giveaway'] = $giveaways->firstWhere('is_active', true)?->id;
}
public function form(Schema $schema): Schema
{
return $schema
->components([
Section::make('Nieuwe Winactie Aanmaken')
->description('Maak een nieuwe winactie aan voor luisteraars')
->columns(1)
->headerActions([
Action::make('create_giveaway')
->label('Winactie Aanmaken')
->action('createGiveaway')
->color('success'),
])
->schema([
TextInput::make('giveaway.title')
->label('Titel')
->required()
->placeholder('Bijv. Win VIP voor 1 maand!'),
Textarea::make('giveaway.description')
->label('Beschrijving')
->rows(3)
->placeholder('Beschrijf de winactie en de prijs...'),
TextInput::make('giveaway.prize')
->label('Prijs')
->required()
->placeholder('Bijv. VIP Lifetime'),
TextInput::make('giveaway.prize_value')
->label('Prijs Waarde (EUR)')
->numeric()
->placeholder('Bijv. 25.00'),
DateTimePicker::make('giveaway.starts_at')
->label('Start Datum/Tijd')
->required()
->default(now()),
DateTimePicker::make('giveaway.ends_at')
->label('Eind Datum/Tijd')
->required()
->default(now()->addDays(7)),
]),
Section::make('Actieve Winacties')
->description('Beheer lopende winacties')
->columns(1)
->schema([
Select::make('selected_giveaway')
->label('Selecteer Winactie')
->options(RadioGiveaway::where('is_active', true)->pluck('title', 'id'))
->placeholder('Kies een winactie...')
->live()
->afterStateUpdated(function ($state) {
if ($state) {
$this->currentGiveaway = RadioGiveaway::find($state);
}
}),
]),
Section::make('Acties')
->description('Winactie beheer')
->columns(2)
->schema([
Action::make('export_participants')
->label('Deelnemers Exporteren')
->action('exportParticipants')
->color('info'),
Action::make('pick_winner')
->label('Winnaar Trekken')
->action('pickWinner')
->color('warning')
->requiresConfirmation(),
Action::make('end_giveaway')
->label('Winactie Beëindigen')
->action('endGiveaway')
->color('danger')
->requiresConfirmation(),
]),
Section::make('Afgelopen Winacties')
->description('Bekijk resultaten van afgelopen winacties')
->columns(1)
->schema([
Select::make('ended_giveaway')
->label('Selecteer Afgelopen Winactie')
->options(RadioGiveaway::where('is_ended', true)->pluck('title', 'id'))
->placeholder('Kies een winactie...'),
]),
])
->statePath('data');
}
public function createGiveaway(): void
{
$data = $this->data['giveaway'] ?? [];
$giveaway = RadioGiveaway::create([
'title' => $data['title'],
'description' => $data['description'] ?? '',
'prize' => $data['prize'],
'prize_value' => $data['prize_value'] ?? 0,
'starts_at' => $data['starts_at'] ?? now(),
'ends_at' => $data['ends_at'] ?? now()->addDays(7),
'is_active' => true,
'is_ended' => false,
'winner_announced' => false,
]);
$this->fillForm();
Notification::make()
->success()
->title('Winactie Aangemaakt')
->body("Winactie '{$giveaway->title}' is succesvol aangemaakt!")
->send();
}
public function pickWinner(): void
{
$giveawayId = $this->data['selected_giveaway'] ?? null;
if (! $giveawayId) {
Notification::make()
->warning()
->title('Geen Winactie Geselecteerd')
->body('Selecteer eerst een winactie.')
->send();
return;
}
$giveaway = RadioGiveaway::find($giveawayId);
if (! $giveaway) {
Notification::make()
->danger()
->title('Winactie Niet Gevonden')
->body('De geselecteerde winactie bestaat niet meer.')
->send();
return;
}
$participants = $giveaway->participants()->with('user')->get();
if ($participants->isEmpty()) {
Notification::make()
->warning()
->title('Geen Deelnemers')
->body('Er zijn nog geen deelnemers voor deze winactie.')
->send();
return;
}
$winner = $participants->random();
$giveaway->announceWinner($winner->user);
Notification::make()
->success()
->title('Winnaar Gekozen!')
->body("Gefeliciteerd {$winner->user->username}! Je hebt {$giveaway->prize} gewonnen!")
->send();
$this->fillForm();
}
public function endGiveaway(): void
{
$giveawayId = $this->data['selected_giveaway'] ?? null;
if (! $giveawayId) {
Notification::make()
->warning()
->title('Geen Winactie Geselecteerd')
->body('Selecteer eerst een winactie.')
->send();
return;
}
$giveaway = RadioGiveaway::find($giveawayId);
$giveaway->update(['is_ended' => true, 'is_active' => false]);
$this->fillForm();
Notification::make()
->success()
->title('Winactie Beëindigd')
->body("Winactie '{$giveaway->title}' is beëindigd.")
->send();
}
public function exportParticipants(): void
{
Notification::make()
->info()
->title('Export Gestart')
->body('Deelnemerslijst wordt gedownload...')
->send();
}
}
+245
View File
@@ -0,0 +1,245 @@
<?php
declare(strict_types=1);
namespace App\Filament\Pages\Radio;
use App\Models\Miscellaneous\WebsiteSetting;
use App\Models\RadioListenerPoint;
use App\Models\User;
use App\Services\PointsService;
use Filament\Actions\Action;
use Filament\Forms\Components\Slider;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
final class PointsSettings extends Page implements HasForms
{
use InteractsWithForms;
#[\Override]
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-currency-euro';
#[\Override]
protected static string|\UnitEnum|null $navigationGroup = 'Radio';
#[\Override]
protected static ?string $navigationLabel = null;
#[\Override]
public static function getNavigationLabel(): string
{
return __('Points System');
}
#[\Override]
protected static ?string $title = 'Punten Systeem';
#[\Override]
protected string $view = 'filament.pages.radio.points-settings';
/** @var array<string, mixed> */
public array $data = [];
private PointsService $pointsService;
public function __construct()
{
$this->pointsService = new PointsService;
}
public function mount(): void
{
$this->fillForm();
}
protected function fillForm(): void
{
$this->data = [
'points_enabled' => $this->getSettingBool('points_enabled'),
'points_per_minute' => (int) $this->getSetting('points_per_minute', '1'),
'max_points_per_day' => (int) $this->getSetting('max_points_per_day', '100'),
'points_for_request' => (int) $this->getSetting('points_for_request', '5'),
'points_for_vote' => (int) $this->getSetting('points_for_vote', '2'),
'points_for_giveaway_win' => (int) $this->getSetting('points_for_giveaway_win', '50'),
'points_for_contest_win' => (int) $this->getSetting('points_for_contest_win', '100'),
'points_for_shout' => (int) $this->getSetting('points_for_shout', '1'),
];
}
public function form(Schema $schema): Schema
{
return $schema
->components([
Section::make('Punten Configuratie')
->description('Configureer het punten systeem')
->columns(2)
->headerActions([
Action::make('save_points')
->label('Opslaan')
->action('savePoints')
->color('primary'),
])
->schema([
Toggle::make('points_enabled')
->label('Punten Systeem Inschakelen')
->columnSpanFull(),
Section::make('Luisteren')
->description('Punten voor luisteren')
->columns(1)
->schema([
Slider::make('points_per_minute')
->label('Punten per Minuut')
->minValue(0)
->maxValue(10)
->step(0.5)
->default(1),
TextInput::make('max_points_per_day')
->label('Max. Punten per Dag')
->numeric()
->default(100),
]),
Section::make('Acties')
->description('Punten voor acties')
->columns(1)
->schema([
TextInput::make('points_for_request')
->label('Punten voor Song Request')
->numeric()
->default(5),
TextInput::make('points_for_vote')
->label('Punten voor Stemmen')
->numeric()
->default(2),
TextInput::make('points_for_shout')
->label('Punten voor Shout')
->numeric()
->default(1),
]),
Section::make('Winnaars')
->description('Punten voor winst')
->columns(1)
->schema([
TextInput::make('points_for_giveaway_win')
->label('Punten voor Winactie Winst')
->numeric()
->default(50),
TextInput::make('points_for_contest_win')
->label('Punten voor Competitie Winst')
->numeric()
->default(100),
]),
]),
Section::make('Acties')
->description('Systeem acties')
->columns(2)
->schema([
Action::make('reset_leaderboard')
->label('Leaderboard Resetten')
->action('resetLeaderboard')
->color('warning')
->requiresConfirmation(),
Action::make('view_stats')
->label('Bekijk Statistieken')
->action('viewStats')
->color('info'),
Action::make('export_leaderboard')
->label('Exporteren')
->action('exportLeaderboard')
->color('secondary'),
]),
])
->statePath('data');
}
public function savePoints(): void
{
$keys = [
'points_enabled',
'points_per_minute',
'max_points_per_day',
'points_for_request',
'points_for_vote',
'points_for_shout',
'points_for_giveaway_win',
'points_for_contest_win',
];
foreach ($keys as $key) {
$value = $this->data[$key] ?? '';
if ($key === 'points_enabled') {
$dbValue = $value ? '1' : '0';
} else {
$dbValue = (string) $value;
}
WebsiteSetting::updateOrCreate(['key' => $key], ['value' => $dbValue]);
}
$this->pointsService->clearSettingsCache();
$this->fillForm();
Notification::make()
->success()
->title(__('Saved'))
->body('Punten instellingen zijn bijgewerkt!')
->send();
}
public function resetLeaderboard(): void
{
User::where('radio_points', '>', 0)->update(['radio_points' => 0]);
RadioListenerPoint::query()->delete();
$this->pointsService->clearLeaderboardCache();
$this->fillForm();
Notification::make()
->success()
->title('Leaderboard Gereset')
->body('Alle punten zijn gewist.')
->send();
}
public function viewStats(): void
{
$stats = $this->pointsService->getStats();
Notification::make()
->title('Punten Statistieken')
->body(
"Totale punten: {$stats['total_points_awarded']}\n" .
"Actieve gebruikers: {$stats['total_active_users']}",
)
->info()
->send();
}
public function exportLeaderboard(): void
{
Notification::make()
->info()
->title('Export Gestart')
->body('Leaderboard wordt gedownload...')
->send();
}
private function getSettingBool(string $key): bool
{
return (bool) WebsiteSetting::where('key', $key)->first()?->value;
}
private function getSetting(string $key, string $default = ''): string
{
return WebsiteSetting::where('key', $key)->first()?->value ?? $default;
}
}
File diff suppressed because it is too large Load Diff
+310
View File
@@ -0,0 +1,310 @@
<?php
declare(strict_types=1);
namespace App\Filament\Pages\Radio;
use App\Models\Miscellaneous\WebsiteSetting;
use App\Services\StreamMonitoringService;
use Filament\Actions\Action;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\Tabs;
use Filament\Schemas\Components\Tabs\Tab;
use Filament\Schemas\Schema;
use Illuminate\Support\Facades\Http;
final class StreamMonitoring extends Page implements HasForms
{
use InteractsWithForms;
#[\Override]
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-signal';
#[\Override]
protected static string|\UnitEnum|null $navigationGroup = 'Radio';
#[\Override]
protected static ?string $navigationLabel = null;
#[\Override]
public static function getNavigationLabel(): string
{
return __('Stream Status');
}
#[\Override]
protected static ?string $title = 'Stream Monitoring';
#[\Override]
protected string $view = 'filament.pages.radio.stream-monitoring';
/** @var array<string, mixed> */
public array $data = [];
private StreamMonitoringService $monitoringService;
public function __construct()
{
$this->monitoringService = new StreamMonitoringService;
}
public function mount(): void
{
$this->fillForm();
}
protected function fillForm(): void
{
$this->data = [
'radio_stream_url' => $this->getSetting('radio_stream_url', ''),
'radio_monitoring_enabled' => $this->getSettingBool('radio_monitoring_enabled'),
'radio_monitoring_interval' => (int) $this->getSetting('radio_monitoring_interval', '5'),
'radio_alert_email' => $this->getSetting('radio_alert_email', ''),
'radio_alert_enabled' => $this->getSettingBool('radio_alert_enabled'),
];
}
public function form(Schema $schema): Schema
{
return $schema
->components([
Tabs::make('Monitoring')
->tabs([
Tab::make('Huidige Status')
->icon('heroicon-o-signal')
->schema([
Section::make('Stream Status')
->description('Real-time stream monitoring')
->columns(2)
->headerActions([
Action::make('refresh')
->label('Vernieuwen')
->action('refreshStatus')
->color('primary')
->icon('heroicon-o-arrow-path'),
])
->schema([
Section::make('Info')
->description('Stream informatie')
->columns(1)
->schema([
TextInput::make('status.online')
->label('Status')
->disabled(),
TextInput::make('status.listeners')
->label('Luisteraars')
->disabled(),
TextInput::make('status.bitrate')
->label('Bitrate')
->disabled(),
TextInput::make('status.format')
->label('Formaat')
->disabled(),
TextInput::make('status.server')
->label('Server')
->disabled(),
]),
]),
Section::make('Uptime')
->description('Uptime statistieken')
->columns(2)
->schema([
TextInput::make('uptime.last_online')
->label('Laatst Online')
->disabled(),
TextInput::make('uptime.uptime_pct')
->label('Uptime %')
->disabled(),
]),
]),
Tab::make('Instellingen')
->icon('heroicon-o-cog-6-tooth')
->schema([
Section::make('Monitoring Configuratie')
->description('Configureer monitoring')
->columns(2)
->headerActions([
Action::make('save_monitoring')
->label('Opslaan')
->action('saveMonitoring')
->color('primary'),
])
->schema([
Toggle::make('radio_monitoring_enabled')
->label('Monitoring Inschakelen')
->columnSpanFull(),
TextInput::make('radio_monitoring_interval')
->label('Check Interval (minuten)')
->numeric()
->default(5),
]),
Section::make('Alert Instellingen')
->description('Alert e-mails')
->columns(2)
->headerActions([
Action::make('test_alert')
->label('Test Alert')
->action('testAlert')
->color('info'),
])
->schema([
Toggle::make('radio_alert_enabled')
->label('Alerts Inschakelen')
->columnSpanFull(),
TextInput::make('radio_alert_email')
->label('Alert E-mail')
->email()
->placeholder('admin@jouwdomein.nl'),
]),
Section::make('Stream URL')
->description('Stream configuratie')
->columns(1)
->schema([
TextInput::make('radio_stream_url')
->label('Stream URL')
->url()
->placeholder('https://radio.nl/stream')
->columnSpanFull(),
Action::make('test_stream')
->label('Test Stream')
->action('testStream')
->color('success'),
]),
]),
]),
])
->statePath('data');
}
public function refreshStatus(): void
{
$status = $this->monitoringService->getStatus();
$this->data['status'] = [
'online' => $status['online'] ? 'Online' : 'Offline',
'listeners' => $status['listeners'],
'bitrate' => $status['bitrate'] ? $status['bitrate'] . ' kbps' : 'N/A',
'format' => $status['format'] ?? 'N/A',
'server' => $status['server_type'] ?? 'N/A',
];
$uptime = $this->monitoringService->getUptime();
$this->data['uptime'] = [
'last_online' => $uptime['last_online_human'] ?? 'Nooit',
'uptime_pct' => ($uptime['uptime_percentage'] ?? 0) . '%',
];
$this->fillForm();
Notification::make()
->success()
->title('Status Vernieuwd')
->body($status['online'] ? 'Stream is Online' : 'Stream is Offline')
->send();
}
public function saveMonitoring(): void
{
$keys = [
'radio_monitoring_enabled', 'radio_monitoring_interval',
'radio_alert_enabled', 'radio_alert_email',
];
foreach ($keys as $key) {
$value = $this->data[$key] ?? '';
if (in_array($key, ['radio_monitoring_enabled', 'radio_alert_enabled'])) {
$dbValue = $value ? '1' : '0';
} else {
$dbValue = (string) $value;
}
WebsiteSetting::updateOrCreate(['key' => $key], ['value' => $dbValue]);
}
$this->fillForm();
Notification::make()
->success()
->title(__('Saved'))
->body('Monitoring instellingen zijn bijgewerkt!')
->send();
}
public function testStream(): void
{
$url = $this->data['radio_stream_url'] ?? '';
if (empty($url)) {
Notification::make()
->warning()
->title('URL Ontbreekt')
->body('Voer eerst een stream URL in.')
->send();
return;
}
try {
$response = Http::timeout(10)->head($url);
if ($response->successful()) {
Notification::make()
->success()
->title('Stream Bereikbaar!')
->body('De stream URL is correct.')
->send();
} else {
Notification::make()
->danger()
->title('Stream Niet Bereikbaar')
->body('Status: ' . $response->status())
->send();
}
} catch (\Exception $e) {
Notification::make()
->danger()
->title('Verbindingsfout')
->body($e->getMessage())
->send();
}
}
public function testAlert(): void
{
$email = $this->data['radio_alert_email'] ?? '';
if (empty($email) || ! filter_var($email, FILTER_VALIDATE_EMAIL)) {
Notification::make()
->warning()
->title('Ongeldig E-mail')
->body('Voer een geldig e-mailadres in.')
->send();
return;
}
Notification::make()
->info()
->title('Test Alert')
->body('Test alert zou verstuurd moeten worden.')
->send();
}
private function getSettingBool(string $key): bool
{
return (bool) WebsiteSetting::where('key', $key)->first()?->value;
}
private function getSetting(string $key, string $default = ''): string
{
return WebsiteSetting::where('key', $key)->first()?->value ?? $default;
}
}
+446
View File
@@ -0,0 +1,446 @@
<?php
declare(strict_types=1);
namespace App\Filament\Pages\Radio;
use App\Models\Miscellaneous\WebsiteSetting;
use App\Services\ContentModerationService;
use Filament\Actions\Action;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Slider;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
use Illuminate\Support\Facades\Http;
final class WordFilterSettings extends Page implements HasForms
{
use InteractsWithForms;
#[\Override]
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-shield-check';
#[\Override]
protected static string|\UnitEnum|null $navigationGroup = 'Radio';
#[\Override]
protected static ?string $navigationLabel = null;
#[\Override]
public static function getNavigationLabel(): string
{
return __('Word Filter');
}
#[\Override]
protected static ?string $title = 'AI Woord Filter';
#[\Override]
protected string $view = 'filament.pages.radio.word-filter-settings';
/** @var array<string, mixed> */
public array $data = [];
/** @var array<mixed> */
public array $blockedWords = [];
private ContentModerationService $moderationService;
public function __construct()
{
$this->moderationService = new ContentModerationService;
}
public function mount(): void
{
$this->fillForm();
}
protected function fillForm(): void
{
$this->data = [
'ai_filter_enabled' => $this->getSettingBool('ai_filter_enabled'),
'ai_filter_auto_reject' => $this->getSettingBool('ai_filter_auto_reject'),
'ai_filter_sensitivity' => (float) $this->getSetting('ai_filter_sensitivity', '0.7'),
'ai_filter_method' => $this->getSetting('ai_filter_method', 'local'),
'ai_filter_shouts' => $this->getSettingBool('ai_filter_shouts'),
'ai_filter_chat' => $this->getSettingBool('ai_filter_chat'),
'ai_filter_applications' => $this->getSettingBool('ai_filter_applications'),
'ai_filter_requests' => $this->getSettingBool('ai_filter_requests'),
'ai_filter_log_rejected' => $this->getSettingBool('ai_filter_log_rejected'),
'openai_api_key' => $this->getSetting('openai_api_key', ''),
];
$this->blockedWords = array_map(fn ($word) => ['word' => $word], $this->moderationService->getBlockedWords());
}
public function form(Schema $schema): Schema
{
return $schema
->components([
Section::make('Algemene Instellingen')
->description('Basis configuratie voor het AI woordfilter')
->columns(2)
->afterHeader([
Action::make('test_filter')
->label('Filter Testen')
->action('testFilter')
->color('info'),
Action::make('save_general')
->label('Opslaan')
->action('saveGeneral')
->color('primary'),
])
->schema([
Toggle::make('ai_filter_enabled')
->label('Filter Inschakelen')
->columnSpanFull()
->helperText('Schakel AI-gebaseerde inhoudsfiltratie in voor alle radio gerelateerde content'),
Toggle::make('ai_filter_auto_reject')
->label('Auto Afwijzen')
->helperText('Inhoud automatisch afwijzen zonder handmatige goedkeuring'),
Select::make('ai_filter_method')
->label('Filter Methode')
->options([
'local' => 'Lokaal (Woordenlijst)',
'openai' => 'OpenAI API (AI-gebaseerd)',
'both' => 'Beide Gecombineerd',
])
->default('local'),
Slider::make('ai_filter_sensitivity')
->label('Gevoeligheid')
->minValue(0.1)
->maxValue(1.0)
->step(0.05)
->default(0.7)
->helperText('Hogere waarde = strenger filter (0.1 = lax, 1.0 = strikt)'),
]),
Section::make('OpenAI API Configuratie')
->description('Configureer OpenAI voor AI-gebaseerde detectie')
->columns(1)
->afterHeader([
Action::make('test_openai')
->label('Test API')
->action('testOpenAI')
->color('success'),
])
->schema([
TextInput::make('openai_api_key')
->label('OpenAI API Key')
->password()
->placeholder('sk-...')
->helperText('Gratis moderatie endpoint beschikbaar via OpenAI. Haal je key op bij platform.openai.com'),
]),
Section::make('Te Filteren Inhoud')
->description('Selecteer welke types content gefilterd moeten worden')
->columns(2)
->afterHeader([
Action::make('save_content')
->label('Opslaan')
->action('saveContent')
->color('primary'),
])
->schema([
Toggle::make('ai_filter_shouts')
->label('Shouts/Chat Berichten')
->helperText('Filter alle shouts en chat berichten'),
Toggle::make('ai_filter_chat')
->label('Live Chat')
->helperText('Filter live chat berichten in real-time'),
Toggle::make('ai_filter_applications')
->label('DJ Aanmeldingen')
->helperText('Filter DJ sollicitatie formulieren'),
Toggle::make('ai_filter_requests')
->label('Song Verzoeken')
->helperText('Filter song request berichten'),
Toggle::make('ai_filter_log_rejected')
->label('Log Afwijzingen')
->helperText('Bewaar een logboek van afgewezen berichten'),
]),
Section::make('Geblokkeerde Woorden')
->description('Beheer de lokale woordenlijst voor directe filtering')
->columns(1)
->afterHeader([
Action::make('add_word')
->label('Woord Toevoegen')
->action('addWord')
->color('success'),
Action::make('clear_all')
->label('Alles Wissen')
->action('clearAllWords')
->color('danger')
->requiresConfirmation(),
Action::make('save_words')
->label('Opslaan')
->action('saveWords')
->color('primary'),
])
->schema([
Repeater::make('blocked_words')
->label('Geblokkeerde Woorden')
->schema([
TextInput::make('word')
->label('Woord')
->required()
->placeholder('vul een woord in'),
])
->addActionLabel('Woord toevoegen')
->defaultItems(0),
]),
Section::make('Bulk Acties')
->description('Snelle acties voor de woordenlijst')
->columns(2)
->schema([
Action::make('add_common_words')
->label('Veelvoorkomende Woorden')
->action('addCommonWords')
->color('secondary')
->requiresConfirmation(),
Action::make('export_words')
->label('Exporteren')
->action('exportWords')
->color('secondary'),
]),
Section::make('Filter Statistieken')
->description('Overzicht van filter activiteit')
->columns(2)
->schema([
Action::make('view_stats')
->label('Bekijk Statistieken')
->action('viewStats')
->color('info'),
Action::make('reset_stats')
->label('Resetten')
->action('resetStats')
->color('warning')
->requiresConfirmation(),
]),
])
->statePath('data');
}
public function testFilter(): void
{
$testPhrases = [
'Dit is een normale zin',
'Dit bevat slecht_woord',
];
$results = [];
foreach ($testPhrases as $phrase) {
$result = $this->moderationService->moderate($phrase);
$results[] = [
'phrase' => $phrase,
'approved' => $result['approved'],
'score' => $result['score'],
];
}
Notification::make()
->title('Filter Test')
->body(json_encode($results, JSON_PRETTY_PRINT))
->success()
->send();
}
public function testOpenAI(): void
{
$apiKey = $this->data['openai_api_key'] ?? '';
if (empty($apiKey)) {
Notification::make()
->warning()
->title('API Key Ontbreekt')
->body('Voer eerst een OpenAI API key in.')
->send();
return;
}
try {
$response = Http::withHeaders([
'Authorization' => 'Bearer ' . $apiKey,
'Content-Type' => 'application/json',
])->post('https://api.openai.com/v1/moderations', [
'input' => 'Test message',
]);
if ($response->successful()) {
Notification::make()
->success()
->title('OpenAI API Werkt!')
->body('API verbinding succesvol.')
->send();
} else {
Notification::make()
->danger()
->title('API Fout')
->body('Status: ' . $response->status())
->send();
}
} catch (\Exception $e) {
Notification::make()
->danger()
->title('Verbindingsfout')
->body($e->getMessage())
->send();
}
}
public function addWord(): void
{
Notification::make()
->info()
->title('Woord Toevoegen')
->body('Gebruik de repeater om woorden toe te voegen.')
->send();
}
public function addCommonWords(): void
{
$commonWords = [
'slecht', 'vervelend', 'stom', 'idiot', 'debiel', 'kanker', 'hoer',
'kut', 'godver', 'verdomme', 'fuck', 'shit', 'bitch', 'asshole',
];
foreach ($commonWords as $word) {
$this->moderationService->addBlockedWord(trim($word));
}
$this->fillForm();
Notification::make()
->success()
->title('Woorden Toegevoegd')
->body(count($commonWords) . ' woorden toegevoegd.')
->send();
}
public function clearAllWords(): void
{
foreach ($this->moderationService->getBlockedWords() as $word) {
$this->moderationService->removeBlockedWord($word);
}
$this->fillForm();
Notification::make()
->success()
->title('Wissen Succesvol')
->body('Alle woorden verwijderd.')
->send();
}
public function saveGeneral(): void
{
$this->saveSettings([
'ai_filter_enabled', 'ai_filter_auto_reject', 'ai_filter_sensitivity',
'ai_filter_method', 'openai_api_key',
]);
}
public function saveContent(): void
{
$this->saveSettings([
'ai_filter_shouts', 'ai_filter_chat', 'ai_filter_applications',
'ai_filter_requests', 'ai_filter_log_rejected',
]);
}
public function saveWords(): void
{
foreach ($this->blockedWords as $item) {
if (! in_array(trim($item['word'] ?? ''), ['', '0'], true)) {
$this->moderationService->addBlockedWord(trim((string) $item['word']));
}
}
Notification::make()
->success()
->title(__('Saved'))
->body('Woordenlijst bijgewerkt.')
->send();
}
public function exportWords(): void
{
$words = $this->moderationService->getBlockedWords();
$filename = 'blocked_words_' . date('Y-m-d') . '.json';
response()->streamDownload(function () use ($words) {
echo json_encode($words, JSON_PRETTY_PRINT);
}, $filename)->send();
Notification::make()
->success()
->title('Export Gestart')
->body('Download is gestart.')
->send();
}
public function viewStats(): void
{
$stats = $this->moderationService->getStats();
Notification::make()
->title('Filter Statistieken')
->body(json_encode($stats, JSON_PRETTY_PRINT))
->info()
->send();
}
public function resetStats(): void
{
WebsiteSetting::where('key', 'like', 'filter_stats_%')->delete();
Notification::make()
->success()
->title('Gereset')
->body('Statistieken zijn gewist.')
->send();
}
private function saveSettings(array $keys): void
{
foreach ($keys as $key) {
$value = $this->data[$key] ?? '';
$boolKeys = [
'ai_filter_enabled', 'ai_filter_auto_reject', 'ai_filter_shouts',
'ai_filter_chat', 'ai_filter_applications', 'ai_filter_requests',
'ai_filter_log_rejected',
];
$dbValue = in_array($key, $boolKeys) ? ($value ? '1' : '0') : (string) $value;
WebsiteSetting::updateOrCreate(['key' => $key], ['value' => $dbValue]);
}
$this->fillForm();
Notification::make()
->success()
->title(__('Saved'))
->body('Instellingen zijn opgeslagen!')
->send();
}
private function getSettingBool(string $key): bool
{
return (bool) WebsiteSetting::where('key', $key)->first()?->value;
}
private function getSetting(string $key, string $default = ''): string
{
return WebsiteSetting::where('key', $key)->first()?->value ?? $default;
}
}