diff --git a/app/Http/Controllers/Admin/BonusScoreDefinitionController.php b/app/Http/Controllers/Admin/BonusScoreDefinitionController.php index 59c8635..43cf6a5 100644 --- a/app/Http/Controllers/Admin/BonusScoreDefinitionController.php +++ b/app/Http/Controllers/Admin/BonusScoreDefinitionController.php @@ -3,17 +3,24 @@ namespace App\Http\Controllers\Admin; use App\Http\Controllers\Controller; +use App\Models\Audition; use App\Models\BonusScoreDefinition; +use App\Rules\ValidateAuditionKey; +use Exception; +use Illuminate\Http\Request; +use function redirect; use function to_route; class BonusScoreDefinitionController extends Controller { public function index() { - $bonusScores = BonusScoreDefinition::all(); + $bonusScores = BonusScoreDefinition::with('auditions')->get(); + // Set auditions equal to the collection of auditions that do not have a related bonus score + $unassignedAuditions = Audition::orderBy('score_order')->doesntHave('bonusScore')->get(); - return view('admin.bonus-scores.index', compact('bonusScores')); + return view('admin.bonus-scores.index', compact('bonusScores', 'unassignedAuditions')); } public function store() @@ -28,4 +35,48 @@ class BonusScoreDefinitionController extends Controller return to_route('admin.bonus-scores.index')->with('success', 'Bonus Score Created'); } + + public function destroy(BonusScoreDefinition $bonusScore) + { + if ($bonusScore->auditions()->count() > 0) { + return to_route('admin.bonus-scores.index')->with('error', 'Bonus Score has auditions attached'); + } + $bonusScore->delete(); + + return to_route('admin.bonus-scores.index')->with('success', 'Bonus Score Deleted'); + } + + public function assignAuditions(Request $request) + { + $validData = $request->validate([ + 'bonus_score_id' => 'required|exists:bonus_score_definitions,id', + 'audition' => 'required|array', + 'audition.*' => ['required', new ValidateAuditionKey()], + ]); + $bonusScore = BonusScoreDefinition::find($validData['bonus_score_id']); + + foreach ($validData['audition'] as $auditionId => $value) { + try { + $bonusScore->auditions()->attach($auditionId); + } catch (Exception $ex) { + return redirect()->route('admin.bonus-scores.index')->with('error', + 'Error assigning auditions to bonus score - '.$ex->getMessage()); + } + } + + return redirect()->route('admin.bonus-scores.index')->with('success', 'Auditions assigned to bonus score'); + } + + public function unassignAudition(Audition $audition) + { + if (! $audition->exists()) { + return redirect()->route('admin.bonus-scores.index')->with('error', 'Audition not found'); + } + if (! $audition->bonusScore()->count() > 0) { + return redirect()->route('admin.bonus-scores.index')->with('error', 'Audition does not have a bonus score'); + } + $audition->bonusScore()->detach(); + + return redirect()->route('admin.bonus-scores.index')->with('success', 'Audition unassigned from bonus score'); + } } diff --git a/app/Models/Audition.php b/app/Models/Audition.php index aba117d..e7a243b 100644 --- a/app/Models/Audition.php +++ b/app/Models/Audition.php @@ -48,6 +48,11 @@ class Audition extends Model return $this->belongsTo(ScoringGuide::class); } + public function bonusScore(): BelongsToMany + { + return $this->belongsToMany(BonusScoreDefinition::class, 'bonus_score_audition_assignment'); + } + public function display_fee(): string { return '$'.number_format($this->entry_fee / 100, 2); diff --git a/app/Models/BonusScoreDefinition.php b/app/Models/BonusScoreDefinition.php index abe0d32..590df30 100644 --- a/app/Models/BonusScoreDefinition.php +++ b/app/Models/BonusScoreDefinition.php @@ -4,10 +4,16 @@ namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; class BonusScoreDefinition extends Model { use HasFactory; protected $fillable = ['name', 'max_score', 'weight']; + + public function auditions(): BelongsToMany + { + return $this->belongsToMany(Audition::class, 'bonus_score_audition_assignment')->orderBy('score_order'); + } } diff --git a/app/Rules/ValidateAuditionKey.php b/app/Rules/ValidateAuditionKey.php new file mode 100644 index 0000000..fcdea0f --- /dev/null +++ b/app/Rules/ValidateAuditionKey.php @@ -0,0 +1,19 @@ +where('id', $key)->exists()) { + $fail('Invalid audition id provided'); + } + } +} diff --git a/database/migrations/2024_07_15_042419_create_bonus_score_audition_assignment_table.php b/database/migrations/2024_07_15_042419_create_bonus_score_audition_assignment_table.php index e229bb4..5dd73e6 100644 --- a/database/migrations/2024_07_15_042419_create_bonus_score_audition_assignment_table.php +++ b/database/migrations/2024_07_15_042419_create_bonus_score_audition_assignment_table.php @@ -18,7 +18,7 @@ return new class extends Migration $table->foreignIdFor(BonusScoreDefinition::class) ->constrained('bonus_score_definitions', 'id', 'bs_audition_assignment_bonus_score_definition_id') ->onDelete('cascade')->onUpdate('cascade'); - $table->foreignIdFor(Audition::class) + $table->foreignIdFor(Audition::class)->unique() ->constrained()->onDelete('cascade')->onUpdate('cascade'); $table->timestamps(); }); diff --git a/resources/views/admin/bonus-scores/index-add-auditions-to-bonus-modal.blade.php b/resources/views/admin/bonus-scores/index-add-auditions-to-bonus-modal.blade.php new file mode 100644 index 0000000..f258fa5 --- /dev/null +++ b/resources/views/admin/bonus-scores/index-add-auditions-to-bonus-modal.blade.php @@ -0,0 +1,15 @@ + + Add auditions to + + + + @foreach($unassignedAuditions as $audition) + + + + @endforeach + + Add Checked Auditions + + + diff --git a/resources/views/admin/bonus-scores/index.blade.php b/resources/views/admin/bonus-scores/index.blade.php index 60a4797..4e42676 100644 --- a/resources/views/admin/bonus-scores/index.blade.php +++ b/resources/views/admin/bonus-scores/index.blade.php @@ -1,4 +1,4 @@ - + Bonus Score Management @include('admin.bonus-scores.index-help-modal') @@ -8,15 +8,46 @@ @endif @foreach($bonusScores as $bonusScore) - + + {{ $bonusScore->name }} Max Points: {{ $bonusScore->max_score }} | Weight: {{ $bonusScore->weight }} + + @if($bonusScore->auditions()->count() === 0) + + Confirm you want to delete the bonus score {{ $bonusScore->name }} + + @endif + + + @foreach($bonusScore->auditions as $audition) + + + @csrf + @method('DELETE') + + + + + {{ $audition->name }} + + @endforeach + + + + Add Auditions to {{ $bonusScore->name }} + @endforeach - + @if($bonusScores->count() !== 0) + Add Bonus Score + @endif + @include('admin.bonus-scores.index-add-auditions-to-bonus-modal') @include('admin.bonus-scores.index-add-bonus-score-modal') diff --git a/resources/views/components/icons/circled-x.blade.php b/resources/views/components/icons/circled-x.blade.php new file mode 100644 index 0000000..49cf742 --- /dev/null +++ b/resources/views/components/icons/circled-x.blade.php @@ -0,0 +1,4 @@ +@props(['color' => 'currentColor', 'title'=>false]) + + + diff --git a/routes/admin.php b/routes/admin.php index 0ff8a52..b06b06c 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -16,11 +16,11 @@ Route::middleware(['auth', 'verified', CheckIfAdmin::class])->prefix('admin/')-> // Admin Bonus Scores Routes Route::prefix('bonus-scores')->controller(\App\Http\Controllers\Admin\BonusScoreDefinitionController::class)->group(function () { Route::get('/', 'index')->name('admin.bonus-scores.index'); - // Route::get('/create', 'create')->name('admin.bonus-scores.create'); Route::post('/', 'store')->name('admin.bonus-scores.store'); - // Route::get('/{bonusScoreDefinition}/edit', 'edit')->name('admin.bonus-scores.edit'); - // Route::patch('/{bonusScoreDefinition}', 'update')->name('admin.bonus-scores.update'); - // Route::delete('/{bonusScoreDefinition}', 'destroy')->name('admin.bonus-scores.destroy'); + Route::post('/assign_auditions', 'assignAuditions')->name('admin.bonus-scores.addAuditions'); + Route::delete('/{audition}/unassign_audition', 'unassignAudition')->name('admin.bonus-scores.unassignAudition'); + Route::delete('/{bonusScore}', 'destroy')->name('admin.bonus-scores.destroy'); + }); // Admin Ensemble Routes diff --git a/tests/Feature/Pages/Setup/BonusScoreIndexTest.php b/tests/Feature/Pages/Setup/BonusScoreIndexTest.php index 6aa1f4d..ab52651 100644 --- a/tests/Feature/Pages/Setup/BonusScoreIndexTest.php +++ b/tests/Feature/Pages/Setup/BonusScoreIndexTest.php @@ -1,5 +1,6 @@ containsInput(['name' => 'weight']); }); }); -it('can create a new subscore', function () { +it('can create a new bonus score', function () { // Arrange $submissionData = [ 'name' => 'New Bonus Score', @@ -76,3 +77,75 @@ it('shows existing bonus scores', function () { $response->assertOk(); $bonusScores->each(fn ($bonusScore) => $response->assertSee($bonusScore->name)); }); +it('can delete a bonus score with no auditions', function () { + // Arrange + $bonusScore = BonusScoreDefinition::factory()->create(); + actAsAdmin(); + // Act & Assert + $this->delete(route('admin.bonus-scores.destroy', $bonusScore)) + ->assertRedirect(route('admin.bonus-scores.index')) + ->assertSessionHas('success', 'Bonus Score Deleted'); + expect(BonusScoreDefinition::count())->toBe(0); +}); +it('will not delete a bonus score that has auditions attached', function () { + // Arrange + $bonusScore = BonusScoreDefinition::factory()->hasAuditions(1)->create(); + actAsAdmin(); + // Act & Assert + $this->delete(route('admin.bonus-scores.destroy', $bonusScore)) + ->assertRedirect(route('admin.bonus-scores.index')) + ->assertSessionHas('error', 'Bonus Score has auditions attached'); + expect(BonusScoreDefinition::count())->toBe(1); +}); +it('can assign auditions to a bonus score', function () { + // Arrange + $bonusScore = BonusScoreDefinition::factory()->create(); + $auditions = Audition::factory()->count(3)->create(); + $submissionData = [ + 'bonus_score_id' => $bonusScore->id, + ]; + foreach ($auditions as $audition) { + $submissionData['audition'][$audition->id] = 'on'; + } + // Act & Assert + actAsAdmin(); + $this->post(route('admin.bonus-scores.addAuditions'), $submissionData) + ->assertRedirect(route('admin.bonus-scores.index')) + ->assertSessionHas('success', 'Auditions assigned to bonus score'); + $bonusScore->refresh(); + $auditions->each(fn ($audition) => expect($bonusScore->auditions->contains($audition))->toBeTrue()); +}); +it('can unassign auditions from a bonus score', function () { + // Arrange + $bonusScore = BonusScoreDefinition::factory()->hasAuditions(3)->create(); + $audition = $bonusScore->auditions->first(); + // Act & Assert + actAsAdmin(); + $this->delete(route('admin.bonus-scores.unassignAudition', $audition)) + ->assertRedirect(route('admin.bonus-scores.index')) + ->assertSessionHas('success', 'Audition unassigned from bonus score'); + $bonusScore->refresh(); + expect($bonusScore->auditions->contains($audition))->toBeFalse(); +}); +it('sends a message when attempting to unassign an audition that is not assigned', function () { + $bonusScore = BonusScoreDefinition::factory()->create(); + $audition = Audition::factory()->create(); + actAsAdmin(); + $this->delete(route('admin.bonus-scores.unassignAudition', $audition)) + ->assertRedirect(route('admin.bonus-scores.index')) + ->assertSessionHas('error', 'Audition does not have a bonus score'); +}); +it('will not allow an audition to be assigned to multiple bonus scores', function () { + $bonusScore1 = BonusScoreDefinition::factory()->create(); + $bonusScore2 = BonusScoreDefinition::factory()->create(); + $audition = Audition::factory()->create(); + $bonusScore1->auditions()->attach($audition); + $submissionData = [ + 'bonus_score_id' => $bonusScore2->id, + 'audition' => [$audition->id => 'on'], + ]; + actAsAdmin(); + $this->post(route('admin.bonus-scores.addAuditions'), $submissionData) + ->assertRedirect(route('admin.bonus-scores.index')) + ->assertSessionHas('error', 'Error assigning auditions to bonus score'); +});