Room assignments working for those with rooms. Need to implement for unassigned auditions

This commit is contained in:
Matt Young 2024-06-04 15:48:34 -05:00
parent 43647583e7
commit 1b0112b73f
18 changed files with 361 additions and 13 deletions

View File

@ -102,6 +102,21 @@ class AuditionController extends Controller
return response()->json(['status' => 'success']); return response()->json(['status' => 'success']);
} }
public function roomUpdate(Request $request)
{
$auditions = $request->all();
foreach ($auditions as $audition) {
Audition::where('id', $audition['id'])
->update([
'room_id' => $audition['room_id'],
'order_in_room' => $audition['room_order']
]);
}
return response()->json(['status' => 'success']);
}
public function destroy(Audition $audition) public function destroy(Audition $audition)
{ {
if(! Auth::user()->is_admin) abort(403); if(! Auth::user()->is_admin) abort(403);

View File

@ -0,0 +1,20 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Audition;
use App\Models\Room;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class RoomController extends Controller
{
public function index()
{
if(! Auth::user()->is_admin) abort(403);
$rooms = Room::with('auditions.entries')->orderBy('name')->get();
$unassignedAuditions = Audition::with('entries')->whereNull('room_id')->orderBy('score_order')->get();
return view('admin.rooms.index', ['rooms' => $rooms, 'unassignedAuditions' => $unassignedAuditions]);
}
}

View File

@ -30,6 +30,11 @@ class Audition extends Model
return $this->hasMany(Entry::class); return $this->hasMany(Entry::class);
} }
public function room(): BelongsTo
{
return $this->belongsTo(Room::class);
}
public function dislpay_fee(): String public function dislpay_fee(): String
{ {
return '$' . number_format($this->entry_fee / 100, 2); return '$' . number_format($this->entry_fee / 100, 2);
@ -39,5 +44,5 @@ class Audition extends Model
// TODO add order column to be able to sort in score order
} }

View File

@ -24,6 +24,12 @@ class Entry extends Model
public function school(): HasOneThrough public function school(): HasOneThrough
{ {
return $this->hasOneThrough(School::class, Student::class, 'id', 'id', 'student_id', 'school_id'); return $this->hasOneThrough(
School::class,
Student::class,
'id',
'id',
'student_id',
'school_id');
} }
} }

30
app/Models/Room.php Normal file
View File

@ -0,0 +1,30 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
class Room extends Model
{
use HasFactory;
public function auditions(): HasMany
{
return $this->hasMany(Audition::class)->orderBy('order_in_room');
}
public function entries(): HasManyThrough
{
return $this->hasManyThrough(
Entry::class,
Audition::class,
'room_id', // Foreign key on the auditions table
'audition_id', //Foreign key on the Entries table
'id', // Local key on the rooms table
'id' // Local key on the Auditions table
);
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Room>
*/
class RoomFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
//
];
}
}

View File

@ -16,7 +16,7 @@ return new class extends Migration
$table->id(); $table->id();
$table->foreignIdFor(Event::class)->constrained()->cascadeOnUpdate()->restrictOnDelete(); $table->foreignIdFor(Event::class)->constrained()->cascadeOnUpdate()->restrictOnDelete();
$table->string('name')->unique(); $table->string('name')->unique();
$table->integer('order')->nullable()->unique(); $table->integer('order')->nullable();
$table->date(('entry_deadline')); $table->date(('entry_deadline'));
$table->integer('entry_fee'); $table->integer('entry_fee');
$table->integer('minimum_grade'); $table->integer('minimum_grade');

View File

@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('rooms', function (Blueprint $table) {
$table->id();
$table->string('name')->unique();
$table->text('description')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('rooms');
}
};

View File

@ -0,0 +1,31 @@
<?php
use App\Models\Room;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('auditions', function (Blueprint $table) {
$table->foreignIdFor(Room::class)->nullable()->constrained()->cascadeOnUpdate()->nullOnDelete();
$table->integer('order_in_room');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('auditions', function (Blueprint $table) {
$table->dropColumn('room_id');
$table->dropColumn('order_in_room');
});
}
};

View File

@ -0,0 +1,17 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
class RoomSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
//
}
}

View File

@ -0,0 +1,91 @@
<x-layout.app>
<x-slot:page_title>Rooms</x-slot:page_title>
<x-slot:title_bar_right><x-form.button href="/admin/rooms/create">New Room</x-form.button></x-slot:title_bar_right>
<div class="grid md:grid-cols-4 gap-5" x-data="roomManager()">
<div> {{-- Unassigned Auditions --}}
<x-card.card>
<x-card.heading>Unassigned Auditions</x-card.heading>
<x-card.list.body id="room-0" class="audition-list">
@foreach($unassignedAuditions as $audition)
<x-card.list.row class="!py-3" data-id="{{ $audition->id }}">
<x-badge_pill>{{ $audition->entries->count() }}</x-badge_pill>
<x-card.list.row-text-subtext>
{{ $audition->name }}
</x-card.list.row-text-subtext>
</x-card.list.row>
@endforeach
</x-card.list.body>
</x-card.card>
</div>
<div class="col-span-3"> {{-- Container for room cards --}}
<div class="grid md:grid-cols-3 gap-3">
@foreach($rooms as $room)
<x-card.card>
<x-card.heading>
{{ $room->name }}
<x-slot:subheading>{{ $room->description }}</x-slot:subheading>
<x-slot:right_side>
<x-badge_pill>{{ $room->entries->count() }}</x-badge_pill>
</x-slot:right_side>
</x-card.heading>
<x-card.list.body id="room-{{ $room->id }}" class="audition-list">
@foreach($room->auditions as $audition)
<x-card.list.row class="!py-3" data-id="{{ $audition->id }}">
<x-badge_pill>{{ $audition->entries->count() }}</x-badge_pill>
<x-card.list.row-text-subtext>{{ $audition->name }}</x-card.list.row-text-subtext>
</x-card.list.row>
@endforeach
</x-card.list.body>
</x-card.card>
@endforeach()
</div>
</div>
</div>
<script>
function roomManager() {
return {
rooms: @json($rooms), // Pass rooms data from the controller
init() {
this.rooms.forEach(room => {
new Sortable(document.getElementById('room-' + room.id), {
group: 'shared',
animation: 150,
onEnd: this.updateOrder.bind(this)
});
});
},
updateOrder(event) {
let order = [];
event.to.querySelectorAll('li').forEach((el, index) => {
order.push({
id: el.dataset.id,
room_id: event.to.id.split('-')[1],
room_order: index
});
});
fetch('/admin/auditions/roomUpdate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}'
},
body: JSON.stringify(order)
});
}
}
}
</script>
</x-layout.app>

View File

@ -0,0 +1,48 @@
<div x-data="auditionManager()">
<template x-for="room in rooms" :key="room.id">
<div>
<h2 x-text="room.name"></h2>
<ul :id="'room-' + room.id" class="audition-list">
<template x-for="audition in room.auditions" :key="audition.id">
<li x-text="audition.name" :data-id="audition.id"></li>
</template>
</ul>
</div>
</template>
</div>
<script>
function auditionManager() {
return {
rooms: @json($rooms), // Pass rooms data from the controller
init() {
this.rooms.forEach(room => {
new Sortable(document.getElementById('room-' + room.id), {
group: 'shared',
animation: 150,
onEnd: this.updateOrder.bind(this)
});
});
},
updateOrder(event) {
let order = [];
event.to.querySelectorAll('li').forEach((el, index) => {
order.push({
id: el.dataset.id,
room_id: event.to.id.split('-')[1],
room_order: index
});
});
fetch('/update-order', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}'
},
body: JSON.stringify(order)
});
}
}
}
</script>

View File

