Skip to content

Commit

Permalink
feat: Improved Stats (#3947)
Browse files Browse the repository at this point in the history
* wip

* wip

* Policy

* Update RosterUpdatePolicy.php
  • Loading branch information
CalumTowers authored Jan 7, 2025
1 parent 3ad7f49 commit 2d13356
Show file tree
Hide file tree
Showing 12 changed files with 399 additions and 13 deletions.
56 changes: 47 additions & 9 deletions app/Console/Commands/Roster/UpdateRoster.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use App\Models\NetworkData\Atc;
use App\Models\Roster;
use App\Models\RosterUpdate;
use App\Models\Training\WaitingList;
use App\Models\Training\WaitingList\WaitingListAccount;
use Illuminate\Console\Command;
Expand All @@ -27,6 +28,11 @@ public function handle()
$this->fromDate = Carbon::parse($this->argument('fromDate'))->startOfDay();
$this->toDate = Carbon::parse($this->argument('toDate'))->endOfDay();

$rosterUpdate = RosterUpdate::create([
'period_start' => $this->fromDate,
'period_end' => $this->toDate,
]);

$meetHourRequirement = Atc::with(['account.states'])
->select(['networkdata_atc.account_id'])
->whereBetween('disconnected_at', [$this->fromDate, $this->toDate])
Expand All @@ -38,25 +44,45 @@ public function handle()

// Automatically mark those on the Gander Oceanic roster as eligible
$eligible = $meetHourRequirement->merge(
Http::get(config('services.gander-oceanic.api.base').'/roster')
$ganderControllers = Http::get(config('services.gander-oceanic.api.base').'/roster')
->collect()
->where('active', true)
->pluck('cid')
->flatten()
)->unique();

// On the roster, do not need to be on...
Roster::withoutGlobalScopes()
->whereNotIn('account_id', $eligible)
->get()
$removeFromRoster = Roster::withoutGlobalScopes()
->whereNotIn('account_id', $eligible);

$homeRemovals = $removeFromRoster->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'])
->orWhereColumn('roster.updated_at', '>', 'mship_account_state.start_at');
});
});

$visitingAndTransferringRemovals = $removeFromRoster->whereHas('account', function ($query) {
$query->whereHas('states', function ($query) {
$query
->join('roster', 'mship_account_state.account_id', '=', 'roster.account_id')
->whereIn('mship_state.code', ['TRANSFERRING', 'VISITING'])
->orWhereColumn('roster.updated_at', '>', 'mship_account_state.start_at');
});
});

$removeFromRoster->get()
->each
->remove();
->remove($rosterUpdate);

// On an ATC waiting list, not on the roster, need to be removed...
WaitingListAccount::whereIn('list_id',
WaitingList::where('department', WaitingList::ATC_DEPARTMENT)->get('id')
)->whereNotIn('account_id', $eligible)
->get()
$removeFromWaitingList = WaitingListAccount::with('waitingList')
->whereIn('list_id', WaitingList::where('department', WaitingList::ATC_DEPARTMENT)->get('id'))
->whereNotIn('account_id', $eligible)
->get();
$removeFromWaitingList
->each
->delete();

Expand All @@ -66,6 +92,18 @@ public function handle()
['account_id']
);

$rosterUpdate->update([
'data' => [
'meetHourRequirement' => $meetHourRequirement->count(),
'ganderControllers' => $ganderControllers->count(),
'eligible' => $eligible->count(),
'removeFromRoster' => $removeFromRoster->count(),
'homeRemovals' => $homeRemovals->count(),
'visitingAndTransferringRemovals' => $visitingAndTransferringRemovals->count(),
'removeFromWaitingList' => $removeFromWaitingList->countBy('list_id'),
],
]);

$this->comment('✅ Roster updated!');
}
}
52 changes: 51 additions & 1 deletion app/Filament/Pages/Operations/GenerateQuarterlyStats.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
namespace App\Filament\Pages\Operations;

