Scobda nomination ensembles #106

Merged
okorpheus merged 25 commits from scobda_first_year into master 2025-02-12 21:51:10 +00:00
40 changed files with 1435 additions and 15 deletions

View File

@ -24,7 +24,8 @@ class AuditionSettings extends Controller
'organizerName' => ['required'],
'organizerEmail' => ['required', 'email'],
'registrationCode' => ['required'],
'fee_structure' => ['required', 'in:oneFeePerEntry,oneFeePerStudent'], // Options should align with the boot method of InvoiceDataServiceProvider
'fee_structure' => ['required', 'in:oneFeePerEntry,oneFeePerStudent'],
// Options should align with the boot method of InvoiceDataServiceProvider
'late_fee' => ['nullable', 'numeric', 'min:0'],
'school_fee' => ['nullable', 'numeric', 'min:0'],
'payment_address' => ['required'],
@ -32,6 +33,8 @@ class AuditionSettings extends Controller
'payment_state' => ['required', 'max:2'],
'payment_zip' => ['required', 'min:5'],
'advanceTo' => ['nullable'],
'nomination_ensemble_rules' => ['required', 'in:disabled,scobda'],
// Options should align with the boot method of NominationEnsembleServiceProvider
]);
// Olympic Scoring Switch
@ -43,6 +46,9 @@ class AuditionSettings extends Controller
// Enable Invoicing Switch
$validData['invoicing_enabled'] = $request->get('invoicing_enabled') == '1';
// Enable collect shirt size switch
$validData['student_data_collect_shirt_size'] = $request->get('student_data_collect_shirt_size') == '1';
// Store currency values as cents
$validData['late_fee'] = $validData['late_fee'] * 100;
$validData['school_fee'] = $validData['school_fee'] * 100;

View File

@ -7,6 +7,7 @@ use App\Models\Audition;
use App\Models\AuditLogEntry;
use App\Models\Entry;
use App\Models\Event;
use App\Models\NominationEnsemble;
use App\Models\School;
use App\Models\Student;
use Illuminate\Support\Facades\Auth;
@ -14,6 +15,8 @@ use Illuminate\Support\Facades\Auth;
use function abort;
use function auth;
use function compact;
use function max;
use function min;
use function request;
use function to_route;
use function view;
@ -54,8 +57,8 @@ class StudentController extends Controller
if (! Auth::user()->is_admin) {
abort(403);
}
$minGrade = Audition::min('minimum_grade');
$maxGrade = Audition::max('maximum_grade');
$minGrade = min(Audition::min('minimum_grade'), NominationEnsemble::min('minimum_grade'));
$maxGrade = max(Audition::max('maximum_grade'), NominationEnsemble::max('maximum_grade'));
$schools = School::orderBy('name')->get();
return view('admin.students.create', ['schools' => $schools, 'minGrade' => $minGrade, 'maxGrade' => $maxGrade]);
@ -105,8 +108,8 @@ class StudentController extends Controller
if (! Auth::user()->is_admin) {
abort(403);
}
$minGrade = Audition::min('minimum_grade');
$maxGrade = Audition::max('maximum_grade');
$minGrade = min(Audition::min('minimum_grade'), NominationEnsemble::min('minimum_grade'));
$maxGrade = max(Audition::max('maximum_grade'), NominationEnsemble::max('maximum_grade'));
$schools = School::orderBy('name')->get();
$student->loadCount('entries');
$entries = $student->entries;

View File

@ -0,0 +1,22 @@
<?php
namespace App\Http\Controllers\NominationEnsembles;
use App\Models\NominationEnsembleEntry;
interface NominationAdminController
{
public function index();
public function show(NominationEnsembleEntry $entry);
public function create();
public function store();
public function edit(NominationEnsembleEntry $entry);
public function update(NominationEnsembleEntry $entry);
public function destroy(NominationEnsembleEntry $entry);
}

View File

@ -0,0 +1,22 @@
<?php
namespace App\Http\Controllers\NominationEnsembles;
use App\Models\NominationEnsemble;
interface NominationEnsembleController
{
public function index();
public function show(NominationEnsemble $ensemble);
public function create();
public function store();
public function edit(NominationEnsemble $ensemble);
public function update(NominationEnsemble $ensemble);
public function destroy(NominationEnsemble $ensemble);
}

View File

@ -0,0 +1,22 @@
<?php
namespace App\Http\Controllers\NominationEnsembles;
use App\Models\NominationEnsembleEntry;
interface NominationEnsembleEntryController
{
public function index();
public function show(NominationEnsembleEntry $entry);
public function create();
public function store();
public function edit(NominationEnsembleEntry $entry);
public function update(NominationEnsembleEntry $entry);
public function destroy(NominationEnsembleEntry $entry);
}

View File

@ -0,0 +1,14 @@
<?php
namespace App\Http\Controllers\NominationEnsembles;
use App\Models\NominationEnsemble;
interface NominationSeatingController
{
public function index();
public function show(NominationEnsemble $ensemble);
public function seat(NominationEnsemble $ensemble);
}

View File

@ -0,0 +1,46 @@
<?php
namespace App\Http\Controllers\NominationEnsembles;
use App\Http\Controllers\Controller;
use App\Models\NominationEnsembleEntry;
class ScobdaNominationAdminController extends Controller implements NominationAdminController
{
public function index()
{
$nominations = NominationEnsembleEntry::with('student')->with('ensemble')->get();
return view('nomination_ensembles.scobda.admin.index', compact('nominations'));
}
public function show(NominationEnsembleEntry $entry)
{
// TODO: Implement show() method.
}
public function create()
{
// TODO: Implement create() method.
}
public function store()
{
// TODO: Implement store() method.
}
public function edit(NominationEnsembleEntry $entry)
{
// TODO: Implement edit() method.
}
public function update(NominationEnsembleEntry $entry)
{
// TODO: Implement update() method.
}
public function destroy(NominationEnsembleEntry $entry)
{
// TODO: Implement destroy() method.
}
}

View File

@ -0,0 +1,113 @@
<?php
namespace App\Http\Controllers\NominationEnsembles;
use App\Http\Controllers\Controller;
use App\Models\NominationEnsemble;
use Illuminate\Validation\Rule;
use function redirect;
class ScobdaNominationEnsembleController extends Controller implements NominationEnsembleController
{
public function index()
{
$ensembles = NominationEnsemble::all();
return view('nomination_ensembles.scobda.admin.ensembles.index', compact('ensembles'));
}
public function show(NominationEnsemble $ensemble)
{
// TODO: Implement show() method.
}
public function create()
{
// TODO: Implement create() method.
}
public function store()
{
//dd(request()->all());
$validated = request()->validate([
'ensemble_name' => 'required|unique:nomination_ensembles,name',
'entry_deadline' => 'required|date',
'min_grade' => 'required|numeric|min:0',
'max_grade' => 'required|numeric|gte:min_grade',
'max_nominations' => 'required|numeric|min:1',
'target_size' => 'required|numeric|min:1',
'rounding_direction' => 'required|in:up,down',
'instrument_list' => 'required|string',
], [
'maximum_grade.gte' => 'The maximum grade must be greater than the minimum grade.',
'rounding_direction.in' => 'The rounding direction must be either "up" or "down".',
]);
$instrument_list = preg_replace('/\s*,\s*/', ',', $validated['instrument_list']);
$instrument_array = explode(',', $instrument_list);
$ensemble = new NominationEnsemble();
$ensemble->name = $validated['ensemble_name'];
$ensemble->entry_deadline = $validated['entry_deadline'];
$ensemble->minimum_grade = $validated['min_grade'];
$ensemble->maximum_grade = $validated['max_grade'];
$data = [];
$data['max_nominations'] = $validated['max_nominations'];
$data['target_size'] = $validated['target_size'];
$data['instruments'] = $instrument_array;
$data['rounding_direction'] = $validated['rounding_direction'];
$ensemble->data = $data;
$ensemble->save();
return redirect()->route('nomination.admin.ensemble.index')->with('success', 'Nomination Ensemble has been created.');
}
public function edit(NominationEnsemble $ensemble)
{
// TODO: Implement edit() method.
}
public function update(NominationEnsemble $ensemble)
{
$validated = request()->validate([
'ensemble_name' => [
'required',
Rule::unique('nomination_ensembles', 'name')->ignore($ensemble->id),
],
'entry_deadline' => 'required|date',
'min_grade' => 'required|numeric|min:0',
'max_grade' => 'required|numeric|gte:min_grade',
'max_nominations' => 'required|numeric|min:1',
'target_size' => 'required|numeric|min:1',
'rounding_direction' => 'required|in:up,down',
'instrument_list' => 'required|string',
], [
'maximum_grade.gte' => 'The maximum grade must be greater than the minimum grade.',
'rounding_direction.in' => 'The rounding direction must be either "up" or "down".',
]);
$instrument_list = preg_replace('/\s*,\s*/', ',', $validated['instrument_list']);
$instrument_array = explode(',', $instrument_list);
$ensemble->name = $validated['ensemble_name'];
$ensemble->entry_deadline = $validated['entry_deadline'];
$ensemble->minimum_grade = $validated['min_grade'];
$ensemble->maximum_grade = $validated['max_grade'];
$data = [];
$data['max_nominations'] = $validated['max_nominations'];
$data['target_size'] = $validated['target_size'];
$data['instruments'] = $instrument_array;
$data['rounding_direction'] = $validated['rounding_direction'];
$ensemble->data = $data;
$ensemble->save();
return redirect()->route('nomination.admin.ensemble.index')->with('success', 'Nomination Ensemble has been modified.');
}
public function destroy(NominationEnsemble $ensemble)
{
$ensemble->delete();
// TODO: Delete associated nomionations.
return redirect()->route('nomination.admin.ensemble.index')->with('success', 'Nomination Ensemble has been deleted.');
}
}

View File

@ -0,0 +1,224 @@
<?php
namespace App\Http\Controllers\NominationEnsembles;
use App\Http\Controllers\Controller;
use App\Models\NominationEnsemble;
use App\Models\NominationEnsembleEntry;
use App\Models\School;
use App\Models\Student;
use Illuminate\Support\Carbon;
class ScobdaNominationEnsembleEntryController extends Controller implements NominationEnsembleEntryController
{
public function index()
{
// Get current date for checking deadlines
$currentDate = Carbon::now('America/Chicago');
$currentDate = $currentDate->format('Y-m-d');
$ensembles = NominationEnsemble::all();
// populate an array with each ensemble id as a key. Each item will be a collection of students available to be nominated
$availableStudents = [];
// populate an array with each ensemble id as a key. Each item will be a collection of available instruments
$availableInstruments = [];
// populate an array with each ensemble id as a key. Each item will be a collection of nominationEntries already made
$nominatedStudents = [];
// an array of bool values with each ensemble id as a key. It will be true if additional nominations are available
$nominationsAvailable = [];
foreach ($ensembles as $ensemble) {
// Gather a collection of students who may be nominated for this ensemble
$availableStudents[$ensemble->id] = Student::where('grade', '<=', $ensemble->maximum_grade)
->where('grade', '>=', $ensemble->minimum_grade)
->where('school_id', auth()->user()->school_id)
->orderBy('last_name')
->orderBy('first_name')
->get();
$availableInstruments[$ensemble->id] = $ensemble->data['instruments'];
$nominatedStudents[$ensemble->id] = $this->collapseNominations(auth()->user()->school, $ensemble,
'nominations');
$nominatedStudentIds = [];
// Removed students already nominated from available students
foreach ($nominatedStudents[$ensemble->id] as $nominatedStudent) {
$nominatedStudentIds[] = $nominatedStudent->student_id;
}
$availableStudents[$ensemble->id] = $availableStudents[$ensemble->id]->reject(function ($student) use (
$nominatedStudentIds
) {
return in_array($student->id, $nominatedStudentIds);
});
$nominationsAvailable[$ensemble->id] = $ensemble->data['max_nominations'] > count($nominatedStudents[$ensemble->id]);
}
return view('nomination_ensembles.scobda.entries.index',
compact('ensembles', 'availableStudents', 'availableInstruments', 'nominatedStudents',
'nominationsAvailable', 'currentDate'));
}
public function show(NominationEnsembleEntry $entry)
{
// TODO: Implement show() method.
}
public function create()
{
// TODO: Implement create() method.
}
public function store()
{
$validData = request()->validate([
'ensemble' => [
'required',
'exists:App\Models\NominationEnsemble,id',
],
'new_student' => [
'required',
'exists:App\Models\Student,id',
],
'new_instrument' => 'required',
]);
if (NominationEnsembleEntry::where('student_id', $validData['new_student'])
->where('nomination_ensemble_id', $validData['ensemble'])
->count() > 0) {
return redirect()->route('nomination.entry.index')->with('error',
'Student already nominated for that ensemble');
}
$proposedEnsemble = NominationEnsemble::find($validData['ensemble']);
$currentDate = Carbon::now('America/Chicago');
$currentDate = $currentDate->format('Y-m-d');
if ($proposedEnsemble->entry_deadline < $currentDate) {
return redirect()->route('nomination.entry.index')->with('error',
'The nomination deadline for that ensemble has passed');
}
if (! in_array($validData['new_instrument'], $proposedEnsemble->data['instruments'])) {
return redirect()->route('nomination.entry.index')->with('error',
'Invalid Instrument specified');
}
$student = Student::find($validData['new_student']);
if (auth()->user()->school_id !== $student->school_id) {
return redirect()->route('nomination.entry.index')->with('error',
'You may only nominate students from your school');
}
$nextRank = $this->collapseNominations($student->school, $proposedEnsemble, 'next');
if ($nextRank > $proposedEnsemble->data['max_nominations']) {
return redirect()->route('nomination.entry.index')->with('error',
'You have already used all of your nominations');
}
$entry = new NominationEnsembleEntry();
$entry->student_id = $validData['new_student'];
$entry->nomination_ensemble_id = $validData['ensemble'];
$data = [];
$data['rank'] = $nextRank;
$data['instrument'] = $validData['new_instrument'];
$entry->data = $data;
$entry->save();
return redirect()->route('nomination.entry.index')->with('success',
'Nomination Recorded');
}
public function edit(NominationEnsembleEntry $entry)
{
// TODO: Implement edit() method.
}
public function update(NominationEnsembleEntry $entry)
{
// TODO: Implement update() method.
}
public function destroy(NominationEnsembleEntry $entry)
{
if ($entry->student->school_id !== auth()->user()->school_id) {
return redirect()->route('nomination.entry.index')->with('error',
'You may only delete nominations from your school');
}
$currentDate = Carbon::now('America/Chicago');
$currentDate = $currentDate->format('Y-m-d');
if ($entry->ensemble->entry_deadline < $currentDate) {
return redirect()->route('nomination.entry.index')->with('error',
'You cannot delete nominations after the deadline');
}
$entry->delete();
return redirect()->route('nomination.entry.index')->with('success', 'Nomination Deleted');
}
/**
* Given a school and nomination ensemble, consolidate the rank valuek
*
* if returnType is next, the next available rank will be returned
* if returnType is nominations, a collection of nominations will be returned
*
* @return int|array
*
* @var returnType = next|nominations
*/
private function collapseNominations(School $school, NominationEnsemble $ensemble, $returnType = 'next')
{
$nominations = $school->nominations()->get()->where('nomination_ensemble_id',
$ensemble->id)->sortBy('data.rank');
$n = 1;
foreach ($nominations as $nomination) {
$nomination->update(['data->rank' => $n]);
$n++;
}
if ($returnType == 'next') {
return $n;
}
return $nominations;
}
public function move()
{
$validData = request()->validate([
'direction' => 'required|in:up,down',
'nominationId' => 'required|exists:App\Models\NominationEnsembleEntry,id',
]);
$direction = $validData['direction'];
$nomination = NominationEnsembleEntry::findOrFail($validData['nominationId']);
// Verify the entry deadline for the ensemble has not passed
$currentDate = Carbon::now('America/Chicago');
$currentDate = $currentDate->format('Y-m-d');
if ($nomination->ensemble->entry_deadline < $currentDate) {
return redirect()->route('nomination.entry.index')->with('error',
'The entry deadline for that nomination ensemble has passed');
}
// Verify the student being moved is from the users school
if (auth()->user()->school_id !== $nomination->student_id) {
return redirect()->route('nomination.entry.index')->with('error',
'You cannot modify nominations of another school');
}
$data = $nomination->data;
if ($validData['direction'] == 'up') {
$data['rank'] = $nomination->data['rank'] - 1.5;
}
if ($validData['direction'] == 'down') {
$data['rank'] = $nomination->data['rank'] + 1.5;
}
$nomination->update(['data' => $data]);
$this->collapseNominations($nomination->student->school, $nomination->ensemble, 'next');
return redirect()->route('nomination.entry.index')->with('success', 'Nomination Moved');
}
}

View File

@ -0,0 +1,103 @@
<?php
namespace App\Http\Controllers\NominationEnsembles;
use App\Http\Controllers\Controller;
use App\Models\NominationEnsemble;
use App\Models\NominationEnsembleEntry;
use function redirect;
class ScobdaNominationSeatingController extends Controller implements NominationSeatingController
{
public function index()
{
$ensembles = NominationEnsemble::all();
$ensemble = null;
return view('nomination_ensembles.scobda.admin.seating.index', compact('ensembles', 'ensemble'));
}
public function show(NominationEnsemble $ensemble)
{
$ensembles = NominationEnsemble::all();
$acceptedNominations = NominationEnsembleEntry::where('nomination_ensemble_id', $ensemble->id)
->where('data->accepted', true)
->orderByRaw('CAST(data->"$.rank" AS UNSIGNED)')
->get();
$acceptedNominations = $acceptedNominations->groupBy(function ($item) {
return $item->data['instrument'];
});
return view('nomination_ensembles.scobda.admin.seating.index',
compact('ensembles', 'ensemble', 'acceptedNominations'));
}
public function seat(NominationEnsemble $ensemble)
{
$nominations = NominationEnsembleEntry::where('nomination_ensemble_id',
$ensemble->id)->orderByRaw('CAST(data->"$.rank" AS UNSIGNED)')->inRandomOrder()->get();
$rankGroupedNominations = $nominations->groupBy(function ($entry) {
return $entry->data['rank'];
});
$validData = request()->validate([
'action' => ['required', 'in:seat,clear'],
]);
$action = $validData['action'];
if ($action == 'clear') {
foreach ($nominations as $nomination) {
$data = $nomination->data;
unset($data['accepted']);
$nomination->update(['data' => $data]);
}
$data = $ensemble->data;
$data['seated'] = false;
$ensemble->data = $data;
$ensemble->update();
return redirect()->route('nomination.admin.seating.show',
['ensemble' => $ensemble])->with('Seating Cleared');
}
$acceptedNominations = collect();
$rankOn = 1;
// Collect students to add to the ensemble
while ($rankOn <= $ensemble->data['max_nominations'] && $rankGroupedNominations->has($rankOn)) {
// If were at or over the target size of the ensemble, stop adding people
if ($acceptedNominations->count() >= $ensemble->data['target_size']) {
break;
}
// Add people of the current rank to the ensemble
foreach ($rankGroupedNominations[$rankOn] as $nomination) {
$acceptedNominations->push($nomination);
}
$rankOn++;
// If we want to round down the ensemble size, quit adding people if hte next rank will exceed the target
if (
$rankGroupedNominations->has($rankOn) &&
$acceptedNominations->count() + $rankGroupedNominations[$rankOn]->count() >= $ensemble->data['target_size'] &&
$ensemble->data['rounding_direction'] === 'down'
) {
break;
}
}
foreach ($acceptedNominations as $nomination) {
$data = $nomination->data;
$data['accepted'] = true;
$nomination->update(['data' => $data]);
}
$data = $ensemble->data;
$data['seated'] = true;
$ensemble->data = $data;
$ensemble->update();
return redirect()->route('nomination.admin.seating.show', ['ensemble' => $ensemble])->with('Seating Complete');
}
}

View File

@ -25,7 +25,10 @@ class StudentController extends Controller
$students = Auth::user()->students()->withCount('entries')->get();
$auditions = Audition::all();
return view('students.index', ['students' => $students, 'auditions' => $auditions]);
$shirtSizes = Student::$shirtSizes;
return view('students.index',
['students' => $students, 'auditions' => $auditions, 'shirtSizes' => $shirtSizes]);
}
/**
@ -51,6 +54,14 @@ class StudentController extends Controller
new UniqueFullNameAtSchool(request('first_name'), request('last_name'), Auth::user()->school_id),
],
'grade' => ['required', 'integer'],
'shirt_size' => [
'nullable',
function ($attribute, $value, $fail) {
if (! array_key_exists($value, Student::$shirtSizes)) {
$fail("The selected $attribute is invalid.");
}
},
],
]);
$student = Student::create([
@ -59,6 +70,9 @@ class StudentController extends Controller
'grade' => request('grade'),
'school_id' => Auth::user()->school_id,
]);
if (request('shirt_size') !== 'none') {
$student->update(['optional_data->shirt_size' => $request['shirt_size']]);
}
$message = 'Created student #'.$student->id.' - '.$student->full_name().'<br>Grade: '.$student->grade.'<br>School: '.$student->school->name;
AuditLogEntry::create([
'user' => auth()->user()->email,
@ -90,7 +104,9 @@ class StudentController extends Controller
abort(403);
}
return view('students.edit', ['student' => $student]);
$shirtSizes = Student::$shirtSizes;
return view('students.edit', ['student' => $student, 'shirtSizes' => $shirtSizes]);
}
/**
@ -106,6 +122,14 @@ class StudentController extends Controller
'first_name' => ['required'],
'last_name' => ['required'],
'grade' => ['required', 'integer'],
'shirt_size' => [
'nullable',
function ($attribute, $value, $fail) {
if (! array_key_exists($value, Student::$shirtSizes)) {
$fail("The selected $attribute is invalid.");
}
},
],
]);
if (Student::where('first_name', request('first_name'))
@ -122,6 +146,9 @@ class StudentController extends Controller
'last_name' => request('last_name'),
'grade' => request('grade'),
]);
$student->update(['optional_data->shirt_size' => $request['shirt_size']]);
$message = 'Updated student #'.$student->id.'<br>Name: '.$student->full_name().'<br>Grade: '.$student->grade.'<br>School: '.$student->school->name;
AuditLogEntry::create([
'user' => auth()->user()->email,

View File

@ -0,0 +1,24 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class NominationEnsemble extends Model
{
use HasFactory;
protected function casts(): array
{
return [
'data' => 'array',
];
}
public function entries(): HasMany
{
return $this->hasMany(NominationEnsembleEntry::class);
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class NominationEnsembleEntry extends Model
{
use HasFactory;
protected $guarded = [];
protected function casts(): array
{
return [
'data' => 'array',
];
}
public function ensemble(): BelongsTo
{
return $this->belongsTo(NominationEnsemble::class, 'nomination_ensemble_id');
}
public function student(): BelongsTo
{
return $this->belongsTo(Student::class);
}
}

View File

@ -51,4 +51,15 @@ class School extends Model
'id',
'id');
}
public function nominations(): HasManyThrough
{
return $this->hasManyThrough(
NominationEnsembleEntry::class,
Student::class,
'school_id',
'student_id',
'id',
'id');
}
}

View File

@ -12,8 +12,34 @@ class Student extends Model
{
use HasFactory;
public static $shirtSizes = [
'none' => '---',
'YS' => 'Youth Small',
'YM' => 'Youth Medium',
'YL' => 'Youth Large',
'YXL' => 'Youth Extra Large',
'S' => 'Small',
'M' => 'Medium',
'L' => 'Large',
'XL' => 'Extra Large',
'2XL' => '2XL',
'3XL' => '3XL',
];
protected $guarded = [];
protected function casts(): array
{
return [
'optional_data' => 'array',
];
}
public function nominations(): HasMany
{
return $this->hasMany(NominationEnsembleEntry::class);
}
public function school(): BelongsTo
{
return $this->belongsTo(School::class);

View File

@ -10,6 +10,14 @@ use App\Actions\Tabulation\CalculateEntryScore;
use App\Actions\Tabulation\CalculateScoreSheetTotal;
use App\Actions\Tabulation\CalculateScoreSheetTotalDivideByTotalWeights;
use App\Actions\Tabulation\CalculateScoreSheetTotalDivideByWeightedPossible;
use App\Http\Controllers\NominationEnsembles\NominationAdminController;
use App\Http\Controllers\NominationEnsembles\NominationEnsembleController;
use App\Http\Controllers\NominationEnsembles\NominationEnsembleEntryController;
use App\Http\Controllers\NominationEnsembles\NominationSeatingController;
use App\Http\Controllers\NominationEnsembles\ScobdaNominationAdminController;
use App\Http\Controllers\NominationEnsembles\ScobdaNominationEnsembleController;
use App\Http\Controllers\NominationEnsembles\ScobdaNominationEnsembleEntryController;
use App\Http\Controllers\NominationEnsembles\ScobdaNominationSeatingController;
use App\Models\Audition;
use App\Models\Entry;
use App\Models\Room;
@ -62,6 +70,11 @@ class AppServiceProvider extends ServiceProvider
$this->app->singleton(UpdateEntry::class, UpdateEntry::class);
$this->app->singleton(SetHeadDirector::class, SetHeadDirector::class);
// Nomination Ensemble
$this->app->bind(NominationEnsembleController::class, ScobdaNominationEnsembleController::class);
$this->app->bind(NominationEnsembleEntryController::class, ScobdaNominationEnsembleEntryController::class);
$this->app->bind(NominationAdminController::class, ScobdaNominationAdminController::class);
$this->app->bind(NominationSeatingController::class, ScobdaNominationSeatingController::class);
}
/**

View File

@ -0,0 +1,23 @@
<?php
namespace Database\Factories;
use App\Models\NominationEnsembleEntry;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Carbon;
class NominationEnsembleEntryFactory extends Factory
{
protected $model = NominationEnsembleEntry::class;
public function definition(): array
{
return [
'student_id' => $this->faker->randomNumber(),
'nomination_ensemble_id' => $this->faker->randomNumber(),
'data' => $this->faker->words(),
'created_at' => Carbon::now(),
'updated_at' => Carbon::now(),
];
}
}

View File

@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
// Check if invoicing_enabled setting exists
$exists = DB::table('site_settings')
->where('setting_key', 'student_data_collect_shirt_size')
->exists();
// If it doesn't insert the new row
if (! $exists) {
DB::table('site_settings')->insert([
'setting_key' => 'student_data_collect_shirt_size',
'setting_value' => '0',
]);
}
}
/**
* Reverse the migrations.
*/
public function down(): void
{
//
}
};

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::table('students', function (Blueprint $table) {
$table->json('optional_data')->nullable()->after('grade');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('students', function (Blueprint $table) {
$table->dropColumn('optional_data');
});
}
};

View File

@ -0,0 +1,32 @@
<?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('nomination_ensembles', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->date('entry_deadline');
$table->integer('minimum_grade');
$table->integer('maximum_grade');
$table->json('data')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('nomination_ensembles');
}
};

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::table('nomination_ensembles', function (Blueprint $table) {
$table->unique('name');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('nomination_ensembles', function (Blueprint $table) {
$table->dropUnique(['name']);
});
}
};

View File

@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
// Check if invoicing_enabled setting exists
$exists = DB::table('site_settings')
->where('setting_key', 'nomination_ensemble_rules')
->exists();
// If it doesn't insert the new row
if (! $exists) {
DB::table('site_settings')->insert([
'setting_key' => 'nomination_ensemble_rules',
'setting_value' => 'disabled',
]);
}
}
/**
* Reverse the migrations.
*/
public function down(): void
{
}
};

View File

@ -0,0 +1,32 @@
<?php
use App\Models\NominationEnsemble;
use App\Models\Student;
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('nomination_ensemble_entries', function (Blueprint $table) {
$table->id();
$table->foreignIdFor(Student::class)->constrained()->cascadeOnUpdate()->restrictOnDelete();
$table->foreignIdFor(NominationEnsemble::class)->constrained()->cascadeOnUpdate()->restrictOnDelete();
$table->json('data');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('nomination_ensemble_entries');
}
};

View File

@ -0,0 +1,76 @@
<?php
namespace Database\Seeders;
use App\Models\NominationEnsemble;
use App\Models\NominationEnsembleEntry;
use App\Models\School;
use App\Models\Student;
use Faker\Factory as Faker;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
class ScobdaNominationEnsembleAndEntrySeeder extends Seeder
{
public function run(): void
{
// Clear existing nomination ensembles and nominations
DB::statement('SET FOREIGN_KEY_CHECKS=0;');
DB::table('nomination_ensemble_entries')->truncate();
DB::table('nomination_ensembles')->truncate();
DB::statement('SET FOREIGN_KEY_CHECKS=1;');
// Create First Year Ensemble
$ensemble = new NominationEnsemble();
$ensemble->name = 'First Year Band';
$ensemble->entry_deadline = '2028-01-01';
$ensemble->minimum_grade = 5;
$ensemble->maximum_grade = 8;
$instruments = [
'Flute',
'Oboe',
'Bassoon',
'Clarinet',
'Bass Clarinet',
'Contra Clarinet',
'Alto Sax',
'Tenor Sax',
'Bari Sax',
'Trumpet',
'Horn',
'Trombone',
'Euphonium',
'Tuba',
'String Bass',
'Percussion',
];
$data = [
'instruments' => $instruments,
'target_size' => 100,
'max_nominations' => 10,
'rounding_direction' => 'up',
];
$ensemble->data = $data;
$ensemble->save();
// Fill the nominations table
$faker = Faker::create();
$schools = School::all();
foreach ($schools as $school) {
$students = Student::factory()->count(10)->create(['school_id' => $school->id, 'grade' => 5]);
$n = 1;
foreach ($students as $student) {
$nomData = [
'rank' => $n,
'instrument' => $faker->randomElement($instruments),
];
NominationEnsembleEntry::create([
'student_id' => $student->id,
'nomination_ensemble_id' => $ensemble->id,
'data' => $nomData,
]);
$n++;
}
}
}
}

View File

@ -19,6 +19,28 @@
<x-form.body-grid columns="12" class="m-3">
<x-form.field label_text="Registration Code" name="registrationCode" colspan="3" :value="auditionSetting('registrationCode')"/>
<x-form.field label_text="Next Event Name" name="advanceTo" colspan="4" :value="auditionSetting('advanceTo')"/>
<x-form.select name="nomination_ensemble_rules" colspan="4">
<x-slot:label>Nomination Ensemble Rules</x-slot:label>
{{-- Values should be one of the options in the boot method NominationEnsembleServiceProvider --}}
<option value="disabled" {{ auditionSetting('nomination_ensemble_rules') === 'disabled' ? 'selected':'' }}>
No Nomination Ensembles
</option>
<option value="scobda" {{ auditionSetting('nomination_ensemble_rules') === 'scobda' ? 'selected':'' }}>
SCOBDA Rules
</option>
</x-form.select>
</x-form.body-grid>
</x-layout.page-section>
<x-layout.page-section>
<x-slot:section_name>Optional Student Data</x-slot:section_name>
<x-form.body-grid columns 12 class="m-3">
<div class="col-span-6 flex space-x-3">
<x-form.toggle-checkbox
checked="{{ auditionSetting('student_data_collect_shirt_size') }}"
name="student_data_collect_shirt_size"/>
<span>Collect Student Shirt Size</span>
</div>
</x-form.body-grid>
</x-layout.page-section>

View File

@ -0,0 +1,3 @@
<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="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19V5m0 14-4-4m4 4 4-4"/>
</svg>

After

Width:  |  Height:  |  Size: 296 B

View File

@ -0,0 +1,3 @@
<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" {{ $attributes }}>
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m14.304 4.844 2.852 2.852M7 7H4a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h11a1 1 0 0 0 1-1v-4.5m2.409-9.91a2.017 2.017 0 0 1 0 2.853l-6.844 6.844L8 14l.713-3.565 6.844-6.844a2.015 2.015 0 0 1 2.852 0Z"/>
</svg>

After

Width:  |  Height:  |  Size: 478 B

View File

@ -0,0 +1,3 @@
<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="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v13m0-13 4 4m-4-4-4 4"/>
</svg>

After

Width:  |  Height:  |  Size: 297 B

View File

@ -25,6 +25,9 @@
<a href="{{route('admin.schools.index')}}" class="block p-2 hover:text-indigo-600">Schools</a>
<a href="{{route('admin.students.index')}}" class="block p-2 hover:text-indigo-600">Students</a>
<a href="{{route('admin.entries.index')}}" class="block p-2 hover:text-indigo-600">Entries</a>
@if(auditionSetting('nomination_ensemble_rules') !== 'disabled')
<x-layout.navbar.menus.menu-item :href="route('nomination.admin.index')">Nominations</x-layout.navbar.menus.menu-item>
@endif
<a href="{{route('admin.view_logs')}}" class="block p-2 hover:text-indigo-600">View Logs</a>
<a href="{{route('admin.export_results')}}" class="block p-2 hover:text-indigo-600">Export Results</a>
<a href="{{route('admin.export_entries')}}" class="block p-2 hover:text-indigo-600">Export Entries</a>

View File

@ -29,6 +29,9 @@
@if(Auth::user()->school_id)
<a href="{{route('students.index')}}" class="block p-2 hover:text-indigo-600">My Students</a>
<a href="{{route('entries.index')}}" class="block p-2 hover:text-indigo-600">My Entries</a>
@if(auditionSetting('nomination_ensemble_rules') !== 'disabled')
<x-layout.navbar.menus.menu-item :href="route('nomination.entry.index')">My Nominations</x-layout.navbar.menus.menu-item>
@endif
<a href="{{route('doubler_request.index')}}" class="block p-2 hover:text-indigo-600">My Doubler Requests</a>
<a href="{{route('my_school')}}" class="block p-2 hover:text-indigo-600">My School</a>
@if(auditionSetting('invoicing_enabled'))

View File

@ -34,6 +34,9 @@
<x-layout.navbar.menus.menu-item :href="route('admin.cards.index')">Print Cards</x-layout.navbar.menus.menu-item>
<x-layout.navbar.menus.menu-item :href="route('admin.signInSheets.index')">Print Sign-In Sheets</x-layout.navbar.menus.menu-item>
<x-layout.navbar.menus.menu-item :href="route('admin.print_room_assignment_report')">Print Room and Judge Assignments</x-layout.navbar.menus.menu-item>
@if(auditionSetting('nomination_ensemble_rules') !== 'disabled')
<x-layout.navbar.menus.menu-item :href="route('nomination.admin.ensemble.index')">Nomination Ensemble Setup</x-layout.navbar.menus.menu-item>
@endif
</div>
</div>

View File

@ -33,6 +33,9 @@
@if(auditionSetting('advanceTo'))
<a href="{{ route('advancement.status') }}" class="block p-2 hover:text-indigo-600">{{ auditionSetting('advanceTo') }} Status</a>
@endif
@if(auditionSetting('nomination_ensemble_rules') !== 'disabled')
<x-layout.navbar.menus.menu-item :href="route('nomination.admin.seating.index')">Nomination Ensemble Seating</x-layout.navbar.menus.menu-item>
@endif
</div>

View File

@ -0,0 +1,73 @@
<x-layout.app>
<x-slot:page_title>Nomination Ensembles</x-slot:page_title>
<x-layout.page-section-container>
<x-layout.page-section>
<x-slot:section_name>Add Nomination Ensemble</x-slot:section_name>
<x-form.form method="POST" action="{{ route('nomination.admin.ensemble.store') }}" class="mb-6 mt-3">
<x-form.body-grid columns="9" class="max-w-full">
<x-form.field name="ensemble_name" label_text="Ensemble Name" colspan="3" autofocus />
<x-form.field name="entry_deadline" label_text="Entry Deadline" type="date" colspan="2"/>
<x-form.field name="min_grade" label_text="Minimum Grade" type="number" colspan="2"/>
<x-form.field name="max_grade" label_text="Maximum Grade" type="number" colspan="2"/>
<x-form.field name="max_nominations" label_text="Maximum Nominations per School" type="number" colspan="3"/>
<x-form.field name="target_size" label_text="Target Ensemble Size" type="number" colspan="3"/>
<x-form.select name="rounding_direction" colspan="3">
<x-slot:label>Round</x-slot:label>
<option value="up">Up</option>
<option value="down">Down</option>
</x-form.select>
<x-form.textarea name="instrument_list" colspan="9">
<x-slot:label>Instrument List (comma separated)</x-slot:label>
</x-form.textarea>
</x-form.body-grid>
<x-form.footer submit-button-text="Create Ensemble"/>
</x-form.form>
</x-layout.page-section>
<x-layout.page-section>
<x-slot:section_name>Nomination Ensembles</x-slot:section_name>
<div class="p-4">
@foreach($ensembles as $ensemble)
<x-card.card class="m-3" x-data="{ editable: false }" >
<x-card.heading>
{{ $ensemble->name }}
<x-slot:right_side class="flex">
<x-icons.pencil @click="editable = true" x-show="!editable"/>
<x-delete-resource-modal
title="Delete Nomination Ensemble {{$ensemble->name}}"
method="DELETE"
action="{{ route('nomination.admin.ensemble.destroy',[$ensemble]) }}"
>
Are you sure you want to delete this nomination ensemble?
</x-delete-resource-modal>
</x-slot:right_side>
</x-card.heading>
<x-form.form method="POST" action="{{ route('nomination.admin.ensemble.update',[$ensemble]) }}" class="mb-6 mt-3">
@method('PATCH')
<x-form.body-grid columns="9" class="max-w-full">
<x-form.field name="ensemble_name" label_text="Ensemble Name" colspan="3" value="{{ $ensemble->name }}" x-bind:readonly="!editable" />
<x-form.field name="entry_deadline" label_text="Entry Deadline" type="date" colspan="2" value="{{ $ensemble->entry_deadline }}" x-bind:readonly="!editable" />
<x-form.field name="min_grade" label_text="Minimum Grade" type="number" colspan="2" value="{{ $ensemble->minimum_grade }}" x-bind:readonly="!editable" />
<x-form.field name="max_grade" label_text="Maximum Grade" type="number" colspan="2" value="{{ $ensemble->maximum_grade }}" x-bind:readonly="!editable" />
<x-form.field name="max_nominations" label_text="Maximum Nominations per School" type="number" colspan="3" value="{{ $ensemble->data['max_nominations'] }}" x-bind:readonly="!editable" />
<x-form.field name="target_size" label_text="Target Ensemble Size" type="number" colspan="3" value="{{ $ensemble->data['target_size'] }}" x-bind:readonly="!editable" />
<x-form.select name="rounding_direction" colspan="3" x-bind:disabled="!editable" >
<x-slot:label>Round</x-slot:label>
<option value="up" @if(($ensemble->data['rounding_direction'] ?? null) == 'up') selected @endif()>Up</option>
<option value="down" @if(($ensemble->data['rounding_direction'] ?? null) == 'down') selected @endif()>Down</option>
</x-form.select>
<x-form.textarea name="instrument_list" colspan="9" x-bind:readonly="!editable" >
<x-slot:label>Instrument List (comma separated)</x-slot:label>
{{ implode(', ',$ensemble->data['instruments']) }}
</x-form.textarea>
</x-form.body-grid>
<x-form.footer submit-button-text="Edit Ensemble" x-show="editable" x-cloak/>
</x-form.form>
</x-card.card>
@endforeach
</div>
</x-layout.page-section>
</x-layout.page-section-container>
</x-layout.app>

View File

