Judge assignment page working

This commit is contained in:
Matt Young 2024-06-05 16:52:15 -05:00
parent 0ac37b6879
commit 3079ff36b9
9 changed files with 323 additions and 56 deletions

View File

@ -5,8 +5,10 @@ namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Audition;
use App\Models\Room;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use function redirect;
class RoomController extends Controller
{
@ -17,4 +19,39 @@ class RoomController extends Controller
$rooms = Room::with('auditions.entries')->orderBy('name')->get();
return view('admin.rooms.index', ['rooms' => $rooms]);
}
public function judgingAssignment()
{
$usersWithoutRooms = User::doesntHave('rooms')->orderBy('last_name')->orderBy('first_name')->get();
$usersWithRooms = User::has('rooms')->orderBy('last_name')->orderBy('first_name')->get();
$rooms = Room::with('users')->get();
return view('admin.rooms.judge_assignments', compact('usersWithoutRooms','usersWithRooms','rooms'));
}
public function updateJudgeAssignment(Request $request, Room $room)
{
$validData = $request->validate([
'judge' => 'exists:users,id'
]);
$judge = User::find($validData['judge']);
if($request->isMethod('post')) {
// attach judge on post
$room->judges()->attach($judge->id);
$message = "Assigned " . $judge->full_name() . " to " . $room->name;
} elseif ($request->isMethod('delete')) {
// detach judge on delete
$room->judges()->detach($judge->id);
$message = "Removed " . $judge->full_name() . " from " . $room->name;
} else {
return redirect('/admin/rooms/judging_assignments')->with('error', 'Invalid request method.');
}
return redirect('/admin/rooms/judging_assignments')->with('success',$message);
}
}

View File

@ -4,6 +4,7 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
@ -27,4 +28,14 @@ class Room extends Model
'id' // Local key on the Auditions table
);
}
public function users(): BelongsToMany
{
return $this->belongsToMany(User::class,'room_user');
}
public function judges(): BelongsToMany
{
return $this->belongsToMany(User::class,'room_user','room_id','user_id');
}
}

View File

