Merge branch 'scoringRewrite'
* scoringRewrite: (67 commits) Advancement working. Correctly show advancement screen. Correct isssue in RankAuditionEntries action for advancmenet. Switch settings to be stored in a static property instead of cache. Remove depricated code. Fix lazy loading issue when an audition is seated. Add TODO Allow for bluk declining seats add ability to fictionalize data rename sync-doublers console command Remove depricated code from bonusscore model When appropriate, include bonus score in ranking entrie. Show if an entry has bonus scores when appropriate. Add console command to force recalculation of scores Deal with bonus scores when calculating total scores. Migration to add bonus score related columns to the total scores table. Remove depricated code from EnterBonusScore action. Cleanup Debugbar Code Seating Publication Working Everything ready for seating the audition. add ability to mark no-shows and accept/decline doublers from the seating page. ...
This commit is contained in:
commit
d0aa29fb1a
|
|
@ -1,99 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
/** @noinspection PhpUnhandledExceptionInspection */
|
|
||||||
|
|
||||||
namespace App\Actions\Tabulation;
|
|
||||||
|
|
||||||
use App\Exceptions\TabulationException;
|
|
||||||
use App\Models\Entry;
|
|
||||||
use App\Services\AuditionService;
|
|
||||||
use App\Services\EntryService;
|
|
||||||
use Illuminate\Support\Facades\Cache;
|
|
||||||
|
|
||||||
class AllJudgesCount implements CalculateEntryScore
|
|
||||||
{
|
|
||||||
protected CalculateScoreSheetTotal $calculator;
|
|
||||||
|
|
||||||
protected AuditionService $auditionService;
|
|
||||||
|
|
||||||
protected EntryService $entryService;
|
|
||||||
|
|
||||||
public function __construct(CalculateScoreSheetTotal $calculator, AuditionService $auditionService, EntryService $entryService)
|
|
||||||
{
|
|
||||||
$this->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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,161 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
/** @noinspection PhpUnhandledExceptionInspection */
|
|
||||||
|
|
||||||
namespace App\Actions\Tabulation;
|
|
||||||
|
|
||||||
use App\Exceptions\TabulationException;
|
|
||||||
use App\Models\BonusScore;
|
|
||||||
use App\Models\CalculatedScore;
|
|
||||||
use App\Models\Entry;
|
|
||||||
use App\Services\AuditionService;
|
|
||||||
use App\Services\EntryService;
|
|
||||||
use Illuminate\Support\Facades\Cache;
|
|
||||||
use Illuminate\Support\Facades\Log;
|
|
||||||
|
|
||||||
use function auditionSetting;
|
|
||||||
|
|
||||||
class AllowForOlympicScoring implements CalculateEntryScore
|
|
||||||
{
|
|
||||||
protected CalculateScoreSheetTotal $calculator;
|
|
||||||
|
|
||||||
protected AuditionService $auditionService;
|
|
||||||
|
|
||||||
protected EntryService $entryService;
|
|
||||||
|
|
||||||
public function __construct(
|
|
||||||
CalculateScoreSheetTotal $calculator,
|
|
||||||
AuditionService $auditionService,
|
|
||||||
EntryService $entryService
|
|
||||||
) {
|
|
||||||
$this->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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Actions\Tabulation;
|
||||||
|
|
||||||
|
use App\Models\Audition;
|
||||||
|
use App\Models\Entry;
|
||||||
|
use Debugbar;
|
||||||
|
|
||||||
|
class CalculateAuditionScores
|
||||||
|
{
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __invoke(Audition $audition): void
|
||||||
|
{
|
||||||
|
$totaler = app(TotalEntryScores::class);
|
||||||
|
$scores_required = $audition->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Actions\Tabulation;
|
|
||||||
|
|
||||||
use App\Models\Entry;
|
|
||||||
|
|
||||||
interface CalculateEntryScore
|
|
||||||
{
|
|
||||||
|
|
||||||
public function calculate(string $mode, Entry $entry): array;
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Actions\Tabulation;
|
|
||||||
|
|
||||||
use App\Models\Entry;
|
|
||||||
use App\Models\User;
|
|
||||||
|
|
||||||
interface CalculateScoreSheetTotal
|
|
||||||
{
|
|
||||||
public function __invoke(string $mode, Entry $entry, User $judge): array;
|
|
||||||
}
|
|
||||||
|
|
@ -1,67 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
/** @noinspection PhpUnhandledExceptionInspection */
|
|
||||||
|
|
||||||
namespace App\Actions\Tabulation;
|
|
||||||
|
|
||||||
use App\Exceptions\TabulationException;
|
|
||||||
use App\Models\Entry;
|
|
||||||
use App\Models\ScoreSheet;
|
|
||||||
use App\Models\User;
|
|
||||||
use App\Services\AuditionService;
|
|
||||||
use App\Services\EntryService;
|
|
||||||
use App\Services\UserService;
|
|
||||||
|
|
||||||
class CalculateScoreSheetTotalDivideByTotalWeights implements CalculateScoreSheetTotal
|
|
||||||
{
|
|
||||||
protected AuditionService $auditionService;
|
|
||||||
|
|
||||||
protected EntryService $entryService;
|
|
||||||
|
|
||||||
protected UserService $userService;
|
|
||||||
|
|
||||||
public function __construct(AuditionService $auditionService, EntryService $entryService, UserService $userService)
|
|
||||||
{
|
|
||||||
$this->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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,74 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
/** @noinspection PhpUnhandledExceptionInspection */
|
|
||||||
|
|
||||||
namespace App\Actions\Tabulation;
|
|
||||||
|
|
||||||
use App\Exceptions\TabulationException;
|
|
||||||
use App\Models\Entry;
|
|
||||||
use App\Models\ScoreSheet;
|
|
||||||
use App\Models\User;
|
|
||||||
use App\Services\AuditionService;
|
|
||||||
use App\Services\EntryService;
|
|
||||||
use App\Services\UserService;
|
|
||||||
|
|
||||||
class CalculateScoreSheetTotalDivideByWeightedPossible implements CalculateScoreSheetTotal
|
|
||||||
{
|
|
||||||
protected AuditionService $auditionService;
|
|
||||||
|
|
||||||
protected EntryService $entryService;
|
|
||||||
|
|
||||||
protected UserService $userService;
|
|
||||||
|
|
||||||
public function __construct(AuditionService $auditionService, EntryService $entryService, UserService $userService)
|
|
||||||
{
|
|
||||||
$this->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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Actions\Tabulation;
|
||||||
|
|
||||||
|
use App\Exceptions\AuditionAdminException;
|
||||||
|
use App\Models\BonusScore;
|
||||||
|
use App\Models\Entry;
|
||||||
|
use App\Models\EntryTotalScore;
|
||||||
|
use App\Models\ScoreSheet;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class EnterNoShow
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handles the no-show or failed-prelim flagging for a given entry.
|
||||||
|
*
|
||||||
|
* This method ensures the specified flag type is valid and validates
|
||||||
|
* that the action can be performed based on the associated audition's state.
|
||||||
|
* Deletes related score records and applies the specified flag ('no_show'
|
||||||
|
* or 'failed_prelim') to the entry, returning a success message.
|
||||||
|
*
|
||||||
|
* @param Entry $entry The entry being flagged.
|
||||||
|
* @param string $flagType The type of flag to apply ('no-show' or 'failed-prelim').
|
||||||
|
* @return string A confirmation message about the flagging operation.
|
||||||
|
*
|
||||||
|
* @throws AuditionAdminException If an invalid flag type is provided,
|
||||||
|
* or the action violates business rules.
|
||||||
|
*/
|
||||||
|
public function __invoke(Entry $entry, string $flagType = 'noshow'): string
|
||||||
|
{
|
||||||
|
if ($flagType !== 'noshow' && $flagType !== 'failprelim') {
|
||||||
|
throw new AuditionAdminException('Invalid flag type');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($entry->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 == '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 $msg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,105 +7,32 @@
|
||||||
namespace App\Actions\Tabulation;
|
namespace App\Actions\Tabulation;
|
||||||
|
|
||||||
use App\Exceptions\ScoreEntryException;
|
use App\Exceptions\ScoreEntryException;
|
||||||
use App\Models\CalculatedScore;
|
use App\Models\AuditLogEntry;
|
||||||
use App\Models\Entry;
|
use App\Models\Entry;
|
||||||
|
use App\Models\EntryTotalScore;
|
||||||
use App\Models\ScoreSheet;
|
use App\Models\ScoreSheet;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Support\Collection;
|
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
use function auth;
|
||||||
|
|
||||||
class EnterScore
|
class EnterScore
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @param User $user A user acting as the judge for this sheet
|
* @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 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 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
|
* @throws ScoreEntryException
|
||||||
*/
|
*/
|
||||||
public function __invoke(User $user, Entry $entry, array $scores, ScoreSheet|false $scoreSheet = false): ScoreSheet
|
public function __invoke(User $user, Entry $entry, array $scores, ScoreSheet|false $scoreSheet = false): ScoreSheet
|
||||||
{
|
{
|
||||||
CalculatedScore::where('entry_id', $entry->id)->delete();
|
EntryTotalScore::where('entry_id', $entry->id)->delete();
|
||||||
$scores = collect($scores);
|
$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;
|
// Basic Validity Checks
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
if (! $user->exists()) {
|
if (! $user->exists()) {
|
||||||
throw new ScoreEntryException('User does not exist');
|
throw new ScoreEntryException('User does not exist');
|
||||||
}
|
}
|
||||||
|
|
@ -118,9 +45,110 @@ class EnterScore
|
||||||
if ($entry->audition->hasFlag('advancement_published')) {
|
if ($entry->audition->hasFlag('advancement_published')) {
|
||||||
throw new ScoreEntryException('Cannot score an entry in an audition with published advancement');
|
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 = [];
|
||||||
|
$seatingTotal = 0;
|
||||||
|
$seatingMaxPossible = 0;
|
||||||
|
$advancementTotal = 0;
|
||||||
|
$advancementMaxPossible = 0;
|
||||||
|
if ($scores->count() !== $subscoresRequired->count()) {
|
||||||
throw new ScoreEntryException('Invalid number of scores');
|
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,
|
||||||
|
];
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
$scoreSheet->update([
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'entry_id' => $entry->id,
|
||||||
|
'subscores' => $subscoresStorageArray,
|
||||||
|
'seating_total' => $finalSeatingTotal,
|
||||||
|
'advancement_total' => $finalAdvancementTotal,
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
$scoreSheet = ScoreSheet::create([
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'entry_id' => $entry->id,
|
||||||
|
'subscores' => $subscoresStorageArray,
|
||||||
|
'seating_total' => $finalSeatingTotal,
|
||||||
|
'advancement_total' => $finalAdvancementTotal,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log the score entry
|
||||||
|
$log_message = 'Entered Score for entry id '.$entry->id.'.<br />';
|
||||||
|
$log_message .= 'Judge: '.$user->full_name().'<br />';
|
||||||
|
foreach ($scoreSheet->subscores as $subscore) {
|
||||||
|
$log_message .= $subscore['subscore_name'].': '.$subscore['score'].'<br />';
|
||||||
|
}
|
||||||
|
$log_message .= 'Seating Total: '.$scoreSheet->seating_total.'<br />';
|
||||||
|
$log_message .= 'Advancement Total: '.$scoreSheet->advancement_total.'<br />';
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,105 +4,109 @@
|
||||||
|
|
||||||
namespace App\Actions\Tabulation;
|
namespace App\Actions\Tabulation;
|
||||||
|
|
||||||
use App\Exceptions\TabulationException;
|
use App\Exceptions\AuditionAdminException;
|
||||||
use App\Models\Audition;
|
use App\Models\Audition;
|
||||||
use Illuminate\Database\Eloquent\Collection;
|
use App\Models\Entry;
|
||||||
use Illuminate\Support\Facades\Cache;
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
use function is_numeric;
|
|
||||||
|
|
||||||
class RankAuditionEntries
|
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
|
* Get ranked entries for the provided audition for either seating or advancement.
|
||||||
* property rank that either is their rank or a flag reflecting no-show, declined, or failed-prelim status
|
|
||||||
*
|
*
|
||||||
* @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<Entry>|void
|
||||||
|
*
|
||||||
|
* @throws AuditionAdminException
|
||||||
*/
|
*/
|
||||||
public function calculateRank(string $mode, Audition $audition): Collection
|
public function __invoke(Audition $audition, string $rank_type)
|
||||||
{
|
{
|
||||||
$this->basicValidation($mode, $audition);
|
if ($rank_type !== 'seating' && $rank_type !== 'advancement') {
|
||||||
$entries = match ($mode) {
|
throw new AuditionAdminException('Invalid rank type: '.$rank_type.' (must be seating or advancement)');
|
||||||
'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;
|
$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);
|
||||||
});
|
});
|
||||||
$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)) {
|
if ($rank_type === 'advancement') {
|
||||||
$rank++;
|
return cache()->remember('rank_advancement_'.$audition->id, $cache_duration, function () use ($audition) {
|
||||||
}
|
return $this->get_advancement_ranks($audition);
|
||||||
$rawRank++;
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return $entries;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function basicValidation($mode, Audition $audition): void
|
private function get_seating_ranks(Audition $audition): Collection
|
||||||
{
|
{
|
||||||
if ($mode !== 'seating' && $mode !== 'advancement') {
|
if ($audition->bonusScore()->count() > 0) {
|
||||||
throw new TabulationException('Mode must be seating or advancement');
|
$totalColumn = 'seating_total_with_bonus';
|
||||||
|
} else {
|
||||||
|
$totalColumn = 'seating_total';
|
||||||
}
|
}
|
||||||
if (! $audition->exists()) {
|
|
||||||
throw new TabulationException('Invalid audition provided');
|
$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.'.$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')
|
||||||
|
->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();
|
||||||
|
|
||||||
|
$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
|
||||||
|
{
|
||||||
|
return $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.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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Actions\Tabulation;
|
||||||
|
|
||||||
|
use App\Models\BonusScore;
|
||||||
|
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()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
// 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
|
||||||
|
// 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();
|
||||||
|
$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;
|
||||||
|
|
||||||
|
// pull in bonus scores
|
||||||
|
$bonusScores = BonusScore::where('entry_id', $entry->id)
|
||||||
|
->selectRaw('SUM(score) as total')
|
||||||
|
->value('total');
|
||||||
|
|
||||||
|
$newTotaledScore->bonus_total = $bonusScores;
|
||||||
|
|
||||||
|
$newTotaledScore->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Models\Doubler;
|
||||||
|
use App\Models\Event;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class SyncDoublers extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'audition:sync-doublers {event? : Optional event ID}';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Update doublers table based on current entries';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
if ($eventId = $this->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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,10 +2,6 @@
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
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\Models\School;
|
||||||
use App\Services\Invoice\InvoiceDataService;
|
use App\Services\Invoice\InvoiceDataService;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
@ -28,33 +24,8 @@ class DashboardController extends Controller
|
||||||
}
|
}
|
||||||
|
|
||||||
public function dashboard(
|
public function dashboard(
|
||||||
CalculateEntryScore $scoreCalc,
|
|
||||||
GetEntrySeatingResult $resultGenerator,
|
|
||||||
RankAuditionEntries $ranker
|
|
||||||
) {
|
) {
|
||||||
|
return view('dashboard.dashboard');
|
||||||
// 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');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ use App\Models\Ensemble;
|
||||||
use App\Models\Entry;
|
use App\Models\Entry;
|
||||||
use App\Models\Seat;
|
use App\Models\Seat;
|
||||||
use App\Services\AuditionService;
|
use App\Services\AuditionService;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Cache;
|
use Illuminate\Support\Facades\Cache;
|
||||||
use Illuminate\Support\Facades\View;
|
use Illuminate\Support\Facades\View;
|
||||||
|
|
@ -28,6 +29,7 @@ class ResultsPage extends Controller
|
||||||
*/
|
*/
|
||||||
public function __invoke(Request $request)
|
public function __invoke(Request $request)
|
||||||
{
|
{
|
||||||
|
Model::preventLazyLoading(false);
|
||||||
$cacheKey = 'publicResultsPage';
|
$cacheKey = 'publicResultsPage';
|
||||||
|
|
||||||
if (Cache::has($cacheKey)) {
|
if (Cache::has($cacheKey)) {
|
||||||
|
|
|
||||||
|
|
@ -2,50 +2,62 @@
|
||||||
|
|
||||||
namespace App\Http\Controllers\Tabulation;
|
namespace App\Http\Controllers\Tabulation;
|
||||||
|
|
||||||
|
use App\Actions\Tabulation\CalculateAuditionScores;
|
||||||
use App\Actions\Tabulation\RankAuditionEntries;
|
use App\Actions\Tabulation\RankAuditionEntries;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\Audition;
|
use App\Models\Audition;
|
||||||
use App\Models\Entry;
|
use App\Models\EntryFlag;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Cache;
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
|
||||||
class AdvancementController extends Controller
|
class AdvancementController extends Controller
|
||||||
{
|
{
|
||||||
protected RankAuditionEntries $ranker;
|
|
||||||
|
|
||||||
public function __construct(RankAuditionEntries $ranker)
|
|
||||||
{
|
|
||||||
$this->ranker = $ranker;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function status()
|
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()
|
$auditions = Audition::forAdvancement()
|
||||||
->with('flags')
|
->with('flags')
|
||||||
->withCount([
|
->withCount([
|
||||||
'entries' => function ($query) {
|
'entries' => function ($query) {
|
||||||
$query->where('for_advancement', 1);
|
$query->where('for_advancement', true);
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
->withCount([
|
->withCount([
|
||||||
'unscoredEntries' => function ($query) {
|
'unscoredEntries' => function ($query) {
|
||||||
$query->where('for_advancement', 1);
|
$query->where('for_advancement', true);
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
->orderBy('score_order')
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
$auditionData = [];
|
$auditionData = [];
|
||||||
$auditions->each(function ($audition) use (&$auditionData) {
|
$auditions->each(function (Audition $audition) use (&$auditionData) {
|
||||||
$scoredPercent = ($audition->entries_count > 0) ?
|
|
||||||
round((($audition->entries_count - $audition->unscored_entries_count) / $audition->entries_count) * 100)
|
|
||||||
: 100;
|
|
||||||
$auditionData[] = [
|
$auditionData[] = [
|
||||||
'id' => $audition->id,
|
'id' => $audition->id,
|
||||||
'name' => $audition->name,
|
'name' => $audition->name,
|
||||||
'entries_count' => $audition->entries_count,
|
'entries_count' => $audition->entries_count,
|
||||||
'unscored_entries_count' => $audition->unscored_entries_count,
|
'unscored_entries_count' => $audition->unscored_entries_count,
|
||||||
'scored_entries_count' => $audition->entries_count - $audition->unscored_entries_count,
|
'scored_entries_count' => $audition->entries_count - $audition->unscored_entries_count,
|
||||||
'scored_percentage' => $scoredPercent,
|
'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,
|
'scoring_complete' => $audition->unscored_entries_count === 0,
|
||||||
'published' => $audition->hasFlag('advancement_published'),
|
'published' => $audition->hasFlag('advancement_published'),
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
@ -55,11 +67,12 @@ class AdvancementController extends Controller
|
||||||
|
|
||||||
public function ranking(Request $request, Audition $audition)
|
public function ranking(Request $request, Audition $audition)
|
||||||
{
|
{
|
||||||
$entries = $this->ranker->rank('advancement', $audition);
|
$ranker = app(RankAuditionEntries::class);
|
||||||
$entries->load('advancementVotes');
|
$entries = $ranker($audition, 'advancement');
|
||||||
|
$entries->load(['advancementVotes', 'totalScore', 'student.school']);
|
||||||
|
|
||||||
$scoringComplete = $entries->every(function ($entry) {
|
$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'));
|
return view('tabulation.advancement.ranking', compact('audition', 'entries', 'scoringComplete'));
|
||||||
|
|
@ -68,16 +81,24 @@ class AdvancementController extends Controller
|
||||||
public function setAuditionPassers(Request $request, Audition $audition)
|
public function setAuditionPassers(Request $request, Audition $audition)
|
||||||
{
|
{
|
||||||
$passingEntries = $request->input('pass');
|
$passingEntries = $request->input('pass');
|
||||||
|
|
||||||
$audition->addFlag('advancement_published');
|
$audition->addFlag('advancement_published');
|
||||||
if (! is_null($passingEntries)) {
|
if (! is_null($passingEntries)) {
|
||||||
$passingEntries = array_keys($passingEntries);
|
$passEntries = collect(array_keys($passingEntries));
|
||||||
$entries = Entry::whereIn('id', $passingEntries)->get();
|
EntryFlag::insert(
|
||||||
foreach ($entries as $entry) {
|
$passEntries
|
||||||
$entry->addFlag('will_advance');
|
->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('audition'.$audition->id.'advancement');
|
||||||
Cache::forget('publicResultsPage');
|
Cache::forget('publicResultsPage');
|
||||||
|
Cache::forget('rank_advancement_'.$audition->id);
|
||||||
|
|
||||||
return redirect()->route('advancement.ranking', ['audition' => $audition->id])->with('success',
|
return redirect()->route('advancement.ranking', ['audition' => $audition->id])->with('success',
|
||||||
'Passers have been set successfully');
|
'Passers have been set successfully');
|
||||||
|
|
@ -86,9 +107,10 @@ class AdvancementController extends Controller
|
||||||
public function clearAuditionPassers(Request $request, Audition $audition)
|
public function clearAuditionPassers(Request $request, Audition $audition)
|
||||||
{
|
{
|
||||||
$audition->removeFlag('advancement_published');
|
$audition->removeFlag('advancement_published');
|
||||||
foreach ($audition->entries as $entry) {
|
$audition->entries
|
||||||
$entry->removeFlag('will_advance');
|
->filter(fn ($entry) => $entry->hasFlag('will_advance'))
|
||||||
}
|
->each(fn ($entry) => $entry->removeFlag('will_advance'));
|
||||||
|
|
||||||
Cache::forget('audition'.$audition->id.'advancement');
|
Cache::forget('audition'.$audition->id.'advancement');
|
||||||
Cache::forget('publicResultsPage');
|
Cache::forget('publicResultsPage');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,10 @@
|
||||||
|
|
||||||
namespace App\Http\Controllers\Tabulation;
|
namespace App\Http\Controllers\Tabulation;
|
||||||
|
|
||||||
|
use App\Exceptions\AuditionAdminException;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\BonusScore;
|
|
||||||
use App\Models\CalculatedScore;
|
|
||||||
use App\Models\Entry;
|
use App\Models\Entry;
|
||||||
use App\Models\ScoreSheet;
|
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\DB;
|
|
||||||
|
|
||||||
use function to_route;
|
use function to_route;
|
||||||
|
|
||||||
|
|
@ -69,27 +66,16 @@ class EntryFlagController extends Controller
|
||||||
'scores'));
|
'scores'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws AuditionAdminException
|
||||||
|
*/
|
||||||
public function enterNoShow(Entry $entry)
|
public function enterNoShow(Entry $entry)
|
||||||
{
|
{
|
||||||
if ($entry->audition->hasFlag('seats_published')) {
|
$recorder = app('App\Actions\Tabulation\EnterNoShow');
|
||||||
return to_route('entry-flags.noShowSelect')->with('error',
|
try {
|
||||||
'Cannot enter a no-show for an entry in an audition where seats are published');
|
$msg = $recorder($entry, request()->input('noshow-type'));
|
||||||
}
|
} catch (AuditionAdminException $e) {
|
||||||
if ($entry->audition->hasFlag('advancement_published')) {
|
return to_route('entry-flags.noShowSelect')->with('error', $e->getMessage());
|
||||||
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();
|
|
||||||
|
|
||||||
$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 {
|
|
||||||
$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);
|
return to_route('entry-flags.noShowSelect')->with('success', $msg);
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,13 @@
|
||||||
|
|
||||||
namespace App\Http\Controllers\Tabulation;
|
namespace App\Http\Controllers\Tabulation;
|
||||||
|
|
||||||
|
use App\Actions\Tabulation\EnterScore;
|
||||||
|
use App\Exceptions\ScoreEntryException;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\CalculatedScore;
|
|
||||||
use App\Models\Entry;
|
use App\Models\Entry;
|
||||||
|
use App\Models\EntryTotalScore;
|
||||||
use App\Models\ScoreSheet;
|
use App\Models\ScoreSheet;
|
||||||
|
use App\Models\User;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Session;
|
use Illuminate\Support\Facades\Session;
|
||||||
|
|
||||||
|
|
@ -22,12 +25,13 @@ class ScoreController extends Controller
|
||||||
|
|
||||||
public function destroyScore(ScoreSheet $score)
|
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')) {
|
if ($score->entry->audition->hasFlag('seats_published')) {
|
||||||
return redirect()->back()->with('error', 'Cannot delete scores for an entry where seats are published');
|
return redirect()->back()->with('error', 'Cannot delete scores for an entry where seats are published');
|
||||||
}
|
}
|
||||||
if ($score->entry->audition->hasFlag('advancement_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();
|
$score->delete();
|
||||||
|
|
||||||
|
|
@ -66,51 +70,35 @@ class ScoreController extends Controller
|
||||||
compact('entry', 'judges', 'scoring_guide', 'subscores', 'existing_sheets'));
|
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);
|
$publishedCheck = $this->checkIfPublished($entry);
|
||||||
if ($publishedCheck) {
|
if ($publishedCheck) {
|
||||||
return $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');
|
$entry->removeFlag('no_show');
|
||||||
|
|
||||||
$judges = $entry->audition->room->judges;
|
return redirect()->route('scores.chooseEntry')->with('success', 'Scores saved');
|
||||||
|
|
||||||
$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');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function checkIfPublished($entry)
|
protected function checkIfPublished($entry)
|
||||||
|
|
|
||||||
|
|
@ -2,184 +2,276 @@
|
||||||
|
|
||||||
namespace App\Http\Controllers\Tabulation;
|
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\Actions\Tabulation\RankAuditionEntries;
|
||||||
use App\Exceptions\AuditionAdminException;
|
use App\Exceptions\AuditionAdminException;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\Audition;
|
use App\Models\Audition;
|
||||||
use App\Services\AuditionService;
|
use App\Models\Doubler;
|
||||||
use App\Services\DoublerService;
|
use App\Models\Ensemble;
|
||||||
use App\Services\EntryService;
|
use App\Models\Entry;
|
||||||
|
use App\Models\Seat;
|
||||||
|
use Debugbar;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
use Illuminate\Support\Facades\Cache;
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
|
||||||
use function redirect;
|
use function redirect;
|
||||||
|
|
||||||
class SeatAuditionFormController extends Controller
|
class SeatAuditionFormController extends Controller
|
||||||
{
|
{
|
||||||
protected CalculateEntryScore $calc;
|
public function showForm(Request $request, Audition $audition)
|
||||||
|
|
||||||
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
|
$seatingProposal = (session('proposedSeatingArray-'.$audition->id));
|
||||||
if ($request->method() == 'POST' && $request->input('ensembleAccept')) {
|
if ($audition->hasFlag('seats_published')) {
|
||||||
$requestedEnsembleAccepts = $request->input('ensembleAccept');
|
$publishedSeats = Seat::where('audition_id', $audition->id)
|
||||||
|
->join('ensembles', 'seats.ensemble_id', '=', 'ensembles.id')
|
||||||
|
->orderBy('ensembles.rank')
|
||||||
|
->orderBy('seats.seat')
|
||||||
|
->select('seats.*')
|
||||||
|
->with(['ensemble', 'student.school'])
|
||||||
|
->get();
|
||||||
} else {
|
} else {
|
||||||
$requestedEnsembleAccepts = false;
|
$publishedSeats = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deal with a mass no-show request
|
$ranker = app(RankAuditionEntries::class);
|
||||||
if ($request->input('mass-no-show')) {
|
// Get scored entries in order
|
||||||
$entries = $audition->entries()->forSeating()->withCount('scoreSheets')->with('flags')->get();
|
$scored_entries = $ranker($audition, 'seating');
|
||||||
foreach ($entries as $entry) {
|
$scored_entries->load(['student.doublers', 'student.school']);
|
||||||
if ($entry->scoreSheets_count == 0 && ! $entry->hasFlag('no_show')) {
|
// Get unscored entries sorted by draw number
|
||||||
$entry->addFlag('no_show');
|
$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();
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
|
||||||
|
// Get Doublers
|
||||||
|
$doublerData = Doubler::where('event_id', $audition->event_id)
|
||||||
|
->whereIn('student_id', $scored_entries->pluck('student_id'))
|
||||||
|
->get()
|
||||||
|
->keyBy('student_id');
|
||||||
|
|
||||||
|
$auditionHasUnresolvedDoublers = false;
|
||||||
|
foreach ($doublerData as $doubler) {
|
||||||
|
if (! is_null($doubler->accepted_entry)) {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
Cache::forget('entryScore-'.$entry->id.'-seating');
|
foreach ($doubler->entries() as $entry) {
|
||||||
Cache::forget('entryScore-'.$entry->id.'-advancement');
|
if ($entry->audition_id === $audition->id && $entry->hasFlag('declined')) {
|
||||||
|
continue 2;
|
||||||
}
|
}
|
||||||
Cache::forget('audition'.$audition->id.'seating');
|
}
|
||||||
Cache::forget('audition'.$audition->id.'advancement');
|
$auditionHasUnresolvedDoublers = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
$entryData = [];
|
$canSeat = ! $auditionHasUnresolvedDoublers && $unscored_entries->count() === 0;
|
||||||
$entries = $this->ranker->rank('seating', $audition);
|
|
||||||
|
|
||||||
// Deal with mass decline doubler request
|
return view('tabulation.auditionSeating',
|
||||||
if ($request->input('decline-below')) {
|
compact('audition',
|
||||||
Cache::forget('audition'.$audition->id.'seating');
|
'scored_entries',
|
||||||
|
'unscored_entries',
|
||||||
|
'noshow_entries',
|
||||||
|
'failed_prelim_entries',
|
||||||
|
'doublerData',
|
||||||
|
'auditionHasUnresolvedDoublers',
|
||||||
|
'canSeat',
|
||||||
|
'seatingProposal',
|
||||||
|
'publishedSeats',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
$changes_made = false;
|
public function declineSeat(Audition $audition, Entry $entry)
|
||||||
foreach ($entries as $entry) {
|
{
|
||||||
$doublerData = $this->doublerService->entryDoublerData($entry);
|
$entry->addFlag('declined');
|
||||||
if ($doublerData && ! $entry->hasFlag('declined') && $entry->rank > $request->input('decline-below')) {
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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')) {
|
||||||
|
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) {
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
try {
|
||||||
$this->decider->decline($entry);
|
$msg = $recorder($entry);
|
||||||
$changes_made = true;
|
|
||||||
} catch (AuditionAdminException $e) {
|
} catch (AuditionAdminException $e) {
|
||||||
return redirect()->back()->with('error', $e->getMessage());
|
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();
|
return redirect()->route('seating.audition', [$audition])->with('success', $msg);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$entries->load('student.school');
|
public function draftSeats(
|
||||||
$entries->load('student.doublerRequests');
|
Audition $audition,
|
||||||
$seatable = [
|
Request $request
|
||||||
'allScored' => true,
|
) {
|
||||||
'doublersResolved' => true,
|
$ranker = app(RankAuditionEntries::class);
|
||||||
];
|
$validated = $request->validate([
|
||||||
foreach ($entries as $entry) {
|
'ensemble' => ['required', 'array'],
|
||||||
$totalScoreColumn = 'No Score';
|
'ensemble.*' => ['required', 'integer', 'min:0'],
|
||||||
$fullyScored = false;
|
]);
|
||||||
if ($entry->score_totals) {
|
$proposedSeatingArray = [];
|
||||||
$totalScoreColumn = $entry->score_totals[0] >= 0 ? $entry->score_totals[0] : $entry->score_message;
|
$rankedEntries = $ranker($audition, 'seating');
|
||||||
$fullyScored = $entry->score_totals[0] >= 0;
|
$rankedEntries = $rankedEntries->reject(function ($entry) {
|
||||||
}
|
return $entry->hasFlag('declined');
|
||||||
// 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;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$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;
|
||||||
}
|
}
|
||||||
|
|
||||||
return view('tabulation.auditionSeating',
|
$thisEntry = $rankedEntries->shift();
|
||||||
compact('entryData', 'audition', 'rightPanel', 'seatableEntries', 'requestedEnsembleAccepts'));
|
$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;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function pickRightPanel(Audition $audition, array $seatable)
|
$ensembleRankOn++;
|
||||||
{
|
|
||||||
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) {
|
$sessionKeyName = 'proposedSeatingArray-'.$audition->id;
|
||||||
$rightPanel['view'] = 'tabulation.auditionSeating-unable-to-seat-card';
|
$request->session()->put($sessionKeyName, $proposedSeatingArray, 10);
|
||||||
$rightPanel['data'] = $seatable;
|
|
||||||
|
|
||||||
return $rightPanel;
|
return redirect()->route('seating.audition', ['audition' => $audition->id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$rightPanel['view'] = 'tabulation.auditionSeating-right-complete-not-published';
|
public function clearDraft(
|
||||||
$rightPanel['data'] = $this->auditionService->getSeatingLimits($audition);
|
Audition $audition
|
||||||
|
) {
|
||||||
|
session()->forget('proposedSeatingArray-'.$audition->id);
|
||||||
|
|
||||||
return $rightPanel;
|
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]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,180 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
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 SeatAuditionFormControllerOLD 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,9 +2,11 @@
|
||||||
|
|
||||||
namespace App\Http\Controllers\Tabulation;
|
namespace App\Http\Controllers\Tabulation;
|
||||||
|
|
||||||
|
use App\Actions\Tabulation\CalculateAuditionScores;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\Audition;
|
use App\Models\Audition;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
|
||||||
class SeatingStatusController extends Controller
|
class SeatingStatusController extends Controller
|
||||||
{
|
{
|
||||||
|
|
@ -13,6 +15,25 @@ class SeatingStatusController extends Controller
|
||||||
*/
|
*/
|
||||||
public function __invoke(Request $request)
|
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');
|
||||||
|
|
||||||
|
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()
|
$auditions = Audition::forSeating()
|
||||||
->withCount([
|
->withCount([
|
||||||
'entries' => function ($query) {
|
'entries' => function ($query) {
|
||||||
|
|
@ -25,6 +46,7 @@ class SeatingStatusController extends Controller
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
->with('flags')
|
->with('flags')
|
||||||
|
->with('entries')
|
||||||
->get();
|
->get();
|
||||||
$auditionData = [];
|
$auditionData = [];
|
||||||
foreach ($auditions as $audition) {
|
foreach ($auditions as $audition) {
|
||||||
|
|
@ -36,7 +58,7 @@ class SeatingStatusController extends Controller
|
||||||
'name' => $audition->name,
|
'name' => $audition->name,
|
||||||
'scoredEntriesCount' => $audition->entries_count - $audition->unscored_entries_count,
|
'scoredEntriesCount' => $audition->entries_count - $audition->unscored_entries_count,
|
||||||
'totalEntriesCount' => $audition->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,
|
'scoringComplete' => $audition->unscored_entries_count === 0,
|
||||||
'seatsPublished' => $audition->hasFlag('seats_published'),
|
'seatsPublished' => $audition->hasFlag('seats_published'),
|
||||||
'audition' => $audition,
|
'audition' => $audition,
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Carbon\Carbon;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
|
@ -15,7 +16,7 @@ class AuditLogEntry extends Model
|
||||||
|
|
||||||
public function getCreatedAtAttribute($value)
|
public function getCreatedAtAttribute($value)
|
||||||
{
|
{
|
||||||
return \Carbon\Carbon::parse($value)
|
return Carbon::parse($value)
|
||||||
->setTimezone('America/Chicago')
|
->setTimezone('America/Chicago')
|
||||||
->format('M j, Y H:i:s');
|
->format('M j, Y H:i:s');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
use function in_array;
|
use function in_array;
|
||||||
|
|
||||||
|
|
@ -35,9 +36,12 @@ class Audition extends Model
|
||||||
public function unscoredEntries(): HasMany
|
public function unscoredEntries(): HasMany
|
||||||
{
|
{
|
||||||
return $this->hasMany(Entry::class)
|
return $this->hasMany(Entry::class)
|
||||||
->whereDoesntHave('scoreSheets')
|
->whereDoesntHave('totalScore')
|
||||||
->whereDoesntHave('flags', function ($query) {
|
->whereDoesntHave('flags', function ($query) {
|
||||||
$query->where('flag_name', 'no_show');
|
$query->where('flag_name', 'no_show');
|
||||||
|
})
|
||||||
|
->whereDoesntHave('flags', function ($query) {
|
||||||
|
$query->where('flag_name', 'failed_prelim');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -56,6 +60,15 @@ class Audition extends Model
|
||||||
return $this->belongsToMany(BonusScoreDefinition::class, 'bonus_score_audition_assignment');
|
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
|
public function display_fee(): string
|
||||||
{
|
{
|
||||||
return '$'.number_format($this->entry_fee / 100, 2);
|
return '$'.number_format($this->entry_fee / 100, 2);
|
||||||
|
|
@ -129,6 +142,17 @@ class Audition extends Model
|
||||||
return $this->hasMany(Seat::class);
|
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
|
public function scopeOpen(Builder $query): void
|
||||||
{
|
{
|
||||||
$currentDate = Carbon::now('America/Chicago');
|
$currentDate = Carbon::now('America/Chicago');
|
||||||
|
|
|
||||||
|
|
@ -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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Models;
|
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
|
|
||||||
class CalculatedScore extends Model
|
|
||||||
{
|
|
||||||
use HasFactory;
|
|
||||||
|
|
||||||
protected $guarded = [];
|
|
||||||
|
|
||||||
protected $casts = ['calculatedScore' => 'json'];
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,104 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class Doubler extends Model
|
||||||
|
{
|
||||||
|
// Specify that we're not using a single primary key
|
||||||
|
protected $primaryKey = null;
|
||||||
|
|
||||||
|
public $incrementing = false;
|
||||||
|
|
||||||
|
protected $guarded = [];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'entries' => 'array',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function student(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Student::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function event(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Event::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function entries()
|
||||||
|
{
|
||||||
|
return Entry::whereIn('id', $this->entries)->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 ($eventId instanceof Event) {
|
||||||
|
$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')
|
||||||
|
->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)->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::create([
|
||||||
|
'student_id' => $student->id,
|
||||||
|
'event_id' => $eventId,
|
||||||
|
'entries' => $entryList,
|
||||||
|
'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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class DoublerEntryCount extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'doubler_entry_counts';
|
||||||
|
|
||||||
|
public function student(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Student::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function event(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Event::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,7 +2,9 @@
|
||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Actions\Tabulation\RankAuditionEntries;
|
||||||
use App\Enums\EntryFlags;
|
use App\Enums\EntryFlags;
|
||||||
|
use App\Exceptions\AuditionAdminException;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
@ -17,16 +19,45 @@ class Entry extends Model
|
||||||
|
|
||||||
protected $guarded = [];
|
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'];
|
protected $with = ['flags'];
|
||||||
|
|
||||||
|
public function totalScore(): HasOne
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// Return false if entry not found, otherwise return 1-based position
|
||||||
|
return $position === false ? false : $position + 1;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
public function student(): BelongsTo
|
public function student(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(Student::class);
|
return $this->belongsTo(Student::class);
|
||||||
|
|
@ -98,7 +129,7 @@ class Entry extends Model
|
||||||
|
|
||||||
public function removeFlag($flag): void
|
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->flags()->where('flag_name', $flag)->delete();
|
||||||
$this->load('flags');
|
$this->load('flags');
|
||||||
}
|
}
|
||||||
|
|
@ -120,11 +151,6 @@ class Entry extends Model
|
||||||
return $this->hasOne(Seat::class);
|
return $this->hasOne(Seat::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function calculatedScores(): HasMany
|
|
||||||
{
|
|
||||||
return $this->hasMany(CalculatedScore::class);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function scopeForSeating(Builder $query): void
|
public function scopeForSeating(Builder $query): void
|
||||||
{
|
{
|
||||||
$query->where('for_seating', 1);
|
$query->where('for_seating', 1);
|
||||||
|
|
|
||||||
|
|
@ -15,36 +15,8 @@ class EntryFlag extends Model
|
||||||
'flag_name' => EntryFlags::class,
|
'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
|
public function entry(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(Entry::class);
|
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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class EntryTotalScore extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'seating_subscore_totals' => 'json',
|
||||||
|
'advancement_subscore_totals' => 'json',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function entry(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Entry::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,7 +5,6 @@ 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\Database\Eloquent\Relations\HasOneThrough;
|
use Illuminate\Database\Eloquent\Relations\HasOneThrough;
|
||||||
use Illuminate\Support\Facades\Cache;
|
|
||||||
|
|
||||||
class ScoreSheet extends Model
|
class ScoreSheet extends Model
|
||||||
{
|
{
|
||||||
|
|
@ -13,26 +12,12 @@ class ScoreSheet extends Model
|
||||||
'user_id',
|
'user_id',
|
||||||
'entry_id',
|
'entry_id',
|
||||||
'subscores',
|
'subscores',
|
||||||
|
'seating_total',
|
||||||
|
'advancement_total',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $casts = ['subscores' => 'json'];
|
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
|
public function entry(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(Entry::class);
|
return $this->belongsTo(Entry::class);
|
||||||
|
|
@ -60,16 +45,4 @@ class ScoreSheet extends Model
|
||||||
return $this->subscores[$id]['score'] ?? false;
|
return $this->subscores[$id]['score'] ?? false;
|
||||||
// this function is used at resources/views/tabulation/entry_score_sheet.blade.php
|
// 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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
|
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
class Student extends Model
|
class Student extends Model
|
||||||
{
|
{
|
||||||
|
|
@ -50,6 +51,9 @@ class Student extends Model
|
||||||
return $this->hasMany(HistoricalSeat::class);
|
return $this->hasMany(HistoricalSeat::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the directors at this student's school.
|
||||||
|
*/
|
||||||
public function users(): HasManyThrough
|
public function users(): HasManyThrough
|
||||||
{
|
{
|
||||||
return $this->hasManyThrough(
|
return $this->hasManyThrough(
|
||||||
|
|
@ -62,6 +66,11 @@ class Student extends Model
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the directors at this student's school.
|
||||||
|
* Alias of users())
|
||||||
|
* '
|
||||||
|
*/
|
||||||
public function directors(): HasManyThrough
|
public function directors(): HasManyThrough
|
||||||
{
|
{
|
||||||
return $this->users();
|
return $this->users();
|
||||||
|
|
@ -85,4 +94,34 @@ class Student extends Model
|
||||||
{
|
{
|
||||||
return $this->hasMany(DoublerRequest::class);
|
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;
|
||||||
|
|
||||||
|
return Doubler::where([
|
||||||
|
'event_id' => $eventId,
|
||||||
|
'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();
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -127,7 +127,11 @@ class User extends Authenticatable implements MustVerifyEmail
|
||||||
|
|
||||||
public function isJudge(): bool
|
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
|
public function possibleSchools(): Collection
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Observers;
|
||||||
|
|
||||||
|
use App\Models\Doubler;
|
||||||
|
use App\Models\EntryFlag;
|
||||||
|
|
||||||
|
class EntryFlagObserver
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle the EntryFlag "created" event.
|
||||||
|
*/
|
||||||
|
public function created(EntryFlag $entryFlag): void
|
||||||
|
{
|
||||||
|
Doubler::syncDoublers();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the EntryFlag "updated" event.
|
||||||
|
*/
|
||||||
|
public function updated(EntryFlag $entryFlag): void
|
||||||
|
{
|
||||||
|
Doubler::syncDoublers();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the EntryFlag "deleted" event.
|
||||||
|
*/
|
||||||
|
public function deleted(EntryFlag $entryFlag): void
|
||||||
|
{
|
||||||
|
Doubler::syncDoublers();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the EntryFlag "restored" event.
|
||||||
|
*/
|
||||||
|
public function restored(EntryFlag $entryFlag): void
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the EntryFlag "force deleted" event.
|
||||||
|
*/
|
||||||
|
public function forceDeleted(EntryFlag $entryFlag): void
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,8 +2,8 @@
|
||||||
|
|
||||||
namespace App\Observers;
|
namespace App\Observers;
|
||||||
|
|
||||||
use App\Events\AuditionChange;
|
use App\Models\Audition;
|
||||||
use App\Events\EntryChange;
|
use App\Models\Doubler;
|
||||||
use App\Models\Entry;
|
use App\Models\Entry;
|
||||||
|
|
||||||
class EntryObserver
|
class EntryObserver
|
||||||
|
|
@ -13,6 +13,16 @@ class EntryObserver
|
||||||
*/
|
*/
|
||||||
public function created(Entry $entry): void
|
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
|
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
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
namespace App\Observers;
|
namespace App\Observers;
|
||||||
|
|
||||||
use App\Events\ScoreSheetChange;
|
use App\Actions\Tabulation\TotalEntryScores;
|
||||||
use App\Models\ScoreSheet;
|
use App\Models\ScoreSheet;
|
||||||
|
|
||||||
class ScoreSheetObserver
|
class ScoreSheetObserver
|
||||||
|
|
@ -12,7 +12,8 @@ class ScoreSheetObserver
|
||||||
*/
|
*/
|
||||||
public function created(ScoreSheet $scoreSheet): void
|
public function created(ScoreSheet $scoreSheet): void
|
||||||
{
|
{
|
||||||
//
|
$calculator = app(TotalEntryScores::class);
|
||||||
|
$calculator($scoreSheet->entry, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -20,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -28,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,10 @@ namespace App\Providers;
|
||||||
use App\Actions\Entries\CreateEntry;
|
use App\Actions\Entries\CreateEntry;
|
||||||
use App\Actions\Entries\UpdateEntry;
|
use App\Actions\Entries\UpdateEntry;
|
||||||
use App\Actions\Schools\SetHeadDirector;
|
use App\Actions\Schools\SetHeadDirector;
|
||||||
use App\Actions\Tabulation\AllowForOlympicScoring;
|
use App\Actions\Tabulation\CalculateAuditionScores;
|
||||||
use App\Actions\Tabulation\CalculateEntryScore;
|
|
||||||
use App\Actions\Tabulation\CalculateScoreSheetTotal;
|
use App\Actions\Tabulation\CalculateScoreSheetTotal;
|
||||||
use App\Actions\Tabulation\CalculateScoreSheetTotalDivideByTotalWeights;
|
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\NominationAdminController;
|
||||||
use App\Http\Controllers\NominationEnsembles\NominationEnsembleController;
|
use App\Http\Controllers\NominationEnsembles\NominationEnsembleController;
|
||||||
use App\Http\Controllers\NominationEnsembles\NominationEnsembleEntryController;
|
use App\Http\Controllers\NominationEnsembles\NominationEnsembleEntryController;
|
||||||
|
|
@ -19,7 +18,9 @@ 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\Room;
|
use App\Models\Room;
|
||||||
use App\Models\RoomUser;
|
use App\Models\RoomUser;
|
||||||
use App\Models\School;
|
use App\Models\School;
|
||||||
|
|
@ -30,6 +31,8 @@ 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\EntryObserver;
|
use App\Observers\EntryObserver;
|
||||||
use App\Observers\RoomObserver;
|
use App\Observers\RoomObserver;
|
||||||
use App\Observers\RoomUserObserver;
|
use App\Observers\RoomUserObserver;
|
||||||
|
|
@ -58,8 +61,6 @@ class AppServiceProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
//$this->app->singleton(CalculateScoreSheetTotal::class, CalculateScoreSheetTotal::class);
|
//$this->app->singleton(CalculateScoreSheetTotal::class, CalculateScoreSheetTotal::class);
|
||||||
//$this->app->singleton(CalculateScoreSheetTotal::class, CalculateScoreSheetTotalDivideByTotalWeights::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(DrawService::class, DrawService::class);
|
||||||
$this->app->singleton(AuditionService::class, AuditionService::class);
|
$this->app->singleton(AuditionService::class, AuditionService::class);
|
||||||
$this->app->singleton(EntryService::class, EntryService::class);
|
$this->app->singleton(EntryService::class, EntryService::class);
|
||||||
|
|
@ -69,6 +70,8 @@ class AppServiceProvider extends ServiceProvider
|
||||||
$this->app->singleton(CreateEntry::class, CreateEntry::class);
|
$this->app->singleton(CreateEntry::class, CreateEntry::class);
|
||||||
$this->app->singleton(UpdateEntry::class, UpdateEntry::class);
|
$this->app->singleton(UpdateEntry::class, UpdateEntry::class);
|
||||||
$this->app->singleton(SetHeadDirector::class, SetHeadDirector::class);
|
$this->app->singleton(SetHeadDirector::class, SetHeadDirector::class);
|
||||||
|
$this->app->singleton(TotalEntryScores::class, TotalEntryScores::class);
|
||||||
|
$this->app->singleton(CalculateAuditionScores::class, CalculateAuditionScores::class);
|
||||||
|
|
||||||
// Nomination Ensemble
|
// Nomination Ensemble
|
||||||
// $this->app->bind(NominationEnsembleController::class, ScobdaNominationEnsembleController::class);
|
// $this->app->bind(NominationEnsembleController::class, ScobdaNominationEnsembleController::class);
|
||||||
|
|
@ -82,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);
|
||||||
|
|
@ -93,7 +97,8 @@ class AppServiceProvider extends ServiceProvider
|
||||||
SubscoreDefinition::observe(SubscoreDefinitionObserver::class);
|
SubscoreDefinition::observe(SubscoreDefinitionObserver::class);
|
||||||
User::observe(UserObserver::class);
|
User::observe(UserObserver::class);
|
||||||
SeatingLimit::observe(SeatingLimitObserver::class);
|
SeatingLimit::observe(SeatingLimitObserver::class);
|
||||||
|
EntryFlag::observe(EntryFlagObserver::class);
|
||||||
|
|
||||||
//Model::preventLazyLoading(! app()->isProduction());
|
Model::preventLazyLoading(! app()->isProduction());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,11 @@
|
||||||
namespace App;
|
namespace App;
|
||||||
|
|
||||||
use App\Models\SiteSetting;
|
use App\Models\SiteSetting;
|
||||||
use Illuminate\Support\Facades\Cache;
|
|
||||||
|
|
||||||
class Settings
|
class Settings
|
||||||
{
|
{
|
||||||
|
public static $settings = null;
|
||||||
|
|
||||||
protected static $cacheKey = 'site_settings';
|
protected static $cacheKey = 'site_settings';
|
||||||
|
|
||||||
public static function __callStatic($key, $arguments)
|
public static function __callStatic($key, $arguments)
|
||||||
|
|
@ -14,19 +15,23 @@ class Settings
|
||||||
return self::get($key);
|
return self::get($key);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load settings from the database and cache them
|
// Load settings from the database
|
||||||
public static function loadSettings()
|
public static function loadSettings()
|
||||||
{
|
{
|
||||||
$settings = SiteSetting::all()->pluck('setting_value', 'setting_key')->toArray();
|
if (self::$settings === null) {
|
||||||
Cache::put(self::$cacheKey, $settings, 3600); // Cache for 1 hour
|
self::$settings = SiteSetting::all()->pluck('setting_value', 'setting_key')->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get a setting value by key
|
// Get a setting value by key
|
||||||
public static function get($key, $default = null)
|
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
|
// Set a setting value by key
|
||||||
|
|
@ -35,15 +40,17 @@ class Settings
|
||||||
// Update the database
|
// Update the database
|
||||||
SiteSetting::updateOrCreate(['setting_key' => $key], ['setting_value' => $value]);
|
SiteSetting::updateOrCreate(['setting_key' => $key], ['setting_value' => $value]);
|
||||||
|
|
||||||
// Update the cache
|
// Update the static property
|
||||||
$settings = Cache::get(self::$cacheKey, []);
|
if (self::$settings === null) {
|
||||||
$settings[$key] = $value;
|
self::loadSettings();
|
||||||
Cache::put(self::$cacheKey, $settings, 3600); // Cache for 1 hour
|
}
|
||||||
|
self::$settings[$key] = $value;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear the cache
|
// Clear the settings
|
||||||
public static function clearCache()
|
public static function clearSettings()
|
||||||
{
|
{
|
||||||
Cache::forget(self::$cacheKey);
|
self::$settings = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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('score_sheets', function (Blueprint $table) {
|
||||||
|
$table->decimal('seating_total', 9, 6)->after('subscores');
|
||||||
|
});
|
||||||
|
Schema::table('score_sheets', function (Blueprint $table) {
|
||||||
|
$table->decimal('advancement_total', 9, 6)->after('seating_total');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('score_sheets', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('seating_total');
|
||||||
|
$table->dropColumn('advancement_total');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Entry;
|
||||||
|
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::dropIfExists('calculated_scores');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::create('calculated_scores', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignIdFor(Entry::class)->constrained()->cascadeOnDelete()->cascadeOnUpdate();
|
||||||
|
$table->string('mode');
|
||||||
|
$table->json('calculatedScore');
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Entry;
|
||||||
|
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::create('entry_total_scores', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignIdFor(Entry::class)->constrained()->cascadeOnDelete()->cascadeOnUpdate();
|
||||||
|
$table->decimal('seating_total', 9, 6);
|
||||||
|
$table->decimal('advancement_total', 9, 6);
|
||||||
|
$table->json('seating_subscore_totals');
|
||||||
|
$table->json('advancement_subscore_totals');
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('entry_total_scores');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
<?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
|
||||||
|
{
|
||||||
|
DB::statement('
|
||||||
|
CREATE VIEW doubler_entry_counts AS
|
||||||
|
SELECT
|
||||||
|
e.event_id,
|
||||||
|
ent.student_id,
|
||||||
|
COUNT(*) as entry_count
|
||||||
|
FROM entries ent
|
||||||
|
JOIN auditions e ON e.id = ent.audition_id
|
||||||
|
GROUP BY e.event_id, ent.student_id
|
||||||
|
HAVING COUNT(*) > 1
|
||||||
|
');
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Entry;
|
||||||
|
use App\Models\Event;
|
||||||
|
use App\Models\Student;
|
||||||
|
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::create('doublers', function (Blueprint $table) {
|
||||||
|
// 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->json('entries')->nullable();
|
||||||
|
$table->foreignIdFor(Entry::class, 'accepted_entry')->nullable()->constrained('entries')->cascadeOnDelete()->cascadeOnUpdate();
|
||||||
|
|
||||||
|
// Set the composite primary key
|
||||||
|
$table->primary(['student_id', 'event_id']);
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('doublers');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -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');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -17,54 +17,62 @@ class EntrySeeder extends Seeder
|
||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
$students = Student::all();
|
$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) {
|
foreach ($students as $student) {
|
||||||
if ($student->grade > 9) {
|
if ($student->grade > 9) {
|
||||||
$audition = Audition::where('maximum_grade', '=', '12')->inRandomOrder()->first();
|
$audition = Audition::where('maximum_grade', '=', '12')
|
||||||
|
->inRandomOrder()->first();
|
||||||
}
|
}
|
||||||
if ($student->grade == 9) {
|
if ($student->grade == 9) {
|
||||||
$audition = Audition::where('maximum_grade', '>', '8')->inRandomOrder()->first();
|
$audition = Audition::where('maximum_grade', '>', '8')
|
||||||
|
->inRandomOrder()->first();
|
||||||
}
|
}
|
||||||
if ($student->grade == 8) {
|
if ($student->grade == 8) {
|
||||||
$audition = Audition::where('maximum_grade', '=', '9')->inRandomOrder()->first();
|
$audition = Audition::where('maximum_grade', '=', '9')
|
||||||
|
->inRandomOrder()->first();
|
||||||
}
|
}
|
||||||
if ($student->grade == 7) {
|
if ($student->grade == 7) {
|
||||||
$audition = Audition::where('maximum_grade', '=', '7')->inRandomOrder()->first();
|
$audition = Audition::where('maximum_grade', '=', '7')
|
||||||
|
->inRandomOrder()->first();
|
||||||
}
|
}
|
||||||
|
|
||||||
Entry::create([
|
Entry::create([
|
||||||
'student_id' => $student->id,
|
'student_id' => $student->id,
|
||||||
'audition_id' => $audition->id,
|
'audition_id' => $audition->id,
|
||||||
|
'for_seating' => 1,
|
||||||
|
'for_advancement' => 1,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (mt_rand(1, 100) > 90) {
|
if (mt_rand(1, 100) > 90) {
|
||||||
if ($student->grade > 9) {
|
if ($student->grade > 9) {
|
||||||
$audition2 = Audition::where('maximum_grade', '=', '12')->where('id', '!=',
|
$audition2 = Audition::where('maximum_grade', '=', '12')
|
||||||
$audition->id)->inRandomOrder()->first();
|
->where('id', '!=', $audition->id)
|
||||||
|
->inRandomOrder()->first();
|
||||||
}
|
}
|
||||||
if ($student->grade == 9) {
|
if ($student->grade == 9) {
|
||||||
$audition2 = Audition::where('maximum_grade', '>', '8')->where('id', '!=',
|
$audition2 = Audition::where('maximum_grade', '>', '8')
|
||||||
$audition->id)->inRandomOrder()->first();
|
->where('id', '!=', $audition->id)
|
||||||
|
->inRandomOrder()->first();
|
||||||
}
|
}
|
||||||
if ($student->grade == 8) {
|
if ($student->grade == 8) {
|
||||||
$audition2 = Audition::where('maximum_grade', '=', '9')->where('id', '!=',
|
$audition2 = Audition::where('maximum_grade', '=', '9')
|
||||||
$audition->id)->inRandomOrder()->first();
|
->where('id', '!=', $audition->id)
|
||||||
|
->inRandomOrder()->first();
|
||||||
}
|
}
|
||||||
if ($student->grade == 7) {
|
if ($student->grade == 7) {
|
||||||
$audition2 = Audition::where('maximum_grade', '=', '7')->where('id', '!=',
|
$audition2 = Audition::where('maximum_grade', '=', '7')
|
||||||
$audition->id)->inRandomOrder()->first();
|
->where('id', '!=', $audition->id)
|
||||||
|
->inRandomOrder()->first();
|
||||||
}
|
}
|
||||||
|
|
||||||
Entry::create([
|
Entry::create([
|
||||||
'student_id' => $student->id,
|
'student_id' => $student->id,
|
||||||
'audition_id' => $audition2->id,
|
'audition_id' => $audition2->id,
|
||||||
|
'for_seating' => 1,
|
||||||
|
'for_advancement' => 1,
|
||||||
]);
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
// Triplers are possible
|
|
||||||
if (mt_rand(1, 100) > 90) {
|
if (mt_rand(1, 100) > 90) {
|
||||||
if ($student->grade > 9) {
|
if ($student->grade > 9) {
|
||||||
$audition3 = Audition::where('maximum_grade', '=', '12')->where('id', '!=',
|
$audition3 = Audition::where('maximum_grade', '=', '12')->where('id', '!=',
|
||||||
|
|
@ -86,11 +94,10 @@ class EntrySeeder extends Seeder
|
||||||
Entry::create([
|
Entry::create([
|
||||||
'student_id' => $student->id,
|
'student_id' => $student->id,
|
||||||
'audition_id' => $audition3->id,
|
'audition_id' => $audition3->id,
|
||||||
|
'for_seating' => 1,
|
||||||
|
'for_advancement' => 1,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@
|
||||||
|
|
||||||
namespace Database\Seeders;
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use App\Actions\Tabulation\EnterScore;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
use App\Models\ScoreSheet;
|
|
||||||
|
|
||||||
class ScoreAllAuditions extends Seeder
|
class ScoreAllAuditions extends Seeder
|
||||||
{
|
{
|
||||||
|
|
@ -13,26 +13,19 @@ class ScoreAllAuditions extends Seeder
|
||||||
*/
|
*/
|
||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
|
$recorder = app(EnterScore::class);
|
||||||
$judges = User::all();
|
$judges = User::all();
|
||||||
foreach ($judges as $judge) {
|
foreach ($judges as $judge) { // Iterate over all users
|
||||||
foreach ($judge->rooms as $room) {
|
foreach ($judge->rooms as $room) { // Iterate over each user's assigned rooms
|
||||||
foreach ($room->auditions as $audition) {
|
foreach ($room->auditions as $audition) { // Iterate over each audition in that room
|
||||||
$scoringGuide = $audition->scoringGuide;
|
$scoringGuide = $audition->scoringGuide; // Load the scoring guide for that audition
|
||||||
$subscores = $scoringGuide->subscores;
|
$subscores = $scoringGuide->subscores; // Get the subscores for that audition
|
||||||
foreach ($audition->entries as $entry) {
|
foreach ($audition->entries as $entry) { // Iterate over each entry in that audition
|
||||||
$scoreArray = [];
|
$scoreArray = [];
|
||||||
foreach ($subscores as $subscore) {
|
foreach ($subscores as $subscore) {
|
||||||
$scoreArray[$subscore->id] = [
|
$scoreArray[$subscore->id] = mt_rand(0, $subscore->max_score);
|
||||||
'score' => mt_rand(0, 100),
|
|
||||||
'subscore_id' => $subscore->id,
|
|
||||||
'subscore_name' => $subscore->name,
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
ScoreSheet::create([
|
$recorder($judge, $entry, $scoreArray);
|
||||||
'user_id' => $judge->id,
|
|
||||||
'entry_id' => $entry->id,
|
|
||||||
'subscores' => $scoreArray,
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@
|
||||||
@foreach($event_entries[$event->id] as $entry)
|
@foreach($event_entries[$event->id] as $entry)
|
||||||
<tr>
|
<tr>
|
||||||
<x-table.td>{{ $entry->id }}</x-table.td>
|
<x-table.td>{{ $entry->id }}</x-table.td>
|
||||||
<x-table.td>{{ $entry->audition->name }}</x-table.td>
|
<x-table.td><a href="{{ route ('seating.audition',[$entry->audition_id]) }}#entry-{{ $entry->id }}">{{ $entry->audition->name }}</a></x-table.td>
|
||||||
<x-table.td>{{ $entry->draw_number }}</x-table.td>
|
<x-table.td>{{ $entry->draw_number }}</x-table.td>
|
||||||
<x-table.td>
|
<x-table.td>
|
||||||
@if($entry->doubler_decision_frozen)
|
@if($entry->doubler_decision_frozen)
|
||||||
|
|
|
||||||
|
|
@ -27,24 +27,24 @@
|
||||||
</x-card.list.row>
|
</x-card.list.row>
|
||||||
</a>
|
</a>
|
||||||
@endif
|
@endif
|
||||||
@if($showRecapLink)
|
{{-- @if($showRecapLink)--}}
|
||||||
<a href="{{ route('recap.selectAudition') }}">
|
{{-- <a href="{{ route('recap.selectAudition') }}">--}}
|
||||||
<x-card.list.row class="hover:bg-gray-200">
|
{{-- <x-card.list.row class="hover:bg-gray-200">--}}
|
||||||
Audition Score Recaps
|
{{-- Audition Score Recaps--}}
|
||||||
</x-card.list.row>
|
{{-- </x-card.list.row>--}}
|
||||||
</a>
|
{{-- </a>--}}
|
||||||
@endif
|
{{-- @endif--}}
|
||||||
</x-card.list.body>
|
</x-card.list.body>
|
||||||
</x-card.card>
|
</x-card.card>
|
||||||
</div>
|
</div>
|
||||||
@if(Auth::user()->school_id)
|
{{-- @if(Auth::user()->school_id)--}}
|
||||||
<div class="md:col-span-3 pl-3"> <!--Column 2 Results -->
|
{{-- <div class="md:col-span-3 pl-3"> <!--Column 2 Results -->--}}
|
||||||
<x-card.card>
|
{{-- <x-card.card>--}}
|
||||||
<x-card.heading>My Results</x-card.heading>
|
{{-- <x-card.heading>My Results</x-card.heading>--}}
|
||||||
@include('dashboard.results-table')
|
{{-- @include('dashboard.results-table')--}}
|
||||||
</x-card.card>
|
{{-- </x-card.card>--}}
|
||||||
</div>
|
{{-- </div>--}}
|
||||||
@endif
|
{{-- @endif--}}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,22 +16,15 @@
|
||||||
|
|
||||||
<x-table.body>
|
<x-table.body>
|
||||||
@foreach($entries as $entry)
|
@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
|
|
||||||
<tr>
|
<tr>
|
||||||
<x-table.td>{{ $entry->rank }}</x-table.td>
|
<x-table.td>{{ $entry->rank('advancement') }}</x-table.td>
|
||||||
<x-table.td>{{ $entry->id }}</x-table.td>
|
<x-table.td>{{ $entry->id }}</x-table.td>
|
||||||
<x-table.td>{{ $entry->draw_number }}</x-table.td>
|
<x-table.td>{{ $entry->draw_number }}</x-table.td>
|
||||||
<x-table.td class="flex flex-col">
|
<x-table.td class="flex flex-col">
|
||||||
<span>{{ $entry->student->full_name() }}</span>
|
<span>{{ $entry->student->full_name() }}</span>
|
||||||
<span class="text-xs text-gray-400">{{ $entry->student->school->name }}</span>
|
<span class="text-xs text-gray-400">{{ $entry->student->school->name }}</span>
|
||||||
</x-table.td>
|
</x-table.td>
|
||||||
<x-table.td>{{ $score }}</x-table.td>
|
<x-table.td>{{ $entry->totalScore->advancement_total }}</x-table.td>
|
||||||
|
|
||||||
<x-table.td class="flex space-x-1">
|
<x-table.td class="flex space-x-1">
|
||||||
@foreach($entry->advancementVotes as $vote)
|
@foreach($entry->advancementVotes as $vote)
|
||||||
|
|
|
||||||
|
|
@ -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')
|
||||||
|
<ul role="list" class="">
|
||||||
|
|
||||||
|
@foreach($entry['doubleData'] as $double)
|
||||||
|
@php($isopen = $double['status'] == 'undecided')
|
||||||
|
<li class="pb-2 pt-0 px-0 my-2 rounded-xl border border-gray-200 max-w-xs" x-data="{ open: {{ $isopen ? 'true':'false' }} }">
|
||||||
|
<div class="flex items-start gap-x-3 bg-gray-100 px-3 py-2 rounded-t-xl" >
|
||||||
|
<p class="text-sm font-semibold leading-6 text-gray-900">
|
||||||
|
<a href="{{ route('seating.audition', $double['audition']) }}">
|
||||||
|
{{ $double['auditionName'] }} - {{ $double['status'] }}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<div class="w-full flex justify-end" >
|
||||||
|
<div x-on:click=" open = ! open ">
|
||||||
|
<x-icons.hamburger-menu />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-4" x-show="open">
|
||||||
|
<div class="mt-1 px-3 text-xs leading-5 text-gray-500 col-span-3">
|
||||||
|
<ul>
|
||||||
|
<li class="">
|
||||||
|
<p class="whitespace-nowrap">Ranked {{ $double['rank'] }}</p>
|
||||||
|
<p class="truncate">{{ $double['unscored_entries'] }} Unscored</p>
|
||||||
|
</li>
|
||||||
|
@foreach($double['seating_limits'] as $limit)
|
||||||
|
<li>{{$limit['ensemble_name']}} accepts {{ $limit['accepts'] }}</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col justify-end gap-y-1 pt-1">
|
||||||
|
@if ($double['status'] == 'undecided')
|
||||||
|
<form method="POST" action="{{ route('doubler.accept',['entry'=>$double['entry']]) }}">
|
||||||
|
@csrf
|
||||||
|
<button class="{{ $doublerButtonClasses }}">Accept</button>
|
||||||
|
</form>
|
||||||
|
<form method="POST" action="{{ route('doubler.decline',['entry'=>$double['entry']]) }}">
|
||||||
|
@csrf
|
||||||
|
<button class="{{ $doublerButtonClasses }}">Decline</button>
|
||||||
|
</form>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
@endforeach
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{{--Complete Badge--}}
|
||||||
|
{{--<p class="mt-0.5 whitespace-nowrap rounded-md bg-green-50 px-1.5 py-0.5 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-600/20">Complete</p>--}}
|
||||||
|
|
||||||
|
{{--In Progres Badge--}}
|
||||||
|
{{--<p class="mt-0.5 whitespace-nowrap rounded-md bg-yellow-50 px-1.5 py-0.5 text-xs font-medium text-yellow-800 ring-1 ring-inset ring-yellow-600/20">In Progress</p>--}}
|
||||||
|
|
@ -1,55 +1,41 @@
|
||||||
@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')
|
<div class="border-2 border-gray-200 p-2 m-2"> {{-- Begin block for doubler entry --}}
|
||||||
<ul role="list" class="">
|
<div class="font-semibold mb-2">
|
||||||
|
<a href="{{route('seating.audition',[$de->audition])}}#entry-{{ $de->id }}">{{ $de->audition->name }}</a> #{{$de->draw_number}}
|
||||||
@foreach($entry['doubleData'] as $double)
|
({{ $de->id }})
|
||||||
@php($isopen = $double['status'] == 'undecided')
|
|
||||||
<li class="pb-2 pt-0 px-0 my-2 rounded-xl border border-gray-200 max-w-xs" x-data="{ open: {{ $isopen ? 'true':'false' }} }">
|
|
||||||
<div class="flex items-start gap-x-3 bg-gray-100 px-3 py-2 rounded-t-xl" >
|
|
||||||
<p class="text-sm font-semibold leading-6 text-gray-900">
|
|
||||||
<a href="{{ route('seating.audition', $double['audition']) }}">
|
|
||||||
{{ $double['auditionName'] }} - {{ $double['status'] }}
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
<div class="w-full flex justify-end" >
|
|
||||||
<div x-on:click=" open = ! open ">
|
|
||||||
<x-icons.hamburger-menu />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
@if($de->hasFlag('no_show'))
|
||||||
</div>
|
<div class="text-red-500">NO-SHOW</div>
|
||||||
|
@elseif($de->hasFlag('failed_prelim'))
|
||||||
<div class="grid grid-cols-4" x-show="open">
|
<div class="text-red-500">Failed Prelim</div>
|
||||||
<div class="mt-1 px-3 text-xs leading-5 text-gray-500 col-span-3">
|
@elseif($de->hasFlag('declined'))
|
||||||
<ul>
|
<div class="text-red-500">Declined</div>
|
||||||
<li class="">
|
@else
|
||||||
<p class="whitespace-nowrap">Ranked {{ $double['rank'] }}</p>
|
@php($unscored = $de->audition->unscoredEntries()->count())
|
||||||
<p class="truncate">{{ $double['unscored_entries'] }} Unscored</p>
|
@if($unscored > 0)
|
||||||
</li>
|
<div>{{ $unscored }} Unscored Entries</div>
|
||||||
@foreach($double['seating_limits'] as $limit)
|
|
||||||
<li>{{$limit['ensemble_name']}} accepts {{ $limit['accepts'] }}</li>
|
|
||||||
@endforeach
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col justify-end gap-y-1 pt-1">
|
|
||||||
@if ($double['status'] == 'undecided')
|
|
||||||
<form method="POST" action="{{ route('doubler.accept',['entry'=>$double['entry']]) }}">
|
|
||||||
@csrf
|
|
||||||
<button class="{{ $doublerButtonClasses }}">Accept</button>
|
|
||||||
</form>
|
|
||||||
<form method="POST" action="{{ route('doubler.decline',['entry'=>$double['entry']]) }}">
|
|
||||||
@csrf
|
|
||||||
<button class="{{ $doublerButtonClasses }}">Decline</button>
|
|
||||||
</form>
|
|
||||||
@endif
|
@endif
|
||||||
</div>
|
|
||||||
</div>
|
@if(! $de->rank('seating'))
|
||||||
</li>
|
<div class="text-red-500">THIS ENTRY NOT SCORED</div>
|
||||||
|
@else
|
||||||
|
<div>Ranked: {{ $de->rank('seating') }}</div>
|
||||||
|
<div class="mt-2">
|
||||||
|
Acceptance Limits<br>
|
||||||
|
@foreach ($de->audition->SeatingLimits as $limit)
|
||||||
|
{{ $limit->ensemble->name }} -> {{ $limit->maximum_accepted }}
|
||||||
|
<br>
|
||||||
@endforeach
|
@endforeach
|
||||||
|
</div>
|
||||||
|
<div class="mt-3">
|
||||||
|
{{-- TODO: Don't show the option to accept if it cannot be done --}}
|
||||||
|
<x-form.form method="POST" action="{{ route('seating.audition.accept',[$audition, $de]) }}" class="mb-3">
|
||||||
|
<x-form.button>Accept {{ $de->audition->name }}</x-form.button>
|
||||||
|
</x-form.form>
|
||||||
|
<x-form.form method="POST" action="{{ route('seating.audition.decline',[$audition,$de]) }}">
|
||||||
|
<x-form.button>Decline {{ $de->audition->name }}</x-form.button>
|
||||||
|
</x-form.form>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
@endif
|
||||||
|
|
||||||
</ul>
|
</div>
|
||||||
|
|
||||||
{{--Complete Badge--}}
|
|
||||||
{{--<p class="mt-0.5 whitespace-nowrap rounded-md bg-green-50 px-1.5 py-0.5 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-600/20">Complete</p>--}}
|
|
||||||
|
|
||||||
{{--In Progres Badge--}}
|
|
||||||
{{--<p class="mt-0.5 whitespace-nowrap rounded-md bg-yellow-50 px-1.5 py-0.5 text-xs font-medium text-yellow-800 ring-1 ring-inset ring-yellow-600/20">In Progress</p>--}}
|
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@
|
||||||
<p class="pt-3"><span class="font-semibold">Request: </span>{{$entry['doublerRequest']}}
|
<p class="pt-3"><span class="font-semibold">Request: </span>{{$entry['doublerRequest']}}
|
||||||
</p>
|
</p>
|
||||||
@endif
|
@endif
|
||||||
@include('tabulation.auditionSeating-doubler-block')
|
@include('tabulation.auditionSeating-doubler-block-OLD')
|
||||||
{{-- DOUBLER<br>--}}
|
{{-- DOUBLER<br>--}}
|
||||||
{{-- @foreach($entry['doubleData'] as $double)--}}
|
{{-- @foreach($entry['doubleData'] as $double)--}}
|
||||||
{{-- ID: {{ $double['entryId'] }} - {{ $double['name'] }} - {{ $double['rank'] }}<br>--}}
|
{{-- ID: {{ $double['entryId'] }} - {{ $double['name'] }} - {{ $double['rank'] }}<br>--}}
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,265 @@
|
||||||
@inject('doublerService','App\Services\DoublerService')
|
|
||||||
@php
|
|
||||||
$blockSeating = []
|
|
||||||
@endphp
|
|
||||||
<x-layout.app>
|
<x-layout.app>
|
||||||
<x-slot:page_title>Audition Seating - {{ $audition->name }}</x-slot:page_title>
|
<x-slot:page_title>Audition Seating - {{ $audition->name }}</x-slot:page_title>
|
||||||
<div class="grid grid-cols-4"></div>
|
<div class="grid grid-cols-4 gap-4">
|
||||||
<div class="grid grid-cols-4">
|
|
||||||
<div class="col-span-3">
|
<div class="col-span-3"> {{-- Entry Ranking Table --}}
|
||||||
@include('tabulation.auditionSeating-results-table')
|
<x-card.card class="px-3"> {{-- Scored Entries --}}
|
||||||
|
<x-card.heading class="-ml-3">Scored Entries</x-card.heading>
|
||||||
|
<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</x-table.th>
|
||||||
|
<x-table.th>Doubler</x-table.th>
|
||||||
|
<x-table.th>Total Score
|
||||||
|
@if($audition->bonusScore()->count() > 0)
|
||||||
|
<br>
|
||||||
|
<div class="display: flex">
|
||||||
|
<span class="text-yellow-500">No Bonus Score</span>
|
||||||
</div>
|
</div>
|
||||||
|
@endif
|
||||||
|
</x-table.th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-gray-200">
|
||||||
|
@foreach($scored_entries as $entry)
|
||||||
|
<tr id="entry-{{ $entry->id }}">
|
||||||
|
<x-table.td class="align-top">{{ $entry->seatingRank }}</x-table.td>
|
||||||
|
<x-table.td class="align-top">{{ $entry->id }}</x-table.td>
|
||||||
|
<x-table.td class="align-top">{{ $entry->draw_number }}</x-table.td>
|
||||||
|
<x-table.td class="align-top">
|
||||||
|
<div>
|
||||||
|
<a href="{{ route('admin.students.edit',[$entry->student_id]) }}">{{ $entry->student->full_name() }}</a>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-400">{{ $entry->student->school->name }}</div>
|
||||||
|
</x-table.td>
|
||||||
|
<x-table.td class="align-top">
|
||||||
|
@php($doubler = $doublerData->get($entry->student_id))
|
||||||
|
@if($doubler)
|
||||||
|
@if($doubler->accepted_entry == $entry->id)
|
||||||
|
ACCEPTED
|
||||||
|
@elseif($entry->hasFlag('declined'))
|
||||||
|
DECLINED
|
||||||
|
@else
|
||||||
|
@if($request = $entry->student->doublerRequests()->where('event_id',$entry->audition->event_id)->first())
|
||||||
|
<div
|
||||||
|
class="border-2 border-gray-200 p-2 m-2"> {{-- Begin block seating request --}}
|
||||||
|
<div class="font-semibold mb-2">
|
||||||
|
Request
|
||||||
|
</div>
|
||||||
|
<div class="text-wrap">
|
||||||
|
<p>{{ $request->request }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@endif
|
||||||
|
@foreach($entry->student->entriesForEvent($entry->audition->event_id) as $de)
|
||||||
|
@include('tabulation.auditionSeating-doubler-block')
|
||||||
|
@endforeach
|
||||||
|
@endif
|
||||||
|
@endif
|
||||||
|
|
||||||
|
|
||||||
|
</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>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</x-table.table>
|
||||||
|
</x-card.card>
|
||||||
|
|
||||||
|
<x-card.card class="mt-3"> {{-- Unscored Entries --}}
|
||||||
|
<x-card.heading class="-ml-3">Unscored Entries</x-card.heading>
|
||||||
|
<x-table.table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<x-table.th>Draw #</x-table.th>
|
||||||
|
<x-table.th>ID</x-table.th>
|
||||||
|
<x-table.th>Student</x-table.th>
|
||||||
|
<x-table.th></x-table.th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach($unscored_entries as $entry)
|
||||||
|
<tr>
|
||||||
|
<x-table.td>{{ $entry->draw_number }}</x-table.td>
|
||||||
|
<x-table.td>{{ $entry->id }}</x-table.td>
|
||||||
|
<x-table.td class="flex flex-col">
|
||||||
|
<span>{{ $entry->student->full_name() }}</span>
|
||||||
|
<span class="text-xs text-gray-400">{{ $entry->student->school->name }}</span>
|
||||||
|
</x-table.td>
|
||||||
|
<x-table.td>
|
||||||
|
<x-form.form method="POST"
|
||||||
|
action="{{ route('seating.audition.noshow',[$audition, $entry]) }}">
|
||||||
|
<x-form.button>Record No Show</x-form.button>
|
||||||
|
</x-form.form>
|
||||||
|
</x-table.td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</x-table.table>
|
||||||
|
</x-card.card>
|
||||||
|
|
||||||
|
<x-card.card class="mt-3"> {{-- No Show Entries --}}
|
||||||
|
<x-card.heading class="-ml-3">No Show Entries</x-card.heading>
|
||||||
|
<x-table.table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<x-table.th>Draw #</x-table.th>
|
||||||
|
<x-table.th>ID</x-table.th>
|
||||||
|
<x-table.th>Student</x-table.th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach($noshow_entries as $entry)
|
||||||
|
<tr>
|
||||||
|
<x-table.td>{{ $entry->draw_number }}</x-table.td>
|
||||||
|
<x-table.td>{{ $entry->id }}</x-table.td>
|
||||||
|
<x-table.td class="flex flex-col">
|
||||||
|
<span>{{ $entry->student->full_name() }}</span>
|
||||||
|
<span class="text-xs text-gray-400">{{ $entry->student->school->name }}</span>
|
||||||
|
</x-table.td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</x-table.table>
|
||||||
|
</x-card.card>
|
||||||
|
|
||||||
|
<x-card.card class="mt-3"> {{-- Failed Prelim Entries --}}
|
||||||
|
<x-card.heading class="-ml-3">Failed Prelim Entries</x-card.heading>
|
||||||
|
<x-table.table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<x-table.th>Draw #</x-table.th>
|
||||||
|
<x-table.th>ID</x-table.th>
|
||||||
|
<x-table.th>Student</x-table.th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach($failed_prelim_entries as $entry)
|
||||||
|
<tr>
|
||||||
|
<x-table.td>{{ $entry->draw_number }}</x-table.td>
|
||||||
|
<x-table.td>{{ $entry->id }}</x-table.td>
|
||||||
|
<x-table.td class="flex flex-col">
|
||||||
|
<span>{{ $entry->student->full_name() }}</span>
|
||||||
|
<span class="text-xs text-gray-400">{{ $entry->student->school->name }}</span>
|
||||||
|
</x-table.td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</x-table.table>
|
||||||
|
</x-card.card>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div> {{-- Right Column Wrapper --}}
|
||||||
|
@if($audition->hasFlag('seats_published'))
|
||||||
|
<x-card.card>
|
||||||
|
<x-card.heading>Published Results</x-card.heading>
|
||||||
|
<x-card.list.body>
|
||||||
|
@php($previousEnsemble = '')
|
||||||
|
@foreach($publishedSeats as $seat)
|
||||||
|
@if($previousEnsemble !== $seat->ensemble->name)
|
||||||
|
@php($previousEnsemble = $seat->ensemble->name)
|
||||||
|
<x-card.list.row class="font-semibold">{{ $seat->ensemble->name }}</x-card.list.row>
|
||||||
|
@endif
|
||||||
|
<x-card.list.row>
|
||||||
|
<div>
|
||||||
|
<p>{{ $seat->seat }}. {{ $seat->student->full_name() }}</p>
|
||||||
|
<p class="ml-5 text-xs">{{ $seat->student->school->name }}</p>
|
||||||
|
</div>
|
||||||
|
</x-card.list.row>
|
||||||
|
@endforeach
|
||||||
|
</x-card.list.body>
|
||||||
|
</x-card.card>
|
||||||
|
|
||||||
|
<x-form.form method="POST" action="{{ route('seating.audition.unpublishSeats',[$audition]) }}">
|
||||||
|
<x-form.button class="mt-3">Unpublish Results</x-form.button>
|
||||||
|
</x-form.form>
|
||||||
|
@else
|
||||||
|
@if($canSeat)
|
||||||
|
@if($seatingProposal)
|
||||||
|
<x-card.card>
|
||||||
|
<x-card.heading>
|
||||||
|
Seating Proposal
|
||||||
|
<x-slot:subheading>Results are not yet published</x-slot:subheading>
|
||||||
|
</x-card.heading>
|
||||||
|
@foreach($seatingProposal as $proposedEnsemble)
|
||||||
|
<h3 class="m-3 font-semibold">{{ $proposedEnsemble['ensemble_name'] }}</h3>
|
||||||
|
<x-card.list.body>
|
||||||
|
@if(isset($proposedEnsemble['seats']))
|
||||||
|
@foreach($proposedEnsemble['seats'] as $seat)
|
||||||
|
<x-card.list.row>{{ $seat['seat'] }}
|
||||||
|
. {{ $seat['entry_name'] }}</x-card.list.row>
|
||||||
|
@endforeach
|
||||||
|
@endif
|
||||||
|
</x-card.list.body>
|
||||||
|
@endforeach
|
||||||
|
<x-form.form method="POST" action="{{ route('seating.audition.clearDraft',[$audition]) }}">
|
||||||
|
<x-form.button class="mb-3">Clear Draft</x-form.button>
|
||||||
|
</x-form.form>
|
||||||
|
<x-form.form method="POST"
|
||||||
|
action="{{ route('seating.audition.publishSeats',[$audition]) }}">
|
||||||
|
<x-form.button class="mb-3">Publish</x-form.button>
|
||||||
|
</x-form.form>
|
||||||
|
</x-card.card>
|
||||||
|
@else
|
||||||
|
<x-card.card class="p-3">
|
||||||
|
<x-form.form metohd="POST" action="{{ route('seating.audition.draftSeats',[$audition]) }}">
|
||||||
|
<x-card.heading class="-ml-5">Seat Audition
|
||||||
|
<x-slot:subheading>Choose how many entries to seat in each ensemble
|
||||||
|
</x-slot:subheading>
|
||||||
|
</x-card.heading>
|
||||||
|
@foreach($audition->SeatingLimits()->where('maximum_accepted','>',0)->get() as $limit)
|
||||||
|
<x-form.select name="ensemble[{{ $limit->ensemble_id }}]">
|
||||||
|
<x-slot:label>{{$limit->ensemble->name}}</x-slot:label>
|
||||||
|
@for($n = 0; $n< $limit->maximum_accepted; $n++)
|
||||||
|
<option value="{{$n}}">{{$n}}</option>
|
||||||
|
@endfor
|
||||||
|
<option value="{{$n}}" SELECTED>{{$n}}</option>
|
||||||
|
</x-form.select>
|
||||||
|
@endforeach
|
||||||
|
<x-form.button class="mt-3">Draft Seats</x-form.button>
|
||||||
|
</x-form.form>
|
||||||
|
</x-card.card>
|
||||||
|
@endif
|
||||||
|
@else
|
||||||
<div class="ml-4">
|
<div class="ml-4">
|
||||||
@include($rightPanel['view'])
|
@if($unscored_entries->count() > 0)
|
||||||
|
<x-card.card class="p-3 text-red-500 mb-3">
|
||||||
|
Cannot seat the audition while entries are unscored.
|
||||||
|
</x-card.card>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if($auditionHasUnresolvedDoublers)
|
||||||
|
<x-card.card class="p-3">
|
||||||
|
<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>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
@endif
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
@inject('doublerService','App\Services\DoublerService')
|
||||||
|
@php
|
||||||
|
$blockSeating = []
|
||||||
|
@endphp
|
||||||
|
<x-layout.app>
|
||||||
|
<x-slot:page_title>Audition Seating - {{ $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.auditionSeating-results-table')
|
||||||
|
</div>
|
||||||
|
<div class="ml-4">
|
||||||
|
@include($rightPanel['view'])
|
||||||
|
</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>
|
||||||
|
|
@ -7,7 +7,6 @@ use App\Http\Controllers\Tabulation\DoublerDecisionController;
|
||||||
use App\Http\Controllers\Tabulation\EntryFlagController;
|
use App\Http\Controllers\Tabulation\EntryFlagController;
|
||||||
use App\Http\Controllers\Tabulation\ScoreController;
|
use App\Http\Controllers\Tabulation\ScoreController;
|
||||||
use App\Http\Controllers\Tabulation\SeatAuditionFormController;
|
use App\Http\Controllers\Tabulation\SeatAuditionFormController;
|
||||||
use App\Http\Controllers\Tabulation\SeatingPublicationController;
|
|
||||||
use App\Http\Controllers\Tabulation\SeatingStatusController;
|
use App\Http\Controllers\Tabulation\SeatingStatusController;
|
||||||
use App\Http\Middleware\CheckIfCanTab;
|
use App\Http\Middleware\CheckIfCanTab;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
@ -42,9 +41,17 @@ Route::middleware(['auth', 'verified', CheckIfCanTab::class])->group(function ()
|
||||||
// Seating Routes
|
// Seating Routes
|
||||||
Route::prefix('seating/')->group(function () {
|
Route::prefix('seating/')->group(function () {
|
||||||
Route::get('/', SeatingStatusController::class)->name('seating.status');
|
Route::get('/', SeatingStatusController::class)->name('seating.status');
|
||||||
Route::match(['get', 'post'], '/{audition}', SeatAuditionFormController::class)->name('seating.audition');
|
Route::get('/{audition}', [SeatAuditionFormController::class, 'showForm'])->name('seating.audition');
|
||||||
Route::post('/{audition}/publish', [SeatingPublicationController::class, 'publishSeats'])->name('seating.audition.publish');
|
Route::post('/{audition}/draftSeats', [SeatAuditionFormController::class, 'draftSeats'])->name('seating.audition.draftSeats');
|
||||||
Route::post('/{audition}/unpublish', [SeatingPublicationController::class, 'unpublishSeats'])->name('seating.audition.unpublish');
|
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',
|
||||||
|
[SeatAuditionFormController::class, 'publishSeats'])->name('seating.audition.publishSeats');
|
||||||
|
Route::post('/{audition}/unpublish',
|
||||||
|
[SeatAuditionFormController::class, 'unpublishSeats'])->name('seating.audition.unpublishSeats');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Advancement Routes
|
// Advancement Routes
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ require __DIR__.'/tabulation.php';
|
||||||
require __DIR__.'/user.php';
|
require __DIR__.'/user.php';
|
||||||
require __DIR__.'/nominationEnsemble.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('/home', 'welcome')->middleware('guest')->name('landing');
|
||||||
Route::view('/', 'landing')->name('home');
|
Route::view('/', 'landing')->name('home');
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue