diff --git a/app/Actions/YearEndProcedures/RecordHistoricalSeats.php b/app/Actions/YearEndProcedures/RecordHistoricalSeats.php new file mode 100644 index 0000000..08701f1 --- /dev/null +++ b/app/Actions/YearEndProcedures/RecordHistoricalSeats.php @@ -0,0 +1,45 @@ +saveSeats(); + } + + /** + * @throws AuditionAdminException + */ + public function saveSeats() + { + if (! auth()->user() or ! auth()->user()->is_admin) { + throw new AuditionAdminException('Only administrators may perform this action'); + } + $seats = Seat::all(); + if ($seats->count() > 0) { + foreach ($seats as $seat) { + $student_id = $seat->student->id; + $year = Carbon::now()->year; + $seat_description = $seat->ensemble->name.' - '.$seat->audition->name.' - '.$seat->seat; + HistoricalSeat::create([ + 'student_id' => $student_id, + 'year' => $year, + 'seat_description' => $seat_description, + ]); + } + } + + return true; + } +} diff --git a/app/Actions/YearEndProcedures/YearEndCleanup.php b/app/Actions/YearEndProcedures/YearEndCleanup.php new file mode 100644 index 0000000..933340b --- /dev/null +++ b/app/Actions/YearEndProcedures/YearEndCleanup.php @@ -0,0 +1,84 @@ +cleanup($options); + } + + /** + * @param $options array array of reset options - possible values are deleteRooms + * removeAuditionsFromRoom unassignJudges + * + * @throws AuditionAdminException + */ + public function cleanup(?array $options = []): true + { + + if (! auth()->user() or ! auth()->user()->is_admin) { + throw new AuditionAdminException('Only administrators may perform this action'); + } + + $historian = new RecordHistoricalSeats; + $historian(); + + // Delete all records in the audit_log_entries table + AuditLogEntry::truncate(); + AuditionFlag::truncate(); + BonusScore::truncate(); + CalculatedScore::truncate(); + DoublerRequest::truncate(); + EntryFlag::truncate(); + ScoreSheet::truncate(); + Seat::truncate(); + JudgeAdvancementVote::truncate(); + DB::table('entries')->delete(); + NominationEnsembleEntry::truncate(); + + Student::query()->increment('grade'); + + if (is_array($options)) { + if (in_array('deleteRooms', $options)) { + DB::table('auditions')->update(['room_id' => null]); + DB::table('auditions')->update(['order_in_room' => '0']); + DB::table('room_user')->truncate(); + DB::table('rooms')->delete(); + } + + if (in_array('removeAuditionsFromRoom', $options)) { + DB::table('auditions')->update(['room_id' => null]); + DB::table('auditions')->update(['order_in_room' => '0']); + } + + if (in_array('unassignJudges', $options)) { + DB::table('room_user')->truncate(); + } + } + + return true; + + } +} diff --git a/app/Http/Controllers/Admin/YearEndResetController.php b/app/Http/Controllers/Admin/YearEndResetController.php new file mode 100644 index 0000000..52d81d2 --- /dev/null +++ b/app/Http/Controllers/Admin/YearEndResetController.php @@ -0,0 +1,26 @@ +options; + $cleanUpProcedure($options); + auditionLog('Executed year end reset.', []); + + return redirect()->route('dashboard')->with('success', 'Year end reset completed'); + } +} diff --git a/app/Models/HistoricalSeat.php b/app/Models/HistoricalSeat.php new file mode 100644 index 0000000..7403cff --- /dev/null +++ b/app/Models/HistoricalSeat.php @@ -0,0 +1,19 @@ +belongsTo(Student::class); + } +} diff --git a/app/Models/Student.php b/app/Models/Student.php index 925fc29..2554aa2 100644 --- a/app/Models/Student.php +++ b/app/Models/Student.php @@ -45,6 +45,11 @@ class Student extends Model return $this->belongsTo(School::class); } + public function historicalSeats(): HasMany + { + return $this->hasMany(HistoricalSeat::class); + } + public function users(): HasManyThrough { return $this->hasManyThrough( diff --git a/database/migrations/2025_05_07_195828_create_historical_seats_table.php b/database/migrations/2025_05_07_195828_create_historical_seats_table.php new file mode 100644 index 0000000..8d52912 --- /dev/null +++ b/database/migrations/2025_05_07_195828_create_historical_seats_table.php @@ -0,0 +1,31 @@ +id(); + $table->timestamps(); + $table->foreignIdFor(Student::class)->constrained()->onDelete('cascade'); + $table->integer('year'); + $table->string('seat_description'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('historical_seats'); + } +}; diff --git a/resources/views/admin/year_end_reset.blade.php b/resources/views/admin/year_end_reset.blade.php new file mode 100644 index 0000000..47cf818 --- /dev/null +++ b/resources/views/admin/year_end_reset.blade.php @@ -0,0 +1,23 @@ + + Year End Reset + + Reset Options + + + + + + + Complete Year End Reset + + + Confirm Year End Reset + Confirm you would like to perform a year end reset. This will delete all seats, scores, entries, and log entries, + as well as any optional data you chose. It will also increment the grade of all students in the database. + This action will result in data loss and cannot be undone. + Confirm Reset + + + + + diff --git a/resources/views/components/layout/navbar/menus/admin.blade.php b/resources/views/components/layout/navbar/menus/admin.blade.php index cfae63a..00abdd5 100644 --- a/resources/views/components/layout/navbar/menus/admin.blade.php +++ b/resources/views/components/layout/navbar/menus/admin.blade.php @@ -32,6 +32,7 @@ Export Results Export Entries Print Stand Name Tags + Year End Reset diff --git a/routes/admin.php b/routes/admin.php index 13a4c17..bb55c10 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -21,6 +21,7 @@ use App\Http\Controllers\Admin\SchoolController; use App\Http\Controllers\Admin\ScoringGuideController; use App\Http\Controllers\Admin\StudentController; use App\Http\Controllers\Admin\UserController; +use App\Http\Controllers\Admin\YearEndResetController; use App\Http\Middleware\CheckIfAdmin; use Illuminate\Support\Facades\Route; @@ -33,6 +34,10 @@ Route::middleware(['auth', 'verified', CheckIfAdmin::class])->prefix('admin/')-> Route::get('/recap', [RecapController::class, 'selectAudition'])->name('admin.recap.selectAudition'); Route::get('/recap/{audition}', [RecapController::class, 'showRecap'])->name('admin.recap.recap'); + // Year end prodecures + Route::get('/year_end_procedures', [YearEndResetController::class, 'index'])->name('admin.year_end_procedures'); + Route::post('/year_end_procedures', [YearEndResetController::class, 'execute'])->name('admin.year_end_procedures'); + Route::post('/auditions/roomUpdate', [ AuditionController::class, 'roomUpdate', ]); // Endpoint for JS assigning auditions to rooms diff --git a/tests/Feature/Actions/RecordHistoricalSeatsTest.php b/tests/Feature/Actions/RecordHistoricalSeatsTest.php new file mode 100644 index 0000000..f1ab21c --- /dev/null +++ b/tests/Feature/Actions/RecordHistoricalSeatsTest.php @@ -0,0 +1,56 @@ + $action())->toThrow( + AuditionAdminException::class, + 'Only administrators may perform this action' + ); + + actAsNormal(); + expect(fn () => $action())->toThrow( + AuditionAdminException::class, + 'Only administrators may perform this action' + ); + + actAsAdmin(); + expect($action->saveSeats())->toBeTrue(); + +}); + +it('saves a seated student to the historical table', function () { + actAsAdmin(); + $entry = Entry::factory()->create(); + Entry::factory(5)->create(); + $action = new RecordHistoricalSeats(); + $ensemble = Ensemble::create([ + 'event_id' => $entry->audition->event_id, + 'name' => 'Test Ensemble', + 'code' => 'te', + 'rank' => 1, + ]); + $seat = Seat::create([ + 'ensemble_id' => $ensemble->id, + 'audition_id' => $entry->audition_id, + 'seat' => '1', + 'entry_id' => $entry->id, + ]); + $action->saveSeats(); + $historical_seats = HistoricalSeat::all(); + $test_seat = $historical_seats->first(); + expect($test_seat->student_id)->toBe($entry->student_id) + ->and($historical_seats)->toHaveCount(1) + ->and($test_seat->seat_description)->toBe($ensemble->name.' - '.$entry->audition->name.' - '.$seat->seat) + ->and(Student::count())->toBe(6); +});