Merge pull request #71 from okorpheus/auditionadmin-64
Auditionadmin 64 Closes #64 Closes #66
This commit is contained in:
commit
b7e26027ef
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
namespace App\Actions\Schools;
|
||||
|
||||
use App\Exceptions\AuditionAdminException;
|
||||
use App\Models\School;
|
||||
use App\Models\User;
|
||||
|
||||
use function auditionLog;
|
||||
use function is_null;
|
||||
|
||||
class SetHeadDirector
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public function __invoke(User $user, School $school): void
|
||||
{
|
||||
$this->setHeadDirector($user, $school);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws AuditionAdminException
|
||||
*/
|
||||
public function setHeadDirector(User $user): void
|
||||
{
|
||||
if (is_null($user->school_id)) {
|
||||
throw new AuditionAdminException('User is not associated with a school');
|
||||
}
|
||||
foreach ($user->school->directors as $director) {
|
||||
$director->removeFlag('head_director');
|
||||
}
|
||||
$user->addFlag('head_director');
|
||||
|
||||
$logMessage = 'Set '.$user->full_name().' as head director at '.$user->school->name;
|
||||
$logAffected = ['users' => [$user->id], 'schools' => [$user->school_id]];
|
||||
auditionLog($logMessage, $logAffected);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum UserFlags: string
|
||||
{
|
||||
case HEAD_DIRECTOR = 'head_director';
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class AuditionAdminException extends Exception
|
||||
{
|
||||
//
|
||||
}
|
||||
|
|
@ -2,10 +2,12 @@
|
|||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Actions\Schools\SetHeadDirector;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\AuditLogEntry;
|
||||
use App\Models\School;
|
||||
use App\Models\SchoolEmailDomain;
|
||||
use App\Models\User;
|
||||
use App\Services\Invoice\InvoiceDataService;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
|
|
@ -176,4 +178,14 @@ class SchoolController extends Controller
|
|||
|
||||
return view('dashboard.invoice', compact('school', 'invoiceData'));
|
||||
}
|
||||
|
||||
public function setHeadDirector(School $school, User $user, SetHeadDirector $headSetter)
|
||||
{
|
||||
if ($user->school_id !== $school->id) {
|
||||
return redirect()->back()->with('error', 'That user is not at that school');
|
||||
}
|
||||
$headSetter->setHeadDirector($user);
|
||||
|
||||
return redirect()->back()->with('success', 'Head director set');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Actions\Schools\SetHeadDirector;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Mail\NewUserPassword;
|
||||
use App\Models\AuditLogEntry;
|
||||
|
|
@ -13,6 +14,8 @@ use Illuminate\Support\Facades\Hash;
|
|||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
use function auditionLog;
|
||||
|
||||
class UserController extends Controller
|
||||
{
|
||||
public function index()
|
||||
|
|
@ -45,7 +48,7 @@ class UserController extends Controller
|
|||
return view('admin.users.create', ['schools' => $schools]);
|
||||
}
|
||||
|
||||
public function update(Request $request, User $user)
|
||||
public function update(Request $request, User $user, SetHeadDirector $headSetter)
|
||||
{
|
||||
if (! Auth::user()->is_admin) {
|
||||
abort(403);
|
||||
|
|
@ -63,6 +66,7 @@ class UserController extends Controller
|
|||
]);
|
||||
$validData['is_admin'] = $request->get('is_admin') == 'on' ? 1 : 0;
|
||||
$validData['is_tab'] = $request->get('is_tab') == 'on' ? 1 : 0;
|
||||
$validData['is_head'] = $request->get('is_head') == 'on' ? 1 : 0;
|
||||
$user->update([
|
||||
'first_name' => $validData['first_name'],
|
||||
'last_name' => $validData['last_name'],
|
||||
|
|
@ -76,11 +80,11 @@ class UserController extends Controller
|
|||
$user->refresh();
|
||||
$logged_school = $user->school_id ? $user->school->name : 'No School';
|
||||
$message = 'Updated user #'.$user->id.' - '.$oldEmail
|
||||
.'<br>Name: '.$user->full_name()
|
||||
.'<br>Email: '.$user->email
|
||||
.'<br>Cell Phone: '.$user->cell_phone
|
||||
.'<br>Judging Pref: '.$user->judging_preference
|
||||
.'<br>School: '.$logged_school;
|
||||
.'<br>Name: '.$user->full_name()
|
||||
.'<br>Email: '.$user->email
|
||||
.'<br>Cell Phone: '.$user->cell_phone
|
||||
.'<br>Judging Pref: '.$user->judging_preference
|
||||
.'<br>School: '.$logged_school;
|
||||
|
||||
AuditLogEntry::create([
|
||||
'user' => auth()->user()->email,
|
||||
|
|
@ -106,6 +110,16 @@ class UserController extends Controller
|
|||
'affected' => ['users' => [$user->id]],
|
||||
]);
|
||||
}
|
||||
if ($user->hasFlag('head_director') != $validData['is_head'] && ! is_null($user->school_id)) {
|
||||
if ($validData['is_head']) {
|
||||
$headSetter->setHeadDirector($user);
|
||||
} else {
|
||||
$user->removeFlag('head_director');
|
||||
$logMessage = 'Removed '.$user->full_name().' as head director at '.$user->school->name;
|
||||
$logAffected = ['users' => [$user->id], 'schools' => [$user->school_id]];
|
||||
auditionLog($logMessage, $logAffected);
|
||||
}
|
||||
}
|
||||
|
||||
return redirect('/admin/users');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,20 +2,29 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Actions\Schools\SetHeadDirector;
|
||||
use App\Exceptions\AuditionAdminException;
|
||||
use App\Mail\NewUserPassword;
|
||||
use App\Models\AuditLogEntry;
|
||||
use App\Models\School;
|
||||
use App\Models\SchoolEmailDomain;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\UniqueConstraintViolationException;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
use function abort;
|
||||
use function auditionLog;
|
||||
use function redirect;
|
||||
use function request;
|
||||
|
||||
class SchoolController extends Controller
|
||||
{
|
||||
public function store(Request $request): RedirectResponse
|
||||
public function store(Request $request, SetHeadDirector $headSetter): RedirectResponse
|
||||
{
|
||||
if ($request->user()->cannot('create', School::class)) {
|
||||
abort(403);
|
||||
|
|
@ -70,6 +79,13 @@ class SchoolController extends Controller
|
|||
'schools' => [$school->id],
|
||||
],
|
||||
]);
|
||||
auth()->user()->refresh();
|
||||
try {
|
||||
$headSetter->setHeadDirector(auth()->user());
|
||||
} catch (AuditionAdminException $e) {
|
||||
redirect(route('schools.show', $school))->with('error', 'Could not set as head director');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return redirect('/schools/'.$school->id);
|
||||
|
|
@ -141,4 +157,101 @@ class SchoolController extends Controller
|
|||
|
||||
return redirect('/schools/create');
|
||||
}
|
||||
|
||||
public function addDirector(School $school)
|
||||
{
|
||||
|
||||
if (auth()->user()->school_id !== $school->id) {
|
||||
return redirect()->back()->with('error', 'No adding directors to another school');
|
||||
}
|
||||
if (! auth()->user()->hasFlag('head_director')) {
|
||||
return redirect()->back()->with('error', 'Only the head director can add directors to a school');
|
||||
}
|
||||
$validData = request()->validate([
|
||||
'first_name' => ['required'],
|
||||
'last_name' => ['required'],
|
||||
'email' => ['required', 'email', 'unique:users'],
|
||||
'cell_phone' => ['required'],
|
||||
'judging_preference' => ['required'],
|
||||
]);
|
||||
// Generate a random password
|
||||
$randomPassword = Str::random(12);
|
||||
$newUser = User::create([
|
||||
'first_name' => $validData['first_name'],
|
||||
'last_name' => $validData['last_name'],
|
||||
'email' => $validData['email'],
|
||||
'cell_phone' => $validData['cell_phone'],
|
||||
'judging_preference' => $validData['judging_preference'],
|
||||
'password' => Hash::make($randomPassword),
|
||||
'school_id' => auth()->user()->school_id,
|
||||
]);
|
||||
$logMessage = 'Created user '.$newUser->full_name().' - '.$newUser->email.' as a director at '.$newUser->school->name;
|
||||
$logAffected = ['users' => [$newUser->id], 'schools' => [$newUser->school_id]];
|
||||
auditionLog($logMessage, $logAffected);
|
||||
Mail::to($newUser->email)->send(new NewUserPassword($newUser, $randomPassword));
|
||||
|
||||
return redirect()->back()->with('success', 'Director added');
|
||||
}
|
||||
|
||||
public function setHeadDirector(School $school, User $user, SetHeadDirector $headSetter)
|
||||
{
|
||||
if (auth()->user()->school_id !== $school->id) {
|
||||
return redirect()->back()->with('error', 'No setting the head director for another school');
|
||||
}
|
||||
if (! auth()->user()->hasFlag('head_director')) {
|
||||
return redirect()->back()->with('error', 'Only the head director can name a new head director');
|
||||
}
|
||||
if ($school->id !== $user->school_id) {
|
||||
return redirect()->back()->with('error', 'The proposed head director must be at your school');
|
||||
}
|
||||
try {
|
||||
$headSetter->setHeadDirector($user);
|
||||
} catch (AuditionAdminException $e) {
|
||||
return redirect()->back()->with('error', $e->getMessage());
|
||||
}
|
||||
|
||||
return redirect()->back()->with('success', 'New head director set');
|
||||
}
|
||||
|
||||
public function addDomain(School $school)
|
||||
{
|
||||
if (auth()->user()->school_id !== $school->id) {
|
||||
return redirect()->back()->with('error', 'No adding domains for another school');
|
||||
}
|
||||
if (! auth()->user()->hasFlag('head_director')) {
|
||||
return redirect()->back()->with('error', 'Only the head director can add domains');
|
||||
}
|
||||
$verifiedData = request()->validate([
|
||||
'domain' => ['required'],
|
||||
]);
|
||||
try {
|
||||
SchoolEmailDomain::create([
|
||||
'school_id' => $school->id,
|
||||
'domain' => $verifiedData['domain'],
|
||||
]);
|
||||
} catch (UniqueConstraintViolationException $e) {
|
||||
return redirect()->back()->with('error', 'That domain is already associated with your school');
|
||||
}
|
||||
$logMessage = 'Added domain '.$verifiedData['domain'].' to school '.$school->name;
|
||||
$logAffected = ['schools' => [$school->id]];
|
||||
auditionLog($logMessage, $logAffected);
|
||||
|
||||
return redirect()->back()->with('success', 'Domain added');
|
||||
}
|
||||
|
||||
public function deleteDomain(SchoolEmailDomain $domain)
|
||||
{
|
||||
if (auth()->user()->school_id !== $domain->school_id) {
|
||||
return redirect()->back()->with('error', 'No deleting domains for another school');
|
||||
}
|
||||
if (! auth()->user()->hasFlag('head_director')) {
|
||||
return redirect()->back()->with('error', 'Only the head director can delete domains');
|
||||
}
|
||||
$logMessage = 'Deleted domain '.$domain->domain.' from school '.$domain->school->name;
|
||||
$logAffected = ['schools' => [$domain->school_id]];
|
||||
auditionLog($logMessage, $logAffected);
|
||||
$domain->delete();
|
||||
|
||||
return redirect()->back()->with('success', 'Domain deleted');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\UserFlags;
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
|
@ -158,6 +159,41 @@ class User extends Authenticatable implements MustVerifyEmail
|
|||
return $this->hasMany(ScoreSheet::class);
|
||||
}
|
||||
|
||||
public function flags(): HasMany
|
||||
{
|
||||
return $this->hasMany(UserFlag::class);
|
||||
}
|
||||
|
||||
public function hasFlag($flag): bool
|
||||
{
|
||||
$flags = [];
|
||||
foreach ($this->flags as $checkFlag) {
|
||||
$flags[] = $checkFlag->flag_name->value;
|
||||
}
|
||||
|
||||
return in_array($flag, $flags);
|
||||
|
||||
}
|
||||
|
||||
public function addFlag($flag): void
|
||||
{
|
||||
if ($this->hasFlag($flag)) {
|
||||
return;
|
||||
}
|
||||
$enum = match ($flag) {
|
||||
'head_director' => UserFlags::HEAD_DIRECTOR,
|
||||
};
|
||||
$this->flags()->create(['flag_name' => $enum]);
|
||||
$this->load('flags');
|
||||
}
|
||||
|
||||
public function removeFlag($flag): void
|
||||
{
|
||||
// remove related userFlag where flag_name = $flag
|
||||
$this->flags()->where('flag_name', $flag)->delete();
|
||||
$this->load('flags');
|
||||
}
|
||||
|
||||
public function scoresForEntry($entry)
|
||||
{
|
||||
return $this->scoreSheets->where('entry_id', '=', $entry)->first()?->subscores;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\UserFlags;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class UserFlag extends Model
|
||||
{
|
||||
protected $guarded = [];
|
||||
|
||||
protected $casts = [
|
||||
'flag_name' => UserFlags::class,
|
||||
];
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,9 @@
|
|||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Actions\Entries\CreateEntry;
|
||||
use App\Actions\Entries\UpdateEntry;
|
||||
use App\Actions\Schools\SetHeadDirector;
|
||||
use App\Actions\Tabulation\AllowForOlympicScoring;
|
||||
use App\Actions\Tabulation\CalculateEntryScore;
|
||||
use App\Actions\Tabulation\CalculateScoreSheetTotal;
|
||||
|
|
@ -32,7 +35,6 @@ use App\Services\DoublerService;
|
|||
use App\Services\DrawService;
|
||||
use App\Services\EntryService;
|
||||
use App\Services\ScoreService;
|
||||
use App\Services\StudentService;
|
||||
use App\Services\UserService;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
|
@ -52,6 +54,10 @@ class AppServiceProvider extends ServiceProvider
|
|||
$this->app->singleton(ScoreService::class, ScoreService::class);
|
||||
$this->app->singleton(UserService::class, UserService::class);
|
||||
$this->app->singleton(DoublerService::class, DoublerService::class);
|
||||
$this->app->singleton(CreateEntry::class, CreateEntry::class);
|
||||
$this->app->singleton(UpdateEntry::class, UpdateEntry::class);
|
||||
$this->app->singleton(SetHeadDirector::class, SetHeadDirector::class);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
use App\Actions\Tabulation\EnterScore;
|
||||
use App\Exceptions\ScoreEntryException;
|
||||
use App\Models\AuditLogEntry;
|
||||
use App\Models\Entry;
|
||||
use App\Models\User;
|
||||
use App\Settings;
|
||||
|
|
@ -35,11 +36,22 @@ function auditionSetting($key)
|
|||
return Settings::get($key);
|
||||
}
|
||||
|
||||
function auditionLog(string $message, array $affected)
|
||||
{
|
||||
AuditLogEntry::create([
|
||||
'user' => auth()->user()->email ?? 'no user',
|
||||
'ip_address' => request()->ip(),
|
||||
'message' => $message,
|
||||
'affected' => $affected,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ScoreEntryException
|
||||
*/
|
||||
function enterScore(User $user, Entry $entry, array $scores): \App\Models\ScoreSheet
|
||||
{
|
||||
$scoreEntry = App::make(EnterScore::class);
|
||||
|
||||
return $scoreEntry($user, $entry, $scores);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
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('user_flags', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignIdFor(User::class)->constrained()->cascadeOnUpdate()->cascadeOnDelete();
|
||||
$table->string('flag_name');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('user_flags');
|
||||
}
|
||||
};
|
||||
|
|
@ -98,7 +98,7 @@
|
|||
@if($bonusScore->judges->contains($judge->id))
|
||||
@continue
|
||||
@endif
|
||||
<option value="{{ $judge->id }}">{{ $judge->full_name() }}
|
||||
<option value="{{ $judge->id }}">{{ $judge->full_name() }}@if($judge->hasFlag('head_director')) (H) @endif
|
||||
- {{ $judge->judging_preference }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@
|
|||
<option>Add a judge</option>
|
||||
<optgroup label="Unassigned Judges">
|
||||
@foreach($usersWithoutRooms as $judge) {{-- skip judges alrady assigned to this audition --}}
|
||||
<option value="{{ $judge->id }}">{{ $judge->full_name() }}
|
||||
<option value="{{ $judge->id }}">{{ $judge->full_name() }}@if($judge->hasFlag('head_director')) (H) @endif
|
||||
- {{ $judge->judging_preference }}</option>
|
||||
@endforeach
|
||||
</optgroup>
|
||||
|
|
@ -111,7 +111,7 @@
|
|||
@if($room->judges->contains($judge->id))
|
||||
@continue
|
||||
@endif
|
||||
<option value="{{ $judge->id }}">{{ $judge->full_name() }}
|
||||
<option value="{{ $judge->id }}">{{ $judge->full_name() }}@if($judge->hasFlag('head_director')) (H) @endif
|
||||
- {{ $judge->judging_preference }}</option>
|
||||
@endforeach
|
||||
</optgroup>
|
||||
|
|
|
|||
|
|
@ -37,7 +37,16 @@
|
|||
<div class="grid md:grid-cols-3 gap-3 mt-3">
|
||||
@foreach($school->directors as $director)
|
||||
<x-card.card>
|
||||
<x-card.heading>{{ $director->full_name() }}</x-card.heading>
|
||||
<x-card.heading>
|
||||
<a href="{{route('admin.users.edit',$director)}}">{{ $director->full_name() }}</a>
|
||||
@if($director->hasFlag('head_director'))
|
||||
<x-slot:right_side><x-badge_pill>Head Director</x-badge_pill></x-slot:right_side>
|
||||
@else
|
||||
<x-slot:subheading>
|
||||
<a href="{{route('admin.schools.set_head_director',['school'=>$school, 'user'=>$director])}}">[ Make Head Director ]</a>
|
||||
</x-slot:subheading>
|
||||
@endif
|
||||
</x-card.heading>
|
||||
<div class="ml-6">
|
||||
<p class="py-2">{{ $director->cell_phone }}</p>
|
||||
<p class="py-2">
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
<x-form.field name="cell_phone" label_text="Cell Phone" colspan="3" value="{{ $user->cell_phone }}"/>
|
||||
<x-form.field name="judging_preference" label_text="Judging Preference" colspan="6"
|
||||
value="{{ $user->judging_preference }}"/>
|
||||
<x-form.select name="school_id" colspan="6">
|
||||
<x-form.select name="school_id" colspan="4">
|
||||
<x-slot:label>School</x-slot:label>
|
||||
<option value="">No School</option>
|
||||
@foreach ($schools as $school)
|
||||
|
|
@ -31,6 +31,11 @@
|
|||
@endforeach
|
||||
|
||||
</x-form.select>
|
||||
<div class="col-span-2 pt-7">
|
||||
<x-form.checkbox name="is_head" :checked="$user->hasFlag('head_director')">
|
||||
<x-slot:label>Head Director</x-slot:label>
|
||||
</x-form.checkbox>
|
||||
</div>
|
||||
<div class="col-span-3">
|
||||
<x-form.checkbox name="is_admin" :checked="$user->is_admin">
|
||||
<x-slot:label>Administrator</x-slot:label>
|
||||
|
|
|
|||
|
|
@ -5,9 +5,10 @@
|
|||
</head>
|
||||
<body>
|
||||
<h1>Hello, {{ $user->first_name }} {{ $user->last_name }}</h1>
|
||||
<p>Your account has been created. Here are your login details:</p>
|
||||
<p>Your AuditionAdmin account for {{ auditionSetting('auditionAbbreviation') }} has been created. Here are your login details:</p>
|
||||
<p><strong>Email:</strong> {{ $user->email }}</p>
|
||||
<p><strong>Password:</strong> {{ $password }}</p>
|
||||
<p><strong>Login at: </strong> {{route('login')}}</p>
|
||||
<p>Please change your password after logging in for the first time.</p>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
</x-card.heading>
|
||||
|
||||
|
||||
<x-form.form method="POST" action="/schools" class="!mt-4">
|
||||
<x-form.form method="POST" action="{{route('schools.store')}}" class="!mt-4">
|
||||
<x-form.body-grid columns="6" class="max-w-full">
|
||||
<x-form.field name="name" label_text="School Name" colspan="6"/>
|
||||
<x-form.field name="address" label_text="School Address" colspan="6"/>
|
||||
|
|
|
|||
|
|
@ -1,39 +1,117 @@
|
|||
<x-layout.app>
|
||||
<x-slot:page_title>School Info - {{ $school->name }}</x-slot:page_title>
|
||||
<div x-data="{ showAddDirectorForm: false, changeHeadDirectorForm: false}">
|
||||
<x-layout.app>
|
||||
<x-slot:page_title>School Info - {{ $school->name }}</x-slot:page_title>
|
||||
|
||||
<div class="mx-auto max-w-xl">
|
||||
<x-card.card>
|
||||
<x-card.info.body>
|
||||
<x-card.info.row row_name="School Address">
|
||||
<div class="md:grid md:grid-cols-3">
|
||||
<div class="md:col-span-2">
|
||||
{{ $school->name }}<br />
|
||||
{{ $school->address }}<br />
|
||||
{{ $school->city }}, {{ $school->state }} {{ $school->zip }}
|
||||
<div class="mx-auto max-w-xl">
|
||||
<x-card.card>
|
||||
<x-card.info.body>
|
||||
<x-card.info.row row_name="School Address">
|
||||
<div class="md:grid md:grid-cols-3">
|
||||
<div class="md:col-span-2">
|
||||
{{ $school->name }}<br />
|
||||
{{ $school->address }}<br />
|
||||
{{ $school->city }}, {{ $school->state }} {{ $school->zip }}
|
||||
</div>
|
||||
<div class="text-indigo-600">
|
||||
<a href="/schools/{{$school->id}}/edit"> [ Edit School ] </a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-indigo-600">
|
||||
<a href="/schools/{{$school->id}}/edit"> [ Edit School ] </a>
|
||||
</x-card.info.row>
|
||||
|
||||
<x-card.info.row row_name="Directors">
|
||||
<ul>
|
||||
@foreach($school->directors as $director)
|
||||
<li>
|
||||
{{ $director->full_name() }}
|
||||
@if($director->hasFlag('head_director')) <span class="font-semibold">(head)</span> @endif
|
||||
-
|
||||
<a class='text-indigo-600' href="mailto:{{ $director->email }}">{{ $director->email }}</a>
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
@if(auth()->user()->hasFlag('head_director'))
|
||||
<div class="grid grid-cols-2 gap-2 mt-3">
|
||||
<x-form.button type="button" @click="showAddDirectorForm=true">Add Director</x-form.button>
|
||||
<x-form.button type="button" @click="changeHeadDirectorForm=true">Change Head</x-form.button>
|
||||
</div>
|
||||
@endif
|
||||
</x-card.info.row>
|
||||
@if(auth()->user()->hasFlag('head_director'))
|
||||
<x-card.info.row row_name="Associated Email Domains">
|
||||
<p class="text-sm text-gray-400 border-b">Users with emails in these domains (the part after the @) that don't already have a school
|
||||
will be able to join your school.</p>
|
||||
<ul>
|
||||
@foreach($school->emailDomains as $domain)
|
||||
<li class="flex my-2">
|
||||
<a href="{{route('schools.delete_domain',$domain)}}">
|
||||
<x-icons.circled-x color="red" />
|
||||
</a>
|
||||
{{ $domain->domain }}
|
||||
</li>
|
||||
@endforeach
|
||||
<li class="border-t">
|
||||
<p class="text-base">Add Domain</p>
|
||||
<x-form.form method="post" action="{{route('schools.add_domain',$school)}}" class="-ml-8">
|
||||
<div class="grid grid-cols-3">
|
||||
<x-form.field name="domain" label_text="Domain (do not include @)" colspan="2" />
|
||||
<x-form.button class="mt-6 ml-2">Add</x-form.button>
|
||||
</div>
|
||||
</x-form.form>
|
||||
</li>
|
||||
</ul>
|
||||
</x-card.info.row>
|
||||
@endif
|
||||
</x-card.info.body>
|
||||
</x-card.card>
|
||||
</div>
|
||||
|
||||
</x-layout.app>
|
||||
|
||||
@if(auth()->user()->hasFlag('head_director'))
|
||||
<x-modal-body showVar="showAddDirectorForm">
|
||||
<x-slot:title>Add Director</x-slot:title>
|
||||
<x-form.form method="POST" action="{{route('schools.add_director', $school)}}">
|
||||
<x-form.body-grid>
|
||||
<x-form.field name="first_name" label_text="First Name" colspan="3" />
|
||||
<x-form.field name="last_name" label_text="Last Name" colspan="3" />
|
||||
<x-form.field name="email" type="email" label_text="Email Address" colspan="3" />
|
||||
<x-form.field name="cell_phone" label_text="Cell Phone" colspan="3" />
|
||||
<x-form.field name="judging_preference" label_text="Judging Preference" colspan="6" />
|
||||
</x-form.body-grid>
|
||||
<x-form.footer submit-button-text="Add Director" />
|
||||
</x-form.form>
|
||||
</x-modal-body>
|
||||
|
||||
<x-modal-body showVar="changeHeadDirectorForm">
|
||||
<x-slot:title>Change Head Director</x-slot:title>
|
||||
{{-- Warming Message --}}
|
||||
<div class="rounded-md bg-red-50 p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-red-800">WARNING!!!</h3>
|
||||
<div class="mt-2 text-sm text-red-700">
|
||||
<p>After making another director head, you will no longer have access to head director functions</p>
|
||||
<p>This action cannot be undone</p>
|
||||
</div>
|
||||
</div>
|
||||
</x-card.info.row>
|
||||
|
||||
<x-card.info.row row_name="Directors">
|
||||
<ul>
|
||||
@foreach($school->directors as $director)
|
||||
<li>{{ $director->full_name() }} - <a class='text-indigo-600' href="mailto:{{ $director->email }}">{{ $director->email }}</a></li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</x-card.info.row>
|
||||
|
||||
<x-card.info.row row_name="Associated Email Domains">
|
||||
<ul>
|
||||
@foreach($school->emailDomains as $domain)
|
||||
<li>{{ $domain->domain }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</x-card.info.row>
|
||||
</x-card.info.body>
|
||||
</x-card.card>
|
||||
</div>
|
||||
|
||||
</x-layout.app>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-3 border-b border-t py-3">Which director should be the head director at {{$school->name}}? (click to set)</p>
|
||||
<x-card.list.body>
|
||||
@foreach($school->directors as $director)
|
||||
@continue($director->id === auth()->user()->id)
|
||||
<x-card.list.row>
|
||||
<a href="{{route('schools.set_head_director',['school'=>$school,'user'=>$director])}}">
|
||||
{{ $director->full_name() }} < {{$director->email }} >
|
||||
</a>
|
||||
</x-card.list.row>
|
||||
@endforeach
|
||||
</x-card.list.body>
|
||||
</x-modal-body>
|
||||
@endif
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -150,6 +150,7 @@ Route::middleware(['auth', 'verified', CheckIfAdmin::class])->prefix('admin/')->
|
|||
Route::post('/', 'store')->name('admin.schools.store');
|
||||
Route::delete('/domain/{domain}', 'destroy_domain')->name('admin.schools.destroy_domain');
|
||||
Route::delete('/{school}', 'destroy')->name('admin.schools.destroy');
|
||||
Route::get('/{school}/set_head_director/{user}', 'setHeadDirector')->name('admin.schools.set_head_director');
|
||||
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -52,6 +52,10 @@ Route::middleware(['auth', 'verified'])->controller(SchoolController::class)->gr
|
|||
Route::get('/schools/{school}/edit', 'edit')->name('schools.edit');
|
||||
Route::get('/schools/{school}', 'show')->name('schools.show');
|
||||
Route::patch('/schools/{school}', 'update')->name('schools.update');
|
||||
Route::post('schools/{school}/add_director', 'addDirector')->name('schools.add_director');
|
||||
Route::get('/schools/{school}/set_head_director/{user}', 'setHeadDirector')->name('schools.set_head_director');
|
||||
Route::post('/schools/{school}/add_domain', 'addDomain')->name('schools.add_domain');
|
||||
Route::get('/schools/delete_domain/{domain}', 'deleteDomain')->name('schools.delete_domain');
|
||||
});
|
||||
|
||||
// Doubler Related Routes
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
use App\Actions\Schools\SetHeadDirector;
|
||||
use App\Exceptions\AuditionAdminException;
|
||||
use App\Models\School;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->setter = app(SetHeadDirector::class);
|
||||
});
|
||||
|
||||
it('sets a head director flag for a user with a school', function () {
|
||||
// Arrange
|
||||
$school = School::factory()->create();
|
||||
$user = User::factory()->create(['school_id' => $school->id]);
|
||||
$this->setter->setHeadDirector($user);
|
||||
$this->assertDatabaseHas('user_flags', [
|
||||
'user_id' => $user->id,
|
||||
'flag_name' => 'head_director',
|
||||
]);
|
||||
});
|
||||
it('throws an error if the user has no school', function () {
|
||||
// Arrange
|
||||
$user = User::factory()->create();
|
||||
// Act & Assert
|
||||
$this->setter->setHeadDirector($user);
|
||||
})->throws(AuditionAdminException::class, 'User is not associated with a school');
|
||||
it('removes the head director flag from any other users as the school', function () {
|
||||
// Arrange
|
||||
$school = School::factory()->create();
|
||||
$oldHead = User::factory()->create(['school_id' => $school->id]);
|
||||
$newHead = User::factory()->create(['school_id' => $school->id]);
|
||||
$oldHead->addFlag('head_director');
|
||||
$this->assertDatabaseHas('user_flags', [
|
||||
'user_id' => $oldHead->id,
|
||||
'flag_name' => 'head_director',
|
||||
]);
|
||||
// Act
|
||||
$this->setter->setHeadDirector($newHead);
|
||||
$this->assertDatabaseHas('user_flags', [
|
||||
'user_id' => $newHead->id,
|
||||
'flag_name' => 'head_director',
|
||||
]);
|
||||
$this->assertDatabaseMissing('user_flags', [
|
||||
'user_id' => $oldHead->id,
|
||||
'flag_name' => 'head_director',
|
||||
]);
|
||||
});
|
||||
|
|
@ -7,6 +7,7 @@ use Illuminate\Foundation\Testing\RefreshDatabase;
|
|||
use function Pest\Laravel\actingAs;
|
||||
use function Pest\Laravel\assertDatabaseHas;
|
||||
use function Pest\Laravel\delete;
|
||||
use function Pest\Laravel\from;
|
||||
use function Pest\Laravel\get;
|
||||
use function Pest\Laravel\patch;
|
||||
|
||||
|
|
@ -150,7 +151,7 @@ it('allows an administrator to modify a user', function () {
|
|||
'school_id' => $newSchool->id,
|
||||
];
|
||||
// Act
|
||||
$response = patch(route('admin.users.update', $this->users[0]), $newData);
|
||||
$response = from(route('admin.users.index'))->patch(route('admin.users.update', $this->users[0]), $newData);
|
||||
/** @noinspection PhpUnhandledExceptionInspection */
|
||||
$response
|
||||
->assertSessionHasNoErrors()
|
||||
|
|
@ -186,7 +187,7 @@ it('allows a users school to be set to no school', function () {
|
|||
];
|
||||
actAsAdmin();
|
||||
// Act & Assert
|
||||
$response = patch(route('admin.users.update', $user), $newData);
|
||||
$response = from(route('admin.users.index'))->patch(route('admin.users.update', $user), $newData);
|
||||
/** @noinspection PhpUnhandledExceptionInspection */
|
||||
$response
|
||||
->assertSessionHasNoErrors()
|
||||
|
|
|
|||
Loading…
Reference in New Issue