From 2d00473b2c2ae752d8f2c98a47da6dcd994d54f1 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Mon, 10 Jun 2024 01:32:18 -0500 Subject: [PATCH] Progress on tab pages --- app/Actions/Fortify/CreateNewUser.php | 1 - app/Exceptions/Handler.php | 19 ++++++ app/Exceptions/TabulationException.php | 24 +++++++ .../Controllers/Admin/EntryController.php | 10 ++- .../Admin/ScoringGuideController.php | 1 - app/Http/Controllers/EntryController.php | 5 +- app/Http/Controllers/FilterController.php | 1 + .../Tabulation/TabulationController.php | 23 +++++++ app/Models/Audition.php | 48 ++++++++++++++ app/Models/Entry.php | 64 ++++++++++++++++++- app/Models/RoomUser.php | 25 ++++++++ app/Models/ScoreSheet.php | 31 +++++++++ app/Models/ScoringGuide.php | 1 + app/Models/User.php | 6 ++ app/helpers.php | 15 ----- database/seeders/ScoreAllAuditions.php | 42 ++++++++++++ resources/views/admin/entries/edit.blade.php | 26 ++++++++ resources/views/admin/entries/index.blade.php | 7 +- .../components/form/button-nocolor.blade.php | 7 +- .../layout/navbar/menus/tabulation.blade.php | 1 + .../tabulation/auditionSeating.blade.php | 34 ++++++++++ resources/views/tabulation/status.blade.php | 26 ++++++++ resources/views/test.blade.php | 26 +++----- routes/web.php | 3 + 24 files changed, 403 insertions(+), 43 deletions(-) create mode 100644 app/Exceptions/Handler.php create mode 100644 app/Exceptions/TabulationException.php create mode 100644 app/Models/RoomUser.php create mode 100644 database/seeders/ScoreAllAuditions.php create mode 100644 resources/views/tabulation/auditionSeating.blade.php create mode 100644 resources/views/tabulation/status.blade.php diff --git a/app/Actions/Fortify/CreateNewUser.php b/app/Actions/Fortify/CreateNewUser.php index 23126a7..36fa9a9 100644 --- a/app/Actions/Fortify/CreateNewUser.php +++ b/app/Actions/Fortify/CreateNewUser.php @@ -12,7 +12,6 @@ use Illuminate\Support\Facades\Validator; use Illuminate\Validation\Rule; use Laravel\Fortify\Contracts\CreatesNewUsers; use function mb_substr; -use function sendMessage; class CreateNewUser implements CreatesNewUsers { diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php new file mode 100644 index 0000000..872bd89 --- /dev/null +++ b/app/Exceptions/Handler.php @@ -0,0 +1,19 @@ +with('warning', $e->getMessage()); + } + return parent::render($request, $e); + } +} diff --git a/app/Exceptions/TabulationException.php b/app/Exceptions/TabulationException.php new file mode 100644 index 0000000..425943d --- /dev/null +++ b/app/Exceptions/TabulationException.php @@ -0,0 +1,24 @@ +with('error', $this->getMessage()); +// } +// return parent::render($request, $e); + } +} diff --git a/app/Http/Controllers/Admin/EntryController.php b/app/Http/Controllers/Admin/EntryController.php index 06ea82c..31f27a5 100644 --- a/app/Http/Controllers/Admin/EntryController.php +++ b/app/Http/Controllers/Admin/EntryController.php @@ -16,7 +16,7 @@ class EntryController extends Controller public function index() { if(! Auth::user()->is_admin) abort(403); - $filters = session('adminEntryFilters'); + $filters = session('adminEntryFilters') ?? null; $minGrade = Audition::min('minimum_grade'); $maxGrade = Audition::max('maximum_grade'); $auditions = Audition::orderBy('score_order')->get(); @@ -26,6 +26,10 @@ class EntryController extends Controller $entries = Entry::with(['student.school','audition']); $entries->orderBy('updated_at','DESC'); if($filters) { + if($filters['id']) { + $entries->where('id', $filters['id']); + } + if($filters['audition']) { $entries->where('audition_id', $filters['audition']); } @@ -91,7 +95,9 @@ class EntryController extends Controller if(! Auth::user()->is_admin) abort(403); $students = Student::with('school')->orderBy('last_name')->orderBy('first_name')->get(); $auditions = Audition::orderBy('score_order')->get(); - return view('admin.entries.edit', ['entry' => $entry, 'students' => $students, 'auditions' => $auditions]); + $scores = $entry->scoreSheets()->get(); +// return view('admin.entries.edit', ['entry' => $entry, 'students' => $students, 'auditions' => $auditions]); + return view('admin.entries.edit', compact('entry', 'students', 'auditions','scores')); } public function update(Entry $entry) diff --git a/app/Http/Controllers/Admin/ScoringGuideController.php b/app/Http/Controllers/Admin/ScoringGuideController.php index e40309d..ac08428 100644 --- a/app/Http/Controllers/Admin/ScoringGuideController.php +++ b/app/Http/Controllers/Admin/ScoringGuideController.php @@ -12,7 +12,6 @@ use function abort; use function dd; use function request; use function response; -use function sendMessage; class ScoringGuideController extends Controller { diff --git a/app/Http/Controllers/EntryController.php b/app/Http/Controllers/EntryController.php index cde34a7..2932b4f 100644 --- a/app/Http/Controllers/EntryController.php +++ b/app/Http/Controllers/EntryController.php @@ -8,7 +8,6 @@ use App\Models\School; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use function abort; -use function sendMessage; class EntryController extends Controller { @@ -43,7 +42,7 @@ class EntryController extends Controller public function destroy(Request $request, Entry $entry) { $entry->delete(); - sendMessage('The ' . $entry->audition->name . 'entry for ' . $entry->student->full_name(). 'has been deleted.','success'); - return redirect('/entries'); + return redirect('/entries')->with('success','The ' . $entry->audition->name . 'entry for ' . $entry->student->full_name(). 'has been deleted.'); + } } diff --git a/app/Http/Controllers/FilterController.php b/app/Http/Controllers/FilterController.php index bfda9ab..00e2012 100644 --- a/app/Http/Controllers/FilterController.php +++ b/app/Http/Controllers/FilterController.php @@ -9,6 +9,7 @@ class FilterController extends Controller public function adminEntryFilter(Request $request) { $filters = array(); + $filters['id'] = request('id_filter') ?? null; $filters['audition'] = request('audition_filter') ? request('audition_filter') : null; $filters['school'] = request('school_filter') ? request('school_filter') : null; $filters['grade'] = request('grade_filter') ? request('grade_filter') : null; diff --git a/app/Http/Controllers/Tabulation/TabulationController.php b/app/Http/Controllers/Tabulation/TabulationController.php index 16950df..88b8d07 100644 --- a/app/Http/Controllers/Tabulation/TabulationController.php +++ b/app/Http/Controllers/Tabulation/TabulationController.php @@ -8,6 +8,7 @@ use App\Models\Entry; use App\Models\ScoreSheet; use Illuminate\Http\Request; use Illuminate\Support\Facades\Session; +use function compact; use function dump; use function redirect; @@ -18,6 +19,11 @@ class TabulationController extends Controller return view('tabulation.choose_entry'); } + public function destroyScore(ScoreSheet $score) { + $score->delete(); + return redirect()->back()->with('success','Score Deleted'); + } + public function entryScoreSheet(Request $request) { $existing_sheets = []; @@ -74,5 +80,22 @@ class TabulationController extends Controller return redirect()->route('tabulation.chooseEntry')->with('success',count($preparedScoreSheets) . " Scores created"); } + public function status() + { + $auditions = Audition::with('entries.scoreSheets')->with('room.judges')->orderBy('score_order')->get(); + + return view('tabulation.status',compact('auditions')); + } + + public function auditionSeating(Audition $audition) + { +// $entries = $audition->entries()->with(['student','scoreSheets.audition.scoringGuide','audition.room.judges'])->get(); +// $entries = $entries->sortByDesc(function ($entry) { +// return $entry->totalScore(); +// }); + $entries = $audition->rankedEntries()->load('student','scoreSheets.audition.scoringGuide.subscores'); + $judges = $audition->judges(); + return view('tabulation.auditionSeating',compact('audition','entries','judges')); + } } diff --git a/app/Models/Audition.php b/app/Models/Audition.php index b030f4b..ee3a19c 100644 --- a/app/Models/Audition.php +++ b/app/Models/Audition.php @@ -2,11 +2,13 @@ namespace App\Models; +use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasOne; +use phpDocumentor\Reflection\Types\Boolean; use PhpParser\Node\Scalar\String_; use function now; @@ -30,6 +32,7 @@ class Audition extends Model return $this->hasMany(Entry::class); } + public function room(): BelongsTo { return $this->belongsTo(Room::class); @@ -119,4 +122,49 @@ class Audition extends Model } return null; } + +// public function judges() +// { +// // Very inefficient, need a better way +// return User::join('room_user', 'users.id', '=', 'room_user.user_id') +// ->join('rooms', 'room_user.room_id', '=', 'rooms.id') +// ->join('auditions', 'rooms.id', '=', 'auditions.room_id') +// ->where('auditions.id', $this->id) +// ->select('users.*') // avoid getting other tables' columns +// ->get(); +// } + /** + * @return Collection + */ + public function judges() + { + return $this->room->judges; + } + + public function scoredEntries() + { + return $this->entries->filter(function($entry) { + return $entry->scoreSheets->count() >= $this->judges()->count(); + }); + } + + public function rankedEntries() + { + $entries = $this->entries()->with(['audition.scoringGuide.subscores','scoreSheets.judge'])->get(); + $entries = $entries->all(); + usort($entries, function($a,$b) { + $aScores = $a->finalScoresArray(); + $bScores = $b->finalScoresArray(); + + $length = min(count($aScores), count($bScores)); + for ($i=0; $i<$length; $i++) { + if ($aScores[$i] !== $bScores[$i]) { + return $bScores[$i] - $aScores[$i]; + } + } + return 0; + }); + $collection = new \Illuminate\Database\Eloquent\Collection($entries); + return $collection; + } } diff --git a/app/Models/Entry.php b/app/Models/Entry.php index f6ac8c4..f00bc67 100644 --- a/app/Models/Entry.php +++ b/app/Models/Entry.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Exceptions\TabulationException; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -13,6 +14,7 @@ class Entry extends Model { use HasFactory; protected $guarded = []; + protected $hasCheckedScoreSheets = false; public function student(): BelongsTo { @@ -38,10 +40,70 @@ class Entry extends Model public function scoreSheets(): HasMany { return $this->hasMany(ScoreSheet::class); + } + function verifyScoreSheets() + { + if ($this->hasCheckedScoreSheets) return true; + $judges = $this->audition->room->judges; + foreach ($this->scoreSheets as $sheet) { + if (! $judges->contains($sheet->user_id)) { + $invalidJudge = User::find($sheet->user_id); +// redirect ('/tabulation')->with('warning','Invalid scores for entry ' . $this->id . ' exist from ' . $invalidJudge->full_name()); + // Abort execution, and redirect to /tabulation with a warning message + throw new TabulationException('Invalid scores for entry ' . $this->id . ' exist from ' . $invalidJudge->full_name()); + } + } + return true; + } + + public function scoreFromJudge($user): ScoreSheet|null { - return $this->scoreSheets()->where('user_id','=',$user)->first() ?? null; +// return $this->scoreSheets()->where('user_id','=',$user)->first() ?? null; + return $this->scoreSheets->firstWhere('user_id', $user) ?? null; + + } + + public function totalScore() + { + $this->verifyScoreSheets(); + $totalScore = 0; + foreach ($this->scoreSheets as $sheet) + { + $totalScore += $sheet->totalScore(); + } + return $totalScore; + } + + /** + * @throws TabulationException + */ + public function finalScoresArray() + { + $this->verifyScoreSheets(); + $finalScoresArray = []; + $subscoresTiebreakOrder = $this->audition->scoringGuide->subscores->sortBy('tiebreak_order'); + // initialize the return array + foreach ($subscoresTiebreakOrder as $subscore) { + $finalScoresArray[$subscore->id] = 0; + } + // add the subscores from each score sheet + foreach($this->scoreSheets as $sheet) { + foreach($sheet->subscores as $ss) { + $finalScoresArray[$ss['subscore_id']] += $ss['score']; + } + } + // calculate weighted final score + $totalScore = 0; + $totalWeight = 0; + foreach ($subscoresTiebreakOrder as $subscore) { + $totalScore += ($finalScoresArray[$subscore->id] * $subscore->weight); + $totalWeight += $subscore->weight; + } + $totalScore = ($totalScore / $totalWeight); + array_unshift($finalScoresArray,$totalScore); + return $finalScoresArray; } } diff --git a/app/Models/RoomUser.php b/app/Models/RoomUser.php new file mode 100644 index 0000000..061368b --- /dev/null +++ b/app/Models/RoomUser.php @@ -0,0 +1,25 @@ +belongsTo(User::class); + } + + public function judge(): BelongsTo + { + return $this->belongsTo(User::class); + } + + public function room(): BelongsTo + { + return $this->belongsTo(Room::class); + } +} diff --git a/app/Models/ScoreSheet.php b/app/Models/ScoreSheet.php index e86441a..eb1ee73 100644 --- a/app/Models/ScoreSheet.php +++ b/app/Models/ScoreSheet.php @@ -5,6 +5,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\HasOneThrough; class ScoreSheet extends Model { @@ -15,6 +16,7 @@ class ScoreSheet extends Model ]; protected $casts = ['subscores' => 'json']; + protected $with = ['entry','judge','audition.scoringGuide']; public function entry(): BelongsTo { @@ -26,8 +28,37 @@ class ScoreSheet extends Model return $this->belongsTo(User::class, 'user_id'); } + public function audition(): HasOneThrough + { + return $this->hasOneThrough( + Audition::class, // The final model you want to access + Entry::class, // The intermediate model + 'id', // Foreign key on the intermediate model (Entry) + 'id', // Foreign key on the final model (Audition) + 'entry_id', // Local key on the current model (ScoreSheet) + 'audition_id' // Local key on the intermediate model (Entry) + ); + } + public function getSubscore($id) { return $this->subscores[$id]['score'] ?? false; } + + public function totalScore() { + $totalScore = 0; + $totalWeights = 0; + foreach ( $this->audition->scoringGuide->subscores as $subscore) { + $totalScore += $this->getSubscore($subscore->id) * $subscore->weight; + $totalWeights += $subscore->weight; + } + return $totalScore / $totalWeights; + } + + public function isValid() { + $judges = $this->audition->judges(); + return $judges->contains('id', $this->judge->id); + } + + } diff --git a/app/Models/ScoringGuide.php b/app/Models/ScoringGuide.php index bd0963f..6455993 100644 --- a/app/Models/ScoringGuide.php +++ b/app/Models/ScoringGuide.php @@ -16,6 +16,7 @@ class ScoringGuide extends Model { use HasFactory; protected $guarded = []; + protected $with = ['subscores']; public function auditions(): HasMany { diff --git a/app/Models/User.php b/app/Models/User.php index 9e29c8b..fa2b74f 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -62,6 +62,12 @@ class User extends Authenticatable implements MustVerifyEmail return $this->first_name . ' ' . $this->last_name; } + public function short_name(): String + { + // return the first letter of $this->first_name and the full $this->last_name + return $this->first_name[0] . '. ' . $this->last_name; + } + public function has_school(): bool { return $this->school_id !== null; diff --git a/app/helpers.php b/app/helpers.php index 78d955c..0542bb6 100644 --- a/app/helpers.php +++ b/app/helpers.php @@ -23,18 +23,3 @@ function tw_max_width_class_array() :Array { return $return; } -function getMessages() { - $flash = Session::get('_flash'); - $messages = $flash['new']; - $return = []; - foreach ($messages as $message) { - if (substr($message, 0,4) != 'msg|') continue; - $type = Session::get($message); - $return[] = ['message' => substr($message,4), 'type' => $type]; - } - return $return; -} - -function sendMessage(String $message, String $type = 'success') { - Session::flash('msg|'.$message,$type); -} diff --git a/database/seeders/ScoreAllAuditions.php b/database/seeders/ScoreAllAuditions.php new file mode 100644 index 0000000..230199d --- /dev/null +++ b/database/seeders/ScoreAllAuditions.php @@ -0,0 +1,42 @@ +rooms as $room) { + foreach ($room->auditions as $audition){ + $scoringGuide = $audition->scoringGuide; + $subscores = $scoringGuide->subscores; + foreach ($audition->entries as $entry){ + $scoreArray = []; + foreach ($subscores as $subscore) { + $scoreArray[$subscore->id] = [ + 'score' => mt_rand(0,100), + 'subscore_id' => $subscore->id, + 'subscore_name' => $subscore->name + ]; + } + ScoreSheet::create([ + 'user_id' => $judge->id, + 'entry_id' => $entry->id, + 'subscores' => $scoreArray + ]); + } + } + } + } + } +} diff --git a/resources/views/admin/entries/edit.blade.php b/resources/views/admin/entries/edit.blade.php index 9a863d1..249b14a 100644 --- a/resources/views/admin/entries/edit.blade.php +++ b/resources/views/admin/entries/edit.blade.php @@ -30,6 +30,32 @@ + + + Scores + + @foreach($scores as $score) + @php($score->isValid()) + +
{{ $score->judge->full_name() }}
+ @foreach($score->subscores as $subscore) +{{-- TODO make this look better--}} +
+

{{$subscore['subscore_name']}}

+

{{$subscore['score'] }}

+
+ @endforeach + @if(! $score->isValid()) +
+ @csrf + @method('DELETE') + INVALID SCORE - DELETE + @endif + +{{-- // TODO make the invalid prettier--}} + @endforeach + + {{--TODO apply javascript to only show appropriate auditions for the students grade--}} diff --git a/resources/views/admin/entries/index.blade.php b/resources/views/admin/entries/index.blade.php index 4ca8ad4..fe21746 100644 --- a/resources/views/admin/entries/index.blade.php +++ b/resources/views/admin/entries/index.blade.php @@ -6,7 +6,8 @@ Set Filters - + + Audition @foreach($auditions as $audition) @@ -15,7 +16,7 @@ @endforeach - + School @foreach($schools as $school) @@ -39,7 +40,7 @@ - Clear Filters + Clear Filters Apply Filters diff --git a/resources/views/components/form/button-nocolor.blade.php b/resources/views/components/form/button-nocolor.blade.php index 98830a4..ca177fd 100644 --- a/resources/views/components/form/button-nocolor.blade.php +++ b/resources/views/components/form/button-nocolor.blade.php @@ -1,6 +1,11 @@ @php $buttonClasses = "text-sm font-semibold leading-6 text-gray-900"; @endphp +@props(['href' => false])
- + @if($href) + merge(['class' => $buttonClasses]) }}>{{ $slot }} + @else + + @endif
diff --git a/resources/views/components/layout/navbar/menus/tabulation.blade.php b/resources/views/components/layout/navbar/menus/tabulation.blade.php index 4deecb4..48a1890 100644 --- a/resources/views/components/layout/navbar/menus/tabulation.blade.php +++ b/resources/views/components/layout/navbar/menus/tabulation.blade.php @@ -21,6 +21,7 @@ diff --git a/resources/views/tabulation/auditionSeating.blade.php b/resources/views/tabulation/auditionSeating.blade.php new file mode 100644 index 0000000..017c36e --- /dev/null +++ b/resources/views/tabulation/auditionSeating.blade.php @@ -0,0 +1,34 @@ + + Audition Seating - {{ $audition->name }} + + + + + ID + Draw # + Student Name + @foreach($judges as $judge) + {{ $judge->short_name() }} + @endforeach + Total Score + + + + + @foreach($entries as $entry) + + {{ $entry->id }} + {{ $entry->draw_number }} + {{ $entry->student->full_name(true) }} + @foreach($judges as $judge) + {{ number_format($entry->scoreFromJudge($judge->id)->totalScore(), 4) }} + @endforeach + {{ number_format($entry->totalScore(), 4) }} + + @endforeach + + + + + + diff --git a/resources/views/tabulation/status.blade.php b/resources/views/tabulation/status.blade.php new file mode 100644 index 0000000..a02b2e4 --- /dev/null +++ b/resources/views/tabulation/status.blade.php @@ -0,0 +1,26 @@ + + Audition Status + + + Auditions + + + + + Audition + Scoring Status + + + + @foreach($auditions as $audition) + + + {{ $audition->name }} + + {{ $audition->scoredEntries()->count() }} / {{ $audition->entries->count() }} Scored + + @endforeach + + + + diff --git a/resources/views/test.blade.php b/resources/views/test.blade.php index 48bebb5..fffc920 100644 --- a/resources/views/test.blade.php +++ b/resources/views/test.blade.php @@ -1,10 +1,10 @@ @php use App\Models\Audition; - use App\Models\Entry;use App\Models\School; + use App\Models\Entry; + use App\Models\School; use App\Models\SchoolEmailDomain; use App\Models\ScoreSheet; use App\Models\ScoringGuide; - use App\Models\User; - use App\Settings; + use App\Models\User;use App\Settings; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Session; @@ -13,19 +13,13 @@ Test Page @php - dump(Auth::user()->scoreSheets->where('entry_id','=','997')->first()->subscores[6]['score']); - echo "-----"; - dump(Auth::user()->scoresForEntry(997)); - echo "-----"; - dump(Auth::user()->scoresForEntry(997)[6]['score']); - echo "-----"; - dump(Auth::user()->scoresForEntry(997)[7]['score']); - echo "-----"; - dump(Auth::user()->scoresForEntry(997)[8]['score']); - echo "-----"; - dump(Auth::user()->scoresForEntry(997)[9]['score']); - echo "-----"; - dump(Auth::user()->scoresForEntry(997)[10]['score']); + $audition = Audition::find(2); + $ranked = $audition->rankedEntries(); + dump($ranked); + echo "
plain entries
"; + dump($audition->entries()); @endphp + + diff --git a/routes/web.php b/routes/web.php index 5a72205..0110ac9 100644 --- a/routes/web.php +++ b/routes/web.php @@ -35,6 +35,8 @@ Route::middleware(['auth','verified',CheckIfCanTab::class])->prefix('tabulation/ Route::get('/record_noshow','chooseEntry'); Route::get('/entries','entryScoreSheet'); Route::post('/entries/{entry}','saveEntryScoreSheet'); + Route::get('/status','status'); + Route::get('/auditions/{audition}','auditionSeating'); }); }); @@ -43,6 +45,7 @@ Route::middleware(['auth','verified',CheckIfCanTab::class])->prefix('tabulation/ // Admin Routes Route::middleware(['auth','verified',CheckIfAdmin::class])->prefix('admin/')->group(function() { Route::view('/','admin.dashboard'); + Route::delete('/scores/{score}',[TabulationController::class,'destroyScore']); Route::post('/auditions/roomUpdate',[\App\Http\Controllers\Admin\AuditionController::class,'roomUpdate']); // Endpoint for JS assigning auditions to rooms Route::post('/scoring/assign_guide_to_audition',[\App\Http\Controllers\Admin\AuditionController::class,'scoringGuideUpdate']); // Endpoint for JS assigning scoring guides to auditions