Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/Enums/EmailType.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public function description(): string
{
return match ($this) {
// Student - Sessions
self::SessionAcceptedByMentor => 'Sent when a mentor accepts your session request',
self::SessionAcceptedByMentor => 'Sent when a mentor books a mentoring session with you',
self::SessionCancelledByMentor => 'Sent when a mentor cancels your session',
self::SessionRescheduledByMentor => 'Sent when a mentor reschedules your session',
self::SessionCancelledByStudent => 'Sent when you cancel your own session',
Expand Down
229 changes: 175 additions & 54 deletions app/Livewire/Training/AvailabilityGantt.php

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions app/Models/Cts/Position.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,14 @@ class Position extends Model
public $timestamps = false;

protected $fillable = [
'rts_id',
'callsign',
'name',
'rating',
'auto_rating',
'vis_roster',
'anon_requests',
'prog_sheet_id',
'prog_sheet_assign_by',
];
}
16 changes: 16 additions & 0 deletions app/Models/Training/TrainingPlace/TrainingPlace.php
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,22 @@ public function trainableCtsPositions(): array
return app(MentorPermissionService::class)->getCtsCallsignsForMentorable($this->trainable);
}

/**
* Primary CTS callsign for display / default session booking.
*/
public function primaryCtsPosition(): ?string
{
$primary = $this->trainingPosition?->cts_primary_position;

if (is_string($primary) && trim($primary) !== '') {
return trim($primary);
}

$first = collect($this->trainableCtsPositions())->filter()->first();

return is_string($first) ? $first : null;
}

public function availabilityChecks(): HasMany
{
return $this->hasMany(AvailabilityCheck::class);
Expand Down
8 changes: 8 additions & 0 deletions app/Policies/Training/Mentoring/MentoringPolicy.php
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,14 @@ public function accept(Account $user, Session $session): bool
return $this->mentorPosition($user, $session->position);
}

/**
* Create a mentoring session for a training-place student on a CTS position.
*/
public function create(Account $user, string $position): bool
{
return $this->mentorPosition($user, $position);
}

/**
* Determine if a user can reschedule a session.
*/
Expand Down
87 changes: 78 additions & 9 deletions app/Services/Training/MentoringSessionsService.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
use App\Models\Cts\CancelReason;
use App\Models\Cts\ExamBooking;
use App\Models\Cts\Member;
use App\Models\Cts\Position as CtsPosition;
use App\Models\Cts\Session;
use App\Models\Mship\Account;
use App\Models\Training\TrainingPlace\TrainingPlace;
use App\Notifications\Training\Mentoring\MentoringSessionAcceptedMentorNotification;
use App\Notifications\Training\Mentoring\MentoringSessionAcceptedStudentNotification;
use App\Notifications\Training\Mentoring\MentoringSessionCancelledByStudentNotification;
Expand All @@ -30,8 +32,84 @@

class MentoringSessionsService
{
/**
* Creates a mentoring session for a training-place student from an availability slot.
*
* Transaction note: `DB::transaction()` only wraps the default (Core) connection.
* CTS writes (`Session`, CTS bookings) use the `cts` connection and are not rolled
* back if a later Core write fails. Notifications are deferred with `DB::afterCommit()`
* so they only run after the Core transaction commits.
*/
public function createSession(
TrainingPlace $trainingPlace,
Availability $availability,
Account $mentorAccount,
string $position,
string $takenFrom,
string $takenTo,
): bool {
return DB::transaction(function () use ($trainingPlace, $availability, $mentorAccount, $position, $takenFrom, $takenTo) {
$trainingPlace->loadMissing(['trainable', 'account']);

$mentorMember = Member::where('cid', $mentorAccount->id)->firstOrFail();
$studentMember = Member::where('cid', $trainingPlace->account_id)->first();

if (! $studentMember || $studentMember->id !== $availability->student_id) {
throw new InvalidArgumentException('The selected availability does not belong to this training place student.');
}

$placeCallsigns = $trainingPlace->trainableCtsPositions();

if (! in_array($position, $placeCallsigns, true)) {
throw new InvalidArgumentException('The selected position is not valid for this training place.');
}

if ($mentorAccount->cannot('create', [Session::class, $position])) {
throw new AuthorizationException('You are not authorized to create mentoring sessions for this position.');
}

if ($trainingPlace->isOnLeaveOfAbsence()) {
throw new InvalidArgumentException('Cannot create a mentoring session while the student is on leave of absence.');
}

$this->validateSessionTimes($availability, $takenFrom, $takenTo);

$ctsPosition = CtsPosition::query()->where('callsign', $position)->first();

if (! $ctsPosition) {
throw new InvalidArgumentException("CTS position not found for callsign [{$position}].");
}

$session = Session::query()->create([
'rts_id' => $ctsPosition->rts_id ?? 0,
'position' => $ctsPosition->callsign,
'progress_sheet_id' => $ctsPosition->prog_sheet_id ?? 0,
'student_id' => $studentMember->id,
'student_rating' => $studentMember->rating ?? 0,
'request_time' => now(),
'mentor_id' => $mentorMember->id,
'mentor_rating' => $mentorAccount->qualification_atc?->vatsim,
'taken' => 1,
'taken_date' => $availability->date,
'taken_from' => $takenFrom,
'taken_to' => $takenTo,
'taken_time' => now(),
]);

DB::afterCommit(function () use ($session) {
$this->notifyParticipants($session, 'accepted');
});

$this->createCoreBooking($session);

return true;
});
}

/**
* Accepts a pending session by claiming a student's availability slot.
*
* @deprecated Prefer createSession() for Training Panel mentoring.
*/
public function acceptSession(int $sessionId, int $availabilityId, Account $mentorAccount, string $takenFrom, string $takenTo): bool
{
Expand Down Expand Up @@ -143,15 +221,6 @@ public function cancelSession(int $sessionId, string $reason, Account $canceller
'reason_by' => $cancellerMember->id,
]);

Session::create([
'rts_id' => $session->rts_id,
'position' => $session->position,
'progress_sheet_id' => $session->progress_sheet_id,
'student_id' => $session->student_id,
'student_rating' => $session->student_rating,
'request_time' => Carbon::now(),
]);

DB::afterCommit(function () use ($session, $reason, $cancellerAccount) {
$this->notifyParticipants($session, 'cancelled', [
'reason' => $reason,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
@extends('emails.messages.post')

@section('body')
<p>{{ $mentorName }} has accepted your mentoring session request. The details are as follows:</p>
<p>{{ $mentorName }} has booked a mentoring session with you. The details are as follows:</p>

<ul>
<li><strong>Position</strong>: {{ $position }}</li>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,19 +50,19 @@ class="w-64 flex-shrink-0 sticky left-0 z-20 bg-inherit border-r border-gray-200
{{ $student->name }}
</a>
</div>
@if ($student->pending_position)
@if ($student->primary_position)
@php
$isAllCategories = empty($category);
$badgeColor = $isAllCategories
? \App\Filament\Training\Support\MentoringTrainingGroupBadgeColor::forCtsCallsign(
$student->pending_position,
$student->primary_position,
)
: 'gray';
@endphp

<span class="shrink-0 max-w-full overflow-visible">
<x-filament::badge :color="$badgeColor" size="sm">
{{ $student->pending_position }}
{{ $student->primary_position }}
</x-filament::badge>
</span>
@endif
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,14 @@ class="w-16 flex-shrink-0 sticky left-0 z-40 bg-gray-50 dark:bg-gray-800 border-
@php
$isAllCategories = empty($category);
$badgeColor = $isAllCategories
? \App\Filament\Training\Support\MentoringTrainingGroupBadgeColor::forCtsCallsign($student->pending_position)
? \App\Filament\Training\Support\MentoringTrainingGroupBadgeColor::forCtsCallsign($student->primary_position)
: 'gray';
@endphp

@if ($student->pending_position)
@if ($student->primary_position)
<div class="mt-0.5 shrink-0 max-w-full overflow-visible">
<x-filament::badge :color="$badgeColor" size="sm">
{{ $student->pending_position }}
{{ $student->primary_position }}
</x-filament::badge>
</div>
@endif
Expand Down
Loading