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);
+});