diff --git a/app/Domain/EquipmentExport.php b/app/Domain/EquipmentExport.php new file mode 100644 index 00000000..01966703 --- /dev/null +++ b/app/Domain/EquipmentExport.php @@ -0,0 +1,103 @@ +equipmentItems as $equipmentItem) { + $row = [ + $equipmentItem->id_number, + $equipmentItem->name, + $equipmentItem->labels?->implode(", "), + $equipmentItem->is_mobile, + $equipmentItem->assignee->profile->full_name ?? "", + $equipmentItem->assigned_at ? Date::dateTimeToExcel($equipmentItem->assigned_at) : "", + ]; + + yield $row; + } + } + + public function headings(): array + { + return [ + __("ID"), + __("Name"), + __("Labels"), + __("Is mobile"), + __("Assignee"), + __("Assigned at"), + ]; + } + + public function columnFormats(): array + { + return [ + "A" => NumberFormat::FORMAT_TEXT, + "B" => NumberFormat::FORMAT_TEXT, + "C" => NumberFormat::FORMAT_TEXT, + "D" => DataType::TYPE_BOOL, + "E" => NumberFormat::FORMAT_TEXT, + "F" => NumberFormat::FORMAT_DATE_DDMMYYYY, + ]; + } + + public function styles(Worksheet $sheet): void + { + $lastRow = $sheet->getHighestRow(); + $lastColumn = $sheet->getHighestColumn(); + + $sheet->getStyle("A1:{$lastColumn}1") + ->getFont() + ->setBold(true); + + $sheet->getStyle("A1:{$lastColumn}1") + ->getAlignment() + ->setVertical(Alignment::VERTICAL_CENTER); + + $sheet->getStyle("A1:{$lastColumn}1") + ->getFill() + ->setFillType(Fill::FILL_SOLID) + ->getStartColor() + ->setRGB("D9D9D9"); + + $sheet->getStyle("A2:A{$lastRow}") + ->getFont() + ->setBold(true); + + $sheet->getStyle("A1:{$lastColumn}{$lastRow}") + ->getBorders() + ->getAllBorders() + ->setBorderStyle(Border::BORDER_THIN) + ->getColor() + ->setRGB("B7B7B7"); + } +} diff --git a/app/Eloquent/Models/EquipmentItem.php b/app/Eloquent/Models/EquipmentItem.php new file mode 100644 index 00000000..62abf9a8 --- /dev/null +++ b/app/Eloquent/Models/EquipmentItem.php @@ -0,0 +1,94 @@ + "date", + "labels" => AsCollection::class, + ]; + protected $fillable = [ + "id_number", + "name", + "is_mobile", + "labels", + "assignee_id", + "assigned_at", + ]; + + public function scopeSearch(Builder $query, ?string $text): Builder + { + if ($text === null) { + return $query; + } + + return $query + ->where("id_number", "ILIKE", "%{$text}%") + ->orWhere("name", "ILIKE", "%{$text}%") + ->orWhere("labels", "ILIKE", "%{$text}%") + ->orWhereRelation( + "assignee", + fn(Builder $query): Builder => $query + ->where("email", "ILIKE", "%{$text}%") + ->orWhereRelation( + "profile", + fn(Builder $query): Builder => $query + ->where("first_name", "ILIKE", "%{$text}%") + ->orWhere("last_name", "ILIKE", "%{$text}%") + ->orWhere(DB::raw("CONCAT(first_name, ' ', last_name)"), "ILIKE", "%{$text}%"), + ), + ); + } + + public function scopeLabels(Builder $query, ?array $labels): Builder + { + if ($labels === null) { + return $query; + } + + $query->where(function (Builder $query) use ($labels): Builder { + foreach ($labels as $label) { + $query->orWhereJsonContains("labels", $label); + } + + return $query; + }); + + return $query; + } + + public function assignee(): BelongsTo + { + return $this->belongsTo(User::class, "assignee_id"); + } + + protected static function newFactory(): EquipmentItemFactory + { + return EquipmentItemFactory::new(); + } +} diff --git a/app/Eloquent/Models/EquipmentLabel.php b/app/Eloquent/Models/EquipmentLabel.php new file mode 100644 index 00000000..c073daa9 --- /dev/null +++ b/app/Eloquent/Models/EquipmentLabel.php @@ -0,0 +1,35 @@ +belongsToMany(EquipmentItem::class, "equipment_items_labels"); + } + + protected static function newFactory(): EquipmentLabelFactory + { + return EquipmentLabelFactory::new(); + } +} diff --git a/app/Eloquent/Models/User.php b/app/Eloquent/Models/User.php index 0d403f30..3a92ff54 100644 --- a/app/Eloquent/Models/User.php +++ b/app/Eloquent/Models/User.php @@ -14,6 +14,7 @@ use Illuminate\Notifications\Notifiable; use Illuminate\Support\Carbon; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\DB; use Spatie\Permission\Traits\HasRoles; use Toby\Domain\Enums\EmploymentForm; use Toby\Domain\Enums\Role; @@ -102,7 +103,8 @@ public function scopeSearch(Builder $query, ?string $text): Builder "profile", fn(Builder $query): Builder => $query ->where("first_name", "ILIKE", "%{$text}%") - ->orWhere("last_name", "ILIKE", "%{$text}%"), + ->orWhere("last_name", "ILIKE", "%{$text}%") + ->orWhere(DB::raw("CONCAT(first_name, ' ', last_name)"), "ILIKE", "%{$text}%"), ); } diff --git a/app/Infrastructure/Http/Controllers/EquipmentController.php b/app/Infrastructure/Http/Controllers/EquipmentController.php new file mode 100644 index 00000000..9487b4fe --- /dev/null +++ b/app/Infrastructure/Http/Controllers/EquipmentController.php @@ -0,0 +1,183 @@ +user()->cannot("manageEquipment")) { + return redirect()->route("equipment-items.indexForEmployee"); + } + + $searchQuery = $request->query("search"); + + $equipmentItems = EquipmentItem::query() + ->search($searchQuery) + ->when( + $request->query("assignee") && $request->query("assignee") !== "unassigned", + fn($query) => $query->where("assignee_id", $request->query("assignee")), + ) + ->when( + $request->query("assignee") === "unassigned", + fn($query) => $query->where("assignee_id", null), + ) + ->labels($request->query("labels")) + ->orderBy("id_number") + ->paginate() + ->withQueryString(); + + $users = User::query() + ->orderByProfileField("last_name") + ->orderByProfileField("first_name") + ->get(); + + return inertia("Equipment/Index", [ + "equipmentItems" => EquipmentItemResource::collection($equipmentItems), + "labels" => EquipmentLabel::query()->pluck("name"), + "users" => SimpleUserResource::collection($users), + "filters" => [ + "search" => $searchQuery, + "labels" => $request->query("labels"), + "assignee" => $request->query("assignee"), + ], + ]); + } + + /** + * @throws AuthorizationException + */ + public function indexForEmployee(Request $request): RedirectResponse|Response + { + $searchQuery = $request->query("search"); + + $equipmentItems = EquipmentItem::query() + ->search($searchQuery) + ->labels($request->query("labels")) + ->where("assignee_id", $request->user()->id) + ->orderBy("id_number") + ->paginate() + ->withQueryString(); + + return inertia("Equipment/IndexForEmployee", [ + "equipmentItems" => EquipmentItemResource::collection($equipmentItems), + "labels" => EquipmentLabel::query()->pluck("name"), + "filters" => [ + "search" => $searchQuery, + "labels" => $request->query("labels"), + ], + ]); + } + + /** + * @throws AuthorizationException + */ + public function create(): Response + { + $this->authorize("manageEquipment"); + + $users = User::query() + ->orderByProfileField("last_name") + ->orderByProfileField("first_name") + ->get(); + + return inertia("Equipment/Create", [ + "users" => SimpleUserResource::collection($users), + "labels" => EquipmentLabel::query()->pluck("name"), + ]); + } + + /** + * @throws AuthorizationException + */ + public function store( + EquipmentRequest $request, + ): RedirectResponse { + $this->authorize("manageEquipment"); + + EquipmentItem::query()->create($request->data()); + + return redirect() + ->route("equipment-items.index") + ->with("success", __("Equipment item created.")); + } + + /** + * @throws AuthorizationException + */ + public function edit(EquipmentItem $equipmentItem): Response + { + $this->authorize("manageEquipment"); + + $users = User::query() + ->orderByProfileField("last_name") + ->orderByProfileField("first_name") + ->get(); + + return inertia("Equipment/Edit", [ + "equipmentItem" => new EquipmentItemResource($equipmentItem), + "users" => SimpleUserResource::collection($users), + "labels" => EquipmentLabel::query()->pluck("name"), + ]); + } + + /** + * @throws AuthorizationException + */ + public function update( + EquipmentRequest $request, + EquipmentItem $equipmentItem, + ): RedirectResponse { + $this->authorize("manageUsers"); + + $equipmentItem->update($request->data()); + + return redirect() + ->route("equipment-items.index") + ->with("success", __("Equipment item updated.")); + } + + public function destroy(EquipmentItem $equipmentItem): RedirectResponse + { + $this->authorize("manageEquipment"); + + $equipmentItem->delete(); + + return redirect() + ->route("equipment-items.index") + ->with("success", __("Equipment item deleted.")); + } + + public function downloadExcel(): BinaryFileResponse + { + $this->authorize("manageEquipment"); + + $equipmentItems = EquipmentItem::query()->get(); + + $equipmentExport = new EquipmentExport($equipmentItems); + + $name = __("Equipment") . " " . Carbon::now()->translatedFormat("d F Y") . ".xlsx"; + + return Excel::download($equipmentExport, $name); + } +} diff --git a/app/Infrastructure/Http/Controllers/EquipmentLabelController.php b/app/Infrastructure/Http/Controllers/EquipmentLabelController.php new file mode 100644 index 00000000..1d951501 --- /dev/null +++ b/app/Infrastructure/Http/Controllers/EquipmentLabelController.php @@ -0,0 +1,57 @@ +authorize("manageEquipment"); + + $labels = EquipmentLabel::query() + ->orderBy("name") + ->get(); + + return inertia("EquipmentLabels", [ + "labels" => EquipmentLabelResource::collection($labels), + ]); + } + + /** + * @throws AuthorizationException + */ + public function store(EquipmentLabelRequest $request): RedirectResponse + { + $this->authorize("manageEquipment"); + + $label = EquipmentLabel::query()->create($request->data()); + + return redirect() + ->back() + ->with("success", __("Label :name created.", [ + "name" => $label->name, + ])); + } + + public function destroy(EquipmentLabel $equipmentLabel): RedirectResponse + { + $this->authorize("manageEquipment"); + + $equipmentLabel->delete(); + + return redirect() + ->back() + ->with("success", __("Label :name deleted.", [ + "name" => $equipmentLabel->name, + ])); + } +} diff --git a/app/Infrastructure/Http/Requests/EquipmentLabelRequest.php b/app/Infrastructure/Http/Requests/EquipmentLabelRequest.php new file mode 100644 index 00000000..b1cb4355 --- /dev/null +++ b/app/Infrastructure/Http/Requests/EquipmentLabelRequest.php @@ -0,0 +1,29 @@ + [ + "required", + Rule::unique("equipment_labels", "name")->ignore($this->equipment_label), + "max:255", + ], + ]; + } + + public function data(): array + { + return [ + "name" => $this->get("name"), + ]; + } +} diff --git a/app/Infrastructure/Http/Requests/EquipmentRequest.php b/app/Infrastructure/Http/Requests/EquipmentRequest.php new file mode 100644 index 00000000..c064637f --- /dev/null +++ b/app/Infrastructure/Http/Requests/EquipmentRequest.php @@ -0,0 +1,35 @@ + ["required", "min:3", "max:80", Rule::unique("equipment_items", "id_number")->ignore($this->equipment_item)], + "name" => ["required", "min:3", "max:80"], + "isMobile" => ["required", "boolean"], + "assignee" => ["required_with:assignedAt", "nullable", "exists:users,id"], + "assignedAt" => ["required_with:assignee", "nullable", "date_format:Y-m-d"], + "labels" => ["array", "distinct"], + ]; + } + + public function data(): array + { + return [ + "id_number" => $this->get("idNumber"), + "name" => $this->get("name"), + "is_mobile" => $this->get("isMobile"), + "assignee_id" => $this->get("assignee"), + "assigned_at" => $this->get("assignedAt"), + "labels" => $this->get("labels"), + ]; + } +} diff --git a/app/Infrastructure/Http/Resources/EquipmentItemResource.php b/app/Infrastructure/Http/Resources/EquipmentItemResource.php new file mode 100644 index 00000000..2581aa42 --- /dev/null +++ b/app/Infrastructure/Http/Resources/EquipmentItemResource.php @@ -0,0 +1,26 @@ + $this->id, + "idNumber" => $this->id_number, + "name" => $this->name, + "isMobile" => $this->is_mobile, + "assignee" => new SimpleUserResource($this->assignee), + "labels" => $this->labels, + "assignedAt" => $this->assigned_at?->toDateString(), + "displayAssignedAt" => $this->assigned_at?->toDisplayString(), + ]; + } +} diff --git a/app/Infrastructure/Http/Resources/EquipmentLabelResource.php b/app/Infrastructure/Http/Resources/EquipmentLabelResource.php new file mode 100644 index 00000000..a160b0c9 --- /dev/null +++ b/app/Infrastructure/Http/Resources/EquipmentLabelResource.php @@ -0,0 +1,20 @@ + $this->id, + "name" => $this->name, + ]; + } +} diff --git a/config/permission.php b/config/permission.php index 0420acb2..a3f4faa3 100644 --- a/config/permission.php +++ b/config/permission.php @@ -53,6 +53,7 @@ "receiveVacationRequestWaitsForApprovalNotification", "receiveVacationRequestStatusChangedNotification", "receiveBenefitsReportCreationNotification", + "manageEquipment", ], "permission_roles" => [ Role::Administrator->value => [ @@ -73,6 +74,7 @@ "receiveVacationRequestsSummaryNotification", "receiveVacationRequestWaitsForApprovalNotification", "receiveVacationRequestStatusChangedNotification", + "manageEquipment", ], Role::AdministrativeApprover->value => [ "managePermissions", @@ -94,6 +96,7 @@ "receiveVacationRequestWaitsForApprovalNotification", "receiveVacationRequestStatusChangedNotification", "receiveBenefitsReportCreationNotification", + "manageEquipment", ], Role::TechnicalApprover->value => [ "manageTechnologies", diff --git a/database/factories/EquipmentItemFactory.php b/database/factories/EquipmentItemFactory.php new file mode 100644 index 00000000..c8ed4e1d --- /dev/null +++ b/database/factories/EquipmentItemFactory.php @@ -0,0 +1,30 @@ +faker->boolean; + + return [ + "id_number" => $this->faker->numerify("ABC#########"), + "name" => $this->faker->word, + "assignee_id" => $isAssigned ? User::factory() : null, + "assigned_at" => $isAssigned ? $this->faker->dateTimeBetween("-1 year") : null, + "is_mobile" => $this->faker->boolean, + "labels" => [ + $this->faker->word, $this->faker->word, $this->faker->word, + ], + ]; + } +} diff --git a/database/factories/EquipmentLabelFactory.php b/database/factories/EquipmentLabelFactory.php new file mode 100644 index 00000000..e90aa8ec --- /dev/null +++ b/database/factories/EquipmentLabelFactory.php @@ -0,0 +1,20 @@ + $this->faker->word, + ]; + } +} diff --git a/database/migrations/2023_09_26_140056_create_equipment_items_table.php b/database/migrations/2023_09_26_140056_create_equipment_items_table.php new file mode 100644 index 00000000..74119295 --- /dev/null +++ b/database/migrations/2023_09_26_140056_create_equipment_items_table.php @@ -0,0 +1,29 @@ +id(); + $table->string("id_number")->unique(); + $table->string("name"); + $table->boolean("is_mobile")->default(false); + $table->foreignIdFor(User::class, "assignee_id")->nullable()->constrained("users")->cascadeOnDelete(); + $table->date("assigned_at")->nullable(); + $table->json("labels")->nullable(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists("equipment_items"); + } +}; diff --git a/database/migrations/2023_09_26_140126_create_equipment_labels_table.php b/database/migrations/2023_09_26_140126_create_equipment_labels_table.php new file mode 100644 index 00000000..2302ddda --- /dev/null +++ b/database/migrations/2023_09_26_140126_create_equipment_labels_table.php @@ -0,0 +1,23 @@ +id(); + $table->string("name"); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists("equipment_labels"); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index e59c21b8..f182e303 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -10,6 +10,8 @@ use Toby\Domain\WorkDaysCalculator; use Toby\Eloquent\Models\Benefit; use Toby\Eloquent\Models\BenefitsReport; +use Toby\Eloquent\Models\EquipmentItem; +use Toby\Eloquent\Models\EquipmentLabel; use Toby\Eloquent\Models\Key; use Toby\Eloquent\Models\User; use Toby\Eloquent\Models\VacationLimit; @@ -106,5 +108,9 @@ public function run(): void ]); BenefitsReport::factory(3)->create(); + + EquipmentLabel::factory(10)->create(); + + EquipmentItem::factory(40)->create(); } } diff --git a/database/seeders/DemoSeeder.php b/database/seeders/DemoSeeder.php index d436c66e..fa8fa849 100644 --- a/database/seeders/DemoSeeder.php +++ b/database/seeders/DemoSeeder.php @@ -21,6 +21,8 @@ use Toby\Domain\WorkDaysCalculator; use Toby\Eloquent\Models\Benefit; use Toby\Eloquent\Models\BenefitsReport; +use Toby\Eloquent\Models\EquipmentItem; +use Toby\Eloquent\Models\EquipmentLabel; use Toby\Eloquent\Models\Key; use Toby\Eloquent\Models\Resume; use Toby\Eloquent\Models\Technology; @@ -395,5 +397,75 @@ public function run(): void "data" => null, "committed_at" => null, ]); + + EquipmentLabel::factory()->createMany([ + ["name" => "Komputery"], + ["name" => "Telefony"], + ["name" => "Urządzenia peryferyjne"], + ["name" => "Monitory"], + ["name" => "Telewizory"], + ["name" => "Wyposażenie biura"], + ["name" => "Inne"], + ]); + + User::query()->each(function (User $user): void { + /** @var EquipmentItem $computer */ + $computer = EquipmentItem::factory([ + "name" => "Laptop Dell Latitude 7400", + "assigned_at" => fake()->dateTimeBetween("-1 year"), + "is_mobile" => true, + "labels" => [ + "Komputery", + ], + ])->for($user, "assignee")->create(); + + EquipmentItem::factory([ + "name" => "Monitor Philips 2" . fake()->numberBetween(1, 8) . '"', + "assigned_at" => $computer->assigned_at, + "is_mobile" => false, + "labels" => [ + "Monitory", + "Urządzenia peryferyjne", + ], + ])->for($user, "assignee")->create(); + + EquipmentItem::factory([ + "name" => "Myszka Logitech MX" . fake()->numerify(), + "assigned_at" => $computer->assigned_at, + "is_mobile" => true, + "labels" => [ + "Urządzenia peryferyjne", + ], + ])->for($user, "assignee")->create(); + + EquipmentItem::factory([ + "name" => "Klawiatura Dell " . fake()->numerify("#####"), + "assigned_at" => $computer->assigned_at, + "is_mobile" => true, + "labels" => [ + "Urządzenia peryferyjne", + ], + ])->for($user, "assignee")->create(); + + EquipmentItem::factory([ + "name" => "Hub USB", + "assigned_at" => $computer->assigned_at, + "is_mobile" => true, + "labels" => [ + "Urządzenia peryferyjne", + ], + ])->for($user, "assignee")->create(); + }); + + EquipmentItem::factory([ + "name" => 'Telewizor TCN 55" 4K', + "is_mobile" => false, + "assigned_at" => null, + "assignee_id" => null, + "labels" => [ + "Telewizory", + "Wyposażenie biura", + ], + ])->create(); } } diff --git a/lang/pl.json b/lang/pl.json index fe9d788e..d8165892 100644 --- a/lang/pl.json +++ b/lang/pl.json @@ -168,5 +168,16 @@ "waiting_for_administrative": "czeka na akceptację od przełożonego administracyjnego", "You do not have permission to perform this action.": "Nie masz uprawnień, aby wykonać tę akcję.", "waiting_for_technical": "czeka na akceptację od przełożonego technicznego", - "You cannot perform this action because the current status of the request :title by user :requester is :status.": "Nie możesz wykonać tej akcji, ponieważ aktualny status wniosku :title użytkownika :requester to :status" + "You cannot perform this action because the current status of the request :title by user :requester is :status.": "Nie możesz wykonać tej akcji, ponieważ aktualny status wniosku :title użytkownika :requester to :status", + "Equipment item created.": "Sprzęt utworzony.", + "Equipment item updated.": "Sprzęt zaktualizowany.", + "Equipment item deleted.": "Sprzęt usunięty.", + "Label :name created.": "Etykieta :name utworzona.", + "Label :name deleted.": "Etykieta :name usunięta.", + "Equipment": "Sprzęt", + "Name": "Nazwa", + "Labels": "Etykiety", + "Is mobile": "Czy mobilny", + "Assignee": "Przypisany do", + "Assigned at": "Przypisany od" } diff --git a/lang/pl/validation.php b/lang/pl/validation.php index e52bfc17..afb575da 100644 --- a/lang/pl/validation.php +++ b/lang/pl/validation.php @@ -158,8 +158,18 @@ "projects.*.tasks" => [ "required" => "Zadania w projekcie są wymagane.", ], + "idNumber" => [ + "required" => "ID jest wymagane.", + ], + "assignee" => [ + "required_with" => "Przydzielona osoba jest wymagana.", + ], + "assignedAt" => [ + "required_with" => "Data przydzielenia jest wymagana.", + ], "name" => [ "unique" => "Taka nazwa już występuje.", + "required" => "Nazwa jest wymagana.", ], "items.*.days" => [ "max" => "Limit dni urlopu nie może być większy niż :max.", @@ -182,5 +192,9 @@ "date" => "data", "name" => "nazwa", "password" => "hasło", + "idNumber" => "ID", + "isMobile" => "mobilny", + "assignee" => "przydzielona osoba", + "assignedAt" => "data przydzielenia", ], ]; diff --git a/resources/js/Composables/permissionInfo.js b/resources/js/Composables/permissionInfo.js index a02c0921..ca0f1a21 100644 --- a/resources/js/Composables/permissionInfo.js +++ b/resources/js/Composables/permissionInfo.js @@ -19,6 +19,11 @@ const permissionsInfo = [ 'value': 'manageKeys', 'section': 'Biuro', }, + { + 'name': 'Zarządzanie sprzętem', + 'value': 'manageEquipment', + 'section': 'Biuro', + }, { 'name': 'Zarządzanie technologiami', 'value': 'manageTechnologies', diff --git a/resources/js/Pages/Equipment/Create.vue b/resources/js/Pages/Equipment/Create.vue new file mode 100644 index 00000000..587fcc07 --- /dev/null +++ b/resources/js/Pages/Equipment/Create.vue @@ -0,0 +1,272 @@ + + + diff --git a/resources/js/Pages/Equipment/Edit.vue b/resources/js/Pages/Equipment/Edit.vue new file mode 100644 index 00000000..5bb761c2 --- /dev/null +++ b/resources/js/Pages/Equipment/Edit.vue @@ -0,0 +1,273 @@ + + + diff --git a/resources/js/Pages/Equipment/Index.vue b/resources/js/Pages/Equipment/Index.vue new file mode 100644 index 00000000..296912e0 --- /dev/null +++ b/resources/js/Pages/Equipment/Index.vue @@ -0,0 +1,410 @@ + + + diff --git a/resources/js/Pages/Equipment/IndexForEmployee.vue b/resources/js/Pages/Equipment/IndexForEmployee.vue new file mode 100644 index 00000000..3668501d --- /dev/null +++ b/resources/js/Pages/Equipment/IndexForEmployee.vue @@ -0,0 +1,163 @@ + + + diff --git a/resources/js/Pages/EquipmentLabels.vue b/resources/js/Pages/EquipmentLabels.vue new file mode 100644 index 00000000..07d5f0fe --- /dev/null +++ b/resources/js/Pages/EquipmentLabels.vue @@ -0,0 +1,231 @@ + + + diff --git a/resources/js/Pages/Resumes/Create.vue b/resources/js/Pages/Resumes/Create.vue index 6807d713..e99ac1cc 100644 --- a/resources/js/Pages/Resumes/Create.vue +++ b/resources/js/Pages/Resumes/Create.vue @@ -531,6 +531,7 @@ function submitResume() {

- {{ form.errors.type }} + {{ form.errors.user }}

diff --git a/resources/js/Shared/Forms/MultipleCombobox.vue b/resources/js/Shared/Forms/MultipleCombobox.vue index f683dce1..d69c71a7 100644 --- a/resources/js/Shared/Forms/MultipleCombobox.vue +++ b/resources/js/Shared/Forms/MultipleCombobox.vue @@ -13,6 +13,8 @@ const props = defineProps({ items: Array, modelValue: Array, id: String, + placeholder: String, + showChips: Boolean, }) const emit = defineEmits(['update:modelValue']) @@ -41,7 +43,10 @@ const filteredItems = computed(() => nullable multiple > -
+
-
+
@@ -79,7 +85,6 @@ const filteredItems = computed(() => + +
  • + + Brak wyników wyszukiwania + +
  • +
    diff --git a/resources/js/Shared/MainMenu.vue b/resources/js/Shared/MainMenu.vue index 6b6a51c1..3e081c99 100644 --- a/resources/js/Shared/MainMenu.vue +++ b/resources/js/Shared/MainMenu.vue @@ -27,6 +27,7 @@ import { BeakerIcon, GiftIcon, BanknotesIcon, + ComputerDesktopIcon, DocumentDuplicateIcon, } from '@heroicons/vue/24/outline' import { CheckIcon, ChevronDownIcon } from '@heroicons/vue/24/solid' @@ -143,6 +144,20 @@ const miscNavigation = computed(() => [ icon: DocumentDuplicateIcon, can: props.auth.can.manageBenefits, }, + { + name: 'Sprzęt', + href: '/equipment-items', + section: 'Equipment', + icon: ComputerDesktopIcon, + can: props.auth.can.manageEquipment, + }, + { + name: 'Mój sprzęt', + href: '/equipment-items/me', + section: 'Equipment', + icon: ComputerDesktopIcon, + can: !props.auth.can.manageEquipment, + }, ].filter(item => item.can)) diff --git a/routes/web.php b/routes/web.php index 6dd4ab64..09f3479d 100644 --- a/routes/web.php +++ b/routes/web.php @@ -8,6 +8,8 @@ use Toby\Infrastructure\Http\Controllers\BenefitController; use Toby\Infrastructure\Http\Controllers\BenefitsReportController; use Toby\Infrastructure\Http\Controllers\DashboardController; +use Toby\Infrastructure\Http\Controllers\EquipmentController; +use Toby\Infrastructure\Http\Controllers\EquipmentLabelController; use Toby\Infrastructure\Http\Controllers\GoogleController; use Toby\Infrastructure\Http\Controllers\HolidayController; use Toby\Infrastructure\Http\Controllers\KeysController; @@ -39,6 +41,18 @@ ->whereNumber("user") ->withTrashed(); + Route::resource("equipment-items", EquipmentController::class) + ->except("show") + ->whereNumber("equipmentItem"); + Route::get("/equipment-items/me", [EquipmentController::class, "indexForEmployee"]) + ->name("equipment-items.indexForEmployee"); + Route::get("/equipment-items/download", [EquipmentController::class, "downloadExcel"]) + ->name("equipment-items.download"); + + Route::resource("equipment-labels", EquipmentLabelController::class) + ->only(["index", "store", "destroy"]) + ->whereNumber("equipmentLabels"); + Route::get("/users/{user}/permissions", [PermissionController::class, "show"]) ->whereNumber("user"); Route::patch("/users/{user}/permissions", [PermissionController::class, "update"]) diff --git a/tests/Feature/EquipmentTest.php b/tests/Feature/EquipmentTest.php new file mode 100644 index 00000000..ccdb5927 --- /dev/null +++ b/tests/Feature/EquipmentTest.php @@ -0,0 +1,289 @@ +count(10)->create(); + $admin = User::factory()->admin()->create(); + + $this->assertDatabaseCount("equipment_items", 10); + + $this->actingAs($admin) + ->get("/equipment-items") + ->assertOk() + ->assertInertia( + fn(Assert $page) => $page + ->component("Equipment/Index") + ->has("equipmentItems.data", 10), + ); + } + + public function testEmployeeCannotSeeEquipmentList(): void + { + $employee = User::factory()->employee()->create(); + + $this->actingAs($employee) + ->get("/equipment-items") + ->assertRedirect("/equipment-items/me"); + } + + public function testAnyUserWithProperPermissionCanSeeEquipmentList(): void + { + $user = User::factory()->create(); + $user->givePermissionTo("manageEquipment"); + + $this->actingAs($user) + ->get("/equipment-items") + ->assertOk() + ->assertInertia( + fn(Assert $page) => $page->component("Equipment/Index"), + ); + } + + public function testAdminCanSearchEquipmentList(): void + { + $admin = User::factory()->admin()->create(); + + EquipmentItem::factory() + ->count(4) + ->sequence( + [ + "name" => "Test1", + ], + [ + "name" => "Test2", + ], + [ + "name" => "Test3", + ], + [ + "name" => "Item1", + ], + ) + ->create(); + + $this->assertDatabaseCount("equipment_items", 4); + + $this->actingAs($admin) + ->get("/equipment-items?search=test") + ->assertOk() + ->assertInertia( + fn(Assert $page) => $page + ->component("Equipment/Index") + ->has("equipmentItems.data", 3), + ); + } + + public function testAdminCanFilterEquipmentListWithLabels(): void + { + $admin = User::factory()->admin()->create(); + + EquipmentItem::factory() + ->count(4) + ->sequence( + [ + "labels" => [ + "Test1", + "Test2", + ], + ], + [ + "labels" => [ + "Test1", + ], + ], + [ + "labels" => [ + "Test2", + ], + ], + [ + "labels" => [ + "Test3", + ], + ], + ) + ->create(); + + $this->assertDatabaseCount("equipment_items", 4); + + $this->actingAs($admin) + ->get("/equipment-items?labels[]=Test1&labels[]=Test2") + ->assertOk() + ->assertInertia( + fn(Assert $page) => $page + ->component("Equipment/Index") + ->has("equipmentItems.data", 3), + ); + } + + public function testEquipmentListIsPaginated(): void + { + EquipmentItem::factory()->count(16)->create(); + $admin = User::factory()->admin()->create(); + + $this->assertDatabaseCount("equipment_items", 16); + + $this->actingAs($admin) + ->get("/equipment-items?page=2") + ->assertInertia( + fn(Assert $page) => $page + ->component("Equipment/Index") + ->has("equipmentItems.data", 1), + ); + } + + public function testAdminCanCreateEquipmentItem(): void + { + $admin = User::factory()->admin()->create(); + $assignee = User::factory()->create(); + + $this->actingAs($admin) + ->post("/equipment-items", [ + "idNumber" => "123", + "name" => "Test", + "isMobile" => true, + "assignee" => $assignee->id, + "assignedAt" => "2023-01-01", + "labels" => ["Test1", "Test2"], + ]) + ->assertSessionHasNoErrors(); + + $equipmentItem = EquipmentItem::query()->where("id_number", "123")->first(); + + $this->assertDatabaseHas("equipment_items", [ + "id_number" => "123", + "name" => "Test", + "is_mobile" => true, + "assignee_id" => $assignee->id, + "assigned_at" => "2023-01-01", + ]); + + $this->assertEquals(["Test1", "Test2"], $equipmentItem->labels->toArray()); + } + + public function testAdminCanEditEquipmentItem(): void + { + $admin = User::factory()->admin()->create(); + + $equipmentItem = EquipmentItem::factory()->create(); + + $this->assertDatabaseHas("equipment_items", [ + "id" => $equipmentItem->id, + "id_number" => $equipmentItem->id_number, + "name" => $equipmentItem->name, + "is_mobile" => $equipmentItem->is_mobile, + "assignee_id" => $equipmentItem->assignee_id, + "assigned_at" => $equipmentItem->assigned_at, + ]); + + $this->actingAs($admin) + ->put("/equipment-items/{$equipmentItem->id}", [ + "idNumber" => "123", + "name" => "Test", + "isMobile" => true, + "assignee" => null, + "assignedAt" => null, + ]) + ->assertSessionHasNoErrors(); + + $this->assertDatabaseHas("equipment_items", [ + "id" => $equipmentItem->id, + "id_number" => "123", + "name" => "Test", + "is_mobile" => true, + "assignee_id" => null, + "assigned_at" => null, + ]); + } + + public function testAdminCanDeleteEquipmentItem(): void + { + $admin = User::factory()->admin()->create(); + + $equipmentItem = EquipmentItem::factory()->create(); + + $this->actingAs($admin) + ->delete("/equipment-items/{$equipmentItem->id}") + ->assertSessionHasNoErrors(); + + $this->assertModelMissing($equipmentItem); + } + + public function testAdminCanSeeEquipmentLabelList(): void + { + EquipmentLabel::factory()->count(10)->create(); + $admin = User::factory()->admin()->create(); + + $this->assertDatabaseCount("equipment_labels", 10); + + $this->actingAs($admin) + ->get("/equipment-labels") + ->assertOk() + ->assertInertia( + fn(Assert $page) => $page + ->component("EquipmentLabels") + ->has("labels.data", 10), + ); + } + + public function testAdminCanCreateEquipmentLabel(): void + { + $admin = User::factory()->admin()->create(); + + $this->actingAs($admin) + ->post("/equipment-labels", [ + "name" => "Test", + ]) + ->assertSessionHasNoErrors(); + + $this->assertDatabaseHas("equipment_labels", [ + "name" => "Test", + ]); + } + + public function testAdminCanDeleteEquipmentLabel(): void + { + $admin = User::factory()->admin()->create(); + + $equipmentLabel = EquipmentLabel::factory()->create(); + + $this->actingAs($admin) + ->delete("/equipment-labels/{$equipmentLabel->id}") + ->assertSessionHasNoErrors(); + + $this->assertModelMissing($equipmentLabel); + } + + public function testEmployeeCanOwnEquipment(): void + { + $employee = User::factory()->employee()->create(); + EquipmentItem::factory() + ->count(10) + ->for($employee, "assignee") + ->create(); + + $this->actingAs($employee) + ->get("/equipment-items/me") + ->assertOk() + ->assertInertia( + fn(Assert $page) => $page + ->component("Equipment/IndexForEmployee") + ->has("equipmentItems.data", 10), + ); + } +} diff --git a/tests/Feature/PermissionTest.php b/tests/Feature/PermissionTest.php index 9d152a3c..acaf741e 100644 --- a/tests/Feature/PermissionTest.php +++ b/tests/Feature/PermissionTest.php @@ -45,7 +45,8 @@ public function testAdminCanSeeEditEmployeePermissionsForm(): void ->where("receiveBenefitsReportCreationNotification", false) ->where("receiveVacationRequestsSummaryNotification", false) ->where("receiveVacationRequestWaitsForApprovalNotification", false) - ->where("receiveVacationRequestStatusChangedNotification", false), + ->where("receiveVacationRequestStatusChangedNotification", false) + ->where("manageEquipment", false), ), ); } @@ -82,7 +83,8 @@ public function testAdminCanSeeEditTechnicalApproverPermissionsForm(): void ->where("receiveBenefitsReportCreationNotification", false) ->where("receiveVacationRequestsSummaryNotification", true) ->where("receiveVacationRequestWaitsForApprovalNotification", true) - ->where("receiveVacationRequestStatusChangedNotification", true), + ->where("receiveVacationRequestStatusChangedNotification", true) + ->where("manageEquipment", false), ), ); } @@ -119,7 +121,8 @@ public function testAdminCanSeeEditAdministrativeApproverPermissionsForm(): void ->where("receiveBenefitsReportCreationNotification", true) ->where("receiveVacationRequestsSummaryNotification", true) ->where("receiveVacationRequestWaitsForApprovalNotification", true) - ->where("receiveVacationRequestStatusChangedNotification", true), + ->where("receiveVacationRequestStatusChangedNotification", true) + ->where("manageEquipment", true), ), ); }