Work on Admin Bonus Score Index

#20 Implement bonus scores
Index shows a card for each bonus score
Include a help modal
Include a form to create a new bonus score
This commit is contained in:
Matt Young 2024-07-15 01:41:06 -05:00
parent 192191c079
commit 0f1ca583dd
11 changed files with 191 additions and 14 deletions

View File

@ -3,12 +3,29 @@
namespace App\Http\Controllers\Admin; namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Http\Request; use App\Models\BonusScoreDefinition;
use function to_route;
class BonusScoreDefinitionController extends Controller class BonusScoreDefinitionController extends Controller
{ {
public function index() public function index()
{ {
return view('admin.bonus-scores.index'); $bonusScores = BonusScoreDefinition::all();
return view('admin.bonus-scores.index', compact('bonusScores'));
}
public function store()
{
$validData = request()->validate([
'name' => 'required',
'max_score' => 'required|numeric',
'weight' => 'required|numeric',
]);
BonusScoreDefinition::create($validData);
return to_route('admin.bonus-scores.index')->with('success', 'Bonus Score Created');
} }
} }

View File

@ -8,4 +8,6 @@ use Illuminate\Database\Eloquent\Model;
class BonusScoreDefinition extends Model class BonusScoreDefinition extends Model
{ {
use HasFactory; use HasFactory;
protected $fillable = ['name', 'max_score', 'weight'];
} }

View File

@ -0,0 +1,25 @@
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\BonusScoreDefinition>
*/
class BonusScoreDefinitionFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'name' => $this->faker->word,
'max_score' => $this->faker->randomNumber(2),
'weight' => $this->faker->randomFloat(2, 0, 2),
];
}
}

View File

@ -15,8 +15,11 @@ return new class extends Migration
{ {
Schema::create('bonus_score_audition_assignment', function (Blueprint $table) { Schema::create('bonus_score_audition_assignment', function (Blueprint $table) {
$table->id(); $table->id();
$table->foreignIdFor(BonusScoreDefinition::class)->constrained()->onDelete('cascade')->onUpdate('cascade'); $table->foreignIdFor(BonusScoreDefinition::class)
$table->foreignIdFor(Audition::class)->constrained()->onDelete('cascade')->onUpdate('cascade'); ->constrained('bonus_score_definitions', 'id', 'bs_audition_assignment_bonus_score_definition_id')
->onDelete('cascade')->onUpdate('cascade');
$table->foreignIdFor(Audition::class)
->constrained()->onDelete('cascade')->onUpdate('cascade');
$table->timestamps(); $table->timestamps();
}); });
} }

View File

@ -0,0 +1,16 @@
<x-modal-body show-var="showAddBonusScoreModal">
<x-slot:title>
Add Bonus Score
</x-slot:title>
<x-form.form id="create-bonus-score-form" action="{{ route('admin.bonus-scores.store') }}">
<x-form.body-grid columns="12">
<x-form.field name="name" label_text="Name" colspan="8" />
<x-form.field name="max_score" type="number" label_text="Max Points" colspan="2" />
<x-form.field name="weight" label_text="Weight" colspan="2" />
<div class="col-start-9 col-span-4 row-start-2">
<x-form.button >Create Bonus Score</x-form.button>
</div>
</x-form.body-grid>
</x-form.form>
</x-modal-body>

View File

@ -0,0 +1,24 @@
<x-help-modal title="Bonus Score Definitions">
<p class="mb-5">Bonus scores are most often used for an improvisation score for jazz band auditions. A bonus score
earned by an entry will be directly added
to that entries final score. When you create a bonus score, you will also specify to which auditions that bonus
score should apply. When a student
earns a bonus score for one entry, that bonus will be applied to all entries that receive that bonus score.</p>
<p class="mb-5">
Let's say you create a bonus score called, "Saxophone Improvisation," and assign Jazz Alto, Jazz Tenor, and Jazz
Bari auditions to that bonus
score. If a student is entered on all three saxes, when they receive an improv score on one sax, that score will
apply to all 3. The system
will not allow another improv score to be assigned by the same judge unless the first one is deleted. If you
want that student to improv on each instrument
separately, you will need to create a separate bonus score for each instrument.
</p>
<P>
The weight allows you to control how much influence the bonus score has on the outcome of the audition. The
bonus score is
multiplied by the weight then added to the final score. The weight may be any positive number, including
decimals.
</P>
</x-help-modal>

