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, 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 @@ +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/app/Actions/Tabulation/TotalEntryScores.php b/app/Actions/Tabulation/TotalEntryScores.php index 448e650..8da321f 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,14 @@ 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) + ->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.'); + } +} 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. 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/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/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'); - } - } } 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..744c46e 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); @@ -96,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/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'); + }); + } +}; 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(); + }); + } +}; diff --git a/resources/views/tabulation/auditionSeating.blade.php b/resources/views/tabulation/auditionSeating.blade.php index 1a66854..78771ac 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 @@ -230,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. @@ -238,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',