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 = [
| 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']);
+});
|