Displayin etudes

This commit is contained in:
Matt Young 2025-12-18 13:56:58 -06:00
parent e9a6379438
commit 750c9792be
16 changed files with 394 additions and 17 deletions

View File

@ -0,0 +1,20 @@
<?php
namespace App\Actions;
use Carbon\Carbon;
class GetCurrentAuditionEtudeSet
{
public function __invoke(): int
{
return $this->getCurrentSet();
}
public function getCurrentSet(?string $date = null): int
{
$date = $date ? Carbon::parse($date) : now();
return $date->month < 3 ? $date->year : $date->year + 1;
}
}

View File

@ -1,7 +1,8 @@
<?php <?php
namespace App\Http\Controllers; namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Http\Requests\EtudeUploadRequest; use App\Http\Requests\EtudeUploadRequest;
use App\Models\AuditionedEnsemble; use App\Models\AuditionedEnsemble;
use App\Models\AuditionEtude; use App\Models\AuditionEtude;
@ -58,6 +59,12 @@ class AuditionEtudeController extends Controller
'file_size' => $fileSize, 'file_size' => $fileSize,
]); ]);
session([
'previous_instrument_id' => $request->instrument_id,
'previous_auditioned_ensemble_id' => $request->auditioned_ensemble_id,
'previous_set' => $request->set,
]);
return redirect()->route('admin.etudes.index')->with('success', 'Etude uploaded successfully.'); return redirect()->route('admin.etudes.index')->with('success', 'Etude uploaded successfully.');
} }

View File

@ -0,0 +1,28 @@
<?php
namespace App\Http\Controllers;
use App\Models\AuditionedEnsemble;
use App\Models\Instrument;
use App\Services\AuditionEtudeService;
class EtudesController extends Controller
{
public function __invoke(AuditionEtudeService $service)
{
$ensembles = AuditionedEnsemble::all();
$instruments = Instrument::has('etudes')->withCount('etudes')->get();
$schoolYear = $service->getActiveSchoolYear();
$currentSet = [];
$etudes = [];
foreach ($ensembles as $ensemble) {
$thisSet = $service->getSetForEnsemble($ensemble);
$currentSet[$ensemble->id] = $thisSet;
$etudes[$ensemble->id] = $ensemble
->etudes()->where('set', '=', $thisSet)
->get()->keyBy('instrument_id');
}
return view('etudes', compact('ensembles', 'schoolYear', 'currentSet', 'instruments','etudes'));
}
}

View File

@ -38,7 +38,7 @@ class EtudeUploadRequest extends FormRequest
} }
}, },
], ],
'file_upload' => ['required', 'file', 'mimes:pdf', 'mimetypes:application/pdf', 'max:10240'], 'file_upload' => ['required', 'file', 'mimes:pdf', 'mimetypes:application/pdf', 'max:51200'],
]; ];
} }
} }

View File

@ -3,10 +3,16 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class AuditionedEnsemble extends Model class AuditionedEnsemble extends Model
{ {
protected $fillable = [ protected $fillable = [
'name', 'set_count', 'name', 'set_count',
]; ];
public function etudes(): HasMany
{
return $this->hasMany(AuditionEtude::class);
}
} }

View File

@ -3,10 +3,16 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Instrument extends Model class Instrument extends Model
{ {
protected $fillable = [ protected $fillable = [
'instrument', 'score_order', 'instrument', 'score_order',
]; ];
public function etudes(): HasMany
{
return $this->hasMany(AuditionEtude::class);
}
} }

View File

@ -2,6 +2,7 @@
namespace App\Providers; namespace App\Providers;
use App\Services\AuditionEtudeService;
use App\Services\SiteDataService; use App\Services\SiteDataService;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
@ -13,6 +14,7 @@ class AppServiceProvider extends ServiceProvider
public function register(): void public function register(): void
{ {
$this->app->singleton(SiteDataService::class); $this->app->singleton(SiteDataService::class);
$this->app->singleton(AuditionEtudeService::class);
} }
/** /**

View File

@ -0,0 +1,80 @@
<?php
namespace App\Services;
use App\Models\AuditionedEnsemble;
use Carbon\Carbon;
readonly class AuditionEtudeService
{
protected int $startYear;
protected int $changeoverMonth;
public function __construct()
{
$this->startYear = config('siteData.etude_start_year');
$this->changeoverMonth = config('siteData.etude_changeover_month');
}
/**
* Get the audition year for a given date.
*
* The audition year follows a cycle based on the configured changeover month.
* Before the changeover month, returns the current calendar year.
* On or after the changeover month, returns the next calendar year.
*
* @param string|null $date Optional date string in 'YYYY-MM-DD' format. Defaults to the current date.
* @return int The audition year
*
* @example
* getCurrentAuditionYear('2024-01-15') // Returns 2024 (if changeover month is 3)
* getCurrentAuditionYear('2024-03-01') // Returns 2025 (if changeover month is 3)
* getCurrentAuditionYear() // Returns current audition year
*/
public function getCurrentAuditionYear(?string $date = null): int
{
$date = $date ? Carbon::parse($date) : now();
return $date->month < $this->changeoverMonth ? $date->year : $date->year + 1;
}
/**
* Get the active school year as a string for a given date.
*
* @param string|null $date Optional date string in 'YYYY-MM-DD' format. Defaults to the current date.
* @return string The school year in "YYYY - YYYY" format
*
* @example
* getActiveSchoolYear('2024-01-15') // Returns "2023 - 2024"
* getActiveSchoolYear('2024-09-01') // Returns "2024 - 2025"
*/
public function getActiveSchoolYear(?string $date = null): string
{
$auditionYear = $this->getCurrentAuditionYear($date);
return ($auditionYear - 1).' - '.$auditionYear;
}
/**
* Get the current set number for an ensemble in a given year.
*
* Sets rotate annually based on the ensemble's set count.
* The rotation is calculated from the configured start year.
*
* @param AuditionedEnsemble $ensemble The ensemble to get the set for
* @param int|null $year Optional year. Defaults to current audition year.
* @return int The set number (1 to ensemble's set_count)
*
* @example
* getSetForEnsemble($ensemble, 2024) // Returns the set number for 2024
*/
public function getSetForEnsemble(AuditionedEnsemble $ensemble, ?int $year = null): int
{
$year = $year ?? $this->getCurrentAuditionYear();
$setCount = $ensemble->set_count;
$yearDiff = $year - $this->startYear;
return ($yearDiff % $setCount) + 1;
}
}

View File

@ -30,7 +30,7 @@ class App extends Component
], ],
[ [
'name' => 'Audition Etudes', 'name' => 'Audition Etudes',
'link' => '#', 'link' => route('etudes'),
], ],
]; ];
} }

View File

