Skip to content
Merged
20 changes: 20 additions & 0 deletions app/Enums/ExamResultEnum.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,35 @@
enum ExamResultEnum: string
{
case Pass = 'P';
case PartialPass = 'S';
case Fail = 'F';
case Incomplete = 'N';

public function human(): string
{
return match ($this) {
self::Pass => 'Pass',
self::PartialPass => 'Partial Pass',
self::Fail => 'Fail',
self::Incomplete => 'Incomplete',
};
}

public static function atcOptions(): array
{
return [
self::Pass->value => self::Pass->human(),
self::Fail->value => self::Fail->human(),
self::Incomplete->value => self::Incomplete->human(),
];
}

public static function pilotOptions(): array
{
return [
self::Pass->value => self::Pass->human(),
self::PartialPass->value => self::PartialPass->human(),
self::Fail->value => self::Fail->human(),
];
}
}
38 changes: 38 additions & 0 deletions app/Enums/PilotExamType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

namespace App\Enums;

enum PilotExamType: string
{
case P1 = 'P1';
case P2 = 'P2';
case P3 = 'P3';

public function label(): string
{
return match ($this) {
self::P1 => 'P1_PPL(A)',
self::P2 => 'P2_SEIR(A)',
self::P3 => 'P3_CMEL(A)',
};
}

public function prerequisiteRating(): string
{
return match ($this) {
self::P1 => 'P0',
self::P2 => 'P1',
self::P3 => 'P2',
};
}

public static function values(): array
{
return array_column(self::cases(), 'value');
}

public static function labelFor(string $type): string
{
return self::from($type)->label();
}
}
16 changes: 10 additions & 6 deletions app/Filament/Training/Pages/Exam/ConductExam.php
Original file line number Diff line number Diff line change
Expand Up @@ -219,11 +219,10 @@ public function examResultForm(Form $form): Form

Select::make('exam_result')
->label('Result')
->options([
ExamResultEnum::Pass->value => ExamResultEnum::Pass->human(),
ExamResultEnum::Fail->value => ExamResultEnum::Fail->human(),
ExamResultEnum::Incomplete->value => ExamResultEnum::Incomplete->human(),
])
->options(fn () => $this->examBooking->isPilotExam()
? ExamResultEnum::pilotOptions()
: ExamResultEnum::atcOptions()
)
->live()
->columnSpan(3)
->required(),
Expand Down Expand Up @@ -284,6 +283,11 @@ public function completeExam()

