Scoring guides and adding subscores working

This commit is contained in:
Matt Young 2024-06-03 20:57:23 -05:00
parent d5fd164049
commit e5da72d31c
22 changed files with 449 additions and 12 deletions

View File

@ -0,0 +1,89 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\ScoringGuide;
use App\Models\SubscoreDefinition;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use function abort;
use function dd;
use function request;
use function sendMessage;
class ScoringGuideController extends Controller
{
public function index()
{
if (! Auth::user()->is_admin) abort(403);
$guides = ScoringGuide::orderBy('name')->get();
return view('admin.scoring.index',['guides'=>$guides]);
}
public function store()
{
if (! Auth::user()->is_admin) abort(403);
request()->validate([
'name' => ['required','unique:scoring_guides']
]);
$guide = ScoringGuide::create([
'name' => request('name')
]);
return redirect('/admin/scoring');
}
public function edit(ScoringGuide $guide)
{
if (! Auth::user()->is_admin) abort(403);
$subscores = SubscoreDefinition::where('scoring_guide_id',$guide->id)->orderBy('display_order')->get();
return view('admin.scoring.edit',['guide'=>$guide,'subscores'=>$subscores]);
}
public function update(ScoringGuide $guide)
{
if (! Auth::user()->is_admin) abort(403);
request()->validate([
'name' => ['required','unique:scoring_guides']
]);
$guide->update([
'name' => request('name')
]);
return redirect('/admin/scoring/guides/' . $guide->id . '/edit' )->with('success','Scoring guide updated');
}
public function subscore_store(Request $request, ScoringGuide $guide)
{
if (! Auth::user()->is_admin) abort(403);
if (!$guide->exists()) abort(409);
$validateData = request()->validate([
'name' => ['required'],
'max_score' => ['required','integer'],
'weight'=>['required','integer'],
'display_order'=>['required','integer'],
'tiebreak_order'=>['required','integer'],
'for_seating'=>['nullable','boolean'],
'for_advance'=>['nullable','boolean'],
]);
$for_seating = $request->has('for_seating') ? (bool) $request->input('for_seating') : false;
$for_advance = $request->has('for_advance') ? (bool) $request->input('for_advance') : false;
$subscore = SubscoreDefinition::create([
'scoring_guide_id' => $guide->id,
'name' => $validateData['name'],
'max_score' => $validateData['max_score'],
'weight' => $validateData['weight'],
'display_order' => $validateData['display_order'],
'tiebreak_order' => $validateData['tiebreak_order'],
'for_seating' => $for_seating,
'for_advance' => $for_advance,
]);
return redirect('/admin/scoring/guides/' . $guide->id . '/edit' )->with('success','Subscore added');
}
}

View File

@ -0,0 +1,12 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class ScoringGuide extends Model
{
use HasFactory;
protected $guarded = [];
}

View File

@ -0,0 +1,12 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class SubscoreDefinition extends Model
{
use HasFactory;
protected $guarded = [];
}

View File

