diff --git a/.env.ci b/.env.ci index c590d6f..849585b 100644 --- a/.env.ci +++ b/.env.ci @@ -21,4 +21,3 @@ DB_PORT=5432 DB_DATABASE=keating DB_USERNAME=keating DB_PASSWORD=password -DB_ROOT_PASSWORD=example diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 359124d..689e297 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @blumilksoftware/blumilk +* @blumilksoftware/keating diff --git a/app/Actions/ActivateSemesterAction.php b/app/Actions/ActivateSemesterAction.php new file mode 100644 index 0000000..aca7191 --- /dev/null +++ b/app/Actions/ActivateSemesterAction.php @@ -0,0 +1,35 @@ +db->beginTransaction(); + + Semester::getActive()?->update(["active" => 0]); + $semester->update(["active" => 1]); + + $this->db->commit(); + } catch (Exception $exception) { + $this->db->rollBack(); + + throw $exception; + } + } +} diff --git a/app/Http/Controllers/Dashboard/SemesterController.php b/app/Http/Controllers/Dashboard/SemesterController.php new file mode 100644 index 0000000..744af63 --- /dev/null +++ b/app/Http/Controllers/Dashboard/SemesterController.php @@ -0,0 +1,76 @@ +orderByDesc("created_at")->get(); + + return inertia("Dashboard/Semester/Index", [ + "semesters" => $semesters, + ]); + } + + public function create(): Response + { + return inertia("Dashboard/Semester/Create"); + } + + public function store(SemesterRequest $request): RedirectResponse + { + Semester::query()->create($request->validated()); + + return redirect() + ->route("semesters.index") + ->with("success", "Dodano semestr"); + } + + public function edit(Semester $semester): Response + { + return inertia("Dashboard/Semester/Edit", [ + "semester" => $semester, + ]); + } + + public function update(SemesterRequest $request, Semester $semester): RedirectResponse + { + $semester->update($request->validated()); + + return redirect() + ->route("semesters.index") + ->with("success", "Zaktualizowano semestr"); + } + + public function destroy(Semester $semester): RedirectResponse + { + $semester->delete(); + + return redirect()->back() + ->with("success", "Usunięto semestr"); + } + + public function toggleActive(Semester $semester, ActivateSemesterAction $activateSemesterAction): RedirectResponse + { + try { + $activateSemesterAction->execute($semester); + + return redirect()->back() + ->with("success", "Semestr aktywny"); + } catch (Exception $e) { + return redirect()->back() + ->with("error", "Wystąpił nieoczekiwany problem"); + } + } +} diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php index dd38acd..492c246 100644 --- a/app/Http/Middleware/HandleInertiaRequests.php +++ b/app/Http/Middleware/HandleInertiaRequests.php @@ -14,7 +14,7 @@ public function share(Request $request): array { return array_merge(parent::share($request), [ "auth" => $this->getAuthData($request), - "flash" => $this->getFlashedData($request), + "flash" => $this->getFlashData($request), ]); } @@ -25,7 +25,7 @@ protected function getAuthData(Request $request): array ]; } - protected function getFlashedData(Request $request): Closure + protected function getFlashData(Request $request): Closure { return fn(): array => [ "success" => $request->session()->get("success"), diff --git a/app/Http/Requests/SemesterRequest.php b/app/Http/Requests/SemesterRequest.php new file mode 100644 index 0000000..9848c9f --- /dev/null +++ b/app/Http/Requests/SemesterRequest.php @@ -0,0 +1,17 @@ + ["required", "max:255"], + ]; + } +} diff --git a/app/Models/Semester.php b/app/Models/Semester.php new file mode 100644 index 0000000..c8521e6 --- /dev/null +++ b/app/Models/Semester.php @@ -0,0 +1,45 @@ + "boolean", + ]; + + public function scopeActive(Builder $query): Builder + { + return $query->where("active", true); + } + + /** + * @return ?Semester + */ + public static function getActive(): ?Model + { + return self::active()->first(); + } +} diff --git a/app/Models/Student.php b/app/Models/Student.php index 004e4f3..a40f402 100644 --- a/app/Models/Student.php +++ b/app/Models/Student.php @@ -4,10 +4,19 @@ namespace App\Models; +use Carbon\Carbon; use Illuminate\Database\Eloquent\Concerns\HasUlids; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +/** + * @property string $id + * @property string $name + * @property string $surname + * @property string $index_number + * @property Carbon $created_at + * @property Carbon $updated_at + */ class Student extends Model { use HasFactory; diff --git a/database/factories/SemesterFactory.php b/database/factories/SemesterFactory.php new file mode 100644 index 0000000..c5c3312 --- /dev/null +++ b/database/factories/SemesterFactory.php @@ -0,0 +1,18 @@ + "Semestr" . fake()->numberBetween(1, 7), + "active" => fake()->boolean, + ]; + } +} diff --git a/database/migrations/2023_09_23_075139_create_semesters_table.php b/database/migrations/2023_09_23_075139_create_semesters_table.php new file mode 100644 index 0000000..db46174 --- /dev/null +++ b/database/migrations/2023_09_23_075139_create_semesters_table.php @@ -0,0 +1,24 @@ +ulid("id")->primary(); + $table->string("name"); + $table->boolean("active")->default(0); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists("semesters"); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 3b04c72..0ff7edb 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -11,6 +11,6 @@ class DatabaseSeeder extends Seeder { public function run(): void { - User::factory(1)->create(); + User::factory()->create(["email" => "admin@example.com"]); } } diff --git a/resources/js/Layouts/DashboardLayout.vue b/resources/js/Layouts/DashboardLayout.vue index 8eabce1..d34a0ea 100644 --- a/resources/js/Layouts/DashboardLayout.vue +++ b/resources/js/Layouts/DashboardLayout.vue @@ -1,9 +1,7 @@ + + diff --git a/resources/js/Pages/Dashboard/Semester/Edit.vue b/resources/js/Pages/Dashboard/Semester/Edit.vue new file mode 100644 index 0000000..b9da67a --- /dev/null +++ b/resources/js/Pages/Dashboard/Semester/Edit.vue @@ -0,0 +1,52 @@ + + + diff --git a/resources/js/Pages/Dashboard/Semester/Index.vue b/resources/js/Pages/Dashboard/Semester/Index.vue new file mode 100644 index 0000000..7691989 --- /dev/null +++ b/resources/js/Pages/Dashboard/Semester/Index.vue @@ -0,0 +1,73 @@ + + + diff --git a/resources/js/Pages/Dashboard/Student/Index.vue b/resources/js/Pages/Dashboard/Student/Index.vue index f7cd1bd..f295928 100644 --- a/resources/js/Pages/Dashboard/Student/Index.vue +++ b/resources/js/Pages/Dashboard/Student/Index.vue @@ -15,6 +15,7 @@ import TextInput from '@/Shared/Forms/TextInput.vue' import { useForm } from '@inertiajs/inertia-vue3' import { Cog6ToothIcon, XCircleIcon } from '@heroicons/vue/24/outline' import ManagementHeader from '../../../Shared/Components/ManagementHeader.vue' +import SecondaryButton from '../../../Shared/Components/Buttons/SecondaryButton.vue' const props = defineProps({ students: Object, @@ -111,7 +112,11 @@ watch(form, debounce(() => { - + + + Dodaj studenta + + diff --git a/resources/js/Shared/Components/EmptyState.vue b/resources/js/Shared/Components/EmptyState.vue index 5675cae..17c047b 100644 --- a/resources/js/Shared/Components/EmptyState.vue +++ b/resources/js/Shared/Components/EmptyState.vue @@ -6,11 +6,11 @@ defineProps({ diff --git a/resources/js/app.js b/resources/js/app.js index abef616..74330a7 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -18,7 +18,7 @@ createInertiaApp({ return createApp({ render: () => h(App, props) }) .use(plugin) .use(Toast, { - position: 'bottom-right', + position: 'top-right', maxToast: 5, timeout: 3000, pauseOnFocusLoss: false, diff --git a/routes/web.php b/routes/web.php index b5ce5bb..0d68f05 100644 --- a/routes/web.php +++ b/routes/web.php @@ -5,6 +5,7 @@ use App\Http\Controllers\Dashboard\DashboardController; use App\Http\Controllers\Dashboard\LogoutController; use App\Http\Controllers\Dashboard\PasswordUpdateController; +use App\Http\Controllers\Dashboard\SemesterController; use App\Http\Controllers\Dashboard\StudentController; use App\Http\Controllers\Public\HomeController; use App\Http\Controllers\Public\LoginController; @@ -32,4 +33,13 @@ Route::patch("/students/{student}", "update")->name("students.update"); Route::delete("/students/{student}", "destroy")->name("students.destroy"); }); + Route::controller(SemesterController::class)->group(function (): void { + Route::get("/semesters", "index")->name("semesters.index"); + Route::get("/semesters/create", "create")->name("semesters.create"); + Route::post("/semesters", "store")->name("semesters.store"); + Route::get("/semesters/{semester}/edit", "edit")->name("semesters.edit"); + Route::patch("/semesters/{semester}", "update")->name("semesters.update"); + Route::delete("/semesters/{semester}", "destroy")->name("semesters.destroy"); + Route::post("/semesters/{semester}/activate", "toggleActive")->name("semesters.toggle.active"); + }); }); diff --git a/tests/Feature/SemesterTest.php b/tests/Feature/SemesterTest.php new file mode 100644 index 0000000..93a38e9 --- /dev/null +++ b/tests/Feature/SemesterTest.php @@ -0,0 +1,99 @@ +user = User::factory()->create(); + $this->actingAs($this->user); + } + + public function testSemesterCanBeCreated(): void + { + $this->assertDatabaseCount("semesters", 0); + + $this->post("/dashboard/semesters", [ + "name" => "Semester 1", + ])->assertSessionHasNoErrors(); + + $this->assertDatabaseCount("semesters", 1); + } + + public function testSemesterCanBeUpdated(): void + { + $semester = Semester::factory()->create(); + + $this->assertDatabaseMissing("semesters", [ + "name" => "Semester 1", + ]); + + $this->patch("/dashboard/semesters/{$semester->id}", [ + "name" => "Semester 1", + ])->assertSessionHasNoErrors(); + + $this->assertDatabaseHas("semesters", [ + "name" => "Semester 1", + ]); + } + + public function testSemesterCannotBeCreatedWithInvalidData(): void + { + $this->post("/dashboard/semesters", [ + "name" => Str::random(256), + ])->assertSessionHasErrors([ + "name", + ]); + + $this->assertDatabaseCount("semesters", 0); + } + + public function testSemesterCanBeDeleted(): void + { + $semester = Semester::factory()->create(); + $this->assertDatabaseCount("semesters", 1); + + $this->delete("/dashboard/semesters/{$semester->id}"); + + $this->assertDatabaseCount("semesters", 0); + } + + public function testSemesterCanBeSetToActiveStatus(): void + { + $semester = Semester::factory()->create(["active" => 0]); + $this->assertFalse($semester->active); + + $this->post("/dashboard/semesters/{$semester->id}/activate"); + + $semester->refresh(); + $this->assertTrue($semester->active); + } + + public function testOnlyOneSemesterCanBeActive(): void + { + $inactiveSemester = Semester::factory()->create(["active" => 0]); + $activeSemester = Semester::factory()->create(["active" => 1]); + $this->assertFalse($inactiveSemester->active); + $this->assertTrue($activeSemester->active); + + $this->post("/dashboard/semesters/{$inactiveSemester->id}/activate"); + + $inactiveSemester->refresh(); + $activeSemester->refresh(); + $this->assertTrue($inactiveSemester->active); + $this->assertFalse($activeSemester->active); + } +} diff --git a/tests/Feature/StudentTest.php b/tests/Feature/StudentTest.php index 7514b8c..c606ad4 100644 --- a/tests/Feature/StudentTest.php +++ b/tests/Feature/StudentTest.php @@ -7,7 +7,6 @@ use App\Models\Student; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; -use Illuminate\Support\Facades\Hash; use Illuminate\Support\Str; use Tests\TestCase; @@ -15,29 +14,23 @@ class StudentTest extends TestCase { use RefreshDatabase; - protected User $user; - protected function setUp(): void { parent::setUp(); - $this->user = User::factory([ - "email" => "test@example.com", - "password" => Hash::make("password"), - ])->create(); + $this->user = User::factory()->create(); + $this->actingAs($this->user); } public function testStudentCanBeCreated(): void { $this->assertDatabaseCount("students", 0); - $this - ->actingAs($this->user) - ->post("/dashboard/students", [ - "name" => "Name", - "surname" => "Surname", - "index_number" => "12345", - ])->assertSessionHasNoErrors(); + $this->post("/dashboard/students", [ + "name" => "Name", + "surname" => "Surname", + "index_number" => "12345", + ])->assertSessionHasNoErrors(); $this->assertDatabaseCount("students", 1); } @@ -52,13 +45,11 @@ public function testStudentCanBeUpdated(): void "index_number" => "12345", ]); - $this - ->actingAs($this->user) - ->patch("/dashboard/students/{$student->id}", [ - "name" => "Name", - "surname" => "Surname", - "index_number" => "12345", - ])->assertSessionHasNoErrors(); + $this->patch("/dashboard/students/{$student->id}", [ + "name" => "Name", + "surname" => "Surname", + "index_number" => "12345", + ])->assertSessionHasNoErrors(); $this->assertDatabaseHas("students", [ "name" => "Name", @@ -72,30 +63,26 @@ public function testStudentCannotBeCreatedWithBusyIndex(): void Student::factory()->create(["index_number" => "12345"]); $this->assertDatabaseCount("students", 1); - $this - ->actingAs($this->user) - ->post("/dashboard/students", [ - "name" => "Name", - "surname" => "Surname", - "index_number" => "12345", - ])->assertSessionHasErrors("index_number"); + $this->post("/dashboard/students", [ + "name" => "Name", + "surname" => "Surname", + "index_number" => "12345", + ])->assertSessionHasErrors("index_number"); $this->assertDatabaseCount("students", 1); } public function testStudentCannotBeCreatedWithInvalidData(): void { - $this - ->actingAs($this->user) - ->post("/dashboard/students", [ - "name" => Str::random(256), - "surname" => Str::random(256), - "index_number" => Str::random(256), - ])->assertSessionHasErrors([ - "name", - "surname", - "index_number", - ]); + $this->post("/dashboard/students", [ + "name" => Str::random(256), + "surname" => Str::random(256), + "index_number" => Str::random(256), + ])->assertSessionHasErrors([ + "name", + "surname", + "index_number", + ]); $this->assertDatabaseCount("students", 0); } @@ -105,9 +92,7 @@ public function testStudentCanBeDeleted(): void $student = Student::factory()->create(); $this->assertDatabaseCount("students", 1); - $this - ->actingAs($this->user) - ->delete("/dashboard/students/{$student->id}"); + $this->delete("/dashboard/students/{$student->id}"); $this->assertDatabaseCount("students", 0); }