public function validateGradesBeforeSubmission(string $result): bool
{
// Pilot exams should pass validation always
if ($this->examBooking->isPilotExam()) {
return true;
}

$formData = collect($this->form->getState())['form'];

$hasNotAssessed = collect($formData)->contains(
Expand Down Expand Up @@ -321,7 +325,7 @@ public function save($withNotification = true): void
{
$this->isSaving = true;

$formData = collect($this->form->getState())['form'];
$formData = collect($this->form->getState())['form'] ?? [];

$flattenedFormData = collect($formData)->map(
fn ($item, $key) => [
Expand Down
12 changes: 10 additions & 2 deletions app/Filament/Training/Pages/Exam/ExamHistory.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace App\Filament\Training\Pages\Exam;

use App\Enums\PilotExamType;
use App\Filament\Training\Pages\Exam\Widgets\ExamOverview;
use App\Services\Training\ExamHistoryService;
use Filament\Forms;
Expand Down Expand Up @@ -59,15 +60,22 @@ public function table(Table $table): Table
Forms\Components\DatePicker::make('exam_date_to')->label('To'),
])->query(fn ($query, array $data) => $examHistoryService->applyExamDateFilter($query, $data))->label('Exam date'),
Filter::make('position')->form([
Forms\Components\Select::make('position')
Forms\Components\Select::make('atc_positions')
->options([
'OBS' => 'Observer',
'TWR' => 'Tower',
'APP' => 'Approach',
'CTR' => 'Enroute',
])
->multiple()
->label('Position'),
->label('ATC position'),
Forms\Components\Select::make('pilot_positions')
->options(collect(PilotExamType::cases())
->mapWithKeys(fn ($type) => [$type->label() => $type->label()])
->toArray()
)
->multiple()
->label('Pilot rating'),
])->query(fn ($query, array $data) => $examHistoryService->applyPositionFilter($query, $data))->label('Position'),
Filter::make('conducted_by_me')->form([
Forms\Components\Checkbox::make('conducted_by_me')
Expand Down
74 changes: 74 additions & 0 deletions app/Filament/Training/Pages/Exam/ExamSetup.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

namespace App\Filament\Training\Pages\Exam;

use App\Enums\PilotExamType;
use App\Models\Atc\Position;
use App\Models\Cts\Member;
use App\Models\Cts\Position as CtsPosition;
use App\Models\Mship\Account;
use App\Models\Training\TrainingPosition\TrainingPosition;
use App\Repositories\Cts\ExamResultRepository;
use App\Repositories\Cts\SessionRepository;
Expand Down Expand Up @@ -39,17 +41,21 @@ public static function canAccess(): bool

public ?array $dataOBS = [];

public ?array $dataPilot = [];

public function mount(): void
{
$this->form->fill();
$this->formOBS->fill();
$this->formPilot->fill();
}

protected function getForms(): array
{
return [
'formOBS',
'form',
'formPilot',
];
}

Expand Down Expand Up @@ -197,6 +203,74 @@ public function form(Form $form): Form
->statePath('data');
}

public function setupExamPilot()
{
$validated = $this->validate([
'dataPilot.exam_type' => 'required',
'dataPilot.student_pilot' => 'required',
]);

$ctsMember = Member::where('id', $validated['dataPilot']['student_pilot'])->first();

$service = new ExamForwardingService;
$service->forwardForPilotExam($ctsMember, $validated['dataPilot']['exam_type'], Auth::user()->id);
$service->notifySuccess($validated['dataPilot']['exam_type']);

return redirect()->route('filament.training.pages.exam-setup');
}

public function formPilot(Form $form): Form
{
return $form
->schema([
Section::make('Exam Setup - Pilot')
->schema([
Select::make('exam_type')
->label('Exam')
->options(collect(PilotExamType::cases())
->mapWithKeys(fn ($type) => [$type->value => $type->label()])
->toArray()
)
->required()
->live()
->afterStateUpdated(fn (callable $set) => $set('student_pilot', null)),

Select::make('student_pilot')
->label('Student')
->getSearchResultsUsing(function (string $search, Get $get): array {
$examType = $get('exam_type');
if (! $examType) {
return [];
}

$prerequisiteRating = PilotExamType::from($examType)->prerequisiteRating();

$eligibleCids = Account::whereHas('qualifications', fn ($q) => $q
->where('type', 'pilot')
->where('code', $prerequisiteRating)
)->pluck('id');

return Member::whereIn('cid', $eligibleCids)
->where(fn ($query) => $query
->where('name', 'LIKE', "%{$search}%")
->orWhere('cid', 'LIKE', "%{$search}%")
)
->limit(25)
->get()
->mapWithKeys(fn ($member) => [$member->id => "{$member->name} ({$member->cid})"])
->toArray();
})
->getOptionLabelUsing(fn ($value): ?string => Member::find($value)?->name)
->searchable()
->placeholder('Select an exam type first')
->disabled(fn (Get $get): bool => ! $get('exam_type'))
->required()
->live(),
]),
])
->statePath('dataPilot');
}

protected function generateStudentOptions(string $positionCallsign, int $daysConsideredRecent, Collection $recentPassedStudentIds, ?Collection $pendingStudentIds = null): Collection
{
$recentCompletedSessions = (new SessionRepository)->getRecentCompletedSessionsForPosition($positionCallsign, daysConsideredRecent: $daysConsideredRecent);
Expand Down
23 changes: 12 additions & 11 deletions app/Filament/Training/Pages/Exam/ViewExamReport.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,25 +92,24 @@ public function infolist(Infolist $infolist): Infolist
Select::make('previous_exam_result')
->label('Previous Result')
->default($this->practicalResult->result)
->options([
ExamResultEnum::Pass->value => ExamResultEnum::Pass->human(),
ExamResultEnum::Fail->value => ExamResultEnum::Fail->human(),
ExamResultEnum::Incomplete->value => ExamResultEnum::Incomplete->human(),
])
->options(fn () => $this->practicalResult->examBooking->isPilotExam()
? ExamResultEnum::pilotOptions()
: ExamResultEnum::atcOptions()
)
->required()
->disabled()
->columns(1)
->dehydrated(true),

Select::make('exam_result')
->label('New Result')
->default($this->practicalResult->result)
->live()
->columns(1)
->options([
ExamResultEnum::Pass->value => ExamResultEnum::Pass->human(),
ExamResultEnum::Fail->value => ExamResultEnum::Fail->human(),
ExamResultEnum::Incomplete->value => ExamResultEnum::Incomplete->human(),
])
->options(fn () => $this->practicalResult->examBooking->isPilotExam()
? ExamResultEnum::pilotOptions()
: ExamResultEnum::atcOptions()
)
->required(),
Textarea::make('reason')
->label('Reason for exam result change')
Expand All @@ -120,7 +119,8 @@ public function infolist(Infolist $infolist): Infolist
]),

\Filament\Forms\Components\Section::make('Exam Criteria')
->visible(fn ($get) => $get('exam_result') !== $this->practicalResult->result
->visible(fn ($get) => ! $this->practicalResult->examBooking->isPilotExam()
&& $get('exam_result') !== $this->practicalResult->result
)
->schema(function () {
$criteria = ExamCriteria::byType($this->practicalResult->examBooking->exam)->get();
Expand Down Expand Up @@ -162,6 +162,7 @@ public function infolist(Infolist $infolist): Infolist
->schema([
TextEntry::make('result')->label('Result')->badge()->color(fn ($state) => match ($state) {
'Passed' => 'success',
'Partial Pass' => 'warning',
'Failed' => 'danger',
'Incomplete' => 'warning',
default => 'gray',
Expand Down
2 changes: 1 addition & 1 deletion app/Filament/Training/Pages/Exam/Widgets/ExamOverview.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ protected function getCards(): array
->description("$overall->passed Passed, $overall->failed Failed, $overall->incomplete Incomplete"),
];

$examOrder = ['OBS', 'TWR', 'APP', 'CTR'];
$examOrder = ['OBS', 'TWR', 'APP', 'CTR', 'P1', 'P2', 'P3'];

foreach ($examOrder as $exam) {
$results = $examStats->get($exam, collect());
Expand Down
2 changes: 1 addition & 1 deletion app/Livewire/Training/AcceptedExamsTable.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public function table(Table $table): Table
if ($examBooking->exam === 'OBS') {
return false;
}
// use CTS member ID rather than Core acocunt ID.
// use CTS member ID rather than Core account ID.
$memberId = auth()->user()->member->id;

return app(ExamAnnouncementService::class)->canPostAnnouncement($examBooking, $memberId);
Expand Down
2 changes: 1 addition & 1 deletion app/Livewire/Training/ExamRequestsTable.php
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,7 @@ protected function getFilteredExamRequestsQuery()
*/
protected function getAllowedExamLevels(): array
{
$examLevels = ['OBS', 'TWR', 'APP', 'CTR']; // Common exam levels
$examLevels = ['OBS', 'TWR', 'APP', 'CTR', 'P1', 'P2', 'P3']; // Common exam levels
$allowedLevels = [];

foreach ($examLevels as $level) {
Expand Down
10 changes: 9 additions & 1 deletion app/Models/Cts/ExamBooking.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace App\Models\Cts;

use App\Enums\PilotExamType;
use App\Models\Mship\Account;
use App\Models\Mship\Qualification;
use Carbon\Carbon;
Expand Down Expand Up @@ -60,10 +61,17 @@ public function endDate(): Attribute
public function studentQualification(): Attribute
{
return Attribute::make(
get: fn ($value) => Qualification::ofType('atc')->where('vatsim', $this->student_rating)->first()
get: fn ($value) => $this->isPilotExam()
? Qualification::ofType('pilot')->where('vatsim', $this->student_rating)->first()
: Qualification::ofType('atc')->where('vatsim', $this->student_rating)->first()
);
}

public function isPilotExam(): bool
{
return in_array($this->exam, PilotExamType::values());
}

#[Scope]
protected function conductable()
{
Expand Down
15 changes: 15 additions & 0 deletions app/Models/Cts/ExaminerSettings.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,21 @@ public function scopeCtr($query)
return $query->where('S3', '=', 1);
}

public function scopeP1($query)
{
return $query->where('P1', '=', 1);
}

public function scopeP2($query)
{
return $query->where('P2', '=', 1);
}

public function scopeP3($query)
{
return $query->where('P3', '=', 1);
}

public function scopeAtc($query)
{
return $query->where('OBS', '=', 1) // OBS to S1 examiner
Expand Down
3 changes: 3 additions & 0 deletions app/Models/Cts/PracticalResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ class PracticalResult extends Model

public const PASSED = 'P';

public const PARTIAL_PASS = 'S';

public const FAILED = 'F';

public const INCOMPLETE = 'N';
Expand All @@ -43,6 +45,7 @@ public function resultHuman(): string
{
return match ($this->result) {
self::PASSED => 'Passed',
self::PARTIAL_PASS => 'Partial Pass',
self::FAILED => 'Failed',
self::INCOMPLETE => 'Incomplete',
self::FAILRESUBMIT => 'Failed - Resubmit',
Expand Down
Loading
Loading