use App\Filament\Helpers\Pages\BasePage;
use App\Models\Atc\PositionGroup;
use App\Models\Mship\Account;
use App\Models\Mship\Account\Endorsement;
use Carbon\Carbon;
use Filament\Forms\Components\Grid;
use Filament\Forms\Components\Select;
Expand Down Expand Up @@ -65,7 +67,13 @@ public function submit(): void
['name' => 'Members Becoming Inactive', 'value' => $this->membersBecomingInactive($startDate, $endDate)],
['name' => 'Visiting Controllers Above S1', 'value' => $this->visitingControllersAboveS1($startDate, $endDate)],
['name' => 'Completed Transfer (Ex OBS)', 'value' => $this->completedTransfersExObs($startDate, $endDate)],
]);
])->merge(collect(['OBS', 'TWR', 'APP', 'CTR'])->map(function ($value) use ($startDate, $endDate) {
return ['name' => "Completed 121 Mentoring Sessions - {$value}", 'value' => $this->completedMentoringSessions($startDate, $endDate, $value)];
}))->merge(collect(['TWR', 'APP', 'CTR'])->map(function ($value) use ($startDate, $endDate) {
return ['name' => "Exam Pass - {$value}", 'value' => $this->examPasses($startDate, $endDate, $value)];
})->merge(collect(['GND', 'TWR', 'APP'])->map(function ($value) use ($startDate, $endDate) {
return ['name' => "Issued Heathrow Endorsement - {$value}", 'value' => $this->issuedHeathrowEndorsements($startDate, $endDate, $value)];
})));
}

protected static function canUse(): bool
Expand Down Expand Up @@ -138,4 +146,46 @@ private function visitingControllersAboveS1($startDate, $endDate)
$q->whereBetween('mship_qualification.id', [3, 10]);
})->count();
}

private function completedMentoringSessions(Carbon $startDate, Carbon $endDate, string $type)
{
$studentRating = match ($type) {
'OBS' => 1,
'TWR' => 2,
'APP' => 3,
'CTR' => 4
};

return DB::connection('cts')
->table('sessions')
->whereBetween('taken_date', [$startDate, $endDate])
->whereNull('cancelled_datetime')
->where('position', 'LIKE', "%$type%")
->where('student_rating', '=', $studentRating)
->count();
}

private function issuedHeathrowEndorsements(Carbon $startDate, Carbon $endDate, string $type)
{
$positionGroup = match ($type) {
'GND' => 18,
'TWR' => 10,
'APP' => 11
};

return Endorsement::whereBetween('created_at', [$startDate, $endDate])
->whereEndorsableType(PositionGroup::class)
->whereEndorsableId($positionGroup)
->count();
}

private function examPasses(Carbon $startDate, Carbon $endDate, string $type)
{
return DB::connection('cts')
->table('practical_results')
->whereBetween('date', [$startDate, $endDate])
->where('result', '=', 'P')
->where('exam', '=', $type)
->count();
}
}
82 changes: 82 additions & 0 deletions app/Filament/Resources/RosterUpdateResource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php

namespace App\Filament\Resources;

use App\Filament\Resources\RosterUpdates\Pages\ListRosterUpdates;
use App\Filament\Resources\RosterUpdates\Pages\ViewRosterUpdate;
use App\Models\RosterUpdate;
use App\Models\Training\WaitingList;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables\Actions\ViewAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;

class RosterUpdateResource extends Resource
{
protected static ?string $model = RosterUpdate::class;

protected static ?string $navigationIcon = 'heroicon-o-list-bullet';

protected static ?string $navigationGroup = 'User Management';

protected static ?int $navigationSort = 2;

public static function form(Form $form): Form
{
return $form
->schema([
Section::make('Details')->schema([
DatePicker::make('created_at')->label('Ran'),
DatePicker::make('period_start'),
DatePicker::make('period_end'),
]),
Section::make('Data')
->statePath('data')
->schema([
TextInput::make('meetHourRequirement')->label('Controllers meeting requirement'),
TextInput::make('ganderControllers')->label('Eligible Gander/Oceanic controllers'),
TextInput::make('removeFromRoster')->label('Removed from roster'),
TextInput::make('homeRemovals')->label('Home members removed from roster'),
TextInput::make('visitingAndTransferringRemovals')->label('V/T removed from roster'),
TextInput::make('removeFromWaitingList')->label('Removed from waiting lists')
->formatStateUsing(fn ($state) => collect($state)->map(function ($value, $key) {
$waitingList = WaitingList::withTrashed()->where('id', '=', $key)?->first();

return "$waitingList?->name: $value";
})->implode('; ')),
]),
]);
}

public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('created_at')->label('Ran'),
TextColumn::make('period_start'),
TextColumn::make('period_end'),
])
->actions([
ViewAction::make(),
])->defaultSort('created_at', 'DESC');
}

