Merge branch 'bonusScoreRewrite' into scoringRewrite

This commit is contained in:
Matt Young 2025-06-26 18:34:06 -05:00
commit f2cb96dc0d
16 changed files with 318 additions and 55 deletions

View File

@ -6,7 +6,6 @@ namespace App\Actions\Tabulation;
use App\Exceptions\ScoreEntryException; use App\Exceptions\ScoreEntryException;
use App\Models\BonusScore; use App\Models\BonusScore;
use App\Models\CalculatedScore;
use App\Models\Entry; use App\Models\Entry;
use App\Models\User; use App\Models\User;
use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Collection;
@ -29,7 +28,6 @@ class EnterBonusScore
// Create the score for each related entry // Create the score for each related entry
foreach ($entries as $relatedEntry) { foreach ($entries as $relatedEntry) {
// Also delete any cached scores // Also delete any cached scores
CalculatedScore::where('entry_id', $relatedEntry->id)->delete();
BonusScore::create([ BonusScore::create([
'entry_id' => $relatedEntry->id, 'entry_id' => $relatedEntry->id,
'user_id' => $judge->id, 'user_id' => $judge->id,

View File

@ -0,0 +1,16 @@
<?php
namespace App\Actions\Tabulation;
use App\Models\Entry;
class ForceRecalculateTotalScores
{
public function __invoke(): void
{
$calculator = app(TotalEntryScores::class);
foreach (Entry::all() as $entry) {
$calculator($entry, true);
}
}
}

View File

@ -42,13 +42,19 @@ class RankAuditionEntries
private function get_seating_ranks(Audition $audition): Collection 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() $sortedEntries = $audition->entries()
->whereHas('totalScore') ->whereHas('totalScore')
->with('totalScore') ->with('totalScore')
->with('student.school') ->with('student.school')
->with('audition') ->with('audition')
->join('entry_total_scores', 'entries.id', '=', 'entry_total_scores.entry_id') ->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, "$[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, "$[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, "$[2]"), -999999) DESC')

View File

@ -2,6 +2,7 @@
namespace App\Actions\Tabulation; namespace App\Actions\Tabulation;
use App\Models\BonusScore;
use App\Models\Entry; use App\Models\Entry;
use App\Models\EntryTotalScore; use App\Models\EntryTotalScore;
use App\Models\ScoreSheet; use App\Models\ScoreSheet;
@ -75,6 +76,14 @@ class TotalEntryScores
$total_advancement_subscores[] = $runningTotal / $scoreSheets->count(); $total_advancement_subscores[] = $runningTotal / $scoreSheets->count();
} }
$newTotaledScore->advancement_subscore_totals = $total_advancement_subscores; $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(); $newTotaledScore->save();
} }
} }

View File

@ -0,0 +1,35 @@
<?php
namespace App\Console\Commands;
use App\Actions\Tabulation\ForceRecalculateTotalScores;
use Illuminate\Console\Command;
class RecalculateScores extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'audition:recalculate-scores';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Forces the recalculation of total scores for all entries';
/**
* Execute the console command.
*/
public function handle(ForceRecalculateTotalScores $action): void
{
$this->info('Starting score recalculation...');
$action();
$this->info('Score recalculation completed successfully.');
}
}

View File

@ -13,7 +13,7 @@ class SyncDoublers extends Command
* *
* @var string * @var string
*/ */
protected $signature = 'doublers:sync {event? : Optional event ID}'; protected $signature = 'audition:sync-doublers {event? : Optional event ID}';
/** /**
* The console command description. * The console command description.

View File

@ -0,0 +1,52 @@
<?php
namespace App\Console\Commands;
use App\Models\School;
use App\Models\Student;
use App\Models\User;
use Faker\Factory;
use Illuminate\Console\Command;
class fictionalize extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'audition:fictionalize';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Command description';
/**
* Execute the console command.
*/
public function handle()
{
$faker = Factory::create();
foreach (Student::all() as $student) {
$student->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();
}
}
}

View File

