From d354a0f692b070993c675ff8bad8bab533775c70 Mon Sep 17 00:00:00 2001 From: Max Brokman Date: Wed, 1 May 2024 20:16:39 +0100 Subject: [PATCH] feat: Heathrow s1 page (#3592) Add page detailing requirements and progress towards those requirements for a heathrow s1 endorsement --- .../Controllers/Atc/EndorsementController.php | 48 ++++ resources/views/components/nav.blade.php | 1 + .../endorsements/gatwick_ground.blade.php | 36 ++- .../endorsements/heathrow_ground_s1.blade.php | 153 +++++++++++++ .../views/site/atc/endorsements.blade.php | 50 ++++- routes/web-main.php | 1 + .../Feature/Atc/HeathrowS1EndorsementTest.php | 211 ++++++++++++++++++ 7 files changed, 484 insertions(+), 16 deletions(-) create mode 100644 resources/views/controllers/endorsements/heathrow_ground_s1.blade.php create mode 100644 tests/Feature/Atc/HeathrowS1EndorsementTest.php diff --git a/app/Http/Controllers/Atc/EndorsementController.php b/app/Http/Controllers/Atc/EndorsementController.php index b9b83deb6b..301a921bb4 100644 --- a/app/Http/Controllers/Atc/EndorsementController.php +++ b/app/Http/Controllers/Atc/EndorsementController.php @@ -13,6 +13,8 @@ class EndorsementController extends BaseController { const GATWICK_HOURS_REQUIREMENT = 50; + const HEATHROW_S1_HOURS_REQUIREMENT = 50; + public function getGatwickGroundIndex() { if (! $this->account->fully_defined || ! $this->account->qualification_atc->isS1) { @@ -43,6 +45,52 @@ public function getGatwickGroundIndex() ->with('conditionsMet', $hoursMet && $onRoster); } + public function getHeathrowGroundS1Index() + { + if (! $this->account->fully_defined || ! $this->account->qualification_atc->isS1) { + return Redirect::route('mship.manage.dashboard') + ->withError('Only S1 rated controllers are eligible for a Heathrow Ground (S1) endorsement.'); + } + + // active on roster + $onRoster = Roster::where('account_id', $this->account->id)->exists(); + + $egkkEndorsement = $this->account->endorsements()->where(function (Builder $builder) { + $builder->whereHasMorph('endorsable', [PositionGroup::class], function (Builder $builder) { + $builder->where('name', 'EGKK_GND'); + }); + })->first(); + + $hasEgkkEndorsement = (bool) $egkkEndorsement; + + $minutesOnline = 0.0; + + // 50 hours on EGKK_GND or EGKK_DEL + // AFTER getting an EGKK endorsement + if ($hasEgkkEndorsement) { + $minutesOnline = $this->account->networkDataAtc() + ->isUK() + ->where('callsign', 'LIKE', 'EGKK_%') + ->where(function (Builder $builder) { + $builder->where('facility_type', Atc::TYPE_GND) + ->orWhere('facility_type', Atc::TYPE_DEL); + }) + ->where('connected_at', '>=', $egkkEndorsement->created_at) + ->sum('minutes_online'); + } + + $totalHours = $minutesOnline / 60; + $hoursMet = $totalHours >= self::HEATHROW_S1_HOURS_REQUIREMENT; + + return $this->viewMake('controllers.endorsements.heathrow_ground_s1') + ->with('totalHours', $totalHours) + ->with('progress', ($totalHours / self::HEATHROW_S1_HOURS_REQUIREMENT) * 100) + ->with('hoursMet', $hoursMet) + ->with('onRoster', $onRoster) + ->with('hasEgkkEndorsement', $hasEgkkEndorsement) + ->with('conditionsMet', $hoursMet && $onRoster); + } + public function getAreaIndex() { return Redirect::route('mship.manage.dashboard') diff --git a/resources/views/components/nav.blade.php b/resources/views/components/nav.blade.php index 67699bb699..4e20953641 100644 --- a/resources/views/components/nav.blade.php +++ b/resources/views/components/nav.blade.php @@ -77,6 +77,7 @@
  • {!! link_to_route("controllers.endorsements.gatwick_ground", "Gatwick Ground") !!}
  • +
  • {!! link_to_route("controllers.endorsements.heathrow_ground_s1", "Heathrow Ground (S1)") !!}
  • {!! link_to_route("site.atc.heathrow", "Heathrow") !!}
  • diff --git a/resources/views/controllers/endorsements/gatwick_ground.blade.php b/resources/views/controllers/endorsements/gatwick_ground.blade.php index 8127a78e8e..5b72a54300 100644 --- a/resources/views/controllers/endorsements/gatwick_ground.blade.php +++ b/resources/views/controllers/endorsements/gatwick_ground.blade.php @@ -5,22 +5,36 @@
    -
      Gatwick Endorsement
    +
      Gatwick Endorsement (S1)

    - Gatwick is one of the busiest airports on the VATSIM network. Before controlling it, we want to - ensure you have the knowledge you need to provide a good service to pilots and get the most from - your controlling session. + Gatwick is one of the busiest airports on the VATSIM Network. +

    +

    + Before controlling at Gatwick, we want to ensure you have the knowledge you need + to provide a good service to pilots and get the most from your controlling session.

    -

    Step One

    - In order to control Gatwick Ground as an S1, you will need to first meet - the requirements outlined on this page. + S1 rated controllers must hold a Gatwick Endorsement in order to control EGKK_GND and EGKK_DEL.

    +

    Step One

    - You must be a home member of the UK, be active on the controller roster, be rated as an S1 - and have controlled for 50 hours on UK GMC or GMP positions. + In order to begin training for your endorsement you must meet the following requirements:

    +
      +
    • + You must be a home member of the UK +
    • +
    • + You must hold an S1 rating +
    • +
    • + You must be active on the controller roster +
    • +
    • + You must have controlled for 50 hours at other UK aerodromes after acquiring your S1 rating. +
    • +

    Step Two

    You will be given access to the 'Gatwick ADC | S1 Endorsement' course. This Moodle course covers @@ -43,7 +57,9 @@

    This is not a test and you will not pass or fail, rather it is an opportunity for you to practically - apply the skills and knowledge which you have learned through completing the Moodle course.
    + apply the skills and knowledge which you have learned through completing the Moodle course. +

    +

    You will do this until the mentor deems you ready for the Gatwick ground endorsement. Once granted the endorsement, you will be able to control EGKK_GND and EGKK_DEL on the live network without supervision. diff --git a/resources/views/controllers/endorsements/heathrow_ground_s1.blade.php b/resources/views/controllers/endorsements/heathrow_ground_s1.blade.php new file mode 100644 index 0000000000..0f8d18ce6a --- /dev/null +++ b/resources/views/controllers/endorsements/heathrow_ground_s1.blade.php @@ -0,0 +1,153 @@ +@extends('layout') + +@section('content') + +

    +
    +
    +
      Heathrow Ground Endorsement (S1)
    +
    +

    + Heathrow is one of the busiest airports on the VATSIM Network. +

    +

    + Before controlling at Heathrow, we want to ensure you have the knowledge you need + to provide a good service to pilots and get the most from your controlling session. +

    +

    + S1 rated controllers must hold a Heathrow Endorsement in order to control EGLL_GND and ELL_DEL positions. +

    +

    Step One

    +

    + In order to control Heathrow Ground as an S1, you will need to meet + the following requirements: +

    +
      +
    • + You must be a home member of the UK +
    • +
    • + You must hold an S1 rating +
    • +
    • + You must be active on the controller roster +
    • +
    • + You must hold a Gatwick Endorsement +
    • +
    • + You must have controlled for 50 hours at Gatwick after acquiring your endorsement +
    • +
    +

    Step Two

    +

    + You will be added to the waiting list for Heathrow training, and be + given access to the 'Heathrow (S1) GMC' course. This Moodle course covers + Heathrow specific procedures, radiotelephony, and local flight planning restrictions. +

    +

    + Once you are close to the top of the waiting list you will be given access to to the + Moodle exam. +

    +

    + If you do not pass the quiz on your first attempt, there is a study period of 72 hours for you to + review the Moodle course and improve your knowledge before you try again. +

    +

    + When you have passed the quiz at the end of the Moodle course, you will be prompted to submit + another ticket to ATC TRAINING. +

    +

    Step Three

    +

    + Begin training toward your Heathrow Ground endorsement. +

    +
    +
    +
    +
    + +
    +
    +
    +
      Membership Status
    +
    + @if($_account->primary_state?->isDivision) +

    You are a home member of the UK.

    + @else +

    You are not a home member of the UK Division. If you wish to hold + a Gatwick endorsement, apply to transfer to the UK + by {!! link_to_route("visiting.landing", "clicking here") !!}.

    + @endif + + @if($onRoster) +

    You are active on the controller roster.

    + @else +

    You are not active on the controller roster. If you wish to hold a + Gatwick endorsement you must be active on the roster.

    + @endif + + @if($hasEgkkEndorsement) +

    You are endorsed to control Gatwick.

    + @else +

    You do not hold a Gatwick endorsement, + you must complete this before + starting your Heathrow training.

    + @endif +
    +
    +
    + +
    +
    +
    50 Hours Controlling at Gatwick
    +
    +
    + @if($hoursMet) +
    + 50+ Hrs +
    + @endif +
    + {{ (round($totalHours,2)) .' Hrs' }} +
    +
    +
    +
    +
    +
    + + +
    + +
    +
    +
      Request Moodle Course
    +
    + @if($conditionsMet) +

    + Open a ticket with ATC Training + to request access to the Moodle Course

    + @else + + @endif +
    +
    +
    +
    +@stop + +@section('scripts') + +@stop diff --git a/resources/views/site/atc/endorsements.blade.php b/resources/views/site/atc/endorsements.blade.php index 61f9cf1097..f59f267b48 100644 --- a/resources/views/site/atc/endorsements.blade.php +++ b/resources/views/site/atc/endorsements.blade.php @@ -38,11 +38,9 @@

    Background

    - London Gatwick (EGKK) was the 2nd busiest airport on the VATSIM network in 2022 with over 84,000 - movements. Controlling at London Gatwick is restricted by the ATC Training Department to S2 rated - members, or S1s that hold a special endorsement. This restriction for S1s is in place to allow - members to gain experience in quieter environments and practice their skills before tackling - the workload at Gatwick. + Controlling at London Gatwick is restricted to S2 rated members, or S1s that hold a special endorsement. + This restriction for S1s is in place to allow members to gain experience in quieter environments + and practice their skills before tackling the workload at Gatwick.

    @@ -67,6 +65,47 @@

    + +
    +
    + +
    +   London Heathrow - GND (S1) + +
    +
    +
    +

    Background

    + +

    + London Heathrow (EGLL) is a Tier 1 aerodrome and the busiest airport on the VATSIM network. + + Controlling at London Heathrow is restricted, members must hold a special endorsement. + + This restriction is in place to allow members to gain experience in quieter environments + and practice their skills before tackling the workload at Heathrow. + + S1 rated members that hold a Gatwick Endorsement may train for a Heathrow Ground (S1) Endorsement. +

    + +

    Endorsement Process

    + +

    + View the requirements for the Heathrow Ground (S1) endorsement by clicking here. +

    + +

    Get Started

    + +

    + The process for getting started with the Heathrow Ground (S1) endorsement can be found by clicking here. +

    + +
    +
    +
    +
    @@ -200,7 +239,6 @@
    -
    View the details for the Heathrow endorsements by clicking here. diff --git a/routes/web-main.php b/routes/web-main.php index af4fba075f..085416b1f2 100644 --- a/routes/web-main.php +++ b/routes/web-main.php @@ -148,6 +148,7 @@ 'middleware' => 'auth_full_group', ], function () { Route::get('endorsements/gatwick')->uses('EndorsementController@getGatwickGroundIndex')->name('endorsements.gatwick_ground'); + Route::get('endorsements/heathrow-s1')->uses('EndorsementController@getHeathrowGroundS1Index')->name('endorsements.heathrow_ground_s1'); Route::get('hour-check/area')->uses('EndorsementController@getAreaIndex')->name('hour_check.area'); }); diff --git a/tests/Feature/Atc/HeathrowS1EndorsementTest.php b/tests/Feature/Atc/HeathrowS1EndorsementTest.php new file mode 100644 index 0000000000..3b18fff378 --- /dev/null +++ b/tests/Feature/Atc/HeathrowS1EndorsementTest.php @@ -0,0 +1,211 @@ +getS1Account(); + $this->endorseForEgkk($account, Carbon::create(2000, 1, 1)); + + factory(Atc::class)->create([ + 'account_id' => $account->id, + 'callsign' => 'EGKK_DEL', + 'minutes_online' => 25 * 60, + 'facility_type' => Atc::TYPE_DEL, + ]); + factory(Atc::class)->create([ + 'account_id' => $account->id, + 'callsign' => 'EGKK_GND', + 'minutes_online' => 25 * 60, + 'facility_type' => Atc::TYPE_GND, + ]); + factory(Atc::class)->create([ + 'account_id' => $account->id, + 'callsign' => 'EGKK__GND', + 'minutes_online' => 5 * 60, + 'facility_type' => Atc::TYPE_GND, + ]); + + $this->actingAs($account->fresh()) + ->get(route(self::ROUTE)) + ->assertStatus(200) + ->assertViewHas('hoursMet', true) + ->assertViewHas('hasEgkkEndorsement', true) + ->assertViewHas('onRoster', true) + ->assertViewHas('conditionsMet', true); + } + + public function testItFailsFor55HoursNonGatwick() + { + $account = $this->getS1Account(); + $this->endorseForEgkk($account, Carbon::create(2000, 1, 1)); + + factory(Atc::class)->create([ + 'account_id' => $account->id, + 'callsign' => 'EGPH_DEL', + 'minutes_online' => 25 * 60, + 'facility_type' => Atc::TYPE_DEL, + ]); + factory(Atc::class)->create([ + 'account_id' => $account->id, + 'callsign' => 'EGCC_GND', + 'minutes_online' => 30 * 60, + 'facility_type' => Atc::TYPE_GND, + ]); + + $this->actingAs($account->fresh()) + ->get(route(self::ROUTE)) + ->assertStatus(200) + ->assertViewHas('hoursMet', false) + ->assertViewHas('onRoster', true) + ->assertViewHas('hasEgkkEndorsement', true) + ->assertViewHas('conditionsMet', false); + } + + public function testItFailsFor55HoursPreEndorsementGatwick() + { + $account = $this->getS1Account(); + $this->endorseForEgkk($account, Carbon::create(2025, 1, 1)); + + factory(Atc::class)->create([ + 'account_id' => $account->id, + 'callsign' => 'EGKK_T_GND', + 'minutes_online' => 55 * 60, + 'facility_type' => Atc::TYPE_DEL, + 'connected_at' => Carbon::create(2024, 1, 1), + ]); + + factory(Atc::class)->create([ + 'account_id' => $account->id, + 'callsign' => 'EGKK_GND', + 'minutes_online' => 55 * 60, + 'facility_type' => Atc::TYPE_DEL, + 'connected_at' => Carbon::create(2024, 2, 1), + ]); + + factory(Atc::class)->create([ + 'account_id' => $account->id, + 'callsign' => 'EGKK_GND', + 'minutes_online' => 25 * 60, + 'facility_type' => Atc::TYPE_GND, + 'connected_at' => Carbon::create(2026, 1, 1), + ]); + + $this->actingAs($account->fresh()) + ->get(route(self::ROUTE)) + ->assertStatus(200) + ->assertViewHas('hoursMet', false) + ->assertViewHas('progress', 50.0) + ->assertViewHas('onRoster', true) + ->assertViewHas('hasEgkkEndorsement', true) + ->assertViewHas('conditionsMet', false); + } + + public function testItDetectsNotOnRoster() + { + $account = $this->getS1AccountNotOnRoster(); + + factory(Atc::class)->create([ + 'account_id' => $account->id, + 'callsign' => 'EGPH_DEL', + 'minutes_online' => 25 * 60, + 'facility_type' => Atc::TYPE_DEL, + ]); + factory(Atc::class)->create([ + 'account_id' => $account->id, + 'callsign' => 'EGPH_GND', + 'minutes_online' => 25 * 60, + 'facility_type' => Atc::TYPE_GND, + ]); + factory(Atc::class)->create([ + 'account_id' => $account->id, + 'callsign' => 'EGCC_GND', + 'minutes_online' => 5 * 60, + 'facility_type' => Atc::TYPE_GND, + ]); + + $this->actingAs($account->fresh()) + ->get(route(self::ROUTE)) + ->assertStatus(200) + ->assertViewHas('onRoster', false) + ->assertViewHas('conditionsMet', false); + } + + public function testItRedirectsForNonS1() + { + $account = Account::factory()->create(); + + $this->actingAs($account->fresh()) + ->get(route(self::ROUTE)) + ->assertRedirect(route('mship.manage.dashboard')); + } + + public function testItRedirectsForS2() + { + $account = Account::factory()->create(); + + $qualification = Qualification::code('S2')->first(); + $account->addQualification($qualification); + $account->save(); + + $this->actingAs($account->fresh()) + ->get(route(self::ROUTE)) + ->assertRedirect(route('mship.manage.dashboard')); + } + + private function getS1Account(): Account + { + $account = Account::factory()->create(); + + $qualification = Qualification::code('S1')->first(); + $account->addQualification($qualification); + $account->save(); + + $divisionState = State::findByCode('DIVISION')->firstOrFail(); + $account->addState($divisionState, 'EUR', 'GBR'); + Roster::create(['account_id' => $account->id])->save(); + + return $account; + } + + public function endorseForEgkk(Account $account, Carbon $from): void + { + $positionGroup = PositionGroup::where('name', 'EGKK_GND')->firstOrFail(); + Account\Endorsement::create([ + 'account_id' => $account->id, + 'endorsable_id' => $positionGroup->id, + 'endorsable_type' => PositionGroup::class, + 'created_at' => $from, + ]); + } + + private function getS1AccountNotOnRoster(): Account + { + $account = Account::factory()->create(); + + $qualification = Qualification::code('S1')->first(); + $account->addQualification($qualification); + $account->save(); + + $divisionState = State::findByCode('DIVISION')->firstOrFail(); + $account->addState($divisionState, 'EUR', 'GBR'); + + return $account; + } +}