Meobda nomination ensemble #107

Merged
okorpheus merged 16 commits from meobda_nomination_ensemble into master 2025-02-17 16:20:06 +00:00
16 changed files with 995 additions and 9 deletions

View File

@ -33,7 +33,7 @@ class AuditionSettings extends Controller
'payment_state' => ['required', 'max:2'], 'payment_state' => ['required', 'max:2'],
'payment_zip' => ['required', 'min:5'], 'payment_zip' => ['required', 'min:5'],
'advanceTo' => ['nullable'], 'advanceTo' => ['nullable'],
'nomination_ensemble_rules' => ['required', 'in:disabled,scobda'], 'nomination_ensemble_rules' => ['required', 'in:disabled,scobda,meobda'],
// Options should align with the boot method of NominationEnsembleServiceProvider // Options should align with the boot method of NominationEnsembleServiceProvider
]); ]);

View File

@ -0,0 +1,113 @@
<?php
namespace App\Http\Controllers\NominationEnsembles;
use App\Http\Controllers\Controller;
use App\Models\NominationEnsemble;
use App\Models\NominationEnsembleEntry;
use App\Models\School;
class MeobdaNominationAdminController extends Controller implements NominationAdminController
{
public function index()
{
if (request('newFilterParameters')) {
$newFilterData = request()->validate([
'school' => 'nullable|exists:schools,id',
'section' => 'nullable|string',
'newFilterParameters' => 'string',
'split' => 'nullable|string',
'clear' => 'nullable|string',
]);
if ($newFilterData['clear'] ?? false == 'clear') {
$filterData = [];
session()->forget('nominationAdminFilters');
} else {
session()->put('nominationAdminFilters', $newFilterData);
$filterData = $newFilterData;
}
} elseif (session()->has('nominationAdminFilters')) {
$filterData = session()->get('nominationAdminFilters');
} else {
$filterData = [];
}
// Populate variables to complete the filter form
$schools = School::orderBy('name')->get();
$ensembles = NominationEnsemble::all();
$sections = [];
$splits = [];
foreach ($ensembles as $ensemble) {
// Populate sections for each ensemble
$sections[$ensemble->id.'---'.'all'] = $ensemble->name.' - ALL';
foreach ($ensemble->data['instruments'] as $instrument) {
$sections[$ensemble->id.'---'.$instrument['name']] = $ensemble->name.' - '.$instrument['name'];
}
// Populate splits for each ensemble
$splits[$ensemble->id.'---'.'all'] = $ensemble->name.' - ALL';
foreach ($ensemble->data['split_names'] as $splitName) {
$splits[$ensemble->id.'---'.$splitName] = $ensemble->name.' - '.$splitName;
}
}
// Get and filter nominations
$nominations = NominationEnsembleEntry::with('student.school');
if ($filterData['school'] ?? false) {
$nominations = $nominations->whereHas('student', function ($query) use ($filterData) {
$query->where('school_id', $filterData['school']);
});
}
if ($filterData['section'] ?? false) {
$sectionFilter = explode('---', $filterData['section']);
$nominations = $nominations->where('nomination_ensemble_id', $sectionFilter[0]);
if ($sectionFilter[1] != 'all') {
$nominations = $nominations->where('data->instrument', $sectionFilter[1]);
}
}
if ($filterData['split'] ?? false) {
$splitFilter = explode('---', $filterData['split']);
$nominations = $nominations->where('nomination_ensemble_id', $splitFilter[0]);
if ($splitFilter[1] != 'all') {
$nominations = $nominations->where('data->split', $splitFilter[1]);
}
}
$nominations = $nominations->paginate(50);
return view('nomination_ensembles.meobda.admin.nominations',
compact('nominations', 'schools', 'filterData', 'ensembles', 'sections', 'splits'));
}
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,141 @@
<?php
namespace App\Http\Controllers\NominationEnsembles;
use App\Http\Controllers\Controller;
use App\Models\NominationEnsemble;
use Illuminate\Validation\Rule;
use function redirect;
class MeobdaNominationEnsembleController extends Controller implements NominationEnsembleController
{
public function index()
{
$ensembles = NominationEnsemble::all();
return view('nomination_ensembles.meobda.admin.ensembles', compact('ensembles'));
}
public function show(NominationEnsemble $ensemble)
{
// TODO: Implement show() method.
}
public function create()
{
// TODO: Implement create() method.
}
public function store()
{
$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',
'instrument_list' => 'required|string',
'split_names' => 'nullable|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+$/m', '', $validated['instrument_list']);
$instrument_list = str_replace(["\r\n", "\r"], "\n", $instrument_list);
$instrument_array = explode("\n", $instrument_list);
$instruments = [];
foreach ($instrument_array as $instrument) {
$inst_arr = explode(':', $instrument);
$name = $inst_arr[0];
$max = $inst_arr[1] ?? null;
$instruments[] = [
'name' => $name,
'max' => $max,
];
}
$split_names = explode(',', $validated['split_names']);
if (count($split_names) == 1) {
$split_names[0] = $validated['ensemble_name'];
}
$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['instruments'] = $instruments;
$data['split_names'] = $split_names;
$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',
'instrument_list' => 'required|string',
'split_names' => 'nullable|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+$/m', '', $validated['instrument_list']);
$instrument_list = str_replace(["\r\n", "\r"], "\n", $instrument_list);
$instrument_array = explode("\n", $instrument_list);
$instruments = [];
foreach ($instrument_array as $instrument) {
$inst_arr = explode(':', $instrument);
$name = $inst_arr[0];
$max = $inst_arr[1] ?? null;
$instruments[] = [
'name' => $name,
'max' => $max,
];
}
$split_names = explode(',', $validated['split_names']);
if (count($split_names) == 1) {
$split_names[0] = $validated['ensemble_name'];
}
$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['instruments'] = $instruments;
$data['split_names'] = $split_names;
$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,188 @@
<?php
namespace App\Http\Controllers\NominationEnsembles;
use App\Http\Controllers\Controller;
use App\Models\NominationEnsemble;
use App\Models\NominationEnsembleEntry;
use App\Models\Student;
use Illuminate\Support\Carbon;
class MeobdaNominationEnsembleEntryController 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();
$availableInstruments = [];
$availableStudents = [];
$existingNominations = [];
$nominationsAvailable = [];
$past_deadline = [];
foreach ($ensembles as $ensemble) {
// Get existing nominations
$existingNominations[$ensemble->id] = auth()->user()->school->nominations()
->where('nomination_ensemble_id', $ensemble->id)
->get()
->keyBy('student_id');
// Count how many nominations exist on each instrument
$nominatedInstrumentCount = [];
foreach ($existingNominations[$ensemble->id] as $nom) {
$nominatedInstrumentCount[$nom->data['instrument']] =
($nominatedInstrumentCount[$nom->data['instrument']] ?? 0) + 1;
}
// Set available instruments
foreach ($ensemble->data['instruments'] as $instrument) {
// Skip it if we're at the limit on this instrument
if (! is_null($instrument['max']) && $instrument['max'] <= ($nominatedInstrumentCount[$instrument['name']] ?? 0)) {
continue;
}
$availableInstruments[$ensemble->id][] = $instrument['name'];
}
// Set available students
$students = Student::where('school_id', auth()->user()->school_id)
->where('grade', '>=', $ensemble->minimum_grade)
->where('grade', '<=', $ensemble->maximum_grade)
->orderBy('last_name', 'asc')
->orderBy('first_name', 'asc')
->get();
foreach ($existingNominations[$ensemble->id] as $checknom) {
$students = $students->reject(function ($student) use ($checknom) {
return $checknom->student_id == $student->id;
});
}
$availableStudents[$ensemble->id] = $students;
$nominationsAvailable[$ensemble->id] = $existingNominations[$ensemble->id]->count() < $ensemble->data['max_nominations'];
$past_deadline[$ensemble->id] = $currentDate > $ensemble->entry_deadline;
}
return view('nomination_ensembles.meobda.entries.index',
compact('ensembles', 'currentDate', 'availableInstruments', 'availableStudents', 'existingNominations',
'nominationsAvailable', 'past_deadline'));
}
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',
]);
$ensemble = NominationEnsemble::find($validData['ensemble']);
// Check that the deadline is not past
$currentDate = Carbon::now('America/Chicago');
$currentDate = $currentDate->format('Y-m-d');
if ($ensemble->entry_deadline < $currentDate) {
return redirect()->route('nomination.entry.index')->with('error',
'The nomination deadline for that ensemble has passed');
}
// Check if new_instrument is valid
foreach ($ensemble->data['instruments'] as $instrument) {
$availableInstruments[] = $instrument['name'];
}
if (! in_array($validData['new_instrument'], $availableInstruments)) {
return redirect()->route('nomination.entry.index')->with('error',
'Invalid Instrument Specified');
}
// Check if the student belongs to the current user
$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');
}
// Check that the user's school has nominations available
$existing_nominations = auth()->user()->school->nominations()->where('nomination_ensemble_id', $validData['ensemble']);
if ($existing_nominations->count() >= $ensemble->data['max_nominations']) {
return redirect()->route('nomination.entry.index')->with('error',
'You have already used all of your nominations for this ensemble');
}
// Check that the user's school isn't over limit for the requested instrument
$instrumentLimit = collect($ensemble->data['instruments'])->firstWhere('name',
$validData['new_instrument'])['max'];
if ($instrumentLimit) {
$used = 0;
foreach ($existing_nominations as $nom) {
if ($nom->data['instrument'] == $validData['new_instrument']) {
$used++;
}
if ($used >= $instrumentLimit) {
return redirect()->route('nomination.entry.index')->with('error',
'You may not nominate any more students on that instrument');
}
}
}
// Check that the student isn't already nominated for this ensemble
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');
}
$newEntry = new NominationEnsembleEntry();
$newEntry->student_id = $validData['new_student'];
$newEntry->nomination_ensemble_id = $validData['ensemble'];
$newEntry->data = ['instrument' => $validData['new_instrument']];
$newEntry->save();
return redirect()->route('nomination.entry.index')->with('success',
'Nomination entered');
}
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');
}
}

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;
class MeobdaNominationSeatingController extends Controller implements NominationSeatingController
{
public function index()
{
$ensembles = NominationEnsemble::all();
$ensemble = null;
return view('nomination_ensembles.meobda.admin.seating', compact('ensembles', 'ensemble'));
}
public function show(NominationEnsemble $ensemble)
{
$ensembles = NominationEnsemble::all();
$nominations = [];
if ($ensemble->data['seated'] ?? false) {
$nominations = NominationEnsembleEntry::where('nomination_ensemble_id', $ensemble->id)
->orderBy('data->instrument')
->orderByRaw('CAST(data->"$.seat" AS UNSIGNED)')
->with('student.school')
->get();
$nominations = $nominations->groupBy(function ($item) {
return $item->data['split'];
});
foreach ($nominations as $split => $splitNoms) {
$nominations[$split] = $splitNoms->groupBy(function ($item) {
return $item->data['instrument'];
});
}
}
return view('nomination_ensembles.meobda.admin.seating', compact('ensembles', 'ensemble', 'nominations'));
}
public function seat(NominationEnsemble $ensemble)
{
$validData = request()->validate([
'action' => 'required|in:seat,clear',
]);
$action = $validData['action'];
$data = $ensemble['data'];
$nominations = NominationEnsembleEntry::where('nomination_ensemble_id',
$ensemble->id)->inRandomOrder()->get();
$groupedNominations = $nominations->groupBy(function ($nom) {
return $nom->data['instrument'];
});
if ($action == 'seat') {
// If no split names are set, make the name of the ensemble the only split name
$splits = $data['split_names'];
if (count($splits) < 2 && strlen($splits[0]) < 2) {
$splits[0] = $ensemble->name;
}
$splitOn = 0;
foreach ($ensemble->data['instruments'] as $instrument) {
if (! $groupedNominations->has($instrument['name'])) {
continue;
}
$seatOn = 1;
foreach ($groupedNominations[$instrument['name']] as $nomination) {
$nomData = $nomination['data'];
$nomData['seat'] = $seatOn;
$nomData['split'] = $splits[$splitOn];
$nomination->data = $nomData;
$nomination->save();
$splitOn++;
if ($splitOn >= count($splits)) {
$seatOn++;
$splitOn = 0;
}
}
}
$data['seated'] = true;
}
if ($action == 'clear') {
$data['seated'] = false;
foreach ($nominations as $nomination) {
$nomData = $nomination['data'];
unset($nomData['split']);
unset($nomData['seat']);
$nomination->data = $nomData;
$nomination->save();
}
}
$ensemble->data = $data;
$ensemble->save();
return redirect(route('nomination.admin.seating.show', [$ensemble]));
}
}

View File

@ -71,10 +71,10 @@ class AppServiceProvider extends ServiceProvider
$this->app->singleton(SetHeadDirector::class, SetHeadDirector::class); $this->app->singleton(SetHeadDirector::class, SetHeadDirector::class);
// Nomination Ensemble // Nomination Ensemble
$this->app->bind(NominationEnsembleController::class, ScobdaNominationEnsembleController::class); // $this->app->bind(NominationEnsembleController::class, ScobdaNominationEnsembleController::class);
$this->app->bind(NominationEnsembleEntryController::class, ScobdaNominationEnsembleEntryController::class); // $this->app->bind(NominationEnsembleEntryController::class, ScobdaNominationEnsembleEntryController::class);
$this->app->bind(NominationAdminController::class, ScobdaNominationAdminController::class); // $this->app->bind(NominationAdminController::class, ScobdaNominationAdminController::class);
$this->app->bind(NominationSeatingController::class, ScobdaNominationSeatingController::class); // $this->app->bind(NominationSeatingController::class, ScobdaNominationSeatingController::class);
} }
/** /**
@ -82,7 +82,6 @@ class AppServiceProvider extends ServiceProvider
*/ */
public function boot(): void public function boot(): void
{ {
Entry::observe(EntryObserver::class); Entry::observe(EntryObserver::class);
Audition::observe(AuditionObserver::class); Audition::observe(AuditionObserver::class);
Room::observe(RoomObserver::class); Room::observe(RoomObserver::class);

View File

@ -0,0 +1,49 @@
<?php
namespace App\Providers;
use App\Http\Controllers\NominationEnsembles\MeobdaNominationAdminController;
use App\Http\Controllers\NominationEnsembles\MeobdaNominationEnsembleController;
use App\Http\Controllers\NominationEnsembles\MeobdaNominationEnsembleEntryController;
use App\Http\Controllers\NominationEnsembles\MeobdaNominationSeatingController;
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 Illuminate\Support\ServiceProvider;
use function auditionSetting;
class NominationEnsembleServiceProvider extends ServiceProvider
{
public function register(): void
{
// $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);
}
public function boot(): void
{
if (auditionSetting('nomination_ensemble_rules') === 'scobda') {
$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);
}
if (auditionSetting('nomination_ensemble_rules') === 'meobda') {
// meobda implementation
$this->app->bind(NominationEnsembleController::class, MeobdaNominationEnsembleController::class);
$this->app->bind(NominationEnsembleEntryController::class, MeobdaNominationEnsembleEntryController::class);
$this->app->bind(NominationAdminController::class, MeobdaNominationAdminController::class);
$this->app->bind(NominationSeatingController::class, MeobdaNominationSeatingController::class);
}
}
}

View File

@ -6,4 +6,5 @@ return [
App\Providers\InvoiceDataServiceProvider::class, App\Providers\InvoiceDataServiceProvider::class,
App\Providers\PrintCardActionProvider::class, App\Providers\PrintCardActionProvider::class,
Barryvdh\Debugbar\ServiceProvider::class, Barryvdh\Debugbar\ServiceProvider::class,
App\Providers\NominationEnsembleServiceProvider::class,
]; ];

View File

@ -17,9 +17,10 @@ class StudentFactory extends Factory
*/ */
public function definition(): array public function definition(): array
{ {
$school = School::factory()->create();
return [ return [
'school_id' => $school->id, 'school_id' => function () {
return School::factory()->create()->id;
},
'first_name' => fake()->firstName(), 'first_name' => fake()->firstName(),
'last_name' => fake()->lastName(), 'last_name' => fake()->lastName(),
'grade' => rand(7, 12), 'grade' => rand(7, 12),

View File

@ -0,0 +1,78 @@
<?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 MeobdaNominationEnsembleAndEntrySeeder 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 = 'Fake Band';
$ensemble->entry_deadline = '2028-01-01';
$ensemble->minimum_grade = 5;
$ensemble->maximum_grade = 8;
$instrumentNames = [
'Flute',
'Oboe',
'Bassoon',
'Clarinet',
'Bass Clarinet',
'Contra Clarinet',
'Alto Sax',
'Tenor Sax',
'Bari Sax',
'Trumpet',
'Horn',
'Trombone',
'Euphonium',
'Tuba',
'String Bass',
'Percussion',
];
foreach ($instrumentNames as $name) {
$instruments[] = [
'name' => $name,
'max' => null,
];
}
$data = [
'instruments' => $instruments,
'max_nominations' => 8,
'split_names' => ['Fake Red', 'Fake Black'],
];
$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]);
foreach ($students as $student) {
$nomData = [
'instrument' => $faker->randomElement($instrumentNames),
];
NominationEnsembleEntry::create([
'student_id' => $student->id,
'nomination_ensemble_id' => $ensemble->id,
'data' => $nomData,
]);
}
}
}
}

