Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1df697afce |
10
CHANGELOG.md
10
CHANGELOG.md
@@ -3,6 +3,16 @@
|
|||||||
All notable changes to this project are documented in this file. Format loosely
|
All notable changes to this project are documented in this file. Format loosely
|
||||||
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
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
|
## [1.2.1] - 2026-07-24
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -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).
|
alike, are configurable from Admin > Konfiguracja (see below).
|
||||||
- **Categories & custom fields** — admin-defined categories/subcategories, each with
|
- **Categories & custom fields** — admin-defined categories/subcategories, each with
|
||||||
its own set of custom fields (text/textarea/select/checkbox/date/number) and an
|
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
|
- **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
|
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
|
an admin. Reassigning a ticket to a team, though, is unrestricted — an operator
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ APP_DEBUG=false
|
|||||||
APP_URL=http://localhost
|
APP_URL=http://localhost
|
||||||
|
|
||||||
AUTHOR_CONTACT=helpdesk@kzbikowski.pl
|
AUTHOR_CONTACT=helpdesk@kzbikowski.pl
|
||||||
VERSION=1.2.1
|
VERSION=1.2.2
|
||||||
|
|
||||||
APP_LOCALE=en
|
APP_LOCALE=en
|
||||||
APP_FALLBACK_LOCALE=en
|
APP_FALLBACK_LOCALE=en
|
||||||
|
|||||||
@@ -316,10 +316,55 @@ class Panel extends Component
|
|||||||
return;
|
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] = '';
|
$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
|
public function openSubcategoryEditForm(int $subcategoryId): void
|
||||||
{
|
{
|
||||||
$sub = Subcategory::query()->with('customFields')->findOrFail($subcategoryId);
|
$sub = Subcategory::query()->with('customFields')->findOrFail($subcategoryId);
|
||||||
|
|||||||
@@ -11,6 +11,6 @@ class Category extends Model
|
|||||||
{
|
{
|
||||||
public function subcategories(): HasMany
|
public function subcategories(): HasMany
|
||||||
{
|
{
|
||||||
return $this->hasMany(Subcategory::class);
|
return $this->hasMany(Subcategory::class)->orderBy('sort_order');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|||||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
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
|
class Subcategory extends Model
|
||||||
{
|
{
|
||||||
public function category(): BelongsTo
|
public function category(): BelongsTo
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('subcategories', function (Blueprint $table) {
|
||||||
|
$table->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');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -85,7 +85,7 @@ $tabGroups = [
|
|||||||
<table class="table" style="margin:0;border-top:none">
|
<table class="table" style="margin:0;border-top:none">
|
||||||
<thead><tr><th>Podkategoria</th><th></th></tr></thead>
|
<thead><tr><th>Podkategoria</th><th></th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@foreach ($cat->subcategories as $sub)
|
@foreach ($cat->subcategories as $i => $sub)
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<div style="display:flex;flex-direction:column;gap:2px">
|
<div style="display:flex;flex-direction:column;gap:2px">
|
||||||
@@ -97,7 +97,9 @@ $tabGroups = [
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div style="display:flex;gap:6px;justify-content:flex-end">
|
<div style="display:flex;gap:6px;justify-content:flex-end;align-items:center">
|
||||||
|
<button class="btn btn-icon" type="button" wire:click="moveSubcategoryUp({{ $sub->id }})" @disabled($i === 0) style="padding:2px;width:24px;height:24px" title="Przesuń wyżej"><span class="material-symbols-outlined" style="font-size:16px">arrow_upward</span></button>
|
||||||
|
<button class="btn btn-icon" type="button" wire:click="moveSubcategoryDown({{ $sub->id }})" @disabled($i === $cat->subcategories->count() - 1) style="padding:2px;width:24px;height:24px" title="Przesuń niżej"><span class="material-symbols-outlined" style="font-size:16px">arrow_downward</span></button>
|
||||||
<button class="btn btn-ghost" type="button" wire:click="openSubcategoryEditForm({{ $sub->id }})">Edytuj</button>
|
<button class="btn btn-ghost" type="button" wire:click="openSubcategoryEditForm({{ $sub->id }})">Edytuj</button>
|
||||||
<button class="btn btn-ghost" type="button" wire:click="removeSubcategory({{ $sub->id }})">Usuń</button>
|
<button class="btn btn-ghost" type="button" wire:click="removeSubcategory({{ $sub->id }})">Usuń</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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.');
|
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']);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user