public static function getRelations(): array
{
return [
//
];
}

public static function getPages(): array
{
return [
'index' => ListRosterUpdates::route('/'),
'view' => ViewRosterUpdate::route('/{record}'),
];
}
}
16 changes: 16 additions & 0 deletions app/Filament/Resources/RosterUpdates/Pages/ListRosterUpdates.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace App\Filament\Resources\RosterUpdates\Pages;

use App\Filament\Helpers\Pages\BaseListRecordsPage;
use App\Filament\Resources\RosterUpdateResource;

class ListRosterUpdates extends BaseListRecordsPage
{
protected static string $resource = RosterUpdateResource::class;

protected function getHeaderActions(): array
{
return [];
}
}
16 changes: 16 additions & 0 deletions app/Filament/Resources/RosterUpdates/Pages/ViewRosterUpdate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace App\Filament\Resources\RosterUpdates\Pages;

use App\Filament\Helpers\Pages\BaseViewRecordPage;
use App\Filament\Resources\RosterUpdateResource;

class ViewRosterUpdate extends BaseViewRecordPage
{
protected static string $resource = RosterUpdateResource::class;

protected function getHeaderActions(): array
{
return [];
}
}
5 changes: 3 additions & 2 deletions app/Models/Roster.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,15 @@ public function account()
return $this->belongsTo(Account::class);
}

public function remove()
public function remove(?RosterUpdate $update = null)
{
DB::transaction(function () {
DB::transaction(function () use ($update) {
RosterHistory::create([
'account_id' => $this->account_id,
'original_created_at' => $this->created_at,
'original_updated_at' => $this->updated_at,
'removed_by' => auth()->user()?->getKey(),
'roster_update_id' => $update?->id,
]);

$this->delete();
Expand Down
2 changes: 1 addition & 1 deletion app/Models/RosterHistory.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@ class RosterHistory extends Model

protected $table = 'roster_history';

protected $fillable = ['account_id', 'original_created_at', 'original_updated_at', 'removed_by'];
protected $fillable = ['account_id', 'original_created_at', 'original_updated_at', 'removed_by', 'roster_update_id'];
}
16 changes: 16 additions & 0 deletions app/Models/RosterUpdate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;

class RosterUpdate extends Model
{
use HasFactory;

protected $fillable = ['period_start', 'period_end', 'data'];

protected $casts = [
'data' => 'json',
];
}
65 changes: 65 additions & 0 deletions app/Policies/RosterUpdatePolicy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

namespace App\Policies;

use App\Models\Mship\Account;
use App\Models\RosterUpdate;

class RosterUpdatePolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(Account $account): bool
{
return $account->can('roster.manage');
}

/**
* Determine whether the user can view the model.
*/
public function view(Account $account, RosterUpdate $rosterUpdate): bool
{
return $account->can('roster.manage');
}

/**
* 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, RosterUpdate $rosterUpdate): bool
{
return false;
}

/**
* Determine whether the user can delete the model.
*/
public function delete(Account $account, RosterUpdate $rosterUpdate): bool
{
return false;
}

/**
* Determine whether the user can restore the model.
*/
public function restore(Account $account, RosterUpdate $rosterUpdate): bool
{
return false;
}

/**
* Determine whether the user can permanently delete the model.
*/
public function forceDelete(Account $account, RosterUpdate $rosterUpdate): bool
{
return false;
}
}
Loading

0 comments on commit 2d13356

Please sign in to comment.