View File

@ -28,6 +28,9 @@
<option value="scobda" {{ auditionSetting('nomination_ensemble_rules') === 'scobda' ? 'selected':'' }}> <option value="scobda" {{ auditionSetting('nomination_ensemble_rules') === 'scobda' ? 'selected':'' }}>
SCOBDA Rules SCOBDA Rules
</option> </option>
<option value="meobda" {{ auditionSetting('nomination_ensemble_rules') === 'meobda' ? 'selected':'' }}>
MEOBDA Rules
</option>
</x-form.select> </x-form.select>
</x-form.body-grid> </x-form.body-grid>
</x-layout.page-section> </x-layout.page-section>

View File

@ -0,0 +1,84 @@
<x-layout.app>
<x-slot:page_title>Nomination Ensembles (MEOBDA Rules)</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="split_names" label_text="Split Names (comma separated, leave blank for one ensemble)" colspan="6" />
<x-form.textarea name="instrument_list" colspan="9">
<x-slot:label>Instrument List (One per line. Add :x to set a limit per school)</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="split_names" label_text="Split Names (comma separated, leave blank for one ensemble)"
colspan="6"
value="{{ implode(',',$ensemble->data['split_names'])}}"
x-bind:readonly="!editable"/>
<x-form.textarea name="instrument_list" colspan="9" x-bind:readonly="!editable">
<x-slot:label>Instrument List (One per line. Add :x to set a limit per school)</x-slot:label>
@foreach($ensemble->data['instruments'] as $instrument){{ trim($instrument['name']) }}@if($instrument['max']):{{trim($instrument['max'])}}@endif{{PHP_EOL}}@endforeach
</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,82 @@
<x-layout.app>
<x-slot:page_title>Nomination Administration</x-slot:page_title>
<x-card.card class="mb-3">
<x-card.heading>Filters</x-card.heading>
<x-form.form method="GET" action="{{route('nomination.admin.index')}}">
<input type="hidden" name="newFilterParameters" value="true">
<x-form.body-grid class="my-2">
<x-form.select name="school" onchange="this.form.submit()">
<x-slot:label>School</x-slot:label>
<option value="">All</option>
@foreach($schools as $school)
<option value="{{ $school->id }}"
@if($school->id == ($filterData['school'] ?? null)) SELECTED @endif>
{{ $school->name }}
</option>
@endforeach
</x-form.select>
<x-form.select name="section" onchange="this.form.submit()" colspan="2">
<x-slot:label>Ensemble / Instrument</x-slot:label>
<option value="">All</option>
@foreach ($sections as $value => $name)
<option value="{{$value}}"
@if($value == ($filterData['section'] ?? null)) SELECTED @endif>
{{$name}}</option>
@endforeach
</x-form.select>
<x-form.select name="split" onchange="this.form.submit()" colspan="2">
<x-slot:label>Split</x-slot:label>
<option value="">All</option>
@foreach ($splits as $value => $name)
<option value="{{$value}}"
@if($value == ($filterData['split'] ?? null)) SELECTED @endif>
{{$name}}</option>
@endforeach
</x-form.select>
<div class="align-bottom">
<p>&nbsp;</p>
<x-form.button value="clear" name="clear">Clear Filters</x-form.button>
</div>
</x-form.body-grid>
</x-form.form>
</x-card.card>
<div class="px-3">
{{ $nominations->onEachSide(3)->links() }}
</div>
<x-card.card class="mt-3">
<x-card.heading>Nominations</x-card.heading>
<x-table.table id="nominationTable">
<thead>
<tr>
<x-table.th>Name</x-table.th>
<x-table.th>School</x-table.th>
<x-table.th>Nominated For</x-table.th>
<x-table.th>Split</x-table.th>
<x-table.th>Seat</x-table.th>
</tr>
</thead>
<x-table.body>
@foreach($nominations as $nomination)
<tr>
<x-table.td>{{ $nomination->student->full_name('fl') }}</x-table.td>
<x-table.td>{{ $nomination->student->school->name }}</x-table.td>
<x-table.td>{{ $nomination->ensemble->name }} - {{ $nomination->data['instrument'] }}</x-table.td>
<x-table.td>{{ $nomination->data['split'] ?? '---' }}</x-table.td>
<x-table.td>{{ $nomination->data['seat'] ?? '---' }}</x-table.td>
</tr>
@endforeach
</x-table.body>
</x-table.table>
</x-card.card>
</x-layout.app>