@ -1,8 +1,15 @@
@props(['subheading' => false]) @props(['subheading' => false, 'right_side' => false])
<div class="px-4 py-2 sm:px-6 border-b border-gray-100"> <div class="px-4 py-2 sm:px-6 border-b border-gray-100 flex justify-between">
<div>
<h3 {{ $attributes->merge(['class' => 'text-base font-semibold leading-7 text-gray-900']) }}>{{ $slot }}</h3> <h3 {{ $attributes->merge(['class' => 'text-base font-semibold leading-7 text-gray-900']) }}>{{ $slot }}</h3>
@if($subheading) @if($subheading)
<p {{ $subheading->attributes->merge(['class' => 'mt-.5 max-w-2xl text-sm leading-6 text-gray-500']) }}>{{ $subheading }}</p> <p {{ $subheading->attributes->merge(['class' => 'mt-.5 max-w-2xl text-sm leading-6 text-gray-500']) }}>{{ $subheading }}</p>
@endif @endif
</div>
@if($right_side)
<div>
{{ $right_side }}
</div>
@endif
</div> </div>

View File

@ -1,6 +1,6 @@
@props(['view_all_href' => false]) @props(['view_all_href' => false])
<div> <div>
<ul role="list" class="divide-y divide-gray-100 "> <ul {{ $attributes->merge(['role'=>'list','class'=>'divide-y divide-gray-100']) }}>
{{ $slot }} {{ $slot }}
</ul> </ul>
@if($view_all_href) @if($view_all_href)

View File

@ -1,4 +1,4 @@
@props(['page_title' => false]) @props(['page_title' => false, 'title_bar_right' => false ])
<!doctype html> <!doctype html>
<html lang="en" class="h-full bg-gray-100"> <html lang="en" class="h-full bg-gray-100">
<head> <head>
@ -26,7 +26,12 @@
<x-layout.navbar /> <x-layout.navbar />
@endif @endif
@if($page_title) @if($page_title)
<x-layout.page-header>{{ $page_title }}</x-layout.page-header> <x-layout.page-header>
{{ $page_title }}
@if($title_bar_right)
<x-slot:title_bar_right>{{ $title_bar_right }}</x-slot:title_bar_right>
@endif
</x-layout.page-header>
@endif @endif

View File

@ -37,6 +37,7 @@
<x-layout.nav-link href="/admin/entries" :active="request()->is('admin/entries')">Entries</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/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/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>
{{-- <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="/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>--}} {{-- <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,5 +1,13 @@
@props(['title_bar_right' => false])
<header class="bg-white shadow-sm"> <header class="bg-white shadow-sm">
<div class="mx-auto max-w-7xl px-4 py-4 sm:px-6 lg:px-8"> <div class="mx-auto max-w-7xl flex justify-between align-middle px-4 py-4 sm:px-6 lg:px-8">
<div>
<h1 class="text-lg font-semibold leading-6 text-gray-900">{{ $slot }}</h1> <h1 class="text-lg font-semibold leading-6 text-gray-900">{{ $slot }}</h1>
</div> </div>
<div>
@if($title_bar_right)
{{ $title_bar_right }}
@endif
</div>
</div>
</header> </header>

View File

@ -1,5 +1,6 @@
<?php <?php
use App\Http\Controllers\Admin\AuditionController;
use App\Http\Controllers\DashboardController; use App\Http\Controllers\DashboardController;
use App\Http\Controllers\EntryController; use App\Http\Controllers\EntryController;
use App\Http\Controllers\FilterController; use App\Http\Controllers\FilterController;
@ -18,6 +19,17 @@ Route::view('/','welcome')->middleware('guest');
// Admin Routes // Admin Routes
Route::middleware(['auth','verified',CheckIfAdmin::class])->prefix('admin/')->group(function() { Route::middleware(['auth','verified',CheckIfAdmin::class])->prefix('admin/')->group(function() {
Route::view('/','admin.dashboard'); Route::view('/','admin.dashboard');
Route::post('/auditions/roomUpdate',[\App\Http\Controllers\Admin\AuditionController::class,'roomUpdate']);
// Rooms
Route::prefix('rooms')->controller(\App\Http\Controllers\Admin\RoomController::class)->group(function() {
Route::get('/','index');
Route::get('/create','create');
Route::post('/','store');
Route::post('/{room}/edit','edit');
Route::patch('/{room}','update');
Route::delete('/{room}','destroy');
});
// Scoring // Scoring
Route::prefix('scoring')->controller(\App\Http\Controllers\Admin\ScoringGuideController::class)->group(function() { Route::prefix('scoring')->controller(\App\Http\Controllers\Admin\ScoringGuideController::class)->group(function() {