Bonus Score Judge Management
#20 Implement bonus scores Bonus score judge management complete.
This commit is contained in:
parent
bfb4b54e18
commit
551491ea87
|
|
@ -5,6 +5,7 @@ namespace App\Http\Controllers\Admin;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\Audition;
|
use App\Models\Audition;
|
||||||
use App\Models\BonusScoreDefinition;
|
use App\Models\BonusScoreDefinition;
|
||||||
|
use App\Models\User;
|
||||||
use App\Rules\ValidateAuditionKey;
|
use App\Rules\ValidateAuditionKey;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
@ -82,6 +83,35 @@ class BonusScoreDefinitionController extends Controller
|
||||||
|
|
||||||
public function judges()
|
public function judges()
|
||||||
{
|
{
|
||||||
echo 'boo';
|
$bonusScores = BonusScoreDefinition::all();
|
||||||
|
$users = User::all();
|
||||||
|
|
||||||
|
return view('admin.bonus-scores.judge-assignments', compact('bonusScores', 'users'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function assignJudge(BonusScoreDefinition $bonusScore)
|
||||||
|
{
|
||||||
|
if (! $bonusScore->exists()) {
|
||||||
|
return redirect()->route('admin.bonus-scores.judges')->with('error', 'Bonus Score not found');
|
||||||
|
}
|
||||||
|
$validData = request()->validate([
|
||||||
|
'judge' => 'required|exists:users,id',
|
||||||
|
]);
|
||||||
|
$bonusScore->judges()->attach($validData['judge']);
|
||||||
|
|
||||||
|
return redirect()->route('admin.bonus-scores.judges')->with('success', 'Judge assigned to bonus score');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeJudge(BonusScoreDefinition $bonusScore)
|
||||||
|
{
|
||||||
|
if (! $bonusScore->exists()) {
|
||||||
|
return redirect()->route('admin.bonus-scores.judges')->with('error', 'Bonus Score not found');
|
||||||
|
}
|
||||||
|
$validData = request()->validate([
|
||||||
|
'judge' => 'required|exists:users,id',
|
||||||
|
]);
|
||||||
|
$bonusScore->judges()->detach($validData['judge']);
|
||||||
|
|
||||||
|
return redirect()->route('admin.bonus-scores.judges')->with('success', 'Judge removed from bonus score');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,4 +16,9 @@ class BonusScoreDefinition extends Model
|
||||||
{
|
{
|
||||||
return $this->belongsToMany(Audition::class, 'bonus_score_audition_assignment')->orderBy('score_order');
|
return $this->belongsToMany(Audition::class, 'bonus_score_audition_assignment')->orderBy('score_order');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function judges(): BelongsToMany
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(User::class, 'bonus_score_judge_assignment');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\BonusScoreDefinition;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('bonus_score_judge_assignment', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignIdFor(BonusScoreDefinition::class)
|
||||||
|
->constrained('bonus_score_definitions', 'id', 'bs_judge_assignment_bonus_score_definition_id')
|
||||||
|
->onDelete('cascade')->onUpdate('cascade');
|
||||||
|
$table->foreignIdFor(User::class)
|
||||||
|
->constrained()->onDelete('cascade')->onUpdate('cascade');
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('bonus_score_judge_assignment');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,111 @@
|
||||||
|
<x-layout.app>
|
||||||
|
<div class="bg-white pt-3 pb-1 px-3 rounded-md">
|
||||||
|
<div class="mb-3">
|
||||||
|
<nav class="flex space-x-4" aria-label="Tabs">
|
||||||
|
<!-- Current: "bg-indigo-100 text-indigo-700", Default: "text-gray-500 hover:text-gray-700" -->
|
||||||
|
<a href="{{route('admin.rooms.judgingAssignment')}}" class="rounded-md px-3 py-2 text-sm font-medium text-gray-500 hover:text-gray-700">Room Judges</a>
|
||||||
|
<a href="{{route('admin.bonus-scores.judges')}}" class="rounded-md px-3 py-2 text-sm font-medium bg-indigo-100 text-indigo-700">Bonus Judges</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="grid md:grid-cols-4 gap-5 mt-5">
|
||||||
|
|
||||||
|
@foreach($bonusScores as $bonusScore)
|
||||||
|
<li id="bonus-{{$bonusScore->id}}-card" class=" rounded-xl border border-gray-200 bg-gray-50 "> {{-- card wrapper --}}
|
||||||
|
<div class="flex items-center gap-x-4 border-b border-gray-900/5 bg-white pt-2 pb-6 px-6"> {{-- card header --}}
|
||||||
|
<div class="text-sm font-medium leading-6 text-gray-900">
|
||||||
|
<p class="text-sm font-medium leading-6 text-gray-900">{{ $bonusScore->name }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="relative ml-auto" x-data="{ open: false }"> {{-- Auditions Dropdown --}}
|
||||||
|
<button type="button"
|
||||||
|
class="-m-2.5 block p-2.5 text-gray-400 hover:text-gray-500"
|
||||||
|
id="options-menu-0-button"
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-haspopup="true"
|
||||||
|
x-on:click="open = ! open">
|
||||||
|
<span class="sr-only">Open details</span>
|
||||||
|
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||||
|
<path
|
||||||
|
d="M3 10a1.5 1.5 0 113 0 1.5 1.5 0 01-3 0zM8.5 10a1.5 1.5 0 113 0 1.5 1.5 0 01-3 0zM15.5 8.5a1.5 1.5 0 100 3 1.5 1.5 0 000-3z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Dropdown menu, show/hide based on menu state.
|
||||||
|
|
||||||
|
Entering: "transition ease-out duration-100"
|
||||||
|
From: "transform opacity-0 scale-95"
|
||||||
|
To: "transform opacity-100 scale-100"
|
||||||
|
Leaving: "transition ease-in duration-75"
|
||||||
|
From: "transform opacity-100 scale-100"
|
||||||
|
To: "transform opacity-0 scale-95"
|
||||||
|
-->
|
||||||
|
<div
|
||||||
|
class="absolute right-5 -top-4 z-10 mt-0.5 w-32 origin-top-right rounded-md bg-white py-0.5 shadow-lg ring-1 ring-gray-900/5 focus:outline-none overflow-y-auto max-h-64"
|
||||||
|
role="menu"
|
||||||
|
aria-orientation="vertical"
|
||||||
|
aria-labelledby="options-menu-0-button"
|
||||||
|
tabindex="-1"
|
||||||
|
x-show="open"
|
||||||
|
x-cloak>
|
||||||
|
|
||||||
|
<!-- Active: "bg-gray-50", Not Active: "" -->
|
||||||
|
@foreach($bonusScore->auditions as $audition)
|
||||||
|
<p class="block px-3 py-0.5 text-xs leading-6 text-gray-900">{{ $audition->name }}</p>
|
||||||
|
@endforeach
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div> {{-- End Card Header --}}
|
||||||
|
|
||||||
|
|
||||||
|
<dl class="-my-3 divide-y divide-gray-100 px-6 pb-4 pt-1 text-sm leading-6 bg-gray-50"> {{-- Judge Listing --}}
|
||||||
|
@foreach($bonusScore->judges as $judge)
|
||||||
|
<div class="flex justify-between items-center gap-x-4 py-1"> {{-- Judge Line --}}
|
||||||
|
<dt>
|
||||||
|
<p>
|
||||||
|
<span class="text-gray-700">{{ $judge->full_name() }} </span>
|
||||||
|
<span class="text-gray-500 text-xs">{{ $judge->school->name ?? '' }}</span>
|
||||||
|
</p>
|
||||||
|
<p class="text-gray-500 text-xs">{{ $judge->judging_preference }}</p>
|
||||||
|
</dt>
|
||||||
|
<dd class="text-gray-500 text-xs">
|
||||||
|
<form method="POST" action="{{route('admin.bonus-scores.judges.remove', $bonusScore) }}" id="removeJudgeFromRoom{{ $bonusScore->id }}">
|
||||||
|
@csrf
|
||||||
|
@method('DELETE')
|
||||||
|
<input type="hidden" name="judge" value="{{ $judge->id }}">
|
||||||
|
<button>
|
||||||
|
<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="none" viewBox="0 0 24 24">
|
||||||
|
<path stroke="#d1d5db" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m15 9-6 6m0-6 6 6m6-3a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
|
||||||
|
<div class="pt-3"> {{-- Add Judge Form --}}
|
||||||
|
<form method="POST" action="{{route('admin.bonus-scores.judges.assign', $bonusScore)}}" id="assignJudgeToRoom{{ $bonusScore->id }}">
|
||||||
|
@csrf
|
||||||
|
<select name="judge"
|
||||||
|
id="judge"
|
||||||
|
class="block w-full rounded-md border-0 py-1.5 pl-3 pr-10 text-gray-900 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-indigo-600 sm:text-sm sm:leading-6"
|
||||||
|
onchange="document.getElementById('assignJudgeToRoom{{ $bonusScore->id }}').submit()">
|
||||||
|
<option>Add a judge</option>
|
||||||
|
@foreach($users as $judge)
|
||||||
|
@if($bonusScore->judges->contains($judge->id))
|
||||||
|
@continue
|
||||||
|
@endif
|
||||||
|
<option value="{{ $judge->id }}">{{ $judge->full_name() }}
|
||||||
|
- {{ $judge->judging_preference }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
</x-layout.app>
|
||||||
|
|
@ -39,6 +39,8 @@ Route::middleware(['auth', 'verified', CheckIfAdmin::class])->prefix('admin/')->
|
||||||
Route::delete('/{audition}/unassign_audition', 'unassignAudition')->name('admin.bonus-scores.unassignAudition');
|
Route::delete('/{audition}/unassign_audition', 'unassignAudition')->name('admin.bonus-scores.unassignAudition');
|
||||||
Route::delete('/{bonusScore}', 'destroy')->name('admin.bonus-scores.destroy');
|
Route::delete('/{bonusScore}', 'destroy')->name('admin.bonus-scores.destroy');
|
||||||
Route::get('/judges', 'judges')->name('admin.bonus-scores.judges');
|
Route::get('/judges', 'judges')->name('admin.bonus-scores.judges');
|
||||||
|
Route::delete('{bonusScore}/judges/', 'removeJudge')->name('admin.bonus-scores.judges.remove');
|
||||||
|
Route::post('{bonusScore}/judges/', 'assignJudge')->name('admin.bonus-scores.judges.assign');
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\BonusScoreDefinition;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
|
||||||
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
|
it('denies access to guests and non administrators', function () {
|
||||||
|
$this->get(route('admin.bonus-scores.judges'))
|
||||||
|
->assertRedirect(route('home'));
|
||||||
|
|
||||||
|
actAsNormal();
|
||||||
|
$this->get(route('admin.bonus-scores.judges'))
|
||||||
|
->assertRedirect(route('dashboard'))
|
||||||
|
->assertSessionHas('error', 'You are not authorized to perform this action');
|
||||||
|
|
||||||
|
actAsTab();
|
||||||
|
$this->get(route('admin.bonus-scores.judges'))
|
||||||
|
->assertRedirect(route('dashboard'))
|
||||||
|
->assertSessionHas('error', 'You are not authorized to perform this action');
|
||||||
|
});
|
||||||
|
it('grants access to an administrator', function () {
|
||||||
|
// Arrange
|
||||||
|
actAsAdmin();
|
||||||
|
// Act & Assert
|
||||||
|
$this->get(route('admin.bonus-scores.judges'))
|
||||||
|
->assertOk()
|
||||||
|
->assertViewIs('admin.bonus-scores.judge-assignments');
|
||||||
|
});
|
||||||
|
it('shows a link to the room judge assignment screen', function () {
|
||||||
|
// Arrange
|
||||||
|
actAsAdmin();
|
||||||
|
// Act & Assert
|
||||||
|
$this->get(route('admin.bonus-scores.judges'))
|
||||||
|
->assertOk()
|
||||||
|
->assertSee(route('admin.rooms.judgingAssignment'));
|
||||||
|
});
|
||||||
|
it('shows a card for each bonus score', function () {
|
||||||
|
// Arrange
|
||||||
|
$bonusScores = BonusScoreDefinition::factory()->count(3)->create();
|
||||||
|
actAsAdmin();
|
||||||
|
// Act & Assert
|
||||||
|
$response = $this->get(route('admin.bonus-scores.judges'));
|
||||||
|
$response->assertOk();
|
||||||
|
$bonusScores->each(fn ($bonus) => $response->assertElementExists('#bonus-'.$bonus->id.'-card'));
|
||||||
|
});
|
||||||
|
it('can assign a judge to a bonus score', function () {
|
||||||
|
// Arrange
|
||||||
|
$bonusScore = BonusScoreDefinition::factory()->create();
|
||||||
|
$judge = User::factory()->create();
|
||||||
|
actAsAdmin();
|
||||||
|
// Act & Assert
|
||||||
|
$this->post(route('admin.bonus-scores.judges.assign', $bonusScore), ['judge' => $judge->id])
|
||||||
|
->assertRedirect(route('admin.bonus-scores.judges'))
|
||||||
|
->assertSessionHas('success', 'Judge assigned to bonus score');
|
||||||
|
expect($bonusScore->judges()->count())->toBe(1);
|
||||||
|
});
|
||||||
|
it('can assign a judge to a room', function () {
|
||||||
|
// Arrange
|
||||||
|
$bonusScore = BonusScoreDefinition::factory()->create();
|
||||||
|
$judge = User::factory()->create();
|
||||||
|
$bonusScore->judges()->attach($judge->id);
|
||||||
|
actAsAdmin();
|
||||||
|
// Act & Assert
|
||||||
|
$this->delete(route('admin.bonus-scores.judges.remove', $bonusScore), ['judge' => $judge->id])
|
||||||
|
->assertRedirect(route('admin.bonus-scores.judges'))
|
||||||
|
->assertSessionHas('success', 'Judge removed from bonus score');
|
||||||
|
expect($bonusScore->judges()->count())->toBe(0);
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue