From 1df697afcec34e41cbf2d817ef383c90b82dc5af Mon Sep 17 00:00:00 2001 From: Kacper Date: Fri, 24 Jul 2026 13:58:18 +0200 Subject: [PATCH] v1.2.2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Subcategories can now be reordered within their category — up/down arrow buttons next to "Edytuj" in Admin > Kategorie. The order set there is used everywhere a subcategory list is shown (pickers, admin listings, etc.). --- CHANGELOG.md | 10 ++++ README.md | 4 +- src/.env.example | 2 +- src/app/Livewire/Admin/Panel.php | 47 +++++++++++++++- src/app/Models/Category.php | 2 +- src/app/Models/Subcategory.php | 2 +- ..._add_sort_order_to_subcategories_table.php | 27 +++++++++ .../views/livewire/admin/panel.blade.php | 6 +- .../Feature/AdminCategoryManagementTest.php | 56 +++++++++++++++++++ 9 files changed, 149 insertions(+), 7 deletions(-) create mode 100644 src/database/migrations/2026_07_24_000157_add_sort_order_to_subcategories_table.php diff --git a/CHANGELOG.md b/CHANGELOG.md index e01f163..1ab787d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,16 @@ All notable changes to this project are documented in this file. Format loosely follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +## [1.2.2] - 2026-07-24 + +### Added + +- Subcategories can now be reordered within their category — up/down arrow + buttons next to "Edytuj" in Admin > Kategorie, right beside each + subcategory's edit/delete buttons. The order set there is used everywhere a + subcategory list is shown (subcategory pickers, admin listings, etc.), not + just the admin panel itself. + ## [1.2.1] - 2026-07-24 ### Added diff --git a/README.md b/README.md index 3e4db9a..46a2c18 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,9 @@ and **[wiki/admin](wiki/admin/README.md)** for role-specific how-to guides. alike, are configurable from Admin > Konfiguracja (see below). - **Categories & custom fields** — admin-defined categories/subcategories, each with its own set of custom fields (text/textarea/select/checkbox/date/number) and an - optional default priority. + optional default priority. Subcategories within a category can be reordered with + up/down arrows in Admin > Kategorie; the order set there is what clients/operators + see everywhere a subcategory picker is shown. - **Teams** — subcategories auto-route to a team; operators only see their own team's queue (plus unrouted tickets and anything assigned to them) unless they're an admin. Reassigning a ticket to a team, though, is unrestricted — an operator diff --git a/src/.env.example b/src/.env.example index d79eca6..f7b50b8 100644 --- a/src/.env.example +++ b/src/.env.example @@ -5,7 +5,7 @@ APP_DEBUG=false APP_URL=http://localhost AUTHOR_CONTACT=helpdesk@kzbikowski.pl -VERSION=1.2.1 +VERSION=1.2.2 APP_LOCALE=en APP_FALLBACK_LOCALE=en diff --git a/src/app/Livewire/Admin/Panel.php b/src/app/Livewire/Admin/Panel.php index 621bddd..aea586b 100644 --- a/src/app/Livewire/Admin/Panel.php +++ b/src/app/Livewire/Admin/Panel.php @@ -316,10 +316,55 @@ class Panel extends Component return; } - Category::query()->find($categoryId)?->subcategories()->create(['name' => $name]); + $category = Category::query()->find($categoryId); + + if (! $category) { + return; + } + + $nextPosition = ((int) $category->subcategories()->max('sort_order')) + 1; + $category->subcategories()->create(['name' => $name, 'sort_order' => $nextPosition]); $this->newSubNames[$categoryId] = ''; } + public function moveSubcategoryUp(int $subcategoryId): void + { + $this->swapSubcategoryOrder($subcategoryId, -1); + } + + public function moveSubcategoryDown(int $subcategoryId): void + { + $this->swapSubcategoryOrder($subcategoryId, 1); + } + + protected function swapSubcategoryOrder(int $subcategoryId, int $direction): void + { + $sub = Subcategory::query()->find($subcategoryId); + + if (! $sub) { + return; + } + + $siblings = Subcategory::query() + ->where('category_id', $sub->category_id) + ->orderBy('sort_order') + ->orderBy('id') + ->get(); + + $idx = $siblings->search(fn (Subcategory $s) => $s->id === $sub->id); + $swapIdx = $idx + $direction; + + if ($idx === false || $swapIdx < 0 || $swapIdx >= $siblings->count()) { + return; + } + + $other = $siblings[$swapIdx]; + [$order, $otherOrder] = [$sub->sort_order, $other->sort_order]; + + $sub->update(['sort_order' => $otherOrder]); + $other->update(['sort_order' => $order]); + } + public function openSubcategoryEditForm(int $subcategoryId): void { $sub = Subcategory::query()->with('customFields')->findOrFail($subcategoryId); diff --git a/src/app/Models/Category.php b/src/app/Models/Category.php index c0e66ab..31b22de 100644 --- a/src/app/Models/Category.php +++ b/src/app/Models/Category.php @@ -11,6 +11,6 @@ class Category extends Model { public function subcategories(): HasMany { - return $this->hasMany(Subcategory::class); + return $this->hasMany(Subcategory::class)->orderBy('sort_order'); } } diff --git a/src/app/Models/Subcategory.php b/src/app/Models/Subcategory.php index 6b63eca..0477cd3 100644 --- a/src/app/Models/Subcategory.php +++ b/src/app/Models/Subcategory.php @@ -8,7 +8,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; -#[Fillable(['category_id', 'name', 'description', 'default_priority_key'])] +#[Fillable(['category_id', 'name', 'description', 'default_priority_key', 'sort_order'])] class Subcategory extends Model { public function category(): BelongsTo diff --git a/src/database/migrations/2026_07_24_000157_add_sort_order_to_subcategories_table.php b/src/database/migrations/2026_07_24_000157_add_sort_order_to_subcategories_table.php new file mode 100644 index 0000000..9a2a79d --- /dev/null +++ b/src/database/migrations/2026_07_24_000157_add_sort_order_to_subcategories_table.php @@ -0,0 +1,27 @@ +unsignedInteger('sort_order')->default(0)->after('default_priority_key'); + }); + + foreach (DB::table('subcategories')->orderBy('category_id')->orderBy('id')->get() as $position => $sub) { + DB::table('subcategories')->where('id', $sub->id)->update(['sort_order' => $position]); + } + } + + public function down(): void + { + Schema::table('subcategories', function (Blueprint $table) { + $table->dropColumn('sort_order'); + }); + } +}; diff --git a/src/resources/views/livewire/admin/panel.blade.php b/src/resources/views/livewire/admin/panel.blade.php index 3225f8e..403fdad 100644 --- a/src/resources/views/livewire/admin/panel.blade.php +++ b/src/resources/views/livewire/admin/panel.blade.php @@ -85,7 +85,7 @@ $tabGroups = [ - @foreach ($cat->subcategories as $sub) + @foreach ($cat->subcategories as $i => $sub)
Podkategoria
@@ -97,7 +97,9 @@ $tabGroups = [
-
+
+ +
diff --git a/src/tests/Feature/AdminCategoryManagementTest.php b/src/tests/Feature/AdminCategoryManagementTest.php index 48ea97b..2e334c0 100644 --- a/src/tests/Feature/AdminCategoryManagementTest.php +++ b/src/tests/Feature/AdminCategoryManagementTest.php @@ -45,3 +45,59 @@ test('admin can add a description to a subcategory via the edit dialog', functio expect($sub->fresh()->description)->toBe('Problemy z połączeniem VPN.'); }); + +test('admin can reorder subcategories with up/down arrows', function () { + $admin = adminUser(); + $category = Category::query()->create(['name' => 'IT-Pomoc']); + $vpn = $category->subcategories()->create(['name' => 'VPN', 'sort_order' => 0]); + $wifi = $category->subcategories()->create(['name' => 'WiFi', 'sort_order' => 1]); + $drukarki = $category->subcategories()->create(['name' => 'Drukarki', 'sort_order' => 2]); + + Livewire::actingAs($admin)->test(Panel::class) + ->call('setTab', 'categories') + ->call('moveSubcategoryUp', $drukarki->id) + ->assertOk(); + + expect($category->fresh()->subcategories->pluck('name')->all()) + ->toBe(['VPN', 'Drukarki', 'WiFi']); + + Livewire::actingAs($admin)->test(Panel::class) + ->call('setTab', 'categories') + ->call('moveSubcategoryDown', $vpn->id) + ->assertOk(); + + expect($category->fresh()->subcategories->pluck('name')->all()) + ->toBe(['Drukarki', 'VPN', 'WiFi']); +}); + +test('moving the top subcategory up or the bottom one down is a no-op', function () { + $admin = adminUser(); + $category = Category::query()->create(['name' => 'IT-Pomoc']); + $vpn = $category->subcategories()->create(['name' => 'VPN', 'sort_order' => 0]); + $wifi = $category->subcategories()->create(['name' => 'WiFi', 'sort_order' => 1]); + + Livewire::actingAs($admin)->test(Panel::class) + ->call('setTab', 'categories') + ->call('moveSubcategoryUp', $vpn->id) + ->call('moveSubcategoryDown', $wifi->id) + ->assertOk(); + + expect($category->fresh()->subcategories->pluck('name')->all()) + ->toBe(['VPN', 'WiFi']); +}); + +test('a new subcategory is appended to the end of the display order', function () { + $admin = adminUser(); + $category = Category::query()->create(['name' => 'IT-Pomoc']); + $category->subcategories()->create(['name' => 'VPN', 'sort_order' => 0]); + $category->subcategories()->create(['name' => 'WiFi', 'sort_order' => 1]); + + Livewire::actingAs($admin)->test(Panel::class) + ->call('setTab', 'categories') + ->set('newSubNames.'.$category->id, 'Drukarki') + ->call('addSubcategory', $category->id) + ->assertOk(); + + expect($category->fresh()->subcategories->pluck('name')->all()) + ->toBe(['VPN', 'WiFi', 'Drukarki']); +});