@ -9,6 +9,11 @@ class Settings
{ {
protected static $cacheKey = 'site_settings'; protected static $cacheKey = 'site_settings';
public static function __callStatic($key, $arguments)
{
return self::get($key);
}
// Load settings from the database and cache them // Load settings from the database and cache them
public static function loadSettings() public static function loadSettings()
{ {

View File

@ -35,6 +35,6 @@ function getMessages() {
return $return; return $return;
} }
function sendMessage(String $message, String $type) { function sendMessage(String $message, String $type = 'success') {
Session::flash('msg|'.$message,$type); Session::flash('msg|'.$message,$type);
} }

View File

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

View File

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

View File

@ -14,7 +14,7 @@ return new class extends Migration
Schema::create('site_settings', function (Blueprint $table) { Schema::create('site_settings', function (Blueprint $table) {
$table->id(); $table->id();
$table->string('setting_key')->unique(); $table->string('setting_key')->unique();
$table->text('setting_value'); $table->text('setting_value')->nullable();
}); });
} }

View File

@ -0,0 +1,28 @@
<?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('scoring_guides', function (Blueprint $table) {
$table->id();
$table->string('name')->unique();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('scoring_guides');
}
};

View File

@ -0,0 +1,36 @@
<?php
use App\Models\ScoringGuide as ScoringGuide;
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('subscore_definitions', function (Blueprint $table) {
$table->id();
$table->foreignIdFor(ScoringGuide::class);
$table->string('name');
$table->integer('max_score');
$table->integer('weight');
$table->integer('display_order');
$table->integer('tiebreak_order');
$table->boolean('for_seating')->default(true);
$table->boolean('for_advance')->default(true);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('subscore_definitions');
}
};

View File

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

View File

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

View File

@ -1,3 +1,4 @@
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
[x-cloak] { display: none !important; }

View File

@ -0,0 +1,86 @@
@php use App\Settings; @endphp
<x-layout.app x-data="{ rename: false }">
<x-slot:page_title>Scoring Rules</x-slot:page_title>
{{-- MAIN CARD--}}
<x-card.card x-show="! rename">
<x-card.heading>
<x-slot:subheading @click="rename = ! rename">Click here to rename</x-slot:subheading>
Scoring Guide: {{ $guide->name }}
</x-card.heading>
<div class="ml-6 -mt-2 mr-6">
<x-table.table with_title_area >
<x-slot:title>Subscores</x-slot:title>
<thead>
<tr>
<x-table.th>Name</x-table.th>
<x-table.th>Max Score</x-table.th>
<x-table.th>Weight</x-table.th>
<x-table.th>Display Order</x-table.th>
<x-table.th>Tiebreak Order</x-table.th>
<x-table.th>For Seating</x-table.th>
@if(Settings::advanceTo())
<x-table.th>For {{ Settings::advanceTo() }}</x-table.th>
@endif
<x-table.th spacer_only></x-table.th>
</tr>
</thead>
<x-table.body>
@foreach ($subscores as $subscore)
<tr>
<x-table.td>{{ $subscore->name }}</x-table.td>
<x-table.td>{{ $subscore->max_score }}</x-table.td>
<x-table.td>{{ $subscore->weight }}</x-table.td>
<x-table.td>{{ $subscore->display_order }}</x-table.td>
<x-table.td>{{ $subscore->tiebreak_order }}</x-table.td>
<x-table.td>{{ $subscore->for_seating ? 'Yes' : 'No' }}</x-table.td>
@if(Settings::advanceTo())
<x-table.td>{{ $subscore->for_advance ? 'Yes' : 'No' }}</x-table.td>
@endif
<x-table.td>
{{-- This came from AI, consider--}}
{{-- <x-form.form method="PATCH" action="/admin/scoring/guides/{{ $guide->id }}/subscore/{{ $subscore->id }}">--}}
{{-- <x-table.button>Save</x-table.button>--}}
{{-- <x-table.button @click="if(confirm('Are you sure?')) { $dispatch('delete', {id: {{ $subscore->id }} }) }">Delete</x-table.button>--}}
{{-- <x-table.button @click="rename = ! rename">Rename</x-table.button>--}}
{{-- </x-form.form>--}}
&nbsp;
</x-table.td>
</tr>
<template x-on:delete.window="document.querySelector('form[action=\'/admin/scoring/guides/{{ $guide->id }}/subscore/\'+event.detail.id+\'\']').submit()"></template>
@endforeach
</x-table.body>
<tfoot>
<tr><x-table.th class="!pb-0">Add Subscore</x-table.th></tr><tr>
<x-form.form method="POST" action="/admin/scoring/guides/{{ $guide->id }}/subscore">
<x-table.td><x-form.field name="name"></x-form.field></x-table.td>
<x-table.td><x-form.field name="max_score" type="number"></x-form.field></x-table.td>
<x-table.td><x-form.field name="weight" type="number"></x-form.field></x-table.td>
<x-table.td><x-form.field name="display_order"></x-form.field></x-table.td>
<x-table.td><x-form.field name="tiebreak_order"></x-form.field></x-table.td>
<x-table.td><x-form.toggle-checkbox name="for_seating" checked></x-form.toggle-checkbox></x-table.td>
@if(Settings::advanceTo())
<x-table.td><x-form.toggle-checkbox name="for_advance"></x-form.toggle-checkbox></x-table.td>
@endif
<td><x-table.button>Save</x-table.button></td>
</x-form.form>
</tr>
</tfoot>
</x-table.table>
</div>
</x-card.card>
{{-- RENAME CARD--}}
<x-card.card x-show="rename" @click.outside="rename = ! rename" x-cloak>
<x-card.heading>Rename</x-card.heading>
<x-form.form method="PATCH" action="/admin/scoring/guides/{{ $guide->id }}/edit" class="!pt-2">
<x-form.field name="name" label_text="New Name" value="" />
<x-form.footer submit-button-text="Rename"></x-form.footer>
</x-form.form>
</x-card.card>
<div class="mt-20">
@php(dump($guide))
</div>
</x-layout.app>

View File

@ -0,0 +1,41 @@
<x-layout.app>
<x-slot:page_title>Scoring Rules</x-slot:page_title>
<div class="mx-auto max-w-lg">
<x-card.card>
<x-card.heading>
Scoring Guides
<x-slot:subheading>Each audition will be assigned a scoring guide.</x-slot:subheading>
</x-card.heading>
<x-table.table>
<thead>
<tr>
<x-table.th>Name</x-table.th>
<x-table.th spacer_only></x-table.th>
</tr>
</thead>
<x-table.body>
@foreach($guides as $guide)
<tr>
<x-table.td>{{ $guide->name }}</x-table.td>
<x-table.td class="text-right text-indigo-600"><a href="/admin/scoring/guides/{{ $guide->id }}/edit">Edit</a></x-table.td>
{{-- TODO add a link to delete if the guide is not in use--}}
</tr>
@endforeach
</x-table.body>
<tfoot>
<tr>
<x-form.form method="POST" action="/admin/scoring/guides" class="!px-0 !py-0">
<x-table.td>
<x-form.field name="name" label_text="Add New Scoring Guide" />
</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-card.card>
</div>
</x-layout.app>

View File

@ -1,5 +1,5 @@
@props(['right_link_button_type' => 'a']) {{-- Use if the link to the right needs to be a button --}} @props(['right_link_button_type' => 'a']) {{-- Use if the link to the right needs to be a button --}}
<li class="flex items-center justify-between gap-x-6 px-4 py-5 sm:px-6"> <li {{ $attributes->merge(['class'=>'flex items-center justify-between gap-x-6 px-4 py-5 sm:px-6']) }}>
<div class="flex min-w-0 gap-x-4"> <div class="flex min-w-0 gap-x-4">
{{ $slot }} {{ $slot }}
</div> </div>

View File

@ -0,0 +1,11 @@
@props(['name', 'checked' => false])
<div x-data="{ toggle: {{ $checked ? 'true':'false'}} }" class="flex items-center">
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" x-model="toggle" name="{{ $name }}" class="sr-only" {{ $checked ? 'checked':''}} value="1">
<div :class="toggle ? 'bg-blue-600' : 'bg-gray-200'" class="w-11 h-6 rounded-full transition-colors duration-300"></div>
<div :class="toggle ? 'translate-x-6' : 'translate-x-1'" class="absolute left-1 top-1 bg-white w-4 h-4 rounded-full transition-transform duration-300"></div>
</label>
@error($name)
<p class="text-xs text-red-500 font-semibold mt-1 ml-3">{{ $message }}</p>
@enderror
</div>

View File

@ -30,7 +30,7 @@
<main> <main {{ $attributes }}>
<div class="mx-auto max-w-7xl py-6 sm:px-6 lg:px-8"> <div class="mx-auto max-w-7xl py-6 sm:px-6 lg:px-8">
@foreach(getMessages() as $message) @foreach(getMessages() as $message)
<x-flash_message :message="$message['message']" :messageType="$message['type']" /> <x-flash_message :message="$message['message']" :messageType="$message['type']" />

View File

@ -12,17 +12,31 @@
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<img class="h-8 w-8" src="https://tailwindui.com/img/logos/mark.svg?color=indigo&shade=300" alt="Your Company"> <img class="h-8 w-8" src="https://tailwindui.com/img/logos/mark.svg?color=indigo&shade=300" alt="Your Company">
</div> </div>
<div class="hidden md:block"> <div class="hidden md:block"
x-data="{
redirectToDashboard(event) {
const selectedValue = event.target.value;
if (selectedValue === 'admin') {
window.location.href = '/admin';
} else if (selectedValue === 'user') {
window.location.href = '/dashboard';
}
}
}">
<div class="ml-10 flex items-baseline space-x-4"> <div class="ml-10 flex items-baseline space-x-4">
<!-- Current: "bg-indigo-700 text-white", <!-- Current: "bg-indigo-700 text-white",
Default: "text-white hover:bg-indigo-500 hover:bg-opacity-75" --> Default: "text-white hover:bg-indigo-500 hover:bg-opacity-75" -->
<x-layout.nav-link href="/dashboard" class="!bg-indigo-900">Exit Admin</x-layout.nav-link> <x-form.select name="mode" @change="redirectToDashboard">
<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" :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/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/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/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/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>
{{-- <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

@ -12,11 +12,26 @@
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<img class="h-8 w-8" src="https://tailwindui.com/img/logos/mark.svg?color=indigo&shade=300" alt="Your Company"> <img class="h-8 w-8" src="https://tailwindui.com/img/logos/mark.svg?color=indigo&shade=300" alt="Your Company">
</div> </div>
<div class="hidden md:block"> <div class="hidden md:block"
@if(Auth::user()->is_admin)x-data="{
redirectToDashboard(event) {
const selectedValue = event.target.value;
if (selectedValue === 'admin') {
window.location.href = '/admin';
} else if (selectedValue === 'user') {
window.location.href = '/dashboard';
}
}
}"@endif>
<div class="ml-10 flex items-baseline space-x-4"> <div class="ml-10 flex items-baseline space-x-4">
<!-- Current: "bg-indigo-700 text-white", <!-- Current: "bg-indigo-700 text-white",
Default: "text-white hover:bg-indigo-500 hover:bg-opacity-75" --> Default: "text-white hover:bg-indigo-500 hover:bg-opacity-75" -->
<x-layout.nav-link href="/admin" class="!bg-indigo-900">Admin</x-layout.nav-link> @if(Auth::user()->is_admin)
<x-form.select name="mode" @change="redirectToDashboard">
<option value="user" selected>Director</option>
<option value="admin" >Admin</option>
</x-form.select>
@endif
<x-layout.nav-link href="/dashboard" :active="request()->is('dashboard')">Dashboard</x-layout.nav-link> <x-layout.nav-link href="/dashboard" :active="request()->is('dashboard')">Dashboard</x-layout.nav-link>
<x-layout.nav-link href="/students" :active="request()->is('students')">Students</x-layout.nav-link> <x-layout.nav-link href="/students" :active="request()->is('students')">Students</x-layout.nav-link>
<x-layout.nav-link href="/entries" :active="request()->is('entries')">Entries</x-layout.nav-link> <x-layout.nav-link href="/entries" :active="request()->is('entries')">Entries</x-layout.nav-link>

View File

@ -2,10 +2,7 @@
<x-layout.app> <x-layout.app>
<x-slot:page_title>Test Page</x-slot:page_title> <x-slot:page_title>Test Page</x-slot:page_title>
{{ Audition::max('maximum_grade') }} is the oldest grade used. <br> {{ Settings::auditionName() }}
{{ Audition::min('minimum_grade') }} is the youngest grade.
@php( session(['testFnameFilter' => 'Matt']))
</x-layout.app> </x-layout.app>

View File

@ -19,6 +19,16 @@ Route::view('/','welcome')->middleware('guest');
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');
// Scoring
Route::prefix('scoring')->controller(\App\Http\Controllers\Admin\ScoringGuideController::class)->group(function() {
Route::get('/','index'); // Scoring Setup Homepage
Route::post('/guides','store'); // Save a new scoring guide
Route::get('/guides/{guide}/edit','edit'); // Edit scoring guide
Route::patch('/guides/{guide}/edit','update'); // Save changes to audition guide (rename)
Route::post('/guides/{guide}/subscore','subscore_store'); // Save a new subscore
});
// Admin Auditions Routes // Admin Auditions Routes
Route::prefix('auditions')->controller(\App\Http\Controllers\Admin\AuditionController::class)->group(function() { Route::prefix('auditions')->controller(\App\Http\Controllers\Admin\AuditionController::class)->group(function() {
Route::get('/','index'); Route::get('/','index');