View File

@ -0,0 +1,22 @@
<div class="text-center">
<svg class="mx-auto w-12 h-12 text-gray-400 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m14.304 4.844 2.852 2.852M7 7H4a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h11a1 1 0 0 0 1-1v-4.5m2.409-9.91a2.017 2.017 0 0 1 0 2.853l-6.844 6.844L8 14l.713-3.565 6.844-6.844a2.015 2.015 0 0 1 2.852 0Z"/>
</svg>
<h3 class="mt-2 text-sm font-semibold text-gray-900">No bonus scores have been created</h3>
<p class="mt-1 text-sm text-gray-500">Get started by creating a new bonus score.</p>
<div class="mt-6">
<button type="button"
x-on:click="showAddBonusScoreModal = true"
class="inline-flex items-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">
<svg class="-ml-0.5 mr-1.5 h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path
d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z"/>
</svg>
New Bonus Score
</button>
</div>
</div>

View File

@ -1,4 +1,22 @@
<x-layout.app> <x-layout.app x-data="{ showAddBonusScoreModal: false }">
<x-slot:page_title>Bonus Score Management</x-slot:page_title>
<x-slot:title_bar_right>
@include('admin.bonus-scores.index-help-modal')
</x-slot:title_bar_right>
@if($bonusScores->count() === 0)
@include('admin.bonus-scores.index-no-bonus-scores-message')
@endif
@foreach($bonusScores as $bonusScore)
<x-card.card>
<x-card.heading>
{{ $bonusScore->name }}
<x-slot:subheading>
Max Points: {{ $bonusScore->max_score }} | Weight: {{ $bonusScore->weight }}
</x-slot:subheading>
</x-card.heading>
</x-card.card>
@endforeach
@include('admin.bonus-scores.index-add-bonus-score-modal')
</x-layout.app> </x-layout.app>

View File

@ -1,12 +1,12 @@
@props(['title'=>false]) @props(['title'=>false, 'showVar'=>'showModal'])
<div <div
class="fixed inset-0 z-30 flex items-center justify-center overflow-auto bg-black bg-opacity-50" class="fixed inset-0 z-30 flex items-center justify-center overflow-auto bg-black bg-opacity-50"
x-show="showModal" x-cloak x-show="{{ $showVar }}" x-cloak
> >
<!-- Modal inner --> <!-- Modal inner -->
<div <div
class="max-w-3xl px-6 py-4 mx-auto text-left bg-white rounded shadow-lg" class="max-w-3xl px-6 py-4 mx-auto text-left bg-white rounded shadow-lg"
@click.away="showModal = false" @click.away="{{ $showVar }} = false"
x-transition:enter="motion-safe:ease-out duration-300" x-transition:enter="motion-safe:ease-out duration-300"
x-transition:enter-start="opacity-0 scale-90" x-transition:enter-start="opacity-0 scale-90"
x-transition:enter-end="opacity-100 scale-100" x-transition:enter-end="opacity-100 scale-100"
@ -17,7 +17,7 @@
<h5 {{ $title->attributes->merge(['class' => 'mr-3 text-black max-w-none']) }}>{{ $title ?? '' }}</h5> <h5 {{ $title->attributes->merge(['class' => 'mr-3 text-black max-w-none']) }}>{{ $title ?? '' }}</h5>
@endif @endif
<button type="button" class="z-50 cursor-pointer" @click="showModal = false"> <button type="button" class="z-50 cursor-pointer" @click="{{ $showVar}} = false">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg> </svg>

View File

@ -16,11 +16,11 @@ Route::middleware(['auth', 'verified', CheckIfAdmin::class])->prefix('admin/')->
// Admin Bonus Scores Routes // Admin Bonus Scores Routes
Route::prefix('bonus-scores')->controller(\App\Http\Controllers\Admin\BonusScoreDefinitionController::class)->group(function () { Route::prefix('bonus-scores')->controller(\App\Http\Controllers\Admin\BonusScoreDefinitionController::class)->group(function () {
Route::get('/', 'index')->name('admin.bonus-scores.index'); Route::get('/', 'index')->name('admin.bonus-scores.index');
// Route::get('/create', 'create')->name('admin.bonus-scores.create'); // Route::get('/create', 'create')->name('admin.bonus-scores.create');
// Route::post('/', 'store')->name('admin.bonus-scores.store'); Route::post('/', 'store')->name('admin.bonus-scores.store');
// Route::get('/{bonusScoreDefinition}/edit', 'edit')->name('admin.bonus-scores.edit'); // Route::get('/{bonusScoreDefinition}/edit', 'edit')->name('admin.bonus-scores.edit');
// Route::patch('/{bonusScoreDefinition}', 'update')->name('admin.bonus-scores.update'); // Route::patch('/{bonusScoreDefinition}', 'update')->name('admin.bonus-scores.update');
// Route::delete('/{bonusScoreDefinition}', 'destroy')->name('admin.bonus-scores.destroy'); // Route::delete('/{bonusScoreDefinition}', 'destroy')->name('admin.bonus-scores.destroy');
}); });
// Admin Ensemble Routes // Admin Ensemble Routes

View File

@ -1,6 +1,8 @@
<?php <?php
use App\Models\BonusScoreDefinition;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Sinnbeck\DomAssertions\Asserts\AssertForm;
uses(RefreshDatabase::class); uses(RefreshDatabase::class);
@ -26,3 +28,51 @@ it('grants access to an administrator', function () {
->assertOk() ->assertOk()
->assertViewIs('admin.bonus-scores.index'); ->assertViewIs('admin.bonus-scores.index');
}); });
it('if no bonus scores exist, show a create bonus score message', function () {
// Arrange
actAsAdmin();
// Act & Assert
$this->get(route('admin.bonus-scores.index'))
->assertOk()
->assertSee('No bonus scores have been created');
});
it('includes a form to add a new bonus score', function () {
// Arrange
actAsAdmin();
// Act & Assert
$this->get(route('admin.bonus-scores.index'))
->assertOk()
->assertFormExists('#create-bonus-score-form', function (AssertForm $form) {
/** @noinspection PhpUndefinedMethodInspection */
$form->hasCSRF()
->hasMethod('POST')
->hasAction(route('admin.bonus-scores.store'))
->containsInput(['name' => 'name'])
->containsInput(['name' => 'max_score', 'type' => 'number'])
->containsInput(['name' => 'weight']);
});
});
it('can create a new subscore', function () {
// Arrange
$submissionData = [
'name' => 'New Bonus Score',
'max_score' => 10,
'weight' => 1,
];
// Act & Assert
actAsAdmin();
$this->post(route('admin.bonus-scores.store'), $submissionData)
->assertRedirect(route('admin.bonus-scores.index'))
->assertSessionHas('success', 'Bonus Score Created');
$test = BonusScoreDefinition::where('name', 'New Bonus Score')->first();
expect($test->exists())->toBeTrue();
});
it('shows existing bonus scores', function () {
// Arrange
$bonusScores = BonusScoreDefinition::factory()->count(3)->create();
actAsAdmin();
// Act & Assert
$response = $this->get(route('admin.bonus-scores.index'));
$response->assertOk();
$bonusScores->each(fn ($bonusScore) => $response->assertSee($bonusScore->name));
});