From c4efd8473ddd55d3495d98676a71ca3ed959ed66 Mon Sep 17 00:00:00 2001 From: stevan Date: Tue, 30 Sep 2025 16:13:41 +0200 Subject: [PATCH] Event Management --- app/Event.php | 12 ++- app/Nova/Audience.php | 23 +++++ app/Nova/Event.php | 208 +++++++++++++++++++++++++++++++++++++----- app/Nova/Tag.php | 24 +++++ app/Nova/Theme.php | 24 +++++ 5 files changed, 266 insertions(+), 25 deletions(-) create mode 100644 app/Nova/Audience.php create mode 100644 app/Nova/Tag.php create mode 100644 app/Nova/Theme.php diff --git a/app/Event.php b/app/Event.php index 5d419ec5c..5d42b3c4a 100644 --- a/app/Event.php +++ b/app/Event.php @@ -79,7 +79,9 @@ class Event extends Model 'is_extracurricular_event', 'is_standard_school_curriculum', 'is_use_resource', - 'ages' + 'ages', + 'source_ref', + 'source_synced_at' ]; public const ACTIVITY_FORMATS = [ @@ -175,6 +177,14 @@ protected function casts(): array 'is_standard_school_curriculum' => 'boolean', 'ages' => 'array', 'is_use_resource' => 'boolean', + + 'start_date' => 'datetime', + 'end_date' => 'datetime', + 'pub_date' => 'datetime', + 'reported_at' => 'datetime', + 'certificate_generated_at' => 'datetime', + 'last_report_notification_sent_at' => 'datetime', + 'source_synced_at' => 'datetime', ]; } diff --git a/app/Nova/Audience.php b/app/Nova/Audience.php new file mode 100644 index 000000000..2f0fe4d1f --- /dev/null +++ b/app/Nova/Audience.php @@ -0,0 +1,23 @@ +sortable(), + Text::make('Name')->sortable()->rules('required', 'max:255'), + ]; + } +} diff --git a/app/Nova/Event.php b/app/Nova/Event.php index bd86ef329..392eb0675 100644 --- a/app/Nova/Event.php +++ b/app/Nova/Event.php @@ -3,10 +3,20 @@ namespace App\Nova; use Illuminate\Http\Request; +use Laravel\Nova\Fields\Boolean; +use Laravel\Nova\Fields\BelongsTo; +use Laravel\Nova\Fields\BelongsToMany; use Laravel\Nova\Fields\Country; +use Laravel\Nova\Fields\DateTime; +use Laravel\Nova\Fields\ID; +use Laravel\Nova\Fields\MultiSelect; +use Laravel\Nova\Fields\Number; use Laravel\Nova\Fields\Select; use Laravel\Nova\Fields\Text; +use Laravel\Nova\Fields\URL; use Laravel\Nova\Http\Requests\NovaRequest; +use Illuminate\Support\Arr; +use Laravel\Nova\Fields\Trix; class Event extends Resource { @@ -31,7 +41,7 @@ class Event extends Resource * @var array */ public static $search = [ - 'id', 'title', + 'id', 'title', 'geoposition' ]; /** @@ -39,36 +49,186 @@ class Event extends Resource */ public function fields(Request $request): array { - return [ + try { + return [ + ID::make()->sortable(), - Text::make('Title')->sortable(), - Text::make('Certificate', function () { - $certificate_url = $this->certificate_url; + // Core + Text::make('Title')->sortable()->rules('required', 'max:255'), + Text::make('Slug')->hideFromIndex()->rules('nullable', 'max:255'), - return "{$certificate_url}"; - })->asHtml()->onlyOnDetail(), - Text::make('Web Link', function () { - $slug = $this->slug; - $id = $this->id; - $url = config('codeweek.app_url'); + Country::make('Country', 'country_iso')->sortable()->rules('required', 'size:2'), - return "View Activity's Page"; - })->asHtml()->onlyOnDetail(), - Text::make('Description')->onlyOnDetail(), - Text::make('Organizer')->onlyOnDetail(), - Text::make('codeweek_for_all_participation_code')->sortable()->onlyOnDetail(), + Select::make('Status') + ->options([ + 'APPROVED' => 'Approved', + 'REJECTED' => 'Rejected', + 'PENDING' => 'Pending', + ])->displayUsingLabels()->sortable()->rules('required'), - Country::make('Country', 'country_iso')->sortable(), + Text::make('Organizer')->hideFromIndex()->rules('nullable', 'max:255'), + Text::make('Location')->hideFromIndex()->rules('nullable', 'max:255'), - Select::make('Status')->options([ - 'APPROVED' => 'Approved', - 'REJECTED' => 'Rejected', - 'PENDING' => 'Pending', - ]), + // Geo + Text::make('Geoposition')->hideFromIndex()->rules('nullable', 'max:255'), + Number::make('Latitude')->step('any')->hideFromIndex()->rules('nullable', 'numeric'), + Number::make('Longitude')->step('any')->hideFromIndex()->rules('nullable', 'numeric'), - Text::make('Creator', 'owner.email'), + // Dates + DateTime::make('Start Date', 'start_date')->sortable()->rules('nullable', 'date'), + DateTime::make('End Date', 'end_date')->sortable()->rules('nullable', 'date'), + DateTime::make('Publication Date', 'pub_date')->onlyOnDetail()->rules('nullable', 'date'), + DateTime::make('Reported At', 'reported_at')->onlyOnDetail()->rules('nullable', 'date'), + DateTime::make('Certificate Generated At', 'certificate_generated_at')->onlyOnDetail()->rules('nullable', 'date'), + DateTime::make('Last Report Notification Sent At', 'last_report_notification_sent_at')->onlyOnDetail()->rules('nullable', 'date'), + DateTime::make('Source Synced At', 'source_synced_at')->onlyOnDetail()->rules('nullable', 'date'), - ]; + // Contacts & Links + URL::make('Event URL', 'event_url')->displayUsing(fn ($v) => $v)->hideFromIndex()->rules('nullable', 'url'), + Text::make('Contact Person')->hideFromIndex()->rules('nullable', 'max:255'), + Text::make('User Email', 'user_email')->hideFromIndex()->rules('nullable', 'email', 'max:255'), + + // Media + URL::make('Picture')->displayUsing(fn ($v) => $v)->hideFromIndex()->rules('nullable', 'url'), + URL::make('Picture Detail', 'picture_detail')->displayUsing(fn ($v) => $v)->hideFromIndex()->rules('nullable', 'url'), + URL::make('Certificate URL', 'certificate_url')->displayUsing(fn ($v) => $v)->onlyOnDetail()->rules('nullable', 'url'), + + // Descriptions + Trix::make('Description')->hideFromIndex(), + Text::make('Web Link', function () { + $slug = $this->slug; + $id = $this->id; + $url = config('codeweek.app_url'); + + return "View Activity's Page"; + })->asHtml()->onlyOnDetail(), + Text::make('Certificate', function () { + $certificate_url = $this->certificate_url; + return $certificate_url + ? "{$certificate_url}" + : '-'; + })->asHtml()->onlyOnDetail(), + + // Classification + Text::make('Activity Type', 'activity_type')->hideFromIndex()->rules('nullable', 'max:255'), + Text::make('Organizer Type', 'organizer_type')->hideFromIndex()->rules('nullable', 'max:255'), + + MultiSelect::make('Activity Format', 'activity_format') + ->options(array_combine(\App\Event::ACTIVITY_FORMATS, \App\Event::ACTIVITY_FORMATS)) + ->nullable() + ->rules(function () { + return [ + function ($attribute, $value, $fail) { + if ($value === null || $value === '' || $value === [] || $value === '[]') { + return; + } + if (!is_array($value)) { + $decoded = json_decode((string) $value, true); + if (json_last_error() !== JSON_ERROR_NONE || !is_array($decoded)) { + $fail(__('validation.array', ['attribute' => $attribute])); + } + } + }, + ]; + }) + ->fillUsing(function ($request, $model, $attribute, $requestAttribute) { + try { + $raw = $request->input($requestAttribute); + $decoded = is_array($raw) ? $raw : (is_string($raw) ? json_decode($raw, true) : null); + $model->{$attribute} = is_array($decoded) ? array_values($decoded) : []; + } catch (\Throwable $e) { + $model->{$attribute} = []; + } + }) + ->hideFromIndex(), + + Select::make('Duration')->options( + array_combine(\App\Event::DURATIONS, \App\Event::DURATIONS) + )->displayUsingLabels()->hideFromIndex()->rules('nullable', 'in:'.implode(',', \App\Event::DURATIONS)), + + Select::make('Recurring Type', 'recurring_type')->options( + array_combine(\App\Event::RECURRING_TYPES, \App\Event::RECURRING_TYPES) + )->displayUsingLabels()->hideFromIndex()->rules('nullable', 'in:'.implode(',', \App\Event::RECURRING_TYPES)), + + Select::make('Recurring Event', 'recurring_event')->options( + array_combine(\App\Event::RECURRING_EVENTS, \App\Event::RECURRING_EVENTS) + )->displayUsingLabels()->hideFromIndex()->rules('nullable', 'in:'.implode(',', \App\Event::RECURRING_EVENTS)), + + MultiSelect::make('Ages', 'ages') + ->options(array_combine(\App\Event::AGES, \App\Event::AGES)) + ->nullable() + ->rules(function () { + return [ + function ($attribute, $value, $fail) { + if ($value === null || $value === '' || $value === [] || $value === '[]') { + return; + } + if (!is_array($value)) { + $decoded = json_decode((string) $value, true); + if (json_last_error() !== JSON_ERROR_NONE || !is_array($decoded)) { + $fail(__('validation.array', ['attribute' => $attribute])); + } + } + }, + ]; + }) + ->fillUsing(function ($request, $model, $attribute, $requestAttribute) { + try { + $raw = $request->input($requestAttribute); + $decoded = is_array($raw) ? $raw : (is_string($raw) ? json_decode($raw, true) : null); + $model->{$attribute} = is_array($decoded) ? array_values($decoded) : []; + } catch (\Throwable $e) { + $model->{$attribute} = []; + } + }) + ->hideFromIndex(), + + // Flags + Boolean::make('Is Extracurricular Event', 'is_extracurricular_event')->hideFromIndex(), + Boolean::make('Is Standard School Curriculum', 'is_standard_school_curriculum')->hideFromIndex(), + Boolean::make('Is Using Resource', 'is_use_resource')->hideFromIndex(), + + // Numbers / Stats + Number::make('Participants Count', 'participants_count')->step(1)->min(0)->hideFromIndex()->rules('nullable', 'integer', 'min:0'), + Number::make('Average Participant Age', 'average_participant_age')->step('any')->hideFromIndex()->rules('nullable', 'numeric', 'min:0'), + Number::make('Percentage of Females', 'percentage_of_females')->step('any')->hideFromIndex()->rules('nullable', 'numeric', 'min:0', 'max:100'), + Number::make('Males Count', 'males_count')->step(1)->min(0)->hideFromIndex()->rules('nullable', 'integer', 'min:0'), + Number::make('Females Count', 'females_count')->step(1)->min(0)->hideFromIndex()->rules('nullable', 'integer', 'min:0'), + Number::make('Other Count', 'other_count')->step(1)->min(0)->hideFromIndex()->rules('nullable', 'integer', 'min:0'), + + // Codes / Misc + Text::make('Codeweek For All Participation Code', 'codeweek_for_all_participation_code') + ->onlyOnForms()->rules('nullable', 'max:255'), + Text::make('Name For Certificate', 'name_for_certificate')->onlyOnForms()->rules('nullable', 'max:255'), + Text::make('Language')->help('Comma-separated language codes (e.g., en,fr,de)') + ->onlyOnForms()->rules('nullable', 'max:255'), + Text::make('Source Ref', 'source_ref')->hideFromIndex()->rules('nullable', 'max:255'), + + // Relations + BelongsTo::make('Owner', 'owner', \App\Nova\User::class)->searchable()->nullable(), + BelongsTo::make('Country Model', 'country', \App\Nova\Country::class)->onlyOnDetail(), + + BelongsToMany::make('Audiences'), + BelongsToMany::make('Themes'), + BelongsToMany::make('Tags')->fields(function () { + return []; + }), + + // Creator email shortcut for index + Text::make('Creator', 'owner.email')->onlyOnIndex(), + + ]; + } catch (\Throwable $e) { + return [ + Text::make('Title')->sortable(), + Country::make('Country', 'country_iso')->sortable(), + Select::make('Status')->options([ + 'APPROVED' => 'Approved', + 'REJECTED' => 'Rejected', + 'PENDING' => 'Pending', + ]), + ]; + } } /** diff --git a/app/Nova/Tag.php b/app/Nova/Tag.php new file mode 100644 index 000000000..485e39bcb --- /dev/null +++ b/app/Nova/Tag.php @@ -0,0 +1,24 @@ +sortable(), + Text::make('Name')->sortable()->rules('required', 'max:255'), + Text::make('Slug')->sortable()->rules('required', 'max:255'), + ]; + } +} diff --git a/app/Nova/Theme.php b/app/Nova/Theme.php new file mode 100644 index 000000000..75699f5ae --- /dev/null +++ b/app/Nova/Theme.php @@ -0,0 +1,24 @@ +sortable(), + Text::make('Name')->sortable()->rules('required', 'max:255'), + Text::make('Order')->sortable()->rules('nullable', 'integer'), + ]; + } +}