diff --git a/Makefile b/Makefile index 5a004854..1773f2f1 100644 --- a/Makefile +++ b/Makefile @@ -57,6 +57,8 @@ fix: lintf: @docker compose --file ${DOCKER_COMPOSE_FILE} exec --user "${CURRENT_USER_ID}:${CURRENT_USER_GROUP_ID}" ${DOCKER_COMPOSE_APP_CONTAINER} bash -c 'npm run lintf' +fix-all: fix lintf + analyse: @docker compose --file ${DOCKER_COMPOSE_FILE} exec --user "${CURRENT_USER_ID}:${CURRENT_USER_GROUP_ID}" ${DOCKER_COMPOSE_APP_CONTAINER} bash -c 'composer analyse' @@ -69,4 +71,4 @@ queue: create-test-db: @docker compose --file ${DOCKER_COMPOSE_FILE} exec ${DOCKER_COMPOSE_DATABASE_CONTAINER} bash -c 'createdb --username=${DATABASE_USERNAME} ${TEST_DATABASE_NAME} &> /dev/null && echo "Created database for tests (${TEST_DATABASE_NAME})." || echo "Database for tests (${TEST_DATABASE_NAME}) exists."' -.PHONY: init check-env-file build run stop restart shell shell-root test fix create-test-db queue analyse dev +.PHONY: init check-env-file build run stop restart shell shell-root test test-specific fix lintf fix-all create-test-db queue analyse dev diff --git a/app/Http/Controllers/AdminController.php b/app/Http/Controllers/AdminController.php index 62712d62..10d15104 100644 --- a/app/Http/Controllers/AdminController.php +++ b/app/Http/Controllers/AdminController.php @@ -4,12 +4,14 @@ namespace App\Http\Controllers; -use App\Http\Requests\Auth\RegisterUserRequest; -use App\Http\Requests\UserRequest; +use App\Helpers\SortHelper; +use App\Http\Requests\AdminRequest; +use App\Http\Requests\Auth\RegisterAdminRequest; use App\Http\Resources\UserResource; use App\Models\School; use App\Models\User; use Illuminate\Auth\Events\Registered; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Http\RedirectResponse; use Illuminate\Support\Facades\Hash; use Inertia\Inertia; @@ -17,27 +19,28 @@ class AdminController extends Controller { - public function index(): Response + public function index(SortHelper $sorter): Response { - $users = User::query()->role("admin")->with("school")->orderBy("id")->get(); + $users = User::query()->role("admin")->with("school"); + + $query = $sorter->sort($users, ["id", "email", "updated_at", "created_at"], ["firstname"]); + $query = $this->sortByName($query, $sorter); + $query = $sorter->search($query, "firstname"); return Inertia::render("Admin/AdminsPanel", [ - "users" => UserResource::collection($users), + "users" => UserResource::collection($query->paginate()), ]); } - public function create(): Response - { - return Inertia::render("Admin/CreateAdmin"); - } - - public function store(RegisterUserRequest $request): RedirectResponse + public function store(RegisterAdminRequest $request): RedirectResponse { + $school = School::query()->where(["is_admin_school" => true])->firstOrFail(); $userExists = User::query()->where("email", $request->email)->exists(); if (!$userExists) { $user = new User($request->validated()); $user->password = Hash::make($request->password); + $user->school()->associate($school); $user->save(); $user->syncRoles("admin"); event(new Registered($user)); @@ -48,22 +51,22 @@ public function store(RegisterUserRequest $request): RedirectResponse ->with("success", "Administrator został utworzony pomyślnie."); } - public function edit(User $user): Response + public function update(AdminRequest $request, User $user): RedirectResponse { $this->authorize("update", $user); + $data = $request->validated(); - return Inertia::render("Admin/EditAdmin", [ - "user" => new UserResource($user), - "schools" => School::all(), - ]); - } + if (!$request->has("password")) { + unset($data["password"]); + } - public function update(UserRequest $request, User $user): RedirectResponse - { - $this->authorize("update", $user); - $data = $request->validated(); $user->update($data); + if ($request->has("password")) { + $user->password = Hash::make($request->validated("password")); + $user->save(); + } + return redirect() ->route("admin.admins.index") ->with("success", "Administrator zaktualizowany pomyślnie."); @@ -76,4 +79,16 @@ public function destroy(User $user): RedirectResponse return redirect()->back(); } + + private function sortByName(Builder $query, SortHelper $sorter): Builder + { + [$field, $order] = $sorter->getSortParameters(); + + if ($field === "firstname") { + return $query->orderBy("firstname", $order) + ->orderBy("surname", $order); + } + + return $query; + } } diff --git a/app/Http/Controllers/InviteController.php b/app/Http/Controllers/InviteController.php index f9e8687e..ef56036d 100644 --- a/app/Http/Controllers/InviteController.php +++ b/app/Http/Controllers/InviteController.php @@ -101,8 +101,11 @@ private function sortBySchool(Builder $query, SortHelper $sorter): Builder { [$field, $order] = $sorter->getSortParameters(); - if ($field === "schoolId") { - return $query->orderBy("school.name", $order); + if ($field === "school") { + return $query + ->leftJoin("schools", "users.school_id", "=", "schools.id") + ->orderBy("schools.name", $order) + ->select("users.*"); } return $query; diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index 2a0f672e..dae145ed 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -4,9 +4,13 @@ namespace App\Http\Controllers; +use App\Helpers\SortHelper; use App\Http\Requests\UserRequest; +use App\Http\Resources\SchoolResource; use App\Http\Resources\UserResource; +use App\Models\School; use App\Models\User; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Inertia\Inertia; @@ -14,12 +18,23 @@ class UserController extends Controller { - public function index(Request $request): Response + public function index(SortHelper $sorter, Request $request): Response { - $users = User::query()->role("user")->with("school")->orderBy("id")->get(); + $users = User::query()->role("user")->with("school"); + $schools = School::query() + ->where("is_admin_school", false) + ->orderBy("name") + ->get(); + + $query = $sorter->sort($users, ["id", "email", "updated_at", "created_at"], ["firstname", "school"]); + $query = $this->filterAnonymizedUsers($query, $request); + $query = $this->sortBySchool($query, $sorter); + $query = $this->sortByName($query, $sorter); + $query = $sorter->search($query, "firstname"); return Inertia::render("Admin/UsersPanel", [ - "users" => UserResource::collection($users), + "users" => UserResource::collection($query->paginate()), + "schools" => SchoolResource::collection($schools), ]); } @@ -31,7 +46,7 @@ public function update(UserRequest $request, User $user): RedirectResponse return redirect() ->route("admin.users.index") - ->with("success", "Użytkownik zaktualizowany pomyślnie."); + ->with("status", "Użytkownik zaktualizowany pomyślnie."); } public function anonymize(User $user): RedirectResponse @@ -46,6 +61,43 @@ public function anonymize(User $user): RedirectResponse return redirect() ->back() - ->with("success", "Dane użytkownika zostały zanonimizowane."); + ->with("status", "Dane użytkownika zostały zanonimizowane."); + } + + private function sortByName(Builder $query, SortHelper $sorter): Builder + { + [$field, $order] = $sorter->getSortParameters(); + + if ($field === "firstname") { + return $query->orderBy("firstname", $order) + ->orderBy("surname", $order); + } + + return $query; + } + + private function sortBySchool(Builder $query, SortHelper $sorter): Builder + { + [$field, $order] = $sorter->getSortParameters(); + + if ($field === "school") { + return $query->join("schools", "users.school_id", "=", "schools.id") + ->orderBy("schools.city", $order) + ->orderBy("schools.name", $order) + ->select("users.*"); + } + + return $query; + } + + private function filterAnonymizedUsers(Builder $query, Request $request): Builder + { + $showAnonymized = $request->query("anonymized", "false") === "true"; + + if (!$showAnonymized) { + return $query->where("is_anonymized", false); + } + + return $query; } } diff --git a/app/Http/Requests/AdminRequest.php b/app/Http/Requests/AdminRequest.php new file mode 100644 index 00000000..34d28edb --- /dev/null +++ b/app/Http/Requests/AdminRequest.php @@ -0,0 +1,25 @@ + ["required", "string", "email", "max:255", "unique:users,email," . $this->user->id], + "firstname" => ["required", "string", "max:255"], + "surname" => ["required", "string", "max:255"], + "password" => ["nullable", "string", "min:8", "max:255"], + ]; + } +} diff --git a/app/Http/Requests/Auth/AuthenticateSessionRequest.php b/app/Http/Requests/Auth/AuthenticateSessionRequest.php index caadae17..af1ae8ee 100644 --- a/app/Http/Requests/Auth/AuthenticateSessionRequest.php +++ b/app/Http/Requests/Auth/AuthenticateSessionRequest.php @@ -17,7 +17,7 @@ public function rules(): array { return [ "email" => ["required", "email", "max:255"], - "password" => ["required", "string"], + "password" => ["required", "string", "min:8", "max:255"], ]; } } diff --git a/app/Http/Requests/Auth/RegisterAdminRequest.php b/app/Http/Requests/Auth/RegisterAdminRequest.php new file mode 100644 index 00000000..50d83e9b --- /dev/null +++ b/app/Http/Requests/Auth/RegisterAdminRequest.php @@ -0,0 +1,25 @@ + ["required", "string", "email:rfc,dns", "max:255"], + "firstname" => ["required", "string", "max:255"], + "surname" => ["required", "string", "max:255"], + "password" => ["required", "string", "min:8", "max:255"], + ]; + } +} diff --git a/app/Http/Requests/Auth/RegisterUserRequest.php b/app/Http/Requests/Auth/RegisterUserRequest.php index fe5a6fed..38da8675 100644 --- a/app/Http/Requests/Auth/RegisterUserRequest.php +++ b/app/Http/Requests/Auth/RegisterUserRequest.php @@ -20,7 +20,7 @@ public function rules(): array "email" => ["required", "string", "email:rfc,dns", "max:255"], "firstname" => ["required", "string", "max:255"], "surname" => ["required", "string", "max:255"], - "password" => ["required", "string", "min:8"], + "password" => ["required", "string", "min:8", "max:255"], "school_id" => ["required", "integer", "exists:schools,id", new IsSchoolValidForRegularUsers()], ]; } diff --git a/app/Http/Requests/Auth/ResetPasswordRequest.php b/app/Http/Requests/Auth/ResetPasswordRequest.php index 603d409a..a9081f97 100644 --- a/app/Http/Requests/Auth/ResetPasswordRequest.php +++ b/app/Http/Requests/Auth/ResetPasswordRequest.php @@ -18,7 +18,7 @@ public function rules(): array return [ "token" => ["required"], "email" => ["required", "email", "max:255"], - "password" => ["required", "min:8", "confirmed:password_confirmation"], + "password" => ["required", "min:8", "max:255", "confirmed:password_confirmation"], ]; } } diff --git a/app/Http/Resources/UserResource.php b/app/Http/Resources/UserResource.php index 10bde42c..ae230c78 100644 --- a/app/Http/Resources/UserResource.php +++ b/app/Http/Resources/UserResource.php @@ -23,6 +23,8 @@ public function toArray(Request $request): array "isAnonymized" => $this->is_anonymized, "isAdmin" => $this->hasRole("admin"), "isSuperAdmin" => $this->hasRole("super_admin"), + "createdAt" => $this->created_at, + "updatedAt" => $this->updated_at, ]; } } diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index 15ac6060..3c056c85 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -11,6 +11,8 @@ use Illuminate\Support\Facades\Hash; use Illuminate\Support\Str; +use function fake; + /** * @extends Factory */ @@ -58,4 +60,16 @@ public function unverifiedUser(): static ]; }); } + + public function anonymized(): static + { + return $this->state(function (array $attributes) { + return [ + "firstname" => "Anonymous", + "surname" => "User", + "email" => "anonymous" . fake()->unique()->randomNumber() . "@email", + "is_anonymized" => true, + ]; + }); + } } diff --git a/resources/js/Helpers/vDynamicInputWidth.ts b/resources/js/Helpers/vDynamicInputWidth.ts index 7a292fe4..3a867210 100644 --- a/resources/js/Helpers/vDynamicInputWidth.ts +++ b/resources/js/Helpers/vDynamicInputWidth.ts @@ -10,12 +10,20 @@ function initDynamicWidthCalc(input:HTMLInputElement & { _calculateDynamicWidth: } function calculateDynamicWidth(input:HTMLInputElement, binding?:DirectiveBinding){ - if (binding?.value === false) return + if (binding?.value === false) { + return + } + const context = canvas.getContext('2d') - if (!context) return + + if (!context) { + return + } + const { fontWeight, fontSize, fontFamily } = window.getComputedStyle(input) context.font = `${fontWeight} ${fontSize} ${fontFamily}` - const width = `${context.measureText(input.value).width}px` + const width = `${context.measureText(input.value || input.placeholder).width}px` + input.style.width = `clamp(1.1rem,${width},100%)` } diff --git a/resources/js/Pages/Admin/AdminsPanel.vue b/resources/js/Pages/Admin/AdminsPanel.vue index 44e07cb0..d1223379 100644 --- a/resources/js/Pages/Admin/AdminsPanel.vue +++ b/resources/js/Pages/Admin/AdminsPanel.vue @@ -1,3 +1,123 @@ + + diff --git a/resources/js/Pages/Admin/SchoolsPanel.vue b/resources/js/Pages/Admin/SchoolsPanel.vue index 5f26eb61..2c1fb0bd 100644 --- a/resources/js/Pages/Admin/SchoolsPanel.vue +++ b/resources/js/Pages/Admin/SchoolsPanel.vue @@ -115,7 +115,6 @@ function customQueries(): string[] { new-button-text="Dodaj szkołę" :new-item-data="{ name: 'Nowa szkoła', regon: '', apartmentNumber: '', street: '', buildingNumber: '', city: '', numberOfStudents: 0, zipCode: '' }" display-search-in-lower-case - mobile-nav creatable >