View File

@ -0,0 +1,65 @@
<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>
<x-form.form action="{{route('nomination.admin.seating.seat',[$ensemble])}}">
@if($ensemble->data['seated'] ?? false)
<input type="hidden" name="action" value="clear">
<x-form.button>Clear Seats</x-form.button>
@else
<input type="hidden" name="action" value="seat">
<x-form.button>Seat</x-form.button>
@endif
</x-form.form>
</x-slot:right_side>
</x-card.heading>
</x-card.card>
@if($ensemble->data['seated'] ?? false)
@foreach($ensemble->data['split_names'] as $split)
<x-card.card class="mt-5">
<x-card.heading>{{ $split }}</x-card.heading>
@foreach ($ensemble->data['instruments'] as $instrument)
@continue(! $nominations[$split]->has($instrument['name']))
<x-card.heading class="ml-5">
{{ $instrument['name'] }}
</x-card.heading>
<x-card.list.body class="ml-10">
@foreach($nominations[$split][$instrument['name']] as $thisNomination)
<x-card.list.row>{{ $thisNomination->data['seat'] }} - {{ $thisNomination->student->full_name() }}, {{ $thisNomination->student->school->name }}</x-card.list.row>
@endforeach
</x-card.list.body>
@endforeach
</x-card.card>
@endforeach
@endif
@endif
</div>
</div>
</x-layout.app>

View File

@ -0,0 +1,78 @@
<x-layout.app>
<x-slot:page_title>Nominations</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-card.card>
<x-card.heading>Existing Nominations</x-card.heading>
<x-table.table>
<thead>
<tr>
<x-table.th>Student</x-table.th>
<x-table.th>Grade</x-table.th>
<x-table.th>Instrument</x-table.th>
</tr>
</thead>
<x-table.body>
@foreach($existingNominations[$ensemble->id] as $nom)
<tr>
<x-table.td class="flex">
{{ $nom->student->full_name() }}
</x-table.td>
<x-table.td>{{ $nom->student->grade }}</x-table.td>
<x-table.td>{{ $nom->data['instrument'] }}</x-table.td>
@if(!$past_deadline[$ensemble->id])
<x-table.td>
<x-delete-resource-modal
title="Delete Nomination"
method="DELETE"
action="{{ route('nomination.entry.destroy', [$nom]) }}">
Confirm you wish to delete the nomination
of {{ $nom->student->full_name() }}<br>
for the {{ $ensemble->name }} ensemble.
</x-delete-resource-modal>
</x-table.td>
@endif
</tr>
@endforeach
</x-table.body>
</x-table.table>
</x-card.card>
@if($nominationsAvailable[$ensemble->id] && !$past_deadline[$ensemble->id])
<x-card.card>
<x-card.heading>New Entry</x-card.heading>
<x-form.form method="POST" action="{{ route('nomination.entry.store') }}">
<x-form.body-grid columns="5" class="mb-4 mt-4">
<input type="hidden" name="ensemble" value="{{ $ensemble->id }}"/>
<x-form.select name="new_student" colspan="2">
@foreach($availableStudents[$ensemble->id] as $student)
<option value="{{$student->id}}">{{ $student->full_name() }}
(Grade {{ $student->grade }})
</option>
@endforeach
</x-form.select>
<x-form.select name="new_instrument" colspan="2">
@foreach($availableInstruments[$ensemble->id] as $instrument)
<option value="{{$instrument}}">{{ $instrument }}</option>
@endforeach
</x-form.select>
<x-form.button>Add Nomination</x-form.button>
</x-form.body-grid>
</x-form.form>
</x-card.card>
@endif
</x-layout.page-section>
@endforeach
</x-layout.page-section-container>
</x-layout.app>

View File

@ -38,6 +38,7 @@
@endif @endif
</x-slot:right_side> </x-slot:right_side>
</x-card.heading> </x-card.heading>
<x-table.table> <x-table.table>
@foreach($ensemble->data['instruments'] as $instrument) @foreach($ensemble->data['instruments'] as $instrument)
@php($seatOn = 1) @php($seatOn = 1)