Compare commits

..

80 Commits

Author SHA1 Message Date
Matt Young 80d7bc3ebe Fix issue with year end resets. 2025-12-13 14:52:11 -06:00
Matt Young 38d7826218 catch exception on judging controller 2025-12-13 11:04:56 -06:00
Matt Young 59629e227d correct missing template info 2025-12-08 20:24:07 -06:00
Matt Young 755f8bdf4a correct testing issue. Monitor controller needs proper tests written. 2025-12-08 15:58:24 -06:00
Matt Young bf502f4cbb fix duplicate route name 2025-12-08 15:49:44 -06:00
Matt Young 2ffe14e43c fix duplicate route nqame 2025-12-08 11:30:18 -06:00
Matt Young d55be47f41 Update logging config. 2025-12-08 10:56:41 -06:00
Matt Young 1c3bb39805 Add console command to force recalculation of judge totals 2025-11-20 11:21:17 -06:00
Matt Young 55d5dba840 Fix error where a modified subscore would not count for seating if there is no advancement. 2025-11-20 10:37:45 -06:00
Matt Young a5b203af2e Allow printing of blank sheet of cards 2025-11-16 15:29:33 -06:00
Matt Young 5bbcccdc22 update vite config 2025-11-16 15:23:42 -06:00
Matt Young a9551a1dd6 update packages 2025-11-09 16:34:32 -06:00
Matt Young 7d94ee2cfb add page listing school email domains. 2025-11-09 15:47:08 -06:00
Matt Young 87e3ec322d Quickfix dealing with a student incorrectly identifying as a doubler when not entered for seating in one audition but only advancement 2025-11-08 17:16:21 -06:00
Matt Young 6f657415aa Fix issue with seating including advance only entries. 2025-11-08 15:55:26 -06:00
Matt Young 67ceae6f01 Fix issue in advancement - ignore seating only 2025-11-08 13:43:08 -06:00
Matt Young a59217db41 Fix error in card printing 2025-11-06 17:22:40 -06:00
Matt Young 402cbf8c83 Show what a student is auditioning for on their cards if advancement audition 2025-11-06 07:30:09 -06:00
Matt Young be621606e2 Show doublers on cards. 2025-11-05 06:55:12 -06:00
Matt Young 59c5ae8526 Show doublers on sign in sheets. 2025-11-05 06:48:52 -06:00
Matt Young 7347059d96 only bill for students entered for seating. 2025-11-05 06:40:39 -06:00
Matt Young 834de902ac Update results on directors page to add explaination. 2025-11-03 09:02:41 -06:00
Matt Young 165d2c9f6c Fixed issue with results output 2025-11-03 07:13:59 -06:00
Matt Young 3208b31524 Undo prelim score on results page 2025-11-01 15:41:59 -05:00
Matt Young b8a4cf5f39 Undo prelim score on results page 2025-11-01 13:44:46 -05:00
Matt Young 67622ec0c9 Undo prelim score on results page 2025-11-01 12:50:37 -05:00
Matt Young fb77923812 Show prelim score on seating page. 2025-11-01 12:45:02 -05:00
Matt Young bfee058078 Show prelim score on seating page. 2025-11-01 12:41:53 -05:00
Matt Young 10a4d1a140 Add abilitly for admin to manually set password for users 2025-10-28 11:35:30 -05:00
Matt Young 2dfb745861 Add artisan commands to import entries from a CSV file 2025-10-27 22:59:54 -05:00
Matt Young 3315efc83b Allow bulk updating of auditions
Closes #31
2025-10-27 17:31:18 -05:00
Matt Young 4548be098a Allow admins to rename events.
Closes #114
2025-10-26 17:46:55 -05:00
Matt Young 1acb286ac8 Allow the modification of Bonus Score definition data.
Closes #100
Closes #58
2025-10-25 22:53:21 -05:00
Matt Young 6ca05bf4d5 Allow admin to set users to have no school.
Fixes #112
2025-10-25 22:21:10 -05:00
Matt Young 550614a317 Show an error when an administrator attempts to duplicate an existing entry. 2025-10-25 22:06:09 -05:00
Matt Young a5f11fb897 Fix error in doubler request page. 2025-10-24 08:32:48 -05:00
Matt Young 69be2b7ed0 Fix error in doubler request page. 2025-10-23 09:09:15 -05:00
Matt Young 956d70a90e Correct issue where a subscore could not be created for seating only. 2025-10-22 19:21:35 -05:00
Matt Young 0ca239d297 Merge branch 'refs/heads/pass_fail_prelims' 2025-10-20 22:33:34 -05:00
Matt Young aa967c317b Update fictionalize command 2025-10-20 12:51:52 -05:00
Matt Young 1af9715682 Show related log entries on admin pages. 2025-10-20 01:49:06 -05:00
Matt Young 0307fbc595 Block entry of a prelim score for an entry with any finals scores. 2025-10-20 01:11:04 -05:00
Matt Young 30cbaf69f8 Allow admin and tabulators to enter and modify prelim scores. 2025-10-20 01:01:27 -05:00
Matt Young 62a3694c03 Enhancement to monitor pages. 2025-10-19 08:36:37 -05:00
Matt Young 3fb3f8b3df Enhancement to monitor pages. 2025-10-19 08:20:34 -05:00
Matt Young 1041d7c96b Enhancement to monitor pages. 2025-10-19 08:14:01 -05:00
Matt Young ee958d350d Enhancement to monitor pages. 2025-10-18 22:31:53 -05:00
Matt Young 40363a5964 Allow editing of scores by prelims judges. 2025-10-18 15:04:12 -05:00
Matt Young b978966c98 Minimize information shown on monitor screen. Allow monitors to enter no-shows for prelim auditions. 2025-10-18 14:04:04 -05:00
Matt Young 3e3b99c56c Update look of monitor page 2025-10-16 14:39:54 -05:00
Matt Young 70f79d031c Fix issue where a scoring guide subscore could not be created if advancement was not enabled. 2025-10-16 11:53:15 -05:00
Matt Young 31d56e5b90 Show failed prelim scores on seating page 2025-10-15 14:02:41 -05:00
Matt Young b2d66eb1b8 If an audition has a prelim, only show finals judges entries that have passed prelims. 2025-10-14 18:20:09 -05:00
Matt Young 982dfa46a0 Rehash monitor page to deal with prelims 2025-10-14 17:35:14 -05:00
Matt Young add9f9e25d Check for a prelim result after entering a prelim score. 2025-10-14 09:12:14 -05:00
Matt Young 761f63aa55 Added CheckPrelimResult action to check if an entry passed it's prelim audition and make the appropriate flag on the entry. 2025-10-13 22:13:11 -05:00
Matt Young ccd206c2af remove depricasted code 2025-10-11 20:36:58 -05:00
Matt Young 011900461a Show previously entered prelim scores on entry list. 2025-10-09 18:58:26 -05:00
Matt Young 0e4b8acce6 Judge prelim entry scores functioning. 2025-10-08 21:50:02 -05:00
Matt Young ca80260bda Action to enter prelim score sheet implemented. 2025-10-08 07:32:59 -05:00
Matt Young 83eff8feee Preliminary work on PrelimJudging entry list. 2025-10-07 21:11:26 -05:00
Matt Young 07f3f37be4 Merge branch 'master' into pass_fail_prelims
* master:
  Correct issue with testing.
2025-10-02 21:57:55 -05:00
Matt Young 87046bb736 Correct issue with testing. 2025-10-02 21:57:31 -05:00
Matt Young 3b6fbc16f1 Merge branch 'master' into pass_fail_prelims
* master:
  Fix issue where entry observer was taking too long during draw. No need to update doublers just to run the draw.
  Fix issue where directors could add students down to first grade if no nomination ensembles were defined.
2025-10-02 21:06:23 -05:00
Matt Young 8a2b2256cf Fix issue where entry observer was taking too long during draw. No need to update doublers just to run the draw. 2025-10-02 21:02:27 -05:00
Matt Young 14b275aa7e Work on auth for PrelimJudging 2025-10-02 20:43:22 -05:00
Matt Young 88608ea5b4 Test for prelim auditions showing on judging dashboard. 2025-09-22 22:13:48 -05:00
Matt Young 49b203cc25 Test for prelim auditions showing on judging dashboard. 2025-09-22 21:21:32 -05:00
Matt Young 23442ad740 prelim score sheet model and migration 2025-09-22 21:15:32 -05:00
Matt Young 2b39ea9a88 Setup PrelimJudgingController.php 2025-09-22 21:05:10 -05:00
Matt Young a609c9d627 Show assigned prelim auditions on judging dashboard 2025-09-22 20:59:32 -05:00
Matt Young 2418873af0 Ease the process of assigning prelim auditions to rooms. 2025-09-22 19:27:18 -05:00
Matt Young 81b10220d6 Set up logging for PrelimDefinitions 2025-09-14 21:19:41 -05:00
Matt Young 674374b6b6 Add ability to delete prelim auditions. Need to add an observer to log. 2025-09-13 22:59:04 -05:00
Matt Young cafa1ddf29 Add prelim auditions to the menu. 2025-09-13 22:17:38 -05:00
Matt Young b7b5d0fc94 Most basic management function of prelim definitions done. Need to add delete method and listener for logging next. 2025-09-11 23:02:37 -05:00
Matt Young 8f41af74f9 Fix issue where directors could add students down to first grade if no nomination ensembles were defined. 2025-09-11 17:59:37 -05:00
Matt Young 352897fa25 development on management of prelims entries 2025-09-11 16:53:03 -05:00
Matt Young 7c0504ea89 prelim definition relationship tests. 2025-09-11 10:04:11 -05:00
Matt Young a7d1776c44 Create model and migration for prelim definitions 2025-09-11 09:25:33 -05:00
109 changed files with 3881 additions and 607 deletions

View File

@ -78,11 +78,15 @@ class PrintSignInSheets
public function addEntryRow(Entry $entry)
{
$nameLine = $entry->student->full_name();
if ($entry->student->isDoublerInEvent($entry->audition->event_id)) {
$nameLine .= ' (D)';
}
$this->pdf->Cell($this->columnWidth['id'], $this->bodyRowHeight, $entry->id, 1, 0, 'L');
$this->pdf->Cell($this->columnWidth['instrument'], $this->bodyRowHeight, $entry->audition->name, 1, 0,
'L');
$this->pdf->Cell($this->columnWidth['drawNumber'], $this->bodyRowHeight, $entry->draw_number, 1, 0, 'L');
$this->pdf->Cell($this->columnWidth['name'], $this->bodyRowHeight, $entry->student->full_name(), 1, 0, 'L');
$this->pdf->Cell($this->columnWidth['name'], $this->bodyRowHeight, $nameLine, 1, 0, 'L');
$this->pdf->Cell($this->columnWidth['school'], $this->bodyRowHeight, $entry->student->school->name, 1, 0, 'L');
$this->pdf->Cell(0, $this->bodyRowHeight, ' ', 1, 1, 'L');
}

View File

@ -58,17 +58,37 @@ class QuarterPageCards implements PrintCards
$this->pdf->Cell(4.5, .5, $entry->audition->name.' #'.$entry->draw_number);
// Fill in student information
$nameLine = $entry->student->full_name();
if ($entry->student->isDoublerInEvent($entry->audition->event_id)) {
$nameLine .= ' (D)';
}
$this->pdf->SetFont('Arial', '', 10);
$xLoc = $this->offset[$this->quadOn][0] + 1;
$yLoc = $this->offset[$this->quadOn][1] + 3.1;
$this->pdf->setXY($xLoc, $yLoc);
$this->pdf->Cell(4.5, .25, $entry->student->full_name());
$this->pdf->Cell(4.5, .25, $nameLine);
$this->pdf->setXY($xLoc, $yLoc + .25);
$this->pdf->Cell(4.5, .25, $entry->student->school->name);
$this->pdf->setXY($xLoc, $yLoc + .5);
if (! is_null($entry->audition->room_id)) {
$this->pdf->Cell(4.5, .25, $entry->audition->room->name);
}
if (auditionSetting('advanceTo')) {
$as = false;
$this->pdf->setXY($xLoc, $yLoc - 1);
$auditioningFor = 'Auditioning for: ';
if ($entry->for_seating) {
$auditioningFor .= auditionSetting('auditionAbbreviation');
$as = true;
}
if ($entry->for_advancement) {
if ($as) {
$auditioningFor .= ' / ';
}
$auditioningFor .= auditionSetting('advanceTo');
}
$this->pdf->Cell(4.5, .25, $auditioningFor);
}
$this->quadOn++;
}

View File

@ -33,7 +33,7 @@ class GetExportData
foreach ($events as $event) {
$auditions = $event->auditions;
foreach ($auditions as $audition) {
$entries = $ranker->rank('seating', $audition);
$entries = $ranker($audition, 'seating');
foreach ($entries as $entry) {
$thisRow = $audition->name.',';
$thisRow .= $entry->raw_rank ?? '';
@ -41,7 +41,7 @@ class GetExportData
$thisRow .= $entry->student->full_name().',';
$thisRow .= $entry->student->school->name.',';
$thisRow .= $entry->student->grade.',';
$thisRow .= $entry->score_totals[0] ?? '';
$thisRow .= $entry->totalScore->seating_total ?? '';
$thisRow .= ',';
if ($entry->hasFlag('failed_prelim')) {
$thisRow .= 'Failed Prelim';

View File

@ -8,19 +8,27 @@ use App\Models\User;
class AssignUserToSchool
{
public function __invoke(User $user, School|int $school): void
public function __invoke(User $user, School|int|null $school): void
{
$this->assign($user, $school);
}
public function assign(User $user, School|int $school, bool $addDomainToSchool = true): void
public function assign(User $user, School|int|null $school, bool $addDomainToSchool = true): void
{
if (! User::where('id', $user->id)->exists()) {
throw new AuditionAdminException('User does not exist');
}
if (is_int($school)) {
$school = School::find($school);
}
if (! User::where('id', $user->id)->exists()) {
throw new AuditionAdminException('User does not exist');
if (is_null($school)) {
$user->update([
'school_id' => null,
]);
return;
}
if (is_null($school) || ! School::where('id', $school->id)->exists()) {

View File

@ -0,0 +1,61 @@
<?php
namespace App\Actions\Tabulation;
use App\Exceptions\AuditionAdminException;
use App\Models\Entry;
class CheckPrelimResult
{
public function __construct()
{
}
/**
* @throws AuditionAdminException
*/
public function __invoke(Entry $entry, bool $recalc = false): string
{
if ($recalc) {
$entry->removeFlag('passed_prelim');
$entry->removeFlag('failed_prelim');
}
if (! $entry->exists) {
throw new AuditionAdminException('Entry does not exist');
}
if (! $entry->audition->prelimDefinition) {
throw new AuditionAdminException('Entry does not have a prelim');
}
if ($entry->hasFlag('failed_prelim') || $entry->hasFlag('passed_prelim')) {
return 'noChange';
}
if (! $entry->audition->prelimDefinition->room || $entry->audition->prelimDefinition->room->judges()->count() == 0) {
return 'noJudgesAssigned';
}
$scoresRequired = $entry->audition->prelimDefinition->room->judges()->count();
$scoresAssigned = $entry->prelimScoreSheets()->count();
if ($scoresAssigned < $scoresRequired) {
return 'missing'.$scoresRequired - $scoresAssigned.'scores';
}
$totalScore = 0;
foreach ($entry->prelimScoreSheets as $scoreSheet) {
$totalScore += $scoreSheet->total;
}
$averageScore = $totalScore / $scoresAssigned;
if ($averageScore >= $entry->audition->prelimDefinition->passing_score) {
$entry->addFlag('passed_prelim');
return 'markedPassed';
} else {
$entry->addFlag('failed_prelim');
return 'markedFailed';
}
}
}

View File

@ -0,0 +1,137 @@
<?php
namespace App\Actions\Tabulation;
use App\Exceptions\AuditionAdminException;
use App\Models\Entry;
use App\Models\PrelimDefinition;
use App\Models\PrelimScoreSheet;
use App\Models\User;
use DB;
use function auditionLog;
class EnterPrelimScore
{
public function __invoke(
User $user,
Entry $entry,
array $scores,
PrelimScoreSheet|false $prelimScoreSheet = false
): PrelimScoreSheet {
$scores = collect($scores);
// Basic Validity Checks
if (! User::where('id', $user->id)->exists()) {
throw new AuditionAdminException('User does not exist');
}
if (! Entry::where('id', $entry->id)->exists()) {
throw new AuditionAdminException('Entry does not exist');
}
if ($entry->audition->hasFlag('seats_published')) {
throw new AuditionAdminException('Cannot score an entry in an audition where seats are published');
}
// Check if the entries audition has a prelim definition
if (! PrelimDefinition::where('audition_id', $entry->audition->id)->exists()) {
throw new AuditionAdminException('The entries audition does not have a prelim');
}
$prelimDefinition = PrelimDefinition::where('audition_id', $entry->audition->id)->first();
// Don't allow changes to prelims scores if the entry has a finals score
if ($entry->scoreSheets()->count() > 0) {
throw new AuditionAdminException('Cannot change prelims scores for an entry that has finals scores');
}
// Check that the specified user is assigned to judge this entry in prelims
$check = DB::table('room_user')
->where('user_id', $user->id)
->where('room_id', $prelimDefinition->room_id)->exists();
if (! $check) {
throw new AuditionAdminException('This judge is not assigned to judge this entry in prelims');
}
// Check if a score already exists
if (! $prelimScoreSheet) {
if (PrelimScoreSheet::where('user_id', $user->id)->where('entry_id', $entry->id)->exists()) {
throw new AuditionAdminException('That judge has already entered a prelim score for that entry');
}
} else {
if ($prelimScoreSheet->user_id != $user->id) {
throw new AuditionAdminException('Existing score sheet is from a different judge');
}
if ($prelimScoreSheet->entry_id != $entry->id) {
throw new AuditionAdminException('Existing score sheet is for a different entry');
}
}
// Check the validity of submitted subscores, format array for storage, and sum score
$subscoresRequired = $prelimDefinition->scoringGuide->subscores;
$subscoresStorageArray = [];
$totalScore = 0;
$maxPossibleTotal = 0;
if ($scores->count() !== $subscoresRequired->count()) {
throw new AuditionAdminException('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 AuditionAdminException('Invalid Score Submission');
}
if ($scores[$subscore->id] > $subscore->max_score) {
throw new AuditionAdminException('Supplied subscore exceeds maximum allowed');
}
// Add subscore to the storage array
$subscoresStorageArray[$subscore->id] = [
'score' => $scores[$subscore->id],
'subscore_id' => $subscore->id,
'subscore_name' => $subscore->name,
];
// Multiply subscore by weight then add to total
$totalScore += ($subscore->weight * $scores[$subscore->id]);
$maxPossibleTotal += ($subscore->weight * $subscore->max_score);
}
$finalTotalScore = ($maxPossibleTotal === 0) ? 0 : (($totalScore / $maxPossibleTotal) * 100);
$entry->removeFlag('no_show');
if ($prelimScoreSheet instanceof PrelimScoreSheet) {
$prelimScoreSheet->update([
'user_id' => $user->id,
'entry_id' => $entry->id,
'subscores' => $subscoresStorageArray,
'total' => $finalTotalScore,
]);
} else {
$prelimScoreSheet = PrelimScoreSheet::create([
'user_id' => $user->id,
'entry_id' => $entry->id,
'subscores' => $subscoresStorageArray,
'total' => $finalTotalScore,
]);
}
// Log the prelim score entry
$log_message = 'Entered prelim score for entry id '.$entry->id.'.<br />';
$log_message .= 'Judge: '.$user->full_name().'<br />';
foreach ($prelimScoreSheet->subscores as $subscore) {
$log_message .= $subscore['subscore_name'].': '.$subscore['score'].'<br />';
}
$log_message .= 'Total :'.$prelimScoreSheet->total.'<br />';
auditionLog($log_message, [
'entries' => [$entry->id],
'users' => [$user->id],
'auditions' => [$entry->audition_id],
'students' => [$entry->student_id],
]);
// Check if we can make a status decision
$checker = app(CheckPrelimResult::class);
$checker($entry, true);
return $prelimScoreSheet;
}
}

View File

@ -24,7 +24,7 @@ class RankAuditionEntries
*
* @throws AuditionAdminException
*/
public function __invoke(Audition $audition, string $rank_type): Collection|Entry
public function __invoke(Audition $audition, string $rank_type, bool $pullDeclinedEntries = true): Collection|Entry
{
if ($rank_type !== 'seating' && $rank_type !== 'advancement') {
throw new AuditionAdminException('Invalid rank type (must be seating or advancement)');
@ -33,8 +33,8 @@ class RankAuditionEntries
$cache_duration = 15;
if ($rank_type === 'seating') {
return cache()->remember('rank_seating_'.$audition->id, $cache_duration, function () use ($audition) {
return $this->get_seating_ranks($audition);
return cache()->remember('rank_seating_'.$audition->id, $cache_duration, function () use ($audition, $pullDeclinedEntries) {
return $this->get_seating_ranks($audition, $pullDeclinedEntries);
});
}
@ -44,7 +44,7 @@ class RankAuditionEntries
}
private function get_seating_ranks(Audition $audition): Collection|Entry
private function get_seating_ranks(Audition $audition, bool $pullDeclinedEntries = true): Collection|Entry
{
if ($audition->bonusScore()->count() > 0) {
$totalColumn = 'seating_total_with_bonus';
@ -53,6 +53,7 @@ class RankAuditionEntries
}
$sortedEntries = $audition->entries()
->where('for_seating', true)
->whereHas('totalScore')
->with('totalScore')
->with('student.school')
@ -74,7 +75,7 @@ class RankAuditionEntries
$rankOn = 1;
foreach ($sortedEntries as $entry) {
if ($entry->hasFlag('declined')) {
if ($entry->hasFlag('declined') && $pullDeclinedEntries) {
$entry->seatingRank = 'declined';
} else {
$entry->seatingRank = $rankOn;

View File

@ -65,14 +65,14 @@ class YearEndCleanup
if (is_array($options)) {
if (in_array('deleteRooms', $options)) {
DB::table('auditions')->update(['room_id' => null]);
DB::table('auditions')->update(['room_id' => 0]);
DB::table('auditions')->update(['order_in_room' => '0']);
DB::table('room_user')->truncate();
DB::table('rooms')->delete();
DB::table('rooms')->where('id', '>', 0)->delete();
}
if (in_array('removeAuditionsFromRoom', $options)) {
DB::table('auditions')->update(['room_id' => null]);
DB::table('auditions')->update(['room_id' => 0]);
DB::table('auditions')->update(['order_in_room' => '0']);
}

View File

@ -0,0 +1,41 @@
<?php
namespace App\Console\Commands;
use App\Actions\Tabulation\EnterScore;
use App\Models\ScoreSheet;
use Illuminate\Console\Command;
class RecalculateJudgeTotalsCommand extends Command
{
protected $signature = 'audition:recalculate-judge-totals';
protected $description = 'Recalculates total scores for all score sheets for unpubished auditions';
public function handle(): void
{
$this->info('Starting score recalculation...');
$scoreSheets = ScoreSheet::all();
foreach ($scoreSheets as $scoreSheet) {
if ($scoreSheet->entry->audition->hasFlag('seats_published')) {
continue;
}
$this->recalculate($scoreSheet);
}
$this->info('Score recalculation completed successfully.');
}
private function recalculate(ScoreSheet|int $scoreSheet): void
{
if (is_int($scoreSheet)) {
$scoreSheet = ScoreSheet::findOrFail($scoreSheet);
}
$scribe = app()->make(EnterScore::class);
$scoreSubmission = [];
foreach ($scoreSheet->subscores as $subscore) {
$scoreSubmission[$subscore['subscore_id']] = $subscore['score'];
}
$scribe($scoreSheet->judge, $scoreSheet->entry, $scoreSubmission, $scoreSheet);
}
}

View File

@ -8,14 +8,14 @@ use Illuminate\Console\Command;
/**
* @codeCoverageIgnore
*/
class RecalculateScores extends Command
class RecalculateTotalScores extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'audition:recalculate-scores';
protected $signature = 'audition:recalculate-total-scores';
/**
* The console command description.

View File

@ -8,48 +8,80 @@ use App\Models\User;
use Faker\Factory;
use Illuminate\Console\Command;
/**
* @codeCoverageIgnore
*/
class fictionalize extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'audition:fictionalize';
protected $signature = 'audition:fictionalize
{--students : Fictionalize student names}
{--schools : Fictionalize school names}
{--users : Fictionalize user data}
{--all : Fictionalize all data types}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Command description';
protected $description = 'Replace real names with fictional data for specified entity types';
/**
* 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();
// If no options are specified or --all is used, process everything
$processAll = $this->option('all') ||
(! $this->option('students') && ! $this->option('schools') && ! $this->option('users'));
if ($processAll || $this->option('students')) {
$this->info('Fictionalizing students...');
$bar = $this->output->createProgressBar(Student::count());
Student::chunk(100, function ($students) use ($faker, $bar) {
foreach ($students as $student) {
$student->update([
'first_name' => $faker->firstName(),
'last_name' => $faker->lastName(),
]);
$bar->advance();
}
});
$bar->finish();
$this->newLine();
}
foreach (School::all() as $school) {
$school->name = $faker->city().' High School';
$school->save();
if ($processAll || $this->option('schools')) {
$this->info('Fictionalizing schools...');
$bar = $this->output->createProgressBar(School::count());
School::chunk(100, function ($schools) use ($faker, $bar) {
foreach ($schools as $school) {
$school->update([
'name' => $faker->city().' High School',
]);
$bar->advance();
}
});
$bar->finish();
$this->newLine();
}
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();
if ($processAll || $this->option('users')) {
$this->info('Fictionalizing users...');
$bar = $this->output->createProgressBar(User::where('email', '!=', 'matt@mattyoung.us')->count());
User::where('email', '!=', 'matt@mattyoung.us')
->chunk(100, function ($users) use ($faker, $bar) {
foreach ($users as $user) {
$user->update([
'email' => $faker->unique()->email(),
'first_name' => $faker->firstName(),
'last_name' => $faker->lastName(),
'cell_phone' => $faker->phoneNumber(),
]);
$bar->advance();
}
});
$bar->finish();
$this->newLine();
}
$this->info('Fictionalization complete!');
}
}

View File

@ -0,0 +1,124 @@
<?php
namespace App\Console\Commands;
use App\Models\Audition;
use App\Models\Event;
use App\Models\Room;
use App\Models\ScoringGuide;
use App\Services\CsvImportService;
use Carbon\Carbon;
use Illuminate\Console\Command;
use function auditionSetting;
use function Laravel\Prompts\select;
class importCheckAuditionsCommand extends Command
{
protected $signature = 'import:check-auditions';
protected $description = 'Check the import file for auditions that do not exist in the database';
protected $csvImporter;
public function __construct(CsvImportService $csvImporter)
{
parent::__construct();
$this->csvImporter = $csvImporter;
}
public function handle(): void
{
$lowestPossibleGrade = 1;
$highestPossibleGrade = 12;
$events = Event::all();
$rows = $this->csvImporter->readCsv(storage_path('app/import/import.csv'));
$checkedAuditions = collect();
foreach ($rows as $row) {
if ($checkedAuditions->contains($row['Instrument'])) {
continue;
}
$checkedAuditions->push($row['Instrument']);
if (Audition::where('name', $row['Instrument'])->count() > 0) {
$this->info('Audition '.$row['Instrument'].' already exists');
} else {
$this->newLine();
$this->alert('Audition '.$row['Instrument'].' does not exist');
if ($events->count() === 1) {
$newEventId = $events->first()->id;
} else {
$newEventId = select(
label: 'Which event does this audition belong to?',
options: $events->pluck('name', 'id')->toArray(),
);
}
$newEventName = $row['Instrument'];
$newEventScoreOrder = Audition::max('score_order') + 1;
$newEventEntryDeadline = Carbon::yesterday('America/Chicago')->format('Y-m-d');
$newEventEntryFee = Audition::max('entry_fee');
$newEventMinimumGrade = select(
label: 'What is the minimum grade for this audition?',
options: range($lowestPossibleGrade, $highestPossibleGrade)
);
$newEventMaximumGrade = select(
label: 'What is the maximum grade for this audition?',
options: range($newEventMinimumGrade, $highestPossibleGrade)
);
$newEventRoomId = select(
label: 'Which room does this audition belong to?',
options: Room::pluck('name', 'id')->toArray(),
);
$newEventScoringGuideId = select(
label: 'Which scoring guide should this audition use',
options: ScoringGuide::pluck('name', 'id')->toArray(),
);
if (auditionSetting('advanceTo')) {
$newEventForSeating = select(
label: 'Is this audition for seating?',
options: [
1 => 'Yes',
0 => 'No',
]
);
$newEventForAdvance = select(
label: 'Is this audition for '.auditionSetting('advanceTo').'?',
options: [
1 => 'Yes',
0 => 'No',
]
);
} else {
$newEventForSeating = 1;
$newEventForAdvance = 0;
}
$this->info('New event ID: '.$newEventId);
$this->info('New event name: '.$newEventName);
$this->info('New event score order: '.$newEventScoreOrder);
$this->info('New event entry deadline: '.$newEventEntryDeadline);
$this->info('New event entry fee: '.$newEventEntryFee);
$this->info('New event minimum grade: '.$newEventMinimumGrade);
$this->info('New event maximum grade: '.$newEventMaximumGrade);
$this->info('New event room ID: '.$newEventRoomId);
$this->info('New event scoring guide ID: '.$newEventScoringGuideId);
$this->info('New event for seating: '.$newEventForSeating);
$this->info('New event for advance: '.$newEventForAdvance);
Audition::create([
'event_id' => $newEventId,
'name' => $newEventName,
'score_order' => $newEventScoreOrder,
'entry_deadline' => $newEventEntryDeadline,
'entry_fee' => $newEventEntryFee,
'minimum_grade' => $newEventMinimumGrade,
'maximum_grade' => $newEventMaximumGrade,
'room_id' => $newEventRoomId,
'scoring_guide_id' => $newEventScoringGuideId,
'for_seating' => $newEventForSeating,
'for_advancement' => $newEventForAdvance,
]);
}
}
}
}

View File

@ -0,0 +1,44 @@
<?php
namespace App\Console\Commands;
use const PHP_EOL;
use App\Models\School;
use App\Services\CsvImportService;
use Illuminate\Console\Command;
class importCheckSchoolsCommand extends Command
{
protected $signature = 'import:check-schools';
protected $description = 'Check the import file for schools that do not exist in the database';
protected $csvImporter;
public function __construct(CsvImportService $csvImporter)
{
parent::__construct();
$this->csvImporter = $csvImporter;
}
public function handle(): void
{
$rows = $this->csvImporter->readCsv(storage_path('app/import/import.csv'));
$checkedSchools = collect();
foreach ($rows as $row) {
if ($checkedSchools->contains($row['School'])) {
continue;
}
$checkedSchools->push($row['School']);
if (School::where('name', $row['School'])->count() > 0) {
$this->info('School '.$row['School'].' already exists');
} else {
$this->newLine();
$this->alert('School '.$row['School'].' does not exist'.PHP_EOL.'Creating school...');
School::create(['name' => $row['School']]);
$this->info('School '.$row['School'].' created');
}
}
}
}

View File

@ -0,0 +1,67 @@
<?php
namespace App\Console\Commands;
use App\Models\Entry;
use App\Models\School;
use App\Models\Student;
use App\Services\CsvImportService;
use Illuminate\Console\Command;
class importCheckStudentsCommand extends Command
{
protected $signature = 'import:check-students';
protected $description = 'Check the import file for students that do not exist in the database';
protected $csvImporter;
public function __construct(CsvImportService $csvImporter)
{
parent::__construct();
$this->csvImporter = $csvImporter;
}
public function handle(): void
{
$purge = $this->confirm('Do you want to purge the database of existing students and entries?', false);
if ($purge) {
Entry::all()->map(function ($entry) {
$entry->delete();
});
Student::all()->map(function ($student) {
$student->delete();
});
$this->info('Database purged');
}
$schools = School::pluck('id', 'name');
$rows = $this->csvImporter->readCsv(storage_path('app/import/import.csv'));
$checkedStudents = collect();
foreach ($rows as $row) {
$uniqueData = $row['School'].$row['LastName'].$row['LastName'];
if ($checkedStudents->contains($uniqueData)) {
// continue;
}
$checkedStudents->push($uniqueData);
$currentFirstName = $row['FirstName'];
$currentLastName = $row['LastName'];
$currentSchoolName = $row['School'];
$currentSchoolId = $schools[$currentSchoolName];
if (Student::where('first_name', $currentFirstName)->where('last_name',
$currentLastName)->where('school_id', $currentSchoolId)->count() > 0) {
$this->info('Student '.$currentFirstName.' '.$currentLastName.' from '.$currentSchoolName.' already exists');
} else {
$this->alert('Student '.$currentFirstName.' '.$currentLastName.' from '.$currentSchoolName.' does not exist');
$newStudent = Student::create([
'school_id' => $currentSchoolId,
'first_name' => $currentFirstName,
'last_name' => $currentLastName,
'grade' => $row['Grade'],
]);
$this->info('Student '.$currentFirstName.' '.$currentLastName.' from '.$currentSchoolName.' created with id of: '.$newStudent->id);
}
}
}
}

View File

@ -0,0 +1,74 @@
<?php
namespace App\Console\Commands;
use App\Models\Audition;
use App\Models\Entry;
use App\Models\School;
use App\Models\Student;
use App\Services\CsvImportService;
use Illuminate\Console\Command;
class importImportEntriesCommand extends Command
{
protected $signature = 'import';
protected $description = 'Import entries from the import.csv file. First check schools, then students, then auditions, then run this import command';
protected $csvImporter;
public function __construct(CsvImportService $csvImporter)
{
parent::__construct();
$this->csvImporter = $csvImporter;
}
public function handle(): void
{
$checkAuditions = $this->confirm('Do you want to check the auditions in the import for validity first?', true);
if ($checkAuditions) {
$this->call('import:check-auditions');
}
$checkSchools = $this->confirm('Do you want to check the schools in the import for validity first?', true);
if ($checkSchools) {
$this->call('import:check-schools');
}
$checkStudents = $this->confirm('Do you want to check the students in the import for validity first?', true);
if ($checkStudents) {
$this->call('import:check-students');
}
$purge = $this->confirm('Do you want to purge the database of existing entries?', false);
if ($purge) {
Entry::all()->map(function ($entry) {
$entry->delete();
});
$this->info('Database purged');
}
$schools = School::pluck('id', 'name');
$auditions = Audition::pluck('id', 'name');
$rows = $this->csvImporter->readCsv(storage_path('app/import/import.csv'));
foreach ($rows as $row) {
$schoolId = $schools[$row['School']];
$student = Student::where('first_name', $row['FirstName'])->where('last_name',
$row['LastName'])->where('school_id', $schoolId)->first();
if (! $student) {
$this->error('Student '.$row['FirstName'].' '.$row['LastName'].' from '.$row['School'].' does not exist');
return;
}
$auditionId = $auditions[$row['Instrument']];
try {
Entry::create([
'student_id' => $student->id,
'audition_id' => $auditionId,
]);
} catch (\Exception $e) {
$this->warn('Entry already exists for student '.$student->full_name().' in audition '.$row['Instrument']);
}
$this->info('Entry created for student '.$student->full_name().' in audition '.$row['Instrument']);
}
}
}

View File

@ -8,5 +8,6 @@ enum EntryFlags: string
case DECLINED = 'declined';
case NO_SHOW = 'no_show';
case FAILED_PRELIM = 'failed_prelim';
case PASSED_PRELIM = 'passed_prelim';
case LATE_FEE_WAIVED = 'late_fee_waived';
}

View File

@ -4,12 +4,14 @@ namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Http\Requests\AuditionStoreOrUpdateRequest;
use App\Http\Requests\BulkAuditionEditRequest;
use App\Models\Audition;
use App\Models\Event;
use App\Models\Room;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use function compact;
use function redirect;
use function request;
use function response;
@ -84,6 +86,57 @@ class AuditionController extends Controller
return to_route('admin.auditions.index')->with('success', 'Audition updated successfully');
}
public function bulkEditForm()
{
$auditions = Audition::with(['event'])->withCount('entries')->orderBy('score_order')->orderBy('created_at',
'desc')->get()->groupBy('event_id');
$events = Event::orderBy('name')->get();
return view('admin.auditions.bulk_edit_form', compact('auditions', 'events'));
}
public function bulkUpdate(BulkAuditionEditRequest $request)
{
$validated = collect($request->validated());
$auditions = Audition::whereIn('id', $validated['auditions'])->get();
foreach ($auditions as $audition) {
if ($validated->has('event_id')) {
$audition->event_id = $validated['event_id'];
}
if ($validated->has('entry_deadline')) {
$audition->entry_deadline = $validated['entry_deadline'];
}
if ($validated->has('entry_fee')) {
$audition->entry_fee = $validated['entry_fee'];
}
if ($validated->has('minimum_grade')) {
$originalMinimumGrade = $audition->minimum_grade;
$audition->minimum_grade = $validated['minimum_grade'];
}
if ($validated->has('maximum_grade')) {
$originalMaximumGrade = $audition->maximum_grade;
$audition->maximum_grade = $validated['maximum_grade'];
}
if ($validated->has('for_seating')) {
$audition->for_seating = $validated['for_seating'];
}
if ($validated->has('for_advancement')) {
$audition->for_advancement = $validated['for_advancement'];
}
if ($audition->minimum_grade > $audition->maximum_grade) {
$audition->minimum_grade = $originalMinimumGrade;
$audition->maximum_grade = $originalMaximumGrade;
}
$audition->save();
}
return to_route('admin.auditions.index')->with('success', $auditions->count().' Auditions updated successfully');
}
public function reorder(Request $request)
{
$order = $request->order;

View File

@ -37,6 +37,20 @@ class BonusScoreDefinitionController extends Controller
return to_route('admin.bonus-scores.index')->with('success', 'Bonus Score Created');
}
public function update(BonusScoreDefinition $bonusScore)
{
$validData = request()->validate([
'name' => 'required|unique:bonus_score_definitions,name,'.$bonusScore->id,
'max_score' => 'required|numeric',
'weight' => 'required|numeric',
]);
$bonusScore->update($validData);
return to_route('admin.bonus-scores.index')->with('success', 'Bonus Score Updated');
}
public function destroy(BonusScoreDefinition $bonusScore)
{
if ($bonusScore->auditions()->count() > 0) {

View File

@ -7,6 +7,7 @@ use App\Actions\Entries\UpdateEntry;
use App\Http\Controllers\Controller;
use App\Http\Requests\EntryStoreRequest;
use App\Models\Audition;
use App\Models\AuditLogEntry;
use App\Models\Entry;
use App\Models\School;
use App\Models\Seat;
@ -139,7 +140,9 @@ class EntryController extends Controller
// TODO: When updating Laravel, can we use the chaperone method I heard about ot load the entry back into the score
$scores = $entry->scoreSheets()->with('audition', 'judge', 'entry')->get();
return view('admin.entries.edit', compact('entry', 'students', 'auditions', 'scores'));
$logEntries = AuditLogEntry::whereJsonContains('affected->entries', $entry->id)->orderBy('created_at', 'desc')->get();
return view('admin.entries.edit', compact('entry', 'students', 'auditions', 'scores', 'logEntries'));
}
public function update(Request $request, Entry $entry, UpdateEntry $updater)

View File

@ -13,8 +13,12 @@ class EventController extends Controller
public function index()
{
$events = Event::all();
$renameModalXdata = '';
foreach ($events as $event) {
$renameModalXdata .= 'showRenameModal_'.$event->id.': false, ';
}
return view('admin.event.index', compact('events'));
return view('admin.event.index', compact('events', 'renameModalXdata'));
}
public function store(Request $request)
@ -30,6 +34,21 @@ class EventController extends Controller
return redirect()->route('admin.events.index')->with('success', 'Event created successfully');
}
public function update(Request $request, Event $event)
{
if ($request->name !== $event->name) {
$validated = request()->validate([
'name' => ['required', 'unique:events,name'],
]);
$event->update([
'name' => $validated['name'],
]);
}
return redirect()->route('admin.events.index')->with('success', 'Event renamed successfully');
}
public function destroy(Request $request, Event $event)
{
if ($event->auditions()->count() > 0) {

View File

@ -0,0 +1,70 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Http\Requests\PrelimDefinitionStoreOrUpdateRequest;
use App\Models\Audition;
use App\Models\PrelimDefinition;
use App\Models\Room;
use App\Models\ScoringGuide;
use function view;
class PrelimDefinitionController extends Controller
{
public function index()
{
$prelims = PrelimDefinition::all();
return view('admin.prelim_definitions.index', compact('prelims'));
}
public function create()
{
$auditions = Audition::doesntHave('prelimDefinition')->get();
$rooms = Room::all();
$guides = ScoringGuide::all();
$method = 'POST';
$action = route('admin.prelim_definitions.store');
$prelim = false;
return view('admin.prelim_definitions.createOrUpdate', compact('auditions', 'rooms', 'guides', 'method', 'action', 'prelim'));
}
public function store(PrelimDefinitionStoreOrUpdateRequest $request)
{
$validated = $request->validated();
PrelimDefinition::create($validated);
return redirect()->route('admin.prelim_definitions.index')->with('success', 'Prelim definition created');
}
public function edit(PrelimDefinition $prelimDefinition)
{
$auditions = Audition::doesntHave('prelimDefinition')->get();
$rooms = Room::all();
$guides = ScoringGuide::all();
$method = 'PATCH';
$action = route('admin.prelim_definitions.update', $prelimDefinition);
$prelim = $prelimDefinition;
return view('admin.prelim_definitions.createOrUpdate', compact('auditions', 'rooms', 'guides', 'method', 'action', 'prelim'));
}
public function update(PrelimDefinition $prelimDefinition, PrelimDefinitionStoreOrUpdateRequest $request)
{
$validated = $request->validated();
$prelimDefinition->update($validated);
return redirect()->route('admin.prelim_definitions.index')->with('success', 'Prelim definition updated');
}
public function destroy(PrelimDefinition $prelimDefinition)
{
$prelimDefinition->delete();
return redirect()->route('admin.prelim_definitions.index')->with('success', 'Prelim definition deleted');
}
}

View File

@ -27,11 +27,15 @@ class PrintCards extends Controller
public function print(\App\Actions\Print\PrintCards $printer)
{
//dump(request()->all());
if (request()->audition == null) {
return redirect()->back()->with('error', 'You must specify at least one audition');
// dump(request()->all());
// if (request()->audition == null) {
// return redirect()->back()->with('error', 'You must specify at least one audition');
// }
if (request()->audition) {
$selectedAuditionIds = array_keys(request()->audition);
} else {
$selectedAuditionIds = [];
}
$selectedAuditionIds = array_keys(request()->audition);
$cardQuery = Entry::whereIn('audition_id', $selectedAuditionIds);
// Process Filters
@ -62,6 +66,6 @@ class PrintCards extends Controller
}
$cards = $cards->sortBy($sorts);
$printer->print($cards);
//return view('admin.print_cards.print', compact('cards'));
// return view('admin.print_cards.print', compact('cards'));
}
}

View File

@ -19,6 +19,8 @@ class RoomController extends Controller
{
$rooms = Room::with('auditions.entries', 'entries')->orderBy('name')->get();
// Check if room id 0 exists, if not, create it and assign all unassigned auditions to it
if (! $rooms->contains('id', 0)) {
$unassignedRoom = Room::create([
'id' => 0,

View File

@ -6,6 +6,7 @@ use App\Actions\Schools\CreateSchool;
use App\Actions\Schools\SetHeadDirector;
use App\Http\Controllers\Controller;
use App\Http\Requests\SchoolStoreRequest;
use App\Models\AuditLogEntry;
use App\Models\School;
use App\Models\SchoolEmailDomain;
use App\Models\User;
@ -37,8 +38,9 @@ class SchoolController extends Controller
public function show(School $school)
{
$logEntries = AuditLogEntry::whereJsonContains('affected->schools', $school->id)->orderBy('created_at', 'desc')->get();
return view('admin.schools.show', ['school' => $school]);
return view('admin.schools.show', compact('school', 'logEntries'));
}
public function edit(School $school)

View File

@ -0,0 +1,16 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\School;
class SchoolEmailDomainController extends Controller
{
public function index()
{
$schools = School::with('emailDomains')->get();
return view('admin.schools.email_domains_index', compact('schools'));
}
}

View File

@ -9,6 +9,7 @@ use App\Models\SubscoreDefinition;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use function auditionSetting;
use function request;
use function response;
@ -81,7 +82,10 @@ class ScoringGuideController extends Controller
// Put the new subscore at the end of the list for both display and tiebreak order
$display_order = SubscoreDefinition::where('scoring_guide_id', '=', $guide->id)->max('display_order') + 1;
$tiebreak_order = SubscoreDefinition::where('scoring_guide_id', '=', $guide->id)->max('tiebreak_order') + 1;
if (! auditionSetting('advanceTo')) {
$validateData['for_advance'] = 0;
$validateData['for_seating'] = 1;
}
SubscoreDefinition::create([
'scoring_guide_id' => $guide->id,
'name' => $validateData['name'],
@ -106,6 +110,10 @@ class ScoringGuideController extends Controller
'Cannot update a subscore for a different scoring guide');
}
$validateData = $validateData = $request->validated();
if (! auditionSetting('advanceTo')) {
$validateData['for_advance'] = 0;
$validateData['for_seating'] = 1;
}
$subscore->update([
'name' => $validateData['name'],

View File

@ -81,8 +81,11 @@ class StudentController extends Controller
$event_entries = $student->entries()->with('audition.flags')->get()->groupBy('audition.event_id');
$events = Event::all();
$logEntries = AuditLogEntry::whereJsonContains('affected->students', $student->id)->orderBy('created_at',
'desc')->get();
return view('admin.students.edit',
compact('student', 'schools', 'minGrade', 'maxGrade', 'events', 'event_entries'));
compact('student', 'schools', 'minGrade', 'maxGrade', 'events', 'event_entries', 'logEntries'));
}
public function update(StudentStoreRequest $request, Student $student)

View File

@ -15,9 +15,12 @@ use App\Models\AuditLogEntry;
use App\Models\School;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str;
use function auditionLog;
class UserController extends Controller
{
public function index()
@ -31,8 +34,11 @@ class UserController extends Controller
{
$schools = School::orderBy('name')->get();
$logEntries = AuditLogEntry::whereJsonContains('affected->users', $user->id)->orderBy('created_at',
'desc')->get();
$userActions = AuditLogEntry::where('user', $user->email)->orderBy('created_at', 'desc')->get();
return view('admin.users.edit', ['user' => $user, 'schools' => $schools]);
return view('admin.users.edit', compact('user', 'schools', 'logEntries', 'userActions'));
}
public function create()
@ -62,6 +68,7 @@ class UserController extends Controller
$profileUpdater->update($user, $profileData);
// Deal with school assignment
dump($request->get('school_id'));
if ($user->school_id != $request->get('school_id')) {
$schoolAssigner($user, $request->get('school_id'));
}
@ -119,4 +126,22 @@ class UserController extends Controller
return redirect()->route('admin.users.index')->with('success', 'User deleted successfully');
}
public function setPassword(User $user, Request $request)
{
$validated = $request->validate([
'admin_password' => ['required', 'string', 'current_password:web'],
'new_password' => ['required', 'string', 'confirmed', 'min:8'],
]);
$user->forceFill([
'password' => Hash::make($validated['new_password']),
])->save();
auditionLog('Manually set password for '.$user->email, [
'users' => [$user->id],
]);
return redirect()->route('admin.users.index')->with('success',
'Password changed successfully for '.$user->email);
}
}

View File

@ -46,7 +46,12 @@ class DoublerRequestController extends Controller
public function makeRequest(DoublerRequestsStoreRequest $request)
{
foreach ($request->getDoublerRequests() as $thisRequest) {
if (! $thisRequest['request']) {
DoublerRequest::where('event_id', $thisRequest['event_id'])
->where('student_id', $thisRequest['student_id'])->delete();
continue;
}
DoublerRequest::upsert([
'event_id' => $thisRequest['event_id'],
'student_id' => $thisRequest['student_id'],

View File

@ -3,6 +3,7 @@
namespace App\Http\Controllers\Judging;
use App\Actions\Tabulation\EnterScore;
use App\Exceptions\AuditionAdminException;
use App\Http\Controllers\Controller;
use App\Models\Audition;
use App\Models\Entry;
@ -27,10 +28,10 @@ class JudgingController extends Controller
public function index()
{
$rooms = Auth::user()->judgingAssignments()->with('auditions')->get();
$rooms = Auth::user()->judgingAssignments()->with('auditions')->with('prelimAuditions')->get();
$bonusScoresToJudge = Auth::user()->bonusJudgingAssignments()->with('auditions')->get();
//$rooms->load('auditions');
// $rooms->load('auditions');
return view('judging.index', compact('rooms', 'bonusScoresToJudge'));
}
@ -41,6 +42,11 @@ class JudgingController extends Controller
return redirect()->route('judging.index')->with('error', 'You are not assigned to judge that audition');
}
$entries = Entry::where('audition_id', '=', $audition->id)->orderBy('draw_number')->with('audition')->get();
// If there is a prelim audition, only show entries that have passed the prelim
if ($audition->prelimDefinition) {
$entries = $entries->reject(fn ($entry) => ! $entry->hasFlag('passed_prelim'));
}
$subscores = $audition->scoringGuide->subscores()->orderBy('display_order')->get();
$votes = JudgeAdvancementVote::where('user_id', Auth::id())->get();
@ -95,7 +101,11 @@ class JudgingController extends Controller
// Enter the score
/** @noinspection PhpUnhandledExceptionInspection */
$enterScore(Auth::user(), $entry, $validatedData['score']);
try {
$enterScore(Auth::user(), $entry, $validatedData['score']);
} catch (AuditionAdminException $e) {
return redirect()->back()->with('error', $e->getMessage());
}
// Deal with an advancement vote if needed
$this->advancementVote($request, $entry);

View File

@ -0,0 +1,111 @@
<?php
namespace App\Http\Controllers\Judging;
use App\Actions\Tabulation\EnterPrelimScore;
use App\Exceptions\AuditionAdminException;
use App\Http\Controllers\Controller;
use App\Models\Entry;
use App\Models\PrelimDefinition;
use App\Models\PrelimScoreSheet;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class PrelimJudgingController extends Controller
{
public function prelimEntryList(PrelimDefinition $prelimDefinition)
{
if (auth()->user()->cannot('judge', $prelimDefinition)) {
return redirect()->route('dashboard')->with('error', 'You are not assigned to judge that prelim audition.');
}
$entries = $prelimDefinition->audition->entries;
$subscores = $prelimDefinition->scoringGuide->subscores()->orderBy('display_order')->get();
$published = $prelimDefinition->audition->hasFlag('seats_published');
$prelimScoresheets = PrelimScoreSheet::where('user_id', Auth::id())->get()->keyBy('entry_id');
return view('judging.prelim_entry_list',
compact('prelimDefinition', 'entries', 'subscores', 'published', 'prelimScoresheets'));
}
public function prelimScoreEntryForm(Entry $entry)
{
if (auth()->user()->cannot('judge', $entry->audition->prelimDefinition)) {
return redirect()->route('dashboard')->with('error', 'You are not assigned to judge that prelim audition.');
}
if ($entry->audition->hasFlag('seats_published')) {
return redirect()->route('dashboard')->with('error',
'Scores for entries in published auditions cannot be modified.');
}
if ($entry->hasFlag('no_show')) {
return redirect()->route('judging.prelimEntryList', $entry->audition->prelimDefinition)->with('error',
'The requested entry is marked as a no-show. Scores cannot be entered.');
}
$oldSheet = PrelimScoreSheet::where('user_id', Auth::id())->where('entry_id',
$entry->id)->value('subscores') ?? null;
if ($oldSheet) {
$formRoute = 'update.savePrelimScoreSheet';
$formMethod = 'PATCH';
} else {
$formRoute = 'judging.savePrelimScoreSheet';
$formMethod = 'POST';
}
return view('judging.prelim_entry_form', compact('entry', 'oldSheet', 'formRoute', 'formMethod'));
}
/**
* @throws AuditionAdminException
*/
public function savePrelimScoreSheet(Entry $entry, Request $request, EnterPrelimScore $scribe)
{
if (auth()->user()->cannot('judge', $entry->audition->prelimDefinition)) {
return redirect()->route('dashboard')->with('error', 'You are not assigned to judge that prelim audition.');
}
// Validate form data
$subscores = $entry->audition->prelimDefinition->scoringGuide->subscores;
$validationChecks = [];
foreach ($subscores as $subscore) {
$validationChecks['score'.'.'.$subscore->id] = 'required|integer|max:'.$subscore->max_score;
}
$validatedData = $request->validate($validationChecks);
// Enter the score
$scribe(auth()->user(), $entry, $validatedData['score']);
return redirect()->route('judging.prelimEntryList', $entry->audition->prelimDefinition)->with('success',
'Entered prelim scores for '.$entry->audition->name.' '.$entry->draw_number);
}
public function updatePrelimScoreSheet(Entry $entry, Request $request, EnterPrelimScore $scribe)
{
if (auth()->user()->cannot('judge', $entry->audition->prelimDefinition)) {
return redirect()->route('dashboard')->with('error', 'You are not assigned to judge that prelim audition.');
}
// Validate form data
$subscores = $entry->audition->prelimDefinition->scoringGuide->subscores;
$validationChecks = [];
foreach ($subscores as $subscore) {
$validationChecks['score'.'.'.$subscore->id] = 'required|integer|max:'.$subscore->max_score;
}
$validatedData = $request->validate($validationChecks);
// Get the existing score
$scoreSheet = PrelimScoreSheet::where('user_id', auth()->user()->id)->where('entry_id', $entry->id)->first();
if (! $scoreSheet) {
return redirect()->back()->with('error', 'No score sheet exists.');
}
// Update the score
$scribe(auth()->user(), $entry, $validatedData['score'], $scoreSheet);
return redirect()->route('judging.prelimEntryList', $entry->audition->prelimDefinition)->with('success',
'Updated prelim scores for '.$entry->audition->name.' '.$entry->draw_number);
}
}

View File

@ -2,8 +2,11 @@
namespace App\Http\Controllers;
use App\Models\Audition;
use App\Models\Entry;
use function compact;
class MonitorController extends Controller
{
public function index()
@ -11,86 +14,44 @@ class MonitorController extends Controller
if (! auth()->user()->hasFlag('monitor')) {
abort(403);
}
$method = 'POST';
$formRoute = 'monitor.enterFlag';
$title = 'Flag Entry';
return view('tabulation.choose_entry', compact('method', 'formRoute', 'title'));
$auditions = Audition::orderBy('score_order')->with('flags')->get();
$audition = null;
return view('monitor.index', compact('audition', 'auditions'));
}
public function flagForm()
public function auditionStatus(Audition $audition)
{
if (! auth()->user()->hasFlag('monitor')) {
abort(403);
}
$validData = request()->validate([
'entry_id' => ['required', 'integer', 'exists:entries,id'],
]);
$entry = Entry::find($validData['entry_id']);
// If the entries audition is published, bounce out
if ($entry->audition->hasFlag('seats_published') || $entry->audition->hasFlag('advancement_published')) {
return redirect()->route('monitor.index')->with('error', 'Cannot set flags while results are published');
if ($audition->hasFlag('seats_published') || $audition->hasFlag('advancement_published')) {
return redirect()->route('monitor.index')->with('error', 'Results for that audition are published');
}
// If entry has scores, bounce on out
if ($entry->scoreSheets()->count() > 0) {
return redirect()->route('monitor.index')->with('error', 'That entry has existing scores');
}
$auditions = Audition::orderBy('score_order')->with('flags')->get();
$entries = $audition->entries()->with('flags')->with('student.school')->withCount([
'prelimScoreSheets', 'scoreSheets',
])->orderBy('draw_number')->get();
return view('monitor_entry_flag_form', compact('entry'));
return view('monitor.index', compact('audition', 'auditions', 'entries'));
}
public function storeFlag(Entry $entry)
public function toggleNoShow(Entry $entry)
{
if (! auth()->user()->hasFlag('monitor')) {
abort(403);
}
// If the entries audition is published, bounce out
if ($entry->audition->hasFlag('seats_published') || $entry->audition->hasFlag('advancement_published')) {
return redirect()->route('monitor.index')->with('error', 'Cannot set flags while results are published');
return redirect()->route('monitor.index')->with('error', 'Results for that audition are published');
}
// If entry has scores, bounce on out
if ($entry->scoreSheets()->count() > 0) {
return redirect()->route('monitor.index')->with('error', 'That entry has existing scores');
}
$action = request()->input('action');
$result = match ($action) {
'failed-prelim' => $this->setFlag($entry, 'failed_prelim'),
'no-show' => $this->setFlag($entry, 'no_show'),
'clear' => $this->setFlag($entry, 'clear'),
default => redirect()->route('monitor.index')->with('error', 'Invalid action requested'),
};
return redirect()->route('monitor.index')->with('success', 'Flag set for entry #'.$entry->id);
}
private function setFlag(Entry $entry, string $flag)
{
if ($flag === 'no_show') {
$entry->removeFlag('failed_prelim');
$entry->addFlag('no_show');
return true;
}
if ($flag === 'failed_prelim') {
$entry->addFlag('failed_prelim');
$entry->addFlag('no_show');
return true;
}
if ($flag === 'clear') {
$entry->removeFlag('failed_prelim');
if ($entry->hasFlag('no_show')) {
$entry->removeFlag('no_show');
return true;
return redirect()->back()->with('success', 'No Show Flag Cleared');
}
$entry->addFlag('no_show');
return redirect()->back()->with('success', 'No Show Entered');
}
}

View File

@ -73,7 +73,7 @@ class AdvancementController extends Controller
$entries = $ranker($audition, 'advancement');
$entries->load(['advancementVotes', 'totalScore', 'student.school']);
$unscoredEntries = $audition->entries()->orderBy('draw_number')->get()->filter(function ($entry) {
$unscoredEntries = $audition->entries()->where('for_advancement', true)->orderBy('draw_number')->get()->filter(function ($entry) {
return ! $entry->totalScore && ! $entry->hasFlag('no_show');
});
@ -81,7 +81,7 @@ class AdvancementController extends Controller
return $entry->hasFlag('no_show');
});
$scoringComplete = $audition->entries->every(function ($entry) {
$scoringComplete = $audition->entries->where('for_advancement', true)->every(function ($entry) {
return $entry->totalScore || $entry->hasFlag('no_show');
});

View File

@ -2,11 +2,13 @@
namespace App\Http\Controllers\Tabulation;
use App\Actions\Tabulation\EnterPrelimScore;
use App\Actions\Tabulation\EnterScore;
use App\Exceptions\AuditionAdminException;
use App\Http\Controllers\Controller;
use App\Models\Entry;
use App\Models\EntryTotalScore;
use App\Models\PrelimScoreSheet;
use App\Models\ScoreSheet;
use App\Models\User;
use Illuminate\Http\Request;
@ -67,8 +69,25 @@ class ScoreController extends Controller
'This entry is marked as a no-show. Entering a score will remove the no-show flag');
}
if ($entry->audition->prelimDefinition) {
$existing_prelim_sheets = [];
$prelim_subscores = $entry->audition->prelimDefinition->scoringGuide->subscores->sortBy('display_order');
$prelim_judges = $entry->audition->prelimDefinition->room->judges;
foreach ($prelim_judges as $judge) {
$prelim_scoreSheet = PrelimScoreSheet::where('entry_id', $entry->id)->where('user_id', $judge->id)->first();
if ($prelim_scoreSheet) {
Session::flash('caution', 'Prelim scores exist for this entry. Now editing existing scores');
$existing_prelim_sheets[$judge->id] = $prelim_scoreSheet;
}
}
} else {
$prelim_subscores = null;
$prelim_judges = null;
$existing_prelim_sheets = null;
}
return view('tabulation.entry_score_sheet',
compact('entry', 'judges', 'scoring_guide', 'subscores', 'existing_sheets'));
compact('entry', 'judges', 'scoring_guide', 'subscores', 'existing_sheets', 'prelim_subscores', 'prelim_judges', 'existing_prelim_sheets'));
}
public function saveEntryScoreSheet(Request $request, Entry $entry, EnterScore $scoreRecorder)
@ -85,7 +104,7 @@ class ScoreController extends Controller
* The array should have a key for each subscore and the value of the score submitted
*/
foreach ($request->all() as $key => $value) {
// We're not interested in submission values that don't ahve judge in the name
// We're not interested in submission values that don't have judge in the name
if (! str_contains($key, 'judge')) {
continue;
}
@ -114,6 +133,50 @@ class ScoreController extends Controller
return redirect()->route('scores.chooseEntry')->with('success', 'Scores saved');
}
public function savePrelimEntryScoreSheet(Request $request, Entry $entry, EnterPrelimScore $scoreRecorder)
{
$publishedCheck = $this->checkIfPublished($entry);
if ($publishedCheck) {
return $publishedCheck;
}
/**
* Here we process the submission from the scoring form.
* We're expecting submitted data to include an array for each judge.
* Each array should be called judge+ the judges ID number
* The array should have a key for each subscore and the value of the score submitted
*/
foreach ($request->all() as $key => $value) {
// We're not interested in submission values that don't have judge in the name
if (! str_contains($key, 'judge')) {
continue;
}
// Extract the judge ID from the field name and load the user
$judge_id = str_replace('judge', '', $key);
$judge = User::find($judge_id);
// Check for existing scores, if so, tell EnterScores action that we're updating it, otherwise a new score
$existingScore = PrelimScoreSheet::where('entry_id', $entry->id)
->where('user_id', $judge->id)->first();
if ($existingScore === null) {
$existingScore = false;
}
try {
$scoreRecorder($judge, $entry, $value, $existingScore);
} catch (AuditionAdminException $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');
return redirect()->route('scores.chooseEntry')->with('success', 'Prelim Scores Saved');
}
protected function checkIfPublished($entry)
{
// We're not going to enter scores if seats are published

View File

@ -69,9 +69,14 @@ class ShowAuditionSeatingPage extends Controller
$query->where('flag_name', 'failed_prelim');
})
->with('student.school')
->with('PrelimScoreSheets')
->orderBy('draw_number')
->get();
$failed_prelim_entries = $failed_prelim_entries->sortByDesc(function ($entry) {
return $entry->prelimTotalScore();
});
// Get Doublers
$doublerData = Doubler::where('event_id', $audition->event_id)
->whereIn('student_id', $scored_entries->pluck('student_id'))

View File

@ -0,0 +1,74 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class BulkAuditionEditRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
// TODO write authorize
}
protected function prepareForValidation(): void
{
if (! $this->has('editEvent')) {
$this->request->remove('event_id');
}
if (! $this->has('editDeadline')) {
$this->request->remove('entry_deadline');
}
if (! $this->has('editFee')) {
$this->request->remove('entry_fee');
}
if (! $this->has('editMinGrade')) {
$this->request->remove('minimum_grade');
}
if (! $this->has('editMaxGrade')) {
$this->request->remove('maximum_grade');
}
if (! $this->has('editScope')) {
$this->request->remove('for_seating');
$this->request->remove('for_advancement');
}
if ($this->has('editScope')) {
if ($this->has('for_seating')) {
$this->merge(['for_seating' => true]);
} else {
$this->merge(['for_seating' => false]);
}
if ($this->has('for_advancement')) {
$this->merge(['for_advancement' => true]);
} else {
$this->merge(['for_advancement' => false]);
}
}
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'auditions' => 'required|array|min:1',
'auditions.*' => 'required|exists:auditions,id',
'event_id' => 'sometimes|exists:events,id',
'entry_deadline' => 'sometimes|date',
'entry_fee' => 'sometimes|numeric',
'minimum_grade' => 'sometimes|integer|min:1',
'maximum_grade' => 'sometimes|integer|min:1',
'for_seating' => 'sometimes|boolean',
'for_advancement' => 'sometimes|boolean',
'editScope' => 'sometimes|boolean',
];
}
}

View File

@ -16,7 +16,7 @@ class DoublerRequestsStoreRequest extends FormRequest
// Validate student IDs (second keys) and their values
'doubler_requests.*.*' => [
'required',
'nullable',
'string',
'max:50',
// Custom validation rule to check if the student ID exists in DB
@ -53,7 +53,6 @@ class DoublerRequestsStoreRequest extends FormRequest
public function getDoublerRequests(): array
{
$validated = $this->validated()['doubler_requests'] ?? [];
$result = [];
foreach ($validated as $eventId => $students) {

View File

@ -3,6 +3,8 @@
namespace App\Http\Requests;
use App\Models\Audition;
use App\Models\Entry;
use App\Models\Student;
use Auth;
use Carbon\Carbon;
use Illuminate\Foundation\Http\FormRequest;
@ -44,6 +46,7 @@ class EntryStoreRequest extends FormRequest
$validator->after(function ($validator) {
$auditionId = $this->input('audition_id');
$audition = Audition::find($auditionId);
$student = Student::find($this->input('student_id'));
if (! $audition) {
$validator->errors()->add('audition_id', 'The selected audition does not exist.');
@ -51,6 +54,11 @@ class EntryStoreRequest extends FormRequest
return;
}
if (Entry::where('student_id', $this->input('student_id'))->where('audition_id', $auditionId)->exists()) {
$validator->errors()->add('student_id',
$student->full_name().' is already entered in the '.$audition->name.' audition.');
}
if (! Auth::user()->is_admin) { //Admins don't care about deadlines
$currentDate = Carbon::now('America/Chicago')->format('Y-m-d');

View File

@ -0,0 +1,28 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class PrelimDefinitionStoreOrUpdateRequest extends FormRequest
{
public function rules(): array
{
return [
'audition_id' => [
'required',
'exists:auditions,id',
Rule::unique('prelim_definitions', 'audition_id')->ignore($this->prelimDefinition),
],
'room_id' => ['nullable', 'exists:rooms,id'],
'scoring_guide_id' => ['nullable', 'exists:scoring_guides,id'],
'passing_score' => ['required', 'integer', 'min:0', 'max:100'],
];
}
public function authorize(): bool
{
return auth()->user()->is_admin;
}
}

View File

@ -21,6 +21,15 @@ class SubscoreDefinitionRequest extends FormRequest
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
protected function prepareForValidation()
{
// Handle checkboxes
$this->merge([
'for_seating' => $this->has('for_seating') ? true : false,
'for_advance' => $this->has('for_advance') ? true : false,
]);
}
public function rules(): array
{
$guideId = $this->route('guide')->id; // get the guide ID from route model binding
@ -36,19 +45,13 @@ class SubscoreDefinitionRequest extends FormRequest
],
'max_score' => ['required', 'integer'],
'weight' => ['required', 'integer'],
'for_seating' => ['sometimes', 'nullable'],
'for_advance' => ['sometimes', 'nullable'],
'for_seating' => ['boolean'],
'for_advance' => ['boolean'],
];
}
protected function passedValidation()
{
// Normalize the boolean inputs
$this->merge([
'for_seating' => $this->has('for_seating') ? (bool) $this->input('for_seating') : false,
'for_advance' => $this->has('for_advance') ? (bool) $this->input('for_advance') : false,
]);
// Apply your custom logic
if (! auditionSetting('advanceTo')) {
$this->merge(['for_seating' => true]);

View File

@ -10,6 +10,7 @@ use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Support\Collection;
use function in_array;
@ -165,6 +166,11 @@ class Audition extends Model
return $this->hasMany(Seat::class);
}
public function prelimDefinition(): HasOne
{
return $this->hasOne(PrelimDefinition::class);
}
/**
* @codeCoverageIgnoreStart
*/

View File

@ -29,7 +29,7 @@ class Entry extends Model
/**
* @throws AuditionAdminException
*/
public function rank(string $type)
public function rank(string $type, bool $pullDeclinedEntries = true)
{
$ranker = app(RankAuditionEntries::class);
@ -39,11 +39,11 @@ class Entry extends Model
}
// Get the ranked entries for this entries audition
$rankedEntries = $ranker($this->audition, $type);
$rankedEntries = $ranker($this->audition, $type, $pullDeclinedEntries);
// 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;
return $rankedEntries->where('id', $this->id)->first()->seatingRank ?? 'No Rank';
}
return $rankedEntries->where('id', $this->id)->first()->advancementRank;
@ -136,6 +136,35 @@ class Entry extends Model
}
public function prelimScoreSheets(): HasMany
{
return $this->hasMany(PrelimScoreSheet::class);
}
public function prelimTotalScore()
{
return once(function () {
$total = 0;
foreach ($this->prelimScoreSheets as $sheet) {
$total += $sheet->total;
}
return $total / $this->prelimScoreSheets->count();
});
}
public function prelimResult()
{
if ($this->hasFlag('passed_prelim')) {
return 'passed';
}
if ($this->hasFlag('failed_prelim')) {
return 'failed';
}
return null;
}
public function bonusScores(): HasMany
{
return $this->hasMany(BonusScore::class);
@ -172,6 +201,7 @@ class Entry extends Model
'declined' => EntryFlags::DECLINED,
'no_show' => EntryFlags::NO_SHOW,
'failed_prelim' => EntryFlags::FAILED_PRELIM,
'passed_prelim' => EntryFlags::PASSED_PRELIM,
'late_fee_waived' => EntryFlags::LATE_FEE_WAIVED,
};
$this->flags()->create(['flag_name' => $enum]);

View File

@ -0,0 +1,32 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class PrelimDefinition extends Model
{
protected $fillable = [
'audition_id',
'room_id',
'order_in_room',
'scoring_guide_id',
'passing_score',
];
public function audition(): BelongsTo
{
return $this->belongsTo(Audition::class);
}
public function room(): BelongsTo
{
return $this->belongsTo(Room::class);
}
public function scoringGuide(): BelongsTo
{
return $this->belongsTo(ScoringGuide::class);
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasOne;
class PrelimScoreSheet extends Model
{
protected $fillable = [
'user_id',
'entry_id',
'subscores',
'total',
];
protected $casts = ['subscores' => 'json'];
public function user(): HasOne
{
return $this->hasOne(User::class);
}
public function entry(): HasOne
{
return $this->hasOne(Entry::class);
}
public function getSubscore($id)
{
return $this->subscores[$id]['score'] ?? false;
// this function is used at resources/views/tabulation/entry_score_sheet.blade.php
}
}

View File

@ -19,6 +19,11 @@ class Room extends Model
return $this->hasMany(Audition::class)->orderBy('order_in_room')->orderBy('score_order');
}
public function prelimAuditions(): HasMany
{
return $this->hasMany(PrelimDefinition::class);
}
public function entries(): HasManyThrough
{
return $this->hasManyThrough(

View File

@ -6,6 +6,7 @@ use App\Actions\Tabulation\DoublerSync;
use App\Models\Audition;
use App\Models\Doubler;
use App\Models\Entry;
use Illuminate\Support\Facades\Request;
use function auditionSetting;
@ -50,9 +51,11 @@ class EntryObserver
*/
public function updated(Entry $entry): void
{
$syncer = app(DoublerSync::class);
// Update doubler table when an entry is updated
$syncer();
if (Request::route()?->getName() !== 'admin.draw.store') { // Don't update doubler table during draw
$syncer = app(DoublerSync::class);
// Update doubler table when an entry is updated
$syncer();
}
// Log entry changes
$message = 'Updated Entry #'.$entry->id;

View File

@ -0,0 +1,51 @@
<?php
namespace App\Observers;
use App\Models\PrelimDefinition;
use App\Models\Room;
use App\Models\ScoringGuide;
class PrelimDefinitionObserver
{
/**
* Handle the PrelimDefinition "created" event.
*/
public function created(PrelimDefinition $prelimDefinition): void
{
$message = 'Created Prelim for '.$prelimDefinition->audition->name.'.';
$affected = ['auditions' => [$prelimDefinition->audition_id]];
auditionLog($message, $affected);
}
/**
* Handle the PrelimDefinition "updated" event.
*/
public function updated(PrelimDefinition $prelimDefinition): void
{
$message = 'Updated Prelim for '.$prelimDefinition->audition->name.'.';
if ($prelimDefinition->getOriginal('room_id') !== $prelimDefinition->room_id) {
$oldRoom = Room::find($prelimDefinition->getOriginal('room_id'));
$message .= '<br>Room: '.$oldRoom->name.' -> '.$prelimDefinition->room->name;
}
if ($prelimDefinition->getOriginal('scoring_guide_id') !== $prelimDefinition->scoring_guide_id) {
$oldScoringGuide = ScoringGuide::find($prelimDefinition->getOriginal('scoring_guide_id'));
$message .= '<br>Scoring Guide: '.$oldScoringGuide->name.' -> '.$prelimDefinition->scoringGuide->name;
}
if ($prelimDefinition->getOriginal('passing_score') !== $prelimDefinition->passing_score) {
$message .= '<br>Passing Score: '.$prelimDefinition->getOriginal('passing_score').' -> '.$prelimDefinition->passing_score;
}
auditionLog($message, ['auditions' => [$prelimDefinition->audition_id]]);
}
/**
* Handle the PrelimDefinition "deleted" event.
*/
public function deleted(PrelimDefinition $prelimDefinition): void
{
$message = 'Deleted Prelim for '.$prelimDefinition->audition->name.'.';
auditionLog($message, ['auditions' => [$prelimDefinition->audition_id]]);
}
}

View File

@ -0,0 +1,50 @@
<?php
namespace App\Policies;
use App\Models\PrelimDefinition;
use App\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization;
class PrelimDefinitionPolicy
{
use HandlesAuthorization;
public function viewAny(User $user): bool
{
return $user->is_admin;
}
public function view(User $user, PrelimDefinition $prelimDefinition): bool
{
return $user->is_admin;
}
public function create(User $user): bool
{
return $user->is_admin;
}
public function update(User $user, PrelimDefinition $prelimDefinition): bool
{
return $user->is_admin;
}
public function delete(User $user, PrelimDefinition $prelimDefinition): bool
{
return $user->is_admin;
}
public function judge(User $user, PrelimDefinition $prelimDefinition): bool
{
return $user->judgingAssignments->contains($prelimDefinition->room_id);
}
public function restore(User $user, PrelimDefinition $prelimDefinition): bool
{
}
public function forceDelete(User $user, PrelimDefinition $prelimDefinition): bool
{
}
}

View File

@ -12,6 +12,7 @@ use App\Models\BonusScore;
use App\Models\Entry;
use App\Models\EntryFlag;
use App\Models\Event;
use App\Models\PrelimDefinition;
use App\Models\School;
use App\Models\SchoolEmailDomain;
use App\Models\ScoreSheet;
@ -23,6 +24,7 @@ use App\Observers\BonusScoreObserver;
use App\Observers\EntryFlagObserver;
use App\Observers\EntryObserver;
use App\Observers\EventObserver;
use App\Observers\PrelimDefinitionObserver;
use App\Observers\SchoolEmailDomainObserver;
use App\Observers\SchoolObserver;
use App\Observers\ScoreSheetObserver;
@ -69,6 +71,7 @@ class AppServiceProvider extends ServiceProvider
EntryFlag::observe(EntryFlagObserver::class);
Event::observe(EventObserver::class);
School::observe(SchoolObserver::class);
PrelimDefinition::observe(PrelimDefinitionObserver::class);
SchoolEmailDomain::observe(SchoolEmailDomainObserver::class);
ScoreSheet::observe(ScoreSheetObserver::class);
ScoringGuide::observe(ScoringGuideObserver::class);

View File

@ -0,0 +1,42 @@
<?php
namespace App\Services;
class CsvImportService
{
/**
* Read a CSV file and return its contents as an array
*
* @param string $filePath Full path to the CSV file
* @param bool $trimHeaders Whether to trim whitespace from header names
* @return array Array of rows with header keys
*/
public function readCsv(string $filePath, bool $trimHeaders = true): array
{
if (! file_exists($filePath)) {
throw new \RuntimeException("File not found: {$filePath}");
}
$handle = fopen($filePath, 'r');
if ($handle === false) {
throw new \RuntimeException("Unable to open file: {$filePath}");
}
$header = null;
$rows = [];
while (($line = fgetcsv($handle, 0, ',')) !== false) {
if (! $header) {
$header = $trimHeaders ? array_map('trim', $line) : $line;
continue;
}
$row = array_combine($header, $line);
$rows[] = $row;
}
fclose($handle);
return $rows;
}
}

View File

@ -35,7 +35,7 @@ class InvoiceOneFeePerStudent implements InvoiceDataService
/** @noinspection PhpArrayIndexImmediatelyRewrittenInspection */
$invoiceData['grandTotal'] = 0;
$entries = $school->entries()->with('audition')->orderBy('created_at', 'desc')->get()->groupBy('student_id');
$entries = $school->entries()->where('for_seating', true)->with('audition')->orderBy('created_at', 'desc')->get()->groupBy('student_id');
foreach ($school->students as $student) {
$firstEntryForStudent = true;
foreach ($entries[$student->id] ?? [] as $entry) {

View File

@ -35,7 +35,7 @@ class InvoiceOneFeePerStudentPerEvent implements InvoiceDataService
/** @noinspection PhpArrayIndexImmediatelyRewrittenInspection */
$invoiceData['grandTotal'] = 0;
$entries = $school->entries()->with('audition')->orderBy('created_at', 'desc')->get()->groupBy('student_id');
$entries = $school->entries()->where('for_seating', true)->with('audition')->orderBy('created_at', 'desc')->get()->groupBy('student_id');
foreach ($school->students as $student) {
$eventsEntered = [];
foreach ($entries[$student->id] ?? [] as $entry) {

View File

@ -2,8 +2,10 @@
use App\Actions\Tabulation\EnterScore;
use App\Exceptions\ScoreEntryException;
use App\Models\Audition;
use App\Models\AuditLogEntry;
use App\Models\Entry;
use App\Models\NominationEnsemble;
use App\Models\User;
use App\Settings;
use Illuminate\Support\Facades\App;
@ -58,3 +60,23 @@ function enterScore(User $user, Entry $entry, array $scores): \App\Models\ScoreS
return $scoreEntry($user, $entry, $scores);
}
function minimumStudentGrade()
{
$minAuditionGrade = Audition::min('minimum_grade');
if (auditionSetting('nomination_ensemble_rules') == 'disabled' || NominationEnsemble::count() == 0) {
return $minAuditionGrade;
}
return min(Audition::min('minimum_grade'), NominationEnsemble::min('minimum_grade'));
}
function maximumStudentGrade()
{
$maxAuditionGrade = Audition::max('maximum_grade');
if (auditionSetting('nomination_ensemble_rules') == 'disabled' || NominationEnsemble::count() == 0) {
return $maxAuditionGrade;
}
return max(Audition::max('maximum_grade'), NominationEnsemble::max('maximum_grade'));
}

4
composer.lock generated
View File

@ -10004,12 +10004,12 @@
],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"stability-flags": {},
"prefer-stable": true,
"prefer-lowest": false,
"platform": {
"php": "^8.2"
},
"platform-dev": [],
"platform-dev": {},
"plugin-api-version": "2.6.0"
}

View File

@ -78,7 +78,7 @@ return [
'url' => env('LOG_SLACK_WEBHOOK_URL'),
'username' => env('LOG_SLACK_USERNAME', 'Laravel Log'),
'emoji' => env('LOG_SLACK_EMOJI', ':boom:'),
'level' => env('LOG_LEVEL', 'critical'),
'level' => env('LOG_LEVEL', 'debug'),
'replace_placeholders' => true,
],

View File

@ -0,0 +1,29 @@
<?php
use App\Models\Audition;
use App\Models\Room;
use App\Models\ScoringGuide;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('prelim_definitions', function (Blueprint $table) {
$table->id();
$table->foreignIdFor(Audition::class)->unique()->constrained()->cascadeOnDelete()->cascadeOnUpdate();
$table->foreignIdFor(Room::class)->nullable()->constrained()->nullOnDelete()->cascadeOnUpdate();
$table->integer('order_in_room')->nullable();
$table->foreignIdFor(ScoringGuide::class)->nullable()->constrained()->nullOnDelete()->cascadeOnUpdate();
$table->float('passing_score');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('prelim_definitions');
}
};

View File

@ -0,0 +1,34 @@
<?php
use App\Models\Entry;
use App\Models\User;
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('prelim_score_sheets', function (Blueprint $table) {
$table->id();
$table->foreignIdFor(User::class)->constrained()->cascadeOnDelete()->cascadeOnUpdate();
$table->foreignIdFor(Entry::class)->constrained()->cascadeOnDelete()->cascadeOnUpdate();
$table->json('subscores');
$table->decimal('total', 9, 6);
$table->timestamps();
$table->unique(['user_id', 'entry_id']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('prelim_score_sheets');
}
};

737
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -18,6 +18,7 @@
</include>
</source>
<php>
<ini name="memory_limit" value="512M"/>
<env name="APP_ENV" value="testing"/>
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
<env name="BCRYPT_ROUNDS" value="4"/>

View File

@ -0,0 +1,114 @@
<x-layout.app x-data="{ selectAuditionsForm: true, newValuesForm: false }">
<x-slot:page_title>Bulk Edit Auditions</x-slot:page_title>
@if($errors->any())
<div class="mt-3">
@foreach($errors->all() as $error)
<div class="ml-3">
<span
class="inline-flex items-center rounded-md bg-red-50 px-2 py-1 text-xs font-medium text-red-700 inset-ring inset-ring-red-600/10 dark:bg-red-400/10 dark:text-red-400 dark:inset-ring-red-400/20">{{$error}}</span>
</div>
@endforeach
</div>
@endif
<x-form.form method="POST" action="{{ route('admin.auditions.bulkEdit') }}">
<div x-show="selectAuditionsForm" x-cloak>
@foreach($events as $event)
<x-card.card class="mb-3">
<x-card.heading>
{{ $event->name }}
<x-slot:right_side>
<x-form.button type="button" @click="checkAllCheckboxesByClass('event-{{$event->id}}')">Select all {{ $event->name }}</x-form.button>
</x-slot:right_side>
</x-card.heading>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 p-3">
@foreach($auditions[$event->id] as $audition)
<div>
<x-form.checkbox name="auditions[{{ $audition->id }}]" label="{{$audition->name}}" value="{{ $audition->id }}"
class="event-{{$event->id}}"/>
</div>
@endforeach
</div>
</x-card.card>
@endforeach
<x-form.button type="button" @click="selectAuditionsForm = false; newValuesForm = true">Edit Selected
Auditions
</x-form.button>
</div>
<div x-show="newValuesForm" x-cloak>
<x-card.card class="max-w-xl mx-auto" x-data="{ editName: false, editEvent: false, editDeadline: false, editFee: false, editMinGrade: false, editMaxGrade: false, editScope: false }">
<x-card.heading>Select Values to Edit</x-card.heading>
<div class="grid grid-cols-2 gap-4 p-3 border-b-2">
<div>
<x-form.checkbox name="editEvent" label="Edit Audition Event" x-model="editEvent"/>
</div>
<x-form.select name="event_id" x-show="editEvent" x-cloak>
<x-slot:label></x-slot:label>
@foreach($events as $event)
<option value="{{ $event->id }}">{{ $event->name }}</option>
@endforeach
</x-form.select>
</div>
<div class="grid grid-cols-2 gap-4 p-3 border-b-2">
<div>
<x-form.checkbox name="editDeadline" label="Edit Entry Deadline" x-model="editDeadline"/>
</div>
<x-form.field name="entry_deadline" type="date" x-cloak x-show="editDeadline"/>
</div>
<div class="grid grid-cols-2 gap-4 p-3 border-b-2">
<div>
<x-form.checkbox name="editFee" label="Edit Entry Fee" x-model="editFee"/>
</div>
<x-form.field name="entry_fee" type="number" placeholder="Enter New Entry Fee" x-cloak x-show="editFee" />
</div>
<div class="grid grid-cols-2 gap-4 p-3 border-b-2">
<div>
<x-form.checkbox name="editMinGrade" label="Edit Minimum Grade" x-model="editMinGrade"/>
</div>
<x-form.field name="minimum_grade" type="number" placeholder="Enter New Minimum Grade" x-cloak x-show="editMinGrade" />
</div>
<div class="grid grid-cols-2 gap-4 p-3 border-b-2">
<div>
<x-form.checkbox name="editMaxGrade" label="Edit Maximum Grade" x-model="editMaxGrade"/>
</div>
<x-form.field name="maximum_grade" type="number" placeholder="Enter New Maximum Grade " x-cloak x-show="editMaxGrade" />
</div>
@if(auditionSetting('advanceTo'))
<div class="grid grid-cols-2 gap-4 p-3 border-b-2">
<div>
<x-form.checkbox name="editScope" label="Edit Audition Scope" x-model="editScope"/>
</div>
<div class="grid grid-cols-2" x-show="editScope" x-cloak>
<div class="align-top">
<x-form.checkbox name="for_seating" label="For Seats" description="Students will be seated in this audition" checked />
</div>
<div class="align-top">
<x-form.checkbox name="for_advancement" label="For {{ auditionSetting('advanceTo') }}" description="Students compete for advancement" checked/>
</div>
</div>
</div>
@endif
</x-card.card>
</div>
<x-form.button x-cloak x-show="newValuesForm" class="mt-3 max-w-xl mx-auto">Submit Changes</x-form.button>
</x-form.form>
<script>
function checkAllCheckboxesByClass(className) {
const checkboxes = document.querySelectorAll(`input[type="checkbox"].${className}`);
checkboxes.forEach(checkbox => {
checkbox.checked = true;
});
}
</script>
</x-layout.app>

View File

@ -2,7 +2,7 @@
<x-slot:page_title>Audition Administration</x-slot:page_title>
<x-card.card>
<x-table.table with_title_area sortable="false" id="auditions-table">
<x-slot:title class="ml-3">Auditions</x-slot:title>
<x-slot:title class="ml-3">Auditions <span class="font-normal text-sm"><a href="{{ route('admin.auditions.bulkEditForm') }}">[Bulk Edit]</a></span></x-slot:title>
<x-slot:subtitle class="ml-3">Drag to reorder. Double click to edit.</x-slot:subtitle>
<x-slot:title_block_right class="mr-3">
<x-form.button href="{{ route('admin.auditions.create') }}">New Audition</x-form.button>

View File

@ -0,0 +1,16 @@
<x-modal-body show-var="showEditBonusScoreModal{{ $bonusScoreModalId }}">
<x-slot:title>
Edit Bonus Score
</x-slot:title>
<x-form.form id="update-bonus-score-form" action="{{ route('admin.bonus-scores.update', $bonusScore) }}" method="PATCH">
<x-form.body-grid columns="12">
<x-form.field name="name" label_text="Name" colspan="8" value="{{ $bonusScore->name }}" />
<x-form.field name="max_score" type="number" label_text="Max Points" colspan="2" value="{{ $bonusScore->max_score }}" />
<x-form.field name="weight" label_text="Weight" colspan="2" value="{{ $bonusScore->weight }}" />
<div class="col-start-9 col-span-4 row-start-2">
<x-form.button >Update Bonus Score</x-form.button>
</div>
</x-form.body-grid>
</x-form.form>
</x-modal-body>

View File

@ -8,10 +8,11 @@
@endif
@foreach($bonusScores as $bonusScore)
<x-card.card class="mx-auto max-w-xl mb-5">
<x-card.card class="mx-auto max-w-xl mb-5" x-data="{ showEditBonusScoreModal{{$bonusScore->id}}: false }">
<x-card.heading>
{{ $bonusScore->name }}
@php($bonusScoreModalId = $bonusScore->id)
@include('admin.bonus-scores.index-edit-bonus-score-modal')
{{ $bonusScore->name }} <button class="text-sm font-normal" x-on:click="showEditBonusScoreModal{{$bonusScore->id}}=true">[Edit]</button>
<x-slot:subheading>
Max Points: {{ $bonusScore->max_score }} | Weight: {{ $bonusScore->weight }}
</x-slot:subheading>

View File

@ -2,9 +2,20 @@
<x-layout.app>
<x-card.card class="mx-auto max-w-2xl">
<x-card.heading>Create Entry</x-card.heading>
<x-form.form id='createEntryForm' method="POST" action="/admin/entries">
@if ($errors->any())
<div class="mt-3">
@foreach($errors->all() as $error)
<div class="ml-3">
<span
class="inline-flex items-center rounded-md bg-red-50 px-2 py-1 text-xs font-medium text-red-700 inset-ring inset-ring-red-600/10 dark:bg-red-400/10 dark:text-red-400 dark:inset-ring-red-400/20">{{$error}}</span>
</div>
@endforeach
</div>
@endif
<x-form.form id='createEntryForm' method="POST" action="/admin/entries" class="mt-3">
<x-form.body-grid columns="3" x-data="studentAuditionFilter()">
<x-form.select name="student_id" colspan="2" x-model="selectedStudentId" @change="filterAuditions">
<x-slot:label>Student</x-slot:label>
<option value="" disabled selected>Select a student</option>

View File

@ -127,4 +127,29 @@
</x-card.list.body>
</x-card.card>
<x-card.card class="mt-5">
<x-card.heading>Log Entries</x-card.heading>
<x-table.table>
<thead>
<tr>
<x-table.th>Timestamp</x-table.th>
<x-table.th>IP</x-table.th>
<x-table.th>User</x-table.th>
<x-table.th>Message</x-table.th>
</tr>
</thead>
<x-table.body>
@foreach($logEntries as $logEntry)
<tr>
<x-table.td>{{ $logEntry->created_at }}</x-table.td>
<x-table.td>{{ $logEntry->ip_address }}</x-table.td>
<x-table.td>{{ $logEntry->user }}</x-table.td>
<x-table.td>{!! $logEntry->message !!}</x-table.td>
</tr>
@endforeach
</x-table.body>
</x-table.table>
</x-card.card>
</x-layout.app>

View File

@ -0,0 +1,14 @@
<x-modal-body show-var="{{$currentRenameModalName}}">
<x-slot:title>
Rename Event
</x-slot:title>
<x-form.form id="update-bonus-score-form" action="{{ route('admin.events.update', $event) }}" method="PATCH">
<x-form.body-grid columns="12">
<x-form.field name="name" label_text="Name" colspan="8" value="{{ $event->name }}" />
<div class="col-start-9 col-span-4 row-start-2">
<x-form.button >Update Event</x-form.button>
</div>
</x-form.body-grid>
</x-form.form>
</x-modal-body>

View File

@ -8,36 +8,50 @@
</x-slot:right_side>
</x-card.heading>
<x-table.table>
<x-table.body>
@foreach($events as $event)
<tr>
<x-table.td>{{ $event->name }}, {{ $event->auditions()->count() }} Auditions</x-table.td>
<x-table.td class="text-right">
&nbsp;
@if($event->auditions()->count() == 0)
<form method="POST" action="{{ route('admin.events.destroy', ['event' => $event->id]) }}">
@csrf
@method('DELETE')
<button type="submit" class="text-red-600 text-right">Delete Event</button>
</form>
@endif
<div x-data="{ {{ $renameModalXdata}} }">
@foreach($events as $event)
@php($currentRenameModalName = "showRenameModal_".$event->id)
@include('admin.event.index-rename-event-modal')
@endforeach
<x-table.table>
<x-table.body>
@foreach($events as $event)
<tr>
<x-table.td>{{ $event->name }}
<button class="text-xs" @click="showRenameModal_{{$event->id}} = true">[rename]</button>
<br/>
<span class="text-xs">{{ $event->auditions()->count() }} Auditions</span>
</x-table.td>
<x-table.td class="text-right">
&nbsp;
@if($event->auditions()->count() == 0)
<form method="POST"
action="{{ route('admin.events.destroy', ['event' => $event->id]) }}">
@csrf
@method('DELETE')
<button type="submit" class="text-red-600 text-right">Delete Event</button>
</form>
@endif
</x-table.td>
</tr>
@endforeach
</x-table.body>
<tfoot>
<tr>
<x-form.form method="POST" :action="route('admin.events.store')" class="!px-0 !py-0">
<x-table.td>
<x-form.field name="name" label_text="Add New Event"/>
</x-table.td>
</tr>
@endforeach
</x-table.body>
<tfoot>
<tr>
<x-form.form method="POST" :action="route('admin.events.store')" class="!px-0 !py-0">
<x-table.td>
<x-form.field name="name" label_text="Add New Event" />
</x-table.td>
<x-table.td>
<x-form.button class="mt-6">Create</x-form.button>
</x-table.td>
</x-form.form>
</tr>
</tfoot>
</x-table.table>
<x-table.td>
<x-form.button class="mt-6">Create</x-form.button>
</x-table.td>
</x-form.form>
</tr>
</tfoot>
</x-table.table>
</div>
</x-card.card>
</x-layout.app>

View File

@ -0,0 +1,66 @@
<x-layout.app>
<x-slot:page_title>Manage Prelim Auditions</x-slot:page_title>
<div class="max-w-lg mx-auto">
<x-card.card class="pb-5">
<x-card.heading>
@if($prelim)
Modify Prelim - {{ $prelim->audition->name }}
<x-slot:right_side>
<x-delete-resource-modal title="Delete {{ $prelim->audition->name }} Prelim" action="{{ route('admin.prelim_definitions.destroy', $prelim->id)}}">
Confirm that you'd like to delete prelim auditions for {{ $prelim->audition->name }}.
{{-- TODO: Block deleting a prelim audition if there are prelim scores --}}
</x-delete-resource-modal>
</x-slot:right_side>
@else
Create Prelim Audition
@endif
</x-card.heading>
<x-form.form method="{{ $method }}" action="{{ $action }}"
x-data="{ canSubmit: {{ ! $prelim ? 'false':'true' }} }">
<x-form.select name="audition_id" @change="canSubmit = true">
<x-slot:label>Audition</x-slot:label>
@if($prelim)
<option value="{{ $prelim->audition_id }}">{{ $prelim->audition->name }}</option>
@else
<option value="" :disabled="canSubmit">Choose Audition</option>
@foreach($auditions as $audition)
<option value="{{ $audition->id }}">{{ $audition->name }}</option>
@endforeach
@endif
</x-form.select>
@error('audition_id')
<div class="text-red-500 text-sm">{{ $message }}</div>
@enderror
<x-form.select name="room_id">
<x-slot:label>Room</x-slot:label>
@foreach($rooms as $room)
<option value="{{ $room->id }}" @if($prelim && $prelim->room_id == $room->id) SELECTED @endif>{{ $room->name }} - {{ $room->description }}</option>
@endforeach
</x-form.select>
@error('room_id')
<div class="text-red-500 text-sm">{{ $message }}</div>
@enderror
<x-form.select name="scoring_guide_id">
<x-slot:label>Scoring Guide</x-slot:label>
@foreach($guides as $guide)
<option value="{{ $guide->id }}" @if($prelim && $prelim->scoring_guide_id == $guide->id) SELECTED @endif>{{ $guide->name }}</option>
@endforeach
</x-form.select>
@error('scoring_guide_id')
<div class="text-red-500 text-sm">{{ $message }}</div>
@enderror
<x-form.field name="passing_score" type="number" max="100" min="0" step="0.1" :value=" $prelim ? $prelim->passing_score : 60"
label_text="Passing Score"/>
<x-form.footer submit-button-text="{{ ! $prelim ? 'Create Prelim Audition':'Modify Prelim Audition' }}" x-show="canSubmit" x-cloak></x-form.footer>
</x-form.form>
</x-card.card>
</div>
</x-layout.app>

View File

@ -0,0 +1,33 @@
<x-layout.app>
<x-slot:page_title>Prelim Audition Setup</x-slot:page_title>
<x-card.card>
<x-card.heading>
Preliminary Auditions
<x-slot:subheading>Click to edit or delete</x-slot:subheading>
<x-slot:right_side>
<x-form.button href="{{ route('admin.prelim_definitions.create') }}">Add Prelim</x-form.button>
</x-slot:right_side>
</x-card.heading>
<x-table.table class="mt-3 mb-3">
<thead>
<tr>
<x-table.th>Audition</x-table.th>
<x-table.th>Passing Score</x-table.th>
<x-table.th>Room</x-table.th>
<x-table.th>Scoring Guide</x-table.th>
</tr>
</thead>
<x-table.body>
@foreach ($prelims as $prelim)
<tr onclick="window.location='{{ route('admin.prelim_definitions.edit', $prelim) }}';"
style="cursor:pointer;">
<x-table.td>{{ $prelim->audition->name }}</x-table.td>
<x-table.td>{{ $prelim->passing_score }}</x-table.td>
<x-table.td>{{ $prelim->room->name }}</x-table.td>
<x-table.td>{{ $prelim->scoringGuide->name }}</x-table.td>
</tr>
@endforeach
</x-table.body>
</x-table.table>
</x-card.card>
</x-layout.app>

View File

@ -0,0 +1,30 @@
<x-layout.app>
<x-slot:page_title>School Email Domains</x-slot:page_title>
<x-card.card class="max-w-2xl mx-auto">
<x-card.heading>School Email Domains</x-card.heading>
<x-table.table>
<thead>
<tr>
<x-table.th>School</x-table.th>
<x-table.th>Domains</x-table.th>
</tr>
</thead>
<x-table.body>
@foreach($schools as $school)
<tr>
<x-table.td>
<a href="{{ route('admin.schools.show', $school) }}">
{{ $school->name }}
</a>
</x-table.td>
<x-table.td>
@foreach($school->emailDomains ?? [] as $domain)
{{ $domain->domain }}
@endforeach
</x-table.td>
</tr>
@endforeach
</x-table.body>
</x-table.table>
</x-card.card>
</x-layout.app>

View File

@ -62,4 +62,30 @@
</div>
</div>
</div>
<x-card.card class="mt-5">
<x-card.heading>Log Entries</x-card.heading>
<x-table.table>
<thead>
<tr>
<x-table.th>Timestamp</x-table.th>
<x-table.th>IP</x-table.th>
<x-table.th>User</x-table.th>
<x-table.th>Message</x-table.th>
</tr>
</thead>
<x-table.body>
@foreach($logEntries as $logEntry)
<tr>
<x-table.td>{{ $logEntry->created_at }}</x-table.td>
<x-table.td>{{ $logEntry->ip_address }}</x-table.td>
<x-table.td>{{ $logEntry->user }}</x-table.td>
<x-table.td>{!! $logEntry->message !!}</x-table.td>
</tr>
@endforeach
</x-table.body>
</x-table.table>
</x-card.card>
</x-layout.app>

View File

@ -80,4 +80,29 @@
</x-table.table>
</x-card.card>
@endforeach
<x-card.card class="mt-5">
<x-card.heading>Log Entries</x-card.heading>
<x-table.table>
<thead>
<tr>
<x-table.th>Timestamp</x-table.th>
<x-table.th>IP</x-table.th>
<x-table.th>User</x-table.th>
<x-table.th>Message</x-table.th>
</tr>
</thead>
<x-table.body>
@foreach($logEntries as $logEntry)
<tr>
<x-table.td>{{ $logEntry->created_at }}</x-table.td>
<x-table.td>{{ $logEntry->ip_address }}</x-table.td>
<x-table.td>{{ $logEntry->user }}</x-table.td>
<x-table.td>{!! $logEntry->message !!}</x-table.td>
</tr>
@endforeach
</x-table.body>
</x-table.table>
</x-card.card>
</x-layout.app>

View File

@ -53,4 +53,69 @@
</x-form.footer>
</x-form.form>
</x-card.card>
<x-card.card class="max-w-lg mx-auto mt-5" x-data="{ showPasswordForm: false}">
<x-card.heading @click="showPasswordForm = !showPasswordForm">
Manually Set Password
</x-card.heading>
<div class="mb-5 mt-3" x-cloak x-show="showPasswordForm">
<x-form.form method="POST" action="{{ route('admin.users.setPassword', $user) }}">
<x-form.field name="admin_password" label_text="YOUR password" type="password"/>
<x-form.field name="new_password" label_text="New password for {{ $user->email }}" type="password"/>
<x-form.field name="new_password_confirmation" label_text="Confirm new password for {{ $user->email }}"
type="password"/>
<x-form.button class="mt-3">Update Password</x-form.button>
</x-form.form>
</div>
</x-card.card>
<x-card.card class="mt-5">
<x-card.heading>User Actions</x-card.heading>
<x-table.table>
<thead>
<tr>
<x-table.th>Timestamp</x-table.th>
<x-table.th>IP</x-table.th>
<x-table.th>User</x-table.th>
<x-table.th>Message</x-table.th>
</tr>
</thead>
<x-table.body>
@foreach($userActions as $logEntry)
<tr>
<x-table.td>{{ $logEntry->created_at }}</x-table.td>
<x-table.td>{{ $logEntry->ip_address }}</x-table.td>
<x-table.td>{{ $logEntry->user }}</x-table.td>
<x-table.td>{!! $logEntry->message !!}</x-table.td>
</tr>
@endforeach
</x-table.body>
</x-table.table>
</x-card.card>
<x-card.card class="mt-5">
<x-card.heading>Log Entries Affecting User</x-card.heading>
<x-table.table>
<thead>
<tr>
<x-table.th>Timestamp</x-table.th>
<x-table.th>IP</x-table.th>
<x-table.th>User</x-table.th>
<x-table.th>Message</x-table.th>
</tr>
</thead>
<x-table.body>
@foreach($logEntries as $logEntry)
<tr>
<x-table.td>{{ $logEntry->created_at }}</x-table.td>
<x-table.td>{{ $logEntry->ip_address }}</x-table.td>
<x-table.td>{{ $logEntry->user }}</x-table.td>
<x-table.td>{!! $logEntry->message !!}</x-table.td>
</tr>
@endforeach
</x-table.body>
</x-table.table>
</x-card.card>
</x-layout.app>

View File

@ -13,8 +13,7 @@
<tr>
<x-table.th>Name</x-table.th>
<x-table.th>School</x-table.th>
<x-table.th>Email</x-table.th>
<x-table.th>Cell Phone</x-table.th>
<x-table.th>Cell Phone<br />Email</x-table.th>
<x-table.th>Judging Preference</x-table.th>
<x-table.th>Privileges</x-table.th>
</tr>
@ -22,10 +21,16 @@
<x-table.body>
@foreach($users as $user)
<tr class="hover:bg-gray-50">
<x-table.td><a href="{{ route('admin.users.edit',$user) }}">{{ $user->full_name(true) }}</a>{{ $user->hasFlag('head_director') ? ' *':'' }}</x-table.td>
<x-table.td>
<a href="{{ route('admin.users.edit',$user) }}">{{ $user->full_name(true) }}</a>
{{ $user->hasFlag('head_director') ? ' *':'' }}
@if(! $user->email_verified_at)
<p class="text-xs font-light">Unverified Account</p>
@endif
</x-table.td>
<x-table.td>{{ $user->has_school() ? $user->school->name : ' ' }}</x-table.td>
<x-table.td>{{ $user->email }}</x-table.td>
<x-table.td>{{ $user->cell_phone }}</x-table.td>
<x-table.td>{{ $user->cell_phone }}<br/>{{ $user->email }}</x-table.td>
<x-table.td>{{ $user->judging_preference }}</x-table.td>
<x-table.td>
@if($user->is_admin)

View File

@ -2,7 +2,7 @@
<x-layout.page-header>Year End Reset</x-layout.page-header>
<x-card.card class="mt-5 max-w-xl m-auto">
<x-card.heading>Reset Options</x-card.heading>
<x-form.form action="{{ route('admin.year_end_procedures') }}">
<x-form.form action="{{ route('admin.execute_year_end_procedures') }}">
<x-form.checkbox name="options[]" label="Delete Rooms" value="deleteRooms" />
<x-form.checkbox name="options[]" label="Remove Auditions From Rooms" value="removeAuditionsFromRoom" />
<x-form.checkbox name="options[]" label="Unassign Judges" value="unassignJudges" />

View File

@ -2,7 +2,9 @@
'label' => false,
'description' => '',
'checked' => false,
'id' => false])
'id' => false,
'value' => 1,
])
@php
if(! $id):
$id = $name;
@ -14,9 +16,9 @@
aria-describedby="comments-description"
name="{{ $name }}"
type="checkbox"
value="1"
value="{{ $value }}"
@if($checked) checked @endif
{{ $attributes->merge(['class' => "h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600"]) }}>
{{ $attributes->merge(['class' => "h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600"]) }}>
</div>
<div class="ml-3 text-sm leading-6">
@if($label)

View File

@ -0,0 +1,3 @@
<div>
</div>

View File

@ -17,7 +17,7 @@
<script src="{{ asset('js/sort_table_by_column.js') }}"></script>
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.14.0/Sortable.min.js"></script>
</head>
<body class="h-full">
<body {{ $attributes->merge(['class' => 'h-full']) }}>
<div class="min-h-full">
{{-- @if(request()->is('*admin*'))--}}
{{-- <x-layout.navbar.navbar-admin />--}}

View File

@ -23,6 +23,7 @@
<a href="{{route('admin.dashboard')}}" class="block p-2 hover:text-indigo-600">Admin Dashboard</a>
<a href="{{route('admin.users.index')}}" class="block p-2 hover:text-indigo-600">Users</a>
<a href="{{route('admin.schools.index')}}" class="block p-2 hover:text-indigo-600">Schools</a>
<a href="{{route('admin.schools.email_domains')}}" class="block p-2 hover:text-indigo-600">School Email Domains</a>
<a href="{{route('admin.students.index')}}" class="block p-2 hover:text-indigo-600">Students</a>
<a href="{{route('admin.entries.index')}}" class="block p-2 hover:text-indigo-600">Entries</a>
@if(auditionSetting('nomination_ensemble_rules') !== 'disabled')

View File

@ -23,6 +23,7 @@
<x-layout.navbar.menus.menu-item :href="route('audition-settings')">Audition Settings</x-layout.navbar.menus.menu-item>
<x-layout.navbar.menus.menu-item :href="route('admin.events.index')">Events</x-layout.navbar.menus.menu-item>
<x-layout.navbar.menus.menu-item :href="route('admin.auditions.index')">Auditions</x-layout.navbar.menus.menu-item>
<x-layout.navbar.menus.menu-item :href="route('admin.prelim_definitions.index')">Prelim Auditions</x-layout.navbar.menus.menu-item>
<x-layout.navbar.menus.menu-item :href="route('admin.ensembles.index')">Ensembles</x-layout.navbar.menus.menu-item>
<x-layout.navbar.menus.menu-item :href="route('admin.ensembles.seatingLimits')">Seating Limits</x-layout.navbar.menus.menu-item>
<x-layout.navbar.menus.menu-item :href="route('admin.scoring.index')">Scoring</x-layout.navbar.menus.menu-item>

View File

@ -29,14 +29,14 @@
<option value="user">Director</option>
<option value="admin" selected>Admin</option>
</x-form.select>
<x-layout.nav-link href="/admin" :active="request()->is('admin')">Dashboard</x-layout.nav-link>
<x-layout.nav-link href="/admin/users" :active="request()->is('admin/users')">Users</x-layout.nav-link>
<x-layout.nav-link href="/admin/schools" :active="request()->is('admin/schools')">Schools</x-layout.nav-link>
<x-layout.nav-link href="/admin/students" :active="request()->is('admin/students')">Students</x-layout.nav-link>
<x-layout.nav-link href="/admin/entries" :active="request()->is('admin/entries')">Entries</x-layout.nav-link>
<x-layout.nav-link href="/admin/auditions" :active="request()->is('admin/auditions')">Auditions</x-layout.nav-link>
<x-layout.nav-link href="/admin/scoring" :active="request()->is('admin/scoring')">Scoring</x-layout.nav-link>
<x-layout.nav-link href="/admin/rooms" :active="request()->is('admin/rooms')">Rooms</x-layout.nav-link>
<x-layout.navbar.nav-link href="/admin" :active="request()->is('admin')">Dashboard</x-layout.navbar.nav-link>
<x-layout.navbar.nav-link href="/admin/users" :active="request()->is('admin/users')">Users</x-layout.navbar.nav-link>
<x-layout.navbar.nav-link href="/admin/schools" :active="request()->is('admin/schools')">Schools</x-layout.navbar.nav-link>
<x-layout.navbar.nav-link href="/admin/students" :active="request()->is('admin/students')">Students</x-layout.navbar.nav-link>
<x-layout.navbar.nav-link href="/admin/entries" :active="request()->is('admin/entries')">Entries</x-layout.navbar.nav-link>
<x-layout.navbar.nav-link href="/admin/auditions" :active="request()->is('admin/auditions')">Auditions</x-layout.navbar.nav-link>
<x-layout.navbar.nav-link href="/admin/scoring" :active="request()->is('admin/scoring')">Scoring</x-layout.navbar.nav-link>
<x-layout.navbar.nav-link href="/admin/rooms" :active="request()->is('admin/rooms')">Rooms</x-layout.navbar.nav-link>
{{-- <a href="/dashboard" class="bg-indigo-700 text-white rounded-md px-3 py-2 text-sm font-medium" aria-current="page">Dashboard</a>--}}
{{-- <a href="/students" class="text-white hover:bg-indigo-500 hover:bg-opacity-75 rounded-md px-3 py-2 text-sm font-medium">Students</a>--}}

View File

@ -1,6 +1,14 @@
<x-layout.app>
<x-slot:page_title>Doubler Requests</x-slot:page_title>
@foreach($errors->all() as $error)
<span
class="inline-flex items-center rounded-md bg-red-50 px-2 py-1 text-xs font-medium text-red-700 inset-ring inset-ring-red-600/10 dark:bg-red-400/10 dark:text-red-400 dark:inset-ring-red-400/20">
{{ $error }}
</span>
@endforeach
<x-form.form method="POST" action="{{route('doubler_request.make_request')}}">
@foreach($events as $event)
<x-card.card class="mb-5">
<x-card.heading>{{ $event->name }}</x-card.heading>
@ -24,7 +32,7 @@
</x-table.td>
<x-table.td>
<x-form.field
value="{{ $existingRequests[$event->id][$student->id] ?? '' }}"
value="{{ $existingRequests[$event->id][$student->id]['request'] ?? '' }}"
name="doubler_requests[{{$event->id}}][{{$student->id}}]"/>
</x-table.td>
</tr>

View File

@ -53,7 +53,12 @@
<x-layout.page-section>
<x-slot:section_name>Entry Listing</x-slot:section_name>
<x-slot:section_description>You have {{ $entries->count() }} entries</x-slot:section_description>
<x-slot:section_description>
You have {{ $entries->count() }} entries <hr />
<p class="mt-3 text-sm">Note on results</p>
<p class="text-sm">Doublers will show declined on all but one entry. The rank shown on this screen does
not account for any doublers that declined a seat in that entries audition.</p>
</x-slot:section_description>
<div class="px-6 md:px-8 py-3">
<x-table.table>
<thead>
@ -67,7 +72,12 @@
@endif
<x-table.th spacer_only>
<span class="sr-only">Edit</span>
</x-table.th>
<x-table.th>
Seat
</x-table.th>
<x-table.th>Rank</x-table.th>
</tr>
</thead>
<x-table.body>
@ -107,6 +117,24 @@
@endif
</x-table.td>
@endif
<td></td>
@if($entry->audition->hasFlag('seats_published'))
<td>
@if($entry->seat)
{{ $entry->seat->ensemble->name }} - {{ $entry->seat->seat }}
@else
@if($entry->hasFlag('declined'))
Declined
@else
Not Seated
@endif
@endif
</td>
<td>
{{ $entry->rank('seating', false) }}
</td>
@endif
<x-table.td for_button>
@php

View File

@ -13,6 +13,11 @@
<x-card.list.row class="!py-3 ml-3">{{ $audition->name }}</x-card.list.row>
</a>
@endforeach
@foreach($room->prelimAuditions as $prelimAudition)
<a href="{{ route('judging.prelimEntryList', $prelimAudition) }}">
<x-card.list.row class="!py-3 ml-3">{{ $prelimAudition->audition->name }} Prelims</x-card.list.row>
</a>
@endforeach
</x-card.list.body>
</x-card.card>
@endforeach

View File

@ -0,0 +1,56 @@
<x-layout.app>
@php
$oldScores = session()->get('oldScores') ?? null;
@endphp
<x-slot:page_title>Prelim Score Entry</x-slot:page_title>
<x-card.card class="mx-auto max-w-md">
<x-card.heading>
{{ $entry->audition->name }} {{ $entry->draw_number }}
<x-slot:subheading>
<ul class="mt-.5 max-w-2xl text-sm leading-6 text-gray-500">
<li>All Scores must be complete</li>
<li>You may enter zero</li>
<li>Whole numbers only</li>
</ul>
</x-slot:subheading>
</x-card.heading>
<x-form.form method="POST" action="{{ route($formRoute, $entry) }}">
@method($formMethod)
<x-card.list.body class="mt-1">
@foreach($entry->audition->prelimDefinition->scoringGuide->subscores()->orderBy('display_order')->get() as $subscore)
@php
if($oldScores) {
$value = $oldScores['score'][$subscore->id];
} elseif ($oldSheet) {
$value = $oldSheet[$subscore->id]['score'];
} else {
$value = '';
}
@endphp
<li class="py-2">
<x-form.field
name="score[{{$subscore->id}}]"
type="number"
placeholder="{{$subscore->name}}"
:value="$value"
max="{{ $subscore->max_score }}"
required
>
<x-slot:label>{{ $subscore->name }} <span class="text-xs text-base text-gray-400 pl-3">max: {{$subscore->max_score}}</span></x-slot:label>
</x-form.field>
</li>
@endforeach
</x-card.list.body>
<x-form.footer>
<x-form.button class="mb-5">Save Scores</x-form.button>
</x-form.footer>
</x-form.form>
</x-card.card>
</x-layout.app>

View File

@ -0,0 +1,58 @@
@php use Illuminate\Support\Facades\Auth; @endphp
<x-layout.app>
<x-slot:page_title>Judging Dashboard</x-slot:page_title>
<x-card.card>
<x-card.heading>
{{ $prelimDefinition->audition->name }} Prelims
@if($published)
<x-slot:subheading class="text-red-500">Results are published. Scores cannot be changed.
</x-slot:subheading>
@endif
</x-card.heading>
<x-table.table>
<thead>
<tr>
<x-table.th :sortable="false"><a href="{{ url()->current() }}">Entry</a></x-table.th>
@foreach($subscores as $subscore)
<x-table.th :sortable="false" class="hidden md:table-cell">{{ $subscore->name }}</x-table.th>
@endforeach
<x-table.th :sortable="true">Timestamp</x-table.th>
</tr>
</thead>
<x-table.body>
@foreach($entries as $entry)
{{-- @continue($entry->hasFlag('no_show'))--}}
<tr>
<x-table.td>
@if(! $published && ! $entry->hasFlag('no_show') && $entry->scoreSheets()->count() < 1)
<a href="{{ route('judging.prelimScoreEntryForm', $entry) }}">
@endif
{{ $prelimDefinition->audition->name }} {{ $entry->draw_number }}
@if($entry->hasFlag('no_show'))
<p class="text-red-600">No Show</p>
@endif
@if($entry->scoreSheets()->count() > 0)
<p class="text-green-600">Has Finals Scores</p>
@endif
@if(! $published && ! $entry->hasFlag('no_show') && $entry->scoreSheets()->count() < 1)
</a>
@endif
</x-table.td>
@foreach($subscores as $subscore)
<x-table.td class="hidden md:table-cell">
@if($prelimScoresheets->has($entry->id))
{{ $prelimScoresheets[$entry->id]->subscores[$subscore->id]['score'] }}
@endif
</x-table.td>
@endforeach
<x-table.td>
@if($prelimScoresheets->has($entry->id))
{{ $prelimScoresheets[$entry->id]->created_at->setTimezone('America/Chicago')->format('m/d/y H:i') }}
@endif
</x-table.td>
</tr>
@endforeach
</x-table.body>
</x-table.table>
</x-card.card>
</x-layout.app>

View File

@ -0,0 +1,76 @@
<x-layout.app>
<x-slot:page_title>Monitor Dashboard</x-slot:page_title>
<x-card.card class="max-w-lg mx-auto mb-5">
<x-card.heading>
Audition Status
<x-slot:right_side>
<x-form.select name="audition_id" onchange="if (this.value) window.location.href = this.value">
<option value="" disabled hidden @if(! $audition) selected @endif>Choose Audition...</option>
@foreach ($auditions as $menuAudition)
@continue($menuAudition->hasFlag('seats_published') || $menuAudition->hasFlag('advance_published'))
<option @if($audition && $audition->id === $menuAudition->id) selected @endif
value="{{ route('monitor.auditionStatus', $menuAudition) }}">{{$menuAudition->name}}</option>
@endforeach
</x-form.select>
</x-slot:right_side>
</x-card.heading>
@if($audition)
<x-table.table>
<thead>
<tr>
<x-table.th>Entry</x-table.th>
@if($audition->prelimDefinition)
<x-table.th>Prelim<br/>Scores</x-table.th>
@endif
<x-table.th>Finals<br/>Scores</x-table.th>
<x-table.th></x-table.th>
</tr>
</thead>
<x-table.body>
@foreach($entries as $entry)
<tr>
<x-table.td>
{{ $audition->name }} {{ $entry->draw_number }}<br class="md:hidden">
@if($audition->prelimDefinition && ! $entry->hasFlag('no_show'))
@if($entry->hasFlag('failed_prelim'))
<span
class="inline-flex items-center rounded-md bg-red-100 px-2 py-1 text-sm font-medium text-red-700 ">Failed</span>
@elseif($entry->hasFlag('passed_prelim'))
<span
class="inline-flex items-center rounded-md bg-green-100 px-2 py-1 text-sm font-medium text-green-700">Passed</span>
@else
<span
class="inline-flex items-center rounded-md bg-yellow-100 px-2 py-1 text-sm font-medium text-yellow-800 ">Pending</span>
@endif
@elseif($entry->hasFlag('no_show'))
<span
class="inline-flex items-center rounded-md bg-red-100 px-2 py-1 text-sm font-medium text-red-700 ">No-Show</span>
@endif
</x-table.td>
@if($audition->prelimDefinition)
<x-table.td>
{{ $entry->prelim_score_sheets_count }}
</x-table.td>
@endif
<x-table.td>
{{ $entry->score_sheets_count }}
</x-table.td>
<x-table.td>
@if($entry->prelim_score_sheets_count < 1 && $entry->score_sheets_count < 1 && ! $entry->hasFlag('no_show') && ! $audition->hasFlag('seats_published'))
@include('monitor.noshow_modal')
@endif
@if($entry->hasFlag('no_show') && ! $audition->hasFlag('seats_published'))
@include('monitor.remove_nowshow_modal')
@endif
</x-table.td>
</tr>
@endforeach
</x-table.body>
</x-table.table>
@endif
</x-card.card>
</x-layout.app>

View File

@ -0,0 +1,19 @@
<x-modal>
<x-slot:button_text>
<x-form.button href="#">Mark No-Show</x-form.button>
</x-slot:button_text>
<x-slot:title>Mark {{ $audition->name }} {{ $entry->draw_number }}
as a no-show
</x-slot:title>
Confirm that you would like to mark this entry as a no-show<br>
{{ $audition->name }} {{ $entry->draw_number }}<br>
Entry ID: {{ $entry->id }}<br>
Name: {{ $entry->student->full_name() }}<br>
School: {{ $entry->student->school->name }}
<hr class="mt-3">
<x-form.form method="POST" action="{{ route('monitor.toggleNoShow', $entry) }}">
<x-form.button class="mt-3" type="submit">Confirm
No-Show<br>{{ $audition->name }} {{ $entry->draw_number }}
</x-form.button>
</x-form.form>
</x-modal>

View File

@ -0,0 +1,18 @@
<x-modal>
<x-slot:button_text>
<x-form.button href="#">Undo No-Show</x-form.button>
</x-slot:button_text>
<x-slot:title>Remove No-Show Flag for {{ $audition->name }} {{ $entry->draw_number }}
</x-slot:title>
Confirm that you would like to remove the no-show flag for this entry<br>
{{ $audition->name }} {{ $entry->draw_number }}<br>
Entry ID: {{ $entry->id }}<br>
Name: {{ $entry->student->full_name() }}<br>
School: {{ $entry->student->school->name }}
<hr class="mt-3">
<x-form.form method="POST" action="{{ route('monitor.toggleNoShow', $entry) }}">
<x-form.button class="mt-3" type="submit">Remove
No-Show Flag<br>{{ $audition->name }} {{ $entry->draw_number }}
</x-form.button>
</x-form.form>
</x-modal>

View File

@ -13,8 +13,8 @@
<x-form.select name="grade">
<x-slot:label>Grade</x-slot:label>
@php($n = min(Audition::min('minimum_grade'),NominationEnsemble::min('minimum_grade')))
@php($maxGrade = max(Audition::max('maximum_grade'), NominationEnsemble::max('maximum_grade')))
@php($n = minimumStudentGrade())
@php($maxGrade = maximumStudentGrade())
@while($n <= $maxGrade)
<option value="{{ $n }}">{{ $n }}</option>
@php($n++);

View File

@ -3,7 +3,17 @@
<x-slot:page_title>Entry Score Sheet</x-slot:page_title>
<x-card.card class="mx-auto max-w-7xl">
<x-card.heading>
{{ $entry->audition->name }} #{{ $entry->draw_number }}
{{ $entry->audition->name }} #{{ $entry->draw_number }} FINALS SCORES
@if($entry->hasFlag('failed_prelim'))
<span
class="inline-flex items-center rounded-md bg-red-100 px-2 py-1 text-sm font-medium text-red-700 ">Failed Prelim</span>
@elseif($entry->hasFlag('passed_prelim'))
<span
class="inline-flex items-center rounded-md bg-green-100 px-2 py-1 text-sm font-medium text-green-700">Passed Prelim</span>
@elseif($entry->audition->prelimDefinition)
<span
class="inline-flex items-center rounded-md bg-yellow-100 px-2 py-1 text-sm font-medium text-yellow-800 ">Prelim Pending</span>
@endif
<x-slot:subheading>ID #{{ $entry->id }}</x-slot:subheading>
<x-slot:right_side class="text-right">
<p>{{ $entry->student->full_name() }}</p>
@ -11,7 +21,8 @@
</x-slot:right_side>
</x-card.heading>
<x-form.form method="POST" id='scoreForm' action="{{ route('scores.saveEntryScoreSheet',[ 'entry' => $entry->id]) }}">
<x-form.form method="POST" id='scoreForm'
action="{{ route('scores.saveEntryScoreSheet',[ 'entry' => $entry->id]) }}">
<x-table.table>
<thead>
<tr>
@ -33,7 +44,7 @@
<x-table.body :sortable="false">
@foreach($judges as $judge)
@php($existingSheet = $existing_sheets[$judge->id] ?? null)
<tr >
<tr>
<x-table.td>{{ $judge->full_name() }}</x-table.td>
@foreach($subscores as $subscore)
<x-table.td>
@ -48,11 +59,11 @@
value="{{ $existingSheet->getSubscore($subscore->id) }}"
@endif
required
{{-- onchange="judge{{$judge->id}}sum()"--}}
{{-- onchange="judge{{$judge->id}}sum()"--}}
>
</x-table.td>
@endforeach
<x-table.td >
<x-table.td>
<p id="judge{{ $judge->id }}total" class="pr-3">
0.000
</p>
@ -67,35 +78,145 @@
</x-form.form>
</x-card.card>
@if($entry->audition->prelimDefinition)
<x-card.card class="mx-auto max-w-7xl mt-5">
<x-card.heading>
{{ $entry->audition->name }} #{{ $entry->draw_number }} PRELIM SCORES
<x-slot:subheading>ID #{{ $entry->id }}</x-slot:subheading>
<x-slot:right_side class="text-right">
<p>{{ $entry->student->full_name() }}</p>
<p>{{ $entry->student->school->name }}</p>
</x-slot:right_side>
</x-card.heading>
<x-form.form method="POST" id='scoreForm'
action="{{ route('scores.savePrelimEntryScoreSheet',[ 'entry' => $entry->id]) }}">
<x-table.table>
<thead>
<tr>
<x-table.th>Judges</x-table.th>
@foreach($prelim_subscores as $subscore)
<x-table.th>
<div class="">
<div>{{ $subscore->name }}</div>
<div class="text-xs text-gray-500">
<span>Max: {{ $subscore->max_score }}</span>
<span class="pl-2">Weight: {{ $subscore->weight }}</span>
</div>
</div>
</x-table.th>
@endforeach
<x-table.th>Total</x-table.th>
</tr>
</thead>
<x-table.body :sortable="false">
@foreach($prelim_judges as $judge)
@php($existingSheet = $existing_prelim_sheets[$judge->id] ?? null)
<tr>
<x-table.td>{{ $judge->full_name() }}</x-table.td>
@foreach($prelim_subscores as $subscore)
<x-table.td>
<input type="number"
max="{{ $subscore->max_score }}"
id="j{{ $judge->id }}ss{{ $subscore->id }}"
name="judge{{ $judge->id }}[{{ $subscore->id }}]"
class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6 judge{{$judge->id}}score"
@if($oldScores)
value="{{ $oldScores['judge'.$judge->id][$subscore->id] }}"
@elseif($existingSheet)
value="{{ $existingSheet->getSubscore($subscore->id) }}"
@endif
required
{{-- onchange="judge{{$judge->id}}sum()"--}}
>
</x-table.td>
@endforeach
<x-table.td>
<p id="judge{{ $judge->id }}total" class="pr-3">
0.000
</p>
</x-table.td>
</tr>
@endforeach
</x-table.body>
</x-table.table>
<x-form.footer class="mb-3">
<x-form.button>Save Scores</x-form.button>
</x-form.footer>
</x-form.form>
</x-card.card>
@endif
<script>
function calculateTotal(judgeId) {
let total = 0;
let totalWeights = 0;
let maxPossible = 0;
let thisSubscore
@foreach($subscores as $subscore)
thisSubscore = parseFloat(document.getElementById("j" + judgeId + "ss{{ $subscore->id }}").value) * {{ $subscore->weight }};
thisSubscore = parseFloat(document.getElementById("j" + judgeId + "ss{{ $subscore->id }}").value) * {{ $subscore->weight }};
if (!isNaN(thisSubscore)) {
total += thisSubscore;
}
totalWeights += {{ $subscore->weight }};
maxPossible += {{ $subscore->weight * $subscore->max_score }};
@endforeach
let finalTotal = (total / totalWeights).toFixed(3);
let finalTotal = ((total / maxPossible) * 100).toFixed(3);
document.getElementById('judge' + judgeId + 'total').innerHTML = finalTotal;
}
@foreach($judges as $judge)
document.querySelectorAll('.judge' + {{ $judge->id }} + 'score').forEach(function(el) {
el.addEventListener('change', function() {
document.querySelectorAll('.judge' + {{ $judge->id }} + 'score').forEach(function (el) {
el.addEventListener('change', function () {
calculateTotal({{ $judge->id }});
});
});
@endforeach
window.onload = function() {
window.onload = function () {
// Call the function for each judge
@foreach($judges as $judge)
calculateTotal({{ $judge->id }});
@endforeach
@foreach($judges as $judge)
calculateTotal({{ $judge->id }});
@if($entry->audition->prelimDefinition)
@foreach($prelim_judges as $judge)
calculatePrelimTotal({{ $judge->id }});
@endforeach
@endif
@endforeach
};
</script>
@if($entry->audition->prelimDefinition)
<script>
function calculatePrelimTotal(judgeId) {
let total = 0;
let totalWeights = 0;
let maxPossible = 0;
let thisSubscore
@foreach($prelim_subscores as $subscore)
thisSubscore = parseFloat(document.getElementById("j" + judgeId + "ss{{ $subscore->id }}").value) * {{ $subscore->weight }};
if (!isNaN(thisSubscore)) {
total += thisSubscore;
}
totalWeights += {{ $subscore->weight }};
maxPossible += {{ $subscore->weight * $subscore->max_score }};
@endforeach
let finalTotal = ((total / maxPossible) * 100).toFixed(3);
document.getElementById('judge' + judgeId + 'total').innerHTML = finalTotal;
}
@foreach($prelim_judges as $judge)
document.querySelectorAll('.judge' + {{ $judge->id }} + 'score').forEach(function (el) {
el.addEventListener('change', function () {
calculatePrelimTotal({{ $judge->id }});
});
});
@endforeach
</script>
@endif
</x-layout.app>

View File

@ -6,6 +6,7 @@
<x-table.th>Draw #</x-table.th>
<x-table.th>ID</x-table.th>
<x-table.th>Student</x-table.th>
<x-table.th>Score</x-table.th>
</tr>
</thead>
<tbody>
@ -17,6 +18,7 @@
<span>{{ $entry->student->full_name() }}</span>
<span class="text-xs text-gray-400">{{ $entry->student->school->name }}</span>
</x-table.td>
<x-table.td>{{ $entry->prelimTotalScore() }}</x-table.td>
</tr>
@endforeach
</tbody>

View File

@ -8,6 +8,9 @@
<x-table.th>Draw #</x-table.th>
<x-table.th>Student</x-table.th>
<x-table.th>Doubler</x-table.th>
@if($audition->prelimDefinition)
<x-table.th>Prelim Score</x-table.th>
@endif
<x-table.th>Total Score
@if($audition->bonusScore()->count() > 0)
<br>
@ -59,6 +62,9 @@
</x-table.td>
@if($audition->prelimDefinition)
<x-table.td>{{ round($entry->prelimTotalScore(),2) }}</x-table.td>
@endif
<x-table.td class="align-top">
@if($audition->bonusScore()->count() > 0)
@if($entry->totalScore->bonus_total)

View File

@ -11,6 +11,7 @@ use App\Http\Controllers\Admin\EntryController;
use App\Http\Controllers\Admin\EventController;
use App\Http\Controllers\Admin\ExportEntriesController;
use App\Http\Controllers\Admin\ExportResultsController;
use App\Http\Controllers\Admin\PrelimDefinitionController;
use App\Http\Controllers\Admin\PrintCards;
use App\Http\Controllers\Admin\PrintRoomAssignmentsController;
use App\Http\Controllers\Admin\PrintSignInSheetsController;
@ -18,6 +19,7 @@ use App\Http\Controllers\Admin\PrintStandNameTagsController;
use App\Http\Controllers\Admin\RecapController;
use App\Http\Controllers\Admin\RoomController;
use App\Http\Controllers\Admin\SchoolController;
use App\Http\Controllers\Admin\SchoolEmailDomainController;
use App\Http\Controllers\Admin\ScoringGuideController;
use App\Http\Controllers\Admin\StudentController;
use App\Http\Controllers\Admin\UserController;
@ -36,7 +38,7 @@ Route::middleware(['auth', 'verified', CheckIfAdmin::class])->prefix('admin/')->
// Year-end procedures
Route::get('/year_end_procedures', [YearEndResetController::class, 'index'])->name('admin.year_end_procedures');
Route::post('/year_end_procedures', [YearEndResetController::class, 'execute'])->name('admin.year_end_procedures');
Route::post('/year_end_procedures', [YearEndResetController::class, 'execute'])->name('admin.execute_year_end_procedures');
Route::post('/auditions/roomUpdate', [
AuditionController::class, 'roomUpdate',
@ -65,6 +67,7 @@ Route::middleware(['auth', 'verified', CheckIfAdmin::class])->prefix('admin/')->
Route::get('/judges', 'judges')->name('admin.bonus-scores.judges');
Route::delete('{bonusScore}/judges/', 'removeJudge')->name('admin.bonus-scores.judges.remove');
Route::post('{bonusScore}/judges/', 'assignJudge')->name('admin.bonus-scores.judges.assign');
Route::patch('/{bonusScore}', 'update')->name('admin.bonus-scores.update');
});
@ -86,6 +89,7 @@ Route::middleware(['auth', 'verified', CheckIfAdmin::class])->prefix('admin/')->
Route::get('/', 'index')->name('admin.events.index');
Route::post('/', 'store')->name('admin.events.store');
Route::delete('/{event}', 'destroy')->name('admin.events.destroy');
Route::patch('/{event}', 'update')->name('admin.events.update');
});
// Admin Rooms Routes
@ -130,6 +134,8 @@ Route::middleware(['auth', 'verified', CheckIfAdmin::class])->prefix('admin/')->
Route::patch('/{audition}', 'update')->name('admin.auditions.update');
Route::post('/reorder', 'reorder')->name('admin.auditions.reorder');
Route::delete('/{audition}', 'destroy')->name('admin.auditions.destroy');
Route::get('/bulk-edit', 'bulkEditForm')->name('admin.auditions.bulkEditForm');
Route::post('/bulk-edit', 'bulkUpdate')->name('admin.auditions.bulkEdit');
});
// Admin Audition Draw Routes
@ -163,6 +169,8 @@ Route::middleware(['auth', 'verified', CheckIfAdmin::class])->prefix('admin/')->
});
// Admin School Routes
Route::get('/schools/email_domains',
[SchoolEmailDomainController::class, 'index'])->name('admin.schools.email_domains');
Route::prefix('schools')->controller(SchoolController::class)->group(function () {
Route::post('/{school}/add_domain', 'add_domain')->name('admin.schools.add_domain');
Route::get('/', 'index')->name('admin.schools.index');
@ -186,6 +194,7 @@ Route::middleware(['auth', 'verified', CheckIfAdmin::class])->prefix('admin/')->
Route::get('/{user}/edit', 'edit')->name('admin.users.edit');
Route::patch('/{user}', 'update')->name('admin.users.update');
Route::delete('/{user}', 'destroy')->name('admin.users.destroy');
Route::post('/{user}/set_password', 'setPassword')->name('admin.users.setPassword');
});
// Admin Card Routes
@ -203,4 +212,14 @@ Route::middleware(['auth', 'verified', CheckIfAdmin::class])->prefix('admin/')->
// Print Room and Judge Assignment Report
Route::get('room_assignment_report',
PrintRoomAssignmentsController::class)->name('admin.print_room_assignment_report');
// PrelimDefinition Routes
Route::prefix('prelim_definitions')->controller(PrelimDefinitionController::class)->group(function () {
Route::get('/', 'index')->name('admin.prelim_definitions.index');
Route::get('/new', 'create')->name('admin.prelim_definitions.create');
Route::post('/', 'store')->name('admin.prelim_definitions.store');
Route::get('/{prelimDefinition}', 'edit')->name('admin.prelim_definitions.edit');
Route::patch('/{prelimDefinition}', 'update')->name('admin.prelim_definitions.update');
Route::delete('/{prelimDefinition}', 'destroy')->name('admin.prelim_definitions.destroy');
});
});

View File

@ -5,6 +5,7 @@ use App\Http\Controllers\Judging\BonusScoreEntryController;
use App\Http\Controllers\Judging\BonusScoreEntryListController;
use App\Http\Controllers\Judging\BonusScoreRecordController;
use App\Http\Controllers\Judging\JudgingController;
use App\Http\Controllers\Judging\PrelimJudgingController;
use App\Http\Middleware\CheckIfCanJudge;
use Illuminate\Support\Facades\Route;
@ -16,6 +17,14 @@ Route::middleware(['auth', 'verified', CheckIfCanJudge::class])->prefix('judging
Route::patch('/entry/{entry}', 'updateScoreSheet')->name('judging.updateScoreSheet');
});
// Prelim Audition Related Routes
Route::middleware(['auth', 'verified', CheckIfCanJudge::class])->prefix('judging/prelims')->controller(PrelimJudgingController::class)->group(function () {
Route::get('/{prelimDefinition}', 'prelimEntryList')->name('judging.prelimEntryList');
route::get('/enterScore/{entry}', 'prelimScoreEntryForm')->name('judging.prelimScoreEntryForm');
route::post('/enterScore/{entry}', 'savePrelimScoreSheet')->name('judging.savePrelimScoreSheet');
route::patch('/enterScore/{entry}', 'updatePrelimScoreSheet')->name('judging.updatePrelimScoreSheet');
});
// Bonus score judging routes
Route::middleware(['auth', 'verified', CheckIfCanJudge::class])->prefix('judging/bonus_scores')->group(function () {
Route::get('/{audition}', BonusScoreEntryListController::class)->name('judging.bonusScore.EntryList'); // List of entries in an audition

View File

@ -21,6 +21,7 @@ Route::middleware(['auth', 'verified', CheckIfCanTab::class])->group(function ()
Route::get('/choose_entry', 'chooseEntry')->name('scores.chooseEntry');
Route::get('/entry', 'entryScoreSheet')->name('scores.entryScoreSheet');
Route::post('/entry/{entry}', 'saveEntryScoreSheet')->name('scores.saveEntryScoreSheet');
Route::post('/entry/prelim/{entry}', 'savePrelimEntryScoreSheet')->name('scores.savePrelimEntryScoreSheet');
Route::delete('/{score}', 'destroyScore')->name('scores.destroy');
});

View File

@ -64,5 +64,5 @@ Route::middleware([
'auth', 'verified',
])->controller(DoublerRequestController::class)->prefix('doubler_request')->group(function () {
Route::get('/', 'index')->name('doubler_request.index');
Route::post('/', 'makeRequest')->name('doubler_request.make_request');
Route::post('/makeRequest', 'makeRequest')->name('doubler_request.make_request');
});

View File

@ -31,8 +31,8 @@ Route::prefix('filters')->middleware(['auth', 'verified'])->controller(FilterCon
// Monitor Related Routes
Route::prefix('monitor')->middleware(['auth', 'verified'])->controller(MonitorController::class)->group(function () {
Route::get('/', 'index')->name('monitor.index');
Route::post('/enter_flag', 'flagForm')->name('monitor.enterFlag');
Route::post('enter_flag/{entry}', 'storeFlag')->name('monitor.storeFlag');
Route::get('/audition/{audition}', 'auditionStatus')->name('monitor.auditionStatus');
Route::post('/toggleNoShow/{entry}', 'toggleNoShow')->name('monitor.toggleNoShow');
});
//Route::get('/my_school', [SchoolController::class, 'my_school'])->middleware('auth','verified');

View File

@ -0,0 +1,174 @@
<?php
use App\Actions\Tabulation\CheckPrelimResult;
use App\Actions\Tabulation\EnterPrelimScore;
use App\Exceptions\AuditionAdminException;
use App\Models\Entry;
use App\Models\PrelimDefinition;
use App\Models\PrelimScoreSheet;
use App\Models\Room;
use App\Models\ScoringGuide;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->tabulator = app(CheckPrelimResult::class);
});
it('throws an exception if the provided entry does not exist', function () {
$entry = Entry::factory()->make();
($this->tabulator)($entry);
})->throws(AuditionAdminException::class, 'Entry does not exist');
it('throws an exception if the entries audition does not have a prelim', function () {
$entry = Entry::factory()->create();
($this->tabulator)($entry);
})->throws(AuditionAdminException::class, 'Entry does not have a prelim');
it('does not change an existing decision unless forced', function () {
$entry = Entry::factory()->create();
PrelimDefinition::create([
'audition_id' => $entry->audition_id,
'passing_score' => 80,
]);
$entry->addFlag('failed_prelim');
$result = ($this->tabulator)($entry);
expect($result)->toBe('noChange');
$entry2 = Entry::factory()->create(['audition_id' => $entry->audition_id]);
$entry2->addFlag('passed_prelim');
$result = ($this->tabulator)($entry);
expect($result)->toBe('noChange');
});
it('doesnt make a decision if there are no judges assigned', function () {
$entry = Entry::factory()->create();
PrelimDefinition::create([
'audition_id' => $entry->audition_id,
'passing_score' => 80,
]);
$result = ($this->tabulator)($entry);
expect($result)->toBe('noJudgesAssigned');
});
it('doesnt make a decision if there are insufficient scores', function () {
$prelimRoom = Room::factory()->create();
$judge1 = User::factory()->create();
$judge2 = User::factory()->create();
$prelimRoom->addJudge($judge1);
$prelimRoom->addJudge($judge2);
$prelimScoringGuide = ScoringGuide::factory()->create();
$entry = Entry::factory()->create();
PrelimDefinition::create([
'audition_id' => $entry->audition_id,
'passing_score' => 80,
'room_id' => $prelimRoom->id,
'scoring_guide_id' => $prelimScoringGuide->id,
]);
$result = ($this->tabulator)($entry);
expect($result)->toBe('missing2scores');
app(EnterPrelimScore::class)($judge1, $entry, [
1 => 50,
2 => 50,
3 => 50,
4 => 50,
5 => 50,
]);
$result = ($this->tabulator)($entry);
expect($result)->toBe('missing1scores');
});
it('correctly identifies passing entries', function () {
$prelimRoom = Room::factory()->create();
$judge1 = User::factory()->create();
$judge2 = User::factory()->create();
$prelimRoom->addJudge($judge1);
$prelimRoom->addJudge($judge2);
$prelimScoringGuide = ScoringGuide::factory()->create();
$entry = Entry::factory()->create();
PrelimDefinition::create([
'audition_id' => $entry->audition_id,
'passing_score' => 80,
'room_id' => $prelimRoom->id,
'scoring_guide_id' => $prelimScoringGuide->id,
]);
PrelimScoreSheet::create([
'entry_id' => $entry->id,
'user_id' => $judge1->id,
'subscores' => [],
'total' => 85,
]);
PrelimScoreSheet::create([
'entry_id' => $entry->id,
'user_id' => $judge2->id,
'subscores' => [],
'total' => 75,
]);
$result = ($this->tabulator)($entry);
expect($result)->toBe('markedPassed');
});
it('correctly identifies failing entries', function () {
$prelimRoom = Room::factory()->create();
$judge1 = User::factory()->create();
$judge2 = User::factory()->create();
$prelimRoom->addJudge($judge1);
$prelimRoom->addJudge($judge2);
$prelimScoringGuide = ScoringGuide::factory()->create();
$entry = Entry::factory()->create();
PrelimDefinition::create([
'audition_id' => $entry->audition_id,
'passing_score' => 81,
'room_id' => $prelimRoom->id,
'scoring_guide_id' => $prelimScoringGuide->id,
]);
PrelimScoreSheet::create([
'entry_id' => $entry->id,
'user_id' => $judge1->id,
'subscores' => [],
'total' => 85,
]);
PrelimScoreSheet::create([
'entry_id' => $entry->id,
'user_id' => $judge2->id,
'subscores' => [],
'total' => 75,
]);
$result = ($this->tabulator)($entry);
expect($result)->toBe('markedFailed');
});
it('can force a recalculation', function () {
$prelimRoom = Room::factory()->create();
$judge1 = User::factory()->create();
$judge2 = User::factory()->create();
$prelimRoom->addJudge($judge1);
$prelimRoom->addJudge($judge2);
$prelimScoringGuide = ScoringGuide::factory()->create();
$entry = Entry::factory()->create();
$entry->addFlag('failed_prelim');
PrelimDefinition::create([
'audition_id' => $entry->audition_id,
'passing_score' => 80,
'room_id' => $prelimRoom->id,
'scoring_guide_id' => $prelimScoringGuide->id,
]);
PrelimScoreSheet::create([
'entry_id' => $entry->id,
'user_id' => $judge1->id,
'subscores' => [],
'total' => 85,
]);
PrelimScoreSheet::create([
'entry_id' => $entry->id,
'user_id' => $judge2->id,
'subscores' => [],
'total' => 75,
]);
$result = ($this->tabulator)($entry);
expect($result)->toBe('noChange');
$result = ($this->tabulator)($entry, true);
expect($result)->toBe('markedPassed');
});

View File

@ -0,0 +1,166 @@
<?php
use App\Actions\Tabulation\EnterPrelimScore;
use App\Exceptions\AuditionAdminException;
use App\Models\Audition;
use App\Models\AuditLogEntry;
use App\Models\Entry;
use App\Models\PrelimDefinition;
use App\Models\PrelimScoreSheet;
use App\Models\Room;
use App\Models\ScoringGuide;
use App\Models\SubscoreDefinition;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
beforeEach(function () {
// Generate Scoring Guide
$this->prelimScoringGuide = ScoringGuide::factory()->create(['id' => 1000]);
SubscoreDefinition::create([
'id' => 1001,
'scoring_guide_id' => $this->prelimScoringGuide->id,
'name' => 'Scale',
'max_score' => 100,
'weight' => 1,
'display_order' => 1,
'tiebreak_order' => 3,
'for_seating' => '1',
'for_advance' => '0',
]);
SubscoreDefinition::create([
'id' => 1002,
'scoring_guide_id' => $this->prelimScoringGuide->id,
'name' => 'Etude 1',
'max_score' => 100,
'weight' => 2,
'display_order' => 2,
'tiebreak_order' => 1,
'for_seating' => '1',
'for_advance' => '1',
]);
SubscoreDefinition::create([
'id' => 1003,
'scoring_guide_id' => $this->prelimScoringGuide->id,
'name' => 'Etude 2',
'max_score' => 100,
'weight' => 2,
'display_order' => 3,
'tiebreak_order' => 2,
'for_seating' => '0',
'for_advance' => '1',
]);
SubscoreDefinition::where('id', '<', 900)->delete();
$this->finalsRoom = Room::factory()->create();
$this->prelimRoom = Room::factory()->create();
$this->audition = Audition::factory()->create(['room_id' => $this->finalsRoom->id]);
$this->prelimDefinition = PrelimDefinition::create([
'room_id' => $this->prelimRoom->id,
'audition_id' => $this->audition->id,
'scoring_guide_id' => $this->prelimScoringGuide->id,
'passing_score' => 60,
]);
$this->judge1 = User::factory()->create();
$this->judge2 = User::factory()->create();
$this->prelimRoom->judges()->attach($this->judge1->id);
$this->prelimRoom->judges()->attach($this->judge2->id);
$this->entry1 = Entry::factory()->create(['audition_id' => $this->audition->id]);
$this->entry2 = Entry::factory()->create(['audition_id' => $this->audition->id]);
$this->scribe = app(EnterPrelimScore::class);
$this->possibleScoreArray = [
1001 => 10,
1002 => 11,
1003 => 12,
];
$this->anotherPossibleScoreArray = [
1001 => 20,
1002 => 21,
1003 => 22,
];
});
it('can enter a prelim score', function () {
($this->scribe)($this->judge1, $this->entry1, $this->possibleScoreArray);
expect($this->entry1->prelimScoreSheets()->count())->toBe(1)
->and($this->entry1->prelimScoreSheets()->first()->total)->toBe(11.2);
($this->scribe)($this->judge1, $this->entry2, $this->anotherPossibleScoreArray);
expect($this->entry2->prelimScoreSheets()->count())->toBe(1)
->and($this->entry2->prelimScoreSheets()->first()->total)->toBe(21.2);
});
it('will not enter a score for a judge that does not exist', function () {
$fakeJudge = User::factory()->make();
($this->scribe)($fakeJudge, $this->entry1, $this->possibleScoreArray);
})->throws(AuditionAdminException::class, 'User does not exist');
it('will not enter a score for an entry that does not exist', function () {
$fakeEntry = Entry::factory()->make();
($this->scribe)($this->judge1, $fakeEntry, $this->possibleScoreArray);
})->throws(AuditionAdminException::class, 'Entry does not exist');
it('will not score an entry if the audition seats are published', function () {
$this->audition->addFlag('seats_published');
($this->scribe)($this->judge1, $this->entry1, $this->possibleScoreArray);
})->throws(AuditionAdminException::class, 'Cannot score an entry in an audition where seats are published');
it('will not score an entry if the judge is not assigned to judge the entry', function () {
$fakeJudge = User::factory()->create();
($this->scribe)($fakeJudge, $this->entry1, $this->possibleScoreArray);
})->throws(AuditionAdminException::class, 'This judge is not assigned to judge this entry');
it('can modify an existing score sheet', function () {
($this->scribe)($this->judge1, $this->entry1, $this->possibleScoreArray);
$scoreSheet = PrelimScoreSheet::first();
($this->scribe)($this->judge1, $this->entry1, $this->anotherPossibleScoreArray, $scoreSheet);
expect($this->entry1->prelimScoreSheets()->count())->toBe(1)
->and($this->entry1->prelimScoreSheets()->first()->total)->toBe(21.2);
});
it('will not change the judge on a score sheet', function () {
($this->scribe)($this->judge1, $this->entry1, $this->possibleScoreArray);
$scoreSheet = PrelimScoreSheet::first();
($this->scribe)($this->judge2, $this->entry1, $this->anotherPossibleScoreArray, $scoreSheet);
})->throws(AuditionAdminException::class, 'Existing score sheet is from a different judge');
it('will not accept a second score sheet for a judge ane entry', function () {
($this->scribe)($this->judge1, $this->entry1, $this->possibleScoreArray);
($this->scribe)($this->judge1, $this->entry1, $this->anotherPossibleScoreArray);
})->throws(AuditionAdminException::class, 'That judge has already entered a prelim score for that entry');
it('will not change the entry on a score sheet', function () {
($this->scribe)($this->judge1, $this->entry1, $this->possibleScoreArray);
$scoreSheet = PrelimScoreSheet::first();
($this->scribe)($this->judge1, $this->entry2, $this->anotherPossibleScoreArray, $scoreSheet);
})->throws(AuditionAdminException::class, 'Existing score sheet is for a different entry');
it('will not accept an incorrect number of subscores', function () {
array_pop($this->possibleScoreArray);
($this->scribe)($this->judge1, $this->entry1, $this->possibleScoreArray);
})->throws(AuditionAdminException::class, 'Invalid number of scores');
it('will not accept an invalid subscores', function () {
array_pop($this->possibleScoreArray);
$this->possibleScoreArray[3001] = 100;
($this->scribe)($this->judge1, $this->entry1, $this->possibleScoreArray);
})->throws(AuditionAdminException::class, 'Invalid Score Submission');
it('will. not accept a subscore in excess of its maximum', function () {
$this->possibleScoreArray[1001] = 1500;
($this->scribe)($this->judge1, $this->entry1, $this->possibleScoreArray);
})->throws(AuditionAdminException::class, 'Supplied subscore exceeds maximum allowed');
it('removes a no-show flag from an entry', function () {
$this->entry1->addFlag('no_show');
($this->scribe)($this->judge1, $this->entry1, $this->possibleScoreArray);
expect($this->entry1->hasFlag('no_show'))->toBeFalse();
});
it('logs score entry', function () {
($this->scribe)($this->judge1, $this->entry1, $this->possibleScoreArray);
$logEntry = AuditLogEntry::orderBy('id', 'desc')->first();
expect($logEntry->message)->toStartWith('Entered prelim score for entry id ');
});

Some files were not shown because too many files have changed in this diff Show More