Advancement tabulation #3

Merged
okorpheus merged 4 commits from advancement-tabulation into master 2024-06-26 22:20:11 +00:00
10 changed files with 174 additions and 15 deletions
Showing only changes of commit 8125ef6a32 - Show all commits

View File

@ -0,0 +1,35 @@
<?php
namespace App\Http\Controllers\Tabulation;
use App\Http\Controllers\Controller;
use App\Models\Audition;
use App\Services\TabulationService;
use Illuminate\Http\Request;
class AdvancementController extends Controller
{
protected TabulationService $tabulationService;
public function __construct(TabulationService $tabulationService)
{
$this->tabulationService = $tabulationService;
}
public function status()
{
$auditions = $this->tabulationService->getAuditionsWithStatus('advancement');
return view('tabulation.advancement.status', compact('auditions'));
}
public function ranking(Request $request, Audition $audition)
{
$entries = $this->tabulationService->auditionEntries($audition->id);
$entries = $entries->filter(function ($entry) {
return $entry->for_advancement;
});
return view('tabulation.advancement.ranking', compact('audition', 'entries'));
}
}

View File

@ -4,6 +4,7 @@ namespace App\Services;
use App\Models\Entry; use App\Models\Entry;
use App\Models\Student; use App\Models\Student;
use Illuminate\Contracts\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
class DoublerService class DoublerService
@ -32,13 +33,19 @@ class DoublerService
public function getDoublers(): \Illuminate\Database\Eloquent\Collection public function getDoublers(): \Illuminate\Database\Eloquent\Collection
{ {
// TODO creating or destroying an entry should refresh the doubler cache // TODO creating or destroying an entry should refresh the doubler cache
// TODO this currently counts total entries, only need to count seating_entries. Would be an edge case, but needs to be fixed. // TODO needs to split by event so that a doubler may enter jazz and concert events for example
return Cache::remember($this->doublersCacheKey, 60, function () { $doublers = Cache::remember($this->doublersCacheKey, 60, function () {
return Student::withCount('entries') return Student::withCount(['entries' => function (Builder $query) {
->with('entries') $query->where('for_seating', true);
}])
->with(['entries' => function (Builder $query) {
$query->where('for_seating', true);
}])
->havingRaw('entries_count > ?', [1]) ->havingRaw('entries_count > ?', [1])
->get(); ->get();
}); });
return $doublers;
} }
public function refreshDoublerCache() public function refreshDoublerCache()

View File

@ -24,17 +24,30 @@ class EntryCacheService
* *
* @return \Illuminate\Database\Eloquent\Collection * @return \Illuminate\Database\Eloquent\Collection
*/ */
public function getEntriesForAudition($auditionId) public function getEntriesForAudition($auditionId, $mode = 'seating')
{ {
// TODO this invokes a lot of lazy loading. Perhaps cache the data for all entries then draw from that for each audition // TODO this invokes a lot of lazy loading. Perhaps cache the data for all entries then draw from that for each audition
$cacheKey = 'audition'.$auditionId.'entries'; $cacheKey = 'audition'.$auditionId.'entries';
return Cache::remember($cacheKey, 3600, function () use ($auditionId) { $entries = Cache::remember($cacheKey, 3600, function () use ($auditionId) {
return Entry::where('audition_id', $auditionId) return Entry::where('audition_id', $auditionId)
->with('student.school') ->with('student.school')
->get() ->get()
->keyBy('id'); ->keyBy('id');
}); });
switch ($mode) {
case 'seating':
return $entries->filter(function ($entry) {
return $entry->for_seating;
});
case 'advancement':
return $entries->filter(function ($entry) {
return $entry->for_advancement;
});
default:
return $entries;
}
} }
/** /**

View File

@ -101,7 +101,7 @@ class ScoreService
* *
* @return void * @return void
*/ */
public function calculateScoresForAudition($auditionId) public function calculateScoresForAudition($auditionId, $mode= 'seating')
{ {
static $alreadyChecked = []; static $alreadyChecked = [];
// if $auditionId is in the array $alreadyChecked return // if $auditionId is in the array $alreadyChecked return
@ -111,7 +111,7 @@ class ScoreService
$alreadyChecked[] = $auditionId; $alreadyChecked[] = $auditionId;
$audition = $this->auditionCache->getAudition($auditionId); $audition = $this->auditionCache->getAudition($auditionId);
$scoringGuideId = $audition->scoring_guide_id; $scoringGuideId = $audition->scoring_guide_id;
$entries = $this->entryCache->getEntriesForAudition($auditionId); $entries = $this->entryCache->getEntriesForAudition($auditionId, $mode);
$entries->load('scoreSheets'); // TODO Cache this somehow, it's expensive and repetitive on the seating page $entries->load('scoreSheets'); // TODO Cache this somehow, it's expensive and repetitive on the seating page
foreach ($entries as $entry) { foreach ($entries as $entry) {

View File

@ -44,7 +44,7 @@ class TabulationService
* *
* @return \Illuminate\Support\Collection|mixed * @return \Illuminate\Support\Collection|mixed
*/ */
public function auditionEntries(int $auditionId) public function auditionEntries(int $auditionId, $mode = 'seating')
{ {
static $cache = []; static $cache = [];
if (isset($cache[$auditionId])) { if (isset($cache[$auditionId])) {
@ -52,7 +52,7 @@ class TabulationService
} }
$audition = $this->auditionCacheService->getAudition($auditionId); $audition = $this->auditionCacheService->getAudition($auditionId);
$entries = $this->entryCacheService->getEntriesForAudition($auditionId); $entries = $this->entryCacheService->getEntriesForAudition($auditionId, $mode);
$this->scoreService->calculateScoresForAudition($auditionId); $this->scoreService->calculateScoresForAudition($auditionId);
foreach ($entries as $entry) { foreach ($entries as $entry) {
@ -117,6 +117,7 @@ class TabulationService
case 'advancement': case 'advancement':
return $audition->advancement_entries_count - $audition->scored_entries_count; return $audition->advancement_entries_count - $audition->scored_entries_count;
} }
return $audition->entries_count - $audition->scored_entries_count; return $audition->entries_count - $audition->scored_entries_count;
} }
@ -132,9 +133,7 @@ class TabulationService
return Cache::remember('auditionsWithStatus', 30, function () use ($mode) { return Cache::remember('auditionsWithStatus', 30, function () use ($mode) {
// Retrieve auditions from the cache and load entry IDs // Retrieve auditions from the cache and load entry IDs
$auditions = $this->auditionCacheService->getAuditions(); $auditions = $this->auditionCacheService->getAuditions($mode);
// Iterate over the auditions and calculate the scored_entries_count // Iterate over the auditions and calculate the scored_entries_count
foreach ($auditions as $audition) { foreach ($auditions as $audition) {
$scored_entries_count = 0; $scored_entries_count = 0;
@ -168,7 +167,9 @@ class TabulationService
$audition->scored_entries_count = $scored_entries_count; $audition->scored_entries_count = $scored_entries_count;
} }
return $auditions; return $auditions;
}); });
} }
} }

View File

@ -21,7 +21,8 @@
<div class="absolute left-1/2 z-10 mt-5 flex w-screen max-w-min -translate-x-1/2 px-4" x-show="open" x-cloak> <div class="absolute left-1/2 z-10 mt-5 flex w-screen max-w-min -translate-x-1/2 px-4" x-show="open" x-cloak>
<div class="w-56 shrink rounded-xl bg-white p-4 text-sm font-semibold leading-6 text-gray-900 shadow-lg ring-1 ring-gray-900/5"> <div class="w-56 shrink rounded-xl bg-white p-4 text-sm font-semibold leading-6 text-gray-900 shadow-lg ring-1 ring-gray-900/5">
<a href="{{ route('scores.chooseEntry') }}" class="block p-2 hover:text-indigo-600">Enter Scores</a> <a href="{{ route('scores.chooseEntry') }}" class="block p-2 hover:text-indigo-600">Enter Scores</a>
<a href="/tabulation/status" class="block p-2 hover:text-indigo-600">Audition Status</a> <a href="{{ route('tabulation.status') }}" class="block p-2 hover:text-indigo-600">Audition Status</a>
<a href="{{ route('advancement.status') }}" class="block p-2 hover:text-indigo-600">{{ auditionSetting('advanceTo') }} Status</a>
</div> </div>
</div> </div>

View File

@ -0,0 +1,23 @@
<x-layout.app>
<x-slot:page_title>{{ auditionSetting('advanceTo') }} Advancement - {{ $audition->name }}</x-slot:page_title>
<div class="grid grid-cols-4"></div>
<div class="grid grid-cols-4">
<div class="col-span-3">
@include('tabulation.advancement.results-table')
</div>
<div class="ml-4">
{{-- @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--}}
</div>
</div>
</x-layout.app>

