From 5a9fea924fbd7ad59a9d7b64b845535b5f989a99 Mon Sep 17 00:00:00 2001 From: Calum Towers Date: Thu, 29 Feb 2024 21:49:38 +0000 Subject: [PATCH] feat: Implement Roster (#3493) --- app/Console/Commands/Roster/UpdateRoster.php | 51 + .../Roster/UpdateRosterGanderControllers.php | 48 + app/Console/Kernel.php | 4 + .../AccountChangedStatusInWaitingList.php | 29 - .../Training/EndorsementRequestApproved.php | 36 + app/Filament/Resources/AccountResource.php | 3 + .../AccountResource/Pages/ViewAccount.php | 20 + .../EndorsementsRelationManager.php | 83 + .../Resources/EndorsementRequestResource.php | 158 ++ .../Pages/CreateEndorsementRequest.php | 11 + .../Pages/EditEndorsementRequest.php | 19 + .../Pages/ListEndorsementRequests.php | 19 + .../Resources/PositionGroupResource.php | 62 + .../Pages/ListPositionGroups.php | 11 + .../Pages/ViewPositionGroup.php | 11 + .../MembershipEndorsementRelationManager.php | 49 + .../Resources/WaitingListResource.php | 7 - .../Pages/ViewWaitingList.php | 6 +- .../AccountsRelationManager.php | 67 +- .../IneligibleAccountsRelationManager.php | 8 - .../Widgets/IndividualWaitingListOverview.php | 11 +- .../Controllers/Atc/EndorsementController.php | 100 +- app/Http/Controllers/Mship/Management.php | 6 +- app/Http/Controllers/Mship/WaitingLists.php | 5 +- .../Controllers/Site/AirportController.php | 4 +- .../Middleware/FilamentAccessMiddleware.php | 1 + app/Http/Middleware/VerifyCsrfToken.php | 1 + .../UpdateAccountWaitingListEligibility.php | 8 +- app/Libraries/UKCP.php | 37 + .../NetworkData/FlushEndorsementCache.php | 4 +- .../CreateEndorsementFromApproval.php | 29 + .../WaitingList/AssignDefaultStatus.php | 34 - ...kAccountWaitingListEligibilityListener.php | 23 - .../WaitingList/LogAccountDemoted.php | 30 - .../WaitingList/LogAccountPromoted.php | 30 - .../WaitingList/LogAccountStatusChange.php | 27 - app/Livewire/Roster/Index.php | 16 + app/Livewire/Roster/Renew.php | 82 + app/Livewire/Roster/Search.php | 33 + app/Livewire/Roster/Show.php | 45 + app/Models/Airport.php | 11 +- app/Models/Atc/Endorseable.php | 12 + app/Models/Atc/Endorsement.php | 47 - app/Models/Atc/Position.php | 108 + app/Models/Atc/PositionGroup.php | 102 + ...ndition.php => PositionGroupCondition.php} | 13 +- app/Models/Atc/PositionGroupPosition.php | 13 + app/Models/Cts/Position.php | 3 + app/Models/Mship/Account.php | 2 + app/Models/Mship/Account/Endorsement.php | 68 + .../Mship/Account/EndorsementRequest.php | 99 + app/Models/Mship/Concerns/HasEndorsement.php | 42 + .../Mship/Concerns/HasQualifications.php | 8 + app/Models/Mship/Qualification.php | 18 +- app/Models/NetworkData/Atc.php | 17 + app/Models/Roster.php | 138 ++ app/Models/Station.php | 94 - app/Models/Training/WaitingList.php | 27 +- .../WaitingList/WaitingListAccount.php | 98 +- .../WaitingList/WaitingListAccountFlag.php | 4 +- .../WaitingList/WaitingListAccountStatus.php | 34 - .../Training/WaitingList/WaitingListFlag.php | 9 +- .../WaitingList/WaitingListStatus.php | 47 - .../Account/EndorsementRequestPolicy.php | 75 + app/Policies/PositionGroupPolicy.php | 70 + app/Policies/Training/EndorsementPolicy.php | 65 + app/Providers/AuthServiceProvider.php | 6 + app/Providers/Filament/AppPanelProvider.php | 6 +- app/Providers/FilamentServiceProvider.php | 5 + app/Providers/RouteServiceProvider.php | 3 + .../TrainingEventServiceProvider.php | 11 +- .../Networkdata/AtcNetworkdataService.php | 14 + app/Services/Training/AddToWaitingList.php | 32 - .../Training/CheckWaitingListEligibility.php | 77 - .../Training/CheckWaitingListFlags.php | 40 + .../Training/EndorsementCreationService.php | 22 + .../Training/WriteWaitingListEligibility.php | 23 - .../Training/WriteWaitingListFlagSummary.php | 17 + composer.json | 4 +- composer.lock | 32 +- database/factories/Airport/StationFactory.php | 13 - database/factories/Atc/PositionFactory.php | 34 + .../factories/Atc/PositionGroupFactory.php | 23 + database/factories/Cts/PositionFactory.php | 32 +- .../Cts/PositionValidationFactory.php | 2 +- .../factories/Cts/TheoryResultFactory.php | 3 + database/factories/EndorsementFactory.php | 8 +- .../Mship/Account/EndorsementFactory.php | 37 + .../Account/EndorsementRequestFactory.php | 31 + database/factories/WaitingListFactory.php | 1 - .../2023_12_07_190336_create_roster_table.php | 28 + ...create_mship_account_endorsement_table.php | 32 + ...3458_create_endorsement_stations_table.php | 29 + ..._210059_rename_tables_for_endorsements.php | 26 + ...mp_endorseable_flag_to_positions_table.php | 28 + ...sts_table_and_polymorphic_endorsements.php | 48 + ...p_endorsements_for_nullable_created_by.php | 28 + ...qualification_to_position_groups_table.php | 28 + ...26_add_soft_deletes_to_position_groups.php | 28 + ...deletes_to_training_waiting_list_flags.php | 28 + ...42_remove_waiting_list_removed_columns.php | 33 + .../seeders/RolesAndPermissionsSeeder.php | 15 + package-lock.json | 1916 +++++++++++++++-- package.json | 13 +- postcss.config.js | 7 + resources/assets/css/tailwind.css | 3 + resources/assets/js/app.js | 13 +- .../views/components/layouts/app.blade.php | 27 + resources/views/components/nav.blade.php | 1 + .../controllers/endorsements/area.blade.php | 8 +- .../endorsements/gatwick_ground.blade.php | 2 +- resources/views/layout.blade.php | 1 + .../views/livewire/roster/index.blade.php | 61 + .../views/livewire/roster/renew.blade.php | 69 + .../views/livewire/roster/search.blade.php | 26 + .../views/livewire/roster/show.blade.php | 83 + .../mship/management/dashboard.blade.php | 15 +- .../waiting-lists/_flag_breakdown.blade.php | 2 +- .../_waiting_lists_list.blade.php | 10 +- .../views/mship/waiting-lists/view.blade.php | 55 +- resources/views/site/airport/view.blade.php | 18 +- resources/views/site/home.blade.php | 3 + routes/web-livewire.php | 16 + tailwind.config.js | 20 + tests/Feature/Account/WaitingListsTest.php | 16 +- tests/Feature/Admin/BaseAdminTestCase.php | 2 +- .../EndorsementRequestApprovalTest.php | 122 ++ .../EndorsementRequestCreateTest.php | 92 + ...EndorsementCreationRelationManagerTest.php | 65 + .../Pages/ViewWaitingListPageTest.php | 54 +- tests/Feature/Atc/AreaHourCheckTest.php | 5 +- tests/Feature/Site/HomePageTest.php | 1 + .../WaitingListEligibilityPlumbingTest.php | 82 - .../Endorsement/EndorsementRequestTest.php | 77 + .../TemporaryEndorsementTimeframeTest.php | 51 + .../Unit/AirfieldInformation/AirportTest.php | 14 +- .../Unit/AirfieldInformation/StationTest.php | 40 - tests/Unit/Atc/PositionGroupTest.php | 85 + tests/Unit/Atc/PositionTest.php | 72 + tests/Unit/CTS/MentorRepositoryTest.php | 22 +- tests/Unit/CTS/StudentRepositoryTest.php | 12 +- .../Unit/Endorsements/ConditionModelTest.php | 38 +- ...delTest.php => PositionGroupModelTest.php} | 77 +- .../WaitingList/WaitingListAccountTest.php | 275 --- ...WaitingListCheckEligibilityServiceTest.php | 226 -- .../WaitingListCheckFlagsServiceTest.php | 145 ++ .../WaitingList/WaitingListFlagTest.php | 131 +- .../WaitingList/WaitingListServiceTest.php | 56 - .../WaitingList/WaitingListStatusTest.php | 48 - .../Training/WaitingList/WaitingListTest.php | 30 - .../WaitingList/WaitingListTestHelper.php | 6 - .../WaitingListWriteEligibilityTest.php | 61 +- vite.config.js | 1 + 153 files changed, 5326 insertions(+), 2228 deletions(-) create mode 100644 app/Console/Commands/Roster/UpdateRoster.php create mode 100644 app/Console/Commands/Roster/UpdateRosterGanderControllers.php delete mode 100644 app/Events/Training/AccountChangedStatusInWaitingList.php create mode 100644 app/Events/Training/EndorsementRequestApproved.php create mode 100644 app/Filament/Resources/AccountResource/RelationManagers/EndorsementsRelationManager.php create mode 100644 app/Filament/Resources/EndorsementRequestResource.php create mode 100644 app/Filament/Resources/EndorsementRequestResource/Pages/CreateEndorsementRequest.php create mode 100644 app/Filament/Resources/EndorsementRequestResource/Pages/EditEndorsementRequest.php create mode 100644 app/Filament/Resources/EndorsementRequestResource/Pages/ListEndorsementRequests.php create mode 100644 app/Filament/Resources/PositionGroupResource.php create mode 100644 app/Filament/Resources/PositionGroupResource/Pages/ListPositionGroups.php create mode 100644 app/Filament/Resources/PositionGroupResource/Pages/ViewPositionGroup.php create mode 100644 app/Filament/Resources/PositionGroupResource/RelationManagers/MembershipEndorsementRelationManager.php delete mode 100644 app/Filament/Resources/WaitingListResource/RelationManagers/IneligibleAccountsRelationManager.php create mode 100644 app/Listeners/Training/Endorsement/CreateEndorsementFromApproval.php delete mode 100644 app/Listeners/Training/WaitingList/AssignDefaultStatus.php delete mode 100644 app/Listeners/Training/WaitingList/CheckAccountWaitingListEligibilityListener.php delete mode 100644 app/Listeners/Training/WaitingList/LogAccountDemoted.php delete mode 100644 app/Listeners/Training/WaitingList/LogAccountPromoted.php delete mode 100644 app/Listeners/Training/WaitingList/LogAccountStatusChange.php create mode 100644 app/Livewire/Roster/Index.php create mode 100644 app/Livewire/Roster/Renew.php create mode 100644 app/Livewire/Roster/Search.php create mode 100644 app/Livewire/Roster/Show.php create mode 100644 app/Models/Atc/Endorseable.php delete mode 100644 app/Models/Atc/Endorsement.php create mode 100644 app/Models/Atc/Position.php create mode 100644 app/Models/Atc/PositionGroup.php rename app/Models/Atc/{Endorsement/Condition.php => PositionGroupCondition.php} (92%) create mode 100644 app/Models/Atc/PositionGroupPosition.php create mode 100644 app/Models/Mship/Account/Endorsement.php create mode 100644 app/Models/Mship/Account/EndorsementRequest.php create mode 100644 app/Models/Mship/Concerns/HasEndorsement.php create mode 100644 app/Models/Roster.php delete mode 100644 app/Models/Station.php delete mode 100644 app/Models/Training/WaitingList/WaitingListAccountStatus.php delete mode 100644 app/Models/Training/WaitingList/WaitingListStatus.php create mode 100644 app/Policies/Mship/Account/EndorsementRequestPolicy.php create mode 100644 app/Policies/PositionGroupPolicy.php create mode 100644 app/Policies/Training/EndorsementPolicy.php create mode 100644 app/Services/Networkdata/AtcNetworkdataService.php delete mode 100644 app/Services/Training/AddToWaitingList.php delete mode 100644 app/Services/Training/CheckWaitingListEligibility.php create mode 100644 app/Services/Training/CheckWaitingListFlags.php create mode 100644 app/Services/Training/EndorsementCreationService.php delete mode 100644 app/Services/Training/WriteWaitingListEligibility.php create mode 100644 app/Services/Training/WriteWaitingListFlagSummary.php delete mode 100644 database/factories/Airport/StationFactory.php create mode 100644 database/factories/Atc/PositionFactory.php create mode 100644 database/factories/Atc/PositionGroupFactory.php create mode 100644 database/factories/Mship/Account/EndorsementFactory.php create mode 100644 database/factories/Mship/Account/EndorsementRequestFactory.php create mode 100644 database/migrations/2023_12_07_190336_create_roster_table.php create mode 100644 database/migrations/2024_01_04_201826_create_mship_account_endorsement_table.php create mode 100644 database/migrations/2024_01_04_203458_create_endorsement_stations_table.php create mode 100644 database/migrations/2024_01_04_210059_rename_tables_for_endorsements.php create mode 100644 database/migrations/2024_01_28_123419_add_temp_endorseable_flag_to_positions_table.php create mode 100644 database/migrations/2024_01_29_192559_create_endorsement_requests_table_and_polymorphic_endorsements.php create mode 100644 database/migrations/2024_02_15_193128_amend_mship_endorsements_for_nullable_created_by.php create mode 100644 database/migrations/2024_02_26_170552_add_maximum_qualification_to_position_groups_table.php create mode 100644 database/migrations/2024_02_28_161526_add_soft_deletes_to_position_groups.php create mode 100644 database/migrations/2024_02_28_162223_add_soft_deletes_to_training_waiting_list_flags.php create mode 100644 database/migrations/2024_02_29_173742_remove_waiting_list_removed_columns.php create mode 100644 postcss.config.js create mode 100644 resources/assets/css/tailwind.css create mode 100644 resources/views/components/layouts/app.blade.php create mode 100644 resources/views/livewire/roster/index.blade.php create mode 100644 resources/views/livewire/roster/renew.blade.php create mode 100644 resources/views/livewire/roster/search.blade.php create mode 100644 resources/views/livewire/roster/show.blade.php create mode 100644 routes/web-livewire.php create mode 100644 tailwind.config.js create mode 100644 tests/Feature/Admin/EndorsementRequest/EndorsementRequestApprovalTest.php create mode 100644 tests/Feature/Admin/EndorsementRequest/EndorsementRequestCreateTest.php create mode 100644 tests/Feature/Admin/PositionGroup/EndorsementCreationRelationManagerTest.php create mode 100644 tests/Unit/Account/Endorsement/EndorsementRequestTest.php create mode 100644 tests/Unit/Account/Endorsement/TemporaryEndorsementTimeframeTest.php delete mode 100644 tests/Unit/AirfieldInformation/StationTest.php create mode 100644 tests/Unit/Atc/PositionGroupTest.php create mode 100644 tests/Unit/Atc/PositionTest.php rename tests/Unit/Endorsements/{EndorsementModelTest.php => PositionGroupModelTest.php} (52%) delete mode 100644 tests/Unit/Training/WaitingList/WaitingListCheckEligibilityServiceTest.php create mode 100644 tests/Unit/Training/WaitingList/WaitingListCheckFlagsServiceTest.php delete mode 100644 tests/Unit/Training/WaitingList/WaitingListServiceTest.php delete mode 100644 tests/Unit/Training/WaitingList/WaitingListStatusTest.php diff --git a/app/Console/Commands/Roster/UpdateRoster.php b/app/Console/Commands/Roster/UpdateRoster.php new file mode 100644 index 0000000000..7954a32379 --- /dev/null +++ b/app/Console/Commands/Roster/UpdateRoster.php @@ -0,0 +1,51 @@ +fromDate = Carbon::parse($this->argument('fromDate'))->startOfDay(); + $this->toDate = Carbon::parse($this->argument('toDate'))->endOfDay(); + + $eligible = Atc::with(['account.states']) + ->select(['networkdata_atc.account_id']) + ->whereBetween('disconnected_at', [$this->fromDate, $this->toDate]) + ->accountIsPartOfUk() + ->positionIsWithinUk() + ->groupBy('account_id') + ->havingRaw("SUM(minutes_online) / 60 > {$this->minimumHours}") + ->pluck('account_id'); + + // On the roster, do not need to be on... + Roster::withoutGlobalScopes() + ->whereNotIn('account_id', $eligible) + ->get() + ->each + ->remove(); + + // Not on the roster, need to be on... + Roster::upsert( + $eligible->map(fn ($value) => ['account_id' => $value])->toArray(), + ['account_id'] + ); + + $this->comment('✅ Roster updated!'); + } +} diff --git a/app/Console/Commands/Roster/UpdateRosterGanderControllers.php b/app/Console/Commands/Roster/UpdateRosterGanderControllers.php new file mode 100644 index 0000000000..1f18912dbe --- /dev/null +++ b/app/Console/Commands/Roster/UpdateRosterGanderControllers.php @@ -0,0 +1,48 @@ +collect() + ->where('active', true) + ->pluck('cid'); + + DB::transaction(function () use ($gander) { + Roster::upsert( + $gander->map(fn ($value) => ['account_id' => $value])->toArray(), + ['account_id'] + ); + + $positionGroup = PositionGroup::where('name', 'Shanwick Oceanic (EGGX)')->firstOrFail(); + + $gander->reject(function ($value) use ($positionGroup) { + return Endorsement::where([ + 'account_id' => $value, + 'endorsable_id' => $positionGroup->id, + 'endorsable_type' => PositionGroup::class, + ])->exists(); + })->each(function ($value) use ($positionGroup) { + Endorsement::create([ + 'account_id' => $value, + 'endorsable_id' => $positionGroup->id, + 'endorsable_type' => PositionGroup::class, + ]); + }); + }); + } +} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index de8dc56767..8692de6b72 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -58,6 +58,10 @@ protected function schedule(Schedule $schedule) ->hourlyAt(15) ->graceTimeInMinutes(15); + $schedule->command('roster:gander') + ->hourlyAt(25) + ->graceTimeInMinutes(5); + // === By Day === // $schedule->command('telescope:prune') diff --git a/app/Events/Training/AccountChangedStatusInWaitingList.php b/app/Events/Training/AccountChangedStatusInWaitingList.php deleted file mode 100644 index 4694f00148..0000000000 --- a/app/Events/Training/AccountChangedStatusInWaitingList.php +++ /dev/null @@ -1,29 +0,0 @@ -account; - } -} diff --git a/app/Events/Training/EndorsementRequestApproved.php b/app/Events/Training/EndorsementRequestApproved.php new file mode 100644 index 0000000000..8bbe5878dd --- /dev/null +++ b/app/Events/Training/EndorsementRequestApproved.php @@ -0,0 +1,36 @@ +endorsementRequest; + } + + public function getExpiryDate() + { + if ($this->days === null) { + return null; + } + + return now()->addDays($this->days)->endOfDay(); + } +} diff --git a/app/Filament/Resources/AccountResource.php b/app/Filament/Resources/AccountResource.php index c191812950..ce7f72fadc 100644 --- a/app/Filament/Resources/AccountResource.php +++ b/app/Filament/Resources/AccountResource.php @@ -7,6 +7,7 @@ use App\Filament\Resources\AccountResource\Pages; use App\Filament\Resources\AccountResource\RelationManagers; use App\Models\Mship\Account; +use App\Models\Roster; use AxonC\FilamentCopyablePlaceholder\Forms\Components\CopyablePlaceholder; use Carbon\CarbonInterface; use Filament\Forms; @@ -75,6 +76,7 @@ public static function form(Form $form): Form Forms\Components\Placeholder::make('has_secondary_password')->content(fn ($record) => $record->hasPassword() ? 'Yes' : 'No'), Forms\Components\Placeholder::make('discord_id')->label('Discord ID')->content(fn ($record) => $record->discord_id ?? new HtmlString('Not Linked')), + Forms\Components\Placeholder::make('roster_status')->label('Roster Status')->content(fn ($record) => Roster::where('account_id', $record->id)->exists() ? 'Active' : 'Inactive'), Forms\Components\Fieldset::make('Emails')->schema([ Forms\Components\TextInput::make('email') @@ -151,6 +153,7 @@ public static function getRelations(): array RelationManagers\RolesRelationManager::class, RelationManagers\BansRelationManager::class, RelationManagers\NotesRelationManager::class, + RelationManagers\EndorsementsRelationManager::class, ]; } diff --git a/app/Filament/Resources/AccountResource/Pages/ViewAccount.php b/app/Filament/Resources/AccountResource/Pages/ViewAccount.php index 8c89942a79..2f27fd3697 100644 --- a/app/Filament/Resources/AccountResource/Pages/ViewAccount.php +++ b/app/Filament/Resources/AccountResource/Pages/ViewAccount.php @@ -7,6 +7,7 @@ use App\Filament\Resources\AccountResource; use App\Jobs\UpdateMember; use App\Models\Contact; +use App\Models\Roster; use App\Notifications\Mship\UserImpersonated; use Filament\Actions; use Filament\Actions\ActionGroup; @@ -30,6 +31,8 @@ protected function getLogActionName(): string protected function getHeaderActions(): array { + $onRoster = Roster::where('account_id', $this->record->id)->exists(); + return [ Actions\Action::make('request_central_update') @@ -43,6 +46,23 @@ protected function getHeaderActions(): array ActionGroup::make([ $this->getImpersonateAction(), + Actions\Action::make('toggle_roster_status') + ->visible(fn () => auth()->user()->can('roster.manage')) + ->color($onRoster ? 'danger' : 'success') + ->icon('heroicon-o-key') + ->name($onRoster ? 'Remove from roster' : 'Add to roster') + ->modalHeading($onRoster ? 'Remove from roster' : 'Add to roster') + ->action(function () use ($onRoster) { + Roster::withoutGlobalScopes()->where('account_id', $this->record->id)->delete(); + + if (! $onRoster) { + Roster::create(['account_id' => $this->record->id]); + } + + $this->refreshFormData(['roster_status']); + }) + ->requiresConfirmation() + ->successNotificationTitle('Roster status updated!'), Actions\Action::make('remove_password') ->visible(fn () => $this->record->hasPassword() && auth()->user()->can('removeSecondaryPassword', $this->record)) ->color('warning') diff --git a/app/Filament/Resources/AccountResource/RelationManagers/EndorsementsRelationManager.php b/app/Filament/Resources/AccountResource/RelationManagers/EndorsementsRelationManager.php new file mode 100644 index 0000000000..568afc8fb7 --- /dev/null +++ b/app/Filament/Resources/AccountResource/RelationManagers/EndorsementsRelationManager.php @@ -0,0 +1,83 @@ +schema([ + Forms\Components\Select::make('position_group_id') + ->label('Endorsement') + ->required() + ->options(PositionGroup::unassignedFor($this->ownerRecord)->mapWithKeys(function (PositionGroup $model) { + return [$model->getKey() => str($model->name)]; + })) + ->hiddenOn('edit'), + + // TODO: determine maximum time in advance. + Forms\Components\DatePicker::make('expires_at') + ->native(false) + ->label('Expires') + ->minDate(now()), + + Forms\Components\Hidden::make('created_by') + ->afterStateHydrated(fn ($component, $state) => $component->state(auth()->user()->getKey())), + ]); + } + + public function isReadOnly(): bool + { + return false; + } + + public function table(Table $table): Table + { + return $table + ->recordTitle(fn ($record) => "{$record->endorsable->name} endorsement") + ->columns([ + Tables\Columns\TextColumn::make('endorsable.name')->label('Name'), + // TODO: color on type + Tables\Columns\TextColumn::make('type')->label('Type')->badge(), + Tables\Columns\TextColumn::make('created_at')->label('Granted')->date(), + Tables\Columns\TextColumn::make('expires_at')->label('Expires')->date()->default(''), + ]) + ->headerActions([ + // Tables\Actions\CreateAction::make()->label('Add endorsement'), + ]) + ->actions([ + // TODO: define permissions for both actions + // Tables\Actions\EditAction::make() + // ->visible(fn ($record) => $record->type == 'Temporary'), + // Tables\Actions\DeleteAction::make() + // ->visible(fn ($record) => $record->type == 'Permanent'), + ]); + } + + public static function createEndorsement(array $data, self $livewire) + { + $creator = auth()->user(); + + Endorsement::create([ + 'account_id' => $livewire->ownerRecord->id, + 'position_group_id' => $data['position_group_id'], + 'expires_at' => $data['expires_at'], + 'created_by' => $creator->id, + ]); + } +} diff --git a/app/Filament/Resources/EndorsementRequestResource.php b/app/Filament/Resources/EndorsementRequestResource.php new file mode 100644 index 0000000000..176f05987d --- /dev/null +++ b/app/Filament/Resources/EndorsementRequestResource.php @@ -0,0 +1,158 @@ +schema([ + Forms\Components\Section::make('Request details')->columns(2)->schema([ + Forms\Components\TextInput::make('account_id')->label('CID')->required(), + + Forms\Components\Select::make('endorsable_type')->options([ + 'App\Models\Atc\PositionGroup' => 'Tier 1 / 2 Endorsements', + 'App\Models\Atc\Position' => 'Solo Endorsement', + 'App\Models\Mship\Qualification' => 'Rating Endorsement', + ])->required()->live(), + + Forms\Components\Hidden::make('requested_by')->default(auth()->id()), + ]), + + Forms\Components\Section::make('Tier 1 Endorsement')->schema([ + Forms\Components\Select::make('endorsable_id')->label('Tier 1 / 2 Name')->options(function () { + return PositionGroup::orderBy('name')->pluck('name', 'id'); + })->required()->searchable(), + ])->visible(fn (Get $get): bool => $get('endorsable_type') === 'App\Models\Atc\PositionGroup'), + + Forms\Components\Section::make('Solo Endorsement')->schema([ + Forms\Components\Select::make('endorsable_id')->label('Endorsement Name')->options(function () { + return Position::temporarilyEndorsable()->orderBy('callsign')->pluck('callsign', 'id'); + })->required()->searchable(), + ])->visible(fn (Get $get): bool => $get('endorsable_type') === 'App\Models\Atc\Position'), + + Forms\Components\Section::make('Rating Endorsement')->schema([ + Forms\Components\Select::make('endorsable_id')->label('Rating')->options(function () { + return Qualification::ofType('atc')->orderBy('vatsim')->pluck('code', 'id'); + })->required()->searchable(), + ])->visible(fn (Get $get): bool => $get('endorsable_type') === 'App\Models\Mship\Qualification'), + + Forms\Components\Section::make('Additional details')->schema([ + Forms\Components\Textarea::make('notes'), + ]), + ]); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + Tables\Columns\TextColumn::make('account_id')->label('CID')->searchable(), + Tables\Columns\TextColumn::make('account.name')->label('Name')->searchable(), + Tables\Columns\TextColumn::make('typeForHumans')->label('Type'), + Tables\Columns\TextColumn::make('endorsable.name')->label('Position/Endorsement'), + Tables\Columns\TextColumn::make('status')->badge()->color(fn (EndorsementRequest $endorsementRequest) => match ($endorsementRequest->status) { + 'Approved' => 'success', + 'Rejected' => 'danger', + default => 'warning', + }), + Tables\Columns\TextColumn::make('requester.name')->label('Requested By'), + Tables\Columns\TextColumn::make('created_at')->label('Requested')->isoDateTimeFormat('lll'), + ]) + ->defaultSort('created_at', 'desc') + ->filters([ + SelectFilter::make('account') + ->label('CID') + ->multiple() + ->relationship('account', 'id'), + SelectFilter::make('actioned_type') + ->label('Status') + ->multiple() + ->options([ + 'approved' => 'Approved', + 'rejected' => 'Rejected', + ]), + ]) + ->actions([ + Tables\Actions\Action::make('approve') + ->form([ + Forms\Components\Select::make('type') + ->options([ + 'Permanent' => 'Permanent', + 'Temporary' => 'Temporary', + ]) + ->default('Temporary') + ->live() + ->required(), + + Forms\Components\TextInput::make('days') + ->label('Valid for (Days)') + ->numeric() + ->step(1) + ->minValue(7) + ->placeholder(7) + ->maxValue(function (EndorsementRequest $endorsementRequest) { + if (! $endorsementRequest->endorsable instanceof Position) { + return 365; + } + + $account = $endorsementRequest->account; + $maximumDays = 90; + + return $maximumDays - $account->daysSpentTemporarilyEndorsedOn($endorsementRequest->endorsable); + }) + ->required(fn (Get $get): bool => $get('type') === 'Temporary') + ->visible(fn (Get $get): bool => $get('type') === 'Temporary'), + + Forms\Components\Textarea::make('notes'), + ]) + ->action(function (EndorsementRequest $endorsementRequest, array $data) { + event(new \App\Events\Training\EndorsementRequestApproved($endorsementRequest, $data['days'] ?? null)); + + Notification::make() + ->title('Endorsement request approved') + ->success(); + })->visible(fn (EndorsementRequest $endorsementRequest) => $endorsementRequest->status === 'Pending' && + auth()->user()->can('approve', $endorsementRequest)), + Tables\Actions\Action::make('reject') + ->requiresConfirmation() + ->action(function (EndorsementRequest $endorsementRequest, array $data) { + $endorsementRequest->markRejected(); + + Notification::make() + ->title('Endorsement request rejected') + ->success(); + })->visible(fn (EndorsementRequest $endorsementRequest) => $endorsementRequest->status === 'Pending' && + auth()->user()->can('approve', $endorsementRequest)), + ]); + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListEndorsementRequests::route('/'), + 'create' => Pages\CreateEndorsementRequest::route('/create'), + ]; + } +} diff --git a/app/Filament/Resources/EndorsementRequestResource/Pages/CreateEndorsementRequest.php b/app/Filament/Resources/EndorsementRequestResource/Pages/CreateEndorsementRequest.php new file mode 100644 index 0000000000..3232b8fc48 --- /dev/null +++ b/app/Filament/Resources/EndorsementRequestResource/Pages/CreateEndorsementRequest.php @@ -0,0 +1,11 @@ +schema([ + Infolists\Components\TextEntry::make('name'), + Infolists\Components\TextEntry::make('positions.callsign'), + ]); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + Tables\Columns\TextColumn::make('name')->label('Name'), + Tables\Columns\TextColumn::make('membership_endorsement_count') + ->label('Endorsed') + ->counts('membershipEndorsement'), + ]) + ->defaultSort('name') + ->actions([ + Tables\Actions\ViewAction::make(), + ]); + } + + public static function getRelations(): array + { + return [ + MembershipEndorsementRelationManager::class, + ]; + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListPositionGroups::route('/'), + 'view' => Pages\ViewPositionGroup::route('/{record}'), + ]; + } +} diff --git a/app/Filament/Resources/PositionGroupResource/Pages/ListPositionGroups.php b/app/Filament/Resources/PositionGroupResource/Pages/ListPositionGroups.php new file mode 100644 index 0000000000..4f88da026f --- /dev/null +++ b/app/Filament/Resources/PositionGroupResource/Pages/ListPositionGroups.php @@ -0,0 +1,11 @@ +recordTitleAttribute('name') + ->columns([ + Tables\Columns\TextColumn::make('account.id')->label('CID')->searchable(), + Tables\Columns\TextColumn::make('account.name')->label('Name')->searchable()->sortable(), + Tables\Columns\TextColumn::make('created_at')->label('Endorsed')->isoDateTimeFormat('lll'), + ]) + ->headerActions([ + Tables\Actions\CreateAction::make()->form([ + Forms\Components\TextInput::make('account_id')->label('CID')->required(), + ])->action(function (array $data) { + try { + $account = \App\Models\Mship\Account::findOrFail($data['account_id']); + } catch (ModelNotFoundException) { + Notification::make()->title('Account not found')->danger()->send(); + + return; + } + + EndorsementCreationService::createPermanent($this->getOwnerRecord(), $account, auth()->user()); + })->visible(fn () => auth()->user()->can('endorse', $this->getOwnerRecord())), + ]); + } +} diff --git a/app/Filament/Resources/WaitingListResource.php b/app/Filament/Resources/WaitingListResource.php index 88da5da770..ca8fcfedec 100644 --- a/app/Filament/Resources/WaitingListResource.php +++ b/app/Filament/Resources/WaitingListResource.php @@ -4,7 +4,6 @@ use App\Filament\Resources\WaitingListResource\Pages; use App\Filament\Resources\WaitingListResource\RelationManagers\AccountsRelationManager; -use App\Filament\Resources\WaitingListResource\RelationManagers\IneligibleAccountsRelationManager; use App\Models\Training\WaitingList; use Filament\Forms; use Filament\Forms\Form; @@ -36,11 +35,6 @@ public static function form(Form $form): Form 'atc' => 'ATC Training', 'pilot' => 'Pilot Training', ])->required(), - - Forms\Components\Select::make('flags_check')->options([ - 'all' => 'ALL Flags', - 'any' => 'ANY Flags', - ])->required(), ]); } @@ -60,7 +54,6 @@ public static function getRelations(): array return [ RelationGroup::make('accounts', [ AccountsRelationManager::class, - IneligibleAccountsRelationManager::class, ]), ]; } diff --git a/app/Filament/Resources/WaitingListResource/Pages/ViewWaitingList.php b/app/Filament/Resources/WaitingListResource/Pages/ViewWaitingList.php index 3dad33c583..a7ea00258f 100644 --- a/app/Filament/Resources/WaitingListResource/Pages/ViewWaitingList.php +++ b/app/Filament/Resources/WaitingListResource/Pages/ViewWaitingList.php @@ -4,7 +4,7 @@ use App\Filament\Resources\WaitingListResource; use App\Filament\Resources\WaitingListResource\Widgets\IndividualWaitingListOverview; -use App\Models\Atc\Endorsement; +use App\Models\Atc\PositionGroup; use App\Models\Mship\Account; use App\Models\Training\WaitingList\WaitingListFlag; use Carbon\Carbon; @@ -71,7 +71,7 @@ protected function getHeaderActions(): array ->action(function ($data, $action) { $flag = WaitingListFlag::create([ 'name' => $data['name'], - 'endorsement_id' => $data['endorsement_id'], + 'position_group_id' => $data['position_group_id'], ]); $this->record->addFlag($flag); @@ -82,7 +82,7 @@ protected function getHeaderActions(): array ->form([ TextInput::make('name')->rules(['required', 'min:3', 'unique:training_waiting_list_flags,name']), - Select::make('endorsement_id')->label('Endorsement')->options(fn () => Endorsement::all()->mapWithKeys(function ($item) { + Select::make('position_group_id')->label('Position Group')->options(fn () => PositionGroup::all()->mapWithKeys(function ($item) { return [$item['id'] => $item['name']]; }))->hint('If an option is chosen here, this will be an automated flag. This cannot be reversed.'), ]) diff --git a/app/Filament/Resources/WaitingListResource/RelationManagers/AccountsRelationManager.php b/app/Filament/Resources/WaitingListResource/RelationManagers/AccountsRelationManager.php index b98ea9d6dc..908df7c4da 100644 --- a/app/Filament/Resources/WaitingListResource/RelationManagers/AccountsRelationManager.php +++ b/app/Filament/Resources/WaitingListResource/RelationManagers/AccountsRelationManager.php @@ -2,7 +2,7 @@ namespace App\Filament\Resources\WaitingListResource\RelationManagers; -use App\Models\Training\WaitingList\WaitingListStatus; +use App\Models\Roster; use AxonC\FilamentCopyablePlaceholder\Forms\Components\CopyablePlaceholder; use Filament\Forms; use Filament\Forms\Form; @@ -15,7 +15,7 @@ class AccountsRelationManager extends RelationManager { - protected static string $relationship = 'eligibleAccounts'; + protected static string $relationship = 'accounts'; protected static ?string $recordTitleAttribute = 'id'; @@ -39,30 +39,6 @@ public function form(Form $form): Form ->placeholder('Add notes here'), ]), - Forms\Components\Fieldset::make('account_status_fieldset') - ->label('Account Status') - ->schema(function ($record) { - return [ - Forms\Components\Radio::make('account_status') - ->label('Status') - ->options([ - WaitingListStatus::DEFAULT_STATUS => 'Active', - WaitingListStatus::DEFERRED => 'Deferred', - ]) - ->afterStateHydrated(fn ($component, $state) => $component->state($record->pivot->current_status->id)), - ]; - }), - Forms\Components\Fieldset::make('automatic_flags') - ->label('Automatic Flags') - ->schema(function ($record) { - return $record->pivot->flags->filter(fn ($flag) => $flag->endorsement_id != null)->map(function ($flag) { - return Forms\Components\Toggle::make('flags.'.$flag->id) - ->disabled() - ->label($flag->name) - ->afterStateHydrated(fn ($component, $state) => $component->state((bool) $flag->pivot->value)); - })->all(); - }) - ->visible(fn ($record) => $record->pivot->flags->filter(fn ($flag) => $flag->endorsement_id != null)->isNotEmpty()), Forms\Components\Fieldset::make('cts_theory_exam') ->label('CTS Theory Exam') @@ -79,34 +55,13 @@ public function form(Form $form): Form Forms\Components\Fieldset::make('manual_flags') ->label('Manual Flags') ->schema(function ($record) { - return $record->pivot->flags->filter(fn ($flag) => $flag->endorsement_id == null)->map(function ($flag) { + return $record->pivot->flags->filter(fn ($flag) => $flag->position_group_id == null)->map(function ($flag) { return Forms\Components\Toggle::make('flags.'.$flag->id) ->label($flag->name) ->afterStateHydrated(fn ($component, $state) => $component->state((bool) $flag->pivot->value)); })->all(); }) ->visible(fn ($record) => $record->pivot->flags->isNotEmpty()), - - Forms\Components\Fieldset::make('eligibility_summary') - ->label('Eligibility Breakdown') - ->schema(function ($record) { - return [ - Forms\Components\Toggle::make('base_controlling_hours') - ->label('Controlling Hours') - ->disabled() - ->afterStateHydrated(fn ($component, $state) => $component->state(Arr::get($record->pivot->eligibility_summary, 'base_controlling_hours'))), - - Forms\Components\Toggle::make('flags_check') - ->label('Flags Check') - ->disabled() - ->afterStateHydrated(fn ($component, $state) => $component->state((bool) Arr::get($record->pivot->flags_status_summary, 'overall'))), - - Forms\Components\Toggle::make('status') - ->label('Status') - ->disabled() - ->afterStateHydrated(fn ($component, $state) => $component->state((bool) $record->pivot->current_status->name == 'Active')), - ]; - }), ]); } @@ -117,17 +72,9 @@ public function table(Table $table): Table Tables\Columns\TextColumn::make('pivot.position')->getStateUsing(fn ($record) => $record->pivot->position ?? '-')->sortable(query: fn (Builder $query, string $direction) => $query->orderBy('pivot_created_at', $direction))->label('Position'), Tables\Columns\TextColumn::make('account_id')->label('CID')->searchable(), Tables\Columns\TextColumn::make('name')->label('Name')->searchable(['name_first', 'name_last']), + Tables\Columns\IconColumn::make('on_roster')->boolean()->label('On roster')->getStateUsing(fn ($record) => Roster::where('account_id', $record->id)->exists()), Tables\Columns\TextColumn::make('pivot.created_at')->label('Added on')->dateTime('d/m/Y'), Tables\Columns\IconColumn::make('pivot.cts_theory_exam')->boolean()->label('CTS Theory Exam')->getStateUsing(fn ($record) => $record->pivot->theory_exam_passed)->visible(fn ($record) => $record->waitingList->feature_toggles['check_cts_theory_exam'] ?? true), - Tables\Columns\IconColumn::make('pivot.atc_hour_check')->boolean()->label('Hour check')->getStateUsing(fn ($record) => Arr::get($record->pivot?->eligibility_summary, 'base_controlling_hours')), - Tables\Columns\IconColumn::make('pivot.flags_check')->boolean()->label('Flags check')->getStateUsing(fn ($record) => (bool) Arr::get($record->pivot?->flags_status_summary, 'overall')), - Tables\Columns\IconColumn::make('pivot.eligible')->boolean()->label('Eligible')->getStateUsing(fn ($record) => $record->pivot->eligible), - Tables\Columns\TextColumn::make('pivot.status') - ->badge() - ->getStateUsing(fn ($record) => $record->pivot->current_status->name)->colors([ - 'success' => 'Active', - 'gray' => 'Deferred', - ])->label('Status'), ]) ->actions([ Tables\Actions\EditAction::make() @@ -136,13 +83,9 @@ public function table(Table $table): Table 'notes' => $data['notes'], ]); - $status = $data['account_status'] == WaitingListStatus::DEFAULT_STATUS ? WaitingListStatus::DEFAULT_STATUS : WaitingListStatus::DEFERRED; - $status = WaitingListStatus::find($status); - $record->pivot->addStatus($status); - $flagsById = collect(Arr::get($data, 'flags', [])); // only update manual flags - $flagsToUpdate = $record->pivot->flags->filter(fn ($flag) => $flag->endorsement_id == null); + $flagsToUpdate = $record->pivot->flags->filter(fn ($flag) => $flag->position_group_id == null); $flagsToUpdate->each(fn ($flag) => $flagsById->get($flag->id) ? $flag->pivot->mark() : $flag->pivot->unMark()); $record->pivot->flags()->sync( diff --git a/app/Filament/Resources/WaitingListResource/RelationManagers/IneligibleAccountsRelationManager.php b/app/Filament/Resources/WaitingListResource/RelationManagers/IneligibleAccountsRelationManager.php deleted file mode 100644 index 52fef5112d..0000000000 --- a/app/Filament/Resources/WaitingListResource/RelationManagers/IneligibleAccountsRelationManager.php +++ /dev/null @@ -1,8 +0,0 @@ -record->eligibleAccounts()->count(); - $ineligibleAccounts = $this->record->ineligibleAccounts()->count(); - $totalAccounts = $eligibleAccounts + $ineligibleAccounts; + $totalAccounts = $this->record->accounts()->count(); $averageWaitTime = $this->record->accounts->average(fn ($account) => $account->pivot->created_at->diffInDays(now())); - $deferredCount = $this->record->accounts->countBy(fn ($account) => $account->pivot->load('status')->currentStatus->id)[WaitingListStatus::DEFERRED] ?? 0; - return [ Stat::make('Total accounts', $totalAccounts), - Stat::make('Eligible accounts', $eligibleAccounts), - Stat::make('Ineligible accounts', $ineligibleAccounts), Stat::make('Average wait time', $averageWaitTime ? round($averageWaitTime).' days' : 'N/A'), - Stat::make('Deferred accounts', $deferredCount), - Stat::make('% eligible', ($totalAccounts ? round($eligibleAccounts / $totalAccounts * 100) : 100).'%'), ]; } } diff --git a/app/Http/Controllers/Atc/EndorsementController.php b/app/Http/Controllers/Atc/EndorsementController.php index 2e74f24529..bd7909844f 100644 --- a/app/Http/Controllers/Atc/EndorsementController.php +++ b/app/Http/Controllers/Atc/EndorsementController.php @@ -3,64 +3,70 @@ namespace App\Http\Controllers\Atc; use App\Http\Controllers\BaseController; -use App\Models\Atc\Endorsement; +use App\Models\Atc\PositionGroup; use Illuminate\Support\Facades\Redirect; class EndorsementController extends BaseController { public function getGatwickGroundIndex() { - $endorsement = Endorsement::with('conditions')->where('name', 'EGKK_GND')->first(); + return Redirect::route('mship.manage.dashboard') + ->withError("We're making some changes to this page, please check back later."); - $hours = $endorsement->conditions->map(function ($condition) { - return $condition->progressForUser($this->account); - }); - - if (! $this->account->fully_defined || ! $this->account->qualificationAtc->isS1) { - return Redirect::route('mship.manage.dashboard') - ->withError('Only S1 rated controllers are eligible for a Gatwick Ground endorsement.'); - } - - return $this->viewMake('controllers.endorsements.gatwick_ground') - ->with('endorsment', $endorsement) - ->with('conditions', $endorsement->conditions) - ->with('hours', $hours->all()); + // $endorsement = PositionGroup::with('conditions')->where('name', 'Gatwick S1 (DEL/GND)')->first(); + // + // $hours = $endorsement->conditions->map(function ($condition) { + // return $condition->progressForUser($this->account); + // }); + // + // if (! $this->account->fully_defined || ! $this->account->qualificationAtc->isS1) { + // return Redirect::route('mship.manage.dashboard') + // ->withError('Only S1 rated controllers are eligible for a Gatwick Ground endorsement.'); + // } + // + // return $this->viewMake('controllers.endorsements.gatwick_ground') + // ->with('endorsment', $endorsement) + // ->with('conditions', $endorsement->conditions) + // ->with('hours', $hours->all()); } public function getAreaIndex() { - $endorsements = Endorsement::whereIn('name', ['LON_S_CTR', 'LON_C_CTR', 'LON_N_CTR', 'SCO_CTR'])->get(); - - if ($endorsements->count() < 1) { - return Redirect::route('mship.manage.dashboard') - ->withError('Endorsements improperly configured'); - } - - if (! $this->account->fully_defined || ! $this->account->qualificationAtc->isS3) { - return Redirect::route('mship.manage.dashboard') - ->withError('Only S3 rated controllers can see their C1 Training Place eligibility.'); - } - - $endorsements = $endorsements->load('conditions')->map(function ($endorsement) { - $conditions = $endorsement->conditions->map(function ($condition) use ($endorsement) { - return [ - 'endorsement_id' => $endorsement->id, - // extract the likely position name from the criterion loaded into the database. - 'position' => str_replace('%', '_', $condition->positions[0]), - 'required_hours' => $condition->required_hours, - 'within_months' => $condition->within_months, - 'progress' => round($condition->progressForUser($this->account)->sum(), 1), - 'complete' => $condition->isMetForUser($this->account), - ]; - }); - - return [ - 'name' => $endorsement->name, - 'conditions' => $conditions, - ]; - }); + return Redirect::route('mship.manage.dashboard') + ->withError("We're making some changes to this page, please check back later."); - return $this->viewMake('controllers.endorsements.area') - ->with('endorsements', $endorsements); + // $positionGroups = PositionGroup::whereIn('name', ['LON_S_CTR', 'LON_C_CTR', 'LON_N_CTR', 'SCO_CTR'])->get(); + // + // if ($positionGroups->count() < 1) { + // return Redirect::route('mship.manage.dashboard') + // ->withError('Endorsements improperly configured'); + // } + // + // if (! $this->account->fully_defined || ! $this->account->qualificationAtc->isS3) { + // return Redirect::route('mship.manage.dashboard') + // ->withError('Only S3 rated controllers can see their C1 Training Place eligibility.'); + // } + // + // $positionGroups = $positionGroups->load('conditions')->map(function ($positionGroup) { + // $conditions = $positionGroup->conditions->map(function ($condition) use ($positionGroup) { + // return [ + // 'position_group_id' => $positionGroup->id, + // // extract the likely position name from the criterion loaded into the database. + // 'position' => str_replace('%', '_', $condition->positions[0]), + // 'required_hours' => $condition->required_hours, + // 'within_months' => $condition->within_months, + // 'progress' => round($condition->progressForUser($this->account)->sum(), 1), + // 'complete' => $condition->isMetForUser($this->account), + // ]; + // }); + // + // return [ + // 'name' => $positionGroup->name, + // 'conditions' => $conditions, + // ]; + // }); + // + // return $this->viewMake('controllers.endorsements.area') + // ->with('positionGroups', $positionGroups); } } diff --git a/app/Http/Controllers/Mship/Management.php b/app/Http/Controllers/Mship/Management.php index 7e4c071274..de10a7f4ed 100644 --- a/app/Http/Controllers/Mship/Management.php +++ b/app/Http/Controllers/Mship/Management.php @@ -5,6 +5,7 @@ use App\Jobs\UpdateMember; use App\Libraries\UKCP as UKCPLibrary; use App\Models\Mship\Account\Email as AccountEmail; +use App\Models\Roster; use App\Models\Sys\Token as SystemToken; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Cache; @@ -46,8 +47,11 @@ public function getDashboard() ); $pluginKeys = $this->ukcp->getValidTokensFor(auth()->user()); + $roster = Roster::where('account_id', auth()->user()->id)->exists(); - return $this->viewMake('mship.management.dashboard')->with('pluginKeys', $pluginKeys); + return $this->viewMake('mship.management.dashboard') + ->with('pluginKeys', $pluginKeys) + ->with('roster', $roster); } public function postInvisibility() diff --git a/app/Http/Controllers/Mship/WaitingLists.php b/app/Http/Controllers/Mship/WaitingLists.php index e40c9acec7..1e42c084c0 100644 --- a/app/Http/Controllers/Mship/WaitingLists.php +++ b/app/Http/Controllers/Mship/WaitingLists.php @@ -13,11 +13,9 @@ public function index(Request $request) $atcWaitingLists = $request->user()->currentWaitingLists() ->withPivot([ 'created_at', - 'eligible', ])->where('department', WaitingList::ATC_DEPARTMENT)->get(); $pilotWaitingLists = $request->user()->currentWaitingLists()->withPivot([ 'created_at', - 'eligible', ])->where('department', WaitingList::PILOT_DEPARTMENT)->get(); return view('mship.waiting-lists.index', [ @@ -31,11 +29,10 @@ public function view(Request $request, $waitingListId) { $list = $request->user()->currentWaitingLists()->where('training_waiting_list.id', $waitingListId)->withPivot([ 'created_at', - 'eligible', ])->firstOrFail(); $automaticFlags = $list->pivot->flags->filter(function ($flag) { - return (bool) $flag->endorsement_id; + return (bool) $flag->position_group_id; }); return view('mship.waiting-lists.view', ['list' => $list, 'automaticFlags' => $automaticFlags]); diff --git a/app/Http/Controllers/Site/AirportController.php b/app/Http/Controllers/Site/AirportController.php index 4238b5e991..5d3c6a8f9f 100644 --- a/app/Http/Controllers/Site/AirportController.php +++ b/app/Http/Controllers/Site/AirportController.php @@ -27,7 +27,7 @@ public function show(Airport $airport) { $airport->load(['navaids', 'runways', 'procedures', 'procedures.runway']); - $stations = $airport->stations()->orderByDesc('type')->get()->groupBy('type')->transform(function ($group) { + $positions = $airport->positions()->orderByDesc('type')->get()->groupBy('type')->transform(function ($group) { return $group->sortBy(function ($station) { return strlen($station->callsign) * ($station->sub_station ? 2 : 1); }); @@ -37,7 +37,7 @@ public function show(Airport $airport) ->with( [ 'airport' => $airport, - 'stations' => $stations, + 'positions' => $positions, 'stands' => $this->ukcp->getStandStatus(Str::upper($airport->icao)), ] ); diff --git a/app/Http/Middleware/FilamentAccessMiddleware.php b/app/Http/Middleware/FilamentAccessMiddleware.php index 254b493e1a..724fb0c40e 100644 --- a/app/Http/Middleware/FilamentAccessMiddleware.php +++ b/app/Http/Middleware/FilamentAccessMiddleware.php @@ -16,6 +16,7 @@ class FilamentAccessMiddleware */ public function handle(Request $request, Closure $next) { + $account = $request->user(); if (! $account) { diff --git a/app/Http/Middleware/VerifyCsrfToken.php b/app/Http/Middleware/VerifyCsrfToken.php index c4f8992b1b..6f793611de 100644 --- a/app/Http/Middleware/VerifyCsrfToken.php +++ b/app/Http/Middleware/VerifyCsrfToken.php @@ -21,6 +21,7 @@ class VerifyCsrfToken extends Middleware protected $except = [ 'webhook/*', 'frame.php', + 'livewire/*', 'external/*', ]; } diff --git a/app/Jobs/Training/UpdateAccountWaitingListEligibility.php b/app/Jobs/Training/UpdateAccountWaitingListEligibility.php index 42c0321dc8..8fa03c9bbf 100644 --- a/app/Jobs/Training/UpdateAccountWaitingListEligibility.php +++ b/app/Jobs/Training/UpdateAccountWaitingListEligibility.php @@ -3,8 +3,8 @@ namespace App\Jobs\Training; use App\Models\Mship\Account; -use App\Services\Training\CheckWaitingListEligibility; -use App\Services\Training\WriteWaitingListEligibility; +use App\Services\Training\CheckWaitingListFlags; +use App\Services\Training\WriteWaitingListFlagSummary; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; @@ -31,12 +31,12 @@ public function __construct(public Account $account) */ public function handle() { - $service = new CheckWaitingListEligibility($this->account); + $service = new CheckWaitingListFlags($this->account); $accountWaitingLists = $this->account->currentWaitingLists; foreach ($accountWaitingLists as $waitingList) { - WriteWaitingListEligibility::handle($waitingList, $service); + WriteWaitingListFlagSummary::handle($waitingList, $service); } } } diff --git a/app/Libraries/UKCP.php b/app/Libraries/UKCP.php index ef8f511818..6ed12e56dd 100644 --- a/app/Libraries/UKCP.php +++ b/app/Libraries/UKCP.php @@ -121,6 +121,43 @@ function () use ($airfield) { } } + public function getUnreadNotificationsForUser(Account $account) + { + try { + $url = config('services.ukcp.url')."/api/user/{$account->id}/notifications/unread"; + $result = $this->client->get($url, [ + 'headers' => [ + 'Authorization' => 'Bearer '.$this->apiKey, + ], + ]); + + return json_decode($result->getBody()->getContents(), true); + } catch (ClientException $e) { + Log::info("UKCP Client Exception {$e->getMessage()} when getting notifications"); + + return []; + } + } + + public function markNotificationReadForUser(Account $account, int $notificationId) + { + try { + $url = config('services.ukcp.url')."/api/user/{$account->id}/notifications/read/{$notificationId}"; + $this->client->put($url, [ + 'headers' => [ + 'Authorization' => 'Bearer '.$this->apiKey, + ], + ]); + + return true; + } catch (ClientException $e) { + dd($e); + Log::info("UKCP Client Exception {$e->getMessage()} when marking notification read"); + + return []; + } + } + private function getStandStatusCacheKey(string $airfieldIcao): string { return sprintf('UKCP_STAND_STATUS_%s', $airfieldIcao); diff --git a/app/Listeners/NetworkData/FlushEndorsementCache.php b/app/Listeners/NetworkData/FlushEndorsementCache.php index 88f4525c71..768ace89a2 100644 --- a/app/Listeners/NetworkData/FlushEndorsementCache.php +++ b/app/Listeners/NetworkData/FlushEndorsementCache.php @@ -3,7 +3,7 @@ namespace App\Listeners\NetworkData; use App\Events\NetworkData\AtcSessionEnded; -use App\Models\Atc\Endorsement; +use App\Models\Atc\PositionGroup; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Support\Facades\Cache; @@ -12,7 +12,7 @@ class FlushEndorsementCache implements ShouldQueue public function handle(AtcSessionEnded $event) { $user = $event->atcSession->account; - Endorsement::get(['id'])->each(function (Endorsement $endorsement) use ($user) { + PositionGroup::get(['id'])->each(function (PositionGroup $endorsement) use ($user) { Cache::forget($endorsement->generateCacheKey($user)); }); } diff --git a/app/Listeners/Training/Endorsement/CreateEndorsementFromApproval.php b/app/Listeners/Training/Endorsement/CreateEndorsementFromApproval.php new file mode 100644 index 0000000000..ee2b16bf95 --- /dev/null +++ b/app/Listeners/Training/Endorsement/CreateEndorsementFromApproval.php @@ -0,0 +1,29 @@ +getEndorsementRequest(); + $endorsableEntity = $endorsementRequest->endorsable; + + Endorsement::create([ + 'account_id' => $endorsementRequest->account_id, + 'endorsement_request_id' => $endorsementRequest->id, + 'created_by' => auth()->id(), + 'endorsable_type' => $endorsableEntity::class, + 'endorsable_id' => $endorsableEntity->id, + 'expires_at' => $event->getExpiryDate(), + ]); + + $endorsementRequest->markApproved(); + } +} diff --git a/app/Listeners/Training/WaitingList/AssignDefaultStatus.php b/app/Listeners/Training/WaitingList/AssignDefaultStatus.php deleted file mode 100644 index 1a0073047f..0000000000 --- a/app/Listeners/Training/WaitingList/AssignDefaultStatus.php +++ /dev/null @@ -1,34 +0,0 @@ -waitingList->accounts - ->where('id', (int) $event->account->id) - ->where('deleted_at', '==', null)->first()->pivot - ->addStatus(WaitingListStatus::find(WaitingListStatus::DEFAULT_STATUS)); - } -} diff --git a/app/Listeners/Training/WaitingList/CheckAccountWaitingListEligibilityListener.php b/app/Listeners/Training/WaitingList/CheckAccountWaitingListEligibilityListener.php deleted file mode 100644 index 0969b9d821..0000000000 --- a/app/Listeners/Training/WaitingList/CheckAccountWaitingListEligibilityListener.php +++ /dev/null @@ -1,23 +0,0 @@ -getAccount()->id}"); - - UpdateAccountWaitingListEligibility::dispatch($event->getAccount()); - } -} diff --git a/app/Listeners/Training/WaitingList/LogAccountDemoted.php b/app/Listeners/Training/WaitingList/LogAccountDemoted.php deleted file mode 100644 index 6bbf34d53e..0000000000 --- a/app/Listeners/Training/WaitingList/LogAccountDemoted.php +++ /dev/null @@ -1,30 +0,0 @@ -info("Account {$event->account} ({$event->account->id}) was demoted within {$event->waitingList} by {$event->staffAccount} ({$event->staffAccount->id})"); - } -} diff --git a/app/Listeners/Training/WaitingList/LogAccountPromoted.php b/app/Listeners/Training/WaitingList/LogAccountPromoted.php deleted file mode 100644 index 1cfd0189a3..0000000000 --- a/app/Listeners/Training/WaitingList/LogAccountPromoted.php +++ /dev/null @@ -1,30 +0,0 @@ -info("Account {$event->account} ({$event->account->id}) was promoted within {$event->waitingList} by {$event->staffAccount} ({$event->staffAccount->id})"); - } -} diff --git a/app/Listeners/Training/WaitingList/LogAccountStatusChange.php b/app/Listeners/Training/WaitingList/LogAccountStatusChange.php deleted file mode 100644 index f79ba9e9f9..0000000000 --- a/app/Listeners/Training/WaitingList/LogAccountStatusChange.php +++ /dev/null @@ -1,27 +0,0 @@ - Roster::where('account_id', auth()->user()->id)->exists(), + ]); + } +} diff --git a/app/Livewire/Roster/Renew.php b/app/Livewire/Roster/Renew.php new file mode 100644 index 0000000000..f46c80660a --- /dev/null +++ b/app/Livewire/Roster/Renew.php @@ -0,0 +1,82 @@ +page = 1; + + $userOnRoster = Roster::where('account_id', auth()->user()->id)->exists(); + + $this->notifications = collect($ukcp->getUnreadNotificationsForUser(auth()->user()))->keyBy('id'); + $this->notifications = collect(); + + if ($userOnRoster) { + session()->flash('error', 'You are already on the roster!'); + + return redirect()->route('site.roster.index'); + } + + if (! auth()->user()->hasState('DIVISION') || ! auth()->user()->has_controller_rating) { + return redirect()->route('site.roster.index'); + } + } + + public function nextPage() + { + $this->page++; + } + + public function render(UKCP $ukcp) + { + $lastLogon = AtcNetworkdataService::getLatestNetworkdataForAccount(auth()->user())?->disconnected_at; + $canReactivate = $lastLogon && Carbon::now()->diffInMonths($lastLogon) <= 18; + + return view('livewire.roster.renew', [ + 'notifications' => $this->notifications, + 'canReactivate' => $canReactivate, + 'lastLogon' => $lastLogon?->diffForHumans(), + 'page' => $this->page, + ]); + } + + public function reactivate() + { + Roster::create([ + 'account_id' => auth()->user()->id, + ]); + + session()->flash('success', '✅ You have been reactivated on the roster! Welcome back!'); + + return redirect()->route('site.roster.index'); + } + + public function markNotificationRead(UKCP $ukcp, int $notificationId, int $arrayIndex) + { + $result = $ukcp->markNotificationReadForUser(auth()->user(), $notificationId); + + if ($result) { + $this->notifications->forget($notificationId); + } + } + + #[Computed] + public function reactivateButtonDisabled() + { + return $this->notifications->isNotEmpty(); + } +} diff --git a/app/Livewire/Roster/Search.php b/app/Livewire/Roster/Search.php new file mode 100644 index 0000000000..34b1d5b76a --- /dev/null +++ b/app/Livewire/Roster/Search.php @@ -0,0 +1,33 @@ +searchTerm); + + $this->redirect(route('site.roster.show', ['account' => $account]), navigate: true); + } catch (ModelNotFoundException $e) { + $this->searchTerm = null; + + Notification::make() + ->title('No account found with that CID.') + ->send(); + } + } + + public function render() + { + return view('livewire.roster.search'); + } +} diff --git a/app/Livewire/Roster/Show.php b/app/Livewire/Roster/Show.php new file mode 100644 index 0000000000..747f182eb2 --- /dev/null +++ b/app/Livewire/Roster/Show.php @@ -0,0 +1,45 @@ +account = $account; + $this->roster = Roster::where('account_id', $this->account->id)->first(); + } + + public function search() + { + try { + $this->position = Position::where('callsign', 'LIKE', "%{$this->searchTerm}%")->firstOrFail(); + } catch (ModelNotFoundException $e) { + $this->searchTerm = null; + + Notification::make() + ->title('Position cannot be found.') + ->send(); + } + } + + public function render() + { + return view('livewire.roster.show'); + } +} diff --git a/app/Models/Airport.php b/app/Models/Airport.php index 0b8c95e376..60358deaf0 100644 --- a/app/Models/Airport.php +++ b/app/Models/Airport.php @@ -5,6 +5,7 @@ use App\Models\Airport\Navaid; use App\Models\Airport\Procedure; use App\Models\Airport\Runway; +use App\Models\Atc\Position; use App\Models\NetworkData\Atc; use App\Models\NetworkData\Pilot; @@ -43,7 +44,7 @@ * @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\Airport\Navaid[] $navaids * @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\Airport\Procedure[] $procedures * @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\Airport\Runway[] $runways - * @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\Station[] $stations + * @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\Atc\Position[] $positions * * @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Airport iCAO($icao) * @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Airport uK() @@ -125,15 +126,15 @@ public function runways() /** * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany */ - public function stations() + public function positions() { - return $this->belongsToMany(Station::class, 'airport_stations'); + return $this->belongsToMany(Position::class, 'airport_positions'); } public function getControllersAttribute() { - if ($this->stations->count() > 0) { - return Atc::withCallsignIn($this->stations->pluck('callsign')->push('%'.$this->icao.'%')->all())->online()->with('account')->get(); + if ($this->positions->count() > 0) { + return Atc::withCallsignIn($this->positions->pluck('callsign')->push('%'.$this->icao.'%')->all())->online()->with('account')->get(); } return Atc::withCallsign('%'.$this->icao.'%')->online()->with('account')->get(); diff --git a/app/Models/Atc/Endorseable.php b/app/Models/Atc/Endorseable.php new file mode 100644 index 0000000000..9949d9a4ee --- /dev/null +++ b/app/Models/Atc/Endorseable.php @@ -0,0 +1,12 @@ +hasMany(Condition::class); - } - - public function conditionsMetForUser(Account $account): bool - { - $cacheKey = $this->generateCacheKey($account); - $cacheTtl = 86400; // 24 hours - - if (Cache::has($cacheKey)) { - return (bool) Cache::get($cacheKey); - } - - // check if every condition for the endorsement has been fulfilled - $allMet = $this->conditions->every(function ($condition) use (&$account) { - return $condition->isMetForUser($account); - }); - - // cache the result of whether or not the conditions have been met - Cache::put($cacheKey, $allMet, $cacheTtl); - - return $allMet; - } - - public function generateCacheKey(Account $account) - { - return "endorsement:{$this->id}:account:{$account->id}:met"; - } -} diff --git a/app/Models/Atc/Position.php b/app/Models/Atc/Position.php new file mode 100644 index 0000000000..a870275422 --- /dev/null +++ b/app/Models/Atc/Position.php @@ -0,0 +1,108 @@ + 'boolean', + ]; + + const TYPE_ATIS = 1; + + const TYPE_DELIVERY = 2; + + const TYPE_GROUND = 3; + + const TYPE_TOWER = 4; + + const TYPE_APPROACH = 5; + + const TYPE_ENROUTE = 6; + + const TYPE_TERMINAL = 7; + + const TYPE_FSS = 8; + + public function airports(): BelongsToMany + { + return $this->belongsToMany(Airport::class, 'airport_positions'); + } + + public function getMinimumVatsimQualificationAttribute() + { + return match ($this->type) { + 'Ground', 'Delivery', 'ATIS' => 2, + 'Tower' => 3, + 'Approach/Radar' => 4, + 'FSS', 'Terminal Control', 'Enroute' => 5, + default => 0, + }; + } + + public function getTypeAttribute(int $type): string + { + switch ($type) { + case self::TYPE_ATIS: + return 'ATIS'; + case self::TYPE_DELIVERY: + return 'Delivery'; + case self::TYPE_GROUND: + return 'Ground'; + case self::TYPE_TOWER: + return 'Tower'; + case self::TYPE_APPROACH: + return 'Approach/Radar'; + case self::TYPE_ENROUTE: + return 'Enroute'; + case self::TYPE_TERMINAL: + return 'Terminal Control'; + case self::TYPE_FSS: + return 'Flight Service Stations'; + default: + return 'Unknown'; + } + } + + public function isTemporarilyEndorsable(): bool + { + return $this->temporarily_endorsable ?? false; + } + + public function scopeTemporarilyEndorsable(Builder $query): Builder + { + return $query->where('temporarily_endorsable', true); + } + + public function name(): Attribute + { + return Attribute::make( + get: fn () => $this->getRawOriginal('name') + ); + } + + public function description(): Attribute + { + return Attribute::make( + get: fn () => $this->callsign + ); + } +} diff --git a/app/Models/Atc/PositionGroup.php b/app/Models/Atc/PositionGroup.php new file mode 100644 index 0000000000..e0a45df5fe --- /dev/null +++ b/app/Models/Atc/PositionGroup.php @@ -0,0 +1,102 @@ +hasMany(PositionGroupCondition::class); + } + + public function positions() + { + return $this->belongsToMany(Position::class, 'position_group_positions', 'position_group_id', 'position_id'); + } + + public function endorsement() + { + return $this->belongsTo(Endorsement::class); + } + + public function maximumAtcQualification() + { + return $this->belongsTo(Qualification::class); + } + + public function membershipEndorsement() + { + return $this->morphMany(MshipEndorsement::class, 'endorsable'); + } + + public function conditionsMetForUser(Account $account): bool + { + $cacheKey = $this->generateCacheKey($account); + $cacheTtl = 86400; // 24 hours + + if (Cache::has($cacheKey)) { + return (bool) Cache::get($cacheKey); + } + + // check if every condition for the endorsement has been fulfilled + $allMet = $this->conditions->every(function ($condition) use (&$account) { + return $condition->isMetForUser($account); + }); + + // cache the result of whether or not the conditions have been met + Cache::put($cacheKey, $allMet, $cacheTtl); + + return $allMet; + } + + public function generateCacheKey(Account $account) + { + return "endorsement:{$this->id}:account:{$account->id}:met"; + } + + public static function unassignedFor(Account $account) + { + return self::all()->reject(function ($positionGroup) use (&$account) { + $nonExpiredEndorsements = $account->load('endorsements')->endorsements->reject(function ($endorsement) { + return $endorsement->hasExpired(); + }); + + return $nonExpiredEndorsements->contains(function ($value, $key) use (&$positionGroup) { + return $value->endorsable_id == $positionGroup->id + && $value->endorsable_type == PositionGroup::class; + }); + }); + } + + public function name(): Attribute + { + return Attribute::make( + get: fn () => $this->getRawOriginal('name') + ); + } + + public function description(): Attribute + { + return Attribute::make( + get: fn () => implode(', ', $this->positions->map( + fn ($position) => $position->callsign + )->toArray()) + ); + } +} diff --git a/app/Models/Atc/Endorsement/Condition.php b/app/Models/Atc/PositionGroupCondition.php similarity index 92% rename from app/Models/Atc/Endorsement/Condition.php rename to app/Models/Atc/PositionGroupCondition.php index bdbb0135c8..f4e6550363 100644 --- a/app/Models/Atc/Endorsement/Condition.php +++ b/app/Models/Atc/PositionGroupCondition.php @@ -1,18 +1,17 @@ belongsTo(Endorsement::class); + return $this->belongsTo(PositionGroup::class); } public function getHumanPositionsAttribute() diff --git a/app/Models/Atc/PositionGroupPosition.php b/app/Models/Atc/PositionGroupPosition.php new file mode 100644 index 0000000000..9ac55ae95e --- /dev/null +++ b/app/Models/Atc/PositionGroupPosition.php @@ -0,0 +1,13 @@ +belongsTo(PositionGroup::class); + } +} diff --git a/app/Models/Cts/Position.php b/app/Models/Cts/Position.php index 8eb90137dc..6439720cc6 100644 --- a/app/Models/Cts/Position.php +++ b/app/Models/Cts/Position.php @@ -2,10 +2,13 @@ namespace App\Models\Cts; +use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class Position extends Model { + use HasFactory; + protected $connection = 'cts'; public $timestamps = false; diff --git a/app/Models/Mship/Account.php b/app/Models/Mship/Account.php index dcad87ad11..9d1ac04c70 100644 --- a/app/Models/Mship/Account.php +++ b/app/Models/Mship/Account.php @@ -12,6 +12,7 @@ use App\Models\Mship\Concerns\HasCTSAccount; use App\Models\Mship\Concerns\HasDiscordAccount; use App\Models\Mship\Concerns\HasEmails; +use App\Models\Mship\Concerns\HasEndorsement; use App\Models\Mship\Concerns\HasForumAccount; use App\Models\Mship\Concerns\HasHelpdeskAccount; use App\Models\Mship\Concerns\HasMoodleAccount; @@ -170,6 +171,7 @@ class Account extends Model implements AuthenticatableContract, AuthorizableCont HasCTSAccount, HasDiscordAccount, HasEmails, + HasEndorsement, HasFactory, HasForumAccount, HasHelpdeskAccount, diff --git a/app/Models/Mship/Account/Endorsement.php b/app/Models/Mship/Account/Endorsement.php new file mode 100644 index 0000000000..08615e2bc8 --- /dev/null +++ b/app/Models/Mship/Account/Endorsement.php @@ -0,0 +1,68 @@ + 'datetime', + ]; + + public function endorsable() + { + return $this->morphTo(); + } + + public function account() + { + return $this->belongsTo(Account::class); + } + + public function type(): Attribute + { + return Attribute::make( + get: function (mixed $value, array $attributes) { + return match ($attributes['endorsable_type']) { + PositionGroup::class => 'Tier Endorsement', + Position::class => 'Solo Endorsement', + Qualification::class => 'Rating Endorsement', + default => 'Unknown' + }; + }, + ); + } + + public function expires(): bool + { + return isset($this->expires_at); + } + + public function hasExpired(): bool + { + return ! is_null($this->expires_at) && $this->expires_at->isPast(); + } + + public function scopeActive(Builder $query) + { + return $query->whereNull('expires_at') + ->orWhereDate('expires_at', '>=', now()); + } +} diff --git a/app/Models/Mship/Account/EndorsementRequest.php b/app/Models/Mship/Account/EndorsementRequest.php new file mode 100644 index 0000000000..1c3a2c07b1 --- /dev/null +++ b/app/Models/Mship/Account/EndorsementRequest.php @@ -0,0 +1,99 @@ +morphTo(); + } + + public function account(): BelongsTo + { + return $this->belongsTo(Account::class); + } + + public function requester(): BelongsTo + { + return $this->belongsTo(Account::class, 'requested_by'); + } + + public function status(): Attribute + { + return Attribute::make( + get: function () { + $hasBeenActioned = (bool) $this->getAttribute('actioned_at'); + + if (! $hasBeenActioned) { + return 'Pending'; + } + + $actionedType = $this->getAttribute('actioned_type'); + + return match ($actionedType) { + self::STATUS_APPROVED => 'Approved', + self::STATUS_REJECTED => 'Rejected', + }; + }, + ); + } + + public function typeForHumans(): Attribute + { + return Attribute::make( + get: function () { + return match ($this->getAttribute('endorsable_type')) { + 'App\Models\Atc\PositionGroup' => 'Tier 1 Endorsement', + 'App\Models\Atc\Position' => 'Solo Endorsement', + 'App\Models\Mship\Qualification' => 'Rating Endorsement', + }; + }, + ); + } + + public function type(): Attribute + { + return Attribute::make( + get: function () { + return match ($this->getAttribute('endorsable_type')) { + 'App\Models\Atc\PositionGroup' => 'permanent', + 'App\Models\Atc\Position' => 'temporary', + 'App\Models\Mship\Qualification' => 'permanent', + }; + }, + ); + } + + public function markApproved() + { + $this->update([ + 'actioned_at' => now(), + 'actioned_type' => self::STATUS_APPROVED, + 'actioned_by' => auth()->user()->id, + ]); + } + + public function markRejected() + { + $this->update([ + 'actioned_at' => now(), + 'actioned_type' => self::STATUS_REJECTED, + 'actioned_by' => auth()->user()->id, + ]); + } +} diff --git a/app/Models/Mship/Concerns/HasEndorsement.php b/app/Models/Mship/Concerns/HasEndorsement.php new file mode 100644 index 0000000000..ebe825869f --- /dev/null +++ b/app/Models/Mship/Concerns/HasEndorsement.php @@ -0,0 +1,42 @@ +hasMany(Endorsement::class, 'account_id'); + } + + public function permanentEndorsements() + { + return $this->endorsements()->whereNull('expires_at'); + } + + public function temporaryEndorsements() + { + return $this->endorsements()->whereNotNull('expires_at'); + } + + public function daysSpentTemporarilyEndorsedOn(Position $endorsable): int + { + $positionTemporaryEndorsements = $this->temporaryEndorsements() + ->where('endorsable_id', $endorsable->id) + ->where('endorsable_type', Position::class) + ->get(); + + $daysFromExpiredEndorsements = $positionTemporaryEndorsements + ->where('expires_at', '<', now()) + ->sum(fn ($endorsement) => $endorsement->created_at->startOfDay()->diffInDays($endorsement->expires_at)); + + $daysFromActiveEndorsements = $positionTemporaryEndorsements + ->where('expires_at', '>=', now()) + ->sum(fn ($endorsement) => $endorsement->created_at->startOfDay()->diffInDays(now()) + 1); + + return $daysFromExpiredEndorsements + $daysFromActiveEndorsements; + } +} diff --git a/app/Models/Mship/Concerns/HasQualifications.php b/app/Models/Mship/Concerns/HasQualifications.php index de4c642da7..a6f119b76d 100644 --- a/app/Models/Mship/Concerns/HasQualifications.php +++ b/app/Models/Mship/Concerns/HasQualifications.php @@ -7,6 +7,7 @@ use App\Events\Mship\Qualifications\QualificationAdded; use App\Models\Mship\AccountQualification; use App\Models\Mship\Qualification; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Support\Facades\Log; trait HasQualifications @@ -193,4 +194,11 @@ public function getQualificationsAdminAttribute() return $qual->type == QualificationTypeEnum::Admin->value; }); } + + public function hasControllerRating(): Attribute + { + return Attribute::make( + get: fn () => $this->qualification_atc->vatsim > 1, + ); + } } diff --git a/app/Models/Mship/Qualification.php b/app/Models/Mship/Qualification.php index e103b90f36..e587466f5b 100644 --- a/app/Models/Mship/Qualification.php +++ b/app/Models/Mship/Qualification.php @@ -2,7 +2,9 @@ namespace App\Models\Mship; +use App\Models\Atc\Endorseable; use App\Models\Model; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; /** @@ -36,7 +38,7 @@ * * @mixin \Eloquent */ -class Qualification extends Model +class Qualification extends Model implements Endorseable { use HasFactory; @@ -167,4 +169,18 @@ public function getIsC3Attribute() { return $this->code == 'C3'; } + + public function name(): Attribute + { + return Attribute::make( + get: fn () => "{$this->name_long} ({$this->code})" + ); + } + + public function description(): Attribute + { + return Attribute::make( + get: fn () => "All standard positions at the {$this->code} level." + ); + } } diff --git a/app/Models/NetworkData/Atc.php b/app/Models/NetworkData/Atc.php index 5a50366268..47deff47ec 100644 --- a/app/Models/NetworkData/Atc.php +++ b/app/Models/NetworkData/Atc.php @@ -199,6 +199,18 @@ public static function scopeThisYear($query) return $query->where('connected_at', '>=', $startOfYear); } + public function scopeAccountIsPartOfUk($query) + { + return $query->join('mship_account_state', function ($join) { + $join->on('mship_account_state.account_id', '=', 'networkdata_atc.account_id') + ->whereNull('mship_account_state.end_at'); + }) + ->join('mship_state', function ($join) { + $join->on('mship_state.id', '=', 'mship_account_state.state_id') + ->whereIn('mship_state.code', ['DIVISION', 'VISITING', 'TRANSFERRING']); + }); + } + public static function scopeIsUK($query) { return $query->where(function ($subQuery) { @@ -217,6 +229,11 @@ public static function scopeIsUK($query) }); } + public static function scopePositionIsWithinUK($query) + { + return $query->isUk(); + } + public function account() { return $this->belongsTo(\App\Models\Mship\Account::class, 'account_id', 'id'); diff --git a/app/Models/Roster.php b/app/Models/Roster.php new file mode 100644 index 0000000000..0a4b2592ed --- /dev/null +++ b/app/Models/Roster.php @@ -0,0 +1,138 @@ +whereHas('account', function ($query) { + $query->whereHas('states', function ($query) { + $query + ->join('roster', 'mship_account_state.account_id', '=', 'roster.account_id') + ->whereIn('mship_state.code', ['DIVISION', 'VISITING', 'TRANSFERRING']) + ->orWhereColumn('roster.updated_at', '>', 'mship_account_state.start_at'); + }); + }); + }); + } + + public function account() + { + return $this->belongsTo(Account::class); + } + + public function remove() + { + // Notify that they were removed (database and email) + // Remove from waiting lists too + $this->delete(); + } + + public function accountCanControl(Position $position) + { + // If the account is not on the roster, + // they cannot control. + if (! $this->account) { + return false; + } + + // If the position is part of a group, + // a) are they a home member with a rating above the position's maximum? + // b) are they a visiting or transferring member with an endorsement up to a rating above the position group's maximum? + // c) are they endorsed on this specific position group? + if ($positionGroupPosition = PositionGroupPosition::where('position_id', $position->id)->first()) { + $isEntitledByHomeMemberRating = $this->account->hasState('DIVISION') && + $this->account->qualification_atc->vatsim > $positionGroupPosition->positionGroup?->maximumAtcQualification?->vatsim; + + $isEndorsedToRating = ($this->account->hasState('VISITING') || $this->account->hasState('TRANSFERRING')) + || $this->account + ->endorsements() + ->active() + ->whereHasMorph('endorsable', + Qualification::class + ) + ->get() + ->sortByDesc('endorsable.vatsim') + ->first()?->endorsable?->vatsim > $positionGroupPosition->positionGroup?->maximumAtcQualification?->vatsim; + + $hasEndorsementForPositionGroup = $this->account + ->endorsements() + ->active() + ->whereHasMorph('endorsable', + PositionGroup::class, + fn ($query) => $query->where('id', $positionGroupPosition->position_group_id) + ) + ->exists(); + + return $isEntitledByHomeMemberRating || $isEndorsedToRating || $hasEndorsementForPositionGroup; + } + + // If the position is above their rating, do they + // have an active solo endorsement? + if ($position->getMinimumVatsimQualificationAttribute() > $this->account->qualification_atc->vatsim) { + return $this->account + ->endorsements() + ->active() + ->whereHasMorph('endorsable', + Position::class, + fn ($query) => $query->where('id', $position->id) + ) + ->exists(); + } + + // If they are visiting or transferring, they need to have been + // specifically given permission to control up to their rating + if ($this->account->hasState('VISITING') || $this->account->hasState('TRANSFERRING')) { + return $this->account + ->endorsements() + ->active() + ->whereHasMorph('endorsable', + Qualification::class + ) + ->get() + ->sortByDesc('endorsable.vatsim') + ->first()?->endorsable?->vatsim >= $position->getMinimumVatsimQualificationAttribute(); + } + + // If they are in a region or international, they cannot control + // without one of the above conditions being met. + if ($this->account->hasState('REGION') || $this->account->hasState('INTERNATIONAL')) { + return false; + } + + // They can control unrestricted up to their rating and + // the position isn't restricted by an endorsement + return true; + } +} diff --git a/app/Models/Station.php b/app/Models/Station.php deleted file mode 100644 index fb72d6e505..0000000000 --- a/app/Models/Station.php +++ /dev/null @@ -1,94 +0,0 @@ - 'boolean', - ]; - - const TYPE_ATIS = 1; - - const TYPE_DELIVERY = 2; - - const TYPE_GROUND = 3; - - const TYPE_TOWER = 4; - - const TYPE_APPROACH = 5; - - const TYPE_ENROUTE = 6; - - const TYPE_TERMINAL = 7; - - const TYPE_FSS = 8; - - /** - * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany - */ - public function airports() - { - return $this->belongsToMany(Airport::class, 'airport_stations'); - } - - public function getTypeAttribute($type) - { - switch ($type) { - case self::TYPE_ATIS: - return 'ATIS'; - case self::TYPE_DELIVERY: - return 'Delivery'; - case self::TYPE_GROUND: - return 'Ground'; - case self::TYPE_TOWER: - return 'Tower'; - case self::TYPE_APPROACH: - return 'Approach/Radar'; - case self::TYPE_ENROUTE: - return 'Enroute'; - case self::TYPE_TERMINAL: - return 'Terminal Control'; - case self::TYPE_FSS: - return 'Flight Service Stations'; - default: - return 'Unknown'; - } - } -} diff --git a/app/Models/Training/WaitingList.php b/app/Models/Training/WaitingList.php index f3dcaec7cb..888aa86653 100644 --- a/app/Models/Training/WaitingList.php +++ b/app/Models/Training/WaitingList.php @@ -74,30 +74,7 @@ public function accounts(): BelongsToMany 'deleted_at', 'notes', 'created_at', - 'eligible', - 'eligibility_summary', - 'flags_status_summary', - ])->wherePivot('deleted_at', null); - } - - public function ineligibleAccounts(): BelongsToMany - { - return $this->accounts() - ->wherePivot('eligible', false); - } - - public function eligibleAccounts(): BelongsToMany - { - return $this->accounts() - ->wherePivot('eligible', true); - } - - public function accountsByEligibility($eligible = true) - { - return $this->accounts() - ->orderByPivot('created_at') - ->get() - ->filter(fn ($model) => $model->pivot->eligible == $eligible)->values(); + ])->wherePivot('deleted_at', null)->orderByPivot('created_at'); } /** @@ -117,7 +94,7 @@ public function flags() */ public function accountPosition(Account $account) { - $key = $this->accountsByEligibility(true)->search(function ($accountItem) use ($account) { + $key = $this->accounts->search(function ($accountItem) use ($account) { return $accountItem->id == $account->id; }); diff --git a/app/Models/Training/WaitingList/WaitingListAccount.php b/app/Models/Training/WaitingList/WaitingListAccount.php index 1b92b89695..5325ab348f 100644 --- a/app/Models/Training/WaitingList/WaitingListAccount.php +++ b/app/Models/Training/WaitingList/WaitingListAccount.php @@ -2,17 +2,13 @@ namespace App\Models\Training\WaitingList; -use App\Events\Training\AccountChangedStatusInWaitingList; use App\Models\Cts\TheoryResult; use App\Models\Mship\Account; -use App\Models\NetworkData\Atc; use App\Models\Training\WaitingList; -use Carbon\Carbon; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Database\Eloquent\Relations\Pivot; use Illuminate\Database\Eloquent\SoftDeletes; -use Illuminate\Support\Facades\Cache; class WaitingListAccount extends Pivot { @@ -20,31 +16,17 @@ class WaitingListAccount extends Pivot public $table = 'training_waiting_list_account'; - public $fillable = ['added_by', 'deleted_at', 'notes', 'eligible', 'flags_status_summary', 'eligibility_summary']; + public $fillable = ['added_by', 'deleted_at', 'notes', 'flags_status_summary']; - protected $appends = ['atcHourCheck', 'theory_exam_passed']; + protected $appends = ['theory_exam_passed']; protected $casts = [ - 'eligible' => 'boolean', - 'eligibility_summary' => 'array', 'flags_status_summary' => 'array', ]; // 24 hours protected $cacheTtl = 86400; - public function status() - { - return $this->belongsToMany( - WaitingListStatus::class, - 'training_waiting_list_account_status', - 'waiting_list_account_id', - 'status_id' - ) - ->withPivot(['start_at', 'end_at', 'id'])->using(WaitingListAccountStatus::class) - ->wherePivot('end_at', null); - } - public function flags() { return $this->belongsToMany( @@ -65,29 +47,6 @@ public function account() return $this->belongsTo(Account::class, 'account_id'); } - public function addStatus(WaitingListStatus $listStatus) - { - $nonEnded = $this->status->reject(function ($value, $key) { - return ! is_null($value->pivot->end_at); - }); - - $nonEnded->each(function ($item, $key) { - $item->pivot->endStatus(); - }); - - $this->status()->attach($listStatus, ['start_at' => now()]); - - event(new AccountChangedStatusInWaitingList($this->account, $this->waitingList, auth()->user())); - } - - /** - * @return int - */ - public function removeStatus(WaitingListStatus $listStatus) - { - return $this->status()->detach($listStatus); - } - public function addFlag(WaitingListFlag $listFlag, $value = null) { return $this->flags()->attach($listFlag, ['marked_at' => $value]); @@ -115,69 +74,16 @@ public function unMarkFlag(WaitingListFlag $listFlag) $flag->unMark(); } - public function getCurrentStatusAttribute() - { - return $this->status()->first(); - } - public function getPositionAttribute() { return $this->waitingList->accountPosition($this->account); } - public function recentATCMinutes() - { - $hourCheckKey = "{$this->cacheKey()}:recentAtcMins"; - - if (Cache::has($hourCheckKey)) { - return Cache::get($hourCheckKey); - } - - // gather the sessions from the last 3 months in the UK (isUK scope) - $hours = Atc::where('account_id', $this->account_id) - ->whereDate('disconnected_at', '>=', Carbon::parse('3 months ago'))->isUk()->sum('minutes_online'); - Cache::put($hourCheckKey, $hours, $this->cacheTtl); - - return $hours; - } - - public function atcHourCheck() - { - if ($this->waitingList->department === WaitingList::PILOT_DEPARTMENT) { - return true; - } - - if (! $this->waitingList->should_check_atc_hours) { - return true; - } - - // 12 hours is represented as 720 minutes - $minutesRequired = 720; - - return $this->recentATCMinutes() >= $minutesRequired; - } - public function getAtcHourCheckAttribute() { return $this->atcHourCheck(); } - public function allFlagsChecker() - { - if ($this->waitingList->flags_check == WaitingList::ALL_FLAGS) { - // iterate through each of the flags to see if they are true. If a false flag is detected, stop iterating. - return $this->flags->every(function ($model) { - return $model->pivot->value; - }); - } elseif ($this->waitingList->flags_check == WaitingList::ANY_FLAGS && $this->flags->count() > 0) { - return $this->flags->some(function ($model) { - return $model->pivot->value; - }); - } - - return true; - } - public function theoryExamPassed(): Attribute { $passed = false; diff --git a/app/Models/Training/WaitingList/WaitingListAccountFlag.php b/app/Models/Training/WaitingList/WaitingListAccountFlag.php index 4f25112a05..0f5e450074 100644 --- a/app/Models/Training/WaitingList/WaitingListAccountFlag.php +++ b/app/Models/Training/WaitingList/WaitingListAccountFlag.php @@ -37,8 +37,8 @@ public function unMark() public function getValueAttribute() { - if ($this->flag->endorsement_id) { - return $this->flag->endorsement->conditionsMetForUser($this->waitingListAccount->account); + if ($this->flag->position_group_id) { + return $this->flag->positionGroup->conditionsMetForUser($this->waitingListAccount->account); } return ! is_null($this->marked_at); diff --git a/app/Models/Training/WaitingList/WaitingListAccountStatus.php b/app/Models/Training/WaitingList/WaitingListAccountStatus.php deleted file mode 100644 index d6207c7377..0000000000 --- a/app/Models/Training/WaitingList/WaitingListAccountStatus.php +++ /dev/null @@ -1,34 +0,0 @@ - 'datetime', - 'end_at' => 'datetime', - ]; - - public function isActive() - { - return is_null($this->end_at); - } - - public function endStatus() - { - $this->end_at = now(); - - $this->save(); - } - - public function scopeActiveStatus($query) - { - return $query->whereNull('deleted_at')->first(); - } -} diff --git a/app/Models/Training/WaitingList/WaitingListFlag.php b/app/Models/Training/WaitingList/WaitingListFlag.php index e618dc575f..81ee2edc35 100644 --- a/app/Models/Training/WaitingList/WaitingListFlag.php +++ b/app/Models/Training/WaitingList/WaitingListFlag.php @@ -2,11 +2,14 @@ namespace App\Models\Training\WaitingList; -use App\Models\Atc\Endorsement; +use App\Models\Atc\PositionGroup; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\SoftDeletes; class WaitingListFlag extends Model { + use SoftDeletes; + protected $guarded = []; protected $table = 'training_waiting_list_flags'; @@ -30,8 +33,8 @@ public function waitingListAccounts() )->withPivot(['marked_at'])->using(WaitingListAccountFlag::class); } - public function endorsement() + public function positionGroup() { - return $this->belongsTo(Endorsement::class); + return $this->belongsTo(PositionGroup::class); } } diff --git a/app/Models/Training/WaitingList/WaitingListStatus.php b/app/Models/Training/WaitingList/WaitingListStatus.php deleted file mode 100644 index f588024da4..0000000000 --- a/app/Models/Training/WaitingList/WaitingListStatus.php +++ /dev/null @@ -1,47 +0,0 @@ - 'boolean', - ]; - - const DEFAULT_STATUS = 1; - - const DEFERRED = 2; - - public function waitingListAccount() - { - return $this->belongsToMany( - WaitingListAccount::class, - 'training_waiting_list_account_status', - 'id', - 'status_id' - )->using(WaitingListAccountStatus::class); - } - - /** - * Get the default state. - * - * @param \Illuminate\Database\Eloquent\Builder $query - * @return \Illuminate\Database\Eloquent\Builder - */ - public function scopeDefault($query) - { - return $query->where('default', true)->first(); - } - - public function __toString() - { - return $this->name; - } -} diff --git a/app/Policies/Mship/Account/EndorsementRequestPolicy.php b/app/Policies/Mship/Account/EndorsementRequestPolicy.php new file mode 100644 index 0000000000..53f1e50c9f --- /dev/null +++ b/app/Policies/Mship/Account/EndorsementRequestPolicy.php @@ -0,0 +1,75 @@ +hasAnyPermission('endorsement-request.access'); + } + + /** + * Determine whether the user can view the model. + */ + public function view(Account $account, EndorsementRequest $endorsementRequest): bool + { + return $account->hasAnyPermission("endorsement-request.view.{$endorsementRequest->type}"); + } + + /** + * Determine whether the user can create models. + */ + public function create(Account $account): bool + { + return $account->hasAnyPermission('endorsement-request.create.*'); + } + + /** + * Determine whether the user can update the model. + */ + public function update(Account $account, EndorsementRequest $endorsementRequest): bool + { + return false; + } + + /** + * Determine whether the user can delete the model. + */ + public function delete(Account $account, EndorsementRequest $endorsementRequest): bool + { + return false; + } + + /** + * Determine whether the user can restore the model. + */ + public function restore(Account $account, EndorsementRequest $endorsementRequest): bool + { + return false; + } + + /** + * Determine whether the user can permanently delete the model. + */ + public function forceDelete(Account $account, EndorsementRequest $endorsementRequest): bool + { + return false; + } + + public function approve(Account $account, EndorsementRequest $endorsementRequest): bool + { + return $account->hasAnyPermission("endorsement-request.approve.{$endorsementRequest->type}"); + } + + public function reject(Account $account, EndorsementRequest $endorsementRequest): bool + { + return $account->hasAnyPermission("endorsement-request.reject.{$endorsementRequest->type}"); + } +} diff --git a/app/Policies/PositionGroupPolicy.php b/app/Policies/PositionGroupPolicy.php new file mode 100644 index 0000000000..909c6f378b --- /dev/null +++ b/app/Policies/PositionGroupPolicy.php @@ -0,0 +1,70 @@ +hasAnyPermission('position-group.view.*'); + } + + /** + * Determine whether the user can view the model. + */ + public function view(Account $account, PositionGroup $positionGroup): bool + { + return $account->hasAnyPermission('position-group.view.*'); + } + + /** + * Determine whether the user can create models. + */ + public function create(Account $account): bool + { + return false; + } + + /** + * Determine whether the user can update the model. + */ + public function update(Account $account, PositionGroup $positionGroup): bool + { + return true; + } + + /** + * Determine whether the user can delete the model. + */ + public function delete(Account $account, PositionGroup $positionGroup): bool + { + return false; + } + + /** + * Determine whether the user can restore the model. + */ + public function restore(Account $account, PositionGroup $positionGroup): bool + { + return false; + } + + /** + * Determine whether the user can permanently delete the model. + */ + public function forceDelete(Account $account, PositionGroup $positionGroup): bool + { + return false; + } + + public function endorse(Account $account): bool + { + return $account->hasAnyPermission('endorsement.create.*'); + } +} diff --git a/app/Policies/Training/EndorsementPolicy.php b/app/Policies/Training/EndorsementPolicy.php new file mode 100644 index 0000000000..c16f424c68 --- /dev/null +++ b/app/Policies/Training/EndorsementPolicy.php @@ -0,0 +1,65 @@ + WaitingListFlagsPolicy::class, Qualification::class => QualificationPolicy::class, Feedback::class => FeedbackPolicy::class, + EndorsementRequest::class => EndorsementRequestPolicy::class, + PositionGroup::class => PositionGroupPolicy::class, Ban::class => BanPolicy::class, Role::class => RolePolicy::class, diff --git a/app/Providers/Filament/AppPanelProvider.php b/app/Providers/Filament/AppPanelProvider.php index c551ee0207..58e25ad370 100644 --- a/app/Providers/Filament/AppPanelProvider.php +++ b/app/Providers/Filament/AppPanelProvider.php @@ -58,7 +58,7 @@ public function panel(Panel $panel): Panel FilamentAccessMiddleware::class, ]) ->navigationGroups([ - NavigationGroup::make('Web Services'), + NavigationGroup::make('Technology'), ]) ->navigationItems([ NavigationItem::make('Legacy Admin Panel') @@ -66,12 +66,12 @@ public function panel(Panel $panel): Panel ->icon('heroicon-o-clock') ->visible(fn () => request()->user()->hasPermissionTo('adm')), NavigationItem::make('Horizon') - ->group('Web Services') + ->group('Technology') ->icon('heroicon-o-bars-arrow-down') ->url(fn () => route('horizon.index')) ->visible(fn () => request()->user()->can('viewHorizon')), NavigationItem::make('Telescope') - ->group('Web Services') + ->group('Technology') ->icon('heroicon-o-magnifying-glass') ->url(fn () => route('telescope')) ->visible(fn () => request()->user()->can('viewTelescope')), diff --git a/app/Providers/FilamentServiceProvider.php b/app/Providers/FilamentServiceProvider.php index 3300e324ee..db4c22e80f 100644 --- a/app/Providers/FilamentServiceProvider.php +++ b/app/Providers/FilamentServiceProvider.php @@ -99,5 +99,10 @@ function ($state, $context) use ($resourceClass) { /** @var \Filament\Tables\Columns\IconColumn $this */ return $this->getStateUsing(fn ($record) => $record->{$this->getName()} !== null)->boolean(); }); + + \Filament\Tables\Columns\TextColumn::macro('isoDateTimeFormat', function (string $format): \Filament\Tables\Columns\TextColumn { + /** @var \Filament\Tables\Columns\TextColumn $this */ + return $this->formatStateUsing(fn ($state) => $state->settings(['formatFunction' => 'isoFormat'])->format($format)); + }); } } diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index 9997d51deb..6729b00350 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -52,6 +52,9 @@ protected function mapWebRoutes() Route::middleware('web') ->namespace($this->namespace) ->group(base_path('routes/web.php')); + + Route::middleware('web') + ->group(base_path('routes/web-livewire.php')); } /** diff --git a/app/Providers/TrainingEventServiceProvider.php b/app/Providers/TrainingEventServiceProvider.php index 17eb344c11..1c1526543f 100644 --- a/app/Providers/TrainingEventServiceProvider.php +++ b/app/Providers/TrainingEventServiceProvider.php @@ -9,9 +9,7 @@ class TrainingEventServiceProvider extends ServiceProvider protected $listen = [ \App\Events\Training\AccountAddedToWaitingList::class => [ \App\Listeners\Training\WaitingList\LogAccountAdded::class, - \App\Listeners\Training\WaitingList\AssignDefaultStatus::class, \App\Listeners\Training\WaitingList\AssignFlags::class, - \App\Listeners\Training\WaitingList\CheckAccountWaitingListEligibilityListener::class, ], \App\Events\Training\AccountRemovedFromWaitingList::class => [ \App\Listeners\Training\WaitingList\LogAccountRemoved::class, @@ -19,15 +17,12 @@ class TrainingEventServiceProvider extends ServiceProvider \App\Events\Training\AccountNoteChanged::class => [ \App\Listeners\Training\WaitingList\LogNoteChanged::class, ], - \App\Events\Training\AccountChangedStatusInWaitingList::class => [ - \App\Listeners\Training\WaitingList\CheckAccountWaitingListEligibilityListener::class, - ], - \App\Events\Training\AccountManualFlagChanged::class => [ - \App\Listeners\Training\WaitingList\CheckAccountWaitingListEligibilityListener::class, - ], \App\Events\Training\FlagAddedToWaitingList::class => [ \App\Listeners\Training\WaitingList\CheckWaitingListFollowingFlagAddition::class, ], + \App\Events\Training\EndorsementRequestApproved::class => [ + \App\Listeners\Training\Endorsement\CreateEndorsementFromApproval::class, + ], ]; /** diff --git a/app/Services/Networkdata/AtcNetworkdataService.php b/app/Services/Networkdata/AtcNetworkdataService.php new file mode 100644 index 0000000000..9dcdee6749 --- /dev/null +++ b/app/Services/Networkdata/AtcNetworkdataService.php @@ -0,0 +1,14 @@ + $account->id])->latest()->first(); + } +} diff --git a/app/Services/Training/AddToWaitingList.php b/app/Services/Training/AddToWaitingList.php deleted file mode 100644 index 6b468b3c35..0000000000 --- a/app/Services/Training/AddToWaitingList.php +++ /dev/null @@ -1,32 +0,0 @@ -waitingList = $waitingList; - $this->account = $account; - $this->staffAccount = $staffAccount; - $this->createdAt = $created_at; - } - - public function handle() - { - $this->waitingList->addToWaitingList($this->account, $this->staffAccount, $this->createdAt); - } -} diff --git a/app/Services/Training/CheckWaitingListEligibility.php b/app/Services/Training/CheckWaitingListEligibility.php deleted file mode 100644 index ae981213bc..0000000000 --- a/app/Services/Training/CheckWaitingListEligibility.php +++ /dev/null @@ -1,77 +0,0 @@ -should_check_atc_hours || $waitingList->department == WaitingList::PILOT_DEPARTMENT) { - return true; - } - - $recentAtcMinutes = Atc::where('account_id', $this->account->id) - ->where('disconnected_at', '>=', Carbon::parse('3 months ago'))->isUk() - ->sum('minutes_online'); - - return $recentAtcMinutes >= 720; - } - - /** - * Check the waiting list flags defined in the waiting list - * and return an array with the overall eligibility and a summary of the flags. - * - * This can either be the manual or automated flags backed by an endorsement. - * The 'overall' key represents the status of the flags based on the waiting list flags check type. - * - * Within the 'summary' key, the key is the flag name and the value is the flag value. - */ - public function checkWaitingListFlags(WaitingList $waitingList): array - { - $waitingListAccount = $waitingList->accounts()->where('account_id', $this->account->id)->first()->pivot; - - if ($waitingList->flags()->doesntExist()) { - return ['overall' => true, 'summary' => null]; - } - - $summaryByFlag = $waitingListAccount->flags()->get()->mapWithKeys(function ($flag) { - return [$flag->name => $flag->pivot->value]; - }); - - $method = $waitingList->flags_check == WaitingList::ALL_FLAGS ? 'every' : 'some'; - // check if all flags are true or if any flags are true depending on the waiting list flags check type - $overall = $summaryByFlag->$method(fn ($value) => $value); - - return ['overall' => $overall, 'summary' => $summaryByFlag->toArray()]; - } - - public function checkAccountStatus(WaitingList $waitingList) - { - $waitingListAccount = $this->getWaitingListAccount($waitingList); - - return $waitingListAccount->current_status->name == 'Active'; - } - - public function getOverallEligibility(WaitingList $waitingList): bool - { - $base_hour_checks = $this->checkBaseControllingHours($waitingList); - $flags_summary = $this->checkWaitingListFlags($waitingList); - - return $base_hour_checks && $flags_summary['overall'] && $this->checkAccountStatus($waitingList); - } - - public function getWaitingListAccount(WaitingList $waitingList) - { - return $waitingList->accounts()->where('account_id', $this->account->id)->first()->pivot; - } -} diff --git a/app/Services/Training/CheckWaitingListFlags.php b/app/Services/Training/CheckWaitingListFlags.php new file mode 100644 index 0000000000..259a71a699 --- /dev/null +++ b/app/Services/Training/CheckWaitingListFlags.php @@ -0,0 +1,40 @@ +accounts()->where('account_id', $this->account->id)->first()->pivot; + + if ($waitingList->flags()->doesntExist()) { + return ['summary' => null]; + } + + $summaryByFlag = $waitingListAccount->flags()->get()->mapWithKeys(function ($flag) { + return [$flag->name => $flag->pivot->value]; + }); + + return ['summary' => $summaryByFlag->toArray()]; + } + + public function getWaitingListAccount(WaitingList $waitingList) + { + return $waitingList->accounts()->where('account_id', $this->account->id)->first()->pivot; + } +} diff --git a/app/Services/Training/EndorsementCreationService.php b/app/Services/Training/EndorsementCreationService.php new file mode 100644 index 0000000000..cbcffbcb10 --- /dev/null +++ b/app/Services/Training/EndorsementCreationService.php @@ -0,0 +1,22 @@ + $account->id, + 'created_by' => $creator->id, + 'endorsable_type' => $endorsable::class, + 'endorsable_id' => $endorsable->id, + 'expires_at' => null, + 'endorsement_request_id' => null, + ]); + } +} diff --git a/app/Services/Training/WriteWaitingListEligibility.php b/app/Services/Training/WriteWaitingListEligibility.php deleted file mode 100644 index 27880818ff..0000000000 --- a/app/Services/Training/WriteWaitingListEligibility.php +++ /dev/null @@ -1,23 +0,0 @@ -getWaitingListAccount($waitingList); - - $waitingListAccount->update([ - 'flags_status_summary' => $service->checkWaitingListFlags($waitingList), - 'eligible' => $service->getOverallEligibility($waitingList), - 'eligibility_summary' => [ - 'base_controlling_hours' => $service->checkBaseControllingHours($waitingList), - 'flags' => $service->checkWaitingListFlags($waitingList), - 'account_status' => $service->checkAccountStatus($waitingList), - ], - ]); - } -} diff --git a/app/Services/Training/WriteWaitingListFlagSummary.php b/app/Services/Training/WriteWaitingListFlagSummary.php new file mode 100644 index 0000000000..939042f836 --- /dev/null +++ b/app/Services/Training/WriteWaitingListFlagSummary.php @@ -0,0 +1,17 @@ +getWaitingListAccount($waitingList); + + $waitingListAccount->update([ + 'flags_status_summary' => $service->checkWaitingListFlags($waitingList), + ]); + } +} diff --git a/composer.json b/composer.json index 59b181460b..50333efc9a 100644 --- a/composer.json +++ b/composer.json @@ -78,6 +78,7 @@ "laravelcollective/html": "^6.0", "league/csv": "^8.0", "league/oauth2-client": "^2.4", + "livewire/livewire": "^3.3", "maatwebsite/excel": "~3.1.17", "malahierba-lab/public-id": "dev-main", "ohdearapp/ohdear-php-sdk": "^3.0", @@ -150,7 +151,8 @@ ], "post-autoload-dump": [ "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump", - "@php artisan package:discover" + "@php artisan package:discover", + "@php artisan filament:upgrade" ], "ide-helper": [ "php artisan ide-helper:generate", diff --git a/composer.lock b/composer.lock index 6b406bd27a..1d00e281c9 100644 --- a/composer.lock +++ b/composer.lock @@ -5109,35 +5109,35 @@ }, { "name": "livewire/livewire", - "version": "v3.2.2", + "version": "v3.3.5", "source": { "type": "git", "url": "https://github.com/livewire/livewire.git", - "reference": "c5366279aeb10f75bb358032bb832702e3d0d35a" + "reference": "1ef880fbcdc7b6e5e405cc9135a62cd5fdbcd06a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/livewire/livewire/zipball/c5366279aeb10f75bb358032bb832702e3d0d35a", - "reference": "c5366279aeb10f75bb358032bb832702e3d0d35a", + "url": "https://api.github.com/repos/livewire/livewire/zipball/1ef880fbcdc7b6e5e405cc9135a62cd5fdbcd06a", + "reference": "1ef880fbcdc7b6e5e405cc9135a62cd5fdbcd06a", "shasum": "" }, "require": { - "illuminate/database": "^10.0", - "illuminate/support": "^10.0", - "illuminate/validation": "^10.0", + "illuminate/database": "^10.0|^11.0", + "illuminate/support": "^10.0|^11.0", + "illuminate/validation": "^10.0|^11.0", "league/mime-type-detection": "^1.9", "php": "^8.1", - "symfony/http-kernel": "^6.2" + "symfony/http-kernel": "^6.2|^7.0" }, "require-dev": { "calebporzio/sushi": "^2.1", - "laravel/framework": "^10.0", + "laravel/framework": "^10.0|^11.0", "laravel/prompts": "^0.1.6", "mockery/mockery": "^1.3.1", - "orchestra/testbench": "^8.0", - "orchestra/testbench-dusk": "^8.0", - "phpunit/phpunit": "^9.0", - "psy/psysh": "@stable" + "orchestra/testbench": "^8.0|^9.0", + "orchestra/testbench-dusk": "^8.0|^9.0", + "phpunit/phpunit": "^10.4", + "psy/psysh": "^0.11.22|^0.12" }, "type": "library", "extra": { @@ -5171,7 +5171,7 @@ "description": "A front-end framework for Laravel.", "support": { "issues": "https://github.com/livewire/livewire/issues", - "source": "https://github.com/livewire/livewire/tree/v3.2.2" + "source": "https://github.com/livewire/livewire/tree/v3.3.5" }, "funding": [ { @@ -5179,7 +5179,7 @@ "type": "github" } ], - "time": "2023-11-29T17:43:29+00:00" + "time": "2024-01-02T14:29:17+00:00" }, { "name": "lorisleiva/cron-translator", @@ -15408,5 +15408,5 @@ "platform-dev": { "ext-json": "*" }, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.3.0" } diff --git a/database/factories/Airport/StationFactory.php b/database/factories/Airport/StationFactory.php deleted file mode 100644 index 167e3d8a84..0000000000 --- a/database/factories/Airport/StationFactory.php +++ /dev/null @@ -1,13 +0,0 @@ -define(\App\Models\Station::class, function (Faker $faker) { - return [ - 'callsign' => strtoupper($faker->word).'_'.$faker->randomElement(['TWR', 'GND', 'DEL', 'APP', 'ATIS', 'CTR']), - 'name' => ucfirst($faker->word).' '.$faker->randomElement(['Tower', 'Ground', 'Delivery', 'Approach', 'Information', 'Control']), - 'frequency' => $faker->randomFloat(3, 0, 130), - 'type' => $faker->numberBetween(1, 8), - 'sub_station' => false, - ]; -}); diff --git a/database/factories/Atc/PositionFactory.php b/database/factories/Atc/PositionFactory.php new file mode 100644 index 0000000000..70c0c2edbb --- /dev/null +++ b/database/factories/Atc/PositionFactory.php @@ -0,0 +1,34 @@ + + */ +class PositionFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'callsign' => strtoupper(fake()->word).'_'.fake()->randomElement(['TWR', 'GND', 'DEL', 'APP', 'ATIS', 'CTR']), + 'name' => ucfirst(fake()->word).' '.fake()->randomElement(['Tower', 'Ground', 'Delivery', 'Approach', 'Information', 'Control']), + 'frequency' => fake()->randomFloat(3, 0, 130), + 'type' => fake()->numberBetween(1, 8), + 'sub_station' => false, + ]; + } + + public function temporarilyEndorsable(): self + { + return $this->state([ + 'temporarily_endorsable' => true, + ]); + } +} diff --git a/database/factories/Atc/PositionGroupFactory.php b/database/factories/Atc/PositionGroupFactory.php new file mode 100644 index 0000000000..4d068e920f --- /dev/null +++ b/database/factories/Atc/PositionGroupFactory.php @@ -0,0 +1,23 @@ + + */ +class PositionGroupFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'name' => fake()->word(), + ]; + } +} diff --git a/database/factories/Cts/PositionFactory.php b/database/factories/Cts/PositionFactory.php index 7c3f41875b..de5c919487 100644 --- a/database/factories/Cts/PositionFactory.php +++ b/database/factories/Cts/PositionFactory.php @@ -1,16 +1,22 @@ define(App\Models\Cts\Position::class, function (Faker $faker) { - return [ - 'rts_id' => 1, - 'callsign' => $faker->randomElement(['EGKK_APP', 'EGCC_APP', 'LON_SC_CTR', 'EGGP_GND']), - 'rating' => 1, - 'auto_rating' => 12, - 'vis_roster' => 1, - 'anon_requests' => 0, - 'prog_sheet_id' => 1, - 'prog_sheet_assign_by' => 1, - ]; -}); +use Illuminate\Database\Eloquent\Factories\Factory; + +class PositionFactory extends Factory +{ + public function definition(): array + { + return [ + 'rts_id' => 1, + 'callsign' => $this->faker->randomElement(['EGKK_APP', 'EGCC_APP', 'LON_SC_CTR', 'EGGP_GND']), + 'rating' => 1, + 'auto_rating' => 12, + 'vis_roster' => 1, + 'anon_requests' => 0, + 'prog_sheet_id' => 1, + 'prog_sheet_assign_by' => 1, + ]; + } +} diff --git a/database/factories/Cts/PositionValidationFactory.php b/database/factories/Cts/PositionValidationFactory.php index 49b7cd998f..11fd657bc4 100644 --- a/database/factories/Cts/PositionValidationFactory.php +++ b/database/factories/Cts/PositionValidationFactory.php @@ -8,7 +8,7 @@ $factory->define(App\Models\Cts\PositionValidation::class, function (Faker $faker) { return [ 'member_id' => factory(Member::class)->create()->id, - 'position_id' => factory(Position::class)->create()->id, + 'position_id' => Position::factory()->create()->id, 'status' => rand(1, 5), 'changed_by' => 1111111, 'date_changed' => Carbon::createFromFormat('Y-m-d H:i:s', now())->toDateTimeString(), diff --git a/database/factories/Cts/TheoryResultFactory.php b/database/factories/Cts/TheoryResultFactory.php index ed8b8a1f18..b6aaafa97c 100644 --- a/database/factories/Cts/TheoryResultFactory.php +++ b/database/factories/Cts/TheoryResultFactory.php @@ -21,6 +21,9 @@ public function definition() 'student_id' => factory(Member::class)->create()->id, 'exam' => $this->faker->randomElement(['S1', 'S2', 'S3']), 'pass' => 0, + 'started' => now()->subMinutes(15), + 'expires' => now()->addDays(7), + 'submitted_time' => now(), ]; } } diff --git a/database/factories/EndorsementFactory.php b/database/factories/EndorsementFactory.php index 54ecc4f521..45cd73cf69 100644 --- a/database/factories/EndorsementFactory.php +++ b/database/factories/EndorsementFactory.php @@ -1,15 +1,15 @@ define(\App\Models\Atc\Endorsement::class, function ($faker) { +$factory->define(\App\Models\Atc\PositionGroup::class, function ($faker) { return [ 'name' => $faker->name, ]; }); -$factory->define(\App\Models\Atc\Endorsement\Condition::class, function ($faker) { +$factory->define(\App\Models\Atc\PositionGroupCondition::class, function ($faker) { return [ - 'endorsement_id' => function () { - return factory(\App\Models\Atc\Endorsement::class)->create()->id; + 'position_group_id' => function () { + return factory(\App\Models\Atc\PositionGroup::class)->create()->id; }, 'positions' => ['EGLL_%'], 'required_hours' => $faker->numberBetween(1, 100), diff --git a/database/factories/Mship/Account/EndorsementFactory.php b/database/factories/Mship/Account/EndorsementFactory.php new file mode 100644 index 0000000000..710de4419d --- /dev/null +++ b/database/factories/Mship/Account/EndorsementFactory.php @@ -0,0 +1,37 @@ + + */ +class EndorsementFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'account_id' => Account::factory(), + 'endorsable_type' => PositionGroup::class, + 'endorsable_id' => PositionGroup::factory(), + 'expires_at' => null, + 'created_by' => Account::factory(), + ]; + } + + public function soloEndorsement() + { + return $this->state([ + 'expires_at' => now()->addDays(30), + ]); + } +} diff --git a/database/factories/Mship/Account/EndorsementRequestFactory.php b/database/factories/Mship/Account/EndorsementRequestFactory.php new file mode 100644 index 0000000000..87c2c1daee --- /dev/null +++ b/database/factories/Mship/Account/EndorsementRequestFactory.php @@ -0,0 +1,31 @@ + + */ +class EndorsementRequestFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'account_id' => Account::factory()->create()->id, + 'endorsable_type' => 'App\Models\Atc\PositionGroup', + 'endorsable_id' => PositionGroup::factory()->create()->id, + 'requested_by' => Account::factory()->create()->id, + 'actioned_at' => null, + 'actioned_type' => null, + 'notes' => null, + ]; + } +} diff --git a/database/factories/WaitingListFactory.php b/database/factories/WaitingListFactory.php index 4f999e54f1..282868f150 100644 --- a/database/factories/WaitingListFactory.php +++ b/database/factories/WaitingListFactory.php @@ -9,7 +9,6 @@ 'name' => $name, 'slug' => str_slug($name), 'department' => 'atc', - 'flags_check' => 'all', 'cts_theory_exam_level' => null, 'feature_toggles' => [ 'check_atc_hours' => true, diff --git a/database/migrations/2023_12_07_190336_create_roster_table.php b/database/migrations/2023_12_07_190336_create_roster_table.php new file mode 100644 index 0000000000..6cc56c2ed2 --- /dev/null +++ b/database/migrations/2023_12_07_190336_create_roster_table.php @@ -0,0 +1,28 @@ +id(); + $table->unsignedInteger('account_id')->unique(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('roster'); + } +}; diff --git a/database/migrations/2024_01_04_201826_create_mship_account_endorsement_table.php b/database/migrations/2024_01_04_201826_create_mship_account_endorsement_table.php new file mode 100644 index 0000000000..b2fcdcece6 --- /dev/null +++ b/database/migrations/2024_01_04_201826_create_mship_account_endorsement_table.php @@ -0,0 +1,32 @@ +id(); + $table->unsignedInteger('account_id'); + $table->unsignedInteger('position_group_id'); + $table->timestamp('expires_at')->nullable(); + $table->unsignedInteger('created_by'); + $table->timestamps(); + $table->softDeletes(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('mship_account_endorsement'); + } +}; diff --git a/database/migrations/2024_01_04_203458_create_endorsement_stations_table.php b/database/migrations/2024_01_04_203458_create_endorsement_stations_table.php new file mode 100644 index 0000000000..d8cc0efe5f --- /dev/null +++ b/database/migrations/2024_01_04_203458_create_endorsement_stations_table.php @@ -0,0 +1,29 @@ +id(); + $table->unsignedInteger('position_group_id'); + $table->unsignedInteger('position_id'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('endorsement_stations'); + } +}; diff --git a/database/migrations/2024_01_04_210059_rename_tables_for_endorsements.php b/database/migrations/2024_01_04_210059_rename_tables_for_endorsements.php new file mode 100644 index 0000000000..61a81226c7 --- /dev/null +++ b/database/migrations/2024_01_04_210059_rename_tables_for_endorsements.php @@ -0,0 +1,26 @@ +renameColumn('endorsement_id', 'position_group_id'); + }); + Schema::rename('endorsement_stations', 'position_group_positions'); + Schema::rename('stations', 'positions'); + Schema::rename('airport_stations', 'airport_positions'); + Schema::table('airport_positions', function (Blueprint $table) { + $table->renameColumn('station_id', 'position_id'); + }); + Schema::table('training_waiting_list_flags', function (Blueprint $table) { + $table->renameColumn('endorsement_id', 'position_group_id'); + }); + } +}; diff --git a/database/migrations/2024_01_28_123419_add_temp_endorseable_flag_to_positions_table.php b/database/migrations/2024_01_28_123419_add_temp_endorseable_flag_to_positions_table.php new file mode 100644 index 0000000000..d5ba40d50f --- /dev/null +++ b/database/migrations/2024_01_28_123419_add_temp_endorseable_flag_to_positions_table.php @@ -0,0 +1,28 @@ +boolean('temporarily_endorsable')->default(false)->after('sub_station'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('positions', function (Blueprint $table) { + $table->dropColumn('temporarily_endorsable'); + }); + } +}; diff --git a/database/migrations/2024_01_29_192559_create_endorsement_requests_table_and_polymorphic_endorsements.php b/database/migrations/2024_01_29_192559_create_endorsement_requests_table_and_polymorphic_endorsements.php new file mode 100644 index 0000000000..3ce149d043 --- /dev/null +++ b/database/migrations/2024_01_29_192559_create_endorsement_requests_table_and_polymorphic_endorsements.php @@ -0,0 +1,48 @@ +id(); + $table->unsignedInteger('account_id'); + $table->morphs('endorsable'); + $table->unsignedInteger('requested_by'); + $table->timestamp('actioned_at')->nullable(); + $table->string('actioned_type')->nullable(); + $table->unsignedInteger('actioned_by')->nullable(); + $table->text('notes')->nullable(); + $table->timestamps(); + }); + + Schema::table('mship_account_endorsement', function (Blueprint $table) { + $table->dropColumn('position_group_id'); + $table->after('account_id', function (Blueprint $table) { + $table->morphs('endorsable'); + $table->unsignedInteger('endorsement_request_id')->nullable(); + }); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('endorsement_requests'); + + Schema::table('mship_account_endorsement', function (Blueprint $table) { + $table->dropMorphs('endorsable'); + $table->unsignedInteger('position_group_id'); + $table->dropColumn('endorsement_request_id'); + }); + } +}; diff --git a/database/migrations/2024_02_15_193128_amend_mship_endorsements_for_nullable_created_by.php b/database/migrations/2024_02_15_193128_amend_mship_endorsements_for_nullable_created_by.php new file mode 100644 index 0000000000..c5fe3888fe --- /dev/null +++ b/database/migrations/2024_02_15_193128_amend_mship_endorsements_for_nullable_created_by.php @@ -0,0 +1,28 @@ +unsignedInteger('created_by')->nullable()->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('mship_account_endorsement', function (Blueprint $table) { + $table->unsignedInteger('created_by')->change(); + }); + } +}; diff --git a/database/migrations/2024_02_26_170552_add_maximum_qualification_to_position_groups_table.php b/database/migrations/2024_02_26_170552_add_maximum_qualification_to_position_groups_table.php new file mode 100644 index 0000000000..c341589de0 --- /dev/null +++ b/database/migrations/2024_02_26_170552_add_maximum_qualification_to_position_groups_table.php @@ -0,0 +1,28 @@ +tinyInteger('maximum_atc_qualification_id')->nullable()->after('description'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('position_groups', function (Blueprint $table) { + // + }); + } +}; diff --git a/database/migrations/2024_02_28_161526_add_soft_deletes_to_position_groups.php b/database/migrations/2024_02_28_161526_add_soft_deletes_to_position_groups.php new file mode 100644 index 0000000000..9fd95613df --- /dev/null +++ b/database/migrations/2024_02_28_161526_add_soft_deletes_to_position_groups.php @@ -0,0 +1,28 @@ +softDeletes(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('position_groups', function (Blueprint $table) { + $table->dropSoftDeletes(); + }); + } +}; diff --git a/database/migrations/2024_02_28_162223_add_soft_deletes_to_training_waiting_list_flags.php b/database/migrations/2024_02_28_162223_add_soft_deletes_to_training_waiting_list_flags.php new file mode 100644 index 0000000000..c12f0712a7 --- /dev/null +++ b/database/migrations/2024_02_28_162223_add_soft_deletes_to_training_waiting_list_flags.php @@ -0,0 +1,28 @@ +softDeletes(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('training_waiting_list_flags', function (Blueprint $table) { + $table->dropSoftDeletes(); + }); + } +}; diff --git a/database/migrations/2024_02_29_173742_remove_waiting_list_removed_columns.php b/database/migrations/2024_02_29_173742_remove_waiting_list_removed_columns.php new file mode 100644 index 0000000000..be064f50cc --- /dev/null +++ b/database/migrations/2024_02_29_173742_remove_waiting_list_removed_columns.php @@ -0,0 +1,33 @@ +dropColumn('flags_check'); + }); + + Schema::table('training_waiting_list_account', function (Blueprint $table) { + $table->dropColumn('eligible'); + $table->dropColumn('eligibility_summary'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // + } +}; diff --git a/database/seeders/RolesAndPermissionsSeeder.php b/database/seeders/RolesAndPermissionsSeeder.php index f22afbc7d4..e912603966 100644 --- a/database/seeders/RolesAndPermissionsSeeder.php +++ b/database/seeders/RolesAndPermissionsSeeder.php @@ -179,6 +179,21 @@ public function run() // 'discord.rostering', // 'discord.livestreaming', 'discord.atc.student.obs', + + 'position-group.view.*', + + 'endorsement.temporary.create.*', + 'endorsement.temporary.edit.*', + + 'endorsement.create.*', + 'endorsement.create.permanent', + 'endorsement.create.temporary', + + 'endorsement-request.access', + 'endorsement-request.create.*', + 'endorsement-request.view.*', + 'endorsement-request.approve.*', + 'endorsement-request.reject.*', ]; foreach ($permissions as $permission) { diff --git a/package-lock.json b/package-lock.json index 34db7a29d6..4fcc8b4c0a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,8 +7,14 @@ "name": "vatsimukcore", "license": "MIT", "dependencies": { + "@alpinejs/collapse": "^3.13.5" + }, + "devDependencies": { "@fortawesome/fontawesome-free": "^6.5.1", + "@tailwindcss/forms": "^0.5.7", + "@tailwindcss/typography": "^0.5.10", "admin-lte": "2.4.18", + "autoprefixer": "^10.4.16", "bootstrap": "^5.3.3", "bootstrap-sass": "^3.4.3", "bootstrap-tour": "^0.12.0", @@ -18,17 +24,34 @@ "less": "^4.2.0", "less-loader": "^12.2.0", "postcss": "^8.4", - "sass": "^1.71.1", - "sass-loader": "^14.1.1", - "vite": "^4.5.1", + "postcss-nesting": "^12.0.2", + "tailwindcss": "^3.4.0", "vue": "^2.7.16", "vue-template-compiler": "^2.7.16" } }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@alpinejs/collapse": { + "version": "3.13.5", + "resolved": "https://registry.npmjs.org/@alpinejs/collapse/-/collapse-3.13.5.tgz", + "integrity": "sha512-LHtSF/T3Zrhr0WOeVm4ebdXNH6ftqoZMbmkBTU1n/j8r0joV3oLUsPCyn5qOU8+27d2P/N2a057etOm0MH60oQ==" + }, "node_modules/@babel/parser": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz", - "integrity": "sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.9.tgz", + "integrity": "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==", + "dev": true, "bin": { "parser": "bin/babel-parser.js" }, @@ -36,6 +59,28 @@ "node": ">=6.0.0" } }, + "node_modules/@csstools/selector-specificity": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-3.0.2.tgz", + "integrity": "sha512-RpHaZ1h9LE7aALeQXmXrJkRG84ZxIsctEN2biEUmFyKpzFM3zZ35eUMcIzZFsw/2olQE6v69+esEqU2f1MKycg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^6.0.13" + } + }, "node_modules/@esbuild/android-arm": { "version": "0.18.20", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", @@ -43,10 +88,12 @@ "cpu": [ "arm" ], + "dev": true, "optional": true, "os": [ "android" ], + "peer": true, "engines": { "node": ">=12" } @@ -58,10 +105,12 @@ "cpu": [ "arm64" ], + "dev": true, "optional": true, "os": [ "android" ], + "peer": true, "engines": { "node": ">=12" } @@ -73,10 +122,12 @@ "cpu": [ "x64" ], + "dev": true, "optional": true, "os": [ "android" ], + "peer": true, "engines": { "node": ">=12" } @@ -88,10 +139,12 @@ "cpu": [ "arm64" ], + "dev": true, "optional": true, "os": [ "darwin" ], + "peer": true, "engines": { "node": ">=12" } @@ -103,10 +156,12 @@ "cpu": [ "x64" ], + "dev": true, "optional": true, "os": [ "darwin" ], + "peer": true, "engines": { "node": ">=12" } @@ -118,10 +173,12 @@ "cpu": [ "arm64" ], + "dev": true, "optional": true, "os": [ "freebsd" ], + "peer": true, "engines": { "node": ">=12" } @@ -133,10 +190,12 @@ "cpu": [ "x64" ], + "dev": true, "optional": true, "os": [ "freebsd" ], + "peer": true, "engines": { "node": ">=12" } @@ -148,10 +207,12 @@ "cpu": [ "arm" ], + "dev": true, "optional": true, "os": [ "linux" ], + "peer": true, "engines": { "node": ">=12" } @@ -163,10 +224,12 @@ "cpu": [ "arm64" ], + "dev": true, "optional": true, "os": [ "linux" ], + "peer": true, "engines": { "node": ">=12" } @@ -178,10 +241,12 @@ "cpu": [ "ia32" ], + "dev": true, "optional": true, "os": [ "linux" ], + "peer": true, "engines": { "node": ">=12" } @@ -193,10 +258,12 @@ "cpu": [ "loong64" ], + "dev": true, "optional": true, "os": [ "linux" ], + "peer": true, "engines": { "node": ">=12" } @@ -208,10 +275,12 @@ "cpu": [ "mips64el" ], + "dev": true, "optional": true, "os": [ "linux" ], + "peer": true, "engines": { "node": ">=12" } @@ -223,10 +292,12 @@ "cpu": [ "ppc64" ], + "dev": true, "optional": true, "os": [ "linux" ], + "peer": true, "engines": { "node": ">=12" } @@ -238,10 +309,12 @@ "cpu": [ "riscv64" ], + "dev": true, "optional": true, "os": [ "linux" ], + "peer": true, "engines": { "node": ">=12" } @@ -253,10 +326,12 @@ "cpu": [ "s390x" ], + "dev": true, "optional": true, "os": [ "linux" ], + "peer": true, "engines": { "node": ">=12" } @@ -268,10 +343,12 @@ "cpu": [ "x64" ], + "dev": true, "optional": true, "os": [ "linux" ], + "peer": true, "engines": { "node": ">=12" } @@ -283,10 +360,12 @@ "cpu": [ "x64" ], + "dev": true, "optional": true, "os": [ "netbsd" ], + "peer": true, "engines": { "node": ">=12" } @@ -298,10 +377,12 @@ "cpu": [ "x64" ], + "dev": true, "optional": true, "os": [ "openbsd" ], + "peer": true, "engines": { "node": ">=12" } @@ -313,10 +394,12 @@ "cpu": [ "x64" ], + "dev": true, "optional": true, "os": [ "sunos" ], + "peer": true, "engines": { "node": ">=12" } @@ -328,10 +411,12 @@ "cpu": [ "arm64" ], + "dev": true, "optional": true, "os": [ "win32" ], + "peer": true, "engines": { "node": ">=12" } @@ -343,10 +428,12 @@ "cpu": [ "ia32" ], + "dev": true, "optional": true, "os": [ "win32" ], + "peer": true, "engines": { "node": ">=12" } @@ -358,10 +445,12 @@ "cpu": [ "x64" ], + "dev": true, "optional": true, "os": [ "win32" ], + "peer": true, "engines": { "node": ">=12" } @@ -370,17 +459,34 @@ "version": "6.5.1", "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.5.1.tgz", "integrity": "sha512-CNy5vSwN3fsUStPRLX7fUYojyuzoEMSXPl7zSLJ8TgtRfjv24LOnOWKT2zYwaHZCJGkdyRnTmstR0P+Ah503Gw==", + "dev": true, "hasInstallScript": true, "engines": { "node": ">=6" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", - "optional": true, - "peer": true, + "dev": true, "dependencies": { "@jridgewell/set-array": "^1.0.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -394,8 +500,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", - "optional": true, - "peer": true, + "dev": true, "engines": { "node": ">=6.0.0" } @@ -404,8 +509,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", - "optional": true, - "peer": true, + "dev": true, "engines": { "node": ">=6.0.0" } @@ -414,6 +518,7 @@ "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", + "dev": true, "optional": true, "peer": true, "dependencies": { @@ -425,34 +530,119 @@ "version": "1.4.15", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "optional": true, - "peer": true + "dev": true }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.20", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", - "optional": true, - "peer": true, + "dev": true, "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@popperjs/core": { "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "dev": true, "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/popperjs" } }, + "node_modules/@tailwindcss/forms": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.7.tgz", + "integrity": "sha512-QE7X69iQI+ZXwldE+rzasvbJiyV/ju1FGHH0Qn2W3FKbuYtqp8LKcy6iSw79fVUT5/Vvf+0XgLCeYVG+UV6hOw==", + "dev": true, + "dependencies": { + "mini-svg-data-uri": "^1.2.3" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1" + } + }, + "node_modules/@tailwindcss/typography": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.10.tgz", + "integrity": "sha512-Pe8BuPJQJd3FfRnm6H0ulKIGoMEQS+Vq01R6M5aCrFB/ccR/shT+0kXLjouGC1gFLm9hopTFN+DMP0pfwRWzPw==", + "dev": true, + "dependencies": { + "lodash.castarray": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.merge": "^4.6.2", + "postcss-selector-parser": "6.0.10" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders" + } + }, + "node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/@types/eslint": { "version": "7.28.0", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.28.0.tgz", "integrity": "sha512-07XlgzX0YJUn4iG1ocY4IX9DzKSmMGUs6ESKlxWhZRaa0fatIWaHWUVapcuGa8r5HFnTqzj+4OCjd5f7EZ/i/A==", + "dev": true, "license": "MIT", "optional": true, "peer": true, @@ -465,6 +655,7 @@ "version": "3.7.4", "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz", "integrity": "sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==", + "dev": true, "license": "MIT", "optional": true, "peer": true, @@ -474,10 +665,10 @@ } }, "node_modules/@types/estree": { - "version": "0.0.50", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.50.tgz", - "integrity": "sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw==", - "license": "MIT", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true, "optional": true, "peer": true }, @@ -485,22 +676,27 @@ "version": "7.0.9", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", + "dev": true, "license": "MIT", "optional": true, "peer": true }, "node_modules/@types/node": { - "version": "16.9.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.2.tgz", - "integrity": "sha512-ZHty/hKoOLZvSz6BtP1g7tc7nUeJhoCf3flLjh8ZEv1vFKBWHXcnMbJMyN/pftSljNyy0kNW/UqI3DccnBnZ8w==", - "license": "MIT", + "version": "20.11.20", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.20.tgz", + "integrity": "sha512-7/rR21OS+fq8IyHTgtLkDK949uzsa6n8BkziAKtPVpugIkO6D+/ooXMvzXxDnZrmtXVfjb1bKQafYpb8s89LOg==", + "dev": true, "optional": true, - "peer": true + "peer": true, + "dependencies": { + "undici-types": "~5.26.4" + } }, "node_modules/@vue/compiler-sfc": { "version": "2.7.16", "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-2.7.16.tgz", "integrity": "sha512-KWhJ9k5nXuNtygPU7+t1rX6baZeqOYLEforUPjgNDBnLicfHCoi48H87Q8XyLZOrNNsmhuwKqtpDQWjEFe6Ekg==", + "dev": true, "dependencies": { "@babel/parser": "^7.23.5", "postcss": "^8.4.14", @@ -514,6 +710,7 @@ "version": "1.11.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", "integrity": "sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==", + "dev": true, "license": "MIT", "optional": true, "peer": true, @@ -526,6 +723,7 @@ "version": "1.11.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz", "integrity": "sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==", + "dev": true, "license": "MIT", "optional": true, "peer": true @@ -534,6 +732,7 @@ "version": "1.11.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz", "integrity": "sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==", + "dev": true, "license": "MIT", "optional": true, "peer": true @@ -542,6 +741,7 @@ "version": "1.11.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz", "integrity": "sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==", + "dev": true, "license": "MIT", "optional": true, "peer": true @@ -550,6 +750,7 @@ "version": "1.11.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz", "integrity": "sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==", + "dev": true, "license": "MIT", "optional": true, "peer": true, @@ -563,6 +764,7 @@ "version": "1.11.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz", "integrity": "sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==", + "dev": true, "license": "MIT", "optional": true, "peer": true @@ -571,6 +773,7 @@ "version": "1.11.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz", "integrity": "sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==", + "dev": true, "license": "MIT", "optional": true, "peer": true, @@ -585,6 +788,7 @@ "version": "1.11.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz", "integrity": "sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==", + "dev": true, "license": "MIT", "optional": true, "peer": true, @@ -596,6 +800,7 @@ "version": "1.11.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.1.tgz", "integrity": "sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==", + "dev": true, "license": "Apache-2.0", "optional": true, "peer": true, @@ -607,6 +812,7 @@ "version": "1.11.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.1.tgz", "integrity": "sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==", + "dev": true, "license": "MIT", "optional": true, "peer": true @@ -615,6 +821,7 @@ "version": "1.11.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz", "integrity": "sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==", + "dev": true, "license": "MIT", "optional": true, "peer": true, @@ -633,6 +840,7 @@ "version": "1.11.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz", "integrity": "sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==", + "dev": true, "license": "MIT", "optional": true, "peer": true, @@ -648,6 +856,7 @@ "version": "1.11.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz", "integrity": "sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==", + "dev": true, "license": "MIT", "optional": true, "peer": true, @@ -662,6 +871,7 @@ "version": "1.11.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz", "integrity": "sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==", + "dev": true, "license": "MIT", "optional": true, "peer": true, @@ -678,6 +888,7 @@ "version": "1.11.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz", "integrity": "sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==", + "dev": true, "license": "MIT", "optional": true, "peer": true, @@ -690,6 +901,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, "license": "BSD-3-Clause", "optional": true, "peer": true @@ -698,6 +910,7 @@ "version": "4.2.2", "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, "license": "Apache-2.0", "optional": true, "peer": true @@ -706,6 +919,7 @@ "version": "8.8.2", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "dev": true, "license": "MIT", "optional": true, "peer": true, @@ -720,6 +934,7 @@ "version": "1.7.6", "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.7.6.tgz", "integrity": "sha512-FlVvVFA1TX6l3lp8VjDnYYq7R1nyW6x3svAt4nDgrWQ9SBaSh9CnbwgSUTasgfNfOG5HlM1ehugCvM+hjo56LA==", + "dev": true, "license": "MIT", "optional": true, "peer": true, @@ -731,6 +946,7 @@ "version": "1.8.2", "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz", "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==", + "dev": true, "license": "Apache-2.0", "dependencies": { "acorn": "^7.0.0", @@ -742,6 +958,7 @@ "version": "7.4.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -754,6 +971,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.4.0" @@ -761,6 +979,7 @@ }, "node_modules/admin-lte": { "version": "v2.4.18", + "dev": true, "license": "MIT", "dependencies": { "bootstrap": "^3.4", @@ -796,6 +1015,7 @@ "version": "3.4.1", "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-3.4.1.tgz", "integrity": "sha512-yN5oZVmRCwe5aKwzRj6736nSmKDX7pLYwsXiCj/EYmo16hODaBiT4En5btW/jhBF/seV+XMx3aYwukYC3A49DA==", + "dev": true, "engines": { "node": ">=6" } @@ -804,6 +1024,7 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/jvectormap/-/jvectormap-1.2.2.tgz", "integrity": "sha1-LkQIskpgRz/xBsHnJD43WuXKhdo=", + "dev": true, "dependencies": { "jquery": ">=1.5" } @@ -812,6 +1033,7 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, "license": "MIT", "optional": true, "peer": true, @@ -830,6 +1052,7 @@ "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, "license": "MIT", "optional": true, "peer": true, @@ -837,10 +1060,41 @@ "ajv": "^6.9.1" } }, + "node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true + }, "node_modules/anymatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", @@ -850,10 +1104,17 @@ "node": ">= 8" } }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true + }, "node_modules/asn1.js": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "dev": true, "license": "MIT", "dependencies": { "bn.js": "^4.0.0", @@ -866,6 +1127,7 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz", "integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==", + "dev": true, "license": "MIT", "dependencies": { "object-assign": "^4.1.1", @@ -876,21 +1138,61 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", + "dev": true, "license": "ISC" }, "node_modules/assert/node_modules/util": { "version": "0.10.3", "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "dev": true, "license": "MIT", "dependencies": { "inherits": "2.0.1" } }, + "node_modules/autoprefixer": { + "version": "10.4.16", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.16.tgz", + "integrity": "sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "browserslist": "^4.21.10", + "caniuse-lite": "^1.0.30001538", + "fraction.js": "^4.3.6", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -903,12 +1205,14 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, "license": "MIT" }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, "funding": [ { "type": "github", @@ -929,6 +1233,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -938,12 +1243,14 @@ "version": "4.12.0", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", + "dev": true, "license": "MIT" }, "node_modules/bootstrap": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz", "integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==", + "dev": true, "funding": [ { "type": "github", @@ -962,6 +1269,7 @@ "version": "2.5.3", "resolved": "https://registry.npmjs.org/bootstrap-colorpicker/-/bootstrap-colorpicker-2.5.3.tgz", "integrity": "sha512-xdllX8LSMvKULs3b8JrgRXTvyvjkSMHHHVuHjjN5FNMqr6kRe5NPiMHFmeAFjlgDF73MspikudLuEwR28LbzLw==", + "dev": true, "license": "Apache-2.0", "dependencies": { "jquery": ">=1.10" @@ -971,6 +1279,7 @@ "version": "1.9.0", "resolved": "https://registry.npmjs.org/bootstrap-datepicker/-/bootstrap-datepicker-1.9.0.tgz", "integrity": "sha512-9rYYbaVOheGYxjOr/+bJCmRPihfy+LkLSg4fIFMT9Od8WwWB/MB50w0JO1eBgKUMbb7PFHQD5uAfI3ArAxZRXA==", + "dev": true, "license": "Apache-2.0", "dependencies": { "jquery": ">=1.7.1 <4.0.0" @@ -980,6 +1289,7 @@ "version": "2.1.30", "resolved": "https://registry.npmjs.org/bootstrap-daterangepicker/-/bootstrap-daterangepicker-2.1.30.tgz", "integrity": "sha1-+JPb//Wk19+qt1Rg6OppabuJaJo=", + "dev": true, "license": "MIT", "dependencies": { "jquery": ">=1.10", @@ -990,18 +1300,21 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/bootstrap-sass/-/bootstrap-sass-3.4.3.tgz", "integrity": "sha512-vPgFnGMp1jWZZupOND65WS6mkR8rxhJxndT/AcMbqcq1hHMdkcH4sMPhznLzzoHOHkSCrd6J9F8pWBriPCKP2Q==", + "dev": true, "license": "MIT" }, "node_modules/bootstrap-slider": { "version": "9.10.0", "resolved": "https://registry.npmjs.org/bootstrap-slider/-/bootstrap-slider-9.10.0.tgz", "integrity": "sha1-EQPWvADPv6jPyaJZmrUYxVZD2j8=", + "dev": true, "license": "MIT" }, "node_modules/bootstrap-tour": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/bootstrap-tour/-/bootstrap-tour-0.12.0.tgz", "integrity": "sha512-J+Nure5q3eHKVAy0Oi0Y7bGhdVsnCl2bfaAYFg7hNcdWK+lyXtS3fLHIUqzbvdW//wcy2FRMHSr7G6EuisBWoA==", + "dev": true, "dependencies": { "bootstrap": "~3", "jquery": ">=1.8" @@ -1014,6 +1327,7 @@ "version": "3.4.1", "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-3.4.1.tgz", "integrity": "sha512-yN5oZVmRCwe5aKwzRj6736nSmKDX7pLYwsXiCj/EYmo16hODaBiT4En5btW/jhBF/seV+XMx3aYwukYC3A49DA==", + "dev": true, "engines": { "node": ">=6" } @@ -1022,6 +1336,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -1032,6 +1347,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.0.1" @@ -1044,12 +1360,14 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", + "dev": true, "license": "MIT" }, "node_modules/browser-pack": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/browser-pack/-/browser-pack-6.1.0.tgz", "integrity": "sha512-erYug8XoqzU3IfcU8fUgyHqyOXqIE4tUTTQ+7mqUjQlvnXkOO6OlT9c/ZoJVHYoAaqGxr09CN53G7XIsO4KtWA==", + "dev": true, "license": "MIT", "dependencies": { "combine-source-map": "~0.8.0", @@ -1067,6 +1385,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-2.0.0.tgz", "integrity": "sha512-7sWsQlYL2rGLy2IWm8WL8DCTJvYLc/qlOnsakDac87SOoCd16WLsaAMdCiAqsTNHIe+SXfaqyxyo6THoWqs8WQ==", + "dev": true, "license": "MIT", "dependencies": { "resolve": "^1.17.0" @@ -1076,6 +1395,7 @@ "version": "17.0.0", "resolved": "https://registry.npmjs.org/browserify/-/browserify-17.0.0.tgz", "integrity": "sha512-SaHqzhku9v/j6XsQMRxPyBrSP3gnwmE27gLJYZgMT2GeK3J0+0toN+MnuNYDfHwVGQfLiMZ7KSNSIXHemy905w==", + "dev": true, "license": "MIT", "dependencies": { "assert": "^1.4.0", @@ -1138,6 +1458,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "dev": true, "license": "MIT", "dependencies": { "buffer-xor": "^1.0.3", @@ -1152,6 +1473,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", + "dev": true, "license": "MIT", "dependencies": { "browserify-aes": "^1.0.4", @@ -1163,6 +1485,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", + "dev": true, "license": "MIT", "dependencies": { "cipher-base": "^1.0.1", @@ -1175,6 +1498,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.0.tgz", "integrity": "sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==", + "dev": true, "license": "MIT", "dependencies": { "bn.js": "^5.0.0", @@ -1185,12 +1509,14 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz", "integrity": "sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw==", + "dev": true, "license": "MIT" }, "node_modules/browserify-sign": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.2.tgz", "integrity": "sha512-1rudGyeYY42Dk6texmv7c4VcQ0EsvVbLwZkA+AQB7SxvXxmcD93jcHie8bzecJ+ChDlmAm2Qyu0+Ccg5uhZXCg==", + "dev": true, "dependencies": { "bn.js": "^5.2.1", "browserify-rsa": "^4.1.0", @@ -1209,12 +1535,14 @@ "node_modules/browserify-sign/node_modules/bn.js": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", - "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" + "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", + "dev": true }, "node_modules/browserify-sign/node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -1228,6 +1556,7 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "dev": true, "license": "MIT", "dependencies": { "pako": "~1.0.5" @@ -1237,6 +1566,7 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.2.1.tgz", "integrity": "sha512-c+Ko0loDaFfuPWiL02ls9Xd3GO3cPVmUobQ6t3rXNUk304u6hGq+8N/kFi+QEIKhzK3uwolVhLzszmfLmMLnqg==", + "dev": true, "license": "MIT", "dependencies": { "base64-js": "^1.0.2", @@ -1247,12 +1577,14 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, "license": "MIT" }, "node_modules/browserify/node_modules/stream-browserify": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", "integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==", + "dev": true, "license": "MIT", "dependencies": { "inherits": "~2.0.4", @@ -1263,6 +1595,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, "license": "MIT", "dependencies": { "inherits": "^2.0.3", @@ -1277,6 +1610,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-3.2.0.tgz", "integrity": "sha512-Oq1bLqisTyK3TSCXpPbT4sdeYNdmyZJv1LxpEm2vu1ZhK89kSE5YXwZc3cWk0MagGaKriBh9mCFbVGtO+vY29A==", + "dev": true, "license": "MIT", "dependencies": { "builtin-status-codes": "^3.0.0", @@ -1289,6 +1623,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, "license": "MIT", "dependencies": { "inherits": "^2.0.3", @@ -1303,6 +1638,7 @@ "version": "1.4.2", "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-1.4.2.tgz", "integrity": "sha1-ycWLV1voQHN1y14kYtrO50NZ9B0=", + "dev": true, "dependencies": { "process": "~0.11.0" }, @@ -1314,12 +1650,14 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.1.tgz", "integrity": "sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==", + "dev": true, "license": "MIT" }, "node_modules/browserify/node_modules/util": { "version": "0.12.4", "resolved": "https://registry.npmjs.org/util/-/util-0.12.4.tgz", "integrity": "sha512-bxZ9qtSlGUWSOy9Qa9Xgk11kSslpuZwaxCg4sNIDj6FLucDab2JxnHwyNTCpHMtK1MjoQiWQ6DiUMZYbSrO+Sw==", + "dev": true, "license": "MIT", "dependencies": { "inherits": "^2.0.3", @@ -1331,58 +1669,70 @@ } }, "node_modules/browserslist": { - "version": "4.17.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.17.0.tgz", - "integrity": "sha512-g2BJ2a0nEYvEFQC208q8mVAhfNwpZ5Mu8BwgtCdZKO3qx98HChmeg448fPdUzld8aFmfLgVh7yymqV+q1lJZ5g==", - "license": "MIT", - "optional": true, - "peer": true, + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz", + "integrity": "sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "dependencies": { - "caniuse-lite": "^1.0.30001254", - "colorette": "^1.3.0", - "electron-to-chromium": "^1.3.830", - "escalade": "^3.1.1", - "node-releases": "^1.1.75" + "caniuse-lite": "^1.0.30001565", + "electron-to-chromium": "^1.4.601", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" }, "bin": { "browserslist": "cli.js" }, "engines": { "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" } }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, "license": "MIT" }, "node_modules/buffer-xor": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", + "dev": true, "license": "MIT" }, "node_modules/builtin-status-codes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", + "dev": true, "license": "MIT" }, "node_modules/cached-path-relative": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/cached-path-relative/-/cached-path-relative-1.1.0.tgz", "integrity": "sha512-WF0LihfemtesFcJgO7xfOoOcnWzY/QHR4qeDqV44jPU3HTI54+LnfXK3SA27AVVGCdZFgjjFFaqUA9Jx7dMJZA==", + "dev": true, "license": "MIT" }, "node_modules/call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.1", @@ -1396,6 +1746,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", + "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.1", @@ -1410,6 +1761,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -1418,35 +1770,60 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/caniuse-lite": { - "version": "1.0.30001258", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001258.tgz", - "integrity": "sha512-RBByOG6xWXUp0CR2/WU2amXz3stjKpSl5J1xU49F1n2OxD//uBZO4wCKUiG+QMGf7CHGfDDcqoKriomoGVxTeA==", - "license": "CC-BY-4.0", - "optional": true, - "peer": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "engines": { + "node": ">= 6" } }, + "node_modules/caniuse-lite": { + "version": "1.0.30001574", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001574.tgz", + "integrity": "sha512-BtYEK4r/iHt/txm81KBudCUcTy7t+s9emrIaHqjYurQ10x71zJ5VQ9x1dYPcz/b+pKSp4y/v1xSI67A+LzpNyg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, "node_modules/charm": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/charm/-/charm-0.1.2.tgz", "integrity": "sha1-BsIe7RobBq62dVPNxT4jJ0usIpY=", + "dev": true, "license": "MIT/X11" }, "node_modules/chart.js": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-1.1.1.tgz", "integrity": "sha1-qbFwVCIL1Fy9sXb9a8uHg++HGn0=", + "dev": true, "license": "MIT" }, "node_modules/chokidar": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", - "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==", - "license": "MIT", + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -1467,6 +1844,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "dev": true, "license": "MIT", "optional": true, "peer": true, @@ -1478,6 +1856,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "dev": true, "license": "MIT", "dependencies": { "inherits": "^2.0.1", @@ -1488,29 +1867,42 @@ "version": "4.12.1", "resolved": "https://registry.npmjs.org/ckeditor/-/ckeditor-4.12.1.tgz", "integrity": "sha512-pH2Su4oi0D4iN/3U8nUcwI7/lXHoOJi0aiN8e2zxnm4Ow5kq8eZP2ZGmpYyuqRyKZ2tHaU8+OyYi7laXcjiq9Q==", + "dev": true, "license": "(GPL-2.0 OR LGPL-2.1 OR MPL-1.1)" }, "node_modules/classie": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/classie/-/classie-1.0.0.tgz", "integrity": "sha1-/JsptH5k43SiBi+2JNBaYc1wOrI=", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.x" } }, - "node_modules/colorette": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", - "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", - "license": "MIT", - "optional": true, - "peer": true + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "node_modules/combine-source-map": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/combine-source-map/-/combine-source-map-0.8.0.tgz", "integrity": "sha1-pY0N8ELBhvz4IqjoAV9UUNLXmos=", + "dev": true, "license": "MIT", "dependencies": { "convert-source-map": "~1.1.0", @@ -1523,18 +1915,21 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.1.3.tgz", "integrity": "sha1-SCnId+n+SbMWHzvzZziI4gRpmGA=", + "dev": true, "license": "MIT" }, "node_modules/combine-source-map/node_modules/lodash.memoize": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-3.0.4.tgz", "integrity": "sha1-LcvSwofLwKVcxCMovQxzYVDVPj8=", + "dev": true, "license": "MIT" }, "node_modules/combine-source-map/node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -1544,6 +1939,7 @@ "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, "license": "MIT", "optional": true, "peer": true @@ -1552,12 +1948,14 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, "license": "MIT" }, "node_modules/concat-stream": { "version": "1.6.2", "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, "engines": [ "node >= 0.8" ], @@ -1572,18 +1970,21 @@ "node_modules/console-browserify": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", - "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==" + "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==", + "dev": true }, "node_modules/constants-browserify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=", + "dev": true, "license": "MIT" }, "node_modules/copy-anything": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.3.tgz", "integrity": "sha512-GK6QUtisv4fNS+XcI7shX0Gx9ORg7QqIznyfho79JTnX1XhLiyZHfftvGiziqzRiEi/Bjhgpi+D2o7HxJFPnDQ==", + "dev": true, "license": "MIT", "dependencies": { "is-what": "^3.12.0" @@ -1593,12 +1994,14 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, "license": "MIT" }, "node_modules/create-ecdh": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", + "dev": true, "license": "MIT", "dependencies": { "bn.js": "^4.1.0", @@ -1609,6 +2012,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "dev": true, "license": "MIT", "dependencies": { "cipher-base": "^1.0.1", @@ -1622,6 +2026,7 @@ "version": "1.1.7", "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "dev": true, "license": "MIT", "dependencies": { "cipher-base": "^1.0.3", @@ -1632,10 +2037,25 @@ "sha.js": "^2.4.8" } }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/crypto-browserify": { "version": "3.12.0", "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", + "dev": true, "license": "MIT", "dependencies": { "browserify-cipher": "^1.0.0", @@ -1654,22 +2074,36 @@ "node": "*" } }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/csstype": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.0.tgz", - "integrity": "sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==", - "license": "MIT" + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true }, "node_modules/dash-ast": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/dash-ast/-/dash-ast-1.0.0.tgz", "integrity": "sha512-Vy4dx7gquTeMcQR/hDkYLGUnwVil6vk4FOOct+djUnHOUWt+zJPJAaRIXaAFkPXtJjvlY7o3rfRu0/3hpnwoUA==", + "dev": true, "license": "Apache-2.0" }, "node_modules/datatables.net": { "version": "1.11.3", "resolved": "https://registry.npmjs.org/datatables.net/-/datatables.net-1.11.3.tgz", "integrity": "sha512-VMj5qEaTebpNurySkM6jy6sGpl+s6onPK8xJhYr296R/vUBnz1+id16NVqNf9z5aR076OGcpGHCuiTuy4E05oQ==", + "dev": true, "license": "MIT", "dependencies": { "jquery": ">=1.7" @@ -1679,6 +2113,7 @@ "version": "1.11.2", "resolved": "https://registry.npmjs.org/datatables.net-bs/-/datatables.net-bs-1.11.2.tgz", "integrity": "sha512-VJfRfJCmZ81XshwxQngdEX6M5YRijkWHKhM9VnefWwZqwbIGiK+ngnmGKoNvnXmvWk4BZ1K0yy3vtkEmhDvvRw==", + "dev": true, "license": "MIT", "dependencies": { "datatables.net": ">=1.10.25", @@ -1688,13 +2123,14 @@ "node_modules/de-indent": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", - "integrity": "sha1-sgOOhG3DO6pXlhKNCAS0VbjB4h0=", - "license": "MIT" + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true }, "node_modules/define-properties": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, "license": "MIT", "dependencies": { "object-keys": "^1.0.12" @@ -1707,12 +2143,14 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=", + "dev": true, "license": "MIT" }, "node_modules/deps-sort": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/deps-sort/-/deps-sort-2.0.1.tgz", "integrity": "sha512-1orqXQr5po+3KI6kQb9A4jnXT1PBwggGl2d7Sq2xsnOeI9GPcE/tGcF9UiSZtZBM7MukY4cAh7MemS6tZYipfw==", + "dev": true, "license": "MIT", "dependencies": { "JSONStream": "^1.0.3", @@ -1728,6 +2166,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", "integrity": "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==", + "dev": true, "license": "MIT", "dependencies": { "inherits": "^2.0.1", @@ -1738,6 +2177,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.0.tgz", "integrity": "sha512-6SsIx+nUUbuK0EthKjv0zrdnajCCXVYGmbYYiYjFVpzcjwEs/JMDZ8tPRG29J/HhN56t3GJp2cGSWDRjjot8Pg==", + "dev": true, "license": "MIT", "dependencies": { "acorn-node": "^1.6.1", @@ -1751,10 +2191,17 @@ "node": ">=0.8.0" } }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true + }, "node_modules/diffie-hellman": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", + "dev": true, "license": "MIT", "dependencies": { "bn.js": "^4.1.0", @@ -1762,10 +2209,17 @@ "randombytes": "^2.0.0" } }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, "node_modules/domain-browser": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.4", @@ -1776,6 +2230,7 @@ "version": "0.9.1", "resolved": "https://registry.npmjs.org/domhelper/-/domhelper-0.9.1.tgz", "integrity": "sha1-JlVOW6wsnpWF3KUAl431Bn1kvQA=", + "dev": true, "dependencies": { "browserify": ">=3.46.0", "classie": ">=0.0.1", @@ -1789,23 +2244,29 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", + "dev": true, "license": "BSD-3-Clause", "dependencies": { "readable-stream": "^2.0.2" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, "node_modules/electron-to-chromium": { - "version": "1.3.842", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.842.tgz", - "integrity": "sha512-P/nDMPIYdb2PyqCQwhTXNi5JFjX1AsDVR0y6FrHw752izJIAJ+Pn5lugqyBq4tXeRSZBMBb2ZGvRGB1djtELEQ==", - "license": "ISC", - "optional": true, - "peer": true + "version": "1.4.620", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.620.tgz", + "integrity": "sha512-a2fcSHOHrqBJsPNXtf6ZCEZpXrFCcbK1FBxfX3txoqWzNgtEDG1f3M59M98iwxhRW4iMKESnSjbJ310/rkrp0g==", + "dev": true }, "node_modules/elliptic": { "version": "6.5.4", "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", + "dev": true, "license": "MIT", "dependencies": { "bn.js": "^4.11.9", @@ -1817,10 +2278,17 @@ "minimalistic-crypto-utils": "^1.0.1" } }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, "node_modules/enhanced-resolve": { "version": "5.12.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz", "integrity": "sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ==", + "dev": true, "license": "MIT", "optional": true, "peer": true, @@ -1836,6 +2304,7 @@ "version": "0.1.8", "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1849,6 +2318,7 @@ "version": "1.18.6", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.6.tgz", "integrity": "sha512-kAeIT4cku5eNLNuUKhlmtuk1/TRZvQoYccn6TO0cSVdf1kzB0T7+dYuVK9MWM7l+/53W2Q8M7N2c6MQvhXFcUQ==", + "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.2", @@ -1881,6 +2351,7 @@ "version": "0.9.3", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz", "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==", + "dev": true, "license": "MIT", "optional": true, "peer": true @@ -1889,6 +2360,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, "license": "MIT", "dependencies": { "is-callable": "^1.1.4", @@ -1906,7 +2378,9 @@ "version": "0.18.20", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, "hasInstallScript": true, + "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -1942,9 +2416,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "license": "MIT", - "optional": true, - "peer": true, + "dev": true, "engines": { "node": ">=6" } @@ -1953,6 +2425,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, "license": "BSD-2-Clause", "optional": true, "peer": true, @@ -1968,6 +2441,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, "license": "BSD-2-Clause", "optional": true, "peer": true, @@ -1982,6 +2456,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true, "license": "BSD-2-Clause", "optional": true, "peer": true, @@ -1993,6 +2468,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, "license": "BSD-2-Clause", "optional": true, "peer": true, @@ -2003,12 +2479,14 @@ "node_modules/eve-raphael": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/eve-raphael/-/eve-raphael-0.5.0.tgz", - "integrity": "sha1-F8dUt5K+7z+maE15z1pHxjxM2jA=" + "integrity": "sha1-F8dUt5K+7z+maE15z1pHxjxM2jA=", + "dev": true }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.8.x" @@ -2018,6 +2496,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "dev": true, "license": "MIT", "dependencies": { "md5.js": "^1.3.4", @@ -2028,14 +2507,32 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, "license": "MIT", "optional": true, "peer": true }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, "license": "MIT", "optional": true, "peer": true @@ -2044,17 +2541,29 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true, "license": "MIT" }, "node_modules/fastclick": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/fastclick/-/fastclick-1.0.6.tgz", - "integrity": "sha1-FhYlsnsaWAZAWTa9qaLBkm0Gvmo=" + "integrity": "sha1-FhYlsnsaWAZAWTa9qaLBkm0Gvmo=", + "dev": true + }, + "node_modules/fastq": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.16.0.tgz", + "integrity": "sha512-ifCoaXsDrsdkWTtiNJX5uzHDsrck5TzfKKDcuFFTIrrc/BS076qgEIfoIy1VeZqViznfKiysPYTh/QeHtnIsYA==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -2066,12 +2575,14 @@ "node_modules/flot": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/flot/-/flot-0.8.3.tgz", - "integrity": "sha512-xg2otcTJDvS+ERK+my4wxG/ASq90QURXtoM4LhacCq0jQW2jbyjdttbRNqU2cPykrpMvJ6b2uSp6SAgYAzj9tQ==" + "integrity": "sha512-xg2otcTJDvS+ERK+my4wxG/ASq90QURXtoM4LhacCq0jQW2jbyjdttbRNqU2cPykrpMvJ6b2uSp6SAgYAzj9tQ==", + "dev": true }, "node_modules/font-awesome": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/font-awesome/-/font-awesome-4.7.0.tgz", "integrity": "sha1-j6jPBBGhoxr9B7BtKQK7n8gVoTM=", + "dev": true, "license": "(OFL-1.1 AND MIT)", "engines": { "node": ">=0.10.3" @@ -2081,23 +2592,55 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=", + "dev": true, "license": "MIT" }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "node_modules/foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], "engines": { "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } @@ -2106,6 +2649,7 @@ "version": "3.10.2", "resolved": "https://registry.npmjs.org/fullcalendar/-/fullcalendar-3.10.2.tgz", "integrity": "sha512-YWZaHdp8ZLBqhPz615PoXdA49ymsBTUF+MGDM6H3vyz71Pv/ZW9Pm9/Mj3x6n822k6bs2txFO7muRTSvBhsqKg==", + "dev": true, "license": "MIT", "peerDependencies": { "jquery": "2 - 3", @@ -2113,21 +2657,26 @@ } }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "license": "MIT" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/get-assigned-identifiers": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/get-assigned-identifiers/-/get-assigned-identifiers-1.2.0.tgz", "integrity": "sha512-mBBwmeGTrxEMO4pMaaf/uUEFHnYtwr8FTe8Y/mer4rcV/bye0qGm6pw1bGZFGStxC5O76c5ZAVBGnqHmOaJpdQ==", + "dev": true, "license": "Apache-2.0" }, "node_modules/get-intrinsic": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.1", @@ -2142,6 +2691,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.2", @@ -2158,6 +2708,7 @@ "version": "7.1.7", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "dev": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -2178,6 +2729,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -2190,6 +2742,7 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, "license": "BSD-2-Clause", "optional": true, "peer": true @@ -2198,6 +2751,7 @@ "version": "4.2.8", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==", + "dev": true, "license": "ISC", "optional": true }, @@ -2205,6 +2759,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.1" @@ -2217,6 +2772,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -2226,6 +2782,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, "license": "MIT", "optional": true, "peer": true, @@ -2237,6 +2794,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -2249,6 +2807,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.2" @@ -2264,6 +2823,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", + "dev": true, "license": "MIT", "dependencies": { "inherits": "^2.0.4", @@ -2278,6 +2838,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, "license": "MIT", "dependencies": { "inherits": "^2.0.3", @@ -2292,17 +2853,30 @@ "version": "1.1.7", "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "dev": true, "license": "MIT", "dependencies": { "inherits": "^2.0.3", "minimalistic-assert": "^1.0.1" } }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "license": "MIT", + "dev": true, "bin": { "he": "bin/he" } @@ -2311,6 +2885,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "dev": true, "license": "MIT", "dependencies": { "hash.js": "^1.0.3", @@ -2322,6 +2897,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/htmlescape/-/htmlescape-1.1.1.tgz", "integrity": "sha1-OgPtwiFLyjtmQko+eVk0lQnLA1E=", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10" @@ -2331,12 +2907,14 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", + "dev": true, "license": "MIT" }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, "funding": [ { "type": "github", @@ -2357,6 +2935,7 @@ "version": "0.5.5", "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", "integrity": "sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w=", + "dev": true, "license": "MIT", "optional": true, "bin": { @@ -2370,12 +2949,16 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.0.0.tgz", "integrity": "sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw==", - "license": "MIT" + "dev": true, + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, "license": "ISC", "dependencies": { "once": "^1.3.0", @@ -2386,12 +2969,14 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, "license": "ISC" }, "node_modules/inline-source-map": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/inline-source-map/-/inline-source-map-0.6.2.tgz", "integrity": "sha1-+Tk0ccGKedFyT4Y/o4tYY3Ct4qU=", + "dev": true, "license": "MIT", "dependencies": { "source-map": "~0.5.3" @@ -2401,6 +2986,7 @@ "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -2410,12 +2996,14 @@ "version": "3.3.11", "resolved": "https://registry.npmjs.org/inputmask/-/inputmask-3.3.11.tgz", "integrity": "sha1-FCHJSuKMPc0bTSYze1CLs0mY4tg=", + "dev": true, "license": "MIT" }, "node_modules/insert-module-globals": { "version": "7.2.1", "resolved": "https://registry.npmjs.org/insert-module-globals/-/insert-module-globals-7.2.1.tgz", "integrity": "sha512-ufS5Qq9RZN+Bu899eA9QCAYThY+gGW7oRkmb0vC93Vlyu/CFGcH0OYPEjVkDXA5FEbTt1+VWzdoOD3Ny9N+8tg==", + "dev": true, "license": "MIT", "dependencies": { "acorn-node": "^1.5.2", @@ -2437,6 +3025,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", + "dev": true, "license": "MIT", "dependencies": { "get-intrinsic": "^1.1.0", @@ -2451,6 +3040,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/ion-rangeslider/-/ion-rangeslider-2.3.1.tgz", "integrity": "sha512-6V+24FD13/feliI485gnRHZYD9Ev64M5NAFTxnVib516ATHa9PlXQrC+nOiPngouRYTCLPJyokAJEi3e1Umi5g==", + "dev": true, "license": "MIT", "peerDependencies": { "jquery": ">=1.8" @@ -2460,6 +3050,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/ionicons/-/ionicons-3.0.0.tgz", "integrity": "sha1-QLja9P16MRUL0AIWD2ZJbiKpjDw=", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.3" @@ -2469,6 +3060,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.2", @@ -2485,6 +3077,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, "license": "MIT", "dependencies": { "has-bigints": "^1.0.1" @@ -2497,6 +3090,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" @@ -2509,6 +3103,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.2", @@ -2525,12 +3120,14 @@ "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true, "license": "MIT" }, "node_modules/is-callable": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -2540,12 +3137,12 @@ } }, "node_modules/is-core-module": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.6.0.tgz", - "integrity": "sha512-wShG8vs60jKfPWpF2KZRaAtvt3a20OAn7+IJ6hLPECpSABLcKtFKTTI4ZtH5QcBruBHlq+WsdHWyz0BCZW7svQ==", - "license": "MIT", + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dev": true, "dependencies": { - "has": "^1.0.3" + "hasown": "^2.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -2555,6 +3152,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, "license": "MIT", "dependencies": { "has-tostringtag": "^1.0.0" @@ -2570,15 +3168,26 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/is-generator-function": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "dev": true, "license": "MIT", "dependencies": { "has-tostringtag": "^1.0.0" @@ -2591,10 +3200,10 @@ } }, "node_modules/is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "license": "MIT", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -2606,6 +3215,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz", "integrity": "sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -2618,6 +3228,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -2627,6 +3238,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.6.tgz", "integrity": "sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g==", + "dev": true, "license": "MIT", "dependencies": { "has-tostringtag": "^1.0.0" @@ -2642,6 +3254,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.2", @@ -2658,6 +3271,7 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, "license": "MIT", "dependencies": { "has-tostringtag": "^1.0.0" @@ -2673,6 +3287,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.2" @@ -2688,6 +3303,7 @@ "version": "1.1.8", "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.8.tgz", "integrity": "sha512-HqH41TNZq2fgtGT8WHVFVJhBVGuY3AnP3Q36K8JKXUxSxRgk/d+7NjmwG2vo2mYmXK8UYZKu0qH8bVP5gEisjA==", + "dev": true, "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.5", @@ -2707,18 +3323,45 @@ "version": "3.14.1", "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz", "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==", + "dev": true, "license": "MIT" }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true, "license": "MIT" }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jest-worker": { "version": "27.2.0", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.2.0.tgz", "integrity": "sha512-laB0ZVIBz+voh/QQy9dmUuuDsadixeerrKqyVpgPz+CCWiOYjOBabUXHIXZhsdvkWbLqSHbgkAHWl5cg24Q6RA==", + "dev": true, "license": "MIT", "optional": true, "peer": true, @@ -2735,6 +3378,7 @@ "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, "license": "MIT", "optional": true, "peer": true, @@ -2748,16 +3392,27 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/jiti": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", + "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", + "dev": true, + "bin": { + "jiti": "bin/jiti.js" + } + }, "node_modules/jquery": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==", + "dev": true, "license": "MIT" }, "node_modules/jquery-datetimepicker": { "version": "2.5.21", "resolved": "https://registry.npmjs.org/jquery-datetimepicker/-/jquery-datetimepicker-2.5.21.tgz", "integrity": "sha512-wDTpZ4f1PWd1XGaIIE0n6jLynlm+akBJ7/NjaB1bk2UJSS593CHJPZ3+FNEXoyvNVUeBlBC0oX6WTfCyfUhX/w==", + "dev": true, "license": "MIT", "dependencies": { "jquery": ">= 1.7.2", @@ -2769,23 +3424,27 @@ "version": "1.2.11", "resolved": "https://registry.npmjs.org/jquery-knob/-/jquery-knob-1.2.11.tgz", "integrity": "sha1-83w528HHpqbBLNsu1Pa/+2g/ENY=", + "dev": true, "license": "MIT" }, "node_modules/jquery-mousewheel": { "version": "3.1.13", "resolved": "https://registry.npmjs.org/jquery-mousewheel/-/jquery-mousewheel-3.1.13.tgz", - "integrity": "sha1-BvAzXxbjU6aV5yBr9QUDy1I6buU=" + "integrity": "sha1-BvAzXxbjU6aV5yBr9QUDy1I6buU=", + "dev": true }, "node_modules/jquery-sparkline": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/jquery-sparkline/-/jquery-sparkline-2.4.0.tgz", "integrity": "sha1-G+i3twTdOFcVJwiu+x1KSzpp+zM=", + "dev": true, "license": "BSD-2-Clause" }, "node_modules/jquery-ui": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/jquery-ui/-/jquery-ui-1.13.2.tgz", "integrity": "sha512-wBZPnqWs5GaYJmo1Jj0k/mrSkzdQzKDwhXNtHKcBdAcKVxMM3KNYFq+iJ2i1rwiG53Z8M4mTn3Qxrm17uH1D4Q==", + "dev": true, "license": "MIT", "dependencies": { "jquery": ">=1.8.0 <4.0.0" @@ -2795,6 +3454,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, "license": "MIT", "optional": true, "peer": true @@ -2803,6 +3463,7 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, "license": "MIT", "optional": true, "peer": true @@ -2811,6 +3472,7 @@ "version": "1.3.1", "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=", + "dev": true, "engines": [ "node >= 0.2.0" ], @@ -2820,6 +3482,7 @@ "version": "1.3.5", "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "dev": true, "license": "(MIT OR Apache-2.0)", "dependencies": { "jsonparse": "^1.2.0", @@ -2837,6 +3500,7 @@ "resolved": "https://registry.npmjs.org/jvectormap/-/jvectormap-2.0.4.tgz", "integrity": "sha512-rIgUaNbT6eUuKxba+q265mM3xnjC0hzKlQsM842yKW+o+BMiV1MWUY1YMq+Q+ukyNuGJNRC58EMRnv+/hJSNcQ==", "deprecated": "jvectormap is not maintened since Aug 2015. You can use jvectormap-next or jqvmap instead.", + "dev": true, "dependencies": { "jquery": ">=1.5" } @@ -2845,6 +3509,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/labeled-stream-splicer/-/labeled-stream-splicer-2.0.2.tgz", "integrity": "sha512-Ca4LSXFFZUjPScRaqOcFxneA0VpKZr4MMYCljyQr4LIewTLb3Y0IUTIsnBBsVubIeEfxeSZpSjSsRM8APEQaAw==", + "dev": true, "license": "MIT", "dependencies": { "inherits": "^2.0.1", @@ -2855,6 +3520,7 @@ "version": "0.8.1", "resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-0.8.1.tgz", "integrity": "sha512-fxzUDjOA37kOsYq8dP+3oPIlw8/kJVXwu0hOXLun82R1LpV02shGeWGYKx2lbpKffL5I0sfPPjfqbYxuqBluAA==", + "dev": true, "dependencies": { "picocolors": "^1.0.0", "vite-plugin-full-reload": "^1.0.5" @@ -2870,6 +3536,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/less/-/less-4.2.0.tgz", "integrity": "sha512-P3b3HJDBtSzsXUl0im2L7gTO5Ubg8mEN6G8qoTS77iXxXX4Hvu4Qj540PZDvQ8V6DmX6iXo98k7Md0Cm1PrLaA==", + "dev": true, "license": "Apache-2.0", "dependencies": { "copy-anything": "^2.0.1", @@ -2896,6 +3563,7 @@ "version": "12.2.0", "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-12.2.0.tgz", "integrity": "sha512-MYUxjSQSBUQmowc0l5nPieOYwMzGPUaTzB6inNW/bdPEG9zOL3eAAD1Qw5ZxSPk7we5dMojHwNODYMV1hq4EVg==", + "dev": true, "engines": { "node": ">= 18.12.0" }, @@ -2921,6 +3589,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -2935,16 +3604,33 @@ "version": "5.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, "license": "ISC", "optional": true, "bin": { "semver": "bin/semver" } }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, "node_modules/loader-runner": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.2.0.tgz", "integrity": "sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==", + "dev": true, "license": "MIT", "optional": true, "peer": true, @@ -2952,10 +3638,38 @@ "node": ">=6.11.5" } }, + "node_modules/lodash.castarray": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz", + "integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==", + "dev": true + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/lru-cache": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz", + "integrity": "sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, "node_modules/md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "dev": true, "license": "MIT", "dependencies": { "hash-base": "^3.0.0", @@ -2967,14 +3681,38 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, "license": "MIT", "optional": true, "peer": true }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/miller-rabin": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "dev": true, "license": "MIT", "dependencies": { "bn.js": "^4.0.0", @@ -2988,6 +3726,7 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, "license": "MIT", "optional": true, "bin": { @@ -3001,6 +3740,7 @@ "version": "1.49.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.49.0.tgz", "integrity": "sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA==", + "dev": true, "license": "MIT", "optional": true, "peer": true, @@ -3012,6 +3752,7 @@ "version": "2.1.32", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.32.tgz", "integrity": "sha512-hJGaVS4G4c9TSMYh2n6SQAGrC4RnfU+daP8G7cSCmaqNjiOoUY0VHCMS42pxnQmVF1GWwFhbHWn3RIxCqTmZ9A==", + "dev": true, "license": "MIT", "optional": true, "peer": true, @@ -3022,22 +3763,34 @@ "node": ">= 0.6" } }, + "node_modules/mini-svg-data-uri": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz", + "integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==", + "dev": true, + "bin": { + "mini-svg-data-uri": "cli.js" + } + }, "node_modules/minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true, "license": "ISC" }, "node_modules/minimalistic-crypto-utils": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", + "dev": true, "license": "MIT" }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -3050,21 +3803,33 @@ "version": "1.2.7", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, "license": "MIT" }, "node_modules/module-deps": { "version": "6.2.3", "resolved": "https://registry.npmjs.org/module-deps/-/module-deps-6.2.3.tgz", "integrity": "sha512-fg7OZaQBcL4/L+AK5f4iVqf9OMbCclXfy/znXRxTVhJSeW5AIlS9AwheYwDaXM3lVW7OBeaeUEY3gbaC6cLlSA==", + "dev": true, "license": "MIT", "dependencies": { "browser-resolve": "^2.0.0", @@ -3094,6 +3859,7 @@ "version": "2.29.4", "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", + "dev": true, "license": "MIT", "engines": { "node": "*" @@ -3103,6 +3869,7 @@ "version": "0.5.0", "resolved": "https://registry.npmjs.org/morris.js/-/morris.js-0.5.0.tgz", "integrity": "sha1-cldnE1z64Fmq51mZuyzmocXRtEs=", + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.8 <0.11" @@ -3112,13 +3879,26 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, "license": "MIT", "optional": true }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, "node_modules/nanoid": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, "funding": [ { "type": "github", @@ -3136,6 +3916,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/needle/-/needle-3.1.0.tgz", "integrity": "sha512-gCE9weDhjVGCRqS8dwDR/D3GTAeyXLXuqp7I8EzH6DllZGXSUyxuqqLh+YX9rMAWaaTFyVAg6rHGL25dqvczKw==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -3154,6 +3935,7 @@ "version": "3.2.7", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -3164,6 +3946,7 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -3177,38 +3960,60 @@ "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "license": "MIT" - }, - "node_modules/node-releases": { - "version": "1.1.75", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.75.tgz", - "integrity": "sha512-Qe5OUajvqrqDSy6wrWFmMwfJ0jVgwiw4T3KqmbTcZ62qW0gQkheXYhcFM1+lOVcGUoRxcEcfyvFMAnDgaF1VWw==", + "dev": true, "license": "MIT", "optional": true, "peer": true }, + "node_modules/node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "dev": true + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" } }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" } }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/object-inspect": { "version": "1.12.2", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3218,6 +4023,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -3227,6 +4033,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.0", @@ -3245,6 +4052,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -3254,12 +4062,14 @@ "version": "0.3.0", "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", + "dev": true, "license": "MIT" }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, "license": "MIT", "optional": true, "peer": true, @@ -3277,6 +4087,7 @@ "version": "0.0.4", "resolved": "https://registry.npmjs.org/pace/-/pace-0.0.4.tgz", "integrity": "sha1-1mQF1fW8EtJUQabibIeNvGnnenc=", + "dev": true, "dependencies": { "charm": "~0.1.0" }, @@ -3288,12 +4099,14 @@ "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true, "license": "(MIT AND Zlib)" }, "node_modules/parents": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parents/-/parents-1.0.1.tgz", "integrity": "sha1-/t1NK/GTp3dF/nHjcdc8MwfZx1E=", + "dev": true, "license": "MIT", "dependencies": { "path-platform": "~0.11.15" @@ -3303,6 +4116,7 @@ "version": "5.1.6", "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz", "integrity": "sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==", + "dev": true, "license": "ISC", "dependencies": { "asn1.js": "^5.2.0", @@ -3316,6 +4130,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.10" @@ -3325,30 +4140,59 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" } }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, "license": "MIT" }, "node_modules/path-platform": { "version": "0.11.15", "resolved": "https://registry.npmjs.org/path-platform/-/path-platform-0.11.15.tgz", "integrity": "sha1-6GQhf3TDaFDwhSt43Hv31KVyG/I=", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8.0" } }, + "node_modules/path-scurry": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", + "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "dev": true, + "dependencies": { + "lru-cache": "^9.1.1 || ^10.0.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/pbkdf2": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz", "integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==", + "dev": true, "license": "MIT", "dependencies": { "create-hash": "^1.1.2", @@ -3365,18 +4209,21 @@ "version": "1.3.6", "resolved": "https://registry.npmjs.org/php-date-formatter/-/php-date-formatter-1.3.6.tgz", "integrity": "sha512-/CKsZYmAwXeNh8KpD/CF9hcJDZNhdb2ICN8+qgqOt5sUu9liZIxZ1R284TNj5MtPt8RjG5X0xn6WSqL0kcKMBg==", + "dev": true, "license": "BSD-3-Clause" }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true, "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, "engines": { "node": ">=8.6" }, @@ -3388,16 +4235,27 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, "license": "MIT", "optional": true, "engines": { "node": ">=6" } }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/postcss": { "version": "8.4.35", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==", + "dev": true, "funding": [ { "type": "opencollective", @@ -3421,10 +4279,155 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dev": true, + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-load-config/node_modules/lilconfig": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.0.0.tgz", + "integrity": "sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/postcss-nested": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", + "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.11" + }, + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-nesting": { + "version": "12.0.3", + "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-12.0.3.tgz", + "integrity": "sha512-yrtMRPFNkfZMv9ikBvZ/Eh3RxhpMBKQ3KzD7LCY8+jYVlgju/Mdcxi4JY8bW2Y7ISXw8GTLuF/o+kFtp+yaVfQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "@csstools/selector-specificity": "^3.0.2", + "postcss-selector-parser": "^6.0.13" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.15", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.15.tgz", + "integrity": "sha512-rEYkQOMUCEMhsKbK66tbEU9QVIxbhN18YiniAwA7XQYTVBqrBy+P2p5JcdqsHgKM2zWylp8d7J6eszocfds5Sw==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, "node_modules/prettier": { "version": "2.8.8", "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, "optional": true, "bin": { "prettier": "bin-prettier.js" @@ -3440,6 +4443,7 @@ "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6.0" @@ -3449,12 +4453,14 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, "license": "MIT" }, "node_modules/prr": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", + "dev": true, "license": "MIT", "optional": true }, @@ -3462,6 +4468,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", + "dev": true, "license": "MIT", "dependencies": { "bn.js": "^4.1.0", @@ -3476,12 +4483,14 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true, "license": "MIT" }, "node_modules/querystring": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "dev": true, "engines": { "node": ">=0.4.x" } @@ -3490,14 +4499,36 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", + "dev": true, "engines": { "node": ">=0.4.x" } }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "^5.1.0" @@ -3507,6 +4538,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", + "dev": true, "license": "MIT", "dependencies": { "randombytes": "^2.0.5", @@ -3517,15 +4549,35 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/raphael/-/raphael-2.3.0.tgz", "integrity": "sha512-w2yIenZAQnp257XUWGni4bLMVxpUpcIl7qgxEgDIXtmSypYtlNxfXWpOBxs7LBTps5sDwhRnrToJrMUrivqNTQ==", + "dev": true, "license": "MIT", "dependencies": { "eve-raphael": "0.5.0" } }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/read-cache/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/read-only-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/read-only-stream/-/read-only-stream-2.0.0.tgz", "integrity": "sha1-JyT9aoET1zdkrCiNQ4YnDB2/F/A=", + "dev": true, "license": "MIT", "dependencies": { "readable-stream": "^2.0.2" @@ -3535,6 +4587,7 @@ "version": "2.3.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", @@ -3550,12 +4603,14 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, "license": "MIT" }, "node_modules/readable-stream/node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "~5.1.0" @@ -3565,6 +4620,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, "license": "MIT", "dependencies": { "picomatch": "^2.2.1" @@ -3574,22 +4630,37 @@ } }, "node_modules/resolve": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", - "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", - "license": "MIT", + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, "dependencies": { - "is-core-module": "^2.2.0", - "path-parse": "^1.0.6" + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, "node_modules/ripemd160": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "dev": true, "license": "MIT", "dependencies": { "hash-base": "^3.0.0", @@ -3600,6 +4671,8 @@ "version": "3.29.4", "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", + "dev": true, + "peer": true, "bin": { "rollup": "dist/bin/rollup" }, @@ -3611,10 +4684,11 @@ "fsevents": "~2.3.2" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, "funding": [ { "type": "github", @@ -3629,18 +4703,45 @@ "url": "https://feross.org/support" } ], - "license": "MIT" + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, "license": "MIT" }, "node_modules/sass": { "version": "1.71.1", "resolved": "https://registry.npmjs.org/sass/-/sass-1.71.1.tgz", "integrity": "sha512-wovtnV2PxzteLlfNzbgm1tFXPLoZILYAMJtvoXXkD7/+1uP41eKkIt1ypWq5/q2uT94qHjXehEYfmjKOvjL9sg==", + "dev": true, + "optional": true, + "peer": true, "dependencies": { "chokidar": ">=3.0.0 <4.0.0", "immutable": "^4.0.0", @@ -3653,49 +4754,11 @@ "node": ">=14.0.0" } }, - "node_modules/sass-loader": { - "version": "14.1.1", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-14.1.1.tgz", - "integrity": "sha512-QX8AasDg75monlybel38BZ49JP5Z+uSKfKwF2rO7S74BywaRmGQMUBw9dtkS+ekyM/QnP+NOrRYq8ABMZ9G8jw==", - "dependencies": { - "neo-async": "^2.6.2" - }, - "engines": { - "node": ">= 18.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "@rspack/core": "0.x || 1.x", - "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", - "sass": "^1.3.0", - "sass-embedded": "*", - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "node-sass": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "webpack": { - "optional": true - } - } - }, "node_modules/sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "dev": true, "license": "ISC", "optional": true }, @@ -3703,6 +4766,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "dev": true, "license": "MIT", "optional": true, "peer": true, @@ -3723,12 +4787,14 @@ "version": "4.0.13", "resolved": "https://registry.npmjs.org/select2/-/select2-4.0.13.tgz", "integrity": "sha512-1JeB87s6oN/TDxQQYCvS5EFoQyvV6eYMZZ0AeA4tdFDYWN3BAGZ8npr17UBFddU0lgAt3H0yjX3X6/ekOj1yjw==", + "dev": true, "license": "MIT" }, "node_modules/serialize-javascript": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, "license": "BSD-3-Clause", "optional": true, "peer": true, @@ -3740,6 +4806,7 @@ "version": "2.4.11", "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "dev": true, "license": "(MIT AND BSD-3-Clause)", "dependencies": { "inherits": "^2.0.1", @@ -3753,21 +4820,45 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/shasum-object/-/shasum-object-1.0.0.tgz", "integrity": "sha512-Iqo5rp/3xVi6M4YheapzZhhGPVs0yZwHj7wvwQ1B9z8H6zk+FEnI7y3Teq7qwnekfEhu8WmG2z0z4iWZaxLWVg==", + "dev": true, "license": "Apache-2.0", "dependencies": { "fast-safe-stringify": "^2.0.7" } }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/shell-quote": { "version": "1.7.3", "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz", "integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==", + "dev": true, "license": "MIT" }, "node_modules/side-channel": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.0", @@ -3782,6 +4873,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", + "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.1", @@ -3796,6 +4888,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -3804,10 +4897,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/simple-concat": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true, "funding": [ { "type": "github", @@ -3828,6 +4934,7 @@ "version": "0.9.1", "resolved": "https://registry.npmjs.org/slimscroll/-/slimscroll-0.9.1.tgz", "integrity": "sha1-9nXNxgHYCtog8WAE0ifRVv0Rh7I=", + "dev": true, "dependencies": { "browserify": ">=3.46.0", "classie": ">=0.0.1", @@ -3842,6 +4949,7 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -3851,6 +4959,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -3860,6 +4969,7 @@ "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, "license": "MIT", "optional": true, "peer": true, @@ -3872,6 +4982,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/stream-combiner2/-/stream-combiner2-1.1.1.tgz", "integrity": "sha1-+02KFCDqNidk4hrUeAOXvry0HL4=", + "dev": true, "license": "MIT", "dependencies": { "duplexer2": "~0.1.0", @@ -3882,6 +4993,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/stream-splicer/-/stream-splicer-2.0.1.tgz", "integrity": "sha512-Xizh4/NPuYSyAXyT7g8IvdJ9HJpxIGL9PjyhtywCZvvP0OPIdqyrr4dMikeuvY8xahpdKEBlBTySe583totajg==", + "dev": true, "license": "MIT", "dependencies": { "inherits": "^2.0.1", @@ -3892,15 +5004,76 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "~5.2.0" } }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string.prototype.trimend": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", + "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.2", @@ -3914,6 +5087,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", + "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.2", @@ -3923,28 +5097,206 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/subarg": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/subarg/-/subarg-1.0.0.tgz", "integrity": "sha1-9izxdYHplrSPyWVpn1TAauJouNI=", + "dev": true, "license": "MIT", "dependencies": { "minimist": "^1.1.0" } }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/sucrase/node_modules/glob": { + "version": "10.3.10", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", + "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.5", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sucrase/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/syntax-error": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/syntax-error/-/syntax-error-1.4.0.tgz", "integrity": "sha512-YPPlu67mdnHGTup2A8ff7BC2Pjq0e0Yp/IyTFN03zWO0RcK07uLcbi7C2KpGR2FvWbaB0+bfE27a+sBKebSo7w==", + "dev": true, "license": "MIT", "dependencies": { "acorn-node": "^1.2.0" } }, + "node_modules/tailwindcss": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.0.tgz", + "integrity": "sha512-VigzymniH77knD1dryXbyxR+ePHihHociZbXnLZHUyzf2MMs2ZVqlUrZ3FvpXP8pno9JzmILt1sZPD19M3IxtA==", + "dev": true, + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.19.1", + "lilconfig": "^2.1.0", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.1", + "postcss-nested": "^6.0.1", + "postcss-selector-parser": "^6.0.11", + "resolve": "^1.22.2", + "sucrase": "^3.32.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/tapable": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, "license": "MIT", "optional": true, "peer": true, @@ -3956,6 +5308,7 @@ "version": "5.24.0", "resolved": "https://registry.npmjs.org/terser/-/terser-5.24.0.tgz", "integrity": "sha512-ZpGR4Hy3+wBEzVEnHvstMvqpD/nABNelQn/z2r0fjVWGQsN3bpOLzQlqDxmb4CDZnXq5lpjnQ+mHQLAOpfM5iw==", + "dev": true, "optional": true, "peer": true, "dependencies": { @@ -3975,6 +5328,7 @@ "version": "5.2.4", "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.2.4.tgz", "integrity": "sha512-E2CkNMN+1cho04YpdANyRrn8CyN4yMy+WdFKZIySFZrGXZxJwJP6PMNGGc/Mcr6qygQHUUqRxnAPmi0M9f00XA==", + "dev": true, "license": "MIT", "optional": true, "peer": true, @@ -4008,16 +5362,39 @@ } } }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true, "license": "MIT" }, "node_modules/through2": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, "license": "MIT", "dependencies": { "readable-stream": "~2.3.6", @@ -4028,6 +5405,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -4036,22 +5414,31 @@ "node": ">=8.0" } }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true + }, "node_modules/tslib": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", + "dev": true, "license": "0BSD" }, "node_modules/typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "dev": true, "license": "MIT" }, "node_modules/umd": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/umd/-/umd-3.0.3.tgz", "integrity": "sha512-4IcGSufhFshvLNcMCV80UnQVlZ5pMOC8mvNPForqwA4+lzYQuetTESLDQkeLmihq8bRcnpbQa48Wb8Lh16/xow==", + "dev": true, "license": "MIT", "bin": { "umd": "bin/cli.js" @@ -4061,6 +5448,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", "integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==", + "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.1", @@ -4076,6 +5464,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/undeclared-identifiers/-/undeclared-identifiers-1.1.3.tgz", "integrity": "sha512-pJOW4nxjlmfwKApE4zvxLScM/njmwj/DiUBv7EabwE4O8kRUy+HIwxQtZLBPll/jx1LJyBcqNfB3/cpv9EZwOw==", + "dev": true, "license": "Apache-2.0", "dependencies": { "acorn-node": "^1.3.0", @@ -4088,10 +5477,49 @@ "undeclared-identifiers": "bin.js" } }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, "license": "BSD-2-Clause", "optional": true, "peer": true, @@ -4103,6 +5531,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true, "license": "MIT", "optional": true, "peer": true, @@ -4114,6 +5543,7 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "dev": true, "license": "MIT", "dependencies": { "punycode": "1.3.2", @@ -4124,24 +5554,29 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", + "dev": true, "license": "MIT" }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true, "license": "MIT" }, "node_modules/util-extend": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/util-extend/-/util-extend-1.0.3.tgz", "integrity": "sha1-p8IW0mdUUWljeztu3GypEZ4v+T8=", + "dev": true, "license": "MIT" }, "node_modules/vite": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.1.tgz", - "integrity": "sha512-AXXFaAJ8yebyqzoNB9fu2pHoo/nWX+xZlaRwoeYUxEqBO+Zj4msE5G+BhGBll9lYEKv9Hfks52PAF2X7qDYXQA==", + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.2.tgz", + "integrity": "sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==", + "dev": true, + "peer": true, "dependencies": { "esbuild": "^0.18.10", "postcss": "^8.4.27", @@ -4193,27 +5628,28 @@ } }, "node_modules/vite-plugin-full-reload": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/vite-plugin-full-reload/-/vite-plugin-full-reload-1.0.5.tgz", - "integrity": "sha512-kVZFDFWr0DxiHn6MuDVTQf7gnWIdETGlZh0hvTiMXzRN80vgF4PKbONSq8U1d0WtHsKaFODTQgJeakLacoPZEQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/vite-plugin-full-reload/-/vite-plugin-full-reload-1.1.0.tgz", + "integrity": "sha512-3cObNDzX6DdfhD9E7kf6w2mNunFpD7drxyNgHLw+XwIYAgb+Xt16SEXo0Up4VH+TMf3n+DSVJZtW2POBGcBYAA==", + "dev": true, "dependencies": { "picocolors": "^1.0.0", "picomatch": "^2.3.1" - }, - "peerDependencies": { - "vite": "^2 || ^3 || ^4" } }, "node_modules/vm-browserify": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", + "dev": true, "license": "MIT" }, "node_modules/vue": { "version": "2.7.16", "resolved": "https://registry.npmjs.org/vue/-/vue-2.7.16.tgz", "integrity": "sha512-4gCtFXaAA3zYZdTp5s4Hl2sozuySsgz4jy1EnpBHNfpMa9dK1ZCG7viqBPCwXtmgc8nHqUsAu3G4gtmXkkY3Sw==", + "deprecated": "Vue 2 has reached EOL and is no longer actively maintained. See https://v2.vuejs.org/eol/ for more details.", + "dev": true, "dependencies": { "@vue/compiler-sfc": "2.7.16", "csstype": "^3.1.0" @@ -4223,6 +5659,7 @@ "version": "2.7.16", "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz", "integrity": "sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==", + "dev": true, "dependencies": { "de-indent": "^1.0.2", "he": "^1.2.0" @@ -4232,6 +5669,7 @@ "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "dev": true, "license": "MIT", "optional": true, "peer": true, @@ -4247,6 +5685,7 @@ "version": "5.76.1", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.1.tgz", "integrity": "sha512-4+YIK4Abzv8172/SGqObnUjaIHjLEuUasz9EwQj/9xmPPkYJy2Mh03Q/lJfSD3YLzbxy5FeTq5Uw0323Oh6SJQ==", + "dev": true, "license": "MIT", "optional": true, "peer": true, @@ -4296,6 +5735,7 @@ "version": "0.0.51", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz", "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==", + "dev": true, "license": "MIT", "optional": true, "peer": true @@ -4304,6 +5744,7 @@ "version": "4.2.10", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true, "license": "ISC", "optional": true, "peer": true @@ -4312,6 +5753,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true, "license": "MIT", "optional": true, "peer": true, @@ -4319,10 +5761,26 @@ "node": ">=10.13.0" } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/which-boxed-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, "license": "MIT", "dependencies": { "is-bigint": "^1.0.1", @@ -4339,6 +5797,7 @@ "version": "1.1.7", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.7.tgz", "integrity": "sha512-vjxaB4nfDqwKI0ws7wZpxIlde1XrLX5uB0ZjpfshgmapJMD7jJWhZI+yToJTqaFByF0eNBcYxbjmCzoRP7CfEw==", + "dev": true, "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.5", @@ -4355,25 +5814,128 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true, "license": "ISC" }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.4" } }, + "node_modules/yaml": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", + "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", + "dev": true, + "engines": { + "node": ">= 14" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, "license": "MIT", "optional": true, "peer": true, diff --git a/package.json b/package.json index fcfe1fe5af..c3ae681b29 100644 --- a/package.json +++ b/package.json @@ -7,9 +7,12 @@ "dev": "vite", "build": "vite build" }, - "dependencies": { + "devDependencies": { "@fortawesome/fontawesome-free": "^6.5.1", + "@tailwindcss/forms": "^0.5.7", + "@tailwindcss/typography": "^0.5.10", "admin-lte": "2.4.18", + "autoprefixer": "^10.4.16", "bootstrap": "^5.3.3", "bootstrap-sass": "^3.4.3", "bootstrap-tour": "^0.12.0", @@ -19,10 +22,12 @@ "less": "^4.2.0", "less-loader": "^12.2.0", "postcss": "^8.4", - "sass": "^1.71.1", - "sass-loader": "^14.1.1", - "vite": "^4.5.1", + "postcss-nesting": "^12.0.2", + "tailwindcss": "^3.4.0", "vue": "^2.7.16", "vue-template-compiler": "^2.7.16" + }, + "dependencies": { + "@alpinejs/collapse": "^3.13.5" } } diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000000..e676bdc8cc --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,7 @@ +module.exports = { + plugins: { + 'tailwindcss/nesting': 'postcss-nesting', + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/resources/assets/css/tailwind.css b/resources/assets/css/tailwind.css new file mode 100644 index 0000000000..b5c61c9567 --- /dev/null +++ b/resources/assets/css/tailwind.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/resources/assets/js/app.js b/resources/assets/js/app.js index 45ef84db1b..db57c3e6a0 100644 --- a/resources/assets/js/app.js +++ b/resources/assets/js/app.js @@ -2,8 +2,15 @@ import "@fortawesome/fontawesome-free/js/fontawesome"; import "@fortawesome/fontawesome-free/js/solid"; import "@fortawesome/fontawesome-free/js/brands"; -$('body').scrollspy({ - target: '.navbar-fixed-top' +$("body").scrollspy({ + target: ".navbar-fixed-top", }); -$('.tooltip_displays').tooltip(); +$(".tooltip_displays").tooltip(); + +import { Alpine } from "../../../vendor/livewire/livewire/dist/livewire.esm"; +import collapse from "@alpinejs/collapse"; + +Alpine.plugin(collapse); + +Livewire.start(); diff --git a/resources/views/components/layouts/app.blade.php b/resources/views/components/layouts/app.blade.php new file mode 100644 index 0000000000..4f5219b300 --- /dev/null +++ b/resources/views/components/layouts/app.blade.php @@ -0,0 +1,27 @@ + + + + + + + + @vite('resources/assets/css/tailwind.css') + @filamentStyles + + {{ isset($title) ? "VATSIM UK | $title" : 'VATSIM UK' }} + + +
+
+
+ + + +
+ {{ $slot }} +
+
+ +@filamentScripts +@livewire('notifications') + diff --git a/resources/views/components/nav.blade.php b/resources/views/components/nav.blade.php index b933aa92bf..67699bb699 100644 --- a/resources/views/components/nav.blade.php +++ b/resources/views/components/nav.blade.php @@ -62,6 +62,7 @@
    • {!! link_to_route("site.atc.landing", "Welcome") !!}
    • +
    • {!! link_to_route("site.roster.index", "Controller Roster") !!}
    • {!! link_to_route("site.atc.newController", "New Controller (OBS)") !!}
    • {!! link_to_route("site.atc.endorsements", "Endorsements") !!}
    • {!! link_to_route("site.atc.mentor", "Becoming a Mentor") !!}
    • diff --git a/resources/views/controllers/endorsements/area.blade.php b/resources/views/controllers/endorsements/area.blade.php index 09442229dc..245441e82c 100644 --- a/resources/views/controllers/endorsements/area.blade.php +++ b/resources/views/controllers/endorsements/area.blade.php @@ -11,18 +11,18 @@
      - @foreach ($endorsements as $endorsement) + @foreach ($positionGroups as $positionGroup)
      -  {{ $endorsement['name'] }} +  {{ $positionGroup['name'] }}

      Required Hours

      The following hours are required on the corresponding positions
        - @foreach($endorsement['conditions'] as $condition) + @foreach($positionGroup['conditions'] as $condition)
      • {{ $condition['position'] }} - {{ $condition['required_hours'] }} hours within the last {{ $condition['within_months'] }} months.
      • @endforeach
      @@ -30,7 +30,7 @@

      Your Progress

      - @foreach($endorsement['conditions'] as $condition) + @foreach($positionGroup['conditions'] as $condition) {{ $condition['position'] }}
      @if ($condition['complete']) diff --git a/resources/views/controllers/endorsements/gatwick_ground.blade.php b/resources/views/controllers/endorsements/gatwick_ground.blade.php index 7558d1d95a..4f93964e66 100644 --- a/resources/views/controllers/endorsements/gatwick_ground.blade.php +++ b/resources/views/controllers/endorsements/gatwick_ground.blade.php @@ -119,7 +119,7 @@
      Once you have completed the requirements above, you will be able to press the button below to request access to the Moodle course and progress to Step 2.

      - @if($endorsment->conditionsMetForUser($_account)) + @if($positionGroup->conditionsMetForUser($_account)) diff --git a/resources/views/layout.blade.php b/resources/views/layout.blade.php index 36e0606274..3c4fea5cec 100644 --- a/resources/views/layout.blade.php +++ b/resources/views/layout.blade.php @@ -79,5 +79,6 @@ @yield('scripts') @include('partials/_snow') @cookieconsentview +@livewireScriptConfig diff --git a/resources/views/livewire/roster/index.blade.php b/resources/views/livewire/roster/index.blade.php new file mode 100644 index 0000000000..487c30e220 --- /dev/null +++ b/resources/views/livewire/roster/index.blade.php @@ -0,0 +1,61 @@ +Roster +
      +{{-- flash messages --}} +@if (session()->has('success')) +
      +
      +

      🎉 {{ session('success') }}

      +
      +
      +@endif +@if (session()->has('error')) +
      +
      +

      ❌ {{session('error')}}

      +
      +
      +@endif +
      +
      +
      +
      + 👋 Hello, {{ auth()->user()->name_first }}! +
      + @if($roster) + Active on Roster + @else + Inactive on Roster + @endif +
      + @if($roster) + + You are currently active on the VATSIM UK roster and can control any positions + listed on your roster page. + + @elseif(auth()->user()->hasState('DIVISION')) + + You are currently inactive on the VATSIM UK roster, and cannot control any UK positions until you + renew your currency. + + @else + + You are currently inactive on the VATSIM UK roster, and cannot control any UK positions. +
      + Please contact Member Services if you believe this is incorrect. +
      + @endif +
      +
      + @if(!$roster && auth()->user()->hasState('DIVISION')) + Renew my currency + @endif + Search the roster +
      +
      +
      +
      +
      diff --git a/resources/views/livewire/roster/renew.blade.php b/resources/views/livewire/roster/renew.blade.php new file mode 100644 index 0000000000..8be9551a9d --- /dev/null +++ b/resources/views/livewire/roster/renew.blade.php @@ -0,0 +1,69 @@ +Renew Roster Currency +
      + @if ($page == 1) +
      +
      +
      + 👋 Hello, {{ auth()->user()->name_first }}! +
      + @if (!$canReactivate) + As it has been 18 months since your last ATC session you cannot automatically reactivate your roster membership. +
      + Please contact Member Services. +
      + @else +

      It has been a while! Our records show it has {{ $lastLogon }} since your last controlling session.

      +

      Because of this, you are required to reactivate onto the VATSIM UK controlling roster.

      +

      Before you do this, please take a read through what has changed in the Division and Procedurally whilst you've been gone!

      + @endif +
      +
      + @if ($canReactivate) +
      +
      + + Notifications +

      There have been a few changes since you have been gone! Please take the time + to read through the below notifications, acknowleding you have read them as you + read through them.

      +

      Click on the arrow or the title of the notification to see more and mark as read.

      +

      You have {{ count($notifications) }} notifications to read.

      + +

      + @if ($notifications->count() > 0) +
      + @foreach ($notifications as $notification) +
      + {{ $notification['title'] }} + +
      +

      {{ $notification['body'] }}

      + @if ($notification['link']) + Read more + @endif +

      Mark read

      +
      +
      + @endforeach +
      +
      + @endif +
      +
      + @endif +
      + @elseif ($page === 2) +
      +
      + + Reactivate +
      +

      Once you are added back onto the roster you must maintain a minimum of 3 hours controlling any UK position + within a calendar quarter e.g. January -> March.

      +

      Click the button below to add yourself back onto the roster.

      + +
      +
      +
      + @endif +
      diff --git a/resources/views/livewire/roster/search.blade.php b/resources/views/livewire/roster/search.blade.php new file mode 100644 index 0000000000..3e09d55d8c --- /dev/null +++ b/resources/views/livewire/roster/search.blade.php @@ -0,0 +1,26 @@ +Search Roster +
      +
      +
      +
      + +
      + +
      +
      + +
      + +
      +
      +
      + Go back +
      +
      +
      diff --git a/resources/views/livewire/roster/show.blade.php b/resources/views/livewire/roster/show.blade.php new file mode 100644 index 0000000000..9b48514f86 --- /dev/null +++ b/resources/views/livewire/roster/show.blade.php @@ -0,0 +1,83 @@ +Roster for {{ $account->id }} +
      +
      + +
      +
      + {{ $account->id }} +
      + {{ $account->qualification_atc }} - + {{ $account->primary_state->name }} Member +
      +
      +
      + @if($roster) + Active on Roster + @else + Inactive on Roster + @endif +
      +
      + +
      +
      +
      + @foreach($account->endorsements()->active()->get()->groupBy('type') as $type => $endorsements) + {{ $type }} + @foreach($endorsements as $endorsement) + {{ $endorsement->endorsable->name }} + @if($endorsement->expires()) + Expires {{ $endorsement->expires_at->toFormattedDateString() }} + @endif + + Covers: {{ $endorsement->endorsable->description }} + @endforeach + @endforeach +
      +
      +
      +
      + @if($roster) +
      +
      +
      + +
      + +
      +
      + +
      + +
      +
      + @endif + @if($position) + + {{ $roster->accountCanControl($position) + ? "✅ $account->id can control $position->callsign." + : "❌ $account->id cannot control $position->callsign." + }} + + @endif + @if(!$roster) + ❌ {{ $account->id }} cannot control any UK positions. + @endif +
      +
      + Go back +
      +
      +
      diff --git a/resources/views/mship/management/dashboard.blade.php b/resources/views/mship/management/dashboard.blade.php index 1eece9cad4..c4edde8320 100644 --- a/resources/views/mship/management/dashboard.blade.php +++ b/resources/views/mship/management/dashboard.blade.php @@ -69,7 +69,7 @@ class="tooltip_displays"
      @endif
      - STATUS: + MEMBERSHIP: {{ $_account->status_string }} {{ !is_null($_account->primary_state) ? $_account->primary_state->name : 'unknown state' }} Member
      @@ -92,6 +92,19 @@ class="tooltip_displays" onclick="event.preventDefault(); document.getElementById('invisibility-form').submit();">{{ $_account->is_invisible ? 'Disable' : 'Enable' }} {!! Form::close() !!}
      + +
      + CONTROLLER ROSTER: + @if($roster) + + Active + + @else + + Inactive + + @endif +
      diff --git a/resources/views/mship/waiting-lists/_flag_breakdown.blade.php b/resources/views/mship/waiting-lists/_flag_breakdown.blade.php index 4a4920624c..f92eabb33d 100644 --- a/resources/views/mship/waiting-lists/_flag_breakdown.blade.php +++ b/resources/views/mship/waiting-lists/_flag_breakdown.blade.php @@ -1,6 +1,6 @@ Requirements:
        - @foreach($flag->endorsement->conditions as $condition) + @foreach($flag->positionGroup->conditions as $condition) @php $progress = $condition->progressForUser($user); $overallProgress = $condition->overallProgressForUser($user); diff --git a/resources/views/mship/waiting-lists/_waiting_lists_list.blade.php b/resources/views/mship/waiting-lists/_waiting_lists_list.blade.php index 5782994b85..6586c5e2e3 100644 --- a/resources/views/mship/waiting-lists/_waiting_lists_list.blade.php +++ b/resources/views/mship/waiting-lists/_waiting_lists_list.blade.php @@ -20,16 +20,16 @@ {{$waitingList->name}} + We cannot currently show you your position in the waiting list due to GCAP implementation. + We expect this to very completed soon and the underlying data is still present i.e. your original position. + + {{-- @if($waitingList->pivot->position) {{$waitingList->pivot->position}} @else - @endif - - - - - {{$waitingList->pivot->current_status}} + --}} {{$waitingList->pivot->created_at->format('d M Y')}} $waitingList->id])}}">View Details diff --git a/resources/views/mship/waiting-lists/view.blade.php b/resources/views/mship/waiting-lists/view.blade.php index bf773912ec..c724b5135e 100644 --- a/resources/views/mship/waiting-lists/view.blade.php +++ b/resources/views/mship/waiting-lists/view.blade.php @@ -17,10 +17,6 @@ Department {{ $list->formatted_department }} - - Your Status - {{$list->pivot->current_status}} - @if($list->pivot->position) Your Position @@ -54,19 +50,6 @@ - @if($list->isATCList()) - - - The following hour check: - - - - Hour Check (Automatic) - - - - - @endif {{$list->isATCList() ? ' and ': null}} {{$list->flags_check}} of the following: @@ -74,7 +57,7 @@ @foreach($list->pivot->flags as $flag) - {{$flag->name}} ({{$flag->endorsement_id ? 'Automatic': 'Manual'}}) + {{$flag->name}} ({{$flag->position_group_id ? 'Automatic': 'Manual'}}) @@ -87,40 +70,4 @@
      @endif
      -@if($list->isATCList() || count($automaticFlags)) -
      - Important: Automated eligibility flags are only calculated every 24 hours, or after the end of an ATC session! If you have just completed - a network session, the flags shown above may not be accurate. -
      -
      - @if($list->isATCList()) -
      -
      -
      - Hour Check -
      -
      -

      - Have at least 12 hours on UK controller positions in the last 3 months. -

      - NB: Only sessions with primary frequencies count (i.e. not mentoring) - -
      -
      -
      - @endif - @foreach($automaticFlags as $flag) -
      -
      -
      - {{$flag->name}} -
      -
      - @include('mship.waiting-lists._flag_breakdown', ["flag" => $flag, "user" => $list->pivot->account]) -
      -
      -
      - @endforeach -
      -@endif @endsection diff --git a/resources/views/site/airport/view.blade.php b/resources/views/site/airport/view.blade.php index 310e8762c7..668ca3bc4d 100644 --- a/resources/views/site/airport/view.blade.php +++ b/resources/views/site/airport/view.blade.php @@ -30,7 +30,7 @@ @if($stands) $('#stands').DataTable( { - "aaSorting": [], + "aaSorting": [], "columnDefs": [ { targets: 'no-sort', orderable: false }, { targets: 'no-search', searchable: false} @@ -330,10 +330,10 @@ function initMap() { @endif @endif - @if($airport->stations->count() > 0) + @if($airport->positions->count() > 0)
      -
      ATC Stations
      +
      ATC Positions
      @@ -342,16 +342,16 @@ function initMap() { - @foreach($stations as $station) + @foreach($positions as $positions) - - - + + @endforeach diff --git a/resources/views/site/home.blade.php b/resources/views/site/home.blade.php index d47ddb2c5a..d3cf472826 100644 --- a/resources/views/site/home.blade.php +++ b/resources/views/site/home.blade.php @@ -140,6 +140,9 @@ function toggleActive() { + diff --git a/routes/web-livewire.php b/routes/web-livewire.php new file mode 100644 index 0000000000..c3470afdad --- /dev/null +++ b/routes/web-livewire.php @@ -0,0 +1,16 @@ + 'site.roster.', + 'prefix' => 'roster', +], function () { + Route::get('/', Index::class)->name('index'); + Route::get('/renew', Renew::class)->name('renew')->middleware('auth'); + Route::get('/search', Search::class)->name('search'); + Route::get('/{account}', Show::class)->name('show')->middleware('auth'); +}); diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000000..5a1e65dbb8 --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,20 @@ +/** @type {import('tailwindcss').Config} */ +import preset from './vendor/filament/support/tailwind.config.preset'; +const colors = require('tailwindcss/colors'); + +module.exports = { + content: [ + './resources/**/*.blade.php', + './vendor/filament/**/*.blade.php', + ], + theme: { + extend: { + colors: { + brand: '#25ADE3' + } + }, + }, + plugins: [ + require('@tailwindcss/forms'), + ], +} diff --git a/tests/Feature/Account/WaitingListsTest.php b/tests/Feature/Account/WaitingListsTest.php index 2d071ce5c4..7ae96fe2cc 100644 --- a/tests/Feature/Account/WaitingListsTest.php +++ b/tests/Feature/Account/WaitingListsTest.php @@ -4,7 +4,6 @@ use App\Events\Mship\AccountAltered; use App\Models\Training\WaitingList; -use App\Services\Training\AddToWaitingList; use Illuminate\Foundation\Testing\DatabaseTransactions; use Illuminate\Support\Facades\Event; use Tests\TestCase; @@ -49,26 +48,21 @@ public function testIndexWithAWaitingListAccounts() public function testViewATCWaitingListDetailsNoFlags() { $list = factory(WaitingList::class)->create(['name' => 'My List']); - handleService(new AddToWaitingList($list, $this->user, $this->privacc)); + $list->addToWaitingList($this->user, $this->privacc); $this->actingAs($this->user) ->get(route('mship.waiting-lists.view', ['waitingListId' => $list->id])) - ->assertSee('My List') - ->assertSee('Hour Check (Automatic)') - ->assertSeeText('Have at least 12 hours on UK controller positions in the last 3 months') - ->assertSeeText('0.0 / 12 hours'); + ->assertSee('My List'); } /** @test */ public function testViewPilotWaitingListDetailsNoFlags() { $list = factory(WaitingList::class)->create(['name' => 'My List', 'department' => WaitingList::PILOT_DEPARTMENT]); - handleService(new AddToWaitingList($list, $this->user, $this->privacc)); + $list->addToWaitingList($this->user, $this->privacc); - $response = $this->actingAs($this->user) + $this->actingAs($this->user) ->get(route('mship.waiting-lists.view', ['waitingListId' => $list->id])) - ->assertSee('My List') - ->assertDontSee('Hour Check (Automatic)') - ->assertDontSeeText('Have at least 12 hours on UK controller positions in the last 3 months'); + ->assertSee('My List'); } } diff --git a/tests/Feature/Admin/BaseAdminTestCase.php b/tests/Feature/Admin/BaseAdminTestCase.php index a89163f8f8..92dabe9c66 100644 --- a/tests/Feature/Admin/BaseAdminTestCase.php +++ b/tests/Feature/Admin/BaseAdminTestCase.php @@ -10,7 +10,7 @@ abstract class BaseAdminTestCase extends TestCase { - protected $adminUser; + protected Account $adminUser; protected function setUp(): void { diff --git a/tests/Feature/Admin/EndorsementRequest/EndorsementRequestApprovalTest.php b/tests/Feature/Admin/EndorsementRequest/EndorsementRequestApprovalTest.php new file mode 100644 index 0000000000..451dc4e512 --- /dev/null +++ b/tests/Feature/Admin/EndorsementRequest/EndorsementRequestApprovalTest.php @@ -0,0 +1,122 @@ +adminUser->givePermissionTo('endorsement-request.access'); + } + + public function test_can_approve_permanent_endorsement_request_with_permission() + { + $endorsementRequest = EndorsementRequest::factory()->create([ + 'endorsable_type' => PositionGroup::class, + 'endorsable_id' => PositionGroup::factory()->create()->id, + ]); + + $this->adminUser->givePermissionTo('endorsement-request.approve.*'); + + Livewire::actingAs($this->adminUser); + Livewire::test(ListEndorsementRequests::class) + ->assertCanSeeTableRecords([$endorsementRequest]) + ->callTableAction('approve', record: $endorsementRequest->id, data: [ + 'type' => 'Permanent', + ]) + ->assertTableActionHidden('approve', $endorsementRequest->id); + + $this->assertDatabaseHas('endorsement_requests', [ + 'id' => $endorsementRequest->id, + 'actioned_at' => now(), + 'actioned_type' => EndorsementRequest::STATUS_APPROVED, + ]); + } + + public function test_cannot_approve_permanent_endorsement_request_without_permission() + { + $endorsementRequest = EndorsementRequest::factory()->create([ + 'endorsable_type' => PositionGroup::class, + 'endorsable_id' => PositionGroup::factory()->create()->id, + ]); + + Livewire::actingAs($this->adminUser); + Livewire::test(ListEndorsementRequests::class) + ->assertCanSeeTableRecords([$endorsementRequest]) + ->assertTableActionHidden('approve', $endorsementRequest->id); + } + + public function test_can_approve_temporary_endorsement_with_days_input_with_permission() + { + $endorsementRequest = EndorsementRequest::factory()->create([ + 'endorsable_type' => Position::class, + 'endorsable_id' => Position::factory()->create()->id, + ]); + + $this->adminUser->givePermissionTo('endorsement-request.approve.*'); + + Livewire::actingAs($this->adminUser); + Livewire::test(ListEndorsementRequests::class) + ->assertCanSeeTableRecords([$endorsementRequest]) + ->assertTableActionVisible('approve', $endorsementRequest->id) + ->callTableAction('approve', $endorsementRequest->id, [ + 'type' => 'Temporary', + 'days' => 7, + ]) + ->assertTableActionHidden('approve', $endorsementRequest->id); + + $this->assertDatabaseHas('endorsement_requests', [ + 'id' => $endorsementRequest->id, + 'actioned_at' => now(), + 'actioned_type' => EndorsementRequest::STATUS_APPROVED, + ]); + } + + public function test_cannot_approve_temporary_endorsement_without_days_input_with_permission() + { + $endorsementRequest = EndorsementRequest::factory()->create([ + 'endorsable_type' => Position::class, + 'endorsable_id' => Position::factory()->create()->id, + ]); + + $this->adminUser->givePermissionTo('endorsement-request.approve.*'); + + Livewire::actingAs($this->adminUser); + Livewire::test(ListEndorsementRequests::class) + ->assertCanSeeTableRecords([$endorsementRequest]) + ->assertTableActionVisible('approve', $endorsementRequest->id) + ->callTableAction('approve', $endorsementRequest->id) + ->assertTableActionVisible('approve', $endorsementRequest->id); + + $this->assertDatabaseHas('endorsement_requests', [ + 'id' => $endorsementRequest->id, + 'actioned_at' => null, + 'actioned_type' => null, + ]); + } + + public function test_cannot_approve_temporary_endorsement_without_permission() + { + $endorsementRequest = EndorsementRequest::factory()->create([ + 'endorsable_type' => Position::class, + 'endorsable_id' => Position::factory()->create()->id, + ]); + + Livewire::actingAs($this->adminUser); + Livewire::test(ListEndorsementRequests::class) + ->assertCanSeeTableRecords([$endorsementRequest]) + ->assertTableActionHidden('approve', $endorsementRequest->id); + } +} diff --git a/tests/Feature/Admin/EndorsementRequest/EndorsementRequestCreateTest.php b/tests/Feature/Admin/EndorsementRequest/EndorsementRequestCreateTest.php new file mode 100644 index 0000000000..387acd91d3 --- /dev/null +++ b/tests/Feature/Admin/EndorsementRequest/EndorsementRequestCreateTest.php @@ -0,0 +1,92 @@ +actingAsAdminUser('endorsement-request.access'); + + Livewire::test(ListEndorsementRequests::class) + ->assertDontSee('New endorsement request'); + } + + public function test_create_visible_when_create_permission() + { + $this->actingAsAdminUser(['endorsement-request.access', 'endorsement-request.create.*']); + + Livewire::test(ListEndorsementRequests::class) + ->assertSee('New endorsement request'); + } + + public function test_cannot_create_endorsement_request_for_position_group_without_permission() + { + $this->actingAsAdminUser('endorsement-request.access'); + + Livewire::test(CreateEndorsementRequest::class) + ->assertForbidden(); + } + + public function test_can_create_endorsement_request_for_position_group() + { + $accountRequestingFor = Account::factory()->create(); + $positionGroup = PositionGroup::factory()->create(); + + $this->actingAsAdminUser(['endorsement-request.access', 'endorsement-request.create.*']); + Livewire::test(CreateEndorsementRequest::class) + // check if the position group is visible + ->set('data.endorsable_type', 'App\Models\Atc\PositionGroup') + ->assertSee($positionGroup->name) + ->fillForm([ + 'account_id' => $accountRequestingFor->id, + 'endorsable_id' => $positionGroup->id, + 'notes' => 'This is a test note', + ]) + ->call('create'); + + $this->assertDatabaseHas('endorsement_requests', [ + 'account_id' => $accountRequestingFor->id, + 'endorsable_id' => $positionGroup->id, + 'endorsable_type' => 'App\Models\Atc\PositionGroup', + 'notes' => 'This is a test note', + ]); + } + + public function test_can_create_endorsement_request_for_temporarily_endorsable_position() + { + $accountRequestingFor = Account::factory()->create(); + $position = Position::factory()->temporarilyEndorsable()->create(); + $nonTemporarilyEndorsablePosition = Position::factory()->create(); + + $this->adminUser->givePermissionTo('endorsement-request.access'); + $this->adminUser->givePermissionTo('endorsement-request.create.*'); + + Livewire::actingAs($this->adminUser); + Livewire::test(CreateEndorsementRequest::class) + ->set('data.endorsable_type', 'App\Models\Atc\Position') + ->assertSee($position->callsign) + ->assertDontSee($nonTemporarilyEndorsablePosition->name) + ->fillForm([ + 'account_id' => $accountRequestingFor->id, + 'endorsable_id' => $position->id, + 'notes' => 'This is a test note', + ]) + ->call('create'); + + $this->assertDatabaseHas('endorsement_requests', [ + 'account_id' => $accountRequestingFor->id, + 'endorsable_id' => $position->id, + 'endorsable_type' => 'App\Models\Atc\Position', + 'notes' => 'This is a test note', + ]); + } +} diff --git a/tests/Feature/Admin/PositionGroup/EndorsementCreationRelationManagerTest.php b/tests/Feature/Admin/PositionGroup/EndorsementCreationRelationManagerTest.php new file mode 100644 index 0000000000..b8d1cd341b --- /dev/null +++ b/tests/Feature/Admin/PositionGroup/EndorsementCreationRelationManagerTest.php @@ -0,0 +1,65 @@ +create(); + + $this->actingAsAdminUser(); + + Livewire::test(MembershipEndorsementRelationManager::class, ['ownerRecord' => $positionGroup, 'pageClass' => ViewWaitingList::class]) + ->assertTableActionHidden('create'); + } + + public function test_can_create_endorsement_within_position_group_with_permission() + { + $positionGroup = PositionGroup::factory()->create(); + $accountToEndorse = Account::factory()->create(); + + $this->actingAsAdminUser(['endorsement.create.*']); + + Livewire::test(MembershipEndorsementRelationManager::class, ['ownerRecord' => $positionGroup, 'pageClass' => ViewWaitingList::class]) + ->assertTableActionExists('create') + ->callTableAction('create', data: [ + 'account_id' => $accountToEndorse->id, + ]); + + $this->assertDatabaseHas('mship_account_endorsement', [ + 'account_id' => $accountToEndorse->id, + 'endorsable_id' => $positionGroup->id, + 'endorsable_type' => PositionGroup::class, + ]); + } + + public function test_does_not_create_endorsement_within_position_group_without_permission() + { + $positionGroup = PositionGroup::factory()->create(); + + $this->actingAsAdminUser(['endorsement.create.*']); + + Livewire::test(MembershipEndorsementRelationManager::class, ['ownerRecord' => $positionGroup, 'pageClass' => ViewWaitingList::class]) + ->assertTableActionVisible('create') + ->callTableAction('create', data: [ + 'account_id' => 9999999, + ]); + + $this->assertDatabaseMissing('mship_account_endorsement', [ + 'account_id' => 9999999, + 'endorsable_id' => $positionGroup->id, + 'endorsable_type' => PositionGroup::class, + ]); + } +} diff --git a/tests/Feature/Admin/WaitingLists/Pages/ViewWaitingListPageTest.php b/tests/Feature/Admin/WaitingLists/Pages/ViewWaitingListPageTest.php index df4088bf6e..bd886510df 100644 --- a/tests/Feature/Admin/WaitingLists/Pages/ViewWaitingListPageTest.php +++ b/tests/Feature/Admin/WaitingLists/Pages/ViewWaitingListPageTest.php @@ -3,13 +3,12 @@ namespace Tests\Feature\Admin\WaitingLists; use App\Filament\Resources\WaitingListResource\Pages\ViewWaitingList; -use App\Filament\Resources\WaitingListResource\RelationManagers\IneligibleAccountsRelationManager; -use App\Models\Atc\Endorsement; +use App\Filament\Resources\WaitingListResource\RelationManagers\AccountsRelationManager; +use App\Models\Atc\PositionGroup; use App\Models\Mship\Account; use App\Models\Mship\State; use App\Models\Training\WaitingList; use App\Models\Training\WaitingList\WaitingListFlag; -use App\Models\Training\WaitingList\WaitingListStatus; use Filament\Tables\Actions\EditAction; use Illuminate\Foundation\Testing\DatabaseTransactions; use Livewire\Livewire; @@ -26,7 +25,7 @@ public function setUp(): void Livewire::actingAs($this->adminUser); } - public function test_two_relation_manager_tables_are_present() + public function test_one_relation_manager_tables_are_present() { $waitingList = factory(WaitingList::class)->create(['department' => 'atc']); $this->adminUser->givePermissionTo('waiting-lists.view.atc'); @@ -34,8 +33,6 @@ public function test_two_relation_manager_tables_are_present() Livewire::test(ViewWaitingList::class, ['record' => $waitingList->id]) ->assertStatus(200); - // ->assertSee('Eligible Accounts') - // ->assertSee('Ineligible Accounts'); } public function test_admin_user_cant_add_student_without_permission() @@ -180,7 +177,7 @@ public function test_admin_can_add_manual_flag_to_waiting_list() $this->assertDatabaseHas('training_waiting_list_flags', [ 'list_id' => $waitingList->id, 'name' => 'My Test Flag', - 'endorsement_id' => null, + 'position_group_id' => null, ]); } @@ -209,7 +206,7 @@ public function test_admin_cant_add_duplicate_named_flag_in_list() public function test_admin_can_create_flag_with_linked_endorsement() { $waitingList = factory(WaitingList::class)->create(['department' => 'atc']); - $endorsement = factory(Endorsement::class)->create(); + $positionGroup = factory(PositionGroup::class)->create(); $this->adminUser->givePermissionTo('waiting-lists.view.atc'); $this->adminUser->givePermissionTo('waiting-lists.access'); @@ -219,14 +216,14 @@ public function test_admin_can_create_flag_with_linked_endorsement() Livewire::test(ViewWaitingList::class, ['record' => $waitingList->id]) ->callAction('add_flag', data: [ 'name' => 'My Test Flag', - 'endorsement_id' => $endorsement->id, + 'position_group_id' => $positionGroup->id, ]) ->assertHasNoActionErrors(); $this->assertDatabaseHas('training_waiting_list_flags', [ 'list_id' => $waitingList->id, 'name' => 'My Test Flag', - 'endorsement_id' => $endorsement->id, + 'position_group_id' => $positionGroup->id, ]); } @@ -254,7 +251,7 @@ public function test_can_view_account_in_waiting_list() $this->adminUser->givePermissionTo('waiting-lists.view.atc'); $this->adminUser->givePermissionTo('waiting-lists.access'); - Livewire::test(IneligibleAccountsRelationManager::class, ['ownerRecord' => $waitingList, 'pageClass' => ViewWaitingList::class]) + Livewire::test(AccountsRelationManager::class, ['ownerRecord' => $waitingList, 'pageClass' => ViewWaitingList::class]) ->assertCanSeeTableRecords([$waitingList->accounts()->first()]) ->assertTableActionVisible('view', record: $waitingList->accounts->first()); } @@ -271,7 +268,7 @@ public function test_cannot_edit_account_in_waiting_list_without_permission() $this->adminUser->givePermissionTo('waiting-lists.view.atc'); $this->adminUser->givePermissionTo('waiting-lists.access'); - Livewire::test(IneligibleAccountsRelationManager::class, ['ownerRecord' => $waitingList, 'pageClass' => ViewWaitingList::class]) + Livewire::test(AccountsRelationManager::class, ['ownerRecord' => $waitingList, 'pageClass' => ViewWaitingList::class]) ->assertCanSeeTableRecords([$waitingList->accounts()->first()]) ->assertTableActionHidden('edit', record: $waitingList->accounts->first()); } @@ -289,7 +286,7 @@ public function test_can_open_edit_action_with_permission() $this->adminUser->givePermissionTo('waiting-lists.access'); $this->adminUser->givePermissionTo('waiting-lists.update-accounts.*'); - Livewire::test(IneligibleAccountsRelationManager::class, ['ownerRecord' => $waitingList, 'pageClass' => ViewWaitingList::class]) + Livewire::test(AccountsRelationManager::class, ['ownerRecord' => $waitingList, 'pageClass' => ViewWaitingList::class]) ->assertCanSeeTableRecords([$waitingList->accounts()->first()]) ->assertTableActionVisible('edit', record: $waitingList->accounts->first()); } @@ -305,17 +302,9 @@ public function test_notes_can_be_added_to_waiting_list_account() $this->adminUser->givePermissionTo('waiting-lists.access'); $this->adminUser->givePermissionTo('waiting-lists.update-accounts.*'); - // assign status to waiting list account - $waitingList->accounts->find($account->id)->pivot->addStatus( - WaitingListStatus::find(WaitingListStatus::DEFAULT_STATUS) - ); - - Livewire::test(IneligibleAccountsRelationManager::class, ['ownerRecord' => $waitingList->refresh(), 'pageClass' => ViewWaitingList::class]) + Livewire::test(AccountsRelationManager::class, ['ownerRecord' => $waitingList->refresh(), 'pageClass' => ViewWaitingList::class]) ->assertCanSeeTableRecords([$waitingList->accounts()->first()]) ->mountTableAction(EditAction::class, record: $waitingList->accounts->first()) - ->assertTableActionDataSet([ - 'account_status' => WaitingListStatus::DEFAULT_STATUS, - ]) ->setTableActionData(data: ['notes' => 'test']) ->callMountedTableAction() ->assertHasNoTableActionErrors(); @@ -343,12 +332,7 @@ public function test_can_modify_manual_flag_to_true() $this->adminUser->givePermissionTo('waiting-lists.access'); $this->adminUser->givePermissionTo('waiting-lists.update-accounts.*'); - // assign status to waiting list account - $waitingList->accounts->find($account->id)->pivot->addStatus( - WaitingListStatus::find(WaitingListStatus::DEFAULT_STATUS) - ); - - Livewire::test(IneligibleAccountsRelationManager::class, ['ownerRecord' => $waitingList->refresh(), 'pageClass' => ViewWaitingList::class]) + Livewire::test(AccountsRelationManager::class, ['ownerRecord' => $waitingList->refresh(), 'pageClass' => ViewWaitingList::class]) ->assertCanSeeTableRecords([$waitingList->accounts()->first()]) ->mountTableAction(EditAction::class, record: $waitingList->accounts->first()) ->assertSee('Test Manual Flag') @@ -378,17 +362,12 @@ public function test_can_modify_manual_flag_to_false() $this->adminUser->givePermissionTo('waiting-lists.access'); $this->adminUser->givePermissionTo('waiting-lists.update-accounts.*'); - // assign status to waiting list account - $waitingList->accounts->find($account->id)->pivot->addStatus( - WaitingListStatus::find(WaitingListStatus::DEFAULT_STATUS) - ); - // set flag to true $waitingList->accounts->find($account->id)->pivot->flags()->sync($manualFlag->id, [ 'marked_at' => now(), ]); - Livewire::test(IneligibleAccountsRelationManager::class, ['ownerRecord' => $waitingList->refresh(), 'pageClass' => ViewWaitingList::class]) + Livewire::test(AccountsRelationManager::class, ['ownerRecord' => $waitingList->refresh(), 'pageClass' => ViewWaitingList::class]) ->assertCanSeeTableRecords([$waitingList->accounts()->first()]) ->mountTableAction('edit', record: $waitingList->accounts->first()) ->assertSee('Test Manual Flag') @@ -417,12 +396,7 @@ public function test_account_can_be_removed() $this->adminUser->givePermissionTo('waiting-lists.remove-accounts.*'); $this->adminUser->givePermissionTo('waiting-lists.access'); - // assign status to waiting list account - $waitingList->accounts->find($account->id)->pivot->addStatus( - WaitingListStatus::find(WaitingListStatus::DEFAULT_STATUS) - ); - - Livewire::test(IneligibleAccountsRelationManager::class, ['ownerRecord' => $waitingList->refresh(), 'pageClass' => ViewWaitingList::class]) + Livewire::test(AccountsRelationManager::class, ['ownerRecord' => $waitingList->refresh(), 'pageClass' => ViewWaitingList::class]) ->assertCanSeeTableRecords([$waitingList->accounts()->first()]) ->callTableAction('detach', record: $waitingList->accounts->first()); diff --git a/tests/Feature/Atc/AreaHourCheckTest.php b/tests/Feature/Atc/AreaHourCheckTest.php index 9ec2cae969..67af253997 100644 --- a/tests/Feature/Atc/AreaHourCheckTest.php +++ b/tests/Feature/Atc/AreaHourCheckTest.php @@ -2,7 +2,7 @@ namespace Tests\Feature\Atc; -use App\Models\Atc\Endorsement; +use App\Models\Atc\PositionGroup; use App\Models\Mship\Account; use App\Models\Mship\Qualification; use Illuminate\Foundation\Testing\DatabaseTransactions; @@ -31,8 +31,9 @@ public function testRedirectsAwayIfUserNotS3() /** @test */ public function testSuccessfulNavigationIfUserIsS3() { + $this->markTestSkipped('Page disabled.'); // create relevant endorsement. - factory(Endorsement::class)->create(['name' => 'LON_S_CTR']); + factory(PositionGroup::class)->create(['name' => 'LON_S_CTR']); $account = Account::factory()->create(); $qualification = Qualification::code('S3')->first(); diff --git a/tests/Feature/Site/HomePageTest.php b/tests/Feature/Site/HomePageTest.php index a6ceaefbb5..6384667d32 100644 --- a/tests/Feature/Site/HomePageTest.php +++ b/tests/Feature/Site/HomePageTest.php @@ -20,6 +20,7 @@ public function testItLoadsTheHomepage() /** @test */ public function testItShowsLiveAtcBookingsOnTheHomepage() { + $this->withoutExceptionHandling(); $booking = factory(Booking::class)->create([ 'date' => $this->knownDate->toDateString(), 'position' => 'EGKK_APP', diff --git a/tests/Feature/Training/WaitingListEligibilityPlumbingTest.php b/tests/Feature/Training/WaitingListEligibilityPlumbingTest.php index dc006c786e..bff122b101 100644 --- a/tests/Feature/Training/WaitingListEligibilityPlumbingTest.php +++ b/tests/Feature/Training/WaitingListEligibilityPlumbingTest.php @@ -7,7 +7,6 @@ use App\Jobs\Training\UpdateAccountWaitingListEligibility; use App\Models\Training\WaitingList; use App\Models\Training\WaitingList\WaitingListFlag; -use App\Models\Training\WaitingList\WaitingListStatus; use Illuminate\Foundation\Testing\DatabaseTransactions; use Illuminate\Support\Facades\Bus; use Illuminate\Support\Facades\Event; @@ -17,87 +16,6 @@ class WaitingListEligibilityPlumbingTest extends TestCase { use DatabaseTransactions; - public function test_checks_eligibility_after_adding_to_waiting_list() - { - Bus::fake(); - - $this->actingAs($this->privacc); - - $waitingList = factory(WaitingList::class)->create(); - $waitingList->addToWaitingList($this->user, $this->privacc); - - Bus::assertDispatched(UpdateAccountWaitingListEligibility::class, function ($job) { - return $job->account->id === $this->user->id; - }); - } - - public function test_checks_eligibility_after_marking_manual_flag() - { - Bus::fake(); - - $this->actingAs($this->privacc); - - Event::fakeFor(function () { - $waitingList = factory(WaitingList::class)->create(); - - $flag = factory(WaitingListFlag::class)->create([ - 'list_id' => $waitingList->id, - 'default_value' => false, - ]); - $waitingList->addFlag($flag); - $waitingList->addToWaitingList($this->user, $this->privacc); - - $waitingListAccount = $waitingList->fresh()->accounts->first()->pivot; - - // manually populate the status and flag as we are - // deliberately supressing the added to waiting list event - $waitingListAccount->addStatus(WaitingListStatus::find(WaitingListStatus::DEFAULT_STATUS)); - $waitingListAccount->addFlag($flag); - - $waitingListAccount->fresh()->markFlag($flag); - - Bus::assertDispatched(UpdateAccountWaitingListEligibility::class, function ($job) { - return $job->account->id === $this->user->id; - }); - }, [AccountAddedToWaitingList::class, FlagAddedToWaitingList::class]); - } - - public function test_checks_eligibility_after_unmarking_manual_flag() - { - Bus::fake(); - - $this->actingAs($this->privacc); - - Event::fakeFor(function () { - $waitingList = factory(WaitingList::class)->create(); - - $flag = factory(WaitingListFlag::class)->create([ - 'list_id' => $waitingList->id, - 'default_value' => false, - ]); - $waitingList->addFlag($flag); - $waitingList->addToWaitingList($this->user, $this->privacc); - - $waitingListAccount = $waitingList->fresh()->accounts->first()->pivot; - - // manually populate the status and flag as we are - // deliberately supressing the added to waiting list event - $waitingListAccount->addStatus(WaitingListStatus::find(WaitingListStatus::DEFAULT_STATUS)); - $waitingListAccount->addFlag($flag); - - // force a marked flag. - $waitingListAccount->flags()->get()->find($flag->id)->pivot->update([ - 'marked_at' => now(), - ]); - - $waitingListAccount->fresh()->unMarkFlag($flag); - - Bus::assertDispatched(UpdateAccountWaitingListEligibility::class, function ($job) { - return $job->account->id === $this->user->id; - }); - }, [AccountAddedToWaitingList::class, FlagAddedToWaitingList::class]); - } - public function test_checks_eligibility_when_command_run_for_user() { Event::fakeFor(function () { diff --git a/tests/Unit/Account/Endorsement/EndorsementRequestTest.php b/tests/Unit/Account/Endorsement/EndorsementRequestTest.php new file mode 100644 index 0000000000..26cd79c06d --- /dev/null +++ b/tests/Unit/Account/Endorsement/EndorsementRequestTest.php @@ -0,0 +1,77 @@ +create(); + $positionGroup = PositionGroup::factory()->create(); + + $endorsementRequest = EndorsementRequest::factory()->create([ + 'account_id' => $account->id, + 'endorsable_id' => $positionGroup->id, + 'endorsable_type' => PositionGroup::class, + 'requested_by' => $this->privacc->id, + ]); + + $this->assertInstanceOf(PositionGroup::class, $endorsementRequest->endorsable); + } + + public function test_can_be_associated_with_position() + { + $position = Position::factory()->create(); + + $endorsementRequest = EndorsementRequest::factory()->create([ + 'account_id' => $this->privacc->id, + 'endorsable_id' => $position->id, + 'endorsable_type' => Position::class, + 'requested_by' => $this->privacc->id, + ]); + + $this->assertInstanceOf(Position::class, $endorsementRequest->endorsable); + } + + public function test_can_be_associated_with_account() + { + $account = Account::factory()->create(); + $positionGroup = PositionGroup::factory()->create(); + + $endorsementRequest = EndorsementRequest::factory()->create([ + 'account_id' => $account->id, + 'endorsable_id' => $positionGroup->id, + 'endorsable_type' => PositionGroup::class, + 'requested_by' => $this->privacc->id, + ]); + + $this->assertInstanceOf(Account::class, $endorsementRequest->account); + } + + public function test_can_be_approved() + { + $this->actingAs($this->privacc); + + $account = Account::factory()->create(); + $positionGroup = PositionGroup::factory()->create(); + + $endorsementRequest = EndorsementRequest::factory()->create([ + 'account_id' => $account->id, + 'endorsable_id' => $positionGroup->id, + 'endorsable_type' => PositionGroup::class, + 'requested_by' => $this->privacc->id, + ]); + + $endorsementRequest->markApproved(); + + $this->assertNotNull($endorsementRequest->actioned_at); + $this->assertEquals($this->privacc->id, $endorsementRequest->actioned_by); + $this->assertEquals(EndorsementRequest::STATUS_APPROVED, $endorsementRequest->actioned_type); + } +} diff --git a/tests/Unit/Account/Endorsement/TemporaryEndorsementTimeframeTest.php b/tests/Unit/Account/Endorsement/TemporaryEndorsementTimeframeTest.php new file mode 100644 index 0000000000..3f177674c7 --- /dev/null +++ b/tests/Unit/Account/Endorsement/TemporaryEndorsementTimeframeTest.php @@ -0,0 +1,51 @@ +create(); + $account = Account::factory()->create(); + + // create a temporary endorsement that has expired + Endorsement::create([ + 'account_id' => $account->id, + 'endorsable_id' => $position->id, + 'endorsable_type' => Position::class, + 'created_at' => now()->subDays(8), + 'expires_at' => now()->subDays(1), + 'created_by' => $this->privacc->id, + ]); + + // create endorsement that is active to ensure it is included + Endorsement::create([ + 'account_id' => $account->id, + 'endorsable_id' => $position->id, + 'endorsable_type' => Position::class, + 'created_at' => now(), + 'expires_at' => now()->addDays(2), + 'created_by' => $this->privacc->id, + ]); + + $result = $account->daysSpentTemporarilyEndorsedOn($position); + + $this->assertEquals(8, $result); + } + + public function test_detects_when_no_days_on_position() + { + $position = Position::factory()->create(); + $account = Account::factory()->create(); + + $result = $account->daysSpentTemporarilyEndorsedOn($position); + + $this->assertEquals(0, $result); + } +} diff --git a/tests/Unit/AirfieldInformation/AirportTest.php b/tests/Unit/AirfieldInformation/AirportTest.php index e1eb7db3a6..ac83588eb3 100644 --- a/tests/Unit/AirfieldInformation/AirportTest.php +++ b/tests/Unit/AirfieldInformation/AirportTest.php @@ -6,7 +6,7 @@ use App\Models\Airport\Navaid; use App\Models\Airport\Procedure; use App\Models\Airport\Runway; -use App\Models\Station; +use App\Models\Atc\Position; use Illuminate\Foundation\Testing\DatabaseTransactions; use Tests\TestCase; @@ -62,16 +62,16 @@ public function itHasWorkingRunwaysRelationship() } /** @test */ - public function itHasWorkingStationsRelationship() + public function itHasWorkingPositionsRelationship() { $airport = factory(Airport::class)->create(); - $station1 = factory(Station::class)->create(); - $station2 = factory(Station::class)->create(); - $airport->stations()->attach([$station1->id, $station2->id]); + $station1 = Position::factory()->create(); + $station2 = Position::factory()->create(); + $airport->positions()->attach([$station1->id, $station2->id]); $airport = $airport->fresh(); - $this->assertInstanceOf(Station::class, $airport->stations->first()); - $this->assertCount(2, $airport->stations); + $this->assertInstanceOf(Position::class, $airport->positions->first()); + $this->assertCount(2, $airport->positions); } /** @test */ diff --git a/tests/Unit/AirfieldInformation/StationTest.php b/tests/Unit/AirfieldInformation/StationTest.php deleted file mode 100644 index b00411c8a8..0000000000 --- a/tests/Unit/AirfieldInformation/StationTest.php +++ /dev/null @@ -1,40 +0,0 @@ -create(); - $this->assertInstanceOf(Station::class, $station); - } - - /** @test */ - public function itHasWorkingAirportsRelationship() - { - $station = factory(Station::class)->create(); - $airport1 = factory(Airport::class)->create(); - $airport2 = factory(Airport::class)->create(); - $station->airports()->attach([$airport1->id, $airport2->id]); - $station = $station->fresh(); - - $this->assertInstanceOf(Airport::class, $station->airports->first()); - $this->assertCount(2, $station->airports); - } - - /** @test */ - public function itReturnsType() - { - $station = factory(Station::class)->create(['type' => Station::TYPE_APPROACH]); - $this->assertEquals('Approach/Radar', $station->type); - } -} diff --git a/tests/Unit/Atc/PositionGroupTest.php b/tests/Unit/Atc/PositionGroupTest.php new file mode 100644 index 0000000000..792dfaf1d9 --- /dev/null +++ b/tests/Unit/Atc/PositionGroupTest.php @@ -0,0 +1,85 @@ +create(); + $positionGroup = PositionGroup::factory()->create(); + Endorsement::factory()->create([ + 'account_id' => $account->id, + 'endorsable_type' => PositionGroup::class, + 'endorsable_id' => $positionGroup->id, + ]); + + $result = $positionGroup->unassignedFor($account); + + $this->assertFalse($result->contains($positionGroup)); + } + + public function test_detects_account_not_assigned_endorsement_for_group() + { + $account = Account::factory()->create(); + $positionGroup = PositionGroup::factory()->create(); + $otherPositionGroup = PositionGroup::factory()->create(); + + Endorsement::factory()->create([ + 'account_id' => $account->id, + 'endorsable_type' => PositionGroup::class, + 'endorsable_id' => $otherPositionGroup->id, + ]); + + $result = $positionGroup->unassignedFor($account); + + $this->assertTrue($result->contains($positionGroup)); + $this->assertFalse($result->contains($otherPositionGroup)); + } + + public function test_detects_as_not_assigned_when_solo_endorsement_expired() + { + $account = Account::factory()->create(); + $positionGroup = PositionGroup::factory()->create(); + $otherPositionGroup = PositionGroup::factory()->create(); + + Endorsement::factory()->create([ + 'account_id' => $account->id, + 'endorsable_type' => PositionGroup::class, + 'endorsable_id' => $otherPositionGroup->id, + 'expires_at' => now()->subDay(), + ]); + + $result = $positionGroup->unassignedFor($account); + + $this->assertFalse($result->contains($positionGroup)); + $this->assertFalse($result->contains($otherPositionGroup)); + } + + public function test_detects_when_active_solo_endorsement_assigned() + { + $account = Account::factory()->create(); + $positionGroup = PositionGroup::factory()->create(); + $otherPositionGroup = PositionGroup::factory()->create(); + + Endorsement::factory()->create([ + 'account_id' => $account->id, + 'endorsable_type' => PositionGroup::class, + 'endorsable_id' => $otherPositionGroup->id, + 'expires_at' => now()->addDay(), + ]); + + $result = $positionGroup->unassignedFor($account); + + $this->assertTrue($result->contains($positionGroup)); + $this->assertFalse($result->contains($otherPositionGroup)); + } +} diff --git a/tests/Unit/Atc/PositionTest.php b/tests/Unit/Atc/PositionTest.php new file mode 100644 index 0000000000..dc48c72dff --- /dev/null +++ b/tests/Unit/Atc/PositionTest.php @@ -0,0 +1,72 @@ +create(); + $this->assertInstanceOf(Position::class, $station); + } + + public function test_detects_as_temporarily_endorsable() + { + $position = Position::factory()->create([ + 'temporarily_endorsable' => true, + ]); + + $result = $position->isTemporarilyEndorsable(); + + $this->assertTrue($result); + } + + public function test_defaults_to_not_temporarily_endorsable() + { + $position = Position::factory()->create(); + + $result = $position->isTemporarilyEndorsable(); + + $this->assertFalse($result); + } + + public function test_can_have_airport_relationship() + { + $station = Position::factory()->create(); + $airport1 = factory(Airport::class)->create(); + $airport2 = factory(Airport::class)->create(); + $station->airports()->attach([$airport1->id, $airport2->id]); + $station = $station->fresh(); + + $this->assertInstanceOf(Airport::class, $station->airports->first()); + $this->assertCount(2, $station->airports); + } + + public function test_returns_type() + { + $station = Position::factory()->create(['type' => Position::TYPE_APPROACH]); + $this->assertEquals('Approach/Radar', $station->type); + } + + public function test_retrieves_only_temporarily_endorsable() + { + $position = Position::factory()->temporarilyEndorsable()->create(); + + $nonEndorsablePosition = Position::factory()->create([ + 'temporarily_endorsable' => false, + ]); + + $result = Position::temporarilyEndorsable()->get(); + + $this->assertTrue($result->contains($position)); + + $this->assertFalse($result->contains($nonEndorsablePosition)); + } +} diff --git a/tests/Unit/CTS/MentorRepositoryTest.php b/tests/Unit/CTS/MentorRepositoryTest.php index cf730a86dd..82d9e32022 100644 --- a/tests/Unit/CTS/MentorRepositoryTest.php +++ b/tests/Unit/CTS/MentorRepositoryTest.php @@ -26,7 +26,7 @@ protected function setUp(): void /** @test */ public function itCanReturnAListOfMentorsOfAnRts() { - $position = factory(Position::class)->create(['rts_id' => 15]); + $position = Position::factory()->create(['rts_id' => 15]); $positionValidation = factory(PositionValidation::class)->create([ 'status' => 5, @@ -41,7 +41,7 @@ public function itCanReturnAListOfMentorsOfAnRts() /** @test */ public function itDoesNotReturnMentorsOfAnotherRts() { - $position = factory(Position::class)->create(['rts_id' => 15]); + $position = Position::factory()->create(['rts_id' => 15]); factory(PositionValidation::class)->create([ 'status' => 5, @@ -58,8 +58,8 @@ public function itOnlyReturnsAMentorOnceWithinAnRts() { $member = factory(Member::class)->create(); - $positionOne = factory(Position::class)->create(['rts_id' => 15]); - $positionTwo = factory(Position::class)->create(['rts_id' => 15]); + $positionOne = Position::factory()->create(['rts_id' => 15]); + $positionTwo = Position::factory()->create(['rts_id' => 15]); factory(PositionValidation::class)->create([ 'member_id' => $member->id, @@ -82,7 +82,7 @@ public function itOnlyReturnsAMentorOnceWithinAnRts() public function itFormatsTheReturnDataForAnRtsCorrectly() { $member = factory(Member::class)->create(); - $position = factory(Position::class)->create(['rts_id' => 15]); + $position = Position::factory()->create(['rts_id' => 15]); factory(PositionValidation::class)->create([ 'member_id' => $member->id, @@ -98,7 +98,7 @@ public function itFormatsTheReturnDataForAnRtsCorrectly() /** @test */ public function itCanReturnAListOfMentorsOfAnAirport() { - $position = factory(Position::class)->create(['callsign' => 'EGKK_GND']); + $position = Position::factory()->create(['callsign' => 'EGKK_GND']); $positionValidation = factory(PositionValidation::class)->create([ 'status' => 5, @@ -113,7 +113,7 @@ public function itCanReturnAListOfMentorsOfAnAirport() /** @test */ public function itCanReturnAListOfMentorsOfASpecificCallsign() { - $position = factory(Position::class)->create(['callsign' => 'EGKK_GND']); + $position = Position::factory()->create(['callsign' => 'EGKK_GND']); $positionValidation = factory(PositionValidation::class)->create([ 'status' => 5, @@ -128,7 +128,7 @@ public function itCanReturnAListOfMentorsOfASpecificCallsign() /** @test */ public function itDoesNotReturnMentorsWithoutPermissionToMentorAPosition() { - $position = factory(Position::class)->create(['callsign' => 'EGKK_APP']); + $position = Position::factory()->create(['callsign' => 'EGKK_APP']); factory(PositionValidation::class)->create([ 'status' => 5, @@ -145,8 +145,8 @@ public function itOnlyReturnsAMentorOnceOnAirportOrCallsignSearches() { $member = factory(Member::class)->create(); - $positionOne = factory(Position::class)->create(['callsign' => 'EGKK_APP']); - $positionTwo = factory(Position::class)->create(['callsign' => 'EGKK_TWR']); + $positionOne = Position::factory()->create(['callsign' => 'EGKK_APP']); + $positionTwo = Position::factory()->create(['callsign' => 'EGKK_TWR']); factory(PositionValidation::class)->create([ 'member_id' => $member->id, @@ -171,7 +171,7 @@ public function itOnlyReturnsAMentorOnceOnAirportOrCallsignSearches() public function itFormatsTheReturnDataForAirportOrPositionSearchesCorrectly() { $member = factory(Member::class)->create(); - $position = factory(Position::class)->create(['callsign' => 'EGKK_APP']); + $position = Position::factory()->create(['callsign' => 'EGKK_APP']); factory(PositionValidation::class)->create([ 'member_id' => $member->id, diff --git a/tests/Unit/CTS/StudentRepositoryTest.php b/tests/Unit/CTS/StudentRepositoryTest.php index a56a881d24..63d4784105 100644 --- a/tests/Unit/CTS/StudentRepositoryTest.php +++ b/tests/Unit/CTS/StudentRepositoryTest.php @@ -26,7 +26,7 @@ protected function setUp(): void /** @test */ public function itCanReturnAListOfStudentsOfAnRts() { - $position = factory(Position::class)->create(['rts_id' => 15]); + $position = Position::factory()->create(['rts_id' => 15]); $positionValidation = factory(PositionValidation::class)->create([ 'status' => 1, @@ -41,7 +41,7 @@ public function itCanReturnAListOfStudentsOfAnRts() /** @test */ public function itDoesNotReturnStudentsOfAnotherRts() { - $position = factory(Position::class)->create(['rts_id' => 15]); + $position = Position::factory()->create(['rts_id' => 15]); factory(PositionValidation::class)->create([ 'status' => 1, @@ -58,8 +58,8 @@ public function itOnlyReturnsAStudentsOnceWithinAnRts() { $member = factory(Member::class)->create(); - $positionOne = factory(Position::class)->create(['rts_id' => 15]); - $positionTwo = factory(Position::class)->create(['rts_id' => 15]); + $positionOne = Position::factory()->create(['rts_id' => 15]); + $positionTwo = Position::factory()->create(['rts_id' => 15]); factory(PositionValidation::class)->create([ 'member_id' => $member->id, @@ -82,7 +82,7 @@ public function itOnlyReturnsAStudentsOnceWithinAnRts() public function itFormatsTheReturnDataForAnRtsCorrectly() { $member = factory(Member::class)->create(); - $position = factory(Position::class)->create(['rts_id' => 15]); + $position = Position::factory()->create(['rts_id' => 15]); factory(PositionValidation::class)->create([ 'member_id' => $member->id, @@ -98,7 +98,7 @@ public function itFormatsTheReturnDataForAnRtsCorrectly() /** @test */ public function itDoesNotReturnMentorsAsStudents() { - $position = factory(Position::class)->create(['rts_id' => 15]); + $position = Position::factory()->create(['rts_id' => 15]); factory(PositionValidation::class)->create([ 'status' => 5, diff --git a/tests/Unit/Endorsements/ConditionModelTest.php b/tests/Unit/Endorsements/ConditionModelTest.php index 3caa13a28b..77a2c3c4f1 100644 --- a/tests/Unit/Endorsements/ConditionModelTest.php +++ b/tests/Unit/Endorsements/ConditionModelTest.php @@ -2,8 +2,8 @@ namespace Tests\Unit\Endorsements; -use App\Models\Atc\Endorsement; -use App\Models\Atc\Endorsement\Condition; +use App\Models\Atc\PositionGroup; +use App\Models\Atc\PositionGroupCondition; use App\Models\Mship\Qualification; use App\Models\NetworkData\Atc; use Carbon\Carbon; @@ -20,26 +20,26 @@ protected function setUp(): void { parent::setUp(); - $this->condition = factory(Condition::class)->create(); + $this->condition = factory(PositionGroupCondition::class)->create(); } /** @test */ public function itCanBeCreated() { - $condition = Condition::create([ - 'endorsement_id' => 100, + $condition = PositionGroupCondition::create([ + 'position_group_id' => 100, 'positions' => ['EGLL_TWR', 'EGPH_%'], 'required_hours' => 10, - 'type' => Condition::TYPE_SUM_OF_AIRFIELDS, + 'type' => PositionGroupCondition::TYPE_SUM_OF_AIRFIELDS, 'within_months' => null, ]); - $this->assertDatabaseHas('endorsement_conditions', [ + $this->assertDatabaseHas('position_group_conditions', [ 'id' => $condition->id, - 'endorsement_id' => 100, + 'position_group_id' => 100, 'positions' => json_encode(['EGLL_TWR', 'EGPH_%']), 'required_hours' => 10, - 'type' => Condition::TYPE_SUM_OF_AIRFIELDS, + 'type' => PositionGroupCondition::TYPE_SUM_OF_AIRFIELDS, 'within_months' => null, ]); } @@ -63,20 +63,20 @@ public function itReturnsAListOfHumanPositions() /** @test */ public function itCanBeAssociatedWithAEndorsement() { - $endorsement = factory(Endorsement::class)->create(); - $condition = factory(Condition::class)->make(['endorsement_id' => null]); + $positionGroup = factory(PositionGroup::class)->create(); + $condition = factory(PositionGroupCondition::class)->make(['position_group_id' => null]); - $this->assertNull($condition->endorsement); - $condition->endorsement()->associate($endorsement); + $this->assertNull($condition->positionGroup); + $condition->positionGroup()->associate($positionGroup); $condition->save(); - $this->assertEquals($endorsement->id, $condition->fresh()->endorsement->id); + $this->assertEquals($positionGroup->id, $condition->fresh()->positionGroup->id); } /** @test */ public function itCorrectlyReportsProgress() { - $condition = factory(Endorsement\Condition::class)->make(['positions' => ['EGLL_%', 'ESSEX_APP'], 'required_hours' => 10, 'within_months' => 2, 'type' => Endorsement\Condition::TYPE_ON_SINGLE_AIRFIELD]); + $condition = factory(PositionGroupCondition::class)->make(['positions' => ['EGLL_%', 'ESSEX_APP'], 'required_hours' => 10, 'within_months' => 2, 'type' => PositionGroupCondition::TYPE_ON_SINGLE_AIRFIELD]); factory(Atc::class)->create([ 'account_id' => $this->user->id, @@ -119,7 +119,7 @@ public function itCorrectlyReportsProgress() /** @test */ public function itCorrectlyReportsMetAndProgressForSingleAirfield() { - $condition = factory(Endorsement\Condition::class)->create(['positions' => ['EGLL_%', 'ESSEX_APP'], 'required_hours' => 10, 'within_months' => 2, 'type' => Endorsement\Condition::TYPE_ON_SINGLE_AIRFIELD]); + $condition = factory(PositionGroupCondition::class)->create(['positions' => ['EGLL_%', 'ESSEX_APP'], 'required_hours' => 10, 'within_months' => 2, 'type' => PositionGroupCondition::TYPE_ON_SINGLE_AIRFIELD]); factory(Atc::class)->create([ 'account_id' => $this->user->id, @@ -148,7 +148,7 @@ public function itCorrectlyReportsMetAndProgressForSingleAirfield() /** @test */ public function itCorrectlyReportsMetAndProgressForSum() { - $condition = factory(Endorsement\Condition::class)->create(['positions' => ['EGLL_%', 'ESSEX_APP'], 'required_hours' => 10, 'within_months' => 2, 'type' => Endorsement\Condition::TYPE_SUM_OF_AIRFIELDS]); + $condition = factory(PositionGroupCondition::class)->create(['positions' => ['EGLL_%', 'ESSEX_APP'], 'required_hours' => 10, 'within_months' => 2, 'type' => PositionGroupCondition::TYPE_SUM_OF_AIRFIELDS]); factory(Atc::class)->create([ 'account_id' => $this->user->id, @@ -179,11 +179,11 @@ public function itCorrectlyChecksForQualificationWhenPresent() { $requiredQualification = Qualification::code('S3')->get()->first()->id; - $condition = factory(Endorsement\Condition::class)->create([ + $condition = factory(PositionGroupCondition::class)->create([ 'positions' => ['ESSEX_APP'], 'required_hours' => 10, 'within_months' => 2, - 'type' => Endorsement\Condition::TYPE_ON_SINGLE_AIRFIELD, + 'type' => PositionGroupCondition::TYPE_ON_SINGLE_AIRFIELD, 'required_qualification' => $requiredQualification, ]); diff --git a/tests/Unit/Endorsements/EndorsementModelTest.php b/tests/Unit/Endorsements/PositionGroupModelTest.php similarity index 52% rename from tests/Unit/Endorsements/EndorsementModelTest.php rename to tests/Unit/Endorsements/PositionGroupModelTest.php index e022706c99..09a2e8fa0e 100644 --- a/tests/Unit/Endorsements/EndorsementModelTest.php +++ b/tests/Unit/Endorsements/PositionGroupModelTest.php @@ -2,50 +2,50 @@ namespace Tests\Unit\Endorsements; -use App\Models\Atc\Endorsement; +use App\Models\Atc\PositionGroup; +use App\Models\Atc\PositionGroupCondition; use App\Models\NetworkData\Atc; -use Carbon\Carbon; use Illuminate\Foundation\Testing\DatabaseTransactions; use Illuminate\Support\Facades\Cache; use Tests\TestCase; -class EndorsementModelTest extends TestCase +class PositionGroupModelTest extends TestCase { use DatabaseTransactions; - private $endorsement; + private $positionGroup; protected function setUp(): void { parent::setUp(); - $this->endorsement = factory(Endorsement::class)->create(); + $this->positionGroup = factory(PositionGroup::class)->create(); } /** @test */ public function itCanBeCreated() { - $endorsement = Endorsement::create([ + $positionGroup = PositionGroup::create([ 'name' => 'My First Endorsement', ]); - $this->assertDatabaseHas('endorsements', ['id' => $endorsement->id, 'name' => $endorsement->name]); + $this->assertDatabaseHas('position_groups', ['id' => $positionGroup->id, 'name' => $positionGroup->name]); } /** @test */ public function itCanBeAssociatedWithACondition() { - $condition = factory(Endorsement\Condition::class)->make(['endorsement_id' => null]); + $condition = factory(PositionGroupCondition::class)->make(['position_group_id' => null]); - $this->assertCount(0, $this->endorsement->fresh()->conditions); - $this->endorsement->conditions()->save($condition); - $this->assertEquals($this->endorsement->id, $condition->fresh()->endorsement_id); - $this->assertEquals($this->endorsement->fresh()->conditions()->first()->id, $condition->id); - $this->assertCount(1, $this->endorsement->fresh()->conditions); + $this->assertCount(0, $this->positionGroup->fresh()->conditions); + $this->positionGroup->conditions()->save($condition); + $this->assertEquals($this->positionGroup->id, $condition->fresh()->position_group_id); + $this->assertEquals($this->positionGroup->fresh()->conditions()->first()->id, $condition->id); + $this->assertCount(1, $this->positionGroup->fresh()->conditions); - $condition = factory(Endorsement\Condition::class)->make(['endorsement_id' => null]); - $this->endorsement->conditions()->save($condition); - $this->assertCount(2, $this->endorsement->fresh()->conditions); + $condition = factory(PositionGroupCondition::class)->make(['position_group_id' => null]); + $this->positionGroup->conditions()->save($condition); + $this->assertCount(2, $this->positionGroup->fresh()->conditions); } /** @test */ @@ -60,7 +60,7 @@ public function itWillReportConditionsMetWhenAllAreMetForSingleAirfield() 'minutes_online' => 60, ]); - $this->assertTrue($this->endorsement->fresh()->conditionsMetForUser($this->user)); + $this->assertTrue($this->positionGroup->fresh()->conditionsMetForUser($this->user)); } /** @test */ @@ -69,13 +69,13 @@ public function itWillReportConditionsNotMetWhenNetworkDataNotPresentForSingleAi $this->createMockCondition(); // network data not present for the specified position - $this->assertFalse($this->endorsement->fresh()->conditionsMetForUser($this->user)); + $this->assertFalse($this->positionGroup->fresh()->conditionsMetForUser($this->user)); } /** @test */ public function itWillReportConditionsMetForMultipleAirfieldsWhenNetworkDataPresent() { - $this->createMockCondition(['EGKK_%', 'EGLL_%'], Endorsement\Condition::TYPE_SUM_OF_AIRFIELDS); + $this->createMockCondition(['EGKK_%', 'EGLL_%'], PositionGroupCondition::TYPE_SUM_OF_AIRFIELDS); // create the network data factory(Atc::class)->create([ @@ -90,13 +90,13 @@ public function itWillReportConditionsMetForMultipleAirfieldsWhenNetworkDataPres ]); // should return true as it sums up to the 60 mins required in the mock condition. - $this->assertTrue($this->endorsement->fresh()->conditionsMetForUser($this->user)); + $this->assertTrue($this->positionGroup->fresh()->conditionsMetForUser($this->user)); } /** @test */ public function itWillReportConditionsNotMetForMultipleAirfieldsWhenRequiredTimeIsntMet() { - $this->createMockCondition(['EGKK_%', 'EGLL_%'], Endorsement\Condition::TYPE_SUM_OF_AIRFIELDS); + $this->createMockCondition(['EGKK_%', 'EGLL_%'], PositionGroupCondition::TYPE_SUM_OF_AIRFIELDS); // create the network data factory(Atc::class)->create([ @@ -110,7 +110,7 @@ public function itWillReportConditionsNotMetForMultipleAirfieldsWhenRequiredTime 'minutes_online' => 10, ]); - $this->assertFalse($this->endorsement->fresh()->conditionsMetForUser($this->user)); + $this->assertFalse($this->positionGroup->fresh()->conditionsMetForUser($this->user)); } /** @test */ @@ -127,40 +127,15 @@ public function itShouldReturnValueFromCacheDuringConditionTest() ->andReturn(true); // true assertion based upon the return value of the mocked cache facade above. - $this->assertTrue($this->endorsement->fresh()->conditionsMetForUser($this->user)); + $this->assertTrue($this->positionGroup->fresh()->conditionsMetForUser($this->user)); } - /** @test */ - public function itFlushesUserEndorsementCacheAfterATCSession() - { - $this->createMockCondition(); - - $spy = Cache::spy(); - - $this->assertFalse($this->endorsement->fresh()->conditionsMetForUser($this->user)); - - $spy->shouldHaveReceived('put') - ->once(); - - $atc = factory(Atc::class)->create([ - 'account_id' => $this->user->id, - 'callsign' => 'EGKK_TWR', - 'connected_at' => Carbon::now()->subHours(2), - ]); - $atc->disconnectAt(Carbon::now()); - - $spy->shouldHaveReceived('forget') - ->times(Endorsement::count()); - - $this->assertTrue($this->endorsement->fresh()->conditionsMetForUser($this->user)); - } - - private function createMockCondition($positions = ['EGKK_%'], $type = Endorsement\Condition::TYPE_ON_SINGLE_AIRFIELD) + private function createMockCondition($positions = ['EGKK_%'], $type = PositionGroupCondition::TYPE_ON_SINGLE_AIRFIELD) { // create condition requiring an hour on a EGKK_TWR - return factory(Endorsement\Condition::class)->create( + return factory(PositionGroupCondition::class)->create( [ - 'endorsement_id' => $this->endorsement->id, + 'position_group_id' => $this->positionGroup->id, 'required_hours' => 1, 'within_months' => null, 'type' => $type, diff --git a/tests/Unit/Training/WaitingList/WaitingListAccountTest.php b/tests/Unit/Training/WaitingList/WaitingListAccountTest.php index 822f284a50..a71b83f888 100644 --- a/tests/Unit/Training/WaitingList/WaitingListAccountTest.php +++ b/tests/Unit/Training/WaitingList/WaitingListAccountTest.php @@ -3,12 +3,8 @@ namespace Tests\Unit\Training\WaitingList; use App\Models\Mship\Account; -use App\Models\NetworkData\Atc; -use App\Models\Training\WaitingList; -use App\Models\Training\WaitingList\WaitingListStatus; use Carbon\Carbon; use Illuminate\Foundation\Testing\DatabaseTransactions; -use Illuminate\Support\Facades\Cache; use Tests\TestCase; class WaitingListAccountTest extends TestCase @@ -26,183 +22,6 @@ protected function setUp(): void $this->actingAs($this->privacc); } - /** @test * */ - public function itCanHaveAStatusAssociatedWithIt() - { - $status = factory(WaitingListStatus::class)->create(); - - $account = Account::factory()->create(); - - $waitingListAccount = $this->waitingList->addToWaitingList($account, $this->privacc); - - $this->waitingList->accounts->first()->pivot->addStatus($status); - - $this->assertDatabaseHas('training_waiting_list_account_status', [ - 'status_id' => $status->id, - 'start_at' => now()->toDateTimeString(), - ]); - } - - /** @test * */ - public function itRemovedOldStatusesOnAdd() - { - $status = factory(WaitingListStatus::class)->create(); - - $secondStatus = factory(WaitingListStatus::class)->create(); - - $account = Account::factory()->create(); - - $this->waitingList->addToWaitingList($account, $this->privacc); - - $this->waitingList->accounts->find($account->id)->pivot->addStatus($status); - - // adding a new status should mean the first status is marked as ended. - $this->waitingList->fresh()->accounts->find($account->id)->pivot->addStatus($secondStatus); - - $this->assertDatabaseHas('training_waiting_list_account_status', [ - 'status_id' => $status->id, - 'end_at' => now(), - ]); - - $this->assertDatabaseHas('training_waiting_list_account_status', [ - 'status_id' => $secondStatus->id, - 'start_at' => now(), - 'end_at' => null, - ]); - } - - /** @test * */ - public function itChecksFor12HourRequirement() - { - $account = Account::factory()->create(); - - factory(Atc::class)->create( - [ - 'account_id' => $account->id, - 'minutes_online' => 721, - 'disconnected_at' => now(), - ]); - - $this->waitingList->addToWaitingList($account, $this->privacc); - - $this->assertTrue($this->waitingList->accounts->find($account->id)->pivot->atcHourCheck()); - } - - /** @test */ - public function itDetectsWhen12HourRequirementHaveNotBeenMet() - { - $account = Account::factory()->create(); - - factory(Atc::class)->create( - [ - 'account_id' => $account->id, - 'minutes_online' => 20, - 'disconnected_at' => now(), - ]); - - $this->waitingList->addToWaitingList($account, $this->privacc); - - $this->assertFalse($this->waitingList->accounts->find($account->id)->pivot->atcHourCheck()); - } - - /** @test */ - public function itChecksForMultipleSessionsIn12HourRequirement() - { - $account = Account::factory()->create(); - - factory(Atc::class, 12)->create( - [ - 'account_id' => $account->id, - 'minutes_online' => 60, - 'disconnected_at' => now(), - ]); - - $this->waitingList->addToWaitingList($account, $this->privacc); - - $this->assertTrue($this->waitingList->accounts->find($account->id)->pivot->atcHourCheck()); - } - - /** @test */ - public function itDisregardsNonUKControllingSessionsForHourCheck() - { - $account = Account::factory()->create(); - - // 12 sessions of an hour each to satisfy the requirement, but with non-uk callsign - factory(Atc::class, 12)->create( - [ - 'account_id' => $account->id, - 'minutes_online' => 60, - 'disconnected_at' => now(), 'callsign' => 'LFPG_APP', - ]); - - $this->waitingList->addToWaitingList($account, $this->privacc); - - $this->assertFalse($this->waitingList->accounts->find($account->id)->pivot->atcHourCheck()); - } - - /** @test */ - public function itDisregardsSessionsGreaterThan3MonthsAgo() - { - $account = Account::factory()->create(); - - // 12 sessions of an hour satisfy the hour requirement, but not the date range - // subtracting 3 months and a day satisfies a boundary condition - factory(Atc::class, 12) - ->create( - [ - 'account_id' => $account->id, 'minutes_online' => 60, - 'disconnected_at' => now()->subMonth(3)->subDay(1), - ]); - - $this->waitingList->addToWaitingList($account, $this->privacc); - - $this->assertFalse($this->waitingList->accounts->find($account->id)->pivot->atcHourCheck()); - } - - /** @test */ - public function itHandlesExactly3MonthsAgoForAtcHourCheckCorrectly() - { - $account = Account::factory()->create(); - - // 12 sessions of an hour which occurred within the 3 month range - factory(Atc::class, 12) - ->create( - [ - 'account_id' => $account->id, 'minutes_online' => 60, - 'disconnected_at' => now()->subMonth(3), - ]); - - $this->waitingList->addToWaitingList($account, $this->privacc); - - $this->assertTrue($this->waitingList->accounts->find($account->id)->pivot->atcHourCheck()); - } - - /** @test */ - public function itHandlesJustShortOfThe12HourRequirement() - { - $account = Account::factory()->create(); - - // 11 sessions of an hour which occurred within the 3 month range - factory(Atc::class, 11) - ->create( - [ - 'account_id' => $account->id, 'minutes_online' => 60, - 'disconnected_at' => now(), - ]); - - // last session brings the total to 11 hours 59 minutes of controlling time. - factory(Atc::class, 1) - ->create( - [ - 'account_id' => $account->id, 'minutes_online' => 59, - 'disconnected_at' => now(), - ]); - - $this->waitingList->addToWaitingList($account, $this->privacc); - - $this->assertFalse($this->waitingList->accounts->find($account->id)->pivot->atcHourCheck()); - } - /** @test */ public function itCanHaveNotesAdded() { @@ -218,76 +37,6 @@ public function itCanHaveNotesAdded() $this->assertEquals('This is a note', $waitingListAccount->notes); } - /** @test */ - public function itCachesHourRequirementFlagWhenNotMetAndNoKeyExists() - { - $ttlDay = 86400; - $account = Account::factory()->create(); - - $this->waitingList->addToWaitingList($account, $this->privacc); - - // grab the pivot model - $waitingListAccount = $this->waitingList->accounts->find($account->id)->pivot; - - Cache::shouldReceive('has') - ->once() - ->andReturn(false); - - Cache::shouldReceive('put') - ->once() - ->with("waiting-list-account:{$waitingListAccount->id}:recentAtcMins", null, $ttlDay); - - $this->assertFalse($waitingListAccount->atcHourCheck()); - } - - /** @test */ - public function itCachesHourRequirementWheMetAndKeyDoesntExist() - { - $ttlDay = 86400; - $account = Account::factory()->create(); - - $this->waitingList->addToWaitingList($account, $this->privacc); - - factory(Atc::class)->create( - [ - 'account_id' => $account->id, - 'minutes_online' => 721, - 'disconnected_at' => now(), - ]); - - // grab the pivot model - $waitingListAccount = $this->waitingList->accounts->find($account->id)->pivot; - - Cache::shouldReceive('has') - ->once() - ->andReturn(false); - - Cache::shouldReceive('put') - ->once() - ->with("waiting-list-account:{$waitingListAccount->id}:recentAtcMins", 721, $ttlDay); - - $this->assertTrue($waitingListAccount->atcHourCheck()); - } - - /** @test */ - public function itShouldReturnTheValueIfExists() - { - $account = Account::factory()->create(); - $this->waitingList->addToWaitingList($account, $this->privacc); - - $waitingListAccount = $this->waitingList->accounts->find($account->id)->pivot; - - Cache::shouldReceive('has') - ->once() - ->andReturn(true); - - Cache::shouldReceive('get') - ->once() - ->andReturn(false); - - $this->assertFalse($waitingListAccount->atcHourCheck()); - } - /** @test */ public function itShouldDefaultCreatedAtToNowIfNotProvided() { @@ -314,28 +63,4 @@ public function itShouldSetCreatedAtToGivenDateIfProvided() 'created_at' => $date, ]); } - - /** @test */ - public function itShouldPassHourCheckIfPilotWaitingList() - { - $pilotList = factory(WaitingList::class)->create(['department' => 'pilot']); - $account = Account::factory()->create(); - - $pilotList->addToWaitingList($account, $this->privacc); - - $this->assertTrue($pilotList->accounts()->find($account)->pivot->atcHourCheck()); - } - - /** @test */ - public function itShouldPassEligibilityIfAtcHourCheckFeatureTogglejDisabled() - { - $this->waitingList->feature_toggles = ['check_atc_hours' => false]; - $this->waitingList->save(); - - $account = Account::factory()->create(); - - $this->waitingList->addToWaitingList($account, $this->privacc); - - $this->assertTrue($this->waitingList->accounts->find($account->id)->pivot->atcHourCheck()); - } } diff --git a/tests/Unit/Training/WaitingList/WaitingListCheckEligibilityServiceTest.php b/tests/Unit/Training/WaitingList/WaitingListCheckEligibilityServiceTest.php deleted file mode 100644 index 9b0e4a070d..0000000000 --- a/tests/Unit/Training/WaitingList/WaitingListCheckEligibilityServiceTest.php +++ /dev/null @@ -1,226 +0,0 @@ -waitingList = factory(WaitingList::class)->create(); - - $this->actingAs($this->privacc); - } - - public function test_returns_false_if_basic_12_hour_check_fails() - { - factory(Atc::class)->create([ - 'account_id' => $this->user->id, - 'minutes_online' => 60, - 'disconnected_at' => now(), - ]); - - $result = (new CheckWaitingListEligibility($this->user))->checkBaseControllingHours($this->waitingList); - - $this->assertFalse($result); - } - - public function test_returns_true_if_basic_12_hour_check_passes() - { - factory(Atc::class)->create([ - 'account_id' => $this->user->id, - 'minutes_online' => 721, - 'disconnected_at' => now(), - ]); - - $result = (new CheckWaitingListEligibility($this->user))->checkBaseControllingHours($this->waitingList); - - $this->assertTrue($result); - } - - public function test_returns_true_when_waiting_list_has_no_flags() - { - $waitingList = factory(WaitingList::class)->create(); - $waitingList->addToWaitingList($this->user, $this->privacc); - - $result = (new CheckWaitingListEligibility($this->user))->checkWaitingListFlags($waitingList->fresh()); - - $this->assertTrue($result['overall']); - $this->assertNull($result['summary']); - } - - public function test_passes_checks_when_manual_flag_is_true_in_all_flags_config() - { - $waitingList = factory(WaitingList::class)->create(); - $waitingList->addToWaitingList($this->user, $this->privacc); - - $flag = factory(WaitingListFlag::class)->create([ - 'name' => 'manual', - 'list_id' => $waitingList->id, - 'default_value' => false, - ]); - $waitingList->addFlag($flag); - $waitingList->accounts()->first()->pivot->markFlag($flag); - - $result = (new CheckWaitingListEligibility($this->user))->checkWaitingListFlags($waitingList->fresh()); - - $this->assertTrue($result['overall']); - $this->assertEquals($result['summary'], [$flag->name => true]); - } - - public function test_fails_checks_when_manual_flag_is_false_in_all_flags_config() - { - $waitingList = factory(WaitingList::class)->create(); - $waitingList->addToWaitingList($this->user, $this->privacc); - - $flag = $this->createFlag('manual', $waitingList, false); - - $result = (new CheckWaitingListEligibility($this->user))->checkWaitingListFlags($waitingList); - - $this->assertFalse($result['overall']); - $this->assertEquals($result['summary'], [$flag->name => false]); - } - - public function test_fails_check_when_endorsement_linked_flag_is_false() - { - $waitingList = factory(WaitingList::class)->create(); - $waitingList->addToWaitingList($this->user, $this->privacc); - - factory(Atc::class)->create(['account_id' => $this->user->id, 'callsign' => 'EGGD_APP', 'minutes_online' => 35]); - $condition = factory(Condition::class)->create(['required_hours' => 1, 'positions' => ['EGGD_APP']]); - - $flag = factory(WaitingListFlag::class)->create([ - 'name' => 'endorsement', - 'list_id' => $waitingList->id, - 'default_value' => false, - 'endorsement_id' => $condition->endorsement->id, - ]); - $waitingList->addFlag($flag); - $waitingList->fresh(); - - $result = (new CheckWaitingListEligibility($this->user))->checkWaitingListFlags($waitingList->fresh()); - - $this->assertFalse($result['overall']); - $this->assertEquals($result['summary'], [$flag->name => false]); - } - - public function test_pass_check_when_endorsement_linked_flag_is_true() - { - $waitingList = factory(WaitingList::class)->create(); - $waitingList->addToWaitingList($this->user, $this->privacc); - - factory(Atc::class)->create(['account_id' => $this->user->id, 'callsign' => 'EGGD_APP', 'minutes_online' => 65]); - $condition = factory(Condition::class)->create(['required_hours' => 1, 'positions' => ['EGGD_APP']]); - - $flag = factory(WaitingListFlag::class)->create([ - 'name' => 'endorsement', - 'list_id' => $waitingList->id, - 'default_value' => false, - 'endorsement_id' => $condition->endorsement->id, - ]); - $waitingList->addFlag($flag); - $waitingList->fresh(); - - $result = (new CheckWaitingListEligibility($this->user))->checkWaitingListFlags($waitingList->fresh()); - - $this->assertTrue($result['overall']); - $this->assertEquals($result['summary'], [$flag->name => true]); - } - - public function test_pass_check_on_any_with_failing_and_passing_flags() - { - $waitingList = factory(WaitingList::class)->create(); - $waitingList->flags_check = WaitingList::ANY_FLAGS; - $waitingList->save(); - $waitingList->addToWaitingList($this->user, $this->privacc); - - $flag1 = $this->createFlag('manual', $waitingList, false); - $flag2 = $this->createFlag('endorsement', $waitingList, false); - - factory(Atc::class)->create(['account_id' => $this->user->id, 'callsign' => 'EGGD_APP', 'minutes_online' => 65]); - $condition = factory(Condition::class)->create(['required_hours' => 1, 'positions' => ['EGGD_APP']]); - - $flag3 = factory(WaitingListFlag::class)->create([ - 'name' => 'endorsement_2', - 'list_id' => $waitingList->id, - 'endorsement_id' => $condition->endorsement->id, - ]); - $waitingList->addFlag($flag3); - $waitingList->fresh(); - - $result = (new CheckWaitingListEligibility($this->user))->checkWaitingListFlags($waitingList->fresh()); - - $this->assertTrue($result['overall']); - $this->assertEquals($result['summary'], [$flag1->name => false, $flag2->name => false, $flag3->name => true]); - } - - public function test_pass_check_on_all_with_all_passing_automated_flags() - { - $waitingList = factory(WaitingList::class)->create(); - $waitingList->addToWaitingList($this->user, $this->privacc); - - factory(Atc::class)->create(['account_id' => $this->user->id, 'callsign' => 'EGGD_APP', 'minutes_online' => 65]); - factory(Atc::class)->create(['account_id' => $this->user->id, 'callsign' => 'EGNX_APP', 'minutes_online' => 65]); - $condition = factory(Condition::class)->create(['required_hours' => 1, 'positions' => ['EGGD_APP']]); - $conditionSecond = factory(Condition::class)->create(['required_hours' => 1, 'positions' => ['EGNX_APP']]); - - $flag1 = factory(WaitingListFlag::class)->create([ - 'name' => 'endorsement', - 'list_id' => $waitingList->id, - 'default_value' => false, - 'endorsement_id' => $condition->endorsement->id, - ]); - $flag2 = factory(WaitingListFlag::class)->create([ - 'name' => 'endorsement', - 'list_id' => $waitingList->id, - 'default_value' => false, - 'endorsement_id' => $conditionSecond->endorsement->id, - ]); - $waitingList->addFlag($flag1); - $waitingList->addFlag($flag2); - $waitingList->fresh(); - - $result = (new CheckWaitingListEligibility($this->user))->checkWaitingListFlags($waitingList->fresh(), 'all'); - - $this->assertTrue($result['overall']); - $this->assertEquals($result['summary'], [$flag1->name => true, $flag2->name => true]); - } - - public function test_hour_check_is_true_when_feature_toggle_is_false() - { - $waitingList = factory(WaitingList::class)->create(); - $waitingList->feature_toggles = ['check_atc_hours' => false]; - - $waitingList->addToWaitingList($this->user, $this->privacc); - - $service = new CheckWaitingListEligibility($this->user); - - $this->assertTrue($service->checkBaseControllingHours($waitingList)); - } - - private function createFlag($name, $waitingList, $defaultValue = false) - { - $flag = factory(WaitingListFlag::class)->create([ - 'name' => $name, - 'list_id' => $waitingList->id, - 'default_value' => $defaultValue, - ]); - $waitingList->addFlag($flag); - $waitingList->fresh(); - - return $flag; - } -} diff --git a/tests/Unit/Training/WaitingList/WaitingListCheckFlagsServiceTest.php b/tests/Unit/Training/WaitingList/WaitingListCheckFlagsServiceTest.php new file mode 100644 index 0000000000..f378a97e28 --- /dev/null +++ b/tests/Unit/Training/WaitingList/WaitingListCheckFlagsServiceTest.php @@ -0,0 +1,145 @@ +waitingList = factory(WaitingList::class)->create(); + + $this->actingAs($this->privacc); + } + + public function test_passes_checks_when_manual_flag_is_true_in_all_flags_config() + { + $waitingList = factory(WaitingList::class)->create(); + $waitingList->addToWaitingList($this->user, $this->privacc); + + $flag = factory(WaitingListFlag::class)->create([ + 'name' => 'manual', + 'list_id' => $waitingList->id, + 'default_value' => false, + ]); + $waitingList->addFlag($flag); + $waitingList->accounts()->first()->pivot->markFlag($flag); + + $result = (new CheckWaitingListFlags($this->user))->checkWaitingListFlags($waitingList->fresh()); + + $this->assertEquals([$flag->name => true], $result['summary']); + } + + public function test_fails_checks_when_manual_flag_is_false_in_all_flags_config() + { + $waitingList = factory(WaitingList::class)->create(); + $waitingList->addToWaitingList($this->user, $this->privacc); + + $flag = $this->createFlag('manual', $waitingList, false); + + $result = (new CheckWaitingListFlags($this->user))->checkWaitingListFlags($waitingList); + + $this->assertEquals([$flag->name => false], $result['summary']); + } + + public function test_fails_check_when_endorsement_linked_flag_is_false() + { + $waitingList = factory(WaitingList::class)->create(); + $waitingList->addToWaitingList($this->user, $this->privacc); + + factory(Atc::class)->create(['account_id' => $this->user->id, 'callsign' => 'EGGD_APP', 'minutes_online' => 35]); + $condition = factory(PositionGroupCondition::class)->create(['required_hours' => 1, 'positions' => ['EGGD_APP']]); + + $flag = factory(WaitingListFlag::class)->create([ + 'name' => 'endorsement', + 'list_id' => $waitingList->id, + 'default_value' => false, + 'position_group_id' => $condition->positionGroup->id, + ]); + $waitingList->addFlag($flag); + $waitingList->fresh(); + + $result = (new CheckWaitingListFlags($this->user))->checkWaitingListFlags($waitingList->fresh()); + + $this->assertEquals([$flag->name => false], $result['summary']); + } + + public function test_pass_check_when_endorsement_linked_flag_is_true() + { + $waitingList = factory(WaitingList::class)->create(); + $waitingList->addToWaitingList($this->user, $this->privacc); + + factory(Atc::class)->create(['account_id' => $this->user->id, 'callsign' => 'EGGD_APP', 'minutes_online' => 65]); + $condition = factory(PositionGroupCondition::class)->create(['required_hours' => 1, 'positions' => ['EGGD_APP']]); + + $flag = factory(WaitingListFlag::class)->create([ + 'name' => 'endorsement', + 'list_id' => $waitingList->id, + 'default_value' => false, + 'position_group_id' => $condition->positionGroup->id, + ]); + $waitingList->addFlag($flag); + $waitingList->fresh(); + + $result = (new CheckWaitingListFlags($this->user))->checkWaitingListFlags($waitingList->fresh()); + + $this->assertEquals([$flag->name => true], $result['summary']); + } + + public function test_pass_check_on_all_with_all_passing_automated_flags() + { + $waitingList = factory(WaitingList::class)->create(); + $waitingList->addToWaitingList($this->user, $this->privacc); + + factory(Atc::class)->create(['account_id' => $this->user->id, 'callsign' => 'EGGD_APP', 'minutes_online' => 65]); + factory(Atc::class)->create(['account_id' => $this->user->id, 'callsign' => 'EGNX_APP', 'minutes_online' => 65]); + $condition = factory(PositionGroupCondition::class)->create(['required_hours' => 1, 'positions' => ['EGGD_APP']]); + $conditionSecond = factory(PositionGroupCondition::class)->create(['required_hours' => 1, 'positions' => ['EGNX_APP']]); + + $flag1 = factory(WaitingListFlag::class)->create([ + 'name' => 'endorsement', + 'list_id' => $waitingList->id, + 'default_value' => false, + 'position_group_id' => $condition->positionGroup->id, + ]); + $flag2 = factory(WaitingListFlag::class)->create([ + 'name' => 'endorsement', + 'list_id' => $waitingList->id, + 'default_value' => false, + 'position_group_id' => $conditionSecond->positionGroup->id, + ]); + $waitingList->addFlag($flag1); + $waitingList->addFlag($flag2); + $waitingList->fresh(); + + $result = (new CheckWaitingListFlags($this->user))->checkWaitingListFlags($waitingList->fresh(), 'all'); + + $this->assertEquals($result['summary'], [$flag1->name => true, $flag2->name => true]); + } + + private function createFlag($name, $waitingList, $defaultValue = false) + { + $flag = factory(WaitingListFlag::class)->create([ + 'name' => $name, + 'list_id' => $waitingList->id, + 'default_value' => $defaultValue, + ]); + $waitingList->addFlag($flag); + $waitingList->fresh(); + + return $flag; + } +} diff --git a/tests/Unit/Training/WaitingList/WaitingListFlagTest.php b/tests/Unit/Training/WaitingList/WaitingListFlagTest.php index d183704855..9902196882 100644 --- a/tests/Unit/Training/WaitingList/WaitingListFlagTest.php +++ b/tests/Unit/Training/WaitingList/WaitingListFlagTest.php @@ -2,17 +2,12 @@ namespace Tests\Unit\Training\WaitingList; -use App\Events\NetworkData\AtcSessionEnded; use App\Events\Training\AccountAddedToWaitingList; use App\Listeners\Training\WaitingList\AssignFlags; -use App\Models\Atc\Endorsement; -use App\Models\Atc\Endorsement\Condition; +use App\Models\Atc\PositionGroup; use App\Models\Mship\Account; -use App\Models\NetworkData\Atc; use App\Models\Training\WaitingList; use App\Models\Training\WaitingList\WaitingListFlag; -use App\Models\Training\WaitingList\WaitingListStatus; -use App\Services\Training\AddToWaitingList; use Illuminate\Foundation\Testing\DatabaseTransactions; use Tests\TestCase; @@ -26,8 +21,8 @@ class WaitingListFlagTest extends TestCase /** @var WaitingList */ private $waitingList; - /** @var Endorsement */ - private $endorsement; + /** @var PositionGroup */ + private $positionGroup; protected function setUp(): void { @@ -41,7 +36,7 @@ protected function setUp(): void $this->waitingList->addFlag($this->flag); $this->waitingList->addToWaitingList($this->privacc, $this->privacc); - $this->endorsement = factory(Endorsement::class)->create(); + $this->positionGroup = factory(PositionGroup::class)->create(); } @@ -91,34 +86,6 @@ public function itCantBeUnMarkedWhenAlreadyUnMarked() $this->assertFalse($waitingListAccount->flags()->first()->pivot->value); } - /** @test */ - public function itCanBeRelatedToAnEndorsement() - { - $this->assertNull($this->flag->endorsement); - $this->flag->update(['endorsement_id' => $this->endorsement->id]); - - $this->assertNotNull($this->flag->fresh()->endorsement); - } - - /** @test */ - public function itDetectsTrueValueForFlagWithEndorsement() - { - $account = Account::factory()->create(); - // populate network data - factory(Atc::class)->create(['account_id' => $account->id, 'callsign' => 'EGGD_APP', 'minutes_online' => 61]); - $condition = factory(Condition::class)->create(['required_hours' => 1, 'positions' => ['EGGD_APP']]); - - $flag = factory(WaitingListFlag::class)->create(['endorsement_id' => $condition->endorsement->id, 'list_id' => $this->waitingList->id]); - - // add to the waiting list - handleService(new AddToWaitingList($this->waitingList, $account, $this->privacc)); - - // find the pivot - $waitingListAccount = $this->waitingList->accounts()->find($account->id)->pivot; - - $this->assertTrue($waitingListAccount->flags()->find($flag->id)->pivot->value); - } - /** @test */ public function itAssignsDefaultFlagsOnAddingAccountToList() { @@ -154,94 +121,4 @@ public function itIsPropagatedToExistingAccountsWhenAFlagIsAdded() $this->assertTrue($account->pivot->flags->contains($flag)); })); } - - /** @test */ - public function itDetectsWhetherAllTheFlagsAreTrue() - { - $account = Account::factory()->create(); - // null list represents a flag which hasn't yet been assigned to list. - // Normal flow wouldn't have this, but needs to emulate the action - // all of the flags below have a default value of false. - $flag = factory(WaitingListFlag::class)->create(['list_id' => null, 'default_value' => false]); - - $this->waitingList->addToWaitingList($account, $this->privacc); - $this->waitingList->addFlag($flag); - - // finds the account in the waiting list and then marks the flags - $waitingListAccount = $this->waitingList->accounts()->findOrFail($account->id)->pivot; - $waitingListAccount->markFlag($flag); - - $this->assertTrue($waitingListAccount->fresh()->allFlagsChecker()); - } - - /** @test */ - public function itDetectsWhetherAnAccountShouldBeInTheActiveBucket() - { - $account = Account::factory()->create(); - // null list represents a flag which hasn't yet been assigned to list. - // Normal flow wouldn't have this, but needs to emulate the action - // all of the flags below have a default value of false. - $flag = factory(WaitingListFlag::class)->create(['list_id' => null, 'default_value' => false]); - - // find the 'active' status - $status = WaitingListStatus::find(WaitingListStatus::DEFAULT_STATUS); - - // an account is automatically allocated an active status (vital to know in the context of this test) - $this->waitingList->addToWaitingList($account, $this->privacc); - $this->waitingList->addFlag($flag); - - // finds the account in the waiting list - $waitingListAccount = $this->waitingList->accounts()->findOrFail($account->id)->pivot; - // then marks the flags - $waitingListAccount->markFlag($flag); - // and sets an active status - $waitingListAccount->addStatus($status); - - // creates an atc session to simulate the 12 hour requirement stipulated within the model - $atcSession = factory(Atc::class)->create(['account_id' => $account->id, 'minutes_online' => 721, 'disconnected_at' => now()]); - - // mock end of network session - event(new AtcSessionEnded($atcSession)); - - $this->assertTrue($waitingListAccount->fresh()->eligible); - } - - /** @test */ - public function itShouldCheckWithOnlyOneFlagWhenAnyDefinedAsWaitingList() - { - $account = Account::factory()->create(); - $anyCheckerWaitingList = factory(WaitingList::class)->create(['flags_check' => WaitingList::ANY_FLAGS]); - - // create an atc session which will pass the 12 hour check hardcoded against a waiting list flags - factory(Atc::class)->create( - [ - 'account_id' => $account->id, - 'minutes_online' => 721, - 'disconnected_at' => now(), - ]); - - $anyCheckerWaitingList->addToWaitingList($account, $this->privacc); - - // create an ATC session and condition which pass - factory(Atc::class)->create(['account_id' => $account->id, 'callsign' => 'EGGD_APP', 'minutes_online' => 61]); - $condition = factory(Condition::class)->create(['required_hours' => 1, 'positions' => ['EGGD_APP']]); - - // create an endorsement condition which would not pass. - $conditionNotMet = factory(Condition::class)->create(['required_hours' => 100, 'positions' => ['EGXX_APP']]); - - // create the two flags with a linked endorsement. Only one should be met, but that is acceptable for an 'ANY' check. - $flag = factory(WaitingListFlag::class)->create(['endorsement_id' => $condition->endorsement->id, 'list_id' => null, 'default_value' => false]); - $unmetFlag = factory(WaitingListFlag::class)->create(['endorsement_id' => $conditionNotMet->endorsement->id, 'list_id' => null, 'default_value' => false]); - - $anyCheckerWaitingList->addFlag($flag); - $anyCheckerWaitingList->addFlag($unmetFlag); - - $waitingListAccount = $anyCheckerWaitingList->fresh()->accounts->find($account->id)->pivot; - // find the 'active' status - $status = WaitingListStatus::find(WaitingListStatus::DEFAULT_STATUS); - // and sets an active status - $waitingListAccount->addStatus($status); - - $this->assertTrue($waitingListAccount->fresh()->eligible); - } } diff --git a/tests/Unit/Training/WaitingList/WaitingListServiceTest.php b/tests/Unit/Training/WaitingList/WaitingListServiceTest.php deleted file mode 100644 index 56165e4ad1..0000000000 --- a/tests/Unit/Training/WaitingList/WaitingListServiceTest.php +++ /dev/null @@ -1,56 +0,0 @@ -waitingList = factory(WaitingList::class)->create(); - - $this->actingAs($this->privacc); - } - - /** @test **/ - public function itAddsAccountToWaitingListWithDefaultStatus() - { - $account = Account::factory()->create(); - - handleService(new AddToWaitingList($this->waitingList, $account->first(), $this->privacc)); - - $this->assertEquals(1, $this->waitingList->fresh()->accounts->count()); - - $this->assertEquals(1, $this->waitingList->fresh()->accounts->first()->pivot->status->count()); - } - - /** @test **/ - public function itHandlesAStudentBeingAddedAfterRemoval() - { - $account = Account::factory()->create(); - - handleService(new AddToWaitingList($this->waitingList, $account->fresh(), $this->privacc)); - - $this->assertEquals(1, $this->waitingList->accounts()->count()); - - $this->waitingList->removeFromWaitingList($account->fresh()); - - $this->assertEquals(0, $this->waitingList->fresh()->accounts()->count()); - - handleService(new AddToWaitingList($this->waitingList, $account, $this->privacc)); - - $this->assertEquals(1, $this->waitingList->fresh()->accounts()->count()); - } -} diff --git a/tests/Unit/Training/WaitingList/WaitingListStatusTest.php b/tests/Unit/Training/WaitingList/WaitingListStatusTest.php deleted file mode 100644 index 69e46c76f9..0000000000 --- a/tests/Unit/Training/WaitingList/WaitingListStatusTest.php +++ /dev/null @@ -1,48 +0,0 @@ -waitingListStatus = factory(WaitingListStatus::class)->states(['default'])->create(); - - $this->actingAs($this->privacc); - } - - /** @test * */ - public function itDefaultsToActive() - { - $this->assertEquals(1, $this->waitingListStatus->default()->id); - } - - /** @test * */ - public function itHasListenerToAssignDefaultStatus() - { - $account = Account::factory()->create(); - $waitingList = factory(WaitingList::class)->create(); - $waitingList->addToWaitingList($account, $this->privacc); - - event(new AccountAddedToWaitingList($account, $waitingList, $this->privacc)); - - $waitingListAccount = $waitingList::find($waitingList->id)->accounts->where('id', $account->id)->first()->pivot->status; - - $this->assertDatabaseHas('training_waiting_list_account_status', [ - 'waiting_list_account_id' => $waitingListAccount->first()->pivot->waiting_list_account_id, 'status_id' => 1, - ]); - } -} diff --git a/tests/Unit/Training/WaitingList/WaitingListTest.php b/tests/Unit/Training/WaitingList/WaitingListTest.php index 57736d1bd1..dfffd4501c 100644 --- a/tests/Unit/Training/WaitingList/WaitingListTest.php +++ b/tests/Unit/Training/WaitingList/WaitingListTest.php @@ -70,30 +70,6 @@ public function itCanHaveStudents() ['account_id' => $account->id, 'list_id' => $this->waitingList->id]); } - /** @test * */ - public function itCanHaveEligibleAccounts() - { - $eligible_account = Account::factory()->create(); - $uneligible_account = Account::factory()->create(); - $this->waitingList->department = WaitingList::PILOT_DEPARTMENT; - $this->waitingList->save(); - - $flag = factory(WaitingListFlag::class)->create(['default_value' => false]); - $this->waitingList->addFlag($flag); - $this->waitingList->addToWaitingList($eligible_account, $this->privacc); - $this->waitingList->addToWaitingList($uneligible_account, $this->privacc); - - $this->waitingList->accounts()->find($eligible_account)->pivot->markFlag($flag); - - $this->waitingList = $this->waitingList->fresh(); - - $this->assertCount(1, $this->waitingList->accountsByEligibility()); - $this->assertEquals($eligible_account->id, $this->waitingList->accountsByEligibility()->first()->id); - - $this->assertCount(1, $this->waitingList->accountsByEligibility(false)); - $this->assertEquals($uneligible_account->id, $this->waitingList->accountsByEligibility(false)->first()->id); - } - /** @test * */ public function itCanFindAccountPosition() { @@ -105,13 +81,8 @@ public function itCanFindAccountPosition() $this->waitingList->department = WaitingList::PILOT_DEPARTMENT; $this->waitingList->save(); - factory(WaitingList\WaitingListStatus::class)->state('default')->create(); $flag = $this->waitingList->addFlag(factory(WaitingListFlag::class)->create(['default_value' => false])); - // Add an ineligible user - $ineligible_user = Account::factory()->create(); - $this->waitingList->addToWaitingList($ineligible_user, $this->privacc); - // Add to list foreach ($accounts as $i => $account) { $this->waitingList->addToWaitingList($account, $this->privacc, $accounts_added_at[$i]); @@ -121,7 +92,6 @@ public function itCanFindAccountPosition() $this->waitingList = $this->waitingList->fresh(); $this->assertNull($this->waitingList->accountPosition(Account::factory()->create())); // A user not in the list should return null - $this->assertNull($this->waitingList->accountPosition($ineligible_user)); // A user not eligible should return null $this->assertEquals(1, $this->waitingList->accountPosition($accounts[0])); // First user is oldest, should be number 1 $this->assertEquals(3, $this->waitingList->accountPosition($accounts[1])); // Second user is newest, should be number 3 $this->assertEquals(2, $this->waitingList->accountPosition($accounts[2])); diff --git a/tests/Unit/Training/WaitingList/WaitingListTestHelper.php b/tests/Unit/Training/WaitingList/WaitingListTestHelper.php index da55c3d07c..477ad78285 100644 --- a/tests/Unit/Training/WaitingList/WaitingListTestHelper.php +++ b/tests/Unit/Training/WaitingList/WaitingListTestHelper.php @@ -4,7 +4,6 @@ use App\Models\Mship\Account; use App\Models\Training\WaitingList; -use App\Models\Training\WaitingList\WaitingListStatus; trait WaitingListTestHelper { @@ -26,11 +25,6 @@ protected function createPopulatedList(array $overrides = [], $accounts = 5) return $waitingList->fresh(); } - protected function createStatus() - { - return factory(WaitingListStatus::class)->create(); - } - private function createAdminAccount() { return Account::factory()->create(); diff --git a/tests/Unit/Training/WaitingList/WaitingListWriteEligibilityTest.php b/tests/Unit/Training/WaitingList/WaitingListWriteEligibilityTest.php index 0c87ef0491..a28b8f9196 100644 --- a/tests/Unit/Training/WaitingList/WaitingListWriteEligibilityTest.php +++ b/tests/Unit/Training/WaitingList/WaitingListWriteEligibilityTest.php @@ -5,9 +5,8 @@ use App\Models\NetworkData\Atc; use App\Models\Training\WaitingList; use App\Models\Training\WaitingList\WaitingListFlag; -use App\Models\Training\WaitingList\WaitingListStatus; -use App\Services\Training\CheckWaitingListEligibility; -use App\Services\Training\WriteWaitingListEligibility; +use App\Services\Training\CheckWaitingListFlags; +use App\Services\Training\WriteWaitingListFlagSummary; use Illuminate\Foundation\Testing\DatabaseTransactions; use Tests\TestCase; @@ -26,22 +25,6 @@ public function setUp(): void $this->actingAs($this->privacc); } - /** @test */ - public function itShouldWriteEligibilityFalseToWaitingListAccount() - { - $this->waitingList->addToWaitingList($this->user, $this->privacc); - $this->waitingList->refresh(); - - $waitingListAccount = $this->waitingList->accounts()->where('account_id', $this->user->id)->first()->pivot; - $waitingListAccount->addStatus(WaitingListStatus::find(WaitingListStatus::DEFAULT_STATUS)); - - $checkEligibility = new CheckWaitingListEligibility($this->user); - - WriteWaitingListEligibility::handle($this->waitingList, $checkEligibility); - - $this->assertFalse($waitingListAccount->fresh()->eligible); - } - /** @test */ public function itShouldWriteEligibilityTrueToWaitingListAccountWithNoFlags() { @@ -49,7 +32,6 @@ public function itShouldWriteEligibilityTrueToWaitingListAccountWithNoFlags() $this->waitingList->refresh(); $waitingListAccount = $this->waitingList->accounts()->where('account_id', $this->user->id)->first()->pivot; - $waitingListAccount->addStatus(WaitingListStatus::find(WaitingListStatus::DEFAULT_STATUS)); factory(Atc::class)->create([ 'account_id' => $this->user->id, @@ -57,26 +39,12 @@ public function itShouldWriteEligibilityTrueToWaitingListAccountWithNoFlags() 'disconnected_at' => now(), ]); - $checkEligibility = new CheckWaitingListEligibility($this->user); - - WriteWaitingListEligibility::handle($this->waitingList, $checkEligibility); + $checkEligibility = new CheckWaitingListFlags($this->user); - $this->assertTrue($waitingListAccount->fresh()->eligible); - - $this->assertEquals($waitingListAccount->fresh()->eligibility_summary, - [ - 'base_controlling_hours' => true, - 'flags' => [ - 'overall' => true, - 'summary' => null, - ], - 'account_status' => true, - ] - ); + WriteWaitingListFlagSummary::handle($this->waitingList, $checkEligibility); $this->assertEquals($waitingListAccount->fresh()->flags_status_summary, [ - 'overall' => true, 'summary' => null, ] ); @@ -92,7 +60,6 @@ public function itShouldWriteEligibilityWithPassingManualFlags() $this->waitingList->refresh(); $waitingListAccount = $this->waitingList->accounts()->where('account_id', $this->user->id)->first()->pivot; - $waitingListAccount->addStatus(WaitingListStatus::find(WaitingListStatus::DEFAULT_STATUS)); $waitingListAccount->markFlag($flag); factory(Atc::class)->create([ @@ -101,28 +68,12 @@ public function itShouldWriteEligibilityWithPassingManualFlags() 'disconnected_at' => now(), ]); - $checkEligibility = new CheckWaitingListEligibility($this->user); - - WriteWaitingListEligibility::handle($this->waitingList, $checkEligibility); + $checkEligibility = new CheckWaitingListFlags($this->user); - $this->assertTrue($waitingListAccount->fresh()->eligible); - - $this->assertEquals($waitingListAccount->fresh()->eligibility_summary, - [ - 'base_controlling_hours' => true, - 'flags' => [ - 'overall' => true, - 'summary' => [ - $flag->name => true, - ], - ], - 'account_status' => true, - ] - ); + WriteWaitingListFlagSummary::handle($this->waitingList, $checkEligibility); $this->assertEquals($waitingListAccount->fresh()->flags_status_summary, [ - 'overall' => true, 'summary' => [ $flag->name => true, ], diff --git a/vite.config.js b/vite.config.js index e2e871b70a..2c3c9cd3cc 100644 --- a/vite.config.js +++ b/vite.config.js @@ -7,6 +7,7 @@ export default defineConfig({ input: [ 'resources/assets/less/admin.less', 'resources/assets/sass/app.scss', + 'resources/assets/css/tailwind.css', 'resources/assets/js/app.js', 'resources/assets/sass/home.scss', 'resources/assets/js/home.js',
      Frequency
      @if(!$station->sub_station) - {{$station->callsign}} + @if(!$positions->sub_station) + {{$positions->callsign}} @else - {{$station->callsign}} + {{$positions->callsign}} @endif {{$station->name}}{{$station->frequency}}{{$positions->name}}{{$positions->frequency}}