You've already forked Atomcms-edit
Initial commit
This commit is contained in:
Executable
+338
@@ -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),
|
||||
];
|
||||
}
|
||||
}
|
||||
Executable
+28
@@ -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';
|
||||
}
|
||||
Executable
+2197
File diff suppressed because it is too large
Load Diff
Executable
+114
@@ -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'],
|
||||
];
|
||||
}
|
||||
}
|
||||
+2854
File diff suppressed because it is too large
Load Diff
+1944
File diff suppressed because it is too large
Load Diff
+239
@@ -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
@@ -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();
|
||||
}
|
||||
}
|
||||
Executable
+245
@@ -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;
|
||||
}
|
||||
}
|
||||
Executable
+1630
File diff suppressed because it is too large
Load Diff
+310
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
Executable
+439
@@ -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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user