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\Models\Audition;
|
||||
use App\Models\BonusScoreDefinition;
|
||||
use App\Models\User;
|
||||
use App\Rules\ValidateAuditionKey;
|
||||
use Exception;
|
||||
use Illuminate\Http\Request;
|
||||
|
|
@ -82,6 +83,35 @@ class BonusScoreDefinitionController extends Controller
|
|||
|
||||
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');
|
||||
}
|
||||
|
||||
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('/{bonusScore}', 'destroy')->name('admin.bonus-scores.destroy');
|
||||
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