@ -6,6 +6,7 @@ namespace App\Models;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
@ -95,6 +96,16 @@ class User extends Authenticatable implements MustVerifyEmail
);
}
public function rooms(): BelongsToMany
{
return $this->belongsToMany(Room::class,'room_user');
}
public function judgingAssignments(): BelongsToMany
{
return $this->rooms();
}
/**
* Return an array of schools using the users email domain
* @return SchoolEmailDomain[]

View File

@ -0,0 +1,31 @@
<?php
use App\Models\Room;
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('room_user', function (Blueprint $table) {
$table->id();
$table->foreignIdFor(Room::class)->constrained()->cascadeOnUpdate()->cascadeOnDelete();
$table->foreignIdFor(User::class)->constrained()->cascadeOnUpdate()->cascadeOnDelete();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('user_room_pivot');
}
};

View File

@ -0,0 +1,115 @@
<x-layout.app>
<ul class="grid md:grid-cols-4 gap-5">
@foreach($rooms as $room)
@if($room->id == 0)
@continue
@endif
<li class=" rounded-xl border border-gray-200 bg-gray-50 "> {{-- card wrapper --}}
<div class="flex items-center gap-x-4 border-b border-gray-900/5 bg-white pt-2 pb-6 px-6"> {{-- card header --}}
<div class="text-sm font-medium leading-6 text-gray-900">
<p class="text-sm font-medium leading-6 text-gray-900">{{ $room->name }}</p>
<p class="mt-1 text-xs leading-5 text-gray-500">{{ $room->description }}</p>
</div>
<div class="relative ml-auto" x-data="{ open: false }"> {{-- Auditions Dropdown --}}
<button type="button"
class="-m-2.5 block p-2.5 text-gray-400 hover:text-gray-500"
id="options-menu-0-button"
aria-expanded="false"
aria-haspopup="true"
x-on:click="open = ! open">
<span class="sr-only">Open details</span>
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path
d="M3 10a1.5 1.5 0 113 0 1.5 1.5 0 01-3 0zM8.5 10a1.5 1.5 0 113 0 1.5 1.5 0 01-3 0zM15.5 8.5a1.5 1.5 0 100 3 1.5 1.5 0 000-3z"/>
</svg>
</button>
<!--
Dropdown menu, show/hide based on menu state.
Entering: "transition ease-out duration-100"
From: "transform opacity-0 scale-95"
To: "transform opacity-100 scale-100"
Leaving: "transition ease-in duration-75"
From: "transform opacity-100 scale-100"
To: "transform opacity-0 scale-95"
-->
<div
class="absolute right-5 -top-4 z-10 mt-0.5 w-32 origin-top-right rounded-md bg-white py-0.5 shadow-lg ring-1 ring-gray-900/5 focus:outline-none overflow-y-auto max-h-64"
role="menu"
aria-orientation="vertical"
aria-labelledby="options-menu-0-button"
tabindex="-1"
x-show="open"
x-cloak>
<!-- Active: "bg-gray-50", Not Active: "" -->
@foreach($room->auditions as $audition)
<p class="block px-3 py-0.5 text-xs leading-6 text-gray-900">{{ $audition->name }}</p>
@endforeach
</div>
</div>
</div> {{-- End Card Header --}}
<dl class="-my-3 divide-y divide-gray-100 px-6 pb-4 pt-1 text-sm leading-6 bg-gray-50"> {{-- Judge Listing --}}
@foreach($room->judges as $judge)
<div class="flex justify-between items-center gap-x-4 py-1"> {{-- Judge Line --}}
<dt>
<p>
<span class="text-gray-700">{{ $judge->full_name() }}, </span>
<span class="text-gray-500 text-xs">Vinita</span>
</p>
<p class="text-gray-500 text-xs">Admin</p>
</dt>
<dd class="text-gray-500 text-xs">
<form method="POST" action="/admin/rooms/{{ $room->id }}/judge" id="removeJudgeFromRoom{{ $room->id }}">
@csrf
@method('DELETE')
<input type="hidden" name="judge" value="{{ $judge->id }}">
<button>
<svg class="w-6 h-6 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="#d1d5db" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m15 9-6 6m0-6 6 6m6-3a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"/>
</svg>
</button>
</form>
</dd>
</div>
@endforeach
<div class="pt-3"> {{-- Add Judge Form --}}
<form method="POST" action="/admin/rooms/{{ $room->id }}/judge" id="assignJudgeToRoom{{ $room->id }}">
@csrf
<select name="judge"
id="judge"
class="block w-full rounded-md border-0 py-1.5 pl-3 pr-10 text-gray-900 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-indigo-600 sm:text-sm sm:leading-6"
onchange="document.getElementById('assignJudgeToRoom{{ $room->id }}').submit()">
<option>Add a judge</option>
<optgroup label="Unassigned Judges">
@foreach($usersWithoutRooms as $judge) {{-- skip judges alrady assigned to this audition --}}
<option value="{{ $judge->id }}">{{ $judge->full_name() }}
- {{ $judge->judging_preference }}</option>
@endforeach
</optgroup>
<optgroup label="Judges with assignments">
@foreach($usersWithRooms as $judge)
@if($room->judges->contains($judge->id))
@continue
@endif
<option value="{{ $judge->id }}">{{ $judge->full_name() }}
- {{ $judge->judging_preference }}</option>
@endforeach
</optgroup>
</select>
</form>
</div>
</dl>
</li>
@endforeach
</ul>
</x-layout.app>

View File

@ -0,0 +1,100 @@
<x-layout.app>
<ul class="grid md:grid-cols-4 gap-3">
@foreach($rooms as $room)
@if($room->id == 0)
@continue
@endif
<li class="overflow-hidden rounded-xl border border-gray-200"> {{-- card wrapper --}}
<div class="flex items-center gap-x-4 border-b border-gray-900/5 bg-white pt-2 pb-6 px-6"> {{-- card header --}}
<div class="text-sm font-medium leading-6 text-gray-900">
<p class="text-sm font-medium leading-6 text-gray-900">{{ $room->name }}</p>
<p class="mt-1 text-xs leading-5 text-gray-500">{{ $room->description }}</p>
</div>
<div class="relative ml-auto" x-data="{ open: false }"> {{-- Auditions Dropdown --}}
<button type="button"
class="-m-2.5 block p-2.5 text-gray-400 hover:text-gray-500"
id="options-menu-0-button"
aria-expanded="false"
aria-haspopup="true"
x-on:click="open = ! open">
<span class="sr-only">Open details</span>
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path
d="M3 10a1.5 1.5 0 113 0 1.5 1.5 0 01-3 0zM8.5 10a1.5 1.5 0 113 0 1.5 1.5 0 01-3 0zM15.5 8.5a1.5 1.5 0 100 3 1.5 1.5 0 000-3z"/>
</svg>
</button>
<!--
Dropdown menu, show/hide based on menu state.
Entering: "transition ease-out duration-100"
From: "transform opacity-0 scale-95"
To: "transform opacity-100 scale-100"
Leaving: "transition ease-in duration-75"
From: "transform opacity-100 scale-100"
To: "transform opacity-0 scale-95"
-->
<div
class="absolute right-5 -top-4 z-10 mt-0.5 w-32 origin-top-right rounded-md bg-white py-0.5 shadow-lg ring-1 ring-gray-900/5 focus:outline-none overflow-y-auto max-h-64"
role="menu"
aria-orientation="vertical"
aria-labelledby="options-menu-0-button"
tabindex="-1"
x-show="open"
x-cloak>
<!-- Active: "bg-gray-50", Not Active: "" -->
@foreach($room->auditions as $audition)
<p class="block px-3 py-0.5 text-xs leading-6 text-gray-900">{{ $audition->name }}</p>
@endforeach
</div>
</div>
</div> {{-- End Card Header --}}
<dl class="-my-3 divide-y divide-gray-100 px-6 pb-4 pt-1 text-sm leading-6 bg-gray-50"> {{-- Judge Listing --}}
<div class="flex justify-between items-center gap-x-4 py-1"> {{-- Judge Line --}}
<dt>
<p>
<span class="text-gray-700">Matt Young, </span>
<span class="text-gray-500 text-xs">Vinita</span>
</p>
<p class="text-gray-500 text-xs">Admin</p>
</dt>
<dd class="text-gray-500 text-xs">
<svg class="w-6 h-6 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="#d1d5db" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m15 9-6 6m0-6 6 6m6-3a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"/>
</svg>
</dd>
</div>
<div class="pt-1"> {{-- Add Judge Form --}}
<form method="POST" action="/admin/rooms/{{ $room->id }}/assign_judge">
<label for="judge" class="text-gray-500 text-xs">Add a judge</label>
@csrf
<select name="judge" id="judge" class="block w-full rounded-md border-0 py-1.5 pl-3 pr-10 text-gray-900 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-indigo-600 sm:text-sm sm:leading-6">
<label for="judge">Add a judge</label>
<optgroup label="Unassigned Judges">
@foreach($usersWithoutRooms as $judge)
<option value="{{ $judge->id }}">{{ $judge->full_name() }} - {{ $judge->judging_preference }}</option>
@endforeach
</optgroup>
<optgroup label="Judges with assignments">
@foreach($usersWithRooms as $judge)
<option value="{{ $judge->id }}">{{ $judge->full_name() }} - {{ $judge->judging_preference }}</option>
@endforeach
</optgroup>
</select>
</form>
</div>
</dl>
</li>
@endforeach
</ul>
</x-layout.app>

View File

@ -1,52 +0,0 @@
<div x-data="sortableData()">
@foreach($guides as $guide)
<div class="grid md:grid-cols-4 sm:grid-cols-2 mx-6 gap-2 mt-3 mb-3 guide-list" id="guide-{{ $guide->id }}" data-guide-id="{{ $guide->id }}">
@foreach($guide->auditions as $audition)
<div class="px-3 py-3 border border-indigo-300 bg-gray-100" data-id="{{ $audition->id }}">{{ $audition->name }}</div>
@endforeach
</div>
@endforeach
</div>
<script>
function sortableData() {
return {
init() {
this.initSortable();
},
initSortable() {
document.querySelectorAll('.guide-list').forEach((el) => {
Sortable.create(el, {
group: 'shared',
animation: 150,
onEnd: (evt) => {
let itemEl = evt.item; // dragged HTMLElement
let newGuideId = evt.to.getAttribute('data-guide-id');
let auditionId = itemEl.getAttribute('data-id');
// Make an AJAX request to update the audition_guide_id
fetch('/update-audition-guide', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}'
},
body: JSON.stringify({
audition_id: auditionId,
new_guide_id: newGuideId
})
}).then(response => response.json())
.then(data => {
if (data.success) {
console.log('Audition updated successfully');
} else {
console.error('Failed to update audition');
}
});
}
});
});
}
}
}
</script>

View File

@ -1,3 +1,4 @@
@php use App\Settings; @endphp
@props(['page_title' => false, 'title_bar_right' => false ])
<!doctype html>
<html lang="en" class="h-full bg-gray-100">
@ -17,7 +18,18 @@
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.14.0/Sortable.min.js"></script>
</head>
<body class="h-full">
<div class="bg-indigo-800 border-b border-b-indigo-900">
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div class="flex h-8 items-center justify-between">
<div>
<span class="text-white rounded-md px-3 py-2 text-sm text-m font-medium">AuditionAdmin</span><span class="text-white rounded-md px-3 py-2 text-sm">{{ Settings::auditionName() }}</span>
</div>
<div>
[ ADMIN ]
</div>
</div>
</div>
</div>
<div class="min-h-full">
@if(request()->is('*admin*'))

View File

@ -14,15 +14,14 @@ use Illuminate\Support\Facades\Route;
Route::get('/test',[TestController::class,'flashTest'])->middleware('auth','verified');
Route::post('/admin/scoring/assign_guide_to_audition',[\App\Http\Controllers\Admin\AuditionController::class,'scoringGuideUpdate']);
Route::post('/admin/scoring/assign_guide_to_audition',[\App\Http\Controllers\Admin\AuditionController::class,'scoringGuideUpdate']); // needs to move inside of admin group
Route::view('/','welcome')->middleware('guest');
// Admin Routes
Route::middleware(['auth','verified',CheckIfAdmin::class])->prefix('admin/')->group(function() {
Route::view('/','admin.dashboard');
Route::post('/auditions/roomUpdate',[\App\Http\Controllers\Admin\AuditionController::class,'roomUpdate']);
Route::post('/auditions/roomUpdate',[\App\Http\Controllers\Admin\AuditionController::class,'roomUpdate']); // Endpoint for JS assigning auditions to rooms
// Rooms
Route::prefix('rooms')->controller(\App\Http\Controllers\Admin\RoomController::class)->group(function() {
@ -32,6 +31,9 @@ Route::middleware(['auth','verified',CheckIfAdmin::class])->prefix('admin/')->gr
Route::post('/{room}/edit','edit');
Route::patch('/{room}','update');
Route::delete('/{room}','destroy');
Route::get('/judging_assignments','judgingAssignment'); // Screen to assign judges to rooms
Route::match(['post', 'delete'], '/{room}/judge', 'updateJudgeAssignment');
});
// Scoring