Auditionadmin 20 - Bonus scores are fully functional #25
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
</x-card.card>
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue