From 6b42f2c1fb19335bb3446bc18ee5ceda4ace2e17 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Tue, 10 Jun 2025 23:35:21 -0500 Subject: [PATCH 01/67] EntrySeeder update --- database/seeders/EntrySeeder.php | 48 +++++++++++++++++++------------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/database/seeders/EntrySeeder.php b/database/seeders/EntrySeeder.php index 9d8334a..c0acbfe 100644 --- a/database/seeders/EntrySeeder.php +++ b/database/seeders/EntrySeeder.php @@ -17,51 +17,59 @@ class EntrySeeder extends Seeder public function run(): void { $students = Student::all(); - $hs_auditions = Audition::where('maximum_grade', '=', '12'); - $freshman_auditions = Audition::where('maximum_grade', '>', '8'); - $jh_auditions = Audition::where('maximum_grade', '=', '9'); - $seventh_auditions = Audition::where('maximum_grade', '=', '7'); foreach ($students as $student) { if ($student->grade > 9) { - $audition = Audition::where('maximum_grade', '=', '12')->inRandomOrder()->first(); + $audition = Audition::where('maximum_grade', '=', '12') + ->inRandomOrder()->first(); } if ($student->grade == 9) { - $audition = Audition::where('maximum_grade', '>', '8')->inRandomOrder()->first(); + $audition = Audition::where('maximum_grade', '>', '8') + ->inRandomOrder()->first(); } if ($student->grade == 8) { - $audition = Audition::where('maximum_grade', '=', '9')->inRandomOrder()->first(); + $audition = Audition::where('maximum_grade', '=', '9') + ->inRandomOrder()->first(); } if ($student->grade == 7) { - $audition = Audition::where('maximum_grade', '=', '7')->inRandomOrder()->first(); + $audition = Audition::where('maximum_grade', '=', '7') + ->inRandomOrder()->first(); } - Entry::factory()->create([ + Entry::create([ 'student_id' => $student->id, 'audition_id' => $audition->id, + 'for_seating' => 1, + 'for_advancement' => 1, ]); if (mt_rand(1, 100) > 90) { if ($student->grade > 9) { - $audition2 = Audition::where('maximum_grade', '=', '12')->where('id', '!=', - $audition->id)->inRandomOrder()->first(); + $audition2 = Audition::where('maximum_grade', '=', '12') + ->where('id', '!=', $audition->id) + ->inRandomOrder()->first(); } if ($student->grade == 9) { - $audition2 = Audition::where('maximum_grade', '>', '8')->where('id', '!=', - $audition->id)->inRandomOrder()->first(); + $audition2 = Audition::where('maximum_grade', '>', '8') + ->where('id', '!=', $audition->id) + ->inRandomOrder()->first(); } if ($student->grade == 8) { - $audition2 = Audition::where('maximum_grade', '=', '9')->where('id', '!=', - $audition->id)->inRandomOrder()->first(); + $audition2 = Audition::where('maximum_grade', '=', '9') + ->where('id', '!=', $audition->id) + ->inRandomOrder()->first(); } if ($student->grade == 7) { - $audition2 = Audition::where('maximum_grade', '=', '7')->where('id', '!=', - $audition->id)->inRandomOrder()->first(); + $audition2 = Audition::where('maximum_grade', '=', '7') + ->where('id', '!=', $audition->id) + ->inRandomOrder()->first(); } - Entry::factory()->create([ + Entry::create([ 'student_id' => $student->id, 'audition_id' => $audition2->id, + 'for_seating' => 1, + 'for_advancement' => 1, ]); } @@ -83,9 +91,11 @@ class EntrySeeder extends Seeder $audition->id)->where('id', '!=', $audition2->id)->inRandomOrder()->first(); } - Entry::factory()->create([ + Entry::create([ 'student_id' => $student->id, 'audition_id' => $audition3->id, + 'for_seating' => 1, + 'for_advancement' => 1, ]); } } From 13ca712ce986be46e1068005290862434a6551cb Mon Sep 17 00:00:00 2001 From: Matt Young Date: Tue, 10 Jun 2025 23:46:49 -0500 Subject: [PATCH 02/67] Add sheet_total column to score_sheets table --- ...eet_total_column_to_score_sheets_table.php | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 database/migrations/2025_06_11_043508_add_sheet_total_column_to_score_sheets_table.php diff --git a/database/migrations/2025_06_11_043508_add_sheet_total_column_to_score_sheets_table.php b/database/migrations/2025_06_11_043508_add_sheet_total_column_to_score_sheets_table.php new file mode 100644 index 0000000..9714f66 --- /dev/null +++ b/database/migrations/2025_06_11_043508_add_sheet_total_column_to_score_sheets_table.php @@ -0,0 +1,28 @@ +decimal('sheet_total', 9, 6)->after('subscores'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('score_sheets', function (Blueprint $table) { + $table->dropColumn('sheet_total'); + }); + } +}; From 78e90cbe25e5f758330342eee1dfce274858e3d8 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Wed, 11 Jun 2025 07:27:48 -0500 Subject: [PATCH 03/67] Rewrite enter score action --- app/Actions/Tabulation/EnterScore.php | 159 ++++++++++++-------------- 1 file changed, 76 insertions(+), 83 deletions(-) diff --git a/app/Actions/Tabulation/EnterScore.php b/app/Actions/Tabulation/EnterScore.php index 407f4cb..ab7be4b 100644 --- a/app/Actions/Tabulation/EnterScore.php +++ b/app/Actions/Tabulation/EnterScore.php @@ -7,11 +7,9 @@ namespace App\Actions\Tabulation; use App\Exceptions\ScoreEntryException; -use App\Models\CalculatedScore; use App\Models\Entry; use App\Models\ScoreSheet; use App\Models\User; -use Illuminate\Support\Collection; use Illuminate\Support\Facades\DB; class EnterScore @@ -20,92 +18,18 @@ class EnterScore * @param User $user A user acting as the judge for this sheet * @param Entry $entry An entry to which this score should be assigned * @param array $scores Scores to be entered in the form of SubscoreID => score + * @param ScoreSheet|false $scoreSheet If this is an update to an existing scoresheet, pass it here + * @return ScoreSheet The scoresheet that was created or updated * * @throws ScoreEntryException */ public function __invoke(User $user, Entry $entry, array $scores, ScoreSheet|false $scoreSheet = false): ScoreSheet { - CalculatedScore::where('entry_id', $entry->id)->delete(); + // TODO: Remove the CalculatedScore model and table when rewrite is complete, they'll be obsolete + // CalculatedScore::where('entry_id', $entry->id)->delete(); $scores = collect($scores); - $this->basicChecks($user, $entry, $scores); - $this->checkJudgeAssignment($user, $entry); - $this->checkForExistingScore($user, $entry, $scoreSheet); - $this->validateScoresSubmitted($entry, $scores); - $entry->removeFlag('no_show'); - if ($scoreSheet instanceof ScoreSheet) { - $scoreSheet->update([ - 'user_id' => $user->id, - 'entry_id' => $entry->id, - 'subscores' => $this->subscoresForStorage($entry, $scores), - ]); - } else { - $scoreSheet = ScoreSheet::create([ - 'user_id' => $user->id, - 'entry_id' => $entry->id, - 'subscores' => $this->subscoresForStorage($entry, $scores), - ]); - } - return $scoreSheet; - } - - protected function subscoresForStorage(Entry $entry, Collection $scores) - { - $subscores = []; - foreach ($entry->audition->scoringGuide->subscores as $subscore) { - $subscores[$subscore->id] = [ - 'score' => $scores[$subscore->id], - 'subscore_id' => $subscore->id, - 'subscore_name' => $subscore->name, - ]; - } - - return $subscores; - } - - protected function checkForExistingScore(User $user, Entry $entry, $existingScoreSheet) - { - if (! $existingScoreSheet) { - if (ScoreSheet::where('user_id', $user->id)->where('entry_id', $entry->id)->exists()) { - throw new ScoreEntryException('That judge has already entered scores for that entry'); - } - } else { - if ($existingScoreSheet->user_id !== $user->id) { - throw new ScoreEntryException('Existing score sheet is from a different judge'); - } - if ($existingScoreSheet->entry_id !== $entry->id) { - throw new ScoreEntryException('Existing score sheet is for a different entry'); - } - } - } - - protected function validateScoresSubmitted(Entry $entry, Collection $scores) - { - $subscoresRequired = $entry->audition->scoringGuide->subscores; - - foreach ($subscoresRequired as $subscore) { - // check that there is an element in the $scores collection with the key = $subscore->id - if (! $scores->keys()->contains($subscore->id)) { - throw new ScoreEntryException('Invalid Score Submission'); - } - if ($scores[$subscore->id] > $subscore->max_score) { - throw new ScoreEntryException('Supplied subscore exceeds maximum allowed'); - } - } - } - - protected function checkJudgeAssignment(User $user, Entry $entry) - { - $check = DB::table('room_user') - ->where('room_id', $entry->audition->room_id) - ->where('user_id', $user->id)->exists(); - if (! $check) { - throw new ScoreEntryException('This judge is not assigned to judge this entry'); - } - } - - protected function basicChecks(User $user, Entry $entry, Collection $scores) - { + // Basic Validity Checks if (! $user->exists()) { throw new ScoreEntryException('User does not exist'); } @@ -118,9 +42,78 @@ class EnterScore if ($entry->audition->hasFlag('advancement_published')) { throw new ScoreEntryException('Cannot score an entry in an audition with published advancement'); } - $requiredScores = $entry->audition->scoringGuide->subscores()->count(); - if ($scores->count() !== $requiredScores) { + + // Check that the specified user is assigned to judge this entry + $check = DB::table('room_user') + ->where('room_id', $entry->audition->room_id) + ->where('user_id', $user->id)->exists(); + if (! $check) { + throw new ScoreEntryException('This judge is not assigned to judge this entry'); + } + + // Check if a score already exists + if (! $scoreSheet) { + if (ScoreSheet::where('user_id', $user->id)->where('entry_id', $entry->id)->exists()) { + throw new ScoreEntryException('That judge has already entered scores for that entry'); + } + } else { + if ($scoreSheet->user_id !== $user->id) { + throw new ScoreEntryException('Existing score sheet is from a different judge'); + } + if ($scoreSheet->entry_id !== $entry->id) { + throw new ScoreEntryException('Existing score sheet is for a different entry'); + } + } + + // Check the validity of submitted subscores, format array for storage, and sum score + $subscoresRequired = $entry->audition->scoringGuide->subscores; + $subscoresStorageArray = []; + $scoreSheetTotal = 0; + $maxPossible = 0; + if ($scores->count() !== $subscoresRequired->count()) { throw new ScoreEntryException('Invalid number of scores'); } + + foreach ($subscoresRequired as $subscore) { + // check that there is an element in the $scores collection with the key = $subscore->id + if (! $scores->keys()->contains($subscore->id)) { + throw new ScoreEntryException('Invalid Score Submission'); + } + if ($scores[$subscore->id] > $subscore->max_score) { + throw new ScoreEntryException('Supplied subscore exceeds maximum allowed'); + } + + // Add subscore to the storage array + $subscoresStorageArray[$subscore->id] = [ + 'score' => $scores[$subscore->id], + 'subscore_id' => $subscore->id, + 'subscore_name' => $subscore->name, + ]; + + // Multiply subscore by weight and add to the total + $scoreSheetTotal += ($subscore->weight * $scores[$subscore->id]); + + // Add weight to total weights + $maxPossible += ($subscore->weight * $subscore->max_score); + } + + $entry->removeFlag('no_show'); + if ($scoreSheet instanceof ScoreSheet) { + $scoreSheet->update([ + 'user_id' => $user->id, + 'entry_id' => $entry->id, + 'subscores' => $subscoresStorageArray, + 'sheet_total' => ($scoreSheetTotal / $maxPossible) * 100, + ]); + } else { + $scoreSheet = ScoreSheet::create([ + 'user_id' => $user->id, + 'entry_id' => $entry->id, + 'subscores' => $subscoresStorageArray, + 'sheet_total' => ($scoreSheetTotal / $maxPossible) * 100, + ]); + } + + return $scoreSheet; } } From bcaab84dede55fa303f32e98776ba60837ac2448 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Tue, 10 Jun 2025 23:35:21 -0500 Subject: [PATCH 04/67] EntrySeeder update --- database/seeders/EntrySeeder.php | 48 +++++++++++++++++++------------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/database/seeders/EntrySeeder.php b/database/seeders/EntrySeeder.php index 9d8334a..c0acbfe 100644 --- a/database/seeders/EntrySeeder.php +++ b/database/seeders/EntrySeeder.php @@ -17,51 +17,59 @@ class EntrySeeder extends Seeder public function run(): void { $students = Student::all(); - $hs_auditions = Audition::where('maximum_grade', '=', '12'); - $freshman_auditions = Audition::where('maximum_grade', '>', '8'); - $jh_auditions = Audition::where('maximum_grade', '=', '9'); - $seventh_auditions = Audition::where('maximum_grade', '=', '7'); foreach ($students as $student) { if ($student->grade > 9) { - $audition = Audition::where('maximum_grade', '=', '12')->inRandomOrder()->first(); + $audition = Audition::where('maximum_grade', '=', '12') + ->inRandomOrder()->first(); } if ($student->grade == 9) { - $audition = Audition::where('maximum_grade', '>', '8')->inRandomOrder()->first(); + $audition = Audition::where('maximum_grade', '>', '8') + ->inRandomOrder()->first(); } if ($student->grade == 8) { - $audition = Audition::where('maximum_grade', '=', '9')->inRandomOrder()->first(); + $audition = Audition::where('maximum_grade', '=', '9') + ->inRandomOrder()->first(); } if ($student->grade == 7) { - $audition = Audition::where('maximum_grade', '=', '7')->inRandomOrder()->first(); + $audition = Audition::where('maximum_grade', '=', '7') + ->inRandomOrder()->first(); } - Entry::factory()->create([ + Entry::create([ 'student_id' => $student->id, 'audition_id' => $audition->id, + 'for_seating' => 1, + 'for_advancement' => 1, ]); if (mt_rand(1, 100) > 90) { if ($student->grade > 9) { - $audition2 = Audition::where('maximum_grade', '=', '12')->where('id', '!=', - $audition->id)->inRandomOrder()->first(); + $audition2 = Audition::where('maximum_grade', '=', '12') + ->where('id', '!=', $audition->id) + ->inRandomOrder()->first(); } if ($student->grade == 9) { - $audition2 = Audition::where('maximum_grade', '>', '8')->where('id', '!=', - $audition->id)->inRandomOrder()->first(); + $audition2 = Audition::where('maximum_grade', '>', '8') + ->where('id', '!=', $audition->id) + ->inRandomOrder()->first(); } if ($student->grade == 8) { - $audition2 = Audition::where('maximum_grade', '=', '9')->where('id', '!=', - $audition->id)->inRandomOrder()->first(); + $audition2 = Audition::where('maximum_grade', '=', '9') + ->where('id', '!=', $audition->id) + ->inRandomOrder()->first(); } if ($student->grade == 7) { - $audition2 = Audition::where('maximum_grade', '=', '7')->where('id', '!=', - $audition->id)->inRandomOrder()->first(); + $audition2 = Audition::where('maximum_grade', '=', '7') + ->where('id', '!=', $audition->id) + ->inRandomOrder()->first(); } - Entry::factory()->create([ + Entry::create([ 'student_id' => $student->id, 'audition_id' => $audition2->id, + 'for_seating' => 1, + 'for_advancement' => 1, ]); } @@ -83,9 +91,11 @@ class EntrySeeder extends Seeder $audition->id)->where('id', '!=', $audition2->id)->inRandomOrder()->first(); } - Entry::factory()->create([ + Entry::create([ 'student_id' => $student->id, 'audition_id' => $audition3->id, + 'for_seating' => 1, + 'for_advancement' => 1, ]); } } From d95e8e577206943ba8758d799a78dc91761c2f3c Mon Sep 17 00:00:00 2001 From: Matt Young Date: Tue, 10 Jun 2025 23:46:49 -0500 Subject: [PATCH 05/67] Add sheet_total column to score_sheets table --- ...eet_total_column_to_score_sheets_table.php | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 database/migrations/2025_06_11_043508_add_sheet_total_column_to_score_sheets_table.php diff --git a/database/migrations/2025_06_11_043508_add_sheet_total_column_to_score_sheets_table.php b/database/migrations/2025_06_11_043508_add_sheet_total_column_to_score_sheets_table.php new file mode 100644 index 0000000..9714f66 --- /dev/null +++ b/database/migrations/2025_06_11_043508_add_sheet_total_column_to_score_sheets_table.php @@ -0,0 +1,28 @@ +decimal('sheet_total', 9, 6)->after('subscores'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('score_sheets', function (Blueprint $table) { + $table->dropColumn('sheet_total'); + }); + } +}; From df732c4f5a92ac4af6110da87bf2753e71dabeb6 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Wed, 11 Jun 2025 07:27:48 -0500 Subject: [PATCH 06/67] Rewrite enter score action --- app/Actions/Tabulation/EnterScore.php | 159 ++++++++++++-------------- 1 file changed, 76 insertions(+), 83 deletions(-) diff --git a/app/Actions/Tabulation/EnterScore.php b/app/Actions/Tabulation/EnterScore.php index 407f4cb..ab7be4b 100644 --- a/app/Actions/Tabulation/EnterScore.php +++ b/app/Actions/Tabulation/EnterScore.php @@ -7,11 +7,9 @@ namespace App\Actions\Tabulation; use App\Exceptions\ScoreEntryException; -use App\Models\CalculatedScore; use App\Models\Entry; use App\Models\ScoreSheet; use App\Models\User; -use Illuminate\Support\Collection; use Illuminate\Support\Facades\DB; class EnterScore @@ -20,92 +18,18 @@ class EnterScore * @param User $user A user acting as the judge for this sheet * @param Entry $entry An entry to which this score should be assigned * @param array $scores Scores to be entered in the form of SubscoreID => score + * @param ScoreSheet|false $scoreSheet If this is an update to an existing scoresheet, pass it here + * @return ScoreSheet The scoresheet that was created or updated * * @throws ScoreEntryException */ public function __invoke(User $user, Entry $entry, array $scores, ScoreSheet|false $scoreSheet = false): ScoreSheet { - CalculatedScore::where('entry_id', $entry->id)->delete(); + // TODO: Remove the CalculatedScore model and table when rewrite is complete, they'll be obsolete + // CalculatedScore::where('entry_id', $entry->id)->delete(); $scores = collect($scores); - $this->basicChecks($user, $entry, $scores); - $this->checkJudgeAssignment($user, $entry); - $this->checkForExistingScore($user, $entry, $scoreSheet); - $this->validateScoresSubmitted($entry, $scores); - $entry->removeFlag('no_show'); - if ($scoreSheet instanceof ScoreSheet) { - $scoreSheet->update([ - 'user_id' => $user->id, - 'entry_id' => $entry->id, - 'subscores' => $this->subscoresForStorage($entry, $scores), - ]); - } else { - $scoreSheet = ScoreSheet::create([ - 'user_id' => $user->id, - 'entry_id' => $entry->id, - 'subscores' => $this->subscoresForStorage($entry, $scores), - ]); - } - return $scoreSheet; - } - - protected function subscoresForStorage(Entry $entry, Collection $scores) - { - $subscores = []; - foreach ($entry->audition->scoringGuide->subscores as $subscore) { - $subscores[$subscore->id] = [ - 'score' => $scores[$subscore->id], - 'subscore_id' => $subscore->id, - 'subscore_name' => $subscore->name, - ]; - } - - return $subscores; - } - - protected function checkForExistingScore(User $user, Entry $entry, $existingScoreSheet) - { - if (! $existingScoreSheet) { - if (ScoreSheet::where('user_id', $user->id)->where('entry_id', $entry->id)->exists()) { - throw new ScoreEntryException('That judge has already entered scores for that entry'); - } - } else { - if ($existingScoreSheet->user_id !== $user->id) { - throw new ScoreEntryException('Existing score sheet is from a different judge'); - } - if ($existingScoreSheet->entry_id !== $entry->id) { - throw new ScoreEntryException('Existing score sheet is for a different entry'); - } - } - } - - protected function validateScoresSubmitted(Entry $entry, Collection $scores) - { - $subscoresRequired = $entry->audition->scoringGuide->subscores; - - foreach ($subscoresRequired as $subscore) { - // check that there is an element in the $scores collection with the key = $subscore->id - if (! $scores->keys()->contains($subscore->id)) { - throw new ScoreEntryException('Invalid Score Submission'); - } - if ($scores[$subscore->id] > $subscore->max_score) { - throw new ScoreEntryException('Supplied subscore exceeds maximum allowed'); - } - } - } - - protected function checkJudgeAssignment(User $user, Entry $entry) - { - $check = DB::table('room_user') - ->where('room_id', $entry->audition->room_id) - ->where('user_id', $user->id)->exists(); - if (! $check) { - throw new ScoreEntryException('This judge is not assigned to judge this entry'); - } - } - - protected function basicChecks(User $user, Entry $entry, Collection $scores) - { + // Basic Validity Checks if (! $user->exists()) { throw new ScoreEntryException('User does not exist'); } @@ -118,9 +42,78 @@ class EnterScore if ($entry->audition->hasFlag('advancement_published')) { throw new ScoreEntryException('Cannot score an entry in an audition with published advancement'); } - $requiredScores = $entry->audition->scoringGuide->subscores()->count(); - if ($scores->count() !== $requiredScores) { + + // Check that the specified user is assigned to judge this entry + $check = DB::table('room_user') + ->where('room_id', $entry->audition->room_id) + ->where('user_id', $user->id)->exists(); + if (! $check) { + throw new ScoreEntryException('This judge is not assigned to judge this entry'); + } + + // Check if a score already exists + if (! $scoreSheet) { + if (ScoreSheet::where('user_id', $user->id)->where('entry_id', $entry->id)->exists()) { + throw new ScoreEntryException('That judge has already entered scores for that entry'); + } + } else { + if ($scoreSheet->user_id !== $user->id) { + throw new ScoreEntryException('Existing score sheet is from a different judge'); + } + if ($scoreSheet->entry_id !== $entry->id) { + throw new ScoreEntryException('Existing score sheet is for a different entry'); + } + } + + // Check the validity of submitted subscores, format array for storage, and sum score + $subscoresRequired = $entry->audition->scoringGuide->subscores; + $subscoresStorageArray = []; + $scoreSheetTotal = 0; + $maxPossible = 0; + if ($scores->count() !== $subscoresRequired->count()) { throw new ScoreEntryException('Invalid number of scores'); } + + foreach ($subscoresRequired as $subscore) { + // check that there is an element in the $scores collection with the key = $subscore->id + if (! $scores->keys()->contains($subscore->id)) { + throw new ScoreEntryException('Invalid Score Submission'); + } + if ($scores[$subscore->id] > $subscore->max_score) { + throw new ScoreEntryException('Supplied subscore exceeds maximum allowed'); + } + + // Add subscore to the storage array + $subscoresStorageArray[$subscore->id] = [ + 'score' => $scores[$subscore->id], + 'subscore_id' => $subscore->id, + 'subscore_name' => $subscore->name, + ]; + + // Multiply subscore by weight and add to the total + $scoreSheetTotal += ($subscore->weight * $scores[$subscore->id]); + + // Add weight to total weights + $maxPossible += ($subscore->weight * $subscore->max_score); + } + + $entry->removeFlag('no_show'); + if ($scoreSheet instanceof ScoreSheet) { + $scoreSheet->update([ + 'user_id' => $user->id, + 'entry_id' => $entry->id, + 'subscores' => $subscoresStorageArray, + 'sheet_total' => ($scoreSheetTotal / $maxPossible) * 100, + ]); + } else { + $scoreSheet = ScoreSheet::create([ + 'user_id' => $user->id, + 'entry_id' => $entry->id, + 'subscores' => $subscoresStorageArray, + 'sheet_total' => ($scoreSheetTotal / $maxPossible) * 100, + ]); + } + + return $scoreSheet; } } From e785d33a2dcf5bde2d80ae8901c3ef6db6b5c492 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Tue, 10 Jun 2025 23:35:21 -0500 Subject: [PATCH 07/67] EntrySeeder update # Conflicts: # database/seeders/EntrySeeder.php --- database/seeders/EntrySeeder.php | 89 +++++++++++++++++--------------- 1 file changed, 48 insertions(+), 41 deletions(-) diff --git a/database/seeders/EntrySeeder.php b/database/seeders/EntrySeeder.php index 54e710d..c0acbfe 100644 --- a/database/seeders/EntrySeeder.php +++ b/database/seeders/EntrySeeder.php @@ -17,80 +17,87 @@ class EntrySeeder extends Seeder public function run(): void { $students = Student::all(); - $hs_auditions = Audition::where('maximum_grade', '=', '12'); - $freshman_auditions = Audition::where('maximum_grade', '>', '8'); - $jh_auditions = Audition::where('maximum_grade', '=', '9'); - $seventh_auditions = Audition::where('maximum_grade', '=', '7'); foreach ($students as $student) { if ($student->grade > 9) { - $audition = Audition::where('maximum_grade', '=', '12')->inRandomOrder()->first(); + $audition = Audition::where('maximum_grade', '=', '12') + ->inRandomOrder()->first(); } if ($student->grade == 9) { - $audition = Audition::where('maximum_grade', '>', '8')->inRandomOrder()->first(); + $audition = Audition::where('maximum_grade', '>', '8') + ->inRandomOrder()->first(); } if ($student->grade == 8) { - $audition = Audition::where('maximum_grade', '=', '9')->inRandomOrder()->first(); + $audition = Audition::where('maximum_grade', '=', '9') + ->inRandomOrder()->first(); } if ($student->grade == 7) { - $audition = Audition::where('maximum_grade', '=', '7')->inRandomOrder()->first(); + $audition = Audition::where('maximum_grade', '=', '7') + ->inRandomOrder()->first(); } Entry::create([ 'student_id' => $student->id, 'audition_id' => $audition->id, + 'for_seating' => 1, + 'for_advancement' => 1, ]); if (mt_rand(1, 100) > 90) { if ($student->grade > 9) { - $audition2 = Audition::where('maximum_grade', '=', '12')->where('id', '!=', - $audition->id)->inRandomOrder()->first(); + $audition2 = Audition::where('maximum_grade', '=', '12') + ->where('id', '!=', $audition->id) + ->inRandomOrder()->first(); } if ($student->grade == 9) { - $audition2 = Audition::where('maximum_grade', '>', '8')->where('id', '!=', - $audition->id)->inRandomOrder()->first(); + $audition2 = Audition::where('maximum_grade', '>', '8') + ->where('id', '!=', $audition->id) + ->inRandomOrder()->first(); } if ($student->grade == 8) { - $audition2 = Audition::where('maximum_grade', '=', '9')->where('id', '!=', - $audition->id)->inRandomOrder()->first(); + $audition2 = Audition::where('maximum_grade', '=', '9') + ->where('id', '!=', $audition->id) + ->inRandomOrder()->first(); } if ($student->grade == 7) { - $audition2 = Audition::where('maximum_grade', '=', '7')->where('id', '!=', - $audition->id)->inRandomOrder()->first(); + $audition2 = Audition::where('maximum_grade', '=', '7') + ->where('id', '!=', $audition->id) + ->inRandomOrder()->first(); } Entry::create([ 'student_id' => $student->id, 'audition_id' => $audition2->id, + 'for_seating' => 1, + 'for_advancement' => 1, ]); - - // Triplers are possible - if (mt_rand(1, 100) > 90) { - if ($student->grade > 9) { - $audition3 = Audition::where('maximum_grade', '=', '12')->where('id', '!=', - $audition->id)->where('id', '!=', $audition2->id)->inRandomOrder()->first(); - } - if ($student->grade == 9) { - $audition3 = Audition::where('maximum_grade', '>', '8')->where('id', '!=', - $audition->id)->where('id', '!=', $audition2->id)->inRandomOrder()->first(); - } - if ($student->grade == 8) { - $audition3 = Audition::where('maximum_grade', '=', '9')->where('id', '!=', - $audition->id)->where('id', '!=', $audition2->id)->inRandomOrder()->first(); - } - if ($student->grade == 7) { - $audition3 = Audition::where('maximum_grade', '=', '7')->where('id', '!=', - $audition->id)->where('id', '!=', $audition2->id)->inRandomOrder()->first(); - } - - Entry::create([ - 'student_id' => $student->id, - 'audition_id' => $audition3->id, - ]); - } } + if (mt_rand(1, 100) > 90) { + if ($student->grade > 9) { + $audition3 = Audition::where('maximum_grade', '=', '12')->where('id', '!=', + $audition->id)->where('id', '!=', $audition2->id)->inRandomOrder()->first(); + } + if ($student->grade == 9) { + $audition3 = Audition::where('maximum_grade', '>', '8')->where('id', '!=', + $audition->id)->where('id', '!=', $audition2->id)->inRandomOrder()->first(); + } + if ($student->grade == 8) { + $audition3 = Audition::where('maximum_grade', '=', '9')->where('id', '!=', + $audition->id)->where('id', '!=', $audition2->id)->inRandomOrder()->first(); + } + if ($student->grade == 7) { + $audition3 = Audition::where('maximum_grade', '=', '7')->where('id', '!=', + $audition->id)->where('id', '!=', $audition2->id)->inRandomOrder()->first(); + } + Entry::create([ + 'student_id' => $student->id, + 'audition_id' => $audition3->id, + 'for_seating' => 1, + 'for_advancement' => 1, + ]); + } } } } From be84a084cc4f0e0958d605949065dc5d4aff1b6e Mon Sep 17 00:00:00 2001 From: Matt Young Date: Tue, 10 Jun 2025 23:46:49 -0500 Subject: [PATCH 08/67] Add sheet_total column to score_sheets table --- ...eet_total_column_to_score_sheets_table.php | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 database/migrations/2025_06_11_043508_add_sheet_total_column_to_score_sheets_table.php diff --git a/database/migrations/2025_06_11_043508_add_sheet_total_column_to_score_sheets_table.php b/database/migrations/2025_06_11_043508_add_sheet_total_column_to_score_sheets_table.php new file mode 100644 index 0000000..9714f66 --- /dev/null +++ b/database/migrations/2025_06_11_043508_add_sheet_total_column_to_score_sheets_table.php @@ -0,0 +1,28 @@ +decimal('sheet_total', 9, 6)->after('subscores'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('score_sheets', function (Blueprint $table) { + $table->dropColumn('sheet_total'); + }); + } +}; From 1d8a3ce7394e6010ce47fa3f656d5ed090b8fd7b Mon Sep 17 00:00:00 2001 From: Matt Young Date: Wed, 11 Jun 2025 07:27:48 -0500 Subject: [PATCH 09/67] Rewrite enter score action --- app/Actions/Tabulation/EnterScore.php | 159 ++++++++++++-------------- 1 file changed, 76 insertions(+), 83 deletions(-) diff --git a/app/Actions/Tabulation/EnterScore.php b/app/Actions/Tabulation/EnterScore.php index 407f4cb..ab7be4b 100644 --- a/app/Actions/Tabulation/EnterScore.php +++ b/app/Actions/Tabulation/EnterScore.php @@ -7,11 +7,9 @@ namespace App\Actions\Tabulation; use App\Exceptions\ScoreEntryException; -use App\Models\CalculatedScore; use App\Models\Entry; use App\Models\ScoreSheet; use App\Models\User; -use Illuminate\Support\Collection; use Illuminate\Support\Facades\DB; class EnterScore @@ -20,92 +18,18 @@ class EnterScore * @param User $user A user acting as the judge for this sheet * @param Entry $entry An entry to which this score should be assigned * @param array $scores Scores to be entered in the form of SubscoreID => score + * @param ScoreSheet|false $scoreSheet If this is an update to an existing scoresheet, pass it here + * @return ScoreSheet The scoresheet that was created or updated * * @throws ScoreEntryException */ public function __invoke(User $user, Entry $entry, array $scores, ScoreSheet|false $scoreSheet = false): ScoreSheet { - CalculatedScore::where('entry_id', $entry->id)->delete(); + // TODO: Remove the CalculatedScore model and table when rewrite is complete, they'll be obsolete + // CalculatedScore::where('entry_id', $entry->id)->delete(); $scores = collect($scores); - $this->basicChecks($user, $entry, $scores); - $this->checkJudgeAssignment($user, $entry); - $this->checkForExistingScore($user, $entry, $scoreSheet); - $this->validateScoresSubmitted($entry, $scores); - $entry->removeFlag('no_show'); - if ($scoreSheet instanceof ScoreSheet) { - $scoreSheet->update([ - 'user_id' => $user->id, - 'entry_id' => $entry->id, - 'subscores' => $this->subscoresForStorage($entry, $scores), - ]); - } else { - $scoreSheet = ScoreSheet::create([ - 'user_id' => $user->id, - 'entry_id' => $entry->id, - 'subscores' => $this->subscoresForStorage($entry, $scores), - ]); - } - return $scoreSheet; - } - - protected function subscoresForStorage(Entry $entry, Collection $scores) - { - $subscores = []; - foreach ($entry->audition->scoringGuide->subscores as $subscore) { - $subscores[$subscore->id] = [ - 'score' => $scores[$subscore->id], - 'subscore_id' => $subscore->id, - 'subscore_name' => $subscore->name, - ]; - } - - return $subscores; - } - - protected function checkForExistingScore(User $user, Entry $entry, $existingScoreSheet) - { - if (! $existingScoreSheet) { - if (ScoreSheet::where('user_id', $user->id)->where('entry_id', $entry->id)->exists()) { - throw new ScoreEntryException('That judge has already entered scores for that entry'); - } - } else { - if ($existingScoreSheet->user_id !== $user->id) { - throw new ScoreEntryException('Existing score sheet is from a different judge'); - } - if ($existingScoreSheet->entry_id !== $entry->id) { - throw new ScoreEntryException('Existing score sheet is for a different entry'); - } - } - } - - protected function validateScoresSubmitted(Entry $entry, Collection $scores) - { - $subscoresRequired = $entry->audition->scoringGuide->subscores; - - foreach ($subscoresRequired as $subscore) { - // check that there is an element in the $scores collection with the key = $subscore->id - if (! $scores->keys()->contains($subscore->id)) { - throw new ScoreEntryException('Invalid Score Submission'); - } - if ($scores[$subscore->id] > $subscore->max_score) { - throw new ScoreEntryException('Supplied subscore exceeds maximum allowed'); - } - } - } - - protected function checkJudgeAssignment(User $user, Entry $entry) - { - $check = DB::table('room_user') - ->where('room_id', $entry->audition->room_id) - ->where('user_id', $user->id)->exists(); - if (! $check) { - throw new ScoreEntryException('This judge is not assigned to judge this entry'); - } - } - - protected function basicChecks(User $user, Entry $entry, Collection $scores) - { + // Basic Validity Checks if (! $user->exists()) { throw new ScoreEntryException('User does not exist'); } @@ -118,9 +42,78 @@ class EnterScore if ($entry->audition->hasFlag('advancement_published')) { throw new ScoreEntryException('Cannot score an entry in an audition with published advancement'); } - $requiredScores = $entry->audition->scoringGuide->subscores()->count(); - if ($scores->count() !== $requiredScores) { + + // Check that the specified user is assigned to judge this entry + $check = DB::table('room_user') + ->where('room_id', $entry->audition->room_id) + ->where('user_id', $user->id)->exists(); + if (! $check) { + throw new ScoreEntryException('This judge is not assigned to judge this entry'); + } + + // Check if a score already exists + if (! $scoreSheet) { + if (ScoreSheet::where('user_id', $user->id)->where('entry_id', $entry->id)->exists()) { + throw new ScoreEntryException('That judge has already entered scores for that entry'); + } + } else { + if ($scoreSheet->user_id !== $user->id) { + throw new ScoreEntryException('Existing score sheet is from a different judge'); + } + if ($scoreSheet->entry_id !== $entry->id) { + throw new ScoreEntryException('Existing score sheet is for a different entry'); + } + } + + // Check the validity of submitted subscores, format array for storage, and sum score + $subscoresRequired = $entry->audition->scoringGuide->subscores; + $subscoresStorageArray = []; + $scoreSheetTotal = 0; + $maxPossible = 0; + if ($scores->count() !== $subscoresRequired->count()) { throw new ScoreEntryException('Invalid number of scores'); } + + foreach ($subscoresRequired as $subscore) { + // check that there is an element in the $scores collection with the key = $subscore->id + if (! $scores->keys()->contains($subscore->id)) { + throw new ScoreEntryException('Invalid Score Submission'); + } + if ($scores[$subscore->id] > $subscore->max_score) { + throw new ScoreEntryException('Supplied subscore exceeds maximum allowed'); + } + + // Add subscore to the storage array + $subscoresStorageArray[$subscore->id] = [ + 'score' => $scores[$subscore->id], + 'subscore_id' => $subscore->id, + 'subscore_name' => $subscore->name, + ]; + + // Multiply subscore by weight and add to the total + $scoreSheetTotal += ($subscore->weight * $scores[$subscore->id]); + + // Add weight to total weights + $maxPossible += ($subscore->weight * $subscore->max_score); + } + + $entry->removeFlag('no_show'); + if ($scoreSheet instanceof ScoreSheet) { + $scoreSheet->update([ + 'user_id' => $user->id, + 'entry_id' => $entry->id, + 'subscores' => $subscoresStorageArray, + 'sheet_total' => ($scoreSheetTotal / $maxPossible) * 100, + ]); + } else { + $scoreSheet = ScoreSheet::create([ + 'user_id' => $user->id, + 'entry_id' => $entry->id, + 'subscores' => $subscoresStorageArray, + 'sheet_total' => ($scoreSheetTotal / $maxPossible) * 100, + ]); + } + + return $scoreSheet; } } From e47265badd7a9097ca5aacb017a59bd3f8d7845e Mon Sep 17 00:00:00 2001 From: Matt Young Date: Wed, 11 Jun 2025 15:41:37 -0500 Subject: [PATCH 10/67] EnterScore action working to add a total score when a score is saved by a judge. --- app/Actions/Tabulation/EnterScore.php | 3 ++- app/Models/ScoreSheet.php | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/Actions/Tabulation/EnterScore.php b/app/Actions/Tabulation/EnterScore.php index ab7be4b..84d7a24 100644 --- a/app/Actions/Tabulation/EnterScore.php +++ b/app/Actions/Tabulation/EnterScore.php @@ -106,11 +106,12 @@ class EnterScore 'sheet_total' => ($scoreSheetTotal / $maxPossible) * 100, ]); } else { + $finalTotal = ($scoreSheetTotal / $maxPossible) * 100; $scoreSheet = ScoreSheet::create([ 'user_id' => $user->id, 'entry_id' => $entry->id, 'subscores' => $subscoresStorageArray, - 'sheet_total' => ($scoreSheetTotal / $maxPossible) * 100, + 'sheet_total' => $finalTotal, ]); } diff --git a/app/Models/ScoreSheet.php b/app/Models/ScoreSheet.php index b8b4698..7b9076e 100644 --- a/app/Models/ScoreSheet.php +++ b/app/Models/ScoreSheet.php @@ -13,6 +13,7 @@ class ScoreSheet extends Model 'user_id', 'entry_id', 'subscores', + 'sheet_total', ]; protected $casts = ['subscores' => 'json']; From 036ed38d1933b27e5c212ff6a7544b8e587f1320 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Wed, 11 Jun 2025 17:57:53 -0500 Subject: [PATCH 11/67] Rewrite admin score entry to use the new action. --- .../Tabulation/ScoreController.php | 64 ++++++++----------- 1 file changed, 26 insertions(+), 38 deletions(-) diff --git a/app/Http/Controllers/Tabulation/ScoreController.php b/app/Http/Controllers/Tabulation/ScoreController.php index d9b5931..b7e2013 100644 --- a/app/Http/Controllers/Tabulation/ScoreController.php +++ b/app/Http/Controllers/Tabulation/ScoreController.php @@ -2,10 +2,13 @@ namespace App\Http\Controllers\Tabulation; +use App\Actions\Tabulation\EnterScore; +use App\Exceptions\ScoreEntryException; use App\Http\Controllers\Controller; use App\Models\CalculatedScore; use App\Models\Entry; use App\Models\ScoreSheet; +use App\Models\User; use Illuminate\Http\Request; use Illuminate\Support\Facades\Session; @@ -27,7 +30,8 @@ class ScoreController extends Controller return redirect()->back()->with('error', 'Cannot delete scores for an entry where seats are published'); } if ($score->entry->audition->hasFlag('advancement_published')) { - return redirect()->back()->with('error', 'Cannot delete scores for an entry where advancement is published'); + return redirect()->back()->with('error', + 'Cannot delete scores for an entry where advancement is published'); } $score->delete(); @@ -66,51 +70,35 @@ class ScoreController extends Controller compact('entry', 'judges', 'scoring_guide', 'subscores', 'existing_sheets')); } - public function saveEntryScoreSheet(Request $request, Entry $entry) + public function saveEntryScoreSheet(Request $request, Entry $entry, EnterScore $scoreRecorder) { - CalculatedScore::where('entry_id', $entry->id)->delete(); $publishedCheck = $this->checkIfPublished($entry); if ($publishedCheck) { return $publishedCheck; } + foreach ($request->all() as $key => $value) { + if (! str_contains($key, 'judge')) { + continue; + } + $judge_id = str_replace('judge', '', $key); + $judge = User::find($judge_id); + $existingScore = ScoreSheet::where('entry_id', $entry->id) + ->where('user_id', $judge->id)->first(); + if ($existingScore === null) { + $existingScore = false; + } + try { + $scoreRecorder($judge, $entry, $value, $existingScore); + } catch (ScoreEntryException $e) { + return redirect()->route('scores.entryScoreSheet', ['entry_id' => $entry->id]) + ->with('error', $e->getMessage()); + } + } + // Since we're entering a score, this apparently isn't a no show. $entry->removeFlag('no_show'); - $judges = $entry->audition->room->judges; - - $subscores = $entry->audition->scoringGuide->subscores->sortBy('tiebreak_order'); - $scoringGuide = $entry->audition->scoringGuide; - $preparedScoreSheets = []; - foreach ($judges as $judge) { - $preparedScoreSheets[$judge->id]['user_id'] = $judge->id; - $preparedScoreSheets[$judge->id]['entry_id'] = $entry->id; - - $scoreValidation = $scoringGuide->validateScores($request->input('judge'.$judge->id)); - if ($scoreValidation != 'success') { - return redirect(url()->previous())->with('error', - $judge->full_name().': '.$scoreValidation)->with('oldScores', $request->all()); - } - - $scoreSubmission = $request->input('judge'.$judge->id); - $scoresToSave = []; - foreach ($subscores as $subscore) { - $scoresToSave[$subscore->id] = [ - 'subscore_id' => $subscore->id, - 'subscore_name' => $subscore->name, - 'score' => intval($scoreSubmission[$subscore->id]), - ]; - } - $preparedScoreSheets[$judge->id]['scores'] = $scoresToSave; - } - foreach ($preparedScoreSheets as $sheet) { - ScoreSheet::updateOrCreate( - ['entry_id' => $sheet['entry_id'], 'user_id' => $sheet['user_id']], - ['subscores' => $sheet['scores']] - ); - } - // TODO rewrite to use EnterScore action or clear score cache - - return redirect()->route('scores.chooseEntry')->with('success', count($preparedScoreSheets).' Scores saved'); + return redirect()->route('scores.chooseEntry')->with('success', 'Scores saved'); } protected function checkIfPublished($entry) From 86f715f086185e9ec6e0e73c454c2592f2972f8a Mon Sep 17 00:00:00 2001 From: Matt Young Date: Wed, 11 Jun 2025 19:55:14 -0500 Subject: [PATCH 12/67] Add logging to EnterScore action. --- app/Actions/Tabulation/EnterScore.php | 22 ++++++++++++++++++++++ app/Models/AuditLogEntry.php | 3 ++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/app/Actions/Tabulation/EnterScore.php b/app/Actions/Tabulation/EnterScore.php index 84d7a24..9745e39 100644 --- a/app/Actions/Tabulation/EnterScore.php +++ b/app/Actions/Tabulation/EnterScore.php @@ -7,11 +7,14 @@ namespace App\Actions\Tabulation; use App\Exceptions\ScoreEntryException; +use App\Models\AuditLogEntry; use App\Models\Entry; use App\Models\ScoreSheet; use App\Models\User; use Illuminate\Support\Facades\DB; +use function auth; + class EnterScore { /** @@ -115,6 +118,25 @@ class EnterScore ]); } + // Log the score entry + $log_message = 'Entered Score for entry id '.$entry->id.'.
'; + $log_message .= 'Judge: '.$user->full_name().'
'; + foreach ($scoreSheet->subscores as $subscore) { + $log_message .= $subscore['subscore_name'].': '.$subscore['score'].'
'; + } + $log_message .= 'Total: '.$scoreSheet->sheet_total; + AuditLogEntry::create([ + 'user' => auth()->user()->email ?? 'no user', + 'ip_address' => request()->ip(), + 'message' => $log_message, + 'affected' => [ + 'entries' => [$entry->id], + 'users' => [$user->id], + 'auditions' => [$entry->audition_id], + 'students' => [$entry->student_id], + ], + ]); + return $scoreSheet; } } diff --git a/app/Models/AuditLogEntry.php b/app/Models/AuditLogEntry.php index 205fd5b..8208348 100644 --- a/app/Models/AuditLogEntry.php +++ b/app/Models/AuditLogEntry.php @@ -2,6 +2,7 @@ namespace App\Models; +use Carbon\Carbon; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; @@ -15,7 +16,7 @@ class AuditLogEntry extends Model public function getCreatedAtAttribute($value) { - return \Carbon\Carbon::parse($value) + return Carbon::parse($value) ->setTimezone('America/Chicago') ->format('M j, Y H:i:s'); } From 783ec991b3cf2aae62d396df24805203bed8cabe Mon Sep 17 00:00:00 2001 From: Matt Young Date: Wed, 11 Jun 2025 21:59:44 -0500 Subject: [PATCH 13/67] Rewrite EnterScore action to deal with both seating and advancement totals. --- app/Actions/Tabulation/EnterScore.php | 32 +++++++++++++------ app/Models/ScoreSheet.php | 3 +- ...eet_total_column_to_score_sheets_table.php | 8 +++-- 3 files changed, 30 insertions(+), 13 deletions(-) diff --git a/app/Actions/Tabulation/EnterScore.php b/app/Actions/Tabulation/EnterScore.php index 9745e39..17b87fa 100644 --- a/app/Actions/Tabulation/EnterScore.php +++ b/app/Actions/Tabulation/EnterScore.php @@ -71,8 +71,10 @@ class EnterScore // Check the validity of submitted subscores, format array for storage, and sum score $subscoresRequired = $entry->audition->scoringGuide->subscores; $subscoresStorageArray = []; - $scoreSheetTotal = 0; - $maxPossible = 0; + $seatingTotal = 0; + $seatingMaxPossible = 0; + $advancementTotal = 0; + $advancementMaxPossible = 0; if ($scores->count() !== $subscoresRequired->count()) { throw new ScoreEntryException('Invalid number of scores'); } @@ -93,12 +95,20 @@ class EnterScore 'subscore_name' => $subscore->name, ]; - // Multiply subscore by weight and add to the total - $scoreSheetTotal += ($subscore->weight * $scores[$subscore->id]); + // If included in seating, multiply by weight and add to the total and max possible + if ($subscore->for_seating) { + $seatingTotal += ($subscore->weight * $scores[$subscore->id]); + $seatingMaxPossible += ($subscore->weight * $subscore->max_score); + } - // Add weight to total weights - $maxPossible += ($subscore->weight * $subscore->max_score); + // If included in advancement, multiply by weight and add to the total and max possible + if ($subscore->for_advance) { + $advancementTotal += ($subscore->weight * $scores[$subscore->id]); + $advancementMaxPossible += ($subscore->weight * $subscore->max_score); + } } + $finalSeatingTotal = ($seatingMaxPossible === 0) ? 0 : (($seatingTotal / $seatingMaxPossible) * 100); + $finalAdvancementTotal = ($advancementMaxPossible === 0) ? 0 : (($advancementTotal / $advancementMaxPossible) * 100); $entry->removeFlag('no_show'); if ($scoreSheet instanceof ScoreSheet) { @@ -106,15 +116,16 @@ class EnterScore 'user_id' => $user->id, 'entry_id' => $entry->id, 'subscores' => $subscoresStorageArray, - 'sheet_total' => ($scoreSheetTotal / $maxPossible) * 100, + 'seating_total' => $finalSeatingTotal, + 'advancement_total' => $finalAdvancementTotal, ]); } else { - $finalTotal = ($scoreSheetTotal / $maxPossible) * 100; $scoreSheet = ScoreSheet::create([ 'user_id' => $user->id, 'entry_id' => $entry->id, 'subscores' => $subscoresStorageArray, - 'sheet_total' => $finalTotal, + 'seating_total' => $finalSeatingTotal, + 'advancement_total' => $finalAdvancementTotal, ]); } @@ -124,7 +135,8 @@ class EnterScore foreach ($scoreSheet->subscores as $subscore) { $log_message .= $subscore['subscore_name'].': '.$subscore['score'].'
'; } - $log_message .= 'Total: '.$scoreSheet->sheet_total; + $log_message .= 'Seating Total: '.$scoreSheet->seating_total.'
'; + $log_message .= 'Advancement Total: '.$scoreSheet->advancement_total.'
'; AuditLogEntry::create([ 'user' => auth()->user()->email ?? 'no user', 'ip_address' => request()->ip(), diff --git a/app/Models/ScoreSheet.php b/app/Models/ScoreSheet.php index 7b9076e..a5b7dd8 100644 --- a/app/Models/ScoreSheet.php +++ b/app/Models/ScoreSheet.php @@ -13,7 +13,8 @@ class ScoreSheet extends Model 'user_id', 'entry_id', 'subscores', - 'sheet_total', + 'seating_total', + 'advancement_total', ]; protected $casts = ['subscores' => 'json']; diff --git a/database/migrations/2025_06_11_043508_add_sheet_total_column_to_score_sheets_table.php b/database/migrations/2025_06_11_043508_add_sheet_total_column_to_score_sheets_table.php index 9714f66..16a372a 100644 --- a/database/migrations/2025_06_11_043508_add_sheet_total_column_to_score_sheets_table.php +++ b/database/migrations/2025_06_11_043508_add_sheet_total_column_to_score_sheets_table.php @@ -12,7 +12,10 @@ return new class extends Migration public function up(): void { Schema::table('score_sheets', function (Blueprint $table) { - $table->decimal('sheet_total', 9, 6)->after('subscores'); + $table->decimal('seating_total', 9, 6)->after('subscores'); + }); + Schema::table('score_sheets', function (Blueprint $table) { + $table->decimal('advancement_total', 9, 6)->after('seating_total'); }); } @@ -22,7 +25,8 @@ return new class extends Migration public function down(): void { Schema::table('score_sheets', function (Blueprint $table) { - $table->dropColumn('sheet_total'); + $table->dropColumn('seating_total'); + $table->dropColumn('advancement_total'); }); } }; From 58f29f326c13835bbc87c4d9bb1b92ed60641da2 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Wed, 11 Jun 2025 23:03:35 -0500 Subject: [PATCH 14/67] Remove depricated files --- app/Actions/Tabulation/AllJudgesCount.php | 99 ----------- .../Tabulation/AllowForOlympicScoring.php | 161 ------------------ .../Tabulation/CalculateEntryScore.php | 11 -- .../Tabulation/CalculateScoreSheetTotal.php | 11 -- ...ateScoreSheetTotalDivideByTotalWeights.php | 67 -------- ...coreSheetTotalDivideByWeightedPossible.php | 74 -------- app/Models/CalculatedScore.php | 15 -- 7 files changed, 438 deletions(-) delete mode 100644 app/Actions/Tabulation/AllJudgesCount.php delete mode 100644 app/Actions/Tabulation/AllowForOlympicScoring.php delete mode 100644 app/Actions/Tabulation/CalculateEntryScore.php delete mode 100644 app/Actions/Tabulation/CalculateScoreSheetTotal.php delete mode 100644 app/Actions/Tabulation/CalculateScoreSheetTotalDivideByTotalWeights.php delete mode 100644 app/Actions/Tabulation/CalculateScoreSheetTotalDivideByWeightedPossible.php delete mode 100644 app/Models/CalculatedScore.php diff --git a/app/Actions/Tabulation/AllJudgesCount.php b/app/Actions/Tabulation/AllJudgesCount.php deleted file mode 100644 index 0089211..0000000 --- a/app/Actions/Tabulation/AllJudgesCount.php +++ /dev/null @@ -1,99 +0,0 @@ -calculator = $calculator; - $this->auditionService = $auditionService; - $this->entryService = $entryService; - } - - public function calculate(string $mode, Entry $entry): array - { - - $cacheKey = 'entryScore-'.$entry->id.'-'.$mode; - - return Cache::remember($cacheKey, 300, function () use ($mode, $entry) { - $this->isEntryANoShow($entry); - $this->basicValidation($mode, $entry); - $this->areAllJudgesIn($entry); - $this->areAllJudgesValid($entry); - - return $this->getJudgeTotals($mode, $entry); - }); - - } - - protected function getJudgeTotals($mode, Entry $entry) - { - - $scores = []; - foreach ($this->auditionService->getJudges($entry->audition) as $judge) { - $scores[] = $this->calculator->__invoke($mode, $entry, $judge); - } - $sums = []; - // Sum each subscore from the judges - foreach ($scores as $score) { - $index = 0; - foreach ($score as $value) { - $sums[$index] = $sums[$index] ?? 0; - $sums[$index] += $value; - $index++; - } - } - - return $sums; - } - - protected function basicValidation($mode, $entry): void - { - if ($mode !== 'seating' && $mode !== 'advancement') { - throw new TabulationException('Mode must be seating or advancement'); - } - - if (! $this->entryService->entryExists($entry)) { - throw new TabulationException('Invalid entry specified'); - } - } - - protected function areAllJudgesIn(Entry $entry): void - { - $assignedJudgeCount = $this->auditionService->getJudges($entry->audition)->count(); - if ($entry->scoreSheets->count() !== $assignedJudgeCount) { - throw new TabulationException('Not all score sheets are in'); - } - } - - protected function areAllJudgesValid(Entry $entry): void - { - $validJudgeIds = $this->auditionService->getJudges($entry->audition)->pluck('id')->sort()->toArray(); - $existingJudgeIds = $entry->scoreSheets->pluck('user_id')->sort()->toArray(); - if ($validJudgeIds !== $existingJudgeIds) { - throw new TabulationException('Score exists from a judge not assigned to this audition'); - } - } - - protected function isEntryANoShow(Entry $entry): void - { - if ($entry->hasFlag('no_show')) { - throw new TabulationException('No Show'); - } - } -} diff --git a/app/Actions/Tabulation/AllowForOlympicScoring.php b/app/Actions/Tabulation/AllowForOlympicScoring.php deleted file mode 100644 index 44c237b..0000000 --- a/app/Actions/Tabulation/AllowForOlympicScoring.php +++ /dev/null @@ -1,161 +0,0 @@ -calculator = $calculator; - $this->auditionService = $auditionService; - $this->entryService = $entryService; - } - - public function calculate(string $mode, Entry $entry): array - { - $calculated = CalculatedScore::where('entry_id', $entry->id)->where('mode', $mode)->first(); - if ($calculated) { - return $calculated->calculatedScore; - } - - $cacheKey = 'entryScore-'.$entry->id.'-'.$mode; - - return Cache::remember($cacheKey, 300, function () use ($mode, $entry) { - $this->basicValidation($mode, $entry); - $this->isEntryANoShow($entry); - $this->areAllJudgesIn($entry); - $this->areAllJudgesValid($entry); - $calculatedScores = $this->getJudgeTotals($mode, $entry); - CalculatedScore::create([ - 'entry_id' => $entry->id, - 'mode' => $mode, - 'calculatedScore' => $calculatedScores, - ]); - - return $calculatedScores; - // return $this->getJudgeTotals($mode, $entry); - }); - - } - - protected function getJudgeTotals($mode, Entry $entry): array - { - - $scores = []; - foreach ($this->auditionService->getJudges($entry->audition) as $judge) { - $scores[] = $this->calculator->__invoke($mode, $entry, $judge); - } - // sort the scores array by the total score - usort($scores, function ($a, $b) { - return $a[0] <=> $b[0]; - }); - - // we can only really do olympic scoring if there are at least 3 scores - if (count($scores) >= 3 && auditionSetting('olympic_scoring')) { - // remove the highest and lowest scores - array_pop($scores); - array_shift($scores); - } - $sums = []; - // Sum each subscore from the judges - foreach ($scores as $score) { - $index = 0; - foreach ($score as $value) { - $sums[$index] = $sums[$index] ?? 0; - $sums[$index] += $value; - $index++; - } - } - // add the bonus points for a seating mode - if ($mode === 'seating' && $sums) { - - $sums[0] += $this->getBonusPoints($entry); - } - - return $sums; - } - - protected function getBonusPoints(Entry $entry) - { - - $bonusScoreDefinition = $entry->audition->bonusScore()->first(); - if (! $bonusScoreDefinition) { - return 0; - } - /** @noinspection PhpPossiblePolymorphicInvocationInspection */ - $bonusJudges = $bonusScoreDefinition->judges; - $bonusScoreSheets = BonusScore::where('entry_id', $entry->id)->get(); - foreach ($bonusScoreSheets as $sheet) { - if (! $bonusJudges->contains($sheet->user_id)) { - throw new TabulationException('Entry has a bonus score from unassigned judge'); - } - } - - // sum the score property of the $bonusScoreSheets - return $bonusScoreSheets->sum('score'); - } - - protected function basicValidation($mode, $entry): void - { - if ($mode !== 'seating' && $mode !== 'advancement') { - throw new TabulationException('Mode must be seating or advancement'); - } - - if (! $this->entryService->entryExists($entry)) { - throw new TabulationException('Invalid entry specified'); - } - } - - protected function areAllJudgesIn(Entry $entry): void - { - $assignedJudgeCount = $this->auditionService->getJudges($entry->audition)->count(); - if ($entry->scoreSheets->count() !== $assignedJudgeCount) { - throw new TabulationException('Not all score sheets are in'); - } - } - - protected function areAllJudgesValid(Entry $entry): void - { - $validJudgeIds = $this->auditionService->getJudges($entry->audition)->pluck('id')->toArray(); - $existingJudgeIds = $entry->scoreSheets->pluck('user_id')->toArray(); - if (array_diff($existingJudgeIds, $validJudgeIds)) { - Log::debug('EntryID: '.$entry->id); - Log::debug('Valid judge ids: ('.gettype($validJudgeIds).') '.json_encode($validJudgeIds)); - Log::debug('Existing judge ids: ('.gettype($existingJudgeIds).') '.json_encode($existingJudgeIds)); - throw new TabulationException('Score exists from a judge not assigned to this audition'); - } - } - - protected function isEntryANoShow(Entry $entry): void - { - if ($entry->hasFlag('failed_prelim')) { - throw new TabulationException('Failed Prelim'); - } - - if ($entry->hasFlag('no_show')) { - throw new TabulationException('No Show'); - } - } -} diff --git a/app/Actions/Tabulation/CalculateEntryScore.php b/app/Actions/Tabulation/CalculateEntryScore.php deleted file mode 100644 index 8cb48a8..0000000 --- a/app/Actions/Tabulation/CalculateEntryScore.php +++ /dev/null @@ -1,11 +0,0 @@ -auditionService = $auditionService; - $this->entryService = $entryService; - $this->userService = $userService; - } - - public function __invoke(string $mode, Entry $entry, User $judge): array - { - $this->basicValidations($mode, $entry, $judge); - $scoreSheet = ScoreSheet::where('entry_id', $entry->id)->where('user_id', $judge->id)->first(); - if (! $scoreSheet) { - throw new TabulationException('No score sheet by that judge for that entry'); - } - $subscores = $this->auditionService->getSubscores($entry->audition, $mode); - $scoreTotal = 0; - $weightsTotal = 0; - $scoreArray = []; - foreach ($subscores as $subscore) { - $weight = $subscore['weight']; - $score = $scoreSheet->subscores[$subscore->id]['score']; - $scoreArray[] = $score; - $scoreTotal += ($score * $weight); - $weightsTotal += $weight; - } - $finalScore = $scoreTotal / $weightsTotal; - // put $final score at the beginning of the $ScoreArray - array_unshift($scoreArray, $finalScore); - - return $scoreArray; - } - - protected function basicValidations($mode, $entry, $judge): void - { - if ($mode !== 'seating' and $mode !== 'advancement') { - throw new TabulationException('Invalid mode requested. Mode must be seating or advancement'); - } - if (! $this->entryService->entryExists($entry)) { - throw new TabulationException('Invalid entry provided'); - } - if (! $this->userService->userExists($judge)) { - throw new TabulationException('Invalid judge provided'); - } - } -} diff --git a/app/Actions/Tabulation/CalculateScoreSheetTotalDivideByWeightedPossible.php b/app/Actions/Tabulation/CalculateScoreSheetTotalDivideByWeightedPossible.php deleted file mode 100644 index 55107bc..0000000 --- a/app/Actions/Tabulation/CalculateScoreSheetTotalDivideByWeightedPossible.php +++ /dev/null @@ -1,74 +0,0 @@ -auditionService = $auditionService; - $this->entryService = $entryService; - $this->userService = $userService; - } - - public function __invoke(string $mode, Entry $entry, User $judge): array - { - $this->basicValidations($mode, $entry, $judge); - $scoreSheet = ScoreSheet::where('entry_id', $entry->id)->where('user_id', $judge->id)->first(); - if (! $scoreSheet) { - throw new TabulationException('No score sheet by that judge for that entry'); - } - $subscores = $this->auditionService->getSubscores($entry->audition, $mode); - $scoreTotal = 0; - $weightsTotal = 0; - $weightedMaxPossible = 0; - $scoreArray = []; - foreach ($subscores as $subscore) { - $weight = $subscore['weight']; - $score = $scoreSheet->subscores[$subscore->id]['score']; - $maxPossible = $subscore['max_score']; - $scoreArray[] = $score; - $scoreTotal += ($score * $weight); - $weightsTotal += $weight; - $weightedMaxPossible += $maxPossible; - } - if ($weightedMaxPossible > 0) { - $finalScore = ($scoreTotal / $weightedMaxPossible) * 100; - } else { - $finalScore = 0; - } - // put $final score at the beginning of the $ScoreArray - array_unshift($scoreArray, $finalScore); - - return $scoreArray; - } - - protected function basicValidations($mode, $entry, $judge): void - { - if ($mode !== 'seating' and $mode !== 'advancement') { - throw new TabulationException('Invalid mode requested. Mode must be seating or advancement'); - } - if (! $this->entryService->entryExists($entry)) { - throw new TabulationException('Invalid entry provided'); - } - if (! $this->userService->userExists($judge)) { - throw new TabulationException('Invalid judge provided'); - } - } -} diff --git a/app/Models/CalculatedScore.php b/app/Models/CalculatedScore.php deleted file mode 100644 index f514d8d..0000000 --- a/app/Models/CalculatedScore.php +++ /dev/null @@ -1,15 +0,0 @@ - 'json']; -} From 9331e61839aa151c284a972acf341335072c7d89 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Wed, 11 Jun 2025 23:03:49 -0500 Subject: [PATCH 15/67] Remove depricated table and create total_scores table --- ..._12_034045_drop_caculated_scores_table.php | 31 ++++++++++++++++++ ...06_12_034242_create_total_scores_table.php | 32 +++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 database/migrations/2025_06_12_034045_drop_caculated_scores_table.php create mode 100644 database/migrations/2025_06_12_034242_create_total_scores_table.php diff --git a/database/migrations/2025_06_12_034045_drop_caculated_scores_table.php b/database/migrations/2025_06_12_034045_drop_caculated_scores_table.php new file mode 100644 index 0000000..5dd6b4d --- /dev/null +++ b/database/migrations/2025_06_12_034045_drop_caculated_scores_table.php @@ -0,0 +1,31 @@ +id(); + $table->foreignIdFor(Entry::class)->constrained()->cascadeOnDelete()->cascadeOnUpdate(); + $table->string('mode'); + $table->json('calculatedScore'); + $table->timestamps(); + }); + } +}; diff --git a/database/migrations/2025_06_12_034242_create_total_scores_table.php b/database/migrations/2025_06_12_034242_create_total_scores_table.php new file mode 100644 index 0000000..0c5d89d --- /dev/null +++ b/database/migrations/2025_06_12_034242_create_total_scores_table.php @@ -0,0 +1,32 @@ +id(); + $table->foreignIdFor(Entry::class)->constrained()->cascadeOnDelete()->cascadeOnUpdate(); + $table->decimal('seating_total', 9, 6)->after('subscores'); + $table->decimal('advancement_total', 9, 6)->after('seating_total'); + $table->json('subscore_totals'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('total_scores'); + } +}; From e800937f4df643c55893c791d0250681638ddc20 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Wed, 11 Jun 2025 23:22:46 -0500 Subject: [PATCH 16/67] remove erroroneous file --- ...06_12_034242_create_total_scores_table.php | 32 ------------------- 1 file changed, 32 deletions(-) delete mode 100644 database/migrations/2025_06_12_034242_create_total_scores_table.php diff --git a/database/migrations/2025_06_12_034242_create_total_scores_table.php b/database/migrations/2025_06_12_034242_create_total_scores_table.php deleted file mode 100644 index 0c5d89d..0000000 --- a/database/migrations/2025_06_12_034242_create_total_scores_table.php +++ /dev/null @@ -1,32 +0,0 @@ -id(); - $table->foreignIdFor(Entry::class)->constrained()->cascadeOnDelete()->cascadeOnUpdate(); - $table->decimal('seating_total', 9, 6)->after('subscores'); - $table->decimal('advancement_total', 9, 6)->after('seating_total'); - $table->json('subscore_totals'); - $table->timestamps(); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('total_scores'); - } -}; From f0f8038e8a5e368a36e50ae3bfd3986c179f3d96 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Wed, 11 Jun 2025 23:32:16 -0500 Subject: [PATCH 17/67] Create entry total score model and table. --- app/Models/EntryTotalScore.php | 11 +++++++ ...042434_create_entry_total_scores_table.php | 32 +++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 app/Models/EntryTotalScore.php create mode 100644 database/migrations/2025_06_12_042434_create_entry_total_scores_table.php diff --git a/app/Models/EntryTotalScore.php b/app/Models/EntryTotalScore.php new file mode 100644 index 0000000..2a5c598 --- /dev/null +++ b/app/Models/EntryTotalScore.php @@ -0,0 +1,11 @@ +id(); + $table->foreignIdFor(Entry::class)->constrained()->cascadeOnDelete()->cascadeOnUpdate(); + $table->decimal('seating_total', 9, 6); + $table->decimal('advancement_total', 9, 6); + $table->json('subscore_totals'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('entry_total_scores'); + } +}; From 3c545f0dce5d81c392b94df393ff542d49ae3610 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Thu, 12 Jun 2025 00:09:05 -0500 Subject: [PATCH 18/67] Make changes to total scores table to have subscores for seating and for advancement. --- .../2025_06_12_042434_create_entry_total_scores_table.php | 3 ++- routes/web.php | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/database/migrations/2025_06_12_042434_create_entry_total_scores_table.php b/database/migrations/2025_06_12_042434_create_entry_total_scores_table.php index 308408d..467c2dd 100644 --- a/database/migrations/2025_06_12_042434_create_entry_total_scores_table.php +++ b/database/migrations/2025_06_12_042434_create_entry_total_scores_table.php @@ -17,7 +17,8 @@ return new class extends Migration $table->foreignIdFor(Entry::class)->constrained()->cascadeOnDelete()->cascadeOnUpdate(); $table->decimal('seating_total', 9, 6); $table->decimal('advancement_total', 9, 6); - $table->json('subscore_totals'); + $table->json('seating_subscore_totals'); + $table->json('advancement_subscore_totals'); $table->timestamps(); }); } diff --git a/routes/web.php b/routes/web.php index 8ca8d03..8221a7c 100644 --- a/routes/web.php +++ b/routes/web.php @@ -12,7 +12,7 @@ require __DIR__.'/tabulation.php'; require __DIR__.'/user.php'; require __DIR__.'/nominationEnsemble.php'; -Route::get('/test', [TestController::class, 'flashTest'])->middleware('auth', 'verified'); +Route::get('/test', [TestController::class, 'test'])->middleware('auth', 'verified'); Route::view('/home', 'welcome')->middleware('guest')->name('landing'); Route::view('/', 'landing')->name('home'); From e79e7e222d9153d3e8668820aca0a9667aa41a13 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Thu, 12 Jun 2025 01:11:08 -0500 Subject: [PATCH 19/67] Add action to total an entries scores. --- app/Actions/Tabulation/TotalEntryScores.php | 72 +++++++++++++++++++++ app/Models/EntryTotalScore.php | 5 ++ 2 files changed, 77 insertions(+) create mode 100644 app/Actions/Tabulation/TotalEntryScores.php diff --git a/app/Actions/Tabulation/TotalEntryScores.php b/app/Actions/Tabulation/TotalEntryScores.php new file mode 100644 index 0000000..be6e305 --- /dev/null +++ b/app/Actions/Tabulation/TotalEntryScores.php @@ -0,0 +1,72 @@ +id)->delete(); + } + // bail out if a total score is already calculated + if (EntryTotalScore::where('entry_id', $entry->id)->count() > 0) { + return; + } + $requiredSubscores = $entry->audition->scoringGuide->subscores; + $newTotaledScore = EntryTotalScore::make(); + $newTotaledScore->entry_id = $entry->id; + + // deal with seating scores + $scoreSheets = ScoreSheet::where('entry_id', $entry->id)->orderBy('seating_total', 'desc')->get(); + if (auditionSetting('olympic_scoring' && $scoreSheets->count() > 2)) { + // under olympic scoring, drop the first and last element + $scoreSheets->shift(); + $scoreSheets->pop(); + } + $newTotaledScore->seating_total = $scoreSheets->avg('seating_total'); + $seatingSubscores = $requiredSubscores + ->filter(fn ($subscore) => $subscore->for_seating == true) + ->sortBy('tiebreak_order'); + $total_seating_subscores = []; + foreach ($seatingSubscores as $subscore) { + $runningTotal = 0; + foreach ($scoreSheets as $scoreSheet) { + $runningTotal += $scoreSheet->subscores[$subscore->id]['score']; + } + $total_seating_subscores[] = $runningTotal / $scoreSheets->count(); + } + $newTotaledScore->seating_subscore_totals = $total_seating_subscores; + + // deal with advancement scores + $scoreSheets = ScoreSheet::where('entry_id', $entry->id)->orderBy('advancement_total', 'desc')->get(); + if (auditionSetting('olympic_scoring' && $scoreSheets->count() > 2)) { + // under olympic scoring, drop the first and last element + $scoreSheets->shift(); + $scoreSheets->pop(); + } + $newTotaledScore->advancement_total = $scoreSheets->avg('advancement_total'); + $advancement_subscores = $requiredSubscores + ->filter(fn ($subscore) => $subscore->for_advance == true) + ->sortBy('tiebreak_order'); + $total_advancement_subscores = []; + foreach ($advancement_subscores as $subscore) { + $runningTotal = 0; + foreach ($scoreSheets as $scoreSheet) { + $runningTotal += $scoreSheet->subscores[$subscore->id]['score']; + } + $total_advancement_subscores[] = $runningTotal / $scoreSheets->count(); + } + $newTotaledScore->advancement_subscore_totals = $total_advancement_subscores; + $newTotaledScore->save(); + dd($newTotaledScore); + } +} diff --git a/app/Models/EntryTotalScore.php b/app/Models/EntryTotalScore.php index 2a5c598..ca07e2b 100644 --- a/app/Models/EntryTotalScore.php +++ b/app/Models/EntryTotalScore.php @@ -8,4 +8,9 @@ use Illuminate\Database\Eloquent\Model; class EntryTotalScore extends Model { use HasFactory; + + protected $casts = [ + 'seating_subscore_totals' => 'json', + 'advancement_subscore_totals' => 'json', + ]; } From 8647a66df80cae3d8c6e67052b4e330781bcdd06 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Thu, 12 Jun 2025 08:47:04 -0500 Subject: [PATCH 20/67] Define Entry and EntryTotalScore relationships --- app/Models/Entry.php | 20 ++++++-------------- app/Models/EntryTotalScore.php | 6 ++++++ 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/app/Models/Entry.php b/app/Models/Entry.php index 2bcde85..d4cc57d 100644 --- a/app/Models/Entry.php +++ b/app/Models/Entry.php @@ -17,16 +17,13 @@ class Entry extends Model protected $guarded = []; - protected $hasCheckedScoreSheets = false; - - public $final_scores_array; // Set by TabulationService - - public $scoring_complete; // Set by TabulationService - - public $is_doubler; // Set by DoublerService - protected $with = ['flags']; + public function totalScore(): HasOne + { + return $this->hasOne(EntryTotalScore::class); + } + public function student(): BelongsTo { return $this->belongsTo(Student::class); @@ -98,7 +95,7 @@ class Entry extends Model public function removeFlag($flag): void { - // remove related auditionFlag where flag_name = $flag + // remove the related auditionFlag where flag_name = $flag $this->flags()->where('flag_name', $flag)->delete(); $this->load('flags'); } @@ -120,11 +117,6 @@ class Entry extends Model return $this->hasOne(Seat::class); } - public function calculatedScores(): HasMany - { - return $this->hasMany(CalculatedScore::class); - } - public function scopeForSeating(Builder $query): void { $query->where('for_seating', 1); diff --git a/app/Models/EntryTotalScore.php b/app/Models/EntryTotalScore.php index ca07e2b..9ec59ea 100644 --- a/app/Models/EntryTotalScore.php +++ b/app/Models/EntryTotalScore.php @@ -4,6 +4,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; class EntryTotalScore extends Model { @@ -13,4 +14,9 @@ class EntryTotalScore extends Model 'seating_subscore_totals' => 'json', 'advancement_subscore_totals' => 'json', ]; + + public function entry(): BelongsTo + { + return $this->belongsTo(Entry::class); + } } From fd198a9972ddeb492555c2abba6b3a2650e26a46 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Thu, 12 Jun 2025 19:03:03 -0500 Subject: [PATCH 21/67] Calculate all total scores for an audition. --- .../Tabulation/CalculateAuditionScores.php | 30 +++++++++++++++++++ app/Actions/Tabulation/TotalEntryScores.php | 5 +++- app/Models/ScoreSheet.php | 29 ------------------ app/Providers/AppServiceProvider.php | 9 +++--- 4 files changed, 38 insertions(+), 35 deletions(-) create mode 100644 app/Actions/Tabulation/CalculateAuditionScores.php diff --git a/app/Actions/Tabulation/CalculateAuditionScores.php b/app/Actions/Tabulation/CalculateAuditionScores.php new file mode 100644 index 0000000..72d774c --- /dev/null +++ b/app/Actions/Tabulation/CalculateAuditionScores.php @@ -0,0 +1,30 @@ +judges->count(); + $pending_entries = Entry::where('audition_id', $audition->id) + ->has('scoreSheets', '=', $scores_required) + ->whereDoesntHave('totalScore') + ->with('audition.scoringGuide.subscores') + ->get(); + foreach ($pending_entries as $entry) { + Debugbar::debug('Calculating scores for entry: '.$entry->id); + $totaler->__invoke($entry); + } + } +} diff --git a/app/Actions/Tabulation/TotalEntryScores.php b/app/Actions/Tabulation/TotalEntryScores.php index be6e305..241a9af 100644 --- a/app/Actions/Tabulation/TotalEntryScores.php +++ b/app/Actions/Tabulation/TotalEntryScores.php @@ -6,6 +6,10 @@ use App\Models\Entry; use App\Models\EntryTotalScore; use App\Models\ScoreSheet; +/** + * Handles the calculation of a total score for an entry, including seating and advancement scores, + * based on scoring sheets and subscores defined in the audition's scoring guide. + */ class TotalEntryScores { public function __construct() @@ -67,6 +71,5 @@ class TotalEntryScores } $newTotaledScore->advancement_subscore_totals = $total_advancement_subscores; $newTotaledScore->save(); - dd($newTotaledScore); } } diff --git a/app/Models/ScoreSheet.php b/app/Models/ScoreSheet.php index a5b7dd8..e025272 100644 --- a/app/Models/ScoreSheet.php +++ b/app/Models/ScoreSheet.php @@ -5,7 +5,6 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasOneThrough; -use Illuminate\Support\Facades\Cache; class ScoreSheet extends Model { @@ -19,22 +18,6 @@ class ScoreSheet extends Model protected $casts = ['subscores' => 'json']; - protected static function boot() - { - parent::boot(); - static::created(function ($scoreSheet) { - $scoreSheet->deleteRelatedCalculatedScores(); - }); - - static::updated(function ($scoreSheet) { - $scoreSheet->deleteRelatedCalculatedScores(); - }); - - static::deleted(function ($scoreSheet) { - $scoreSheet->deleteRelatedCalculatedScores(); - }); - } - public function entry(): BelongsTo { return $this->belongsTo(Entry::class); @@ -62,16 +45,4 @@ class ScoreSheet extends Model return $this->subscores[$id]['score'] ?? false; // this function is used at resources/views/tabulation/entry_score_sheet.blade.php } - - public function deleteRelatedCalculatedScores(): void - { - $entry = $this->entry; - if ($entry) { - $entry->calculatedScores()->delete(); - Cache::forget('entryScore-'.$entry->id.'-seating'); - Cache::forget('entryScore-'.$entry->id.'-advancement'); - Cache::forget('audition'.$entry->audition_id.'seating'); - Cache::forget('audition'.$entry->audition_id.'advancement'); - } - } } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 5211a7e..1ff4c7b 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -5,11 +5,10 @@ namespace App\Providers; use App\Actions\Entries\CreateEntry; use App\Actions\Entries\UpdateEntry; use App\Actions\Schools\SetHeadDirector; -use App\Actions\Tabulation\AllowForOlympicScoring; -use App\Actions\Tabulation\CalculateEntryScore; +use App\Actions\Tabulation\CalculateAuditionScores; use App\Actions\Tabulation\CalculateScoreSheetTotal; use App\Actions\Tabulation\CalculateScoreSheetTotalDivideByTotalWeights; -use App\Actions\Tabulation\CalculateScoreSheetTotalDivideByWeightedPossible; +use App\Actions\Tabulation\TotalEntryScores; use App\Http\Controllers\NominationEnsembles\NominationAdminController; use App\Http\Controllers\NominationEnsembles\NominationEnsembleController; use App\Http\Controllers\NominationEnsembles\NominationEnsembleEntryController; @@ -58,8 +57,6 @@ class AppServiceProvider extends ServiceProvider { //$this->app->singleton(CalculateScoreSheetTotal::class, CalculateScoreSheetTotal::class); //$this->app->singleton(CalculateScoreSheetTotal::class, CalculateScoreSheetTotalDivideByTotalWeights::class); - $this->app->singleton(CalculateScoreSheetTotal::class, CalculateScoreSheetTotalDivideByWeightedPossible::class); - $this->app->singleton(CalculateEntryScore::class, AllowForOlympicScoring::class); $this->app->singleton(DrawService::class, DrawService::class); $this->app->singleton(AuditionService::class, AuditionService::class); $this->app->singleton(EntryService::class, EntryService::class); @@ -69,6 +66,8 @@ class AppServiceProvider extends ServiceProvider $this->app->singleton(CreateEntry::class, CreateEntry::class); $this->app->singleton(UpdateEntry::class, UpdateEntry::class); $this->app->singleton(SetHeadDirector::class, SetHeadDirector::class); + $this->app->singleton(TotalEntryScores::class, TotalEntryScores::class); + $this->app->singleton(CalculateAuditionScores::class, CalculateAuditionScores::class); // Nomination Ensemble // $this->app->bind(NominationEnsembleController::class, ScobdaNominationEnsembleController::class); From 727d4d70483af843ca4b537d3ef9c29ade494dd0 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Thu, 12 Jun 2025 23:28:31 -0500 Subject: [PATCH 22/67] Initial work on seating page rewrite --- .../Tabulation/EntryFlagController.php | 4 +- .../Tabulation/SeatAuditionFormController.php | 198 +++++------------- .../SeatAuditionFormControllerOLD.php | 180 ++++++++++++++++ app/Models/Audition.php | 2 +- app/Models/EntryFlag.php | 28 --- app/Models/User.php | 6 +- .../tabulation/auditionSeating.blade.php | 139 ++++++++++-- .../tabulation/auditionSeatingOLD.blade.php | 30 +++ 8 files changed, 393 insertions(+), 194 deletions(-) create mode 100644 app/Http/Controllers/Tabulation/SeatAuditionFormControllerOLD.php create mode 100644 resources/views/tabulation/auditionSeatingOLD.blade.php diff --git a/app/Http/Controllers/Tabulation/EntryFlagController.php b/app/Http/Controllers/Tabulation/EntryFlagController.php index 249816b..a3ee379 100644 --- a/app/Http/Controllers/Tabulation/EntryFlagController.php +++ b/app/Http/Controllers/Tabulation/EntryFlagController.php @@ -81,14 +81,14 @@ class EntryFlagController extends Controller } DB::table('score_sheets')->where('entry_id', $entry->id)->delete(); - $entry->addFlag('no_show'); + ScoreSheet::where('entry_id', $entry->id)->delete(); - CalculatedScore::where('entry_id', $entry->id)->delete(); BonusScore::where('entry_id', $entry->id)->delete(); if (request()->input('noshow-type') == 'failprelim') { $msg = 'Failed prelim has been entered for '.$entry->audition->name.' #'.$entry->draw_number.' (ID: '.$entry->id.').'; $entry->addFlag('failed_prelim'); } else { + $entry->addFlag('no_show'); $msg = 'No Show has been entered for '.$entry->audition->name.' #'.$entry->draw_number.' (ID: '.$entry->id.').'; } diff --git a/app/Http/Controllers/Tabulation/SeatAuditionFormController.php b/app/Http/Controllers/Tabulation/SeatAuditionFormController.php index 84dcf06..f0c0307 100644 --- a/app/Http/Controllers/Tabulation/SeatAuditionFormController.php +++ b/app/Http/Controllers/Tabulation/SeatAuditionFormController.php @@ -2,163 +2,75 @@ namespace App\Http\Controllers\Tabulation; -use App\Actions\Entries\DoublerDecision; -use App\Actions\Tabulation\CalculateEntryScore; use App\Actions\Tabulation\GetAuditionSeats; -use App\Actions\Tabulation\RankAuditionEntries; -use App\Exceptions\AuditionAdminException; use App\Http\Controllers\Controller; use App\Models\Audition; -use App\Services\AuditionService; -use App\Services\DoublerService; -use App\Services\EntryService; use Illuminate\Http\Request; -use Illuminate\Support\Facades\Cache; - -use function redirect; class SeatAuditionFormController extends Controller { - protected CalculateEntryScore $calc; - - protected DoublerService $doublerService; - - protected RankAuditionEntries $ranker; - - protected EntryService $entryService; - - protected AuditionService $auditionService; - - protected DoublerDecision $decider; - - public function __construct( - CalculateEntryScore $calc, - RankAuditionEntries $ranker, - DoublerService $doublerService, - EntryService $entryService, - AuditionService $auditionService, - DoublerDecision $decider, - ) { - $this->calc = $calc; - $this->ranker = $ranker; - $this->doublerService = $doublerService; - $this->entryService = $entryService; - $this->auditionService = $auditionService; - $this->decider = $decider; - } - public function __invoke(Request $request, Audition $audition) { - // If a seating proposal was posted, deal wth it - if ($request->method() == 'POST' && $request->input('ensembleAccept')) { - $requestedEnsembleAccepts = $request->input('ensembleAccept'); - } else { - $requestedEnsembleAccepts = false; - } + // Get scored entries in order + $scored_entries = $audition->entries() + ->whereHas('totalScore') + ->with('totalScore') + ->with('student.school') + ->join('entry_total_scores', 'entries.id', '=', 'entry_total_scores.entry_id') + ->orderBy('entry_total_scores.seating_total', 'desc') + ->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.seating_subscore_totals, "$[0]"), -999999) DESC') + ->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.seating_subscore_totals, "$[1]"), -999999) DESC') + ->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.seating_subscore_totals, "$[2]"), -999999) DESC') + ->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.seating_subscore_totals, "$[3]"), -999999) DESC') + ->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.seating_subscore_totals, "$[4]"), -999999) DESC') + ->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.seating_subscore_totals, "$[5]"), -999999) DESC') + ->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.seating_subscore_totals, "$[6]"), -999999) DESC') + ->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.seating_subscore_totals, "$[7]"), -999999) DESC') + ->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.seating_subscore_totals, "$[8]"), -999999) DESC') + ->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.seating_subscore_totals, "$[9]"), -999999) DESC') + ->select('entries.*') + ->get(); - // Deal with a mass no-show request - if ($request->input('mass-no-show')) { - $entries = $audition->entries()->forSeating()->withCount('scoreSheets')->with('flags')->get(); - foreach ($entries as $entry) { - if ($entry->scoreSheets_count == 0 && ! $entry->hasFlag('no_show')) { - $entry->addFlag('no_show'); - } - Cache::forget('entryScore-'.$entry->id.'-seating'); - Cache::forget('entryScore-'.$entry->id.'-advancement'); - } - Cache::forget('audition'.$audition->id.'seating'); - Cache::forget('audition'.$audition->id.'advancement'); - } + // Get unscored entries sorted by draw number + $unscored_entries = $audition->entries() + ->whereDoesntHave('totalScore') + ->whereDoesntHave('flags', function ($query) { + $query->where('flag_name', 'no_show'); + }) + ->whereDoesntHave('flags', function ($query) { + $query->where('flag_name', 'failed_prelim'); + }) + ->with('student.school') + ->orderBy('draw_number', 'asc') + ->get(); - $entryData = []; - $entries = $this->ranker->rank('seating', $audition); + // Get no show entries sorted by draw number + $noshow_entries = $audition->entries() + ->whereDoesntHave('totalScore') + ->whereHas('flags', function ($query) { + $query->where('flag_name', 'no_show'); + }) + ->with('student.school') + ->orderBy('draw_number', 'asc') + ->get(); - // Deal with mass decline doubler request - if ($request->input('decline-below')) { - Cache::forget('audition'.$audition->id.'seating'); - - $changes_made = false; - foreach ($entries as $entry) { - $doublerData = $this->doublerService->entryDoublerData($entry); - if ($doublerData && ! $entry->hasFlag('declined') && $entry->rank > $request->input('decline-below')) { - try { - $this->decider->decline($entry); - $changes_made = true; - } catch (AuditionAdminException $e) { - return redirect()->back()->with('error', $e->getMessage()); - } - } - } - if ($changes_made) { - $cache_key = 'event'.$audition->event_id.'doublers-seating'; - Cache::forget($cache_key); - - return redirect()->back(); - } - } - - $entries->load('student.school'); - $entries->load('student.doublerRequests'); - $seatable = [ - 'allScored' => true, - 'doublersResolved' => true, - ]; - foreach ($entries as $entry) { - $totalScoreColumn = 'No Score'; - $fullyScored = false; - if ($entry->score_totals) { - $totalScoreColumn = $entry->score_totals[0] >= 0 ? $entry->score_totals[0] : $entry->score_message; - $fullyScored = $entry->score_totals[0] >= 0; - } - // No Shows are fully scored - if ($entry->hasFlag('no_show')) { - $fullyScored = true; - } - $doublerData = $this->doublerService->entryDoublerData($entry); - - $entryData[] = [ - 'rank' => $entry->rank, - 'id' => $entry->id, - 'studentName' => $entry->student->full_name(), - 'schoolName' => $entry->student->school->name, - 'drawNumber' => $entry->draw_number, - 'totalScore' => $totalScoreColumn, - 'fullyScored' => $fullyScored, - 'hasBonusScores' => $entry->bonus_scores_count > 0, - 'doubleData' => $doublerData, - 'doublerRequest' => $entry->student->doublerRequests()->where('event_id', - $audition->event_id)->first()?->request, - ]; - // If this entries double decision isn't made, block seating - if ($doublerData && $doublerData[$entry->id]['status'] == 'undecided') { - $seatable['doublersResolved'] = false; - } - // If entry is unscored, block seating - if (! $fullyScored) { - $seatable['allScored'] = false; - } - } - - $rightPanel = $this->pickRightPanel($audition, $seatable); - $seatableEntries = []; - if ($seatable['doublersResolved'] && $seatable['allScored']) { - $seatableEntries = $entries->reject(function ($entry) { - if ($entry->hasFlag('declined')) { - return true; - } - if ($entry->hasFlag('no_show')) { - return true; - } - if ($entry->hasFlag('failed_prelim')) { - return true; - } - - return false; - }); - } + // Get failed prelim entries sorted by draw number + $failed_prelim_entries = $audition->entries() + ->whereDoesntHave('totalScore') + ->whereHas('flags', function ($query) { + $query->where('flag_name', 'failed_prelim'); + }) + ->with('student.school') + ->orderBy('draw_number', 'asc') + ->get(); return view('tabulation.auditionSeating', - compact('entryData', 'audition', 'rightPanel', 'seatableEntries', 'requestedEnsembleAccepts')); + compact('audition', + 'scored_entries', + 'unscored_entries', + 'noshow_entries', + 'failed_prelim_entries') + ); } protected function pickRightPanel(Audition $audition, array $seatable) diff --git a/app/Http/Controllers/Tabulation/SeatAuditionFormControllerOLD.php b/app/Http/Controllers/Tabulation/SeatAuditionFormControllerOLD.php new file mode 100644 index 0000000..9e27ec4 --- /dev/null +++ b/app/Http/Controllers/Tabulation/SeatAuditionFormControllerOLD.php @@ -0,0 +1,180 @@ +calc = $calc; + $this->ranker = $ranker; + $this->doublerService = $doublerService; + $this->entryService = $entryService; + $this->auditionService = $auditionService; + $this->decider = $decider; + } + + public function __invoke(Request $request, Audition $audition) + { + // If a seating proposal was posted, deal wth it + if ($request->method() == 'POST' && $request->input('ensembleAccept')) { + $requestedEnsembleAccepts = $request->input('ensembleAccept'); + } else { + $requestedEnsembleAccepts = false; + } + + // Deal with a mass no-show request + if ($request->input('mass-no-show')) { + $entries = $audition->entries()->forSeating()->withCount('scoreSheets')->with('flags')->get(); + foreach ($entries as $entry) { + if ($entry->scoreSheets_count == 0 && ! $entry->hasFlag('no_show')) { + $entry->addFlag('no_show'); + } + Cache::forget('entryScore-'.$entry->id.'-seating'); + Cache::forget('entryScore-'.$entry->id.'-advancement'); + } + Cache::forget('audition'.$audition->id.'seating'); + Cache::forget('audition'.$audition->id.'advancement'); + } + + $entryData = []; + $entries = $this->ranker->rank('seating', $audition); + + // Deal with mass decline doubler request + if ($request->input('decline-below')) { + Cache::forget('audition'.$audition->id.'seating'); + + $changes_made = false; + foreach ($entries as $entry) { + $doublerData = $this->doublerService->entryDoublerData($entry); + if ($doublerData && ! $entry->hasFlag('declined') && $entry->rank > $request->input('decline-below')) { + try { + $this->decider->decline($entry); + $changes_made = true; + } catch (AuditionAdminException $e) { + return redirect()->back()->with('error', $e->getMessage()); + } + } + } + if ($changes_made) { + $cache_key = 'event'.$audition->event_id.'doublers-seating'; + Cache::forget($cache_key); + + return redirect()->back(); + } + } + + $entries->load('student.school'); + $entries->load('student.doublerRequests'); + $seatable = [ + 'allScored' => true, + 'doublersResolved' => true, + ]; + foreach ($entries as $entry) { + $totalScoreColumn = 'No Score'; + $fullyScored = false; + if ($entry->score_totals) { + $totalScoreColumn = $entry->score_totals[0] >= 0 ? $entry->score_totals[0] : $entry->score_message; + $fullyScored = $entry->score_totals[0] >= 0; + } + // No Shows are fully scored + if ($entry->hasFlag('no_show')) { + $fullyScored = true; + } + $doublerData = $this->doublerService->entryDoublerData($entry); + + $entryData[] = [ + 'rank' => $entry->rank, + 'id' => $entry->id, + 'studentName' => $entry->student->full_name(), + 'schoolName' => $entry->student->school->name, + 'drawNumber' => $entry->draw_number, + 'totalScore' => $totalScoreColumn, + 'fullyScored' => $fullyScored, + 'hasBonusScores' => $entry->bonus_scores_count > 0, + 'doubleData' => $doublerData, + 'doublerRequest' => $entry->student->doublerRequests()->where('event_id', + $audition->event_id)->first()?->request, + ]; + // If this entries double decision isn't made, block seating + if ($doublerData && $doublerData[$entry->id]['status'] == 'undecided') { + $seatable['doublersResolved'] = false; + } + // If entry is unscored, block seating + if (! $fullyScored) { + $seatable['allScored'] = false; + } + } + + $rightPanel = $this->pickRightPanel($audition, $seatable); + $seatableEntries = []; + if ($seatable['doublersResolved'] && $seatable['allScored']) { + $seatableEntries = $entries->reject(function ($entry) { + if ($entry->hasFlag('declined')) { + return true; + } + if ($entry->hasFlag('no_show')) { + return true; + } + if ($entry->hasFlag('failed_prelim')) { + return true; + } + + return false; + }); + } + + return view('tabulation.auditionSeating', + compact('entryData', 'audition', 'rightPanel', 'seatableEntries', 'requestedEnsembleAccepts')); + } + + protected function pickRightPanel(Audition $audition, array $seatable) + { + if ($audition->hasFlag('seats_published')) { + $resultsWindow = new GetAuditionSeats; + $rightPanel['view'] = 'tabulation.auditionSeating-show-published-seats'; + $rightPanel['data'] = $resultsWindow($audition); + + return $rightPanel; + } + if ($seatable['allScored'] == false || $seatable['doublersResolved'] == false) { + $rightPanel['view'] = 'tabulation.auditionSeating-unable-to-seat-card'; + $rightPanel['data'] = $seatable; + + return $rightPanel; + } + + $rightPanel['view'] = 'tabulation.auditionSeating-right-complete-not-published'; + $rightPanel['data'] = $this->auditionService->getSeatingLimits($audition); + + return $rightPanel; + } +} diff --git a/app/Models/Audition.php b/app/Models/Audition.php index ecf32f9..c8599cb 100644 --- a/app/Models/Audition.php +++ b/app/Models/Audition.php @@ -35,7 +35,7 @@ class Audition extends Model public function unscoredEntries(): HasMany { return $this->hasMany(Entry::class) - ->whereDoesntHave('scoreSheets') + ->whereDoesntHave('totalScore') ->whereDoesntHave('flags', function ($query) { $query->where('flag_name', 'no_show'); }); diff --git a/app/Models/EntryFlag.php b/app/Models/EntryFlag.php index 4e2db12..ee1eaa0 100644 --- a/app/Models/EntryFlag.php +++ b/app/Models/EntryFlag.php @@ -15,36 +15,8 @@ class EntryFlag extends Model 'flag_name' => EntryFlags::class, ]; - protected static function boot() - { - parent::boot(); - static::created(function ($flag) { - $flag->deleteRelatedCalculatedScores(); - }); - - static::updated(function ($flag) { - $flag->deleteRelatedCalculatedScores(); - }); - - static::deleted(function ($flag) { - $flag->deleteRelatedCalculatedScores(); - }); - } - public function entry(): BelongsTo { return $this->belongsTo(Entry::class); } - - public function deleteRelatedCalculatedScores(): void - { - $entry = $this->entry; - if ($entry) { - $entry->calculatedScores()->delete(); - Cache::forget('entryScore-'.$entry->id.'-seating'); - Cache::forget('entryScore-'.$entry->id.'-advancement'); - Cache::forget('audition'.$entry->audition_id.'seating'); - Cache::forget('audition'.$entry->audition_id.'advancement'); - } - } } diff --git a/app/Models/User.php b/app/Models/User.php index 1c6907b..78968d5 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -127,7 +127,11 @@ class User extends Authenticatable implements MustVerifyEmail public function isJudge(): bool { - return $this->judgingAssignments()->count() > 0 || $this->bonusJudgingAssignments()->count() > 0; + return once(function () { + return $this->judgingAssignments()->count() > 0 + || $this->bonusJudgingAssignments()->count() > 0; + }); + } public function possibleSchools(): Collection diff --git a/resources/views/tabulation/auditionSeating.blade.php b/resources/views/tabulation/auditionSeating.blade.php index 065ae86..765c55f 100644 --- a/resources/views/tabulation/auditionSeating.blade.php +++ b/resources/views/tabulation/auditionSeating.blade.php @@ -1,29 +1,130 @@ -@inject('doublerService','App\Services\DoublerService') -@php - $blockSeating = [] -@endphp Audition Seating - {{ $audition->name }}
-
- @include('tabulation.auditionSeating-results-table') + +
{{-- Entry Ranking Table --}} + {{-- Scored Entries --}} + Scored Entries + + + + Rank + ID + Draw # + Student + Doubler + Total Score + @if($audition->bonusScore()->count() > 0) +
+
+ + Has Bonus +
+ @endif +
+ + + + @foreach($scored_entries as $entry) + + {{ $loop->iteration }} + {{ $entry->id }} + {{ $entry->draw_number }} + + {{ $entry->student->full_name() }} + {{ $entry->student->school->name }} + + Doubler to Come + {{ $entry->totalScore->seating_total }} + + @endforeach + +
+
+ + {{-- Unscored Entries --}} + Unscored Entries + + + + Draw # + ID + Student + + + + @foreach($unscored_entries as $entry) + + {{ $entry->draw_number }} + {{ $entry->id }} + + {{ $entry->student->full_name() }} + {{ $entry->student->school->name }} + + + @endforeach + + + + + {{-- No Show Entries --}} + No Show Entries + + + + Draw # + ID + Student + + + + @foreach($noshow_entries as $entry) + + {{ $entry->draw_number }} + {{ $entry->id }} + + {{ $entry->student->full_name() }} + {{ $entry->student->school->name }} + + + @endforeach + + + + + {{-- Failed Prelim Entries --}} + Failed Prelim Entries + + + + Draw # + ID + Student + + + + @foreach($failed_prelim_entries as $entry) + + {{ $entry->draw_number }} + {{ $entry->id }} + + {{ $entry->student->full_name() }} + {{ $entry->student->school->name }} + + + @endforeach + + + + +
+ +
- @include($rightPanel['view']) + Controls
-{{--
--}} -{{-- @if($audition->hasFlag('seats_published'))--}} -{{-- @include('tabulation.auditionSeating-show-published-seats')--}} -{{-- @elseif(! $auditionComplete)--}} -{{-- @include('tabulation.auditionSeating-unable-to-seat-card')--}} -{{-- @else--}} -{{-- @include('tabulation.auditionSeating-fill-seats-form')--}} -{{-- @include('tabulation.auditionSeating-show-proposed-seats')--}} -{{-- @endif--}} - - -{{--
--}}
diff --git a/resources/views/tabulation/auditionSeatingOLD.blade.php b/resources/views/tabulation/auditionSeatingOLD.blade.php new file mode 100644 index 0000000..065ae86 --- /dev/null +++ b/resources/views/tabulation/auditionSeatingOLD.blade.php @@ -0,0 +1,30 @@ +@inject('doublerService','App\Services\DoublerService') +@php + $blockSeating = [] +@endphp + + Audition Seating - {{ $audition->name }} +
+
+
+ @include('tabulation.auditionSeating-results-table') +
+
+ @include($rightPanel['view']) +
+{{--
--}} +{{-- @if($audition->hasFlag('seats_published'))--}} +{{-- @include('tabulation.auditionSeating-show-published-seats')--}} +{{-- @elseif(! $auditionComplete)--}} +{{-- @include('tabulation.auditionSeating-unable-to-seat-card')--}} +{{-- @else--}} +{{-- @include('tabulation.auditionSeating-fill-seats-form')--}} +{{-- @include('tabulation.auditionSeating-show-proposed-seats')--}} +{{-- @endif--}} + + +{{--
--}} +
+ + +
From 250a3856bae6166691a3de3408fd97faa7978890 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Sat, 14 Jun 2025 08:40:20 -0500 Subject: [PATCH 23/67] Separate failed prelim and noshow flags --- app/Http/Controllers/Tabulation/EntryFlagController.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Http/Controllers/Tabulation/EntryFlagController.php b/app/Http/Controllers/Tabulation/EntryFlagController.php index a3ee379..d9c8aac 100644 --- a/app/Http/Controllers/Tabulation/EntryFlagController.php +++ b/app/Http/Controllers/Tabulation/EntryFlagController.php @@ -4,8 +4,8 @@ namespace App\Http\Controllers\Tabulation; use App\Http\Controllers\Controller; use App\Models\BonusScore; -use App\Models\CalculatedScore; use App\Models\Entry; +use App\Models\EntryTotalScore; use App\Models\ScoreSheet; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; @@ -81,9 +81,9 @@ class EntryFlagController extends Controller } DB::table('score_sheets')->where('entry_id', $entry->id)->delete(); - ScoreSheet::where('entry_id', $entry->id)->delete(); BonusScore::where('entry_id', $entry->id)->delete(); + EntryTotalScore::where('entry_id', $entry->id)->delete(); if (request()->input('noshow-type') == 'failprelim') { $msg = 'Failed prelim has been entered for '.$entry->audition->name.' #'.$entry->draw_number.' (ID: '.$entry->id.').'; $entry->addFlag('failed_prelim'); From 34e22187ddf21ecc95f9b409fd587c092dc4731a Mon Sep 17 00:00:00 2001 From: Matt Young Date: Sat, 14 Jun 2025 11:12:36 -0500 Subject: [PATCH 24/67] Set foundation for new handling of doublers --- app/Models/Audition.php | 14 +++++++- app/Models/DoublerEntryCount.php | 21 +++++++++++ app/Models/Student.php | 18 ++++++++++ ...2025_06_14_142507_doubler_entry_counts.php | 35 +++++++++++++++++++ 4 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 app/Models/DoublerEntryCount.php create mode 100644 database/migrations/2025_06_14_142507_doubler_entry_counts.php diff --git a/app/Models/Audition.php b/app/Models/Audition.php index c8599cb..e3957d0 100644 --- a/app/Models/Audition.php +++ b/app/Models/Audition.php @@ -11,6 +11,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Support\Collection; use function in_array; class Audition extends Model @@ -81,7 +82,7 @@ class Audition extends Model */ public function getJudgesCountAttribute() { - if (! isset($this->attributes['judges_count'])) { + if (!isset($this->attributes['judges_count'])) { $this->attributes['judges_count'] = $this->judges()->count(); } @@ -129,6 +130,17 @@ class Audition extends Model return $this->hasMany(Seat::class); } + public function getDoublerEntries(): Collection + { + return $this->entries() + ->whereIn('student_id', function ($query) { + $query->select('student_id') + ->from('doubler_entry_counts') + ->where('event_id', $this->event_id); + }) + ->get(); + } + public function scopeOpen(Builder $query): void { $currentDate = Carbon::now('America/Chicago'); diff --git a/app/Models/DoublerEntryCount.php b/app/Models/DoublerEntryCount.php new file mode 100644 index 0000000..38ffb02 --- /dev/null +++ b/app/Models/DoublerEntryCount.php @@ -0,0 +1,21 @@ +belongsTo(Student::class); + } + + public function event(): BelongsTo + { + return $this->belongsTo(Event::class); + } +} diff --git a/app/Models/Student.php b/app/Models/Student.php index 2554aa2..004f4bf 100644 --- a/app/Models/Student.php +++ b/app/Models/Student.php @@ -50,6 +50,9 @@ class Student extends Model return $this->hasMany(HistoricalSeat::class); } + /** + * Returns the directors at this student's school. + */ public function users(): HasManyThrough { return $this->hasManyThrough( @@ -62,6 +65,11 @@ class Student extends Model ); } + /** + * Returns the directors at this student's school. + * Alias of users()) + * ' + */ public function directors(): HasManyThrough { return $this->users(); @@ -85,4 +93,14 @@ class Student extends Model { return $this->hasMany(DoublerRequest::class); } + + public function isDoublerInEvent(Event|int $event): bool + { + $eventId = $event instanceof Event ? $event->id : $event; + + return DoublerEntryCount::where([ + 'event_id' => $eventId, + 'student_id' => $this->id, + ])->exists(); + } } diff --git a/database/migrations/2025_06_14_142507_doubler_entry_counts.php b/database/migrations/2025_06_14_142507_doubler_entry_counts.php new file mode 100644 index 0000000..886aa51 --- /dev/null +++ b/database/migrations/2025_06_14_142507_doubler_entry_counts.php @@ -0,0 +1,35 @@ + 1 + '); + + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // + } +}; From 40a9133a79be54a8f95b4d731b74fb0a70b8e451 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Sat, 14 Jun 2025 15:57:32 -0500 Subject: [PATCH 25/67] Don't allow lazy loading on development --- app/Providers/AppServiceProvider.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 1ff4c7b..27db897 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -93,6 +93,6 @@ class AppServiceProvider extends ServiceProvider User::observe(UserObserver::class); SeatingLimit::observe(SeatingLimitObserver::class); - //Model::preventLazyLoading(! app()->isProduction()); + Model::preventLazyLoading(! app()->isProduction()); } } From 6057211836a73674649b674f79943edb70e6d411 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Sat, 14 Jun 2025 15:58:12 -0500 Subject: [PATCH 26/67] Don't count a failed prelim audition as an unscored entry. --- app/Models/Audition.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/Models/Audition.php b/app/Models/Audition.php index e3957d0..0531535 100644 --- a/app/Models/Audition.php +++ b/app/Models/Audition.php @@ -10,8 +10,8 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; - use Illuminate\Support\Collection; + use function in_array; class Audition extends Model @@ -39,6 +39,9 @@ class Audition extends Model ->whereDoesntHave('totalScore') ->whereDoesntHave('flags', function ($query) { $query->where('flag_name', 'no_show'); + }) + ->whereDoesntHave('flags', function ($query) { + $query->where('flag_name', 'failed_prelim'); }); } @@ -82,7 +85,7 @@ class Audition extends Model */ public function getJudgesCountAttribute() { - if (!isset($this->attributes['judges_count'])) { + if (! isset($this->attributes['judges_count'])) { $this->attributes['judges_count'] = $this->judges()->count(); } From 49b609e9b7dfa37a0f43413cca812fa8e003d336 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Sat, 14 Jun 2025 15:58:32 -0500 Subject: [PATCH 27/67] Avoid a math inaccuracy. --- app/Http/Controllers/Tabulation/SeatingStatusController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Http/Controllers/Tabulation/SeatingStatusController.php b/app/Http/Controllers/Tabulation/SeatingStatusController.php index 19f5a0c..b494477 100644 --- a/app/Http/Controllers/Tabulation/SeatingStatusController.php +++ b/app/Http/Controllers/Tabulation/SeatingStatusController.php @@ -36,7 +36,7 @@ class SeatingStatusController extends Controller 'name' => $audition->name, 'scoredEntriesCount' => $audition->entries_count - $audition->unscored_entries_count, 'totalEntriesCount' => $audition->entries_count, - 'scoredPercentage' => $audition->entries_count > 0 ? ($audition->entries_count - $audition->unscored_entries_count) / $audition->entries_count * 100 : 100, + 'scoredPercentage' => $audition->entries_count > 0 ? ((($audition->entries_count - $audition->unscored_entries_count)) / $audition->entries_count) * 100 : 100, 'scoringComplete' => $audition->unscored_entries_count === 0, 'seatsPublished' => $audition->hasFlag('seats_published'), 'audition' => $audition, From 33bca1cfdf140c4094746d1aab2c0767c5a63249 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Sun, 15 Jun 2025 14:31:52 -0500 Subject: [PATCH 28/67] Avoid division by zero errors. --- app/Actions/Tabulation/TotalEntryScores.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/Actions/Tabulation/TotalEntryScores.php b/app/Actions/Tabulation/TotalEntryScores.php index 241a9af..448e650 100644 --- a/app/Actions/Tabulation/TotalEntryScores.php +++ b/app/Actions/Tabulation/TotalEntryScores.php @@ -30,7 +30,12 @@ class TotalEntryScores $newTotaledScore->entry_id = $entry->id; // deal with seating scores + // TODO: Consider a rewrite to pull the scoreSheets from the entry model so they may be preloaded $scoreSheets = ScoreSheet::where('entry_id', $entry->id)->orderBy('seating_total', 'desc')->get(); + // bail out if there are no score sheets + if ($scoreSheets->count() == 0) { + return; + } if (auditionSetting('olympic_scoring' && $scoreSheets->count() > 2)) { // under olympic scoring, drop the first and last element $scoreSheets->shift(); From 4f317f145829930e4d2ed308a7f4cec83c2d1646 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Sun, 15 Jun 2025 14:32:17 -0500 Subject: [PATCH 29/67] Throttle recalculating scores. --- .../Tabulation/SeatingStatusController.php | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/app/Http/Controllers/Tabulation/SeatingStatusController.php b/app/Http/Controllers/Tabulation/SeatingStatusController.php index b494477..fca1b6a 100644 --- a/app/Http/Controllers/Tabulation/SeatingStatusController.php +++ b/app/Http/Controllers/Tabulation/SeatingStatusController.php @@ -2,9 +2,11 @@ namespace App\Http\Controllers\Tabulation; +use App\Actions\Tabulation\CalculateAuditionScores; use App\Http\Controllers\Controller; use App\Models\Audition; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Cache; class SeatingStatusController extends Controller { @@ -13,6 +15,24 @@ class SeatingStatusController extends Controller */ public function __invoke(Request $request) { + if (! Cache::has('seating_status_audition_totaler_throttle')) { + $lock = Cache::lock('seating_status_audition_totaler_lock'); + + if ($lock->get()) { + try { + $totaler = app(CalculateAuditionScores::class); + foreach (Audition::forSeating()->with('judges')->get() as $audition) { + $totaler($audition); + } + + // set throttle + Cache::put('seating_status_audition_totaler_throttle', true, 15); + } finally { + $lock->release(); + } + } + } + $auditions = Audition::forSeating() ->withCount([ 'entries' => function ($query) { @@ -25,6 +45,7 @@ class SeatingStatusController extends Controller }, ]) ->with('flags') + ->with('entries') ->get(); $auditionData = []; foreach ($auditions as $audition) { From b8ce2bc6db845dacd05bdd9b971acaa24e19ba0d Mon Sep 17 00:00:00 2001 From: Matt Young Date: Sun, 15 Jun 2025 14:33:55 -0500 Subject: [PATCH 30/67] Rewrite scoring all auditions. --- database/seeders/ScoreAllAuditions.php | 27 ++++++++++---------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/database/seeders/ScoreAllAuditions.php b/database/seeders/ScoreAllAuditions.php index 776d074..4d90c54 100644 --- a/database/seeders/ScoreAllAuditions.php +++ b/database/seeders/ScoreAllAuditions.php @@ -2,9 +2,9 @@ namespace Database\Seeders; +use App\Actions\Tabulation\EnterScore; use App\Models\User; use Illuminate\Database\Seeder; -use App\Models\ScoreSheet; class ScoreAllAuditions extends Seeder { @@ -13,26 +13,19 @@ class ScoreAllAuditions extends Seeder */ public function run(): void { + $recorder = app(EnterScore::class); $judges = User::all(); - foreach ($judges as $judge) { - foreach ($judge->rooms as $room) { - foreach ($room->auditions as $audition) { - $scoringGuide = $audition->scoringGuide; - $subscores = $scoringGuide->subscores; - foreach ($audition->entries as $entry) { + foreach ($judges as $judge) { // Iterate over all users + foreach ($judge->rooms as $room) { // Iterate over each user's assigned rooms + foreach ($room->auditions as $audition) { // Iterate over each audition in that room + $scoringGuide = $audition->scoringGuide; // Load the scoring guide for that audition + $subscores = $scoringGuide->subscores; // Get the subscores for that audition + foreach ($audition->entries as $entry) { // Iterate over each entry in that audition $scoreArray = []; foreach ($subscores as $subscore) { - $scoreArray[$subscore->id] = [ - 'score' => mt_rand(0, 100), - 'subscore_id' => $subscore->id, - 'subscore_name' => $subscore->name, - ]; + $scoreArray[$subscore->id] = mt_rand(0, $subscore->max_score); } - ScoreSheet::create([ - 'user_id' => $judge->id, - 'entry_id' => $entry->id, - 'subscores' => $scoreArray, - ]); + $recorder($judge, $entry, $scoreArray); } } } From 349da644b7cb5d90c08bc7451a6d2f72ce7eafa2 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Sun, 15 Jun 2025 15:44:05 -0500 Subject: [PATCH 31/67] Rewrite RankAuditionEntries action and use it in the new seat audition form controller. --- .../Tabulation/RankAuditionEntries.php | 151 ++++++++---------- .../Tabulation/SeatAuditionFormController.php | 21 +-- 2 files changed, 67 insertions(+), 105 deletions(-) diff --git a/app/Actions/Tabulation/RankAuditionEntries.php b/app/Actions/Tabulation/RankAuditionEntries.php index cd3586e..6a3e7d8 100644 --- a/app/Actions/Tabulation/RankAuditionEntries.php +++ b/app/Actions/Tabulation/RankAuditionEntries.php @@ -4,105 +4,82 @@ namespace App\Actions\Tabulation; -use App\Exceptions\TabulationException; +use App\Exceptions\AuditionAdminException; use App\Models\Audition; -use Illuminate\Database\Eloquent\Collection; -use Illuminate\Support\Facades\Cache; - -use function is_numeric; +use Illuminate\Support\Collection; class RankAuditionEntries { - protected CalculateEntryScore $calculator; - - public function __construct(CalculateEntryScore $calculator) - { - $this->calculator = $calculator; - } - - public function rank(string $mode, Audition $audition): Collection - { - $cacheKey = 'audition'.$audition->id.$mode; - - return Cache::remember($cacheKey, 300, function () use ($mode, $audition) { - return $this->calculateRank($mode, $audition); - }); - - } - /** - * For a given audition, return a collection of entries ranked by total score. Each entry will have a - * property rank that either is their rank or a flag reflecting no-show, declined, or failed-prelim status + * Get ranked entries for the provided audition for either seating or advancement. * - * @throws TabulationException + * If the rank_type is seating, the ranked entries are returned in descending order of seating total. + * If the rank_type is advancement, the ranked entries are returned in descending order of advancement total. + * + * The ranked entries are returned as a Collection of Entry objects. + * + * @param string $rank_type advancement|seating + * @return Collection|void + * + * @throws AuditionAdminException */ - public function calculateRank(string $mode, Audition $audition): Collection + public function __invoke(Audition $audition, string $rank_type) { - $this->basicValidation($mode, $audition); - $entries = match ($mode) { - 'seating' => $audition->entries()->forSeating()->with('scoreSheets')->withCount('bonusScores')->get(), - 'advancement' => $audition->entries()->forAdvancement()->with('scoreSheets')->get(), - }; - - foreach ($entries as $entry) { - $entry->setRelation('audition', $audition); - try { - $entry->score_totals = $this->calculator->calculate($mode, $entry); - } catch (TabulationException $ex) { - $entry->score_totals = [-1]; - $entry->score_message = $ex->getMessage(); - } - } - // Sort entries based on their total score, then by subscores in tiebreak order - $entries = $entries->sort(function ($a, $b) { - for ($i = 0; $i < count($a->score_totals); $i++) { - if (! array_key_exists($i, $a->score_totals)) { - return -1; - } - if (! array_key_exists($i, $b->score_totals)) { - return -1; - } - if ($a->score_totals[$i] > $b->score_totals[$i]) { - return -1; - } elseif ($a->score_totals[$i] < $b->score_totals[$i]) { - return 1; - } - } - - return 0; - }); - $rank = 1; - $rawRank = 1; - foreach ($entries as $entry) { - $entry->rank = $rank; - $entry->raw_rank = $rawRank; - // We don't really get a rank for seating if we have certain flags - if ($mode === 'seating') { - if ($entry->hasFlag('failed_prelim')) { - $entry->rank = 'Failed Prelim'; - } elseif ($entry->hasFlag('declined')) { - $entry->rank = 'Declined'; - } elseif ($entry->hasFlag('no_show')) { - $entry->rank = 'No Show'; - } - } - - if (is_numeric($entry->rank)) { - $rank++; - } - $rawRank++; + if ($rank_type !== 'seating' && $rank_type !== 'advancement') { + throw new AuditionAdminException('Invalid rank type: '.$rank_type.' (must be seating or advancement)'); + } + + $cache_duration = 15; + + if ($rank_type === 'seating') { + return cache()->remember('rank_seating_'.$audition->id, $cache_duration, function () use ($audition) { + return $this->get_seating_ranks($audition); + }); } - return $entries; } - protected function basicValidation($mode, Audition $audition): void + private function get_seating_ranks(Audition $audition): Collection { - if ($mode !== 'seating' && $mode !== 'advancement') { - throw new TabulationException('Mode must be seating or advancement'); - } - if (! $audition->exists()) { - throw new TabulationException('Invalid audition provided'); - } + return $audition->entries() + ->whereHas('totalScore') + ->with('totalScore') + ->with('student.school') + ->join('entry_total_scores', 'entries.id', '=', 'entry_total_scores.entry_id') + ->orderBy('entry_total_scores.seating_total', 'desc') + ->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.seating_subscore_totals, "$[0]"), -999999) DESC') + ->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.seating_subscore_totals, "$[1]"), -999999) DESC') + ->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.seating_subscore_totals, "$[2]"), -999999) DESC') + ->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.seating_subscore_totals, "$[3]"), -999999) DESC') + ->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.seating_subscore_totals, "$[4]"), -999999) DESC') + ->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.seating_subscore_totals, "$[5]"), -999999) DESC') + ->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.seating_subscore_totals, "$[6]"), -999999) DESC') + ->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.seating_subscore_totals, "$[7]"), -999999) DESC') + ->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.seating_subscore_totals, "$[8]"), -999999) DESC') + ->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.seating_subscore_totals, "$[9]"), -999999) DESC') + ->select('entries.*') + ->get(); + } + + private function get_advancement_ranks(Audition $audition): Collection + { + return $audition->entries() + ->whereHas('totalScore') + ->with('totalScore') + ->with('student.school') + ->join('entry_total_scores', 'entries.id', '=', 'entry_total_scores.entry_id') + ->orderBy('entry_total_scores.advancement_total', 'desc') + ->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.advancement_subscore_totals, "$[0]"), -999999) DESC') + ->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.advancement_subscore_totals, "$[1]"), -999999) DESC') + ->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.advancement_subscore_totals, "$[2]"), -999999) DESC') + ->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.advancement_subscore_totals, "$[3]"), -999999) DESC') + ->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.advancement_subscore_totals, "$[4]"), -999999) DESC') + ->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.advancement_subscore_totals, "$[5]"), -999999) DESC') + ->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.advancement_subscore_totals, "$[6]"), -999999) DESC') + ->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.advancement_subscore_totals, "$[7]"), -999999) DESC') + ->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.advancement_subscore_totals, "$[8]"), -999999) DESC') + ->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.advancement_subscore_totals, "$[9]"), -999999) DESC') + ->select('entries.*') + ->get(); } } diff --git a/app/Http/Controllers/Tabulation/SeatAuditionFormController.php b/app/Http/Controllers/Tabulation/SeatAuditionFormController.php index f0c0307..b6f0ccb 100644 --- a/app/Http/Controllers/Tabulation/SeatAuditionFormController.php +++ b/app/Http/Controllers/Tabulation/SeatAuditionFormController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers\Tabulation; use App\Actions\Tabulation\GetAuditionSeats; +use App\Actions\Tabulation\RankAuditionEntries; use App\Http\Controllers\Controller; use App\Models\Audition; use Illuminate\Http\Request; @@ -11,25 +12,9 @@ class SeatAuditionFormController extends Controller { public function __invoke(Request $request, Audition $audition) { + $ranker = app(RankAuditionEntries::class); // Get scored entries in order - $scored_entries = $audition->entries() - ->whereHas('totalScore') - ->with('totalScore') - ->with('student.school') - ->join('entry_total_scores', 'entries.id', '=', 'entry_total_scores.entry_id') - ->orderBy('entry_total_scores.seating_total', 'desc') - ->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.seating_subscore_totals, "$[0]"), -999999) DESC') - ->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.seating_subscore_totals, "$[1]"), -999999) DESC') - ->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.seating_subscore_totals, "$[2]"), -999999) DESC') - ->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.seating_subscore_totals, "$[3]"), -999999) DESC') - ->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.seating_subscore_totals, "$[4]"), -999999) DESC') - ->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.seating_subscore_totals, "$[5]"), -999999) DESC') - ->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.seating_subscore_totals, "$[6]"), -999999) DESC') - ->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.seating_subscore_totals, "$[7]"), -999999) DESC') - ->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.seating_subscore_totals, "$[8]"), -999999) DESC') - ->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.seating_subscore_totals, "$[9]"), -999999) DESC') - ->select('entries.*') - ->get(); + $scored_entries = $ranker($audition, 'seating'); // Get unscored entries sorted by draw number $unscored_entries = $audition->entries() From ad24a67baa857660af42881b94f70680a19d9767 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Tue, 17 Jun 2025 00:43:50 -0500 Subject: [PATCH 32/67] Progress on seating form --- .../Tabulation/RankAuditionEntries.php | 5 +- .../Tabulation/SeatAuditionFormController.php | 11 ++++- app/Models/Audition.php | 9 ++++ app/Models/Entry.php | 29 ++++++++++++ app/Models/Student.php | 16 +++++++ .../tabulation/auditionSeating.blade.php | 47 +++++++++++++++---- 6 files changed, 106 insertions(+), 11 deletions(-) diff --git a/app/Actions/Tabulation/RankAuditionEntries.php b/app/Actions/Tabulation/RankAuditionEntries.php index 6a3e7d8..5e18fb7 100644 --- a/app/Actions/Tabulation/RankAuditionEntries.php +++ b/app/Actions/Tabulation/RankAuditionEntries.php @@ -6,6 +6,7 @@ namespace App\Actions\Tabulation; use App\Exceptions\AuditionAdminException; use App\Models\Audition; +use App\Models\Entry; use Illuminate\Support\Collection; class RankAuditionEntries @@ -19,7 +20,7 @@ class RankAuditionEntries * The ranked entries are returned as a Collection of Entry objects. * * @param string $rank_type advancement|seating - * @return Collection|void + * @return Collection|void * * @throws AuditionAdminException */ @@ -45,6 +46,7 @@ class RankAuditionEntries ->whereHas('totalScore') ->with('totalScore') ->with('student.school') + ->with('audition') ->join('entry_total_scores', 'entries.id', '=', 'entry_total_scores.entry_id') ->orderBy('entry_total_scores.seating_total', 'desc') ->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.seating_subscore_totals, "$[0]"), -999999) DESC') @@ -67,6 +69,7 @@ class RankAuditionEntries ->whereHas('totalScore') ->with('totalScore') ->with('student.school') + ->with('audition') ->join('entry_total_scores', 'entries.id', '=', 'entry_total_scores.entry_id') ->orderBy('entry_total_scores.advancement_total', 'desc') ->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.advancement_subscore_totals, "$[0]"), -999999) DESC') diff --git a/app/Http/Controllers/Tabulation/SeatAuditionFormController.php b/app/Http/Controllers/Tabulation/SeatAuditionFormController.php index b6f0ccb..e18f8c6 100644 --- a/app/Http/Controllers/Tabulation/SeatAuditionFormController.php +++ b/app/Http/Controllers/Tabulation/SeatAuditionFormController.php @@ -49,12 +49,21 @@ class SeatAuditionFormController extends Controller ->orderBy('draw_number', 'asc') ->get(); + // Get Doubler Data + $doubler_data = []; + foreach ($scored_entries as $e) { + if ($e->student->isDoublerInEvent($audition->event_id)) { + $doubler_data[$e->id] = $e->student->entriesForEvent($e->audition->event_id); + } + } + return view('tabulation.auditionSeating', compact('audition', 'scored_entries', 'unscored_entries', 'noshow_entries', - 'failed_prelim_entries') + 'failed_prelim_entries', + 'doubler_data', ) ); } diff --git a/app/Models/Audition.php b/app/Models/Audition.php index 0531535..d4d6e26 100644 --- a/app/Models/Audition.php +++ b/app/Models/Audition.php @@ -60,6 +60,15 @@ class Audition extends Model return $this->belongsToMany(BonusScoreDefinition::class, 'bonus_score_audition_assignment'); } + public function SeatingLimits(): HasMany + { + return $this->hasMany(SeatingLimit::class) + ->with('ensemble') + ->join('ensembles', 'seating_limits.ensemble_id', '=', 'ensembles.id') + ->orderBy('ensembles.rank') + ->select('seating_limits.*'); + } + public function display_fee(): string { return '$'.number_format($this->entry_fee / 100, 2); diff --git a/app/Models/Entry.php b/app/Models/Entry.php index d4cc57d..4b1cc64 100644 --- a/app/Models/Entry.php +++ b/app/Models/Entry.php @@ -2,7 +2,9 @@ namespace App\Models; +use App\Actions\Tabulation\RankAuditionEntries; use App\Enums\EntryFlags; +use App\Exceptions\AuditionAdminException; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; @@ -24,6 +26,33 @@ class Entry extends Model return $this->hasOne(EntryTotalScore::class); } + /** + * @throws AuditionAdminException + */ + public function rank(string $type) + { + $ranker = app(RankAuditionEntries::class); + + if ($type !== 'seating' && $type !== 'advancement') { + throw new AuditionAdminException('Invalid type specified. Must be either seating or advancement.'); + } + + // Return false if no score. If we have no score, we can't have a rank + if (! $this->totalScore) { + return false; + } + + // Get the ranked entries for this entries audition + $rankedEntries = $ranker($this->audition, $type); + + // Find position of current entry in the ranked entries (1-based index) + $position = $rankedEntries->search(fn ($entry) => $entry->id === $this->id); + + // Return false if entry not found, otherwise return 1-based position + return $position === false ? false : $position + 1; + + } + public function student(): BelongsTo { return $this->belongsTo(Student::class); diff --git a/app/Models/Student.php b/app/Models/Student.php index 004f4bf..3769f74 100644 --- a/app/Models/Student.php +++ b/app/Models/Student.php @@ -7,6 +7,7 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasManyThrough; +use Illuminate\Support\Collection; class Student extends Model { @@ -103,4 +104,19 @@ class Student extends Model 'student_id' => $this->id, ])->exists(); } + + public function entriesForEvent(Event|int $event): Collection + { + $eventId = $event instanceof Event ? $event->id : $event; + + return Entry::query() + ->where('student_id', $this->id) + ->whereHas('audition', function ($query) use ($event) { + $query->where('event_id', $event); + }) + ->with('audition.SeatingLimits') // Eager load the audition relation if needed + ->with('totalScore') + ->get(); + + } } diff --git a/resources/views/tabulation/auditionSeating.blade.php b/resources/views/tabulation/auditionSeating.blade.php index 765c55f..8b10c6c 100644 --- a/resources/views/tabulation/auditionSeating.blade.php +++ b/resources/views/tabulation/auditionSeating.blade.php @@ -25,18 +25,47 @@ - + @foreach($scored_entries as $entry) - {{ $loop->iteration }} - {{ $entry->id }} - {{ $entry->draw_number }} - - {{ $entry->student->full_name() }} - {{ $entry->student->school->name }} + {{ $loop->iteration }} + {{ $entry->id }} + {{ $entry->draw_number }} + +
{{ $entry->student->full_name() }}
+
{{ $entry->student->school->name }}
- Doubler to Come - {{ $entry->totalScore->seating_total }} + + @if( isset($doubler_data[$entry->id]) ) + {{-- Check if this entry is a doubler --}} + @foreach($doubler_data[$entry->id] as $de) + {{-- If it is, render doubler blocks --}} +
+ {{-- @var \App\Models\Entry $de --}} +
+ {{ $de->audition->name }} #{{$de->draw_number}} ({{ $de->id }}) +
+ @php($unscored = $de->audition->unscoredEntries()->count()) + @if($unscored > 0) +
{{ $unscored }} Unscored Entries
+ @endif + @if(! $de->rank('seating')) +
THIS ENTRY NOT SCORED
+ @else +
Ranked: {{ $de->rank('seating') }}
+
+ Acceptance Limits
+ @foreach ($de->audition->SeatingLimits as $limit) + {{ $limit->ensemble->name }} -> {{ $limit->maximum_accepted }} +
+ @endforeach +
+ @endif +
+ @endforeach + @endif +
+ {{ $entry->totalScore->seating_total }} @endforeach From 7754b6df125c31fda613605178d7497848866a35 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Fri, 20 Jun 2025 11:12:29 -0500 Subject: [PATCH 33/67] Move SeatAuditionFormController away from being invokable --- .../Controllers/Tabulation/SeatAuditionFormController.php | 2 +- routes/tabulation.php | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/Http/Controllers/Tabulation/SeatAuditionFormController.php b/app/Http/Controllers/Tabulation/SeatAuditionFormController.php index e18f8c6..5b879c1 100644 --- a/app/Http/Controllers/Tabulation/SeatAuditionFormController.php +++ b/app/Http/Controllers/Tabulation/SeatAuditionFormController.php @@ -10,7 +10,7 @@ use Illuminate\Http\Request; class SeatAuditionFormController extends Controller { - public function __invoke(Request $request, Audition $audition) + public function showForm(Request $request, Audition $audition) { $ranker = app(RankAuditionEntries::class); // Get scored entries in order diff --git a/routes/tabulation.php b/routes/tabulation.php index fe60a38..274f631 100644 --- a/routes/tabulation.php +++ b/routes/tabulation.php @@ -42,9 +42,11 @@ Route::middleware(['auth', 'verified', CheckIfCanTab::class])->group(function () // Seating Routes Route::prefix('seating/')->group(function () { Route::get('/', SeatingStatusController::class)->name('seating.status'); - Route::match(['get', 'post'], '/{audition}', SeatAuditionFormController::class)->name('seating.audition'); - Route::post('/{audition}/publish', [SeatingPublicationController::class, 'publishSeats'])->name('seating.audition.publish'); - Route::post('/{audition}/unpublish', [SeatingPublicationController::class, 'unpublishSeats'])->name('seating.audition.unpublish'); + Route::get('/{audition}', [SeatAuditionFormController::class, 'showForm'])->name('seating.audition'); + Route::post('/{audition}/publish', + [SeatingPublicationController::class, 'publishSeats'])->name('seating.audition.publish'); + Route::post('/{audition}/unpublish', + [SeatingPublicationController::class, 'unpublishSeats'])->name('seating.audition.unpublish'); }); // Advancement Routes From b6d89f294dd437806bb25149487d4ee8d9ebd7d9 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Fri, 20 Jun 2025 12:52:46 -0500 Subject: [PATCH 34/67] Initial work on doubler column --- .../Tabulation/SeatAuditionFormController.php | 18 +++++++++++++++++- .../views/tabulation/auditionSeating.blade.php | 8 +++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/app/Http/Controllers/Tabulation/SeatAuditionFormController.php b/app/Http/Controllers/Tabulation/SeatAuditionFormController.php index 5b879c1..6893a20 100644 --- a/app/Http/Controllers/Tabulation/SeatAuditionFormController.php +++ b/app/Http/Controllers/Tabulation/SeatAuditionFormController.php @@ -53,7 +53,23 @@ class SeatAuditionFormController extends Controller $doubler_data = []; foreach ($scored_entries as $e) { if ($e->student->isDoublerInEvent($audition->event_id)) { - $doubler_data[$e->id] = $e->student->entriesForEvent($e->audition->event_id); + // get the other entries for this student + $doubler_data[$e->id]['entries'] = $e->student->entriesForEvent($e->audition->event_id); + + // How many of this student's entries have been declined + $declined_count = $doubler_data[$e->id]['entries']->filter(function ($entry) { + return $entry->hasFlag('declined'); + })->count(); + + // set status + // declined status is easy + if ($e->hasFlag('declined')) { + $doubler_data[$e->id]['status'] = 'declined'; + } elseif ($doubler_data[$e->id]['entries']->count() - $declined_count == 1) { + $doubler_data[$e->id]['status'] = 'accepted'; + } else { + $doubler_data[$e->id]['status'] = 'undecided'; + } } } diff --git a/resources/views/tabulation/auditionSeating.blade.php b/resources/views/tabulation/auditionSeating.blade.php index 8b10c6c..992817b 100644 --- a/resources/views/tabulation/auditionSeating.blade.php +++ b/resources/views/tabulation/auditionSeating.blade.php @@ -38,7 +38,7 @@ @if( isset($doubler_data[$entry->id]) ) {{-- Check if this entry is a doubler --}} - @foreach($doubler_data[$entry->id] as $de) + @foreach($doubler_data[$entry->id]['entries'] as $de) {{-- If it is, render doubler blocks --}}
{{-- @var \App\Models\Entry $de --}} @@ -60,6 +60,12 @@
@endforeach
+
+ + + + +
@endif
@endforeach From cdd0d2bd5067250a1068b4f1cf3c85f2c601e141 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Fri, 20 Jun 2025 12:59:25 -0500 Subject: [PATCH 35/67] Doubler model and migration stems --- app/Models/Doubler.php | 11 ++++++++ ...025_06_20_175904_create_doublers_table.php | 27 +++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 app/Models/Doubler.php create mode 100644 database/migrations/2025_06_20_175904_create_doublers_table.php diff --git a/app/Models/Doubler.php b/app/Models/Doubler.php new file mode 100644 index 0000000..c280c05 --- /dev/null +++ b/app/Models/Doubler.php @@ -0,0 +1,11 @@ +id(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('doublers'); + } +}; From d9a7e97047ac2251cd28db9146ac2bb2d3846ee6 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Sun, 22 Jun 2025 23:19:38 -0500 Subject: [PATCH 36/67] Doubler model and migration created --- app/Models/Doubler.php | 82 ++++++++++++++++++- ...025_06_20_175904_create_doublers_table.php | 13 ++- 2 files changed, 92 insertions(+), 3 deletions(-) diff --git a/app/Models/Doubler.php b/app/Models/Doubler.php index c280c05..e0714bb 100644 --- a/app/Models/Doubler.php +++ b/app/Models/Doubler.php @@ -2,10 +2,88 @@ namespace App\Models; -use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; + +use function PHPUnit\Framework\isInstanceOf; class Doubler extends Model { - use HasFactory; + // Specify that we're not using a single primary key + protected $primaryKey = null; + + public $incrementing = false; + + protected $guarded = []; + + public function student(): BelongsTo + { + return $this->belongsTo(Student::class); + } + + public function event(): BelongsTo + { + return $this->belongsTo(Event::class); + } + + // Find a doubler based on both keys + public static function findDoubler($studentId, $eventId) + { + return static::where('student_id', $studentId) + ->where('event_id', $eventId) + ->first(); + } + + /** + * Sync doubler records for a specified event + */ + public static function syncForEvent($eventId): void + { + if (isInstanceOf(Event::class, $eventId)) { + $eventId = $eventId->id; + } + + // Get students with multiple entries in this event's auditions + $studentsWithMultipleEntries = Student::query() + ->select('students.id') + ->join('entries', 'students.id', '=', 'entries.student_id') + ->join('auditions', 'entries.audition_id', '=', 'auditions.id') + ->where('auditions.event_id', $eventId) + ->groupBy('students.id') + ->havingRaw('COUNT(entries.id) > 1') + ->get(); + + foreach ($studentsWithMultipleEntries as $student) { + // Get entries that are not declined. If only one, they're our accepted entry. + $availableEntries = $student->entries()->available()->get(); + if ($availableEntries->count() === 1) { + $acceptedEntryId = $availableEntries->first()->id; + } else { + $acceptedEntryId = null; + } + // Create or update the doubler record + static::updateOrCreate( + [ + 'student_id' => $student->id, + 'event_id' => $eventId, + ], + [ + 'accepted_entry' => $acceptedEntryId, + ] + ); + } + + // remove doubler records for students who no longer have multiple entries + static::where('event_id', $eventId) + ->whereNotIn('student_id', $studentsWithMultipleEntries->pluck('id')) + ->delete(); + } + + public static function syncDoublers(): void + { + $events = Event::all(); + foreach ($events as $event) { + static::syncForEvent($event); + } + } } diff --git a/database/migrations/2025_06_20_175904_create_doublers_table.php b/database/migrations/2025_06_20_175904_create_doublers_table.php index 8e26400..8d2c16b 100644 --- a/database/migrations/2025_06_20_175904_create_doublers_table.php +++ b/database/migrations/2025_06_20_175904_create_doublers_table.php @@ -1,5 +1,7 @@ id(); + // Foreign keys that will form the composite primary key + $table->foreignIdFor(Student::class)->constrained()->cascadeOnDelete()->cascadeOnUpdate(); + $table->foreignIdFor(Event::class)->constrained()->cascadeOnDelete()->cascadeOnUpdate(); + + // Doubler Specific Fields + $table->integer('entry_count'); + $table->foreignIdFor(Entry::class, 'accepted_entry')->nullable()->constrained()->cascadeOnDelete()->cascadeOnUpdate(); + + // Set the composite primary key + $table->primary(['student_id', 'event_id']); $table->timestamps(); }); } From f3591e9a08c9c03bc482d8550405a15a1be0d176 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Sun, 22 Jun 2025 23:28:37 -0500 Subject: [PATCH 37/67] Correct errors in migration --- .../migrations/2025_06_20_175904_create_doublers_table.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/database/migrations/2025_06_20_175904_create_doublers_table.php b/database/migrations/2025_06_20_175904_create_doublers_table.php index 8d2c16b..740800e 100644 --- a/database/migrations/2025_06_20_175904_create_doublers_table.php +++ b/database/migrations/2025_06_20_175904_create_doublers_table.php @@ -1,6 +1,7 @@ integer('entry_count'); - $table->foreignIdFor(Entry::class, 'accepted_entry')->nullable()->constrained()->cascadeOnDelete()->cascadeOnUpdate(); + $table->foreignIdFor(Entry::class, 'accepted_entry')->nullable()->constrained('entries')->cascadeOnDelete()->cascadeOnUpdate(); // Set the composite primary key $table->primary(['student_id', 'event_id']); From f3013670a346b01da61785ec39987ac9feaca1ad Mon Sep 17 00:00:00 2001 From: Matt Young Date: Sun, 22 Jun 2025 23:38:35 -0500 Subject: [PATCH 38/67] Correct error in doubler class. Add artisan command to sync doublers. --- app/Console/Commands/SyncDoublers.php | 39 +++++++++++++++++++++++++++ app/Models/Doubler.php | 1 + 2 files changed, 40 insertions(+) create mode 100644 app/Console/Commands/SyncDoublers.php diff --git a/app/Console/Commands/SyncDoublers.php b/app/Console/Commands/SyncDoublers.php new file mode 100644 index 0000000..d50235f --- /dev/null +++ b/app/Console/Commands/SyncDoublers.php @@ -0,0 +1,39 @@ +argument('event')) { + $event = Event::findOrFail($eventId); + Doubler::syncForEvent($event); + $this->info("Synced doublers for event {$event->name}"); + } else { + Doubler::syncDoublers(); + $this->info('Synced doublers for all events'); + } + } +} diff --git a/app/Models/Doubler.php b/app/Models/Doubler.php index e0714bb..907aca3 100644 --- a/app/Models/Doubler.php +++ b/app/Models/Doubler.php @@ -69,6 +69,7 @@ class Doubler extends Model ], [ 'accepted_entry' => $acceptedEntryId, + 'entry_count' => $student->entriesForEvent($eventId)->count(), ] ); } From 1f635e6ecf3e40925cf4383e726d0272964a10b6 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Mon, 23 Jun 2025 00:25:37 -0500 Subject: [PATCH 39/67] Set up an observer to update doublers whenever an entry is created, modified, or deleted. --- app/Observers/EntryObserver.php | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/app/Observers/EntryObserver.php b/app/Observers/EntryObserver.php index c18ec21..72268bc 100644 --- a/app/Observers/EntryObserver.php +++ b/app/Observers/EntryObserver.php @@ -2,8 +2,8 @@ namespace App\Observers; -use App\Events\AuditionChange; -use App\Events\EntryChange; +use App\Models\Audition; +use App\Models\Doubler; use App\Models\Entry; class EntryObserver @@ -13,6 +13,16 @@ class EntryObserver */ public function created(Entry $entry): void { + // Count how many entries the student has for the event + $count = $entry->student->entriesForEvent($entry->audition->event_id)->count(); + + // If less than two entries, they're not a doubler + if ($count < 2) { + return; + } + + // Update doublers for the event + Doubler::syncForEvent($entry->audition->event_id); } @@ -21,7 +31,12 @@ class EntryObserver */ public function updated(Entry $entry): void { - + // Update doubler table when an entry is updated + Doubler::syncForEvent($entry->audition->event_id); + if ($entry->wasChanged('audition_id')) { + $originalData = $entry->getOriginal(); + Doubler::syncForEvent($originalData->audition->event_id); + } } /** @@ -29,7 +44,9 @@ class EntryObserver */ public function deleted(Entry $entry): void { - + Doubler::where('student_id', $entry->student_id)->delete(); + $audition = Audition::where('id', $entry->audition_id)->first(); + Doubler::syncForEvent($audition->event_id); } /** From a27b8166e25d6852a15c9d9a41f39bf22760d78a Mon Sep 17 00:00:00 2001 From: Matt Young Date: Mon, 23 Jun 2025 00:42:53 -0500 Subject: [PATCH 40/67] add entries column to doublers table migration --- database/migrations/2025_06_20_175904_create_doublers_table.php | 1 + 1 file changed, 1 insertion(+) diff --git a/database/migrations/2025_06_20_175904_create_doublers_table.php b/database/migrations/2025_06_20_175904_create_doublers_table.php index 740800e..32cae70 100644 --- a/database/migrations/2025_06_20_175904_create_doublers_table.php +++ b/database/migrations/2025_06_20_175904_create_doublers_table.php @@ -21,6 +21,7 @@ return new class extends Migration // Doubler Specific Fields $table->integer('entry_count'); + $table->json('entries')->nullable(); $table->foreignIdFor(Entry::class, 'accepted_entry')->nullable()->constrained('entries')->cascadeOnDelete()->cascadeOnUpdate(); // Set the composite primary key From 630efaf00f882eae9b3cc01c695be838137a322a Mon Sep 17 00:00:00 2001 From: Matt Young Date: Mon, 23 Jun 2025 00:52:19 -0500 Subject: [PATCH 41/67] remove entry totals from Doubler database. Save list of entries for each doubler. --- app/Models/Doubler.php | 16 ++++++++++++---- .../2025_06_20_175904_create_doublers_table.php | 1 - 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/app/Models/Doubler.php b/app/Models/Doubler.php index 907aca3..0da8d7a 100644 --- a/app/Models/Doubler.php +++ b/app/Models/Doubler.php @@ -5,8 +5,6 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; -use function PHPUnit\Framework\isInstanceOf; - class Doubler extends Model { // Specify that we're not using a single primary key @@ -16,6 +14,10 @@ class Doubler extends Model protected $guarded = []; + protected $casts = [ + 'entries' => 'array', + ]; + public function student(): BelongsTo { return $this->belongsTo(Student::class); @@ -39,7 +41,8 @@ class Doubler extends Model */ public static function syncForEvent($eventId): void { - if (isInstanceOf(Event::class, $eventId)) { + + if ($eventId instanceof Event) { $eventId = $eventId->id; } @@ -61,6 +64,11 @@ class Doubler extends Model } else { $acceptedEntryId = null; } + + // Form a list of entry IDs for this doubler + $entryList = []; + $entryList = $student->entriesForEvent($eventId)->pluck('id')->toArray(); + // Create or update the doubler record static::updateOrCreate( [ @@ -68,8 +76,8 @@ class Doubler extends Model 'event_id' => $eventId, ], [ + 'entries' => $entryList, 'accepted_entry' => $acceptedEntryId, - 'entry_count' => $student->entriesForEvent($eventId)->count(), ] ); } diff --git a/database/migrations/2025_06_20_175904_create_doublers_table.php b/database/migrations/2025_06_20_175904_create_doublers_table.php index 32cae70..60208c9 100644 --- a/database/migrations/2025_06_20_175904_create_doublers_table.php +++ b/database/migrations/2025_06_20_175904_create_doublers_table.php @@ -20,7 +20,6 @@ return new class extends Migration $table->foreignIdFor(Event::class)->constrained()->cascadeOnDelete()->cascadeOnUpdate(); // Doubler Specific Fields - $table->integer('entry_count'); $table->json('entries')->nullable(); $table->foreignIdFor(Entry::class, 'accepted_entry')->nullable()->constrained('entries')->cascadeOnDelete()->cascadeOnUpdate(); From f0daa05fcf240db7ba64c55435bcad11abbeace1 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Mon, 23 Jun 2025 02:36:44 -0500 Subject: [PATCH 42/67] Model updates dealing with doublers. --- app/Models/Doubler.php | 25 +++++++++++++++---------- app/Models/Student.php | 2 +- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/app/Models/Doubler.php b/app/Models/Doubler.php index 0da8d7a..74d1c56 100644 --- a/app/Models/Doubler.php +++ b/app/Models/Doubler.php @@ -4,6 +4,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Support\Collection; class Doubler extends Model { @@ -28,6 +29,11 @@ class Doubler extends Model return $this->belongsTo(Event::class); } + public function entries(): Collection + { + return Entry::whereIn('id', $this->entries)->get(); + } + // Find a doubler based on both keys public static function findDoubler($studentId, $eventId) { @@ -54,20 +60,19 @@ class Doubler extends Model ->where('auditions.event_id', $eventId) ->groupBy('students.id') ->havingRaw('COUNT(entries.id) > 1') + ->with('entries') ->get(); foreach ($studentsWithMultipleEntries as $student) { // Get entries that are not declined. If only one, they're our accepted entry. - $availableEntries = $student->entries()->available()->get(); - if ($availableEntries->count() === 1) { - $acceptedEntryId = $availableEntries->first()->id; - } else { - $acceptedEntryId = null; - } - - // Form a list of entry IDs for this doubler - $entryList = []; - $entryList = $student->entriesForEvent($eventId)->pluck('id')->toArray(); + $entryList = collect(); // List of entry ids for th is student in this event + $undecidedEntries = collect(); // List of entry ids that are not declined, no-show, or failed prelim + $entryList = $student->entriesForEvent($eventId)->pluck('id'); + $undecidedEntries = $student->entriesForEvent($eventId) + ->whereDoesntHave('flags', function ($query) { + $query->whereIn('flag_name', ['declined', 'no-show', 'failed-prelim']); + }) + ->pluck('id'); // Create or update the doubler record static::updateOrCreate( diff --git a/app/Models/Student.php b/app/Models/Student.php index 3769f74..6b3c943 100644 --- a/app/Models/Student.php +++ b/app/Models/Student.php @@ -99,7 +99,7 @@ class Student extends Model { $eventId = $event instanceof Event ? $event->id : $event; - return DoublerEntryCount::where([ + return Doubler::where([ 'event_id' => $eventId, 'student_id' => $this->id, ])->exists(); From 88ef36d8be9934d7f79d71c4058a8f4cb7f98e36 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Mon, 23 Jun 2025 03:25:10 -0500 Subject: [PATCH 43/67] Cleanup on doubler model --- app/Models/Doubler.php | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/app/Models/Doubler.php b/app/Models/Doubler.php index 74d1c56..3c8e370 100644 --- a/app/Models/Doubler.php +++ b/app/Models/Doubler.php @@ -62,29 +62,31 @@ class Doubler extends Model ->havingRaw('COUNT(entries.id) > 1') ->with('entries') ->get(); - + Doubler::where('event_id', $eventId)->delete(); foreach ($studentsWithMultipleEntries as $student) { // Get entries that are not declined. If only one, they're our accepted entry. $entryList = collect(); // List of entry ids for th is student in this event $undecidedEntries = collect(); // List of entry ids that are not declined, no-show, or failed prelim $entryList = $student->entriesForEvent($eventId)->pluck('id'); - $undecidedEntries = $student->entriesForEvent($eventId) - ->whereDoesntHave('flags', function ($query) { - $query->whereIn('flag_name', ['declined', 'no-show', 'failed-prelim']); - }) - ->pluck('id'); + $undecidedEntries = $student->entriesForEvent($eventId)->filter(function ($entry) { + return ! $entry->hasFlag('declined') + && ! $entry->hasFlag('no_show') + && ! $entry->hasFlag('failed_prelim'); + })->pluck('id'); + if ($undecidedEntries->count() < 2) { + $acceptedEntryId = $undecidedEntries->first(); + } else { + $acceptedEntryId = null; + } // Create or update the doubler record - static::updateOrCreate( - [ - 'student_id' => $student->id, - 'event_id' => $eventId, - ], - [ - 'entries' => $entryList, - 'accepted_entry' => $acceptedEntryId, - ] - ); + static::create([ + 'student_id' => $student->id, + 'event_id' => $eventId, + 'entries' => $entryList, + 'accepted_entry' => $acceptedEntryId, + ]); + } // remove doubler records for students who no longer have multiple entries From 63b60e6bf5f39702abf02dbb694a626963677a33 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Mon, 23 Jun 2025 08:25:23 -0500 Subject: [PATCH 44/67] Work on doubler blocks on seating page --- .../Tabulation/SeatAuditionFormController.php | 33 ++------ app/Models/Doubler.php | 3 +- app/Models/Student.php | 5 ++ app/Observers/EntryFlagObserver.php | 49 +++++++++++ app/Providers/AppServiceProvider.php | 5 +- ...uditionSeating-doubler-block-OLD.blade.php | 55 +++++++++++++ .../auditionSeating-doubler-block.blade.php | 82 +++++++------------ .../auditionSeating-results-table.blade.php | 2 +- .../tabulation/auditionSeating.blade.php | 48 ++++------- 9 files changed, 169 insertions(+), 113 deletions(-) create mode 100644 app/Observers/EntryFlagObserver.php create mode 100644 resources/views/tabulation/auditionSeating-doubler-block-OLD.blade.php diff --git a/app/Http/Controllers/Tabulation/SeatAuditionFormController.php b/app/Http/Controllers/Tabulation/SeatAuditionFormController.php index 6893a20..a01e3ed 100644 --- a/app/Http/Controllers/Tabulation/SeatAuditionFormController.php +++ b/app/Http/Controllers/Tabulation/SeatAuditionFormController.php @@ -6,6 +6,7 @@ use App\Actions\Tabulation\GetAuditionSeats; use App\Actions\Tabulation\RankAuditionEntries; use App\Http\Controllers\Controller; use App\Models\Audition; +use App\Models\Doubler; use Illuminate\Http\Request; class SeatAuditionFormController extends Controller @@ -15,7 +16,7 @@ class SeatAuditionFormController extends Controller $ranker = app(RankAuditionEntries::class); // Get scored entries in order $scored_entries = $ranker($audition, 'seating'); - + $scored_entries->load(['student.doublers', 'student.school']); // Get unscored entries sorted by draw number $unscored_entries = $audition->entries() ->whereDoesntHave('totalScore') @@ -49,29 +50,11 @@ class SeatAuditionFormController extends Controller ->orderBy('draw_number', 'asc') ->get(); - // Get Doubler Data - $doubler_data = []; - foreach ($scored_entries as $e) { - if ($e->student->isDoublerInEvent($audition->event_id)) { - // get the other entries for this student - $doubler_data[$e->id]['entries'] = $e->student->entriesForEvent($e->audition->event_id); - - // How many of this student's entries have been declined - $declined_count = $doubler_data[$e->id]['entries']->filter(function ($entry) { - return $entry->hasFlag('declined'); - })->count(); - - // set status - // declined status is easy - if ($e->hasFlag('declined')) { - $doubler_data[$e->id]['status'] = 'declined'; - } elseif ($doubler_data[$e->id]['entries']->count() - $declined_count == 1) { - $doubler_data[$e->id]['status'] = 'accepted'; - } else { - $doubler_data[$e->id]['status'] = 'undecided'; - } - } - } + // Get Doublers + $doublerData = Doubler::where('event_id', $audition->event_id) + ->whereIn('student_id', $scored_entries->pluck('student_id')) + ->get() + ->keyBy('student_id'); return view('tabulation.auditionSeating', compact('audition', @@ -79,7 +62,7 @@ class SeatAuditionFormController extends Controller 'unscored_entries', 'noshow_entries', 'failed_prelim_entries', - 'doubler_data', ) + 'doublerData') ); } diff --git a/app/Models/Doubler.php b/app/Models/Doubler.php index 3c8e370..ad7c4f9 100644 --- a/app/Models/Doubler.php +++ b/app/Models/Doubler.php @@ -4,7 +4,6 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; -use Illuminate\Support\Collection; class Doubler extends Model { @@ -29,7 +28,7 @@ class Doubler extends Model return $this->belongsTo(Event::class); } - public function entries(): Collection + public function entries() { return Entry::whereIn('id', $this->entries)->get(); } diff --git a/app/Models/Student.php b/app/Models/Student.php index 6b3c943..f68c4b4 100644 --- a/app/Models/Student.php +++ b/app/Models/Student.php @@ -95,6 +95,11 @@ class Student extends Model return $this->hasMany(DoublerRequest::class); } + public function doublers(): HasMany + { + return $this->hasMany(Doubler::class); + } + public function isDoublerInEvent(Event|int $event): bool { $eventId = $event instanceof Event ? $event->id : $event; diff --git a/app/Observers/EntryFlagObserver.php b/app/Observers/EntryFlagObserver.php new file mode 100644 index 0000000..e66d8cc --- /dev/null +++ b/app/Observers/EntryFlagObserver.php @@ -0,0 +1,49 @@ +isProduction()); + // Model::preventLazyLoading(! app()->isProduction()); } } diff --git a/resources/views/tabulation/auditionSeating-doubler-block-OLD.blade.php b/resources/views/tabulation/auditionSeating-doubler-block-OLD.blade.php new file mode 100644 index 0000000..9f7635d --- /dev/null +++ b/resources/views/tabulation/auditionSeating-doubler-block-OLD.blade.php @@ -0,0 +1,55 @@ +@php($doublerButtonClasses = 'hidden rounded-md bg-white px-2.5 py-1.5 text-xs text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 sm:block') +
    + + @foreach($entry['doubleData'] as $double) + @php($isopen = $double['status'] == 'undecided') +
  • + + +
    +
    +
      +
    • +

      Ranked {{ $double['rank'] }}

      +

      {{ $double['unscored_entries'] }} Unscored

      +
    • + @foreach($double['seating_limits'] as $limit) +
    • {{$limit['ensemble_name']}} accepts {{ $limit['accepts'] }}
    • + @endforeach +
    +
    + +
    + @if ($double['status'] == 'undecided') +
    + @csrf + +
    +
    + @csrf + +
    + @endif +
    +
    +
  • + @endforeach + +
+ +{{--Complete Badge--}} +{{--

Complete

--}} + +{{--In Progres Badge--}} +{{--

In Progress

--}} diff --git a/resources/views/tabulation/auditionSeating-doubler-block.blade.php b/resources/views/tabulation/auditionSeating-doubler-block.blade.php index 9f7635d..133b19a 100644 --- a/resources/views/tabulation/auditionSeating-doubler-block.blade.php +++ b/resources/views/tabulation/auditionSeating-doubler-block.blade.php @@ -1,55 +1,35 @@ -@php($doublerButtonClasses = 'hidden rounded-md bg-white px-2.5 py-1.5 text-xs text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 sm:block') -
    +
    {{-- Begin block for doubler entry --}} +
    + {{ $de->audition->name }} #{{$de->draw_number}} + ({{ $de->id }}) +
    + @if($de->hasFlag('no_show')) +
    NO-SHOW
    + @elseif($de->hasFlag('failed_prelim')) +
    Failed Prelim
    + @elseif($de->hasFlag('declined')) +
    Declined
    + @else + @php($unscored = $de->audition->unscoredEntries()->count()) + @if($unscored > 0) +
    {{ $unscored }} Unscored Entries
    + @endif - @foreach($entry['doubleData'] as $double) - @php($isopen = $double['status'] == 'undecided') -
  • -
    -

    - - {{ $double['auditionName'] }} - {{ $double['status'] }} - -

    -
    -
    - -
    -
    + @if(! $de->rank('seating')) +
    THIS ENTRY NOT SCORED
    + @else +
    Ranked: {{ $de->rank('seating') }}
    +
    + Acceptance Limits
    + @foreach ($de->audition->SeatingLimits as $limit) + {{ $limit->ensemble->name }} -> {{ $limit->maximum_accepted }} +
    + @endforeach
    - -
    -
    -
      -
    • -

      Ranked {{ $double['rank'] }}

      -

      {{ $double['unscored_entries'] }} Unscored

      -
    • - @foreach($double['seating_limits'] as $limit) -
    • {{$limit['ensemble_name']}} accepts {{ $limit['accepts'] }}
    • - @endforeach -
    -
    - -
    - @if ($double['status'] == 'undecided') -
    - @csrf - -
    -
    - @csrf - -
    - @endif -
    +
    + Decline {{ $de->audition->name }}
    -
  • - @endforeach + @endif + @endif -
- -{{--Complete Badge--}} -{{--

Complete

--}} - -{{--In Progres Badge--}} -{{--

In Progress

--}} + diff --git a/resources/views/tabulation/auditionSeating-results-table.blade.php b/resources/views/tabulation/auditionSeating-results-table.blade.php index 41f7691..0e3552d 100644 --- a/resources/views/tabulation/auditionSeating-results-table.blade.php +++ b/resources/views/tabulation/auditionSeating-results-table.blade.php @@ -40,7 +40,7 @@

Request: {{$entry['doublerRequest']}}

@endif - @include('tabulation.auditionSeating-doubler-block') + @include('tabulation.auditionSeating-doubler-block-OLD') {{-- DOUBLER
--}} {{-- @foreach($entry['doubleData'] as $double)--}} {{-- ID: {{ $double['entryId'] }} - {{ $double['name'] }} - {{ $double['rank'] }}
--}} diff --git a/resources/views/tabulation/auditionSeating.blade.php b/resources/views/tabulation/auditionSeating.blade.php index 992817b..ba705b3 100644 --- a/resources/views/tabulation/auditionSeating.blade.php +++ b/resources/views/tabulation/auditionSeating.blade.php @@ -32,44 +32,26 @@ {{ $entry->id }} {{ $entry->draw_number }} -
{{ $entry->student->full_name() }}
+
{{ $entry->student->school->name }}
- @if( isset($doubler_data[$entry->id]) ) - {{-- Check if this entry is a doubler --}} - @foreach($doubler_data[$entry->id]['entries'] as $de) - {{-- If it is, render doubler blocks --}} -
- {{-- @var \App\Models\Entry $de --}} -
- {{ $de->audition->name }} #{{$de->draw_number}} ({{ $de->id }}) -
- @php($unscored = $de->audition->unscoredEntries()->count()) - @if($unscored > 0) -
{{ $unscored }} Unscored Entries
- @endif - @if(! $de->rank('seating')) -
THIS ENTRY NOT SCORED
- @else -
Ranked: {{ $de->rank('seating') }}
-
- Acceptance Limits
- @foreach ($de->audition->SeatingLimits as $limit) - {{ $limit->ensemble->name }} -> {{ $limit->maximum_accepted }} -
- @endforeach -
-
- - - - -
- @endif -
+ @php($doubler = $doublerData->get($entry->student_id)) + @if($doubler) + @if($doubler->accepted_entry == $entry->id) + ACCEPTED + @elseif($entry->hasFlag('declined')) + DECLINED + @else + UNDECIDED + + @foreach($entry->student->entriesForEvent($entry->audition->event_id) as $de) + @include('tabulation.auditionSeating-doubler-block') @endforeach + @endif @endif + +
{{ $entry->totalScore->seating_total }} From e1719c64fa627ab50f8f297841c0a704b1cb5135 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Tue, 24 Jun 2025 09:24:53 -0500 Subject: [PATCH 45/67] Seats can be declined from seating page. Doubler system functioning. --- app/Http/Controllers/DashboardController.php | 31 +------------------ .../Tabulation/SeatAuditionFormController.php | 9 ++++++ resources/views/dashboard/dashboard.blade.php | 30 +++++++++--------- .../auditionSeating-doubler-block.blade.php | 4 ++- .../tabulation/auditionSeating.blade.php | 12 ++++++- routes/tabulation.php | 1 + 6 files changed, 40 insertions(+), 47 deletions(-) diff --git a/app/Http/Controllers/DashboardController.php b/app/Http/Controllers/DashboardController.php index 3e04766..8ece8e6 100644 --- a/app/Http/Controllers/DashboardController.php +++ b/app/Http/Controllers/DashboardController.php @@ -2,10 +2,6 @@ namespace App\Http\Controllers; -use App\Actions\Entries\GetEntrySeatingResult; -use App\Actions\Tabulation\CalculateEntryScore; -use App\Actions\Tabulation\RankAuditionEntries; -use App\Models\AuditionFlag; use App\Models\School; use App\Services\Invoice\InvoiceDataService; use Illuminate\Support\Facades\Auth; @@ -28,33 +24,8 @@ class DashboardController extends Controller } public function dashboard( - CalculateEntryScore $scoreCalc, - GetEntrySeatingResult $resultGenerator, - RankAuditionEntries $ranker ) { - - // Info for director results report - $entries = Auth::user()->entries; - $entries = $entries->filter(function ($entry) { - return $entry->audition->hasFlag('seats_published'); - }); - $entries = $entries->sortBy(function ($entry) { - return $entry->student->full_name(true); - }); - $scores = []; - $results = []; - $ranks = []; - foreach ($entries as $entry) { - $results[$entry->id] = $resultGenerator->getResult($entry); - if (! $entry->hasFlag('no_show') && ! $entry->hasFlag('failed_prelim')) { - $scores[$entry->id] = $scoreCalc->calculate('seating', $entry); - $auditionResults = $ranker->rank('seating', $entry->audition); - $ranks[$entry->id] = $auditionResults->firstWhere('id', $entry->id)->raw_rank; - } - } - $showRecapLink = AuditionFlag::where('flag_name', 'seats_published')->count() > 0; - - return view('dashboard.dashboard', compact('entries', 'scores', 'results', 'ranks', 'showRecapLink')); + return view('dashboard.dashboard'); // return view('dashboard.dashboard'); } diff --git a/app/Http/Controllers/Tabulation/SeatAuditionFormController.php b/app/Http/Controllers/Tabulation/SeatAuditionFormController.php index a01e3ed..5a0411f 100644 --- a/app/Http/Controllers/Tabulation/SeatAuditionFormController.php +++ b/app/Http/Controllers/Tabulation/SeatAuditionFormController.php @@ -7,6 +7,7 @@ use App\Actions\Tabulation\RankAuditionEntries; use App\Http\Controllers\Controller; use App\Models\Audition; use App\Models\Doubler; +use App\Models\Entry; use Illuminate\Http\Request; class SeatAuditionFormController extends Controller @@ -66,6 +67,14 @@ class SeatAuditionFormController extends Controller ); } + public function declineSeat(Audition $audition, Entry $entry) + { + $entry->addFlag('declined'); + + return redirect()->route('seating.audition', ['audition' => $audition->id])->with('success', + $entry->student->full_name().' has declined '.$audition->name); + } + protected function pickRightPanel(Audition $audition, array $seatable) { if ($audition->hasFlag('seats_published')) { diff --git a/resources/views/dashboard/dashboard.blade.php b/resources/views/dashboard/dashboard.blade.php index 7eda747..cf0a19c 100644 --- a/resources/views/dashboard/dashboard.blade.php +++ b/resources/views/dashboard/dashboard.blade.php @@ -27,24 +27,24 @@ @endif - @if($showRecapLink) - - - Audition Score Recaps - - - @endif +{{-- @if($showRecapLink)--}} +{{-- --}} +{{-- --}} +{{-- Audition Score Recaps--}} +{{-- --}} +{{-- --}} +{{-- @endif--}} - @if(Auth::user()->school_id) -
- - My Results - @include('dashboard.results-table') - -
- @endif +{{-- @if(Auth::user()->school_id)--}} +{{--
--}} +{{-- --}} +{{-- My Results--}} +{{-- @include('dashboard.results-table')--}} +{{-- --}} +{{--
--}} +{{-- @endif--}}
diff --git a/resources/views/tabulation/auditionSeating-doubler-block.blade.php b/resources/views/tabulation/auditionSeating-doubler-block.blade.php index 133b19a..e3c5a85 100644 --- a/resources/views/tabulation/auditionSeating-doubler-block.blade.php +++ b/resources/views/tabulation/auditionSeating-doubler-block.blade.php @@ -27,7 +27,9 @@ @endforeach
- Decline {{ $de->audition->name }} + + Decline {{ $de->audition->name }} +
@endif @endif diff --git a/resources/views/tabulation/auditionSeating.blade.php b/resources/views/tabulation/auditionSeating.blade.php index ba705b3..dd5c094 100644 --- a/resources/views/tabulation/auditionSeating.blade.php +++ b/resources/views/tabulation/auditionSeating.blade.php @@ -43,8 +43,18 @@ @elseif($entry->hasFlag('declined')) DECLINED @else - UNDECIDED + @if($request = $entry->student->doublerRequests()->where('event_id',$entry->audition->event_id)->first()) +
{{-- Begin block seating request --}} +
+ Request +
+
+

{{ $request->request }}

+
+
+ + @endif @foreach($entry->student->entriesForEvent($entry->audition->event_id) as $de) @include('tabulation.auditionSeating-doubler-block') @endforeach diff --git a/routes/tabulation.php b/routes/tabulation.php index 274f631..2633ad6 100644 --- a/routes/tabulation.php +++ b/routes/tabulation.php @@ -43,6 +43,7 @@ Route::middleware(['auth', 'verified', CheckIfCanTab::class])->group(function () Route::prefix('seating/')->group(function () { Route::get('/', SeatingStatusController::class)->name('seating.status'); Route::get('/{audition}', [SeatAuditionFormController::class, 'showForm'])->name('seating.audition'); + Route::post('/{audition}/{entry}/decline', [SeatAuditionFormController::class, 'declineSeat'])->name('seating.audition.decline'); Route::post('/{audition}/publish', [SeatingPublicationController::class, 'publishSeats'])->name('seating.audition.publish'); Route::post('/{audition}/unpublish', From fba625c3165f57ef17e23e7014f9b6659158b290 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Wed, 25 Jun 2025 15:25:10 -0500 Subject: [PATCH 46/67] Create action for entering no_show and failed_prelim flags --- app/Actions/Tabulation/EnterNoShow.php | 55 ++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 app/Actions/Tabulation/EnterNoShow.php diff --git a/app/Actions/Tabulation/EnterNoShow.php b/app/Actions/Tabulation/EnterNoShow.php new file mode 100644 index 0000000..5d3e0d0 --- /dev/null +++ b/app/Actions/Tabulation/EnterNoShow.php @@ -0,0 +1,55 @@ +audition->hasFlag('seats_published')) { + throw new AuditionAdminException('Cannot enter a no-show for an entry in an audition where seats are published'); + } + if ($entry->audition->hasFlag('advancement_published')) { + throw new AuditionAdminException('Cannot enter a no-show for an entry in an audition where advancement is published'); + } + DB::table('score_sheets')->where('entry_id', $entry->id)->delete(); + + ScoreSheet::where('entry_id', $entry->id)->delete(); + BonusScore::where('entry_id', $entry->id)->delete(); + EntryTotalScore::where('entry_id', $entry->id)->delete(); + if ($flagType == 'failed-prelim') { + $msg = 'Failed prelim has been entered for '.$entry->audition->name.' #'.$entry->draw_number.' (ID: '.$entry->id.').'; + $entry->addFlag('failed_prelim'); + } else { + $entry->addFlag('no_show'); + $msg = 'No Show has been entered for '.$entry->audition->name.' #'.$entry->draw_number.' (ID: '.$entry->id.').'; + } + + return $msg; + } +} From 5e687bcbc698139f7abf957371cb7625d212f99b Mon Sep 17 00:00:00 2001 From: Matt Young Date: Wed, 25 Jun 2025 15:38:57 -0500 Subject: [PATCH 47/67] Modify EntryFlagController to use teh new enter no show action. --- app/Actions/Tabulation/EnterNoShow.php | 7 +-- .../Tabulation/EntryFlagController.php | 47 ++++++++++++------- 2 files changed, 33 insertions(+), 21 deletions(-) diff --git a/app/Actions/Tabulation/EnterNoShow.php b/app/Actions/Tabulation/EnterNoShow.php index 5d3e0d0..3bd1718 100644 --- a/app/Actions/Tabulation/EnterNoShow.php +++ b/app/Actions/Tabulation/EnterNoShow.php @@ -7,6 +7,7 @@ use App\Models\BonusScore; use App\Models\Entry; use App\Models\EntryTotalScore; use App\Models\ScoreSheet; +use Illuminate\Support\Facades\DB; class EnterNoShow { @@ -25,9 +26,9 @@ class EnterNoShow * @throws AuditionAdminException If an invalid flag type is provided, * or the action violates business rules. */ - public function __invoke(Entry $entry, string $flagType = 'no-show'): string + public function __invoke(Entry $entry, string $flagType = 'noshow'): string { - if ($flagType !== 'no-show' && $flagType !== 'failed-prelim') { + if ($flagType !== 'noshow' && $flagType !== 'failprelim') { throw new AuditionAdminException('Invalid flag type'); } @@ -42,7 +43,7 @@ class EnterNoShow ScoreSheet::where('entry_id', $entry->id)->delete(); BonusScore::where('entry_id', $entry->id)->delete(); EntryTotalScore::where('entry_id', $entry->id)->delete(); - if ($flagType == 'failed-prelim') { + if ($flagType == 'failprelim') { $msg = 'Failed prelim has been entered for '.$entry->audition->name.' #'.$entry->draw_number.' (ID: '.$entry->id.').'; $entry->addFlag('failed_prelim'); } else { diff --git a/app/Http/Controllers/Tabulation/EntryFlagController.php b/app/Http/Controllers/Tabulation/EntryFlagController.php index d9c8aac..8154120 100644 --- a/app/Http/Controllers/Tabulation/EntryFlagController.php +++ b/app/Http/Controllers/Tabulation/EntryFlagController.php @@ -2,6 +2,7 @@ namespace App\Http\Controllers\Tabulation; +use App\Exceptions\AuditionAdminException; use App\Http\Controllers\Controller; use App\Models\BonusScore; use App\Models\Entry; @@ -69,28 +70,38 @@ class EntryFlagController extends Controller 'scores')); } + /** + * @throws AuditionAdminException + */ public function enterNoShow(Entry $entry) { - if ($entry->audition->hasFlag('seats_published')) { - return to_route('entry-flags.noShowSelect')->with('error', - 'Cannot enter a no-show for an entry in an audition where seats are published'); + $recorder = app('App\Actions\Tabulation\EnterNoShow'); + try { + $msg = $recorder($entry, request()->input('noshow-type')); + } catch (AuditionAdminException $e) { + return to_route('entry-flags.noShowSelect')->with('error', $e->getMessage()); } - if ($entry->audition->hasFlag('advancement_published')) { - return to_route('entry-flags.noShowSelect')->with('error', - 'Cannot enter a no-show for an entry in an audition where advancement is published'); - } - DB::table('score_sheets')->where('entry_id', $entry->id)->delete(); - ScoreSheet::where('entry_id', $entry->id)->delete(); - BonusScore::where('entry_id', $entry->id)->delete(); - EntryTotalScore::where('entry_id', $entry->id)->delete(); - if (request()->input('noshow-type') == 'failprelim') { - $msg = 'Failed prelim has been entered for '.$entry->audition->name.' #'.$entry->draw_number.' (ID: '.$entry->id.').'; - $entry->addFlag('failed_prelim'); - } else { - $entry->addFlag('no_show'); - $msg = 'No Show has been entered for '.$entry->audition->name.' #'.$entry->draw_number.' (ID: '.$entry->id.').'; - } + // if ($entry->audition->hasFlag('seats_published')) { + // return to_route('entry-flags.noShowSelect')->with('error', + // 'Cannot enter a no-show for an entry in an audition where seats are published'); + // } + // if ($entry->audition->hasFlag('advancement_published')) { + // return to_route('entry-flags.noShowSelect')->with('error', + // 'Cannot enter a no-show for an entry in an audition where advancement is published'); + // } + // DB::table('score_sheets')->where('entry_id', $entry->id)->delete(); + // + // ScoreSheet::where('entry_id', $entry->id)->delete(); + // BonusScore::where('entry_id', $entry->id)->delete(); + // EntryTotalScore::where('entry_id', $entry->id)->delete(); + // if (request()->input('noshow-type') == 'failprelim') { + // $msg = 'Failed prelim has been entered for '.$entry->audition->name.' #'.$entry->draw_number.' (ID: '.$entry->id.').'; + // $entry->addFlag('failed_prelim'); + // } else { + // $entry->addFlag('no_show'); + // $msg = 'No Show has been entered for '.$entry->audition->name.' #'.$entry->draw_number.' (ID: '.$entry->id.').'; + // } return to_route('entry-flags.noShowSelect')->with('success', $msg); } From 0468cb5d11daab45d853285ee19fb59b50502d57 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Wed, 25 Jun 2025 21:20:33 -0500 Subject: [PATCH 48/67] add ability to mark no-shows and accept/decline doublers from the seating page. --- .../Tabulation/EntryFlagController.php | 25 --------------- .../Tabulation/SeatAuditionFormController.php | 32 +++++++++++++++++++ .../auditionSeating-doubler-block.blade.php | 4 +++ .../tabulation/auditionSeating.blade.php | 32 +++++++++++++++---- routes/tabulation.php | 2 ++ 5 files changed, 64 insertions(+), 31 deletions(-) diff --git a/app/Http/Controllers/Tabulation/EntryFlagController.php b/app/Http/Controllers/Tabulation/EntryFlagController.php index 8154120..5a1a8c0 100644 --- a/app/Http/Controllers/Tabulation/EntryFlagController.php +++ b/app/Http/Controllers/Tabulation/EntryFlagController.php @@ -4,12 +4,8 @@ namespace App\Http\Controllers\Tabulation; use App\Exceptions\AuditionAdminException; use App\Http\Controllers\Controller; -use App\Models\BonusScore; use App\Models\Entry; -use App\Models\EntryTotalScore; -use App\Models\ScoreSheet; use Illuminate\Http\Request; -use Illuminate\Support\Facades\DB; use function to_route; @@ -82,27 +78,6 @@ class EntryFlagController extends Controller return to_route('entry-flags.noShowSelect')->with('error', $e->getMessage()); } - // if ($entry->audition->hasFlag('seats_published')) { - // return to_route('entry-flags.noShowSelect')->with('error', - // 'Cannot enter a no-show for an entry in an audition where seats are published'); - // } - // if ($entry->audition->hasFlag('advancement_published')) { - // return to_route('entry-flags.noShowSelect')->with('error', - // 'Cannot enter a no-show for an entry in an audition where advancement is published'); - // } - // DB::table('score_sheets')->where('entry_id', $entry->id)->delete(); - // - // ScoreSheet::where('entry_id', $entry->id)->delete(); - // BonusScore::where('entry_id', $entry->id)->delete(); - // EntryTotalScore::where('entry_id', $entry->id)->delete(); - // if (request()->input('noshow-type') == 'failprelim') { - // $msg = 'Failed prelim has been entered for '.$entry->audition->name.' #'.$entry->draw_number.' (ID: '.$entry->id.').'; - // $entry->addFlag('failed_prelim'); - // } else { - // $entry->addFlag('no_show'); - // $msg = 'No Show has been entered for '.$entry->audition->name.' #'.$entry->draw_number.' (ID: '.$entry->id.').'; - // } - return to_route('entry-flags.noShowSelect')->with('success', $msg); } diff --git a/app/Http/Controllers/Tabulation/SeatAuditionFormController.php b/app/Http/Controllers/Tabulation/SeatAuditionFormController.php index 5a0411f..c83b59d 100644 --- a/app/Http/Controllers/Tabulation/SeatAuditionFormController.php +++ b/app/Http/Controllers/Tabulation/SeatAuditionFormController.php @@ -4,6 +4,7 @@ namespace App\Http\Controllers\Tabulation; use App\Actions\Tabulation\GetAuditionSeats; use App\Actions\Tabulation\RankAuditionEntries; +use App\Exceptions\AuditionAdminException; use App\Http\Controllers\Controller; use App\Models\Audition; use App\Models\Doubler; @@ -75,6 +76,37 @@ class SeatAuditionFormController extends Controller $entry->student->full_name().' has declined '.$audition->name); } + public function acceptSeat(Audition $audition, Entry $entry) + { + $doublerData = Doubler::findDoubler($entry->student_id, $audition->event_id); + foreach ($doublerData->entries() as $doublerEntry) { + if (! $doublerEntry->totalScore && ! $doublerEntry->hasFlag('declined') && ! $doublerEntry->hasFlag('no_show') && ! $doublerEntry->hasFlag('failed_prelim')) { + return redirect()->route('seating.audition', ['audition' => $audition->id])->with('error', + 'Cannot accept seating for '.$entry->student->full_name().' because student has unscored entries'); + } + } + foreach ($doublerData->entries() as $doublerEntry) { + if ($doublerEntry->id !== $entry->id && ! $doublerEntry->hasFlag('no_show') && ! $doublerEntry->hasFlag('failed_prelim') && ! $doublerEntry->hasFlag('declined')) { + $doublerEntry->addFlag('declined'); + } + } + + return redirect()->route('seating.audition', ['audition' => $audition->id])->with('success', + $entry->student->full_name().' has accepted '.$audition->name); + } + + public function noshow(Audition $audition, Entry $entry) + { + $recorder = app('App\Actions\Tabulation\EnterNoShow'); + try { + $msg = $recorder($entry); + } catch (AuditionAdminException $e) { + return redirect()->back()->with('error', $e->getMessage()); + } + + return redirect()->route('seating.audition', [$audition])->with('success', $msg); + } + protected function pickRightPanel(Audition $audition, array $seatable) { if ($audition->hasFlag('seats_published')) { diff --git a/resources/views/tabulation/auditionSeating-doubler-block.blade.php b/resources/views/tabulation/auditionSeating-doubler-block.blade.php index e3c5a85..99b5829 100644 --- a/resources/views/tabulation/auditionSeating-doubler-block.blade.php +++ b/resources/views/tabulation/auditionSeating-doubler-block.blade.php @@ -27,6 +27,10 @@ @endforeach
+ {{-- TODO: Don't show the option to accept if it cannot be done --}} + + Accept {{ $de->audition->name }} + Decline {{ $de->audition->name }} diff --git a/resources/views/tabulation/auditionSeating.blade.php b/resources/views/tabulation/auditionSeating.blade.php index dd5c094..8abe770 100644 --- a/resources/views/tabulation/auditionSeating.blade.php +++ b/resources/views/tabulation/auditionSeating.blade.php @@ -32,7 +32,9 @@ {{ $entry->id }} {{ $entry->draw_number }} - +
{{ $entry->student->school->name }}
@@ -44,7 +46,8 @@ DECLINED @else @if($request = $entry->student->doublerRequests()->where('event_id',$entry->audition->event_id)->first()) -
{{-- Begin block seating request --}} +
{{-- Begin block seating request --}}
Request
@@ -55,9 +58,9 @@
@endif - @foreach($entry->student->entriesForEvent($entry->audition->event_id) as $de) - @include('tabulation.auditionSeating-doubler-block') - @endforeach + @foreach($entry->student->entriesForEvent($entry->audition->event_id) as $de) + @include('tabulation.auditionSeating-doubler-block') + @endforeach @endif @endif @@ -78,6 +81,7 @@ Draw # ID Student + @@ -89,6 +93,11 @@ {{ $entry->student->full_name() }} {{ $entry->student->school->name }} + + + Record No Show + + @endforeach @@ -150,7 +159,18 @@
- Controls + {{-- TODO: Add in bulk delince doubler option --}} + @if($unscored_entries->count() > 0) + + Cannot seat the audition while entries are unscored. + + @endif +
+ @if($doublerData->contains('accepted_entry', null)) + + Cannot seat the audition while there are unresolved doublers. + + @endif
diff --git a/routes/tabulation.php b/routes/tabulation.php index 2633ad6..ca24ea2 100644 --- a/routes/tabulation.php +++ b/routes/tabulation.php @@ -44,6 +44,8 @@ Route::middleware(['auth', 'verified', CheckIfCanTab::class])->group(function () Route::get('/', SeatingStatusController::class)->name('seating.status'); Route::get('/{audition}', [SeatAuditionFormController::class, 'showForm'])->name('seating.audition'); Route::post('/{audition}/{entry}/decline', [SeatAuditionFormController::class, 'declineSeat'])->name('seating.audition.decline'); + Route::post('/{audition}/{entry}/accept', [SeatAuditionFormController::class, 'acceptSeat'])->name('seating.audition.accept'); + Route::post('/{audition}/{entry}/noshow', [SeatAuditionFormController::class, 'noshow'])->name('seating.audition.noshow'); Route::post('/{audition}/publish', [SeatingPublicationController::class, 'publishSeats'])->name('seating.audition.publish'); Route::post('/{audition}/unpublish', From 14b6bb61c75c1501be5a39158965089b8f8a11ff Mon Sep 17 00:00:00 2001 From: Matt Young Date: Wed, 25 Jun 2025 22:51:27 -0500 Subject: [PATCH 49/67] Everything ready for seating the audition. --- app/Actions/Tabulation/EnterScore.php | 4 +-- .../Tabulation/RankAuditionEntries.php | 14 ++++++++++- .../Tabulation/ScoreController.php | 4 +-- .../Tabulation/SeatAuditionFormController.php | 25 ++++++++++++++++++- app/Models/Entry.php | 5 ++++ app/Observers/EntryFlagObserver.php | 1 + app/Observers/ScoreSheetObserver.php | 5 ++-- resources/views/admin/students/edit.blade.php | 2 +- .../auditionSeating-doubler-block.blade.php | 2 +- .../tabulation/auditionSeating.blade.php | 8 +++--- 10 files changed, 56 insertions(+), 14 deletions(-) diff --git a/app/Actions/Tabulation/EnterScore.php b/app/Actions/Tabulation/EnterScore.php index 17b87fa..e38faa8 100644 --- a/app/Actions/Tabulation/EnterScore.php +++ b/app/Actions/Tabulation/EnterScore.php @@ -9,6 +9,7 @@ namespace App\Actions\Tabulation; use App\Exceptions\ScoreEntryException; use App\Models\AuditLogEntry; use App\Models\Entry; +use App\Models\EntryTotalScore; use App\Models\ScoreSheet; use App\Models\User; use Illuminate\Support\Facades\DB; @@ -28,8 +29,7 @@ class EnterScore */ public function __invoke(User $user, Entry $entry, array $scores, ScoreSheet|false $scoreSheet = false): ScoreSheet { - // TODO: Remove the CalculatedScore model and table when rewrite is complete, they'll be obsolete - // CalculatedScore::where('entry_id', $entry->id)->delete(); + EntryTotalScore::where('entry_id', $entry->id)->delete(); $scores = collect($scores); // Basic Validity Checks diff --git a/app/Actions/Tabulation/RankAuditionEntries.php b/app/Actions/Tabulation/RankAuditionEntries.php index 5e18fb7..77fd90d 100644 --- a/app/Actions/Tabulation/RankAuditionEntries.php +++ b/app/Actions/Tabulation/RankAuditionEntries.php @@ -42,7 +42,7 @@ class RankAuditionEntries private function get_seating_ranks(Audition $audition): Collection { - return $audition->entries() + $sortedEntries = $audition->entries() ->whereHas('totalScore') ->with('totalScore') ->with('student.school') @@ -61,6 +61,18 @@ class RankAuditionEntries ->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.seating_subscore_totals, "$[9]"), -999999) DESC') ->select('entries.*') ->get(); + + $rankOn = 1; + foreach ($sortedEntries as $entry) { + if ($entry->hasFlag('declined')) { + $entry->seatingRank = 'declined'; + } else { + $entry->seatingRank = $rankOn; + $rankOn++; + } + } + + return $sortedEntries; } private function get_advancement_ranks(Audition $audition): Collection diff --git a/app/Http/Controllers/Tabulation/ScoreController.php b/app/Http/Controllers/Tabulation/ScoreController.php index b7e2013..b4863a8 100644 --- a/app/Http/Controllers/Tabulation/ScoreController.php +++ b/app/Http/Controllers/Tabulation/ScoreController.php @@ -5,8 +5,8 @@ namespace App\Http\Controllers\Tabulation; use App\Actions\Tabulation\EnterScore; use App\Exceptions\ScoreEntryException; use App\Http\Controllers\Controller; -use App\Models\CalculatedScore; use App\Models\Entry; +use App\Models\EntryTotalScore; use App\Models\ScoreSheet; use App\Models\User; use Illuminate\Http\Request; @@ -25,7 +25,7 @@ class ScoreController extends Controller public function destroyScore(ScoreSheet $score) { - CalculatedScore::where('entry_id', $score->entry_id)->delete(); + EntryTotalScore::where('entry_id', $score->entry_id)->delete(); if ($score->entry->audition->hasFlag('seats_published')) { return redirect()->back()->with('error', 'Cannot delete scores for an entry where seats are published'); } diff --git a/app/Http/Controllers/Tabulation/SeatAuditionFormController.php b/app/Http/Controllers/Tabulation/SeatAuditionFormController.php index c83b59d..f0bb00c 100644 --- a/app/Http/Controllers/Tabulation/SeatAuditionFormController.php +++ b/app/Http/Controllers/Tabulation/SeatAuditionFormController.php @@ -10,6 +10,9 @@ use App\Models\Audition; use App\Models\Doubler; use App\Models\Entry; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Cache; + +use function PHPUnit\Framework\isNull; class SeatAuditionFormController extends Controller { @@ -58,19 +61,38 @@ class SeatAuditionFormController extends Controller ->get() ->keyBy('student_id'); + $auditionHasUnresolvedDoublers = false; + foreach ($doublerData as $doubler) { + if (! isNull($doubler->accepted_entry)) { + continue; + } + foreach ($doubler->entries() as $entry) { + if ($entry->audition_id === $audition->id && $entry->hasFlag('declined')) { + continue 2; + } + } + $auditionHasUnresolvedDoublers = true; + } + + $canSeat = ! $auditionHasUnresolvedDoublers && $unscored_entries->count() === 0; + return view('tabulation.auditionSeating', compact('audition', 'scored_entries', 'unscored_entries', 'noshow_entries', 'failed_prelim_entries', - 'doublerData') + 'doublerData', + 'auditionHasUnresolvedDoublers', + 'canSeat', + ) ); } public function declineSeat(Audition $audition, Entry $entry) { $entry->addFlag('declined'); + Cache::forget('rank_seating_'.$entry->audition_id); return redirect()->route('seating.audition', ['audition' => $audition->id])->with('success', $entry->student->full_name().' has declined '.$audition->name); @@ -86,6 +108,7 @@ class SeatAuditionFormController extends Controller } } foreach ($doublerData->entries() as $doublerEntry) { + Cache::forget('rank_seating_'.$doublerEntry->audition_id); if ($doublerEntry->id !== $entry->id && ! $doublerEntry->hasFlag('no_show') && ! $doublerEntry->hasFlag('failed_prelim') && ! $doublerEntry->hasFlag('declined')) { $doublerEntry->addFlag('declined'); } diff --git a/app/Models/Entry.php b/app/Models/Entry.php index 4b1cc64..b7c0e82 100644 --- a/app/Models/Entry.php +++ b/app/Models/Entry.php @@ -45,6 +45,11 @@ class Entry extends Model // Get the ranked entries for this entries audition $rankedEntries = $ranker($this->audition, $type); + // If we're looking for seating rank, return the rank from the list of ranked entries + if ($type === 'seating') { + return $rankedEntries->where('id', $this->id)->first()->seatingRank; + } + // Find position of current entry in the ranked entries (1-based index) $position = $rankedEntries->search(fn ($entry) => $entry->id === $this->id); diff --git a/app/Observers/EntryFlagObserver.php b/app/Observers/EntryFlagObserver.php index e66d8cc..7747ca1 100644 --- a/app/Observers/EntryFlagObserver.php +++ b/app/Observers/EntryFlagObserver.php @@ -13,6 +13,7 @@ class EntryFlagObserver public function created(EntryFlag $entryFlag): void { Doubler::syncDoublers(); + } /** diff --git a/app/Observers/ScoreSheetObserver.php b/app/Observers/ScoreSheetObserver.php index ceb8711..0b419af 100644 --- a/app/Observers/ScoreSheetObserver.php +++ b/app/Observers/ScoreSheetObserver.php @@ -2,7 +2,7 @@ namespace App\Observers; -use App\Events\ScoreSheetChange; +use App\Actions\Tabulation\TotalEntryScores; use App\Models\ScoreSheet; class ScoreSheetObserver @@ -12,7 +12,8 @@ class ScoreSheetObserver */ public function created(ScoreSheet $scoreSheet): void { - // + $calculator = app(TotalEntryScores::class); + $calculator($scoreSheet->entry, true); } /** diff --git a/resources/views/admin/students/edit.blade.php b/resources/views/admin/students/edit.blade.php index c908aa4..e7df592 100644 --- a/resources/views/admin/students/edit.blade.php +++ b/resources/views/admin/students/edit.blade.php @@ -55,7 +55,7 @@ @foreach($event_entries[$event->id] as $entry) {{ $entry->id }} - {{ $entry->audition->name }} + {{ $entry->audition->name }} {{ $entry->draw_number }} @if($entry->doubler_decision_frozen) diff --git a/resources/views/tabulation/auditionSeating-doubler-block.blade.php b/resources/views/tabulation/auditionSeating-doubler-block.blade.php index 99b5829..6b7fcbb 100644 --- a/resources/views/tabulation/auditionSeating-doubler-block.blade.php +++ b/resources/views/tabulation/auditionSeating-doubler-block.blade.php @@ -1,6 +1,6 @@
{{-- Begin block for doubler entry --}}
- {{ $de->audition->name }} #{{$de->draw_number}} + {{ $de->audition->name }} #{{$de->draw_number}} ({{ $de->id }})
@if($de->hasFlag('no_show')) diff --git a/resources/views/tabulation/auditionSeating.blade.php b/resources/views/tabulation/auditionSeating.blade.php index 8abe770..a5fd3cd 100644 --- a/resources/views/tabulation/auditionSeating.blade.php +++ b/resources/views/tabulation/auditionSeating.blade.php @@ -27,8 +27,8 @@ @foreach($scored_entries as $entry) - - {{ $loop->iteration }} + + {{ $entry->seatingRank }} {{ $entry->id }} {{ $entry->draw_number }} @@ -165,8 +165,8 @@ Cannot seat the audition while entries are unscored. @endif -
- @if($doublerData->contains('accepted_entry', null)) + + @if($auditionHasUnresolvedDoublers) Cannot seat the audition while there are unresolved doublers. From 98378c6182aecf7cec13bcc7541a98c51ed82683 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Thu, 26 Jun 2025 03:44:34 -0500 Subject: [PATCH 50/67] Seating Publication Working --- .../Tabulation/SeatAuditionFormController.php | 108 +++++++++++++++++- .../tabulation/auditionSeating.blade.php | 104 ++++++++++++++--- routes/tabulation.php | 7 +- 3 files changed, 200 insertions(+), 19 deletions(-) diff --git a/app/Http/Controllers/Tabulation/SeatAuditionFormController.php b/app/Http/Controllers/Tabulation/SeatAuditionFormController.php index f0bb00c..8ed4d1a 100644 --- a/app/Http/Controllers/Tabulation/SeatAuditionFormController.php +++ b/app/Http/Controllers/Tabulation/SeatAuditionFormController.php @@ -8,16 +8,33 @@ use App\Exceptions\AuditionAdminException; use App\Http\Controllers\Controller; use App\Models\Audition; use App\Models\Doubler; +use App\Models\Ensemble; use App\Models\Entry; +use App\Models\Seat; +use Debugbar; use Illuminate\Http\Request; +use Illuminate\Support\Arr; use Illuminate\Support\Facades\Cache; -use function PHPUnit\Framework\isNull; +use function redirect; class SeatAuditionFormController extends Controller { public function showForm(Request $request, Audition $audition) { + $seatingProposal = (session('proposedSeatingArray-'.$audition->id)); + if ($audition->hasFlag('seats_published')) { + $publishedSeats = Seat::where('audition_id', $audition->id) + ->join('ensembles', 'seats.ensemble_id', '=', 'ensembles.id') + ->orderBy('ensembles.rank') + ->orderBy('seats.seat') + ->select('seats.*') + ->with(['ensemble', 'entry.student.school']) + ->get(); + } else { + $publishedSeats = false; + } + $ranker = app(RankAuditionEntries::class); // Get scored entries in order $scored_entries = $ranker($audition, 'seating'); @@ -63,7 +80,7 @@ class SeatAuditionFormController extends Controller $auditionHasUnresolvedDoublers = false; foreach ($doublerData as $doubler) { - if (! isNull($doubler->accepted_entry)) { + if (! is_null($doubler->accepted_entry)) { continue; } foreach ($doubler->entries() as $entry) { @@ -75,6 +92,7 @@ class SeatAuditionFormController extends Controller } $canSeat = ! $auditionHasUnresolvedDoublers && $unscored_entries->count() === 0; + Debugbar::info($seatingProposal); return view('tabulation.auditionSeating', compact('audition', @@ -85,6 +103,8 @@ class SeatAuditionFormController extends Controller 'doublerData', 'auditionHasUnresolvedDoublers', 'canSeat', + 'seatingProposal', + 'publishedSeats', ) ); } @@ -130,6 +150,90 @@ class SeatAuditionFormController extends Controller return redirect()->route('seating.audition', [$audition])->with('success', $msg); } + public function draftSeats(Audition $audition, Request $request) + { + $ranker = app(RankAuditionEntries::class); + $validated = $request->validate([ + 'ensemble' => ['required', 'array'], + 'ensemble.*' => ['required', 'integer', 'min:0'], + ]); + $proposedSeatingArray = []; + $rankedEntries = $ranker($audition, 'seating'); + $rankedEntries = $rankedEntries->reject(function ($entry) { + return $entry->hasFlag('declined'); + }); + + $rankedEntries->load(['student.school']); + $rankedEnembles = Ensemble::orderBy('rank')->where('event_id', $audition->event_id)->get(); + $ensembleRankOn = 1; + foreach ($rankedEnembles as $ensemble) { + if (! Arr::has($validated['ensemble'], $ensemble->id)) { + continue; + } + $proposedSeatingArray[$ensembleRankOn]['ensemble_id'] = $ensemble->id; + $proposedSeatingArray[$ensembleRankOn]['ensemble_name'] = $ensemble->name; + $proposedSeatingArray[$ensembleRankOn]['accept_count'] = $validated['ensemble'][$ensemble->id]; + for ($n = 1; $n <= $validated['ensemble'][$ensemble->id]; $n++) { + // Escape the loop if we're out of entries + if ($rankedEntries->isEmpty()) { + break; + } + + $thisEntry = $rankedEntries->shift(); + $proposedSeatingArray[$ensembleRankOn]['seats'][$n]['seat'] = $n; + $proposedSeatingArray[$ensembleRankOn]['seats'][$n]['entry_id'] = $thisEntry->id; + $proposedSeatingArray[$ensembleRankOn]['seats'][$n]['entry_name'] = $thisEntry->student->full_name(); + $proposedSeatingArray[$ensembleRankOn]['seats'][$n]['entry_school'] = $thisEntry->student->school->name; + } + + $ensembleRankOn++; + } + $sessionKeyName = 'proposedSeatingArray-'.$audition->id; + $request->session()->put($sessionKeyName, $proposedSeatingArray, 10); + + return redirect()->route('seating.audition', ['audition' => $audition->id]); + } + + public function clearDraft(Audition $audition) + { + session()->forget('proposedSeatingArray-'.$audition->id); + + return redirect()->route('seating.audition', ['audition' => $audition->id]); + } + + public function publishSeats(Audition $audition) + { + $publisher = app('App\Actions\Tabulation\PublishSeats'); + $seatingProposal = (session('proposedSeatingArray-'.$audition->id)); + $proposal = []; + foreach ($seatingProposal as $ensemble) { + $ensembleId = $ensemble['ensemble_id']; + if (isset($ensemble['seats'])) { + foreach ($ensemble['seats'] as $seat) { + $proposal[] = [ + 'ensemble_id' => $ensembleId, + 'audition_id' => $audition->id, + 'seat' => $seat['seat'], + 'entry_id' => $seat['entry_id'], + ]; + } + } + } + $publisher($audition, $proposal); + session()->forget('proposedSeatingArray-'.$audition->id); + + return redirect()->route('seating.audition', [$audition]); + } + + public function unpublishSeats(Audition $audition) + { + $unpublisher = app('App\Actions\Tabulation\UnpublishSeats'); + $unpublisher($audition); + session()->forget('proposedSeatingArray-'.$audition->id); + + return redirect()->route('seating.audition', [$audition]); + } + protected function pickRightPanel(Audition $audition, array $seatable) { if ($audition->hasFlag('seats_published')) { diff --git a/resources/views/tabulation/auditionSeating.blade.php b/resources/views/tabulation/auditionSeating.blade.php index a5fd3cd..1a66854 100644 --- a/resources/views/tabulation/auditionSeating.blade.php +++ b/resources/views/tabulation/auditionSeating.blade.php @@ -1,7 +1,6 @@ Audition Seating - {{ $audition->name }} -
-
+
{{-- Entry Ranking Table --}} {{-- Scored Entries --}} @@ -94,7 +93,8 @@ {{ $entry->student->school->name }} - + Record No Show @@ -157,21 +157,97 @@
- -
- {{-- TODO: Add in bulk delince doubler option --}} - @if($unscored_entries->count() > 0) - - Cannot seat the audition while entries are unscored. +
{{-- Right Column Wrapper --}} + @if($audition->hasFlag('seats_published')) + + Published Results + + @php($previousEnsemble = '') + @foreach($publishedSeats as $seat) + @if($previousEnsemble !== $seat->ensemble->name) + @php($previousEnsemble = $seat->ensemble->name) + {{ $seat->ensemble->name }} + @endif + +
+

{{ $seat->seat }}. {{ $seat->student->full_name() }}

+

{{ $seat->student->school->name }}

+
+
+ @endforeach +
- @endif - @if($auditionHasUnresolvedDoublers) - - Cannot seat the audition while there are unresolved doublers. - + + Unpublish Results + + @else + @if($canSeat) + @if($seatingProposal) + + + Seating Proposal + Results are not yet published + + @foreach($seatingProposal as $proposedEnsemble) +

{{ $proposedEnsemble['ensemble_name'] }}

+ + @if(isset($proposedEnsemble['seats'])) + @foreach($proposedEnsemble['seats'] as $seat) + {{ $seat['seat'] }} + . {{ $seat['entry_name'] }} + @endforeach + @endif + + @endforeach + + Clear Draft + + + Publish + +
+ @else + + + Seat Audition + Choose how many entries to seat in each ensemble + + + @foreach($audition->SeatingLimits()->where('maximum_accepted','>',0)->get() as $limit) + + {{$limit->ensemble->name}} + @for($n = 0; $n< $limit->maximum_accepted; $n++) + + @endfor + + + @endforeach + Draft Seats + + + @endif + @else +
+ {{-- TODO: Add in bulk decline doubler option --}} + @if($unscored_entries->count() > 0) + + Cannot seat the audition while entries are unscored. + + @endif + + @if($auditionHasUnresolvedDoublers) + + Cannot seat the audition while there are unresolved doublers. + + @endif +
+ @endif @endif
+ +
diff --git a/routes/tabulation.php b/routes/tabulation.php index ca24ea2..0accdec 100644 --- a/routes/tabulation.php +++ b/routes/tabulation.php @@ -7,7 +7,6 @@ use App\Http\Controllers\Tabulation\DoublerDecisionController; use App\Http\Controllers\Tabulation\EntryFlagController; use App\Http\Controllers\Tabulation\ScoreController; use App\Http\Controllers\Tabulation\SeatAuditionFormController; -use App\Http\Controllers\Tabulation\SeatingPublicationController; use App\Http\Controllers\Tabulation\SeatingStatusController; use App\Http\Middleware\CheckIfCanTab; use Illuminate\Support\Facades\Route; @@ -43,13 +42,15 @@ Route::middleware(['auth', 'verified', CheckIfCanTab::class])->group(function () Route::prefix('seating/')->group(function () { Route::get('/', SeatingStatusController::class)->name('seating.status'); Route::get('/{audition}', [SeatAuditionFormController::class, 'showForm'])->name('seating.audition'); + Route::post('/{audition}/draftSeats', [SeatAuditionFormController::class, 'draftSeats'])->name('seating.audition.draftSeats'); + Route::post('/{audition}/clearDraft', [SeatAuditionFormController::class, 'clearDraft'])->name('seating.audition.clearDraft'); Route::post('/{audition}/{entry}/decline', [SeatAuditionFormController::class, 'declineSeat'])->name('seating.audition.decline'); Route::post('/{audition}/{entry}/accept', [SeatAuditionFormController::class, 'acceptSeat'])->name('seating.audition.accept'); Route::post('/{audition}/{entry}/noshow', [SeatAuditionFormController::class, 'noshow'])->name('seating.audition.noshow'); Route::post('/{audition}/publish', - [SeatingPublicationController::class, 'publishSeats'])->name('seating.audition.publish'); + [SeatAuditionFormController::class, 'publishSeats'])->name('seating.audition.publishSeats'); Route::post('/{audition}/unpublish', - [SeatingPublicationController::class, 'unpublishSeats'])->name('seating.audition.unpublish'); + [SeatAuditionFormController::class, 'unpublishSeats'])->name('seating.audition.unpublishSeats'); }); // Advancement Routes From cee9f487bc46f5318238d6560ed30f054149a8d9 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Thu, 26 Jun 2025 04:08:35 -0500 Subject: [PATCH 51/67] Cleanup Debugbar Code --- app/Http/Controllers/Tabulation/SeatAuditionFormController.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/Http/Controllers/Tabulation/SeatAuditionFormController.php b/app/Http/Controllers/Tabulation/SeatAuditionFormController.php index 8ed4d1a..1a0e8a1 100644 --- a/app/Http/Controllers/Tabulation/SeatAuditionFormController.php +++ b/app/Http/Controllers/Tabulation/SeatAuditionFormController.php @@ -11,7 +11,6 @@ use App\Models\Doubler; use App\Models\Ensemble; use App\Models\Entry; use App\Models\Seat; -use Debugbar; use Illuminate\Http\Request; use Illuminate\Support\Arr; use Illuminate\Support\Facades\Cache; @@ -92,7 +91,6 @@ class SeatAuditionFormController extends Controller } $canSeat = ! $auditionHasUnresolvedDoublers && $unscored_entries->count() === 0; - Debugbar::info($seatingProposal); return view('tabulation.auditionSeating', compact('audition', From 6c52aa255c0ffd4bf0ec5abe989dfc99cbff2754 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Thu, 26 Jun 2025 08:10:09 -0500 Subject: [PATCH 52/67] Remove depricated code from EnterBonusScore action. --- app/Actions/Tabulation/EnterBonusScore.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/Actions/Tabulation/EnterBonusScore.php b/app/Actions/Tabulation/EnterBonusScore.php index b42a183..7f57d8e 100644 --- a/app/Actions/Tabulation/EnterBonusScore.php +++ b/app/Actions/Tabulation/EnterBonusScore.php @@ -6,7 +6,6 @@ namespace App\Actions\Tabulation; use App\Exceptions\ScoreEntryException; use App\Models\BonusScore; -use App\Models\CalculatedScore; use App\Models\Entry; use App\Models\User; use Illuminate\Database\Eloquent\Collection; @@ -29,7 +28,6 @@ class EnterBonusScore // Create the score for each related entry foreach ($entries as $relatedEntry) { // Also delete any cached scores - CalculatedScore::where('entry_id', $relatedEntry->id)->delete(); BonusScore::create([ 'entry_id' => $relatedEntry->id, 'user_id' => $judge->id, From ee45499e7a10b9177290c84ef6f8ee29a75c0aed Mon Sep 17 00:00:00 2001 From: Matt Young Date: Thu, 26 Jun 2025 09:15:50 -0500 Subject: [PATCH 53/67] Migration to add bonus score related columns to the total scores table. --- ...dd_bonus_columns_to_entry_total_scores.php | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 database/migrations/2025_06_26_131356_add_bonus_columns_to_entry_total_scores.php diff --git a/database/migrations/2025_06_26_131356_add_bonus_columns_to_entry_total_scores.php b/database/migrations/2025_06_26_131356_add_bonus_columns_to_entry_total_scores.php new file mode 100644 index 0000000..54c4c08 --- /dev/null +++ b/database/migrations/2025_06_26_131356_add_bonus_columns_to_entry_total_scores.php @@ -0,0 +1,32 @@ +decimal('bonus_total', 9, 6)->nullable()->after('advancement_subscore_totals'); + $table->decimal('seating_total_with_bonus', 9, 6) + ->storedAs('seating_total + COALESCE(bonus_total, 0)') + ->after('bonus_total'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('entry_total_scores', function (Blueprint $table) { + $table->dropColumn('bonus_total'); + $table->dropColumn('seating_total_with_bonus'); + }); + } +}; From 86ec4f4062b5cb10ea0fb7ff4aef24369dda866d Mon Sep 17 00:00:00 2001 From: Matt Young Date: Thu, 26 Jun 2025 09:32:06 -0500 Subject: [PATCH 54/67] Deal with bonus scores when calculating total scores. --- app/Actions/Tabulation/TotalEntryScores.php | 6 +++ app/Observers/BonusScoreObserver.php | 52 +++++++++++++++++++++ app/Observers/ScoreSheetObserver.php | 6 ++- app/Providers/AppServiceProvider.php | 3 ++ 4 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 app/Observers/BonusScoreObserver.php diff --git a/app/Actions/Tabulation/TotalEntryScores.php b/app/Actions/Tabulation/TotalEntryScores.php index 448e650..196d04f 100644 --- a/app/Actions/Tabulation/TotalEntryScores.php +++ b/app/Actions/Tabulation/TotalEntryScores.php @@ -2,6 +2,7 @@ namespace App\Actions\Tabulation; +use App\Models\BonusScore; use App\Models\Entry; use App\Models\EntryTotalScore; use App\Models\ScoreSheet; @@ -75,6 +76,11 @@ class TotalEntryScores $total_advancement_subscores[] = $runningTotal / $scoreSheets->count(); } $newTotaledScore->advancement_subscore_totals = $total_advancement_subscores; + + // pull in bonus scores + $bonusScores = BonusScore::where('entry_id', $entry->id)->sum('score'); + $newTotaledScore->bonus_score = $bonusScores; + $newTotaledScore->save(); } } diff --git a/app/Observers/BonusScoreObserver.php b/app/Observers/BonusScoreObserver.php new file mode 100644 index 0000000..79878fd --- /dev/null +++ b/app/Observers/BonusScoreObserver.php @@ -0,0 +1,52 @@ +entry, true); + } + + /** + * Handle the ScoreSheet "updated" event. + */ + public function updated(BonusScore $bonusScore): void + { + $calculator = app(TotalEntryScores::class); + $calculator($bonusScore->entry, true); + } + + /** + * Handle the ScoreSheet "deleted" event. + */ + public function deleted(BonusScore $bonusScore): void + { + $calculator = app(TotalEntryScores::class); + $calculator($bonusScore->entry, true); + } + + /** + * Handle the ScoreSheet "restored" event. + */ + public function restored(BonusScore $bonusScore): void + { + // + } + + /** + * Handle the ScoreSheet "force deleted" event. + */ + public function forceDeleted(BonusScore $bonusScore): void + { + // + } +} diff --git a/app/Observers/ScoreSheetObserver.php b/app/Observers/ScoreSheetObserver.php index 0b419af..5ade31e 100644 --- a/app/Observers/ScoreSheetObserver.php +++ b/app/Observers/ScoreSheetObserver.php @@ -21,7 +21,8 @@ class ScoreSheetObserver */ public function updated(ScoreSheet $scoreSheet): void { - // + $calculator = app(TotalEntryScores::class); + $calculator($scoreSheet->entry, true); } /** @@ -29,7 +30,8 @@ class ScoreSheetObserver */ public function deleted(ScoreSheet $scoreSheet): void { - // + $calculator = app(TotalEntryScores::class); + $calculator($scoreSheet->entry, true); } /** diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 4d3a795..09e5005 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -18,6 +18,7 @@ use App\Http\Controllers\NominationEnsembles\ScobdaNominationEnsembleController; use App\Http\Controllers\NominationEnsembles\ScobdaNominationEnsembleEntryController; use App\Http\Controllers\NominationEnsembles\ScobdaNominationSeatingController; use App\Models\Audition; +use App\Models\BonusScore; use App\Models\Entry; use App\Models\EntryFlag; use App\Models\Room; @@ -30,6 +31,7 @@ use App\Models\Student; use App\Models\SubscoreDefinition; use App\Models\User; use App\Observers\AuditionObserver; +use App\Observers\BonusScoreObserver; use App\Observers\EntryFlagObserver; use App\Observers\EntryObserver; use App\Observers\RoomObserver; @@ -83,6 +85,7 @@ class AppServiceProvider extends ServiceProvider */ public function boot(): void { + BonusScore::observe(BonusScoreObserver::class); Entry::observe(EntryObserver::class); Audition::observe(AuditionObserver::class); Room::observe(RoomObserver::class); From fd3855a775c7fc4e4af4134f9750d93de0fde7ec Mon Sep 17 00:00:00 2001 From: Matt Young Date: Thu, 26 Jun 2025 10:07:51 -0500 Subject: [PATCH 55/67] Add console command to force recalculation of scores --- .../ForceRecalculateTotalScores.php | 16 +++++++++ app/Actions/Tabulation/TotalEntryScores.php | 7 ++-- app/Console/Commands/RecalculateScores.php | 35 +++++++++++++++++++ 3 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 app/Actions/Tabulation/ForceRecalculateTotalScores.php create mode 100644 app/Console/Commands/RecalculateScores.php diff --git a/app/Actions/Tabulation/ForceRecalculateTotalScores.php b/app/Actions/Tabulation/ForceRecalculateTotalScores.php new file mode 100644 index 0000000..d5c6bde --- /dev/null +++ b/app/Actions/Tabulation/ForceRecalculateTotalScores.php @@ -0,0 +1,16 @@ +advancement_subscore_totals = $total_advancement_subscores; // pull in bonus scores - $bonusScores = BonusScore::where('entry_id', $entry->id)->sum('score'); - $newTotaledScore->bonus_score = $bonusScores; + $bonusScores = BonusScore::where('entry_id', $entry->id) + ->selectRaw('SUM(score) as total') + ->value('total'); + + $newTotaledScore->bonus_total = $bonusScores; $newTotaledScore->save(); } diff --git a/app/Console/Commands/RecalculateScores.php b/app/Console/Commands/RecalculateScores.php new file mode 100644 index 0000000..acc43ad --- /dev/null +++ b/app/Console/Commands/RecalculateScores.php @@ -0,0 +1,35 @@ +info('Starting score recalculation...'); + + $action(); + + $this->info('Score recalculation completed successfully.'); + } +} From 04cfde353ed6e0335deeaf0fad369477f685a0ba Mon Sep 17 00:00:00 2001 From: Matt Young Date: Thu, 26 Jun 2025 10:22:59 -0500 Subject: [PATCH 56/67] When appropriate, include bonus score in ranking entrie. Show if an entry has bonus scores when appropriate. --- app/Actions/Tabulation/RankAuditionEntries.php | 8 +++++++- .../views/tabulation/auditionSeating.blade.php | 15 ++++++++++++--- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/app/Actions/Tabulation/RankAuditionEntries.php b/app/Actions/Tabulation/RankAuditionEntries.php index 77fd90d..1f64d56 100644 --- a/app/Actions/Tabulation/RankAuditionEntries.php +++ b/app/Actions/Tabulation/RankAuditionEntries.php @@ -42,13 +42,19 @@ class RankAuditionEntries private function get_seating_ranks(Audition $audition): Collection { + if ($audition->bonusScore()->count() > 0) { + $totalColumn = 'seating_total_with_bonus'; + } else { + $totalColumn = 'seating_total'; + } + $sortedEntries = $audition->entries() ->whereHas('totalScore') ->with('totalScore') ->with('student.school') ->with('audition') ->join('entry_total_scores', 'entries.id', '=', 'entry_total_scores.entry_id') - ->orderBy('entry_total_scores.seating_total', 'desc') + ->orderBy('entry_total_scores.'.$totalColumn, 'desc') ->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.seating_subscore_totals, "$[0]"), -999999) DESC') ->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.seating_subscore_totals, "$[1]"), -999999) DESC') ->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.seating_subscore_totals, "$[2]"), -999999) DESC') diff --git a/resources/views/tabulation/auditionSeating.blade.php b/resources/views/tabulation/auditionSeating.blade.php index 1a66854..6e1f3e8 100644 --- a/resources/views/tabulation/auditionSeating.blade.php +++ b/resources/views/tabulation/auditionSeating.blade.php @@ -17,8 +17,7 @@ @if($audition->bonusScore()->count() > 0)
- - Has Bonus + No Bonus Score
@endif @@ -65,7 +64,17 @@ - {{ $entry->totalScore->seating_total }} + + @if($audition->bonusScore()->count() > 0) + @if($entry->totalScore->bonus_total) + {{ $entry->totalScore->seating_total_with_bonus }} + @else + {{ $entry->totalScore->seating_total_with_bonus }} + @endif + @else + {{ $entry->totalScore->seating_total }} + @endif + @endforeach From abc86ba726f6413a727a2ec2ddff178e23cffd67 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Thu, 26 Jun 2025 10:31:43 -0500 Subject: [PATCH 57/67] Remove depricated code from bonusscore model --- app/Models/BonusScore.php | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/app/Models/BonusScore.php b/app/Models/BonusScore.php index ece28cb..0423629 100644 --- a/app/Models/BonusScore.php +++ b/app/Models/BonusScore.php @@ -4,28 +4,11 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; -use Illuminate\Support\Facades\Cache; class BonusScore extends Model { protected $guarded = []; - protected static function boot() - { - parent::boot(); - static::created(function ($bonusScore) { - $bonusScore->deleteRelatedCalculatedScores(); - }); - - static::updated(function ($bonusScore) { - $bonusScore->deleteRelatedCalculatedScores(); - }); - - static::deleted(function ($bonusScore) { - $bonusScore->deleteRelatedCalculatedScores(); - }); - } - public function entry(): BelongsTo { return $this->belongsTo(Entry::class); @@ -40,16 +23,4 @@ class BonusScore extends Model { return $this->belongsTo(Entry::class, 'originally_scored_entry'); } - - public function deleteRelatedCalculatedScores(): void - { - $entry = $this->entry; - if ($entry) { - $entry->calculatedScores()->delete(); - Cache::forget('entryScore-'.$entry->id.'-seating'); - Cache::forget('entryScore-'.$entry->id.'-advancement'); - Cache::forget('audition'.$entry->audition_id.'seating'); - Cache::forget('audition'.$entry->audition_id.'advancement'); - } - } } From 0bc80002bb58f7263dc3b9dfd538573fb2fbe418 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Thu, 26 Jun 2025 10:35:01 -0500 Subject: [PATCH 58/67] rename sync-doublers console command --- app/Console/Commands/SyncDoublers.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Console/Commands/SyncDoublers.php b/app/Console/Commands/SyncDoublers.php index d50235f..a79a93c 100644 --- a/app/Console/Commands/SyncDoublers.php +++ b/app/Console/Commands/SyncDoublers.php @@ -13,7 +13,7 @@ class SyncDoublers extends Command * * @var string */ - protected $signature = 'doublers:sync {event? : Optional event ID}'; + protected $signature = 'audition:sync-doublers {event? : Optional event ID}'; /** * The console command description. From a3e8785767fc98b7ad823d0721687725083cbb3f Mon Sep 17 00:00:00 2001 From: Matt Young Date: Thu, 26 Jun 2025 11:00:13 -0500 Subject: [PATCH 59/67] add ability to fictionalize data --- app/Console/Commands/fictionalize.php | 52 +++++++++++++++++++ ...dit_log_entries_message_column_to_text.php | 28 ++++++++++ 2 files changed, 80 insertions(+) create mode 100644 app/Console/Commands/fictionalize.php create mode 100644 database/migrations/2025_06_26_155129_change_audit_log_entries_message_column_to_text.php diff --git a/app/Console/Commands/fictionalize.php b/app/Console/Commands/fictionalize.php new file mode 100644 index 0000000..b860470 --- /dev/null +++ b/app/Console/Commands/fictionalize.php @@ -0,0 +1,52 @@ +first_name = $faker->firstName(); + $student->last_name = $faker->lastName(); + $student->save(); + } + + foreach (School::all() as $school) { + $school->name = $faker->city().' High School'; + $school->save(); + } + + foreach (User::where('email', '!=', 'matt@mattyoung.us')->get() as $user) { + $user->email = $faker->email(); + $user->first_name = $faker->firstName(); + $user->last_name = $faker->lastName(); + $user->cell_phone = $faker->phoneNumber(); + $user->save(); + } + } +} diff --git a/database/migrations/2025_06_26_155129_change_audit_log_entries_message_column_to_text.php b/database/migrations/2025_06_26_155129_change_audit_log_entries_message_column_to_text.php new file mode 100644 index 0000000..fde2bcd --- /dev/null +++ b/database/migrations/2025_06_26_155129_change_audit_log_entries_message_column_to_text.php @@ -0,0 +1,28 @@ +text('message')->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('audit_log_entries', function (Blueprint $table) { + $table->string('message')->change(); + }); + } +}; From 7670e91f43008653d21ef8f065d649a013b97cd1 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Thu, 26 Jun 2025 18:32:16 -0500 Subject: [PATCH 60/67] Allow for bluk declining seats --- .../Tabulation/SeatAuditionFormController.php | 72 +++++++++++++++---- app/Providers/AppServiceProvider.php | 2 +- .../tabulation/auditionSeating.blade.php | 9 ++- routes/tabulation.php | 1 + 4 files changed, 67 insertions(+), 17 deletions(-) diff --git a/app/Http/Controllers/Tabulation/SeatAuditionFormController.php b/app/Http/Controllers/Tabulation/SeatAuditionFormController.php index 1a0e8a1..8eb9f0b 100644 --- a/app/Http/Controllers/Tabulation/SeatAuditionFormController.php +++ b/app/Http/Controllers/Tabulation/SeatAuditionFormController.php @@ -11,6 +11,7 @@ use App\Models\Doubler; use App\Models\Ensemble; use App\Models\Entry; use App\Models\Seat; +use Debugbar; use Illuminate\Http\Request; use Illuminate\Support\Arr; use Illuminate\Support\Facades\Cache; @@ -116,8 +117,44 @@ class SeatAuditionFormController extends Controller $entry->student->full_name().' has declined '.$audition->name); } - public function acceptSeat(Audition $audition, Entry $entry) + public function massDecline(Audition $audition) { + $validData = request()->validate([ + 'decline-below' => ['required', 'integer', 'min:0'], + ]); + $ranker = app(RankAuditionEntries::class); + // Get scored entries in order + $scored_entries = $ranker($audition, 'seating'); + $scored_entries->load(['student.doublers', 'student.school']); + foreach ($scored_entries as $entry) { + Debugbar::info('Starting entry '.$entry->student->full_name()); + if ($entry->hasFlag('declined')) { + Debugbar::info('Skipping '.$entry->student->full_name().' because they have already been declined'); + + continue; + } + if (! $entry->student->isDoublerInEvent($audition->event_id)) { + Debugbar::info('Skipping '.$entry->student->full_name().' because they are not a doubler'); + + continue; + } + if ($entry->student->doublers->where('event_id', $audition->event_id)->first()->accepted_entry) { + Debugbar::info('Skipping '.$entry->student->full_name().' because they have already accepted a seat'); + + continue; + } + $entry->addFlag('declined'); + } + Cache::forget('rank_seating_'.$entry->audition_id); + + return redirect()->route('seating.audition', ['audition' => $audition->id]); + + } + + public function acceptSeat( + Audition $audition, + Entry $entry + ) { $doublerData = Doubler::findDoubler($entry->student_id, $audition->event_id); foreach ($doublerData->entries() as $doublerEntry) { if (! $doublerEntry->totalScore && ! $doublerEntry->hasFlag('declined') && ! $doublerEntry->hasFlag('no_show') && ! $doublerEntry->hasFlag('failed_prelim')) { @@ -136,8 +173,10 @@ class SeatAuditionFormController extends Controller $entry->student->full_name().' has accepted '.$audition->name); } - public function noshow(Audition $audition, Entry $entry) - { + public function noshow( + Audition $audition, + Entry $entry + ) { $recorder = app('App\Actions\Tabulation\EnterNoShow'); try { $msg = $recorder($entry); @@ -148,8 +187,10 @@ class SeatAuditionFormController extends Controller return redirect()->route('seating.audition', [$audition])->with('success', $msg); } - public function draftSeats(Audition $audition, Request $request) - { + public function draftSeats( + Audition $audition, + Request $request + ) { $ranker = app(RankAuditionEntries::class); $validated = $request->validate([ 'ensemble' => ['required', 'array'], @@ -192,15 +233,17 @@ class SeatAuditionFormController extends Controller return redirect()->route('seating.audition', ['audition' => $audition->id]); } - public function clearDraft(Audition $audition) - { + public function clearDraft( + Audition $audition + ) { session()->forget('proposedSeatingArray-'.$audition->id); return redirect()->route('seating.audition', ['audition' => $audition->id]); } - public function publishSeats(Audition $audition) - { + public function publishSeats( + Audition $audition + ) { $publisher = app('App\Actions\Tabulation\PublishSeats'); $seatingProposal = (session('proposedSeatingArray-'.$audition->id)); $proposal = []; @@ -223,8 +266,9 @@ class SeatAuditionFormController extends Controller return redirect()->route('seating.audition', [$audition]); } - public function unpublishSeats(Audition $audition) - { + public function unpublishSeats( + Audition $audition + ) { $unpublisher = app('App\Actions\Tabulation\UnpublishSeats'); $unpublisher($audition); session()->forget('proposedSeatingArray-'.$audition->id); @@ -232,8 +276,10 @@ class SeatAuditionFormController extends Controller return redirect()->route('seating.audition', [$audition]); } - protected function pickRightPanel(Audition $audition, array $seatable) - { + protected function pickRightPanel( + Audition $audition, + array $seatable + ) { if ($audition->hasFlag('seats_published')) { $resultsWindow = new GetAuditionSeats; $rightPanel['view'] = 'tabulation.auditionSeating-show-published-seats'; diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 09e5005..744c46e 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -99,6 +99,6 @@ class AppServiceProvider extends ServiceProvider SeatingLimit::observe(SeatingLimitObserver::class); EntryFlag::observe(EntryFlagObserver::class); - // Model::preventLazyLoading(! app()->isProduction()); + Model::preventLazyLoading(! app()->isProduction()); } } diff --git a/resources/views/tabulation/auditionSeating.blade.php b/resources/views/tabulation/auditionSeating.blade.php index 6e1f3e8..78771ac 100644 --- a/resources/views/tabulation/auditionSeating.blade.php +++ b/resources/views/tabulation/auditionSeating.blade.php @@ -239,7 +239,6 @@ @endif @else
- {{-- TODO: Add in bulk decline doubler option --}} @if($unscored_entries->count() > 0) Cannot seat the audition while entries are unscored. @@ -247,8 +246,12 @@ @endif @if($auditionHasUnresolvedDoublers) - - Cannot seat the audition while there are unresolved doublers. + +

Cannot seat the audition while there are unresolved doublers.

+ + + Decline +
@endif
diff --git a/routes/tabulation.php b/routes/tabulation.php index 0accdec..b566875 100644 --- a/routes/tabulation.php +++ b/routes/tabulation.php @@ -45,6 +45,7 @@ Route::middleware(['auth', 'verified', CheckIfCanTab::class])->group(function () Route::post('/{audition}/draftSeats', [SeatAuditionFormController::class, 'draftSeats'])->name('seating.audition.draftSeats'); Route::post('/{audition}/clearDraft', [SeatAuditionFormController::class, 'clearDraft'])->name('seating.audition.clearDraft'); Route::post('/{audition}/{entry}/decline', [SeatAuditionFormController::class, 'declineSeat'])->name('seating.audition.decline'); + Route::post('/{audition}/mass_decline', [SeatAuditionFormController::class, 'massDecline'])->name('seating.audition.mass_decline'); Route::post('/{audition}/{entry}/accept', [SeatAuditionFormController::class, 'acceptSeat'])->name('seating.audition.accept'); Route::post('/{audition}/{entry}/noshow', [SeatAuditionFormController::class, 'noshow'])->name('seating.audition.noshow'); Route::post('/{audition}/publish', From 62dab98906aecb567d7b78d36d08b10161790508 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Fri, 27 Jun 2025 16:24:25 -0500 Subject: [PATCH 61/67] Add TODO --- app/Actions/Tabulation/TotalEntryScores.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Actions/Tabulation/TotalEntryScores.php b/app/Actions/Tabulation/TotalEntryScores.php index 8da321f..1e2a252 100644 --- a/app/Actions/Tabulation/TotalEntryScores.php +++ b/app/Actions/Tabulation/TotalEntryScores.php @@ -19,6 +19,7 @@ class TotalEntryScores public function __invoke(Entry $entry, bool $force_recalculation = false): void { + // TODO Verify accuracy of calculations, particularly for olympic scoring if ($force_recalculation) { EntryTotalScore::where('entry_id', $entry->id)->delete(); } From 57780846e383ed32a9a4b5177c178c511ae2d703 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Fri, 27 Jun 2025 16:28:56 -0500 Subject: [PATCH 62/67] Fix lazy loading issue when an audition is seated. --- app/Http/Controllers/Tabulation/SeatAuditionFormController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Http/Controllers/Tabulation/SeatAuditionFormController.php b/app/Http/Controllers/Tabulation/SeatAuditionFormController.php index 8eb9f0b..5ae417f 100644 --- a/app/Http/Controllers/Tabulation/SeatAuditionFormController.php +++ b/app/Http/Controllers/Tabulation/SeatAuditionFormController.php @@ -29,7 +29,7 @@ class SeatAuditionFormController extends Controller ->orderBy('ensembles.rank') ->orderBy('seats.seat') ->select('seats.*') - ->with(['ensemble', 'entry.student.school']) + ->with(['ensemble', 'student.school']) ->get(); } else { $publishedSeats = false; From e14b678c7437d1ef1c172e01cf15fd976d3a92a3 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Sat, 28 Jun 2025 09:04:04 -0500 Subject: [PATCH 63/67] Remove depricated code. --- .../Tabulation/SeatAuditionFormController.php | 25 ------------------- 1 file changed, 25 deletions(-) diff --git a/app/Http/Controllers/Tabulation/SeatAuditionFormController.php b/app/Http/Controllers/Tabulation/SeatAuditionFormController.php index 5ae417f..7f3ac23 100644 --- a/app/Http/Controllers/Tabulation/SeatAuditionFormController.php +++ b/app/Http/Controllers/Tabulation/SeatAuditionFormController.php @@ -2,7 +2,6 @@ namespace App\Http\Controllers\Tabulation; -use App\Actions\Tabulation\GetAuditionSeats; use App\Actions\Tabulation\RankAuditionEntries; use App\Exceptions\AuditionAdminException; use App\Http\Controllers\Controller; @@ -275,28 +274,4 @@ class SeatAuditionFormController extends Controller return redirect()->route('seating.audition', [$audition]); } - - protected function pickRightPanel( - Audition $audition, - array $seatable - ) { - if ($audition->hasFlag('seats_published')) { - $resultsWindow = new GetAuditionSeats; - $rightPanel['view'] = 'tabulation.auditionSeating-show-published-seats'; - $rightPanel['data'] = $resultsWindow($audition); - - return $rightPanel; - } - if ($seatable['allScored'] == false || $seatable['doublersResolved'] == false) { - $rightPanel['view'] = 'tabulation.auditionSeating-unable-to-seat-card'; - $rightPanel['data'] = $seatable; - - return $rightPanel; - } - - $rightPanel['view'] = 'tabulation.auditionSeating-right-complete-not-published'; - $rightPanel['data'] = $this->auditionService->getSeatingLimits($audition); - - return $rightPanel; - } } From 6f207edb0a89e2b69cef6dbb5974b258336ea119 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Sat, 28 Jun 2025 12:18:00 -0500 Subject: [PATCH 64/67] Switch settings to be stored in a static property instead of cache. --- app/Settings.php | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/app/Settings.php b/app/Settings.php index d26e314..ac4f15e 100644 --- a/app/Settings.php +++ b/app/Settings.php @@ -3,10 +3,11 @@ namespace App; use App\Models\SiteSetting; -use Illuminate\Support\Facades\Cache; class Settings { + public static $settings = null; + protected static $cacheKey = 'site_settings'; public static function __callStatic($key, $arguments) @@ -14,19 +15,23 @@ class Settings return self::get($key); } - // Load settings from the database and cache them + // Load settings from the database public static function loadSettings() { - $settings = SiteSetting::all()->pluck('setting_value', 'setting_key')->toArray(); - Cache::put(self::$cacheKey, $settings, 3600); // Cache for 1 hour + if (self::$settings === null) { + self::$settings = SiteSetting::all()->pluck('setting_value', 'setting_key')->toArray(); + } + } // Get a setting value by key public static function get($key, $default = null) { - $settings = Cache::get(self::$cacheKey, []); + if (self::$settings === null) { + self::loadSettings(); + } - return $settings[$key] ?? $default; + return self::$settings[$key] ?? $default; } // Set a setting value by key @@ -35,15 +40,17 @@ class Settings // Update the database SiteSetting::updateOrCreate(['setting_key' => $key], ['setting_value' => $value]); - // Update the cache - $settings = Cache::get(self::$cacheKey, []); - $settings[$key] = $value; - Cache::put(self::$cacheKey, $settings, 3600); // Cache for 1 hour + // Update the static property + if (self::$settings === null) { + self::loadSettings(); + } + self::$settings[$key] = $value; + } - // Clear the cache - public static function clearCache() + // Clear the settings + public static function clearSettings() { - Cache::forget(self::$cacheKey); + self::$settings = null; } } From c011d9161519cddc7a3ec64feb59bd0357d8a1bf Mon Sep 17 00:00:00 2001 From: Matt Young Date: Sun, 29 Jun 2025 23:51:50 -0500 Subject: [PATCH 65/67] Correct isssue in RankAuditionEntries action for advancmenet. --- app/Actions/Tabulation/RankAuditionEntries.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/Actions/Tabulation/RankAuditionEntries.php b/app/Actions/Tabulation/RankAuditionEntries.php index 1f64d56..0984cf4 100644 --- a/app/Actions/Tabulation/RankAuditionEntries.php +++ b/app/Actions/Tabulation/RankAuditionEntries.php @@ -38,6 +38,12 @@ class RankAuditionEntries }); } + if ($rank_type === 'advancement') { + return cache()->remember('rank_advancement_'.$audition->id, $cache_duration, function () use ($audition) { + return $this->get_advancement_ranks($audition); + }); + } + } private function get_seating_ranks(Audition $audition): Collection From 24e1c3d95ea4f7752557748d3e7f1b7da16e80be Mon Sep 17 00:00:00 2001 From: Matt Young Date: Sun, 29 Jun 2025 23:57:10 -0500 Subject: [PATCH 66/67] Correctly show advancement screen. --- .../Tabulation/AdvancementController.php | 49 ++++++++++++------- .../Tabulation/SeatingStatusController.php | 1 + .../advancement/results-table.blade.php | 11 +---- 3 files changed, 34 insertions(+), 27 deletions(-) diff --git a/app/Http/Controllers/Tabulation/AdvancementController.php b/app/Http/Controllers/Tabulation/AdvancementController.php index 5b18550..de0498f 100644 --- a/app/Http/Controllers/Tabulation/AdvancementController.php +++ b/app/Http/Controllers/Tabulation/AdvancementController.php @@ -2,6 +2,7 @@ namespace App\Http\Controllers\Tabulation; +use App\Actions\Tabulation\CalculateAuditionScores; use App\Actions\Tabulation\RankAuditionEntries; use App\Http\Controllers\Controller; use App\Models\Audition; @@ -11,41 +12,52 @@ use Illuminate\Support\Facades\Cache; class AdvancementController extends Controller { - protected RankAuditionEntries $ranker; - - public function __construct(RankAuditionEntries $ranker) - { - $this->ranker = $ranker; - } - public function status() { + // Total auditions scores if we haven't done it lately + if (! Cache::has('advancement_status_audition_totaler_throttle')) { + $lock = Cache::lock('advancement_status_audition_totaler_lock'); + + if ($lock->get()) { + try { + $totaler = app(CalculateAuditionScores::class); + foreach (Audition::forAdvancement()->with('judges')->get() as $audition) { + $totaler($audition); + } + + // set throttle + Cache::put('advancement_status_audition_totaler_throttle', true, 15); + } finally { + $lock->release(); + } + } + } + $auditions = Audition::forAdvancement() ->with('flags') ->withCount([ 'entries' => function ($query) { - $query->where('for_advancement', 1); + $query->where('for_advancement', true); }, ]) ->withCount([ 'unscoredEntries' => function ($query) { - $query->where('for_advancement', 1); + $query->where('for_advancement', true); }, ]) + ->orderBy('score_order') ->get(); + $auditionData = []; - $auditions->each(function ($audition) use (&$auditionData) { - $scoredPercent = ($audition->entries_count > 0) ? - round((($audition->entries_count - $audition->unscored_entries_count) / $audition->entries_count) * 100) - : 100; + $auditions->each(function (Audition $audition) use (&$auditionData) { $auditionData[] = [ 'id' => $audition->id, 'name' => $audition->name, 'entries_count' => $audition->entries_count, 'unscored_entries_count' => $audition->unscored_entries_count, 'scored_entries_count' => $audition->entries_count - $audition->unscored_entries_count, - 'scored_percentage' => $scoredPercent, - 'scoring_complete' => $audition->unscored_entries_count == 0, + 'scored_percentage' => $audition->entries_count > 0 ? ((($audition->entries_count - $audition->unscored_entries_count) / $audition->entries_count) * 100) : 0, + 'scoring_complete' => $audition->unscored_entries_count === 0, 'published' => $audition->hasFlag('advancement_published'), ]; }); @@ -55,11 +67,12 @@ class AdvancementController extends Controller public function ranking(Request $request, Audition $audition) { - $entries = $this->ranker->rank('advancement', $audition); - $entries->load('advancementVotes'); + $ranker = app(RankAuditionEntries::class); + $entries = $ranker($audition, 'advancement'); + $entries->load(['advancementVotes', 'totalScore', 'student.school']); $scoringComplete = $entries->every(function ($entry) { - return $entry->score_totals[0] >= 0 || $entry->hasFlag('no_show'); + return $entry->totalScore || $entry->hasFlag('no_show'); }); return view('tabulation.advancement.ranking', compact('audition', 'entries', 'scoringComplete')); diff --git a/app/Http/Controllers/Tabulation/SeatingStatusController.php b/app/Http/Controllers/Tabulation/SeatingStatusController.php index fca1b6a..e31721f 100644 --- a/app/Http/Controllers/Tabulation/SeatingStatusController.php +++ b/app/Http/Controllers/Tabulation/SeatingStatusController.php @@ -15,6 +15,7 @@ class SeatingStatusController extends Controller */ public function __invoke(Request $request) { + // Total auditions scores if we haven't done it lately if (! Cache::has('seating_status_audition_totaler_throttle')) { $lock = Cache::lock('seating_status_audition_totaler_lock'); diff --git a/resources/views/tabulation/advancement/results-table.blade.php b/resources/views/tabulation/advancement/results-table.blade.php index 0cba0b4..9d3f81e 100644 --- a/resources/views/tabulation/advancement/results-table.blade.php +++ b/resources/views/tabulation/advancement/results-table.blade.php @@ -16,22 +16,15 @@ @foreach($entries as $entry) - @php - if ($entry->score_totals[0] < 0) { - $score = $entry->score_message; - } else { - $score = number_format($entry->score_totals[0] ?? 0,4); - } - @endphp - {{ $entry->rank }} + {{ $entry->rank('advancement') }} {{ $entry->id }} {{ $entry->draw_number }} {{ $entry->student->full_name() }} {{ $entry->student->school->name }} - {{ $score }} + {{ $entry->totalScore->advancement_total }} @foreach($entry->advancementVotes as $vote) From d0bd3f509258b8c7f1fb9aae749479f78e556832 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Mon, 30 Jun 2025 00:38:42 -0500 Subject: [PATCH 67/67] Advancement working. --- app/Http/Controllers/ResultsPage.php | 2 ++ .../Tabulation/AdvancementController.php | 27 ++++++++++++------- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/app/Http/Controllers/ResultsPage.php b/app/Http/Controllers/ResultsPage.php index ba2ddf0..32fcbd3 100644 --- a/app/Http/Controllers/ResultsPage.php +++ b/app/Http/Controllers/ResultsPage.php @@ -8,6 +8,7 @@ use App\Models\Ensemble; use App\Models\Entry; use App\Models\Seat; use App\Services\AuditionService; +use Illuminate\Database\Eloquent\Model; use Illuminate\Http\Request; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\View; @@ -28,6 +29,7 @@ class ResultsPage extends Controller */ public function __invoke(Request $request) { + Model::preventLazyLoading(false); $cacheKey = 'publicResultsPage'; if (Cache::has($cacheKey)) { diff --git a/app/Http/Controllers/Tabulation/AdvancementController.php b/app/Http/Controllers/Tabulation/AdvancementController.php index de0498f..10bbb7c 100644 --- a/app/Http/Controllers/Tabulation/AdvancementController.php +++ b/app/Http/Controllers/Tabulation/AdvancementController.php @@ -6,7 +6,7 @@ use App\Actions\Tabulation\CalculateAuditionScores; use App\Actions\Tabulation\RankAuditionEntries; use App\Http\Controllers\Controller; use App\Models\Audition; -use App\Models\Entry; +use App\Models\EntryFlag; use Illuminate\Http\Request; use Illuminate\Support\Facades\Cache; @@ -81,16 +81,24 @@ class AdvancementController extends Controller public function setAuditionPassers(Request $request, Audition $audition) { $passingEntries = $request->input('pass'); + $audition->addFlag('advancement_published'); if (! is_null($passingEntries)) { - $passingEntries = array_keys($passingEntries); - $entries = Entry::whereIn('id', $passingEntries)->get(); - foreach ($entries as $entry) { - $entry->addFlag('will_advance'); - } + $passEntries = collect(array_keys($passingEntries)); + EntryFlag::insert( + $passEntries + ->map(fn ($entryId) => [ + 'entry_id' => $entryId, + 'flag_name' => 'will_advance', + 'created_at' => now(), + 'updated_at' => now(), + ])->toArray() + ); + } Cache::forget('audition'.$audition->id.'advancement'); Cache::forget('publicResultsPage'); + Cache::forget('rank_advancement_'.$audition->id); return redirect()->route('advancement.ranking', ['audition' => $audition->id])->with('success', 'Passers have been set successfully'); @@ -99,9 +107,10 @@ class AdvancementController extends Controller public function clearAuditionPassers(Request $request, Audition $audition) { $audition->removeFlag('advancement_published'); - foreach ($audition->entries as $entry) { - $entry->removeFlag('will_advance'); - } + $audition->entries + ->filter(fn ($entry) => $entry->hasFlag('will_advance')) + ->each(fn ($entry) => $entry->removeFlag('will_advance')); + Cache::forget('audition'.$audition->id.'advancement'); Cache::forget('publicResultsPage');