@ -11,6 +11,7 @@ use App\Models\Doubler;
use App\Models\Ensemble; use App\Models\Ensemble;
use App\Models\Entry; use App\Models\Entry;
use App\Models\Seat; use App\Models\Seat;
use Debugbar;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
@ -116,8 +117,44 @@ class SeatAuditionFormController extends Controller
$entry->student->full_name().' has declined '.$audition->name); $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); $doublerData = Doubler::findDoubler($entry->student_id, $audition->event_id);
foreach ($doublerData->entries() as $doublerEntry) { foreach ($doublerData->entries() as $doublerEntry) {
if (! $doublerEntry->totalScore && ! $doublerEntry->hasFlag('declined') && ! $doublerEntry->hasFlag('no_show') && ! $doublerEntry->hasFlag('failed_prelim')) { 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); $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'); $recorder = app('App\Actions\Tabulation\EnterNoShow');
try { try {
$msg = $recorder($entry); $msg = $recorder($entry);
@ -148,8 +187,10 @@ class SeatAuditionFormController extends Controller
return redirect()->route('seating.audition', [$audition])->with('success', $msg); 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); $ranker = app(RankAuditionEntries::class);
$validated = $request->validate([ $validated = $request->validate([
'ensemble' => ['required', 'array'], 'ensemble' => ['required', 'array'],
@ -192,15 +233,17 @@ class SeatAuditionFormController extends Controller
return redirect()->route('seating.audition', ['audition' => $audition->id]); return redirect()->route('seating.audition', ['audition' => $audition->id]);
} }
public function clearDraft(Audition $audition) public function clearDraft(
{ Audition $audition
) {
session()->forget('proposedSeatingArray-'.$audition->id); session()->forget('proposedSeatingArray-'.$audition->id);
return redirect()->route('seating.audition', ['audition' => $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'); $publisher = app('App\Actions\Tabulation\PublishSeats');
$seatingProposal = (session('proposedSeatingArray-'.$audition->id)); $seatingProposal = (session('proposedSeatingArray-'.$audition->id));
$proposal = []; $proposal = [];
@ -223,8 +266,9 @@ class SeatAuditionFormController extends Controller
return redirect()->route('seating.audition', [$audition]); return redirect()->route('seating.audition', [$audition]);
} }
public function unpublishSeats(Audition $audition) public function unpublishSeats(
{ Audition $audition
) {
$unpublisher = app('App\Actions\Tabulation\UnpublishSeats'); $unpublisher = app('App\Actions\Tabulation\UnpublishSeats');
$unpublisher($audition); $unpublisher($audition);
session()->forget('proposedSeatingArray-'.$audition->id); session()->forget('proposedSeatingArray-'.$audition->id);
@ -232,8 +276,10 @@ class SeatAuditionFormController extends Controller
return redirect()->route('seating.audition', [$audition]); 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')) { if ($audition->hasFlag('seats_published')) {
$resultsWindow = new GetAuditionSeats; $resultsWindow = new GetAuditionSeats;
$rightPanel['view'] = 'tabulation.auditionSeating-show-published-seats'; $rightPanel['view'] = 'tabulation.auditionSeating-show-published-seats';

View File

@ -4,28 +4,11 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Facades\Cache;
class BonusScore extends Model class BonusScore extends Model
{ {
protected $guarded = []; 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 public function entry(): BelongsTo
{ {
return $this->belongsTo(Entry::class); return $this->belongsTo(Entry::class);
@ -40,16 +23,4 @@ class BonusScore extends Model
{ {
return $this->belongsTo(Entry::class, 'originally_scored_entry'); 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');
}
}
} }

View File

@ -0,0 +1,52 @@
<?php
namespace App\Observers;
use App\Actions\Tabulation\TotalEntryScores;
use App\Models\BonusScore;
class BonusScoreObserver
{
/**
* Handle the ScoreSheet "created" event.
*/
public function created(BonusScore $bonusScore): void
{
$calculator = app(TotalEntryScores::class);
$calculator($bonusScore->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
{
//
}
}

View File

@ -21,7 +21,8 @@ class ScoreSheetObserver
*/ */
public function updated(ScoreSheet $scoreSheet): void 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 public function deleted(ScoreSheet $scoreSheet): void
{ {
// $calculator = app(TotalEntryScores::class);
$calculator($scoreSheet->entry, true);
} }
/** /**

View File

@ -18,6 +18,7 @@ use App\Http\Controllers\NominationEnsembles\ScobdaNominationEnsembleController;
use App\Http\Controllers\NominationEnsembles\ScobdaNominationEnsembleEntryController; use App\Http\Controllers\NominationEnsembles\ScobdaNominationEnsembleEntryController;
use App\Http\Controllers\NominationEnsembles\ScobdaNominationSeatingController; use App\Http\Controllers\NominationEnsembles\ScobdaNominationSeatingController;
use App\Models\Audition; use App\Models\Audition;
use App\Models\BonusScore;
use App\Models\Entry; use App\Models\Entry;
use App\Models\EntryFlag; use App\Models\EntryFlag;
use App\Models\Room; use App\Models\Room;
@ -30,6 +31,7 @@ use App\Models\Student;
use App\Models\SubscoreDefinition; use App\Models\SubscoreDefinition;
use App\Models\User; use App\Models\User;
use App\Observers\AuditionObserver; use App\Observers\AuditionObserver;
use App\Observers\BonusScoreObserver;
use App\Observers\EntryFlagObserver; use App\Observers\EntryFlagObserver;
use App\Observers\EntryObserver; use App\Observers\EntryObserver;
use App\Observers\RoomObserver; use App\Observers\RoomObserver;
@ -83,6 +85,7 @@ class AppServiceProvider extends ServiceProvider
*/ */
public function boot(): void public function boot(): void
{ {
BonusScore::observe(BonusScoreObserver::class);
Entry::observe(EntryObserver::class); Entry::observe(EntryObserver::class);
Audition::observe(AuditionObserver::class); Audition::observe(AuditionObserver::class);
Room::observe(RoomObserver::class); Room::observe(RoomObserver::class);
@ -96,6 +99,6 @@ class AppServiceProvider extends ServiceProvider
SeatingLimit::observe(SeatingLimitObserver::class); SeatingLimit::observe(SeatingLimitObserver::class);
EntryFlag::observe(EntryFlagObserver::class); EntryFlag::observe(EntryFlagObserver::class);
// Model::preventLazyLoading(! app()->isProduction()); Model::preventLazyLoading(! app()->isProduction());
} }
} }

View File

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('entry_total_scores', function (Blueprint $table) {
$table->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');
});
}
};

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('audit_log_entries', function (Blueprint $table) {
$table->text('message')->change();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('audit_log_entries', function (Blueprint $table) {
$table->string('message')->change();
});
}
};

View File

@ -17,8 +17,7 @@
@if($audition->bonusScore()->count() > 0) @if($audition->bonusScore()->count() > 0)
<br> <br>
<div class="display: flex"> <div class="display: flex">
<x-icons.checkmark color="green"/> <span class="text-yellow-500">No Bonus Score</span>
Has Bonus
</div> </div>
@endif @endif
</x-table.th> </x-table.th>
@ -65,7 +64,17 @@
</x-table.td> </x-table.td>
<x-table.td class="align-top">{{ $entry->totalScore->seating_total }}</x-table.td> <x-table.td class="align-top">
@if($audition->bonusScore()->count() > 0)
@if($entry->totalScore->bonus_total)
<span>{{ $entry->totalScore->seating_total_with_bonus }}</span>
@else
<span class="text-yellow-500">{{ $entry->totalScore->seating_total_with_bonus }}</span>
@endif
@else
{{ $entry->totalScore->seating_total }}
@endif
</x-table.td>
</tr> </tr>
@endforeach @endforeach
</tbody> </tbody>
@ -230,7 +239,6 @@
@endif @endif
@else @else
<div class="ml-4"> <div class="ml-4">
{{-- TODO: Add in bulk decline doubler option --}}
@if($unscored_entries->count() > 0) @if($unscored_entries->count() > 0)
<x-card.card class="p-3 text-red-500 mb-3"> <x-card.card class="p-3 text-red-500 mb-3">
Cannot seat the audition while entries are unscored. Cannot seat the audition while entries are unscored.
@ -238,8 +246,12 @@
@endif @endif
@if($auditionHasUnresolvedDoublers) @if($auditionHasUnresolvedDoublers)
<x-card.card class="p-3 text-red-500"> <x-card.card class="p-3">
Cannot seat the audition while there are unresolved doublers. <p class="text-red-500">Cannot seat the audition while there are unresolved doublers.</p>
<x-form.form method="POST" action="{{ route('seating.audition.mass_decline',[$audition]) }}">
<x-form.field type="number" name="decline-below" label_text="Decline doubler ranked lower than:" />
<x-form.button>Decline</x-form.button>
</x-form.form>
</x-card.card> </x-card.card>
@endif @endif
</div> </div>

View File

@ -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}/draftSeats', [SeatAuditionFormController::class, 'draftSeats'])->name('seating.audition.draftSeats');
Route::post('/{audition}/clearDraft', [SeatAuditionFormController::class, 'clearDraft'])->name('seating.audition.clearDraft'); 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}/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}/accept', [SeatAuditionFormController::class, 'acceptSeat'])->name('seating.audition.accept');
Route::post('/{audition}/{entry}/noshow', [SeatAuditionFormController::class, 'noshow'])->name('seating.audition.noshow'); Route::post('/{audition}/{entry}/noshow', [SeatAuditionFormController::class, 'noshow'])->name('seating.audition.noshow');
Route::post('/{audition}/publish', Route::post('/{audition}/publish',