@ -0,0 +1,84 @@
<x-layout.app>
<script>
function sortTable(n) {
var table, rows, switching, i, x, y, shouldSwitch, dir, switchcount = 0;
table = document.getElementById("nominationTable");
switching = true;
// Set the sorting direction to ascending:
dir = "asc";
/* Make a loop that will continue until
no switching has been done: */
while (switching) {
// Start by saying: no switching is done:
switching = false;
rows = table.rows;
/* Loop through all table rows (except the
first, which contains table headers): */
for (i = 1; i < (rows.length - 1); i++) {
// Start by saying there should be no switching:
shouldSwitch = false;
/* Get the two elements you want to compare,
one from current row and one from the next: */
x = rows[i].getElementsByTagName("TD")[n];
y = rows[i + 1].getElementsByTagName("TD")[n];
/* Check if the two rows should switch place,
based on the direction, asc or desc: */
if (dir == "asc") {
if (x.innerHTML.toLowerCase() > y.innerHTML.toLowerCase()) {
// If so, mark as a switch and break the loop:
shouldSwitch = true;
break;
}
} else if (dir == "desc") {
if (x.innerHTML.toLowerCase() < y.innerHTML.toLowerCase()) {
// If so, mark as a switch and break the loop:
shouldSwitch = true;
break;
}
}
}
if (shouldSwitch) {
/* If a switch has been marked, make the switch
and mark that a switch has been done: */
rows[i].parentNode.insertBefore(rows[i + 1], rows[i]);
switching = true;
// Each time a switch is done, increase this count by 1:
switchcount ++;
} else {
/* If no switching has been done AND the direction is "asc",
set the direction to "desc" and run the while loop again. */
if (switchcount == 0 && dir == "asc") {
dir = "desc";
switching = true;
}
}
}
}
</script>
<x-slot:page_title>Nomination Administration</x-slot:page_title>
<x-table.table id="nominationTable">
<x-slot:title>Nominations</x-slot:title>
<thead>
<tr>
<x-table.th onclick="sortTable(0)">Name</x-table.th>
<x-table.th onclick="sortTable(1)">School</x-table.th>
<x-table.th onclick="sortTable(2)">Nomination</x-table.th>
<x-table.th onclick="sortTable(3)">Rank</x-table.th>
<x-table.th onclick="sortTable(4)">Instrument</x-table.th>
</tr>
</thead>
<x-table.body>
@foreach($nominations as $nomination)
<tr>
<x-table.td>{{ $nomination->student->full_name('lf') }}</x-table.td>
<x-table.td>{{ $nomination->student->school->name }}</x-table.td>
<x-table.td>{{ $nomination->ensemble->name }}</x-table.td>
<x-table.td>{{ $nomination->data['rank'] }}</x-table.td>
<x-table.td>{{ $nomination->data['instrument'] }}</x-table.td>
</tr>
@endforeach
</x-table.body>
</x-table.table>
</x-layout.app>

View File

@ -0,0 +1,67 @@
<x-layout.app>
<x-slot:page_title>Nomination Ensemble Seating</x-slot:page_title>
<div class="grid grid-cols-5 gap-3">
<div id="left-ensemble-list">
<nav class="flex flex-1 flex-col" aria-label="Sidebar Ensemble List">
<p class="text-md/6 font-semibold text-gray-800 mb-3">Select Ensemble</p>
<ul role="list" class="-mx2 space-y-1">
@foreach($ensembles as $menuItem)
<a href="{{ route('nomination.admin.seating.show',[$menuItem->id]) }}"
class="group flex gap-x-3 rounded-md p-2 pl-3 text-sm/6 font-semibold text-gray-700 hover:bg-gray-50 hover:text-indigo-600">
{{ $menuItem->name }}
</a>
@endforeach
</ul>
</nav>
</div>
<div id="seating-pane" class="col-span-4">
@if($ensemble)
<x-card.card>
<x-card.heading>
{{ $ensemble->name }}
<x-slot:right_side class="flex">
@if($ensemble->data['seated'] ?? false)
<x-form.form method="POST"
action="{{ route('nomination.admin.seating.seat',[$ensemble]) }}">
<input type="hidden" name="action" value="clear">
<x-form.button>Clear Seats</x-form.button>
</x-form.form>
@else
<x-form.form method="POST"
action="{{ route('nomination.admin.seating.seat',[$ensemble]) }}">
<input type="hidden" name="action" value="seat">
<x-form.button>Seat Ensemble</x-form.button>
</x-form.form>
@endif
</x-slot:right_side>
</x-card.heading>
<x-table.table>
@foreach($ensemble->data['instruments'] as $instrument)
@php($seatOn = 1)
@continue(! $acceptedNominations->has($instrument))
<tr class="border-t-2 border b-2">
<x-table.th>{{ $instrument }}</x-table.th>
<x-table.th>Student Name</x-table.th>
<x-table.th>School (Nom Rank)</x-table.th>
</tr>
@foreach($acceptedNominations[$instrument] as $nom)
<tr>
<x-table.td>{{ $seatOn }}</x-table.td>
<x-table.td>{{ $nom->student->full_name() }}</x-table.td>
<x-table.td>{{ $nom->student->school->name }} ({{ $nom->data['rank'] }})
</x-table.td>
</tr>
@php($seatOn++)
@endforeach
@endforeach
</x-table.table>
</x-card.card>
@endif
</div>
</div>
</x-layout.app>

View File

@ -0,0 +1,99 @@
@php($n=1)
<x-layout.app>
<x-slot:page_title>Nomination Entries</x-slot:page_title>
<x-layout.page-section-container>
@foreach($ensembles as $ensemble)
<x-layout.page-section>
<x-slot:section_name>{{ $ensemble->name }}</x-slot:section_name>
<x-slot:section_description>
{{ $ensemble->data['max_nominations'] }} nominations accepted<br>
Entry Deadline {{ \Carbon\Carbon::parse($ensemble->entry_deadline)->format('M j, Y') }}
</x-slot:section_description>
<x-table.table>
<thead>
<tr>
<x-table.th>Rank</x-table.th>
<x-table.th>Student</x-table.th>
<x-table.th>Instrument</x-table.th>
</tr>
</thead>
<x-table.body>
{{-- List existing nominations--}}
@foreach($nominatedStudents[$ensemble->id] as $nomination)
<tr>
<x-table.td>{{ $nomination->data['rank'] }}</x-table.td>
<x-table.td>{{ $nomination->student->full_name() }}</x-table.td>
<x-table.td>{{ $nomination->data['instrument'] }}</x-table.td>
@if($currentDate <= $ensemble->entry_deadline)
<x-table.td class="flex">
<x-delete-resource-modal
title="Delete Nomination"
method="DELETE"
action="{{ route('nomination.entry.destroy', [$nomination]) }}">
Confirm you wish to delete the nomination
of {{ $nomination->student->full_name() }}<br>
for the {{ $ensemble->name }} ensemble.
</x-delete-resource-modal>
<form method="POST" action="{{ route('nomination.entry.move') }}">
@csrf
<input type="hidden" name="direction" value="up">
<input type="hidden" name="nominationId" value="{{ $nomination->id }}">
<button class="ml-3" type="submit">
<x-icons.up-arrow/>
</button>
</form>
<form method="POST" action="{{ route('nomination.entry.move') }}">
@csrf
<input type="hidden" name="direction" value="down">
<input type="hidden" name="nominationId" value="{{ $nomination->id }}">
<button class="ml-3" type="submit">
<x-icons.down-arrow/>
</button>
</form>
</x-table.td>
@endif
</tr>
@endforeach
{{-- LINE TO ADD A NOMINATION--}}
@if($currentDate <= $ensemble->entry_deadline && $nominationsAvailable[$ensemble->id] && $availableStudents[$ensemble->id]->count() > 0)
<tr>
<x-form.form method="POST" action="{{ route('nomination.entry.store') }}">
<input type="hidden" name="ensemble" value="{{ $ensemble->id }}"/>
<x-table.th>NEW</x-table.th>
<x-table.td>
<x-form.select name="new_student">
@foreach($availableStudents[$ensemble->id] as $student)
<option value="{{$student->id}}">{{ $student->full_name() }}
(Grade {{ $student->grade }})
</option>
@endforeach
</x-form.select>
</x-table.td>
<x-table.td>
<x-form.select name="new_instrument">
@foreach($availableInstruments[$ensemble->id] as $instrument)
<option value="{{$instrument}}">{{$instrument}}</option>
@endforeach
</x-form.select>
</x-table.td>
<x-table.td>
<x-form.button class="bg-green-800">Add</x-form.button>
</x-table.td>
</x-form.form>
</tr>
@endif
</x-table.body>
</x-table.table>
</x-layout.page-section>
@endforeach
</x-layout.page-section-container>
</x-layout.app>

