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
+338
View File
@@ -0,0 +1,338 @@
<?php
namespace App\Filament\Pages;
use App\Filament\Traits\TranslatableResource;
use App\Models\User;
use App\Services\Parsers\ExternalTextsParser;
use Filament\Actions\Action;
use Filament\Actions\Action as PageAction;
use Filament\Actions\ActionGroup;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
use Filament\Schemas\Components\Form;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Components\Utilities\Set;
use Filament\Schemas\Schema;
use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Throwable;
/**
* @property-read Form $form
*/
class BadgePage extends Page
{
use InteractsWithForms, TranslatableResource;
#[\Override]
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-document-text';
#[\Override]
protected static string|\UnitEnum|null $navigationGroup = 'Hotel';
#[\Override]
protected string $view = 'filament.pages.badge-page';
protected static string $translateIdentifier = 'badge-resource';
public bool $badgeWasPreviouslyCreated = false;
/** @var array<string, mixed> */
public array $data = ['code' => '', 'image' => '', 'nitro' => ['title' => '', 'description' => '']];
public static string $roleName = 'badge_page';
#[\Override]
public static function canAccess(): bool
{
/** @var User|null $user */
$user = auth()->user();
return $user && $user->can('view::admin::' . static::$roleName);
}
#[\Override]
public function getTitle(): string|Htmlable
{
return (string) __(
sprintf('filament::resources.resources.%s.navigation_label', static::$translateIdentifier),
);
}
public function form(Schema $schema): Schema
{
return $schema
->components([
Section::make((string) __('filament::resources.tabs.Main'))
->schema([
TextInput::make('code')
->label((string) __('filament::resources.inputs.badge_code'))
->helperText((string) __('filament::resources.helpers.badge_code_helper'))
->afterStateUpdated(function (?string $state, Set $set) {
$set('code', strtoupper((string) $state));
})
->suffixAction(fn (): PageAction => PageAction::make('search')->icon('heroicon-o-magnifying-glass')->action(fn () => $this->searchBadgesByCode()),
),
TextInput::make('image')
->label((string) __('filament::resources.inputs.badge_image'))
->placeholder('...')
->autocomplete()
->visible(fn (Get $get) => isset($this->data['image']))
->prefixAction(
fn (?string $state): PageAction => PageAction::make('visit')
->icon('heroicon-s-arrow-top-right-on-square')
->tooltip((string) __('filament::resources.common.Open link'))
->url($state)
->visible(fn () => ! in_array($state, [null, '', '0'], true))
->openUrlInNewTab(),
),
]),
Section::make(__('Nitro Texts'))
->collapsible()
->visible(fn () => isset($this->data['nitro']) && ! empty($this->data['nitro']))
->schema([
TextInput::make('nitro.title')
->label(__('filament::resources.inputs.badge_title'))
->placeholder('...')
->visible(fn () => isset($this->data['nitro']['title'])),
TextInput::make('nitro.description')
->label(__('filament::resources.inputs.badge_description'))
->placeholder('...')
->visible(fn () => isset($this->data['nitro']['description'])),
]),
Section::make(__('Flash Texts'))
->collapsible()
->visible(fn () => isset($this->data['flash']) && ! empty($this->data['flash']))
->schema([
TextInput::make('flash.title')
->label(__('filament::resources.inputs.badge_title'))
->placeholder('...')
->visible(fn () => isset($this->data['flash']['title'])),
TextInput::make('flash.description')
->label(__('filament::resources.inputs.badge_description'))
->placeholder('...')
->visible(fn () => isset($this->data['flash']['description'])),
]),
])
->statePath('data');
}
private function searchBadgesByCode(): void
{
$badgeCode = $this->form->getState()['code'] ?? null;
if (empty($badgeCode)) {
Notification::make()
->icon('heroicon-o-exclamation-triangle')
->iconColor('danger')
->color('danger')
->title(__('filament::resources.notifications.badge_code_required'))
->send();
return;
}
$badgeData = app(ExternalTextsParser::class)->getBadgeData($badgeCode);
$this->badgeWasPreviouslyCreated = is_array($badgeData['nitro'] ?? null) || is_array($badgeData['flash'] ?? null);
if ($this->badgeWasPreviouslyCreated) {
Notification::make()
->icon('heroicon-o-check-circle')
->iconColor('success')
->color('success')
->title(__('filament::resources.notifications.badge_found'))
->send();
$this->data = [
'code' => $badgeCode,
...$this->getDefaultDataBehavior(
$badgeData['image'] ?? null,
$badgeData['nitro']['title'] ?? null,
$badgeData['nitro']['description'] ?? null,
$badgeData['flash']['title'] ?? null,
$badgeData['flash']['description'] ?? null,
),
];
return;
}
Notification::make()
->color('success')
->icon('heroicon-o-check-circle')
->iconColor('success')
->title(__('filament::resources.notifications.create_badge'))
->send();
$this->data = [
'code' => $badgeCode,
...$this->getDefaultDataBehavior(),
];
}
private function getDefaultDataBehavior(
?string $badgeImageUrl = null,
?string $nitroTitle = null,
?string $nitroDesc = null,
?string $flashTitle = null,
?string $flashDesc = null,
): array {
return [
'image' => $badgeImageUrl ?? '',
'nitro' => [
'title' => $nitroTitle ?? '',
'description' => $nitroDesc ?? '',
],
'flash' => [
'title' => $flashTitle ?? '',
'description' => $flashDesc ?? '',
],
];
}
public function create(): void
{
$nitroEnabled = config('hotel.client.nitro.enabled');
$flashEnabled = config('hotel.client.flash.enabled');
// image and code fields are required when creating a new badge
if (! $this->badgeWasPreviouslyCreated && (empty($this->data['image']) || empty($this->data['code']))) {
$notificationTitle = empty($this->data['image']) ?
__('filament::resources.notifications.badge_image_required') :
__('filament::resources.notifications.badge_code_required');
Notification::make()
->icon('heroicon-o-exclamation-triangle')
->iconColor('danger')
->color('danger')
->title($notificationTitle)
->send();
return;
}
$externalTextsParser = app(ExternalTextsParser::class);
if ((empty($this->data['nitro']) && $nitroEnabled) || (empty($this->data['flash']) && $flashEnabled)) {
Notification::make()
->icon('heroicon-o-exclamation-triangle')
->iconColor('danger')
->color('danger')
->title(__('filament::resources.notifications.badge_texts_required'))
->send();
return;
}
try {
$this->uploadBadgeImage($externalTextsParser);
$nitroData = $this->data['nitro'] ?? [];
if (! empty($nitroData) && $nitroEnabled && is_array($nitroData)) {
$externalTextsParser->updateNitroBadgeTexts($this->data['code'], ...$nitroData);
}
$flashData = $this->data['flash'] ?? [];
if (! empty($flashData) && $flashEnabled && is_array($flashData)) {
$externalTextsParser->updateFlashBadgeTexts($this->data['code'], ...$flashData);
}
} catch (Throwable $exception) {
Log::channel('badge')->error('[ORION BADGE RESOURCE] - ERROR: ' . $exception->getMessage());
Notification::make()
->icon('heroicon-o-exclamation-triangle')
->iconColor('danger')
->color('danger')
->title(__('filament::resources.notifications.badge_update_failed'))
->send();
return;
}
$this->data['image'] = $externalTextsParser->getBadgeImageUrl($this->data['code']);
$this->badgeWasPreviouslyCreated = true;
Notification::make()
->icon('heroicon-o-check-circle')
->iconColor('success')
->color('success')
->title(__('filament::resources.notifications.badge_updated'))
->send();
}
protected function uploadBadgeImage(ExternalTextsParser $parser): void
{
if (empty($this->data['image']) || ! filter_var($this->data['image'], FILTER_VALIDATE_URL)) {
return;
}
if ($this->data['image'] == $parser->getBadgeImageUrl($this->data['code'])) {
return;
}
$image = Http::get($this->data['image']);
if (! $image->successful()) {
return;
}
$contentType = $image->header('content-type');
$gdImage = match ($contentType) {
'image/png' => imagecreatefrompng($this->data['image']),
'image/gif' => imagecreatefromgif($this->data['image']),
'image/jpeg' => imagecreatefromjpeg($this->data['image']),
default => false
};
if ($gdImage === false) {
Notification::make()
->icon('heroicon-o-exclamation-triangle')
->iconColor('danger')
->color('danger')
->title(__('filament::resources.notifications.badge_image_upload_failed'))
->send();
return;
}
$uploadPath = public_path(sprintf('%s%s%s.gif',
rtrim((string) config('hotel.client.flash.relative_files_path'), '\//'),
'/c_images/album1584/',
$this->data['code'],
));
imagegif($gdImage, $uploadPath);
}
/**
* @return array<Action|ActionGroup>
*/
#[\Override]
protected function getHeaderActions(): array
{
return [
PageAction::make('save')
->label(__('filament::resources.common.Update'))
->action(fn () => $this->create())
->color('primary')
->visible(fn () => isset($this->data['code']) && $this->badgeWasPreviouslyCreated),
PageAction::make('create')
->label(__('filament::resources.common.Create'))
->action(fn () => $this->create())
->color('success')
->visible(fn () => isset($this->data['code']) && ! $this->badgeWasPreviouslyCreated),
];
}
}
+28
View File
@@ -0,0 +1,28 @@
<?php
namespace App\Filament\Pages;
use App\Filament\Traits\TranslatableResource;
use Filament\Pages\Dashboard as FilamentDashboard;
class Dashboard extends FilamentDashboard
{
use TranslatableResource;
#[\Override]
protected static string|\UnitEnum|null $navigationGroup = 'Dashboard';
#[\Override]
protected static ?string $navigationLabel = null;
#[\Override]
public static function getNavigationLabel(): string
{
return __('Homepage');
}
#[\Override]
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-home';
public static string $translateIdentifier = 'dashboard';
}
File diff suppressed because it is too large Load Diff
+114
View File
@@ -0,0 +1,114 @@
<?php
namespace App\Filament\Pages;
use DanHarrin\LivewireRateLimiting\Exceptions\TooManyRequestsException;
use Filament\Auth\Http\Responses\Contracts\LoginResponse;
use Filament\Facades\Filament;
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\TextInput;
use Filament\Models\Contracts\FilamentUser;
use Filament\Notifications\Notification;
use Filament\Schemas\Components\Component;
use Illuminate\Validation\ValidationException;
class Login extends \Filament\Auth\Pages\Login
{
public string $username = '';
#[\Override]
public function authenticate(): ?LoginResponse
{
try {
$this->rateLimit(15);
} catch (TooManyRequestsException $exception) {
$titleMessage = __('filament-panels::pages/auth/login.notifications.throttled.title', [
'seconds' => $exception->secondsUntilAvailable,
'minutes' => ceil($exception->secondsUntilAvailable / 60),
]);
$bodyMessage = __('filament-panels::pages/auth/login.notifications.throttled.body', [
'seconds' => $exception->secondsUntilAvailable,
'minutes' => ceil($exception->secondsUntilAvailable / 60),
]);
Notification::make()
->title($titleMessage)
->body(is_string($bodyMessage) ? $bodyMessage : null)
->danger()
->send();
return null;
}
$data = $this->form->getState();
if (! Filament::auth()->attempt($this->getCredentialsFromFormData($data), $data['remember'] ?? false)) {
$this->throwFailureValidationException();
}
$user = Filament::auth()->user();
$panel = Filament::getCurrentOrDefaultPanel();
if (
($user instanceof FilamentUser) &&
(! $user->canAccessPanel($panel))
) {
Filament::auth()->logout();
$this->throwFailureValidationException();
}
session()->regenerate();
return app(LoginResponse::class);
}
protected function throwFailureValidationException(): never
{
throw ValidationException::withMessages([
'data.username' => __('filament-panels::pages/auth/login.messages.failed'),
]);
}
protected function getFormSchema(): array
{
return [
TextInput::make('username')
->label(__('filament::login.fields.username.label'))
->required()
->autocomplete(),
TextInput::make('password')
->label(__('filament::login.fields.password.label'))
->password()
->required(),
Checkbox::make('remember')
->label(__('filament::login.fields.remember.label')),
];
}
#[\Override]
protected function getEmailFormComponent(): Component
{
return TextInput::make('username')
->label(__('filament::login.fields.username.label'))
->required()
->autocomplete()
->autofocus()
->extraInputAttributes(['tabindex' => 1]);
}
/**
* @param array<string, mixed> $data
*
* @return array<string, mixed>
*/
#[\Override]
protected function getCredentialsFromFormData(array $data): array
{
return [
'username' => $data['username'],
'password' => $data['password'],
];
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+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;
}
}
+439
View File
@@ -0,0 +1,439 @@
<?php
declare(strict_types=1);
namespace App\Filament\Pages\VPN;
use App\Models\Miscellaneous\WebsiteBlockedCountry;
use App\Models\Miscellaneous\WebsiteIpBlacklist;
use App\Models\Miscellaneous\WebsiteIpWhitelist;
use App\Models\Miscellaneous\WebsiteSetting;
use App\Services\IpLookupService;
use Filament\Actions\Action;
use Filament\Forms\Components\Select;
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 Filament\Tables\Columns\TextColumn;
final class VPNManagement extends Page implements HasForms
{
use InteractsWithForms;
#[\Override]
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-shield-exclamation';
#[\Override]
protected static string|\UnitEnum|null $navigationGroup = 'Website';
#[\Override]
protected static ?string $navigationLabel = 'VPN Beheer';
#[\Override]
protected static ?string $title = 'VPN & IP Beheer';
#[\Override]
protected string $view = 'filament.pages.vpn.vpn-management';
/** @var array<string, mixed> */
public array $data = [];
public $blockedCountries;
public $whitelistedIps;
public $blacklistedIps;
public $blocklistStats;
public function mount(): void
{
$this->fillForm();
$this->loadData();
}
protected function loadData(): void
{
$this->blockedCountries = WebsiteBlockedCountry::orderBy('country_name')->get();
$this->whitelistedIps = WebsiteIpWhitelist::orderBy('created_at', 'desc')->get();
$this->blacklistedIps = WebsiteIpBlacklist::orderBy('created_at', 'desc')->get();
$ipService = new IpLookupService('');
$this->blocklistStats = $ipService->getBlocklistStats();
}
protected function fillForm(): void
{
$this->data = [
'vpn_block_enabled' => $this->getSettingBool('vpn_block_enabled'),
'country_block_enabled' => $this->getSettingBool('country_block_enabled'),
'block_vpn' => $this->getSettingBool('block_vpn'),
'block_tor' => $this->getSettingBool('block_tor'),
'block_malicious' => $this->getSettingBool('block_malicious'),
];
}
protected function getSetting(string $key, string $default = ''): string
{
return WebsiteSetting::where('key', $key)->first()?->value ?? $default;
}
protected function getSettingBool(string $key): bool
{
return WebsiteSetting::where('key', $key)->first()?->value === '1';
}
public function form(Schema $schema): Schema
{
return $schema
->components([
Tabs::make('VPN Beheer')
->tabs([
Tab::make('Instellingen')
->icon('heroicon-o-cog-6-tooth')
->schema([
Section::make('Gratis Blokkering (Onbeperkt)')
->description('Gebruikt gratis community blocklists - geen API keys nodig')
->schema([
Toggle::make('data.vpn_block_enabled')
->label('VPN/TOR/Proxy Blokker Actief')
->helperText('Blokkeer VPN, TOR exit nodes, proxies en bekende malicious IPs'),
]),
Section::make('Wat blokkeren?')
->schema([
Toggle::make('data.block_vpn')
->label('VPN & Proxies')
->helperText('FireHol blocklist'),
Toggle::make('data.block_tor')
->label('TOR Exit Nodes')
->helperText('Alle bekende TOR exit nodes'),
Toggle::make('data.block_malicious')
->label('Malicious IPs')
->helperText('Bekende kwaadwillende IPs uit community databases'),
]),
Section::make('Land Blokkering')
->schema([
Toggle::make('data.country_block_enabled')
->label('Land Blokkering Actief')
->helperText('Sta alleen bezoekers uit toegestane landen toe'),
]),
]),
Tab::make('Geblokkeerde Landen')
->icon('heroicon-o-globe-alt')
->schema([
Section::make('Land toevoegen')
->schema([
Select::make('country_to_block')
->label('Selecteer Land')
->options($this->getCountryOptions())
->searchable()
->placeholder('Kies een land'),
Action::make('addCountry')
->label('Blokkeer Land')
->action('addBlockedCountry')
->color('danger'),
]),
]),
Tab::make('Whitelist')
->icon('heroicon-o-check-circle')
->schema([
Section::make('Whitelist toevoegen')
->schema([
TextInput::make('whitelist_ip')
->label('IP Adres')
->placeholder('192.168.1.1'),
TextInput::make('whitelist_asn')
->label('ASN')
->placeholder('AS15169'),
Select::make('whitelist_country')
->label('Land')
->options($this->getCountryOptions())
->searchable(),
Action::make('addToWhitelist')
->label('Toevoegen')
->action('addToWhitelist')
->color('success'),
]),
]),
Tab::make('Blacklist')
->icon('heroicon-o-x-circle')
->schema([
Section::make('Blacklist toevoegen')
->schema([
TextInput::make('blacklist_ip')
->label('IP Adres')
->placeholder('192.168.1.1'),
TextInput::make('blacklist_asn')
->label('ASN')
->placeholder('AS15169'),
Select::make('blacklist_country')
->label('Land')
->options($this->getCountryOptions())
->searchable(),
Action::make('addToBlacklist')
->label('Toevoegen')
->action('addToBlacklist')
->color('danger'),
]),
]),
Tab::make('IP Lookup')
->icon('heroicon-o-magnifying-glass')
->schema([
Section::make('IP Opzoeken')
->schema([
TextInput::make('lookup_ip')
->label('IP Adres')
->placeholder('Voer IP in om te controleren'),
Action::make('lookupIp')
->label('Controleer')
->action('lookupIp')
->color('info'),
]),
]),
Tab::make('Blocklist Stats')
->icon('heroicon-o-chart-bar')
->schema([
Section::make('Huidige Blocklists')
->description('Deze lijsten worden automatisch gedownload en dagelijks ververst')
->schema([
TextColumn::make('type')->label('Type'),
TextColumn::make('count')->label('Aantal IPs'),
]),
]),
]),
])
->statePath('data');
}
protected function getCountryOptions(): array
{
return [
'AD' => 'Andorra', 'AE' => 'United Arab Emirates', 'AF' => 'Afghanistan',
'AG' => 'Antigua and Barbuda', 'AI' => 'Anguilla', 'AL' => 'Albania',
'AM' => 'Armenia', 'AO' => 'Angola', 'AR' => 'Argentina',
'AT' => 'Austria', 'AU' => 'Australia', 'AW' => 'Aruba',
'AZ' => 'Azerbaijan', 'BA' => 'Bosnia and Herzegovina', 'BB' => 'Barbados',
'BD' => 'Bangladesh', 'BE' => 'Belgium', 'BG' => 'Bulgaria',
'BH' => 'Bahrain', 'BI' => 'Burundi', 'BJ' => 'Benin',
'BM' => 'Bermuda', 'BN' => 'Brunei', 'BO' => 'Bolivia',
'BR' => 'Brazil', 'BS' => 'Bahamas', 'BT' => 'Bhutan',
'BW' => 'Botswana', 'BY' => 'Belarus', 'BZ' => 'Belize',
'CA' => 'Canada', 'CH' => 'Switzerland', 'CL' => 'Chile',
'CN' => 'China', 'CO' => 'Colombia', 'CR' => 'Costa Rica',
'CU' => 'Cuba', 'CY' => 'Cyprus', 'CZ' => 'Czechia',
'DE' => 'Germany', 'DK' => 'Denmark', 'DO' => 'Dominican Republic',
'DZ' => 'Algeria', 'EC' => 'Ecuador', 'EE' => 'Estonia',
'EG' => 'Egypt', 'ES' => 'Spain', 'ET' => 'Ethiopia',
'FI' => 'Finland', 'FJ' => 'Fiji', 'FR' => 'France',
'GB' => 'United Kingdom', 'GD' => 'Grenada', 'GE' => 'Georgia',
'GH' => 'Ghana', 'GI' => 'Gibraltar', 'GL' => 'Greenland',
'GM' => 'Gambia', 'GN' => 'Guinea', 'GR' => 'Greece',
'GT' => 'Guatemala', 'HK' => 'Hong Kong', 'HN' => 'Honduras',
'HR' => 'Croatia', 'HU' => 'Hungary', 'ID' => 'Indonesia',
'IE' => 'Ireland', 'IL' => 'Israel', 'IN' => 'India',
'IQ' => 'Iraq', 'IR' => 'Iran', 'IS' => 'Iceland',
'IT' => 'Italy', 'JM' => 'Jamaica', 'JO' => 'Jordan',
'JP' => 'Japan', 'KE' => 'Kenya', 'KH' => 'Cambodia',
'KR' => 'South Korea', 'KW' => 'Kuwait', 'KZ' => 'Kazakhstan',
'LA' => 'Laos', 'LB' => 'Lebanon', 'LK' => 'Sri Lanka',
'LT' => 'Lithuania', 'LU' => 'Luxembourg', 'LV' => 'Latvia',
'MA' => 'Morocco', 'MC' => 'Monaco', 'MD' => 'Moldova',
'ME' => 'Montenegro', 'MG' => 'Madagascar', 'MK' => 'North Macedonia',
'MM' => 'Myanmar', 'MN' => 'Mongolia', 'MO' => 'Macao',
'MT' => 'Malta', 'MU' => 'Mauritius', 'MV' => 'Maldives',
'MX' => 'Mexico', 'MY' => 'Malaysia', 'MZ' => 'Mozambique',
'NA' => 'Namibia', 'NG' => 'Nigeria', 'NI' => 'Nicaragua',
'NL' => 'Netherlands', 'NO' => 'Norway', 'NP' => 'Nepal',
'NZ' => 'New Zealand', 'OM' => 'Oman', 'PA' => 'Panama',
'PE' => 'Peru', 'PH' => 'Philippines', 'PK' => 'Pakistan',
'PL' => 'Poland', 'PR' => 'Puerto Rico', 'PT' => 'Portugal',
'PY' => 'Paraguay', 'QA' => 'Qatar', 'RO' => 'Romania',
'RS' => 'Serbia', 'RU' => 'Russia', 'RW' => 'Rwanda',
'SA' => 'Saudi Arabia', 'SC' => 'Seychelles', 'SD' => 'Sudan',
'SE' => 'Sweden', 'SG' => 'Singapore', 'SI' => 'Slovenia',
'SK' => 'Slovakia', 'SN' => 'Senegal', 'SO' => 'Somalia',
'SR' => 'Suriname', 'SV' => 'El Salvador', 'SY' => 'Syria',
'TH' => 'Thailand', 'TJ' => 'Tajikistan', 'TN' => 'Tunisia',
'TR' => 'Turkey', 'TT' => 'Trinidad and Tobago', 'TW' => 'Taiwan',
'TZ' => 'Tanzania', 'UA' => 'Ukraine', 'UG' => 'Uganda',
'US' => 'United States', 'UY' => 'Uruguay', 'UZ' => 'Uzbekistan',
'VE' => 'Venezuela', 'VN' => 'Vietnam', 'ZA' => 'South Africa',
'ZM' => 'Zambia', 'ZW' => 'Zimbabwe',
];
}
public function save(): void
{
$settings = [
'vpn_block_enabled' => $this->data['vpn_block_enabled'] ? '1' : '0',
'country_block_enabled' => $this->data['country_block_enabled'] ? '1' : '0',
'block_vpn' => $this->data['block_vpn'] ?? true ? '1' : '0',
'block_tor' => $this->data['block_tor'] ?? true ? '1' : '0',
'block_malicious' => $this->data['block_malicious'] ?? true ? '1' : '0',
];
foreach ($settings as $key => $value) {
WebsiteSetting::updateOrCreate(['key' => $key], ['value' => $value]);
}
Notification::make()->title(__('Saved'))->success()->send();
}
public function refreshBlocklists(): void
{
$ipService = new IpLookupService('');
$ipService->refreshBlocklists();
$this->loadData();
Notification::make()->title('Blocklists vernieuwd')->success()->send();
}
public function addBlockedCountry(): void
{
$countryCode = $this->data['country_to_block'] ?? null;
if (! $countryCode) {
Notification::make()->title('Selecteer een land')->danger()->send();
return;
}
if (WebsiteBlockedCountry::where('country_code', $countryCode)->exists()) {
Notification::make()->title('Land is al geblokkeerd')->warning()->send();
return;
}
$countries = $this->getCountryOptions();
$countryCode = (string) $countryCode;
WebsiteBlockedCountry::create([
'country_code' => $countryCode,
'country_name' => $countries[$countryCode] ?? $countryCode,
]);
$this->loadData();
Notification::make()->title('Land geblokkeerd')->success()->send();
}
public function removeBlockedCountry(int $id): void
{
WebsiteBlockedCountry::destroy($id);
$this->loadData();
Notification::make()->title('Verwijderd')->success()->send();
}
public function addToWhitelist(): void
{
$ip = $this->data['whitelist_ip'] ?? null;
$asn = $this->data['whitelist_asn'] ?? null;
$countryCode = $this->data['whitelist_country'] ?? null;
if (! $ip && ! $asn && ! $countryCode) {
Notification::make()->title('Voer IP, ASN of land in')->danger()->send();
return;
}
$countries = $this->getCountryOptions();
$countryCodeStr = (string) ($countryCode ?? '');
$countryName = $countryCode ? ($countries[$countryCodeStr] ?? null) : null;
WebsiteIpWhitelist::create([
'ip_address' => $ip ?? null,
'asn' => $asn ?? null,
'country_code' => $countryCodeStr,
'country_name' => $countryName,
'whitelist_asn' => ! empty($asn),
'whitelist_country' => ! empty($countryCode),
]);
$this->loadData();
Notification::make()->title('Toegevoegd aan whitelist')->success()->send();
}
public function addToBlacklist(): void
{
$ip = $this->data['blacklist_ip'] ?? null;
$asn = $this->data['blacklist_asn'] ?? null;
$countryCode = $this->data['blacklist_country'] ?? null;
if (! $ip && ! $asn && ! $countryCode) {
Notification::make()->title('Voer IP, ASN of land in')->danger()->send();
return;
}
$countries = $this->getCountryOptions();
$countryCodeStr = (string) ($countryCode ?? '');
$countryName = $countryCode ? ($countries[$countryCodeStr] ?? null) : null;
WebsiteIpBlacklist::create([
'ip_address' => $ip ?? null,
'asn' => $asn ?? null,
'country_code' => $countryCodeStr,
'country_name' => $countryName,
'blacklist_asn' => ! empty($asn),
'blacklist_country' => ! empty($countryCode),
]);
$this->loadData();
Notification::make()->title('Toegevoegd aan blacklist')->success()->send();
}
public function removeFromWhitelist(int $id): void
{
WebsiteIpWhitelist::destroy($id);
$this->loadData();
Notification::make()->title('Verwijderd')->success()->send();
}
public function removeFromBlacklist(int $id): void
{
WebsiteIpBlacklist::destroy($id);
$this->loadData();
Notification::make()->title('Verwijderd')->success()->send();
}
public function lookupIp(): void
{
$ip = $this->data['lookup_ip'] ?? null;
if (! $ip) {
Notification::make()->title('Voer een IP in')->danger()->send();
return;
}
$ipService = new IpLookupService('');
$countryInfo = $ipService->getCountryInfo($ip);
$threatInfo = $ipService->checkVpnProxyTor($ip);
$message = "IP: {$ip}\n\n";
if (! empty($countryInfo['country_name'])) {
$message .= "📍 {$countryInfo['country_name']} ({$countryInfo['country_code']})\n";
$message .= " {$countryInfo['city']} - {$countryInfo['isp']}\n\n";
}
$message .= "🚫 Blokkeren:\n";
$message .= ' VPN/Proxy: ' . ($threatInfo['is_vpn'] ? 'JA' : 'Nee') . "\n";
$message .= ' TOR: ' . ($threatInfo['is_tor'] ? 'JA' : 'Nee') . "\n";
$message .= ' Malicious: ' . ($threatInfo['is_malicious'] ? 'JA' : 'Nee') . "\n";
Notification::make()->title('Resultaat')->body($message)->success()->send();
}
#[\Override]
protected function getActions(): array
{
return [
Action::make('save')->label('Opslaan')->action('save')->color('primary'),
Action::make('refreshBlocklists')->label('Vernieuw Blocklists')->action('refreshBlocklists')->color('warning'),
];
}
}