Auditionadmin 20 - Bonus scores are fully functional #25

Merged
okorpheus merged 16 commits from auditionadmin-20 into master 2024-07-16 08:04:08 +00:00
10 changed files with 215 additions and 11 deletions
Showing only changes of commit 977618cd2e - Show all commits

View File

@ -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');
}
}

View File

@ -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);

View File

@ -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');
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Rules;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Support\Facades\DB;
class ValidateAuditionKey implements ValidationRule
{
public function validate(string $attribute, mixed $value, Closure $fail): void
{
// Extract the key from the attribute
$key = explode('.', $attribute)[1];
if (! DB::table('auditions')->where('id', $key)->exists()) {
$fail('Invalid audition id provided');
}
}
}

View File

@ -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();
});

View File

@ -0,0 +1,15 @@
<x-modal-body showVar="showAddAuditionModal">
<x-slot:title>Add auditions to <span x-text="addAuditionToName"></span></x-slot:title>
<x-form.form method="POST" action="{{ route('admin.bonus-scores.addAuditions') }}">
<input type="hidden" name=bonus_score_id x-bind:value="addAuditionTo">
<div class="grid grid-cols-3">
@foreach($unassignedAuditions as $audition)
<div class="mx-5 my-1">
<x-form.checkbox name="audition[{{$audition->id}}]" label="{{ $audition->name }}" />
</div>
@endforeach
</div>
<x-form.button class="mt-3" type="submit">Add Checked Auditions</x-form.button>
</x-form.form>
</x-modal-body>

View File

@ -1,4 +1,4 @@
<x-layout.app x-data="{ showAddBonusScoreModal: false }">
<x-layout.app x-data="{ showAddBonusScoreModal: false, addAuditionTo: null, showAddAuditionModal: false, addAuditionToName: null }">
<x-slot:page_title>Bonus Score Management</x-slot:page_title>
<x-slot:title_bar_right>
@include('admin.bonus-scores.index-help-modal')
@ -8,15 +8,46 @@
@endif
@foreach($bonusScores as $bonusScore)
<x-card.card>
<x-card.card class="mx-auto max-w-xl mb-5">
<x-card.heading>
{{ $bonusScore->name }}
<x-slot:subheading>
Max Points: {{ $bonusScore->max_score }} | Weight: {{ $bonusScore->weight }}
</x-slot:subheading>
<x-slot:right_side>
@if($bonusScore->auditions()->count() === 0)
<x-delete-resource-modal title="Delete Bonus Score" action="{{route('admin.bonus-scores.destroy', $bonusScore)}}">
Confirm you want to delete the bonus score {{ $bonusScore->name }}
</x-delete-resource-modal>
@endif
</x-slot:right_side>
</x-card.heading>
<div class="grid grid-cols-3 mx-5 my-2">
@foreach($bonusScore->auditions as $audition)
<div class="flex gap-x-2">
<form method="post" id="unassign{{$audition->id}}" action="{{ route('admin.bonus-scores.unassignAudition', $audition) }}">
@csrf
@method('DELETE')
<button type="submit">
<x-icons.circled-x color="crimson" />
</button>
</form>
{{ $audition->name }}
</div>
@endforeach
</div>
<x-form.button
x-on:click="showAddAuditionModal=true; addAuditionTo={{ $bonusScore->id }}; addAuditionToName='{{ $bonusScore->name }}'"
class="mx-auto max-w-sm mb-3">
Add Auditions to {{ $bonusScore->name }}
</x-form.button>
</x-card.card>
@endforeach
@if($bonusScores->count() !== 0)
<x-form.button class="mx-auto max-w-xs mt-5" x-on:click="showAddBonusScoreModal=true">Add Bonus Score</x-form.button>
@endif
@include('admin.bonus-scores.index-add-auditions-to-bonus-modal')
@include('admin.bonus-scores.index-add-bonus-score-modal')
</x-layout.app>

View File

@ -0,0 +1,4 @@
@props(['color' => 'currentColor', 'title'=>false])
<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="{{$color}}" viewBox="0 0 24 24">
<path fill-rule="evenodd" d="M2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm7.707-3.707a1 1 0 0 0-1.414 1.414L10.586 12l-2.293 2.293a1 1 0 1 0 1.414 1.414L12 13.414l2.293 2.293a1 1 0 0 0 1.414-1.414L13.414 12l2.293-2.293a1 1 0 0 0-1.414-1.414L12 10.586 9.707 8.293Z" clip-rule="evenodd"/>
</svg>

View File

@ -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

View File

@ -1,5 +1,6 @@
<?php
use App\Models\Audition;
use App\Models\BonusScoreDefinition;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Sinnbeck\DomAssertions\Asserts\AssertForm;
@ -52,7 +53,7 @@ it('includes a form to add a new bonus score', function () {
->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');
});