@ -17,6 +17,7 @@
"laravel/tinker": "^2.10.1" "laravel/tinker": "^2.10.1"
}, },
"require-dev": { "require-dev": {
"barryvdh/laravel-debugbar": "^3.16",
"fakerphp/faker": "^1.23", "fakerphp/faker": "^1.23",
"laravel/boost": "^1.8", "laravel/boost": "^1.8",
"laravel/pail": "^1.2.2", "laravel/pail": "^1.2.2",

160
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "5157014187d3ae76a3586965840ec1f0", "content-hash": "b97c7859ce60e3e2bd8fd47d5ed2f249",
"packages": [ "packages": [
{ {
"name": "bacon/bacon-qr-code", "name": "bacon/bacon-qr-code",
@ -6445,6 +6445,91 @@
} }
], ],
"packages-dev": [ "packages-dev": [
{
"name": "barryvdh/laravel-debugbar",
"version": "v3.16.2",
"source": {
"type": "git",
"url": "https://github.com/barryvdh/laravel-debugbar.git",
"reference": "730dbf8bf41f5691e026dd771e64dd54ad1b10b3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/730dbf8bf41f5691e026dd771e64dd54ad1b10b3",
"reference": "730dbf8bf41f5691e026dd771e64dd54ad1b10b3",
"shasum": ""
},
"require": {
"illuminate/routing": "^10|^11|^12",
"illuminate/session": "^10|^11|^12",
"illuminate/support": "^10|^11|^12",
"php": "^8.1",
"php-debugbar/php-debugbar": "^2.2.4",
"symfony/finder": "^6|^7"
},
"require-dev": {
"mockery/mockery": "^1.3.3",
"orchestra/testbench-dusk": "^7|^8|^9|^10",
"phpunit/phpunit": "^9.5.10|^10|^11",
"squizlabs/php_codesniffer": "^3.5"
},
"type": "library",
"extra": {
"laravel": {
"aliases": {
"Debugbar": "Barryvdh\\Debugbar\\Facades\\Debugbar"
},
"providers": [
"Barryvdh\\Debugbar\\ServiceProvider"
]
},
"branch-alias": {
"dev-master": "3.16-dev"
}
},
"autoload": {
"files": [
"src/helpers.php"
],
"psr-4": {
"Barryvdh\\Debugbar\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Barry vd. Heuvel",
"email": "barryvdh@gmail.com"
}
],
"description": "PHP Debugbar integration for Laravel",
"keywords": [
"debug",
"debugbar",
"dev",
"laravel",
"profiler",
"webprofiler"
],
"support": {
"issues": "https://github.com/barryvdh/laravel-debugbar/issues",
"source": "https://github.com/barryvdh/laravel-debugbar/tree/v3.16.2"
},
"funding": [
{
"url": "https://fruitcake.nl",
"type": "custom"
},
{
"url": "https://github.com/barryvdh",
"type": "github"
}
],
"time": "2025-12-03T14:52:46+00:00"
},
{ {
"name": "brianium/paratest", "name": "brianium/paratest",
"version": "v7.8.4", "version": "v7.8.4",
@ -8059,6 +8144,79 @@
}, },
"time": "2022-02-21T01:04:05+00:00" "time": "2022-02-21T01:04:05+00:00"
}, },
{
"name": "php-debugbar/php-debugbar",
"version": "v2.2.4",
"source": {
"type": "git",
"url": "https://github.com/php-debugbar/php-debugbar.git",
"reference": "3146d04671f51f69ffec2a4207ac3bdcf13a9f35"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-debugbar/php-debugbar/zipball/3146d04671f51f69ffec2a4207ac3bdcf13a9f35",
"reference": "3146d04671f51f69ffec2a4207ac3bdcf13a9f35",
"shasum": ""
},
"require": {
"php": "^8",
"psr/log": "^1|^2|^3",
"symfony/var-dumper": "^4|^5|^6|^7"
},
"replace": {
"maximebf/debugbar": "self.version"
},
"require-dev": {
"dbrekelmans/bdi": "^1",
"phpunit/phpunit": "^8|^9",
"symfony/panther": "^1|^2.1",
"twig/twig": "^1.38|^2.7|^3.0"
},
"suggest": {
"kriswallsmith/assetic": "The best way to manage assets",
"monolog/monolog": "Log using Monolog",
"predis/predis": "Redis storage"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.1-dev"
}
},
"autoload": {
"psr-4": {
"DebugBar\\": "src/DebugBar/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Maxime Bouroumeau-Fuseau",
"email": "maxime.bouroumeau@gmail.com",
"homepage": "http://maximebf.com"
},
{
"name": "Barry vd. Heuvel",
"email": "barryvdh@gmail.com"
}
],
"description": "Debug bar in the browser for php application",
"homepage": "https://github.com/php-debugbar/php-debugbar",
"keywords": [
"debug",
"debug bar",
"debugbar",
"dev"
],
"support": {
"issues": "https://github.com/php-debugbar/php-debugbar/issues",
"source": "https://github.com/php-debugbar/php-debugbar/tree/v2.2.4"
},
"time": "2025-07-22T14:01:30+00:00"
},
{ {
"name": "phpdocumentor/reflection-common", "name": "phpdocumentor/reflection-common",
"version": "2.2.0", "version": "2.2.0",

View File

@ -2,6 +2,24 @@
return [ return [
/* /*
|--------------------------------------------------------------------------
| Etude Start Year
|--------------------------------------------------------------------------
|
| A year in the past where all ensembles used set 1
|
*/
'etude_start_year' => env('ETUDE_START_YEAR', 2009),
/*
|--------------------------------------------------------------------------
| Etude Start Year
|--------------------------------------------------------------------------
|
| A year in the past where all ensembles used set 1
|
*/
'etude_changeover_month' => env('ETUDE_CHANGEOVER_MONTH', 3),
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------

View File

@ -34,7 +34,12 @@ class InstrumentSeeder extends Seeder
['instrument' => 'Euphonium TC', 'score_order' => 170], ['instrument' => 'Euphonium TC', 'score_order' => 170],
['instrument' => 'Tuba', 'score_order' => 180], ['instrument' => 'Tuba', 'score_order' => 180],
['instrument' => 'Percussion', 'score_order' => 200], ['instrument' => 'Percussion', 'score_order' => 200],
['instrument' => 'Drums', 'score_order' => 210],
['instrument' => 'Vibes', 'score_order' => 220],
['instrument' => 'String Bass', 'score_order' => 300], ['instrument' => 'String Bass', 'score_order' => 300],
['instrument' => 'Bass', 'score_order' => 400],
['instrument' => 'Piano', 'score_order' => 410],
['instrument' => 'Guitar', 'score_order' => 420],
]; ];
Instrument::insert($defaultInstruments); Instrument::insert($defaultInstruments);

View File

@ -7,11 +7,14 @@
<div class="flex"> <div class="flex">
<div class="shrink-0"> <div class="shrink-0">
<svg class="size-5 text-red-400" viewBox="0 0 20 20" fill="currentColor"> <svg class="size-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z" clip-rule="evenodd" /> <path fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z"
clip-rule="evenodd"/>
</svg> </svg>
</div> </div>
<div class="ml-3"> <div class="ml-3">
<h3 class="text-sm font-medium text-red-800 dark:text-red-200">There were {{ $errors->count() }} error(s) with your submission</h3> <h3 class="text-sm font-medium text-red-800 dark:text-red-200">There
were {{ $errors->count() }} error(s) with your submission</h3>
<div class="mt-2 text-sm text-red-700 dark:text-red-300"> <div class="mt-2 text-sm text-red-700 dark:text-red-300">
<ul class="list-disc space-y-1 pl-5"> <ul class="list-disc space-y-1 pl-5">
@foreach($errors->all() as $error) @foreach($errors->all() as $error)
@ -24,13 +27,20 @@
</div> </div>
@endif @endif
<x-form.form method="POST" action="{{ route('admin.etudes.store') }}" enctype="multipart/form-data" x-data="{ selectedEnsemble: '', selectedInstrument: '', selectedSetNumber: '' }"> <x-form.form method="POST" action="{{ route('admin.etudes.store') }}" enctype="multipart/form-data"
x-data="{ selectedEnsemble: '', selectedInstrument: '', selectedSetNumber: '' }">
<div> <div>
<x-form.select name="auditioned_ensemble_id" x-model="selectedEnsemble"> <x-form.select name="auditioned_ensemble_id" x-model="selectedEnsemble">
<x-slot:label>Ensemble</x-slot:label> <x-slot:label>Ensemble</x-slot:label>
<option value="">Select Ensemble...</option> <option value="">Select Ensemble...</option>
@foreach($ensembles as $ensemble) @foreach($ensembles as $ensemble)
<option value="{{ $ensemble->id }}" {{ old('auditioned_ensemble_id') === $ensemble->id ? ' SELECTED ':'' }}>{{ $ensemble->name }}</option> <option
value="{{ $ensemble->id }}"
{{ old('auditioned_ensemble_id') == $ensemble->id ? ' SELECTED ':'' }}
{{ session('previous_auditioned_ensemble_id') == $ensemble->id ? ' SELECTED ':'' }}
>
{{ $ensemble->name }}
</option>
@endforeach @endforeach
</x-form.select> </x-form.select>
</div> </div>
@ -39,18 +49,31 @@
<option value="">Select Instrument...</option> <option value="">Select Instrument...</option>
<x-slot:label>Instrument</x-slot:label> <x-slot:label>Instrument</x-slot:label>
@foreach($instruments as $instrument) @foreach($instruments as $instrument)
<option value="{{ $instrument->id }}" {{ old('instrument_id') === $instrument->id ? ' SELECTED ':'' }}>{{ $instrument->instrument }}</option> <option
value="{{ $instrument->id }}"
{{ old('instrument_id') === $instrument->id ? ' SELECTED ':'' }}
{{ session('previous_instrument_id') === $instrument->id ? ' SELECTED ':'' }}
>
{{ $instrument->instrument }}
</option>
@endforeach @endforeach
</x-form.select> </x-form.select>
</div> </div>
<div class="mt-3"> <div class="mt-3">
<x-form.input name="set" label="Set Number" value="{{ old('set') }}" type="number" min="1" x-bind:max="selectedEnsemble ? getSetCount(selectedEnsemble) : ''" x-bind:disabled="!selectedEnsemble" x-model="selectedSetNumber" /> <x-form.input name="set" label="Set Number"
:value="session('previous_set')"
type="number" min="1"
x-bind:max="selectedEnsemble ? getSetCount(selectedEnsemble) : ''"
x-bind:disabled="!selectedEnsemble" x-model="selectedSetNumber"/>
</div> </div>
<div class="mt-3"> <div class="mt-3">
<x-form.input name="file_upload" type="file" label="Etude PDF" /> <x-form.input name="file_upload" type="file" label="Etude PDF"/>
</div> </div>
<div class="mt-3 text-right"> <div class="mt-3 text-right">
<x-form.button type="submit" x-bind:disabled="!selectedEnsemble || !selectedInstrument || !selectedSetNumber">Save Etude</x-form.button> <x-form.button type="submit"
x-bind:disabled="!selectedEnsemble || !selectedInstrument || !selectedSetNumber">Save
Etude
</x-form.button>
</div> </div>
</x-form.form> </x-form.form>
</x-slot:body> </x-slot:body>
@ -65,6 +88,7 @@
}, },
@endforeach @endforeach
}; };
function getSetCount(ensembleId) { function getSetCount(ensembleId) {
// noinspection JSUnresolvedVariable // noinspection JSUnresolvedVariable
return ensembles[ensembleId]?.setCount || ''; return ensembles[ensembleId]?.setCount || '';

View File

@ -0,0 +1,20 @@
<x-layout.app>
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-3">
@foreach($ensembles as $ensemble)
<x-card class="mb-8">
<x-slot:header>{{ $ensemble->name }}<br />{{ $schoolYear }}<br />Set {{ $currentSet[$ensemble->id] }}</x-slot:header>
<ul>
@foreach($instruments as $instrument)
@continue(! $etudes[$ensemble->id]->has($instrument->id))
<li class="mb-2">
<a class="text-blue-800 underline" href="/{{ $etudes[$ensemble->id][$instrument->id]->file_path }}">
{{ $instrument->instrument }}
</a>
</li>
@endforeach
</ul>
</x-card>
@endforeach
</div>
</x-layout.app>

View File

@ -1,17 +1,19 @@
<?php <?php
use App\Http\Controllers\Admin\AuditionEtudeController;
use App\Http\Controllers\Admin\DashboardController; use App\Http\Controllers\Admin\DashboardController;
use App\Http\Controllers\Admin\SiteDataController; use App\Http\Controllers\Admin\SiteDataController;
use App\Http\Controllers\Admin\UsersController; use App\Http\Controllers\Admin\UsersController;
use App\Http\Controllers\AuditionEtudeController;
use App\Http\Controllers\AuditionInformationPageController; use App\Http\Controllers\AuditionInformationPageController;
use App\Http\Controllers\ClinicInformationPageController; use App\Http\Controllers\ClinicInformationPageController;
use App\Http\Controllers\EtudesController;
use App\Http\Controllers\WelcomeController; use App\Http\Controllers\WelcomeController;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
Route::get('/', WelcomeController::class)->name('welcome'); Route::get('/', WelcomeController::class)->name('welcome');
Route::get('/audition-information', AuditionInformationPageController::class)->name('audition-information'); Route::get('/audition-information', AuditionInformationPageController::class)->name('audition-information');
Route::get('/clinic-information', ClinicInformationPageController::class)->name('clinic-information'); Route::get('/clinic-information', ClinicInformationPageController::class)->name('clinic-information');
Route::get('/etudes', EtudesController::class)->name('etudes');
Route::middleware(['auth'])->prefix('admin')->name('admin.')->group(function () { Route::middleware(['auth'])->prefix('admin')->name('admin.')->group(function () {
Route::get('/', DashboardController::class)->name('dashboard'); Route::get('/', DashboardController::class)->name('dashboard');