View File

@ -0,0 +1,22 @@
<x-card.card class="px-3">
<x-table.table>
<thead>
<tr>
<x-table.th>Rank</x-table.th>
<x-table.th>ID</x-table.th>
<x-table.th>Draw #</x-table.th>
<x-table.th>Student Name</x-table.th>
<x-table.th>Total Score</x-table.th>
<x-table.th>All Scores?</x-table.th>
</tr>
</thead>
<x-table.body>
@foreach($entries as $entry)
<tr>
<x-table.td>{{ $entry->rank }}</x-table.td>
</tr>
@endforeach
</x-table.body>
</x-table.table>
</x-card.card>

View File

@ -0,0 +1,51 @@
<x-layout.app>
<x-slot:page_title>{{ auditionSetting('advanceTo') }} Status</x-slot:page_title>
<x-card.card class="mx-auto max-w-2xl">
<x-card.heading>
{{ auditionSetting('advanceTo') }} Advancement Status
</x-card.heading>
<x-table.table>
<thead>
<tr>
<x-table.th>Audition</x-table.th>
<x-table.th>Scoring Complete</x-table.th>
<x-table.th>Advancement Published</x-table.th>
</tr>
</thead>
<x-table.body>
@foreach($auditions as $audition)
@php
$percent = 100;
if($audition->advancement_entries_count > 0) {
$percent = round(($audition->scored_entries_count / $audition->advancement_entries_count) * 100);
}
@endphp
<tr class="hover:bg-gray-50">
<x-table.td class="">
<a href="{{ route('advancement.ranking', ['audition' => $audition->id]) }}">
<div class="flex justify-between mb-1">
<span class="text-base font-medium text-indigo-700 dark:text-white">{{ $audition->name }}</span>
<span class="text-sm font-medium text-indigo-700 dark:text-white">{{ $audition->scored_entries_count }} / {{ $audition->advancement_entries_count }} Scored</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2.5 dark:bg-gray-700">
<div class="bg-indigo-600 h-2.5 rounded-full" style="width: {{ $percent }}%"></div>
</div>
</a>
</x-table.td>
<td class="px-8">
@if( $audition->scored_entries_count == $audition->advancement_entries_count)
<x-icons.checkmark color="green"/>
@endif
</td>
<td class="px-8">
@if( $audition->hasFlag('advancement_published'))
<x-icons.checkmark color="green"/>
@endif
</td>
</tr>
@endforeach
</x-table.body>
</x-table.table>
</x-card.card>
</x-layout.app>

View File

@ -16,12 +16,18 @@ Route::middleware(['auth', 'verified', CheckIfCanTab::class])->group(function ()
// Generic Tabulation Routes // Generic Tabulation Routes
Route::prefix('tabulation/')->controller(\App\Http\Controllers\Tabulation\TabulationController::class)->group(function () { Route::prefix('tabulation/')->controller(\App\Http\Controllers\Tabulation\TabulationController::class)->group(function () {
Route::get('/status', 'status'); Route::get('/status', 'status')->name('tabulation.status');
Route::match(['get', 'post'], '/auditions/{audition}', 'auditionSeating')->name('tabulation.audition.seat'); Route::match(['get', 'post'], '/auditions/{audition}', 'auditionSeating')->name('tabulation.audition.seat');
Route::post('/auditions/{audition}/publish-seats', 'publishSeats')->name('tabulation.seat.publish'); Route::post('/auditions/{audition}/publish-seats', 'publishSeats')->name('tabulation.seat.publish');
Route::post('/auditions/{audition}/unpublish-seats', 'unpublishSeats')->name('tabulation.seat.unpublish'); Route::post('/auditions/{audition}/unpublish-seats', 'unpublishSeats')->name('tabulation.seat.unpublish');
}); });
// Advancement Routes
Route::prefix('advancement/')->controller(\App\Http\Controllers\Tabulation\AdvancementController::class)->group(function () {
Route::get('/status', 'status')->name('advancement.status');
Route::get('/{audition}', 'ranking')->name('advancement.ranking');
});
// Doubler decision routes // Doubler decision routes
Route::prefix('doubler-decision')->controller(DoublerDecisionController::class)->group(function () { Route::prefix('doubler-decision')->controller(DoublerDecisionController::class)->group(function () {
Route::post('{entry}/accept', 'accept')->name('doubler.accept'); Route::post('{entry}/accept', 'accept')->name('doubler.accept');