View File

@ -7,7 +7,15 @@
<x-form.field name="first_name" label_text="First Name" type="text" value="{{ $student->first_name }}"/>
<x-form.field name="last_name" label_text="Last Name" type="text" value="{{ $student->last_name }}"/>
<x-form.field name="grade" label_text="Grade" type="number" class="mb-3" value="{{ $student->grade }}"/>
<x-form.footer submit-button-text="Save Changes" />
@if(auditionSetting('student_data_collect_shirt_size'))
<x-form.select name="shirt_size" colspan="2">
<x-slot:label>Shirt Size</x-slot:label>
@foreach($shirtSizes as $abbreviation => $name)
<option value="{{ $abbreviation }}" @if($abbreviation === ($student->optional_data['shirt_size'] ?? null) ) SELECTED @endif>{{ $name }}</option>
@endforeach
</x-form.select>
@endif
<x-form.footer submit-button-text="Save Changes"/>
</x-form.form>
</x-card.card>
</div>

View File

@ -1,4 +1,4 @@
@php use App\Models\Audition;use Illuminate\Support\Facades\Auth; @endphp
@php use App\Models\Audition;use App\Models\NominationEnsemble;use Illuminate\Support\Facades\Auth; @endphp
<x-layout.app>
<x-slot:page_title>Students</x-slot:page_title>
@ -8,19 +8,29 @@
<x-slot:section_name>Add Student</x-slot:section_name>
<x-form.form method="POST" action="{{ route('students.store') }}" class="mb-6 mt-3">
<x-form.body-grid columns="8" class="max-w-full">
<x-form.field name="first_name" label_text="First Name" colspan="3" autofocus />
<x-form.field name="first_name" label_text="First Name" colspan="3" autofocus/>
<x-form.field name="last_name" label_text="Last Name" colspan="3"/>
{{-- <x-form.field name="grade" label_text="Grade" colspan="1" />--}}
<x-form.select name="grade">
<x-slot:label>Grade</x-slot:label>
@php($n = Audition::min('minimum_grade'))
@php($maxGrade = Audition::max('maximum_grade'))
@php($n = min(Audition::min('minimum_grade'),NominationEnsemble::min('minimum_grade')))
@php($maxGrade = max(Audition::max('maximum_grade'), NominationEnsemble::max('maximum_grade')))
@while($n <= $maxGrade)
<option value="{{ $n }}">{{ $n }}</option>
@php($n++);
@endwhile
</x-form.select>
@if(auditionSetting('student_data_collect_shirt_size'))
<x-form.select name="shirt_size" colspan="2">
<x-slot:label>Shirt Size</x-slot:label>
@foreach($shirtSizes as $abbreviation => $name)
<option value="{{ $abbreviation }}">{{ $name }}</option>
@endforeach
</x-form.select>
@endif
<x-form.button class="mt-6">Save</x-form.button>
</x-form.body-grid>
</x-form.form>
@ -35,21 +45,28 @@
<tr>
<x-table.th first>Name</x-table.th>
<x-table.th>Grade</x-table.th>
@if(auditionSetting('student_data_collect_shirt_size'))
<x-table.th>Shirt</x-table.th>
@endif
<x-table.th class="hidden md:table-cell">Entries</x-table.th>
<x-table.th spacer_only>
<span class="sr-only">Edit</span>
</x-table.th>
</tr>
</thead>
<x-table.body >
<x-table.body>
@foreach($students as $student)
<tr>
<x-table.td first>{{ $student->full_name(true) }}</x-table.td>
<x-table.td>{{ $student->grade }}</x-table.td>
@if(auditionSetting('student_data_collect_shirt_size'))
<x-table.th>{{ $student->optional_data['shirt_size'] ?? '' }}</x-table.th>
@endif
<x-table.td class="hidden md:table-cell">{{ $student->entries_count }}</x-table.td>
<x-table.td for_button>
@if( $student->entries_count === 0)
<form method="POST" action="{{ route('students.destroy',$student) }}" class="inline">
<form method="POST" action="{{ route('students.destroy',$student) }}"
class="inline">
@csrf
@method('DELETE')
<x-table.button

View File

@ -0,0 +1,36 @@
<?php
use App\Http\Controllers\NominationEnsembles\NominationAdminController;
use App\Http\Controllers\NominationEnsembles\NominationEnsembleController;
use App\Http\Controllers\NominationEnsembles\NominationEnsembleEntryController;
use App\Http\Controllers\NominationEnsembles\NominationSeatingController;
use App\Http\Middleware\CheckIfAdmin;
use Illuminate\Support\Facades\Route;
Route::middleware(['auth', 'verified', CheckIfAdmin::class])->prefix('nomination/admin/')->group(function () {
Route::prefix('ensemble/')->controller(NominationEnsembleController::class)->group(function () {
Route::get('/', 'index')->name('nomination.admin.ensemble.index');
Route::post('/', 'store')->name('nomination.admin.ensemble.store');
Route::patch('/{ensemble}', 'update')->name('nomination.admin.ensemble.update');
Route::delete('/{ensemble}', 'destroy')->name('nomination.admin.ensemble.destroy');
});
Route::prefix('nominations/')->controller(NominationAdminController::class)->group(function () {
Route::get('/', 'index')->name('nomination.admin.index');
});
Route::prefix('seating/')->controller(NominationSeatingController::class)->group(function () {
Route::get('/', 'index')->name('nomination.admin.seating.index');
Route::get('/{ensemble}', 'show')->name('nomination.admin.seating.show');
Route::post('/{ensemble}', 'seat')->name('nomination.admin.seating.seat');
});
});
Route::middleware(['auth', 'verified'])->prefix('nominations/')->group(function () {
Route::controller(NominationEnsembleEntryController::class)->group(function () {
Route::get('/', 'index')->name('nomination.entry.index');
Route::post('/', 'store')->name('nomination.entry.store');
Route::delete('/{entry}', 'destroy')->name('nomination.entry.destroy');
Route::post('/move', 'move')->name('nomination.entry.move');
});
});

View File

@ -10,6 +10,7 @@ require __DIR__.'/admin.php';
require __DIR__.'/judging.php';
require __DIR__.'/tabulation.php';
require __DIR__.'/user.php';
require __DIR__.'/nominationEnsemble.php';
Route::get('/test', [TestController::class, 'flashTest'])->middleware('auth', 'verified');