Judge assignment page working
This commit is contained in:
parent
0ac37b6879
commit
3079ff36b9
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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[]
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
};
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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*'))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue