+
{{ $actions }}
@endisset
diff --git a/Modules/Helpdesk/Resources/views/partials/sidebar-support-menu.blade.php b/Modules/Helpdesk/Resources/views/partials/sidebar-support-menu.blade.php
deleted file mode 100644
index 8a4447d8..00000000
--- a/Modules/Helpdesk/Resources/views/partials/sidebar-support-menu.blade.php
+++ /dev/null
@@ -1,16 +0,0 @@
-@php
- $helpdeskEnabled = Route::has('helpdesk.index');
-
- if (class_exists(\Nwidart\Modules\Facades\Module::class)) {
- $helpdeskModule = \Nwidart\Modules\Facades\Module::find('Helpdesk');
- $helpdeskEnabled = $helpdeskModule !== null && $helpdeskModule->isEnabled() && Route::has('helpdesk.index');
- }
-@endphp
-
-@if ($helpdeskEnabled)
-
-@endif
diff --git a/Modules/Helpdesk/Routes/web.php b/Modules/Helpdesk/Routes/web.php
index b7ac99e7..353f833a 100644
--- a/Modules/Helpdesk/Routes/web.php
+++ b/Modules/Helpdesk/Routes/web.php
@@ -6,6 +6,7 @@ use Illuminate\Support\Facades\Route;
use Modules\Helpdesk\Http\Controllers\AttachmentDownloadController;
use Modules\Helpdesk\Http\Controllers\HelpdeskController;
use Modules\Helpdesk\Http\Controllers\TicketAttachmentStoreController;
+use Modules\Helpdesk\Http\Controllers\TicketBoardController;
use Modules\Helpdesk\Http\Controllers\TicketCommentDeleteController;
use Modules\Helpdesk\Http\Controllers\TicketCommentStoreController;
use Modules\Helpdesk\Http\Controllers\TicketControlsController;
@@ -33,6 +34,7 @@ Route::get('/tickets/{ticket}/edit', TicketEdit::class)->name('tickets.edit');
Route::post('/tickets/{ticket}/agent', [TicketControlsController::class, 'updateAgent'])->name('tickets.agent.update');
Route::post('/tickets/{ticket}/status', [TicketControlsController::class, 'updateStatus'])->name('tickets.status.update');
Route::post('/tickets/{ticket}/priority', [TicketControlsController::class, 'updatePriority'])->name('tickets.priority.update');
+Route::post('/tickets/{ticket}/board', TicketBoardController::class)->name('tickets.board.update');
Route::post('/tickets/{ticket}/comments', TicketCommentStoreController::class)->name('tickets.comments.store');
Route::post('/tickets/{ticket}/attachments', TicketAttachmentStoreController::class)->name('tickets.attachments.store');
Route::delete('/tickets/{ticket}/comments/{comment}', TicketCommentDeleteController::class)->name('tickets.comments.destroy');
diff --git a/Modules/Helpdesk/Tests/Browser/HelpdeskCreateTicketAttachmentBrowserTest.php b/Modules/Helpdesk/Tests/Browser/HelpdeskCreateTicketAttachmentBrowserTest.php
new file mode 100644
index 00000000..17694ced
--- /dev/null
+++ b/Modules/Helpdesk/Tests/Browser/HelpdeskCreateTicketAttachmentBrowserTest.php
@@ -0,0 +1,303 @@
+seedLookupData();
+ $user = $this->user(['name' => 'Browser Attachment User']);
+ $subject = 'Browser attachment ticket ' . uniqid('', true);
+ $filePath = $this->makeBrowserUploadFile('browser-upload-proof.log', 'Browser upload proof file.');
+
+ $this->browse(function (Browser $browser) use ($user, $fixtures, $subject, $filePath): void {
+ $browser->loginAs($user)
+ ->visitRoute('helpdesk.tickets.create')
+ ->waitFor('@helpdesk-ticket-create', 10)
+ ->assertSee('Create ticket')
+ ->type('@ticket-subject', $subject);
+
+ $browser->attach('#attachments', $filePath);
+
+ $this->waitForHelpdeskUploadToFinish($browser);
+
+ $browser->type('@ticket-subject', $subject);
+ $this->setHelpdeskSelect($browser, 'board_id', $fixtures['board']->id);
+ $this->setHelpdeskSelect($browser, 'priority_id', $fixtures['high']->id);
+ $this->setHelpdeskSelect($browser, 'category_id', $fixtures['category']->id);
+ $this->setHelpdeskSelect($browser, 'team_id', $fixtures['team']->id);
+ $this->setHelpdeskSelect($browser, 'project_id', $fixtures['project']->id);
+ $this->setHelpdeskEditor(
+ $browser,
+ 'ticket-body-editor',
+ 'This browser ticket proves attachments can be uploaded and saved.',
+ );
+
+ $this->submitTicketCreateComponent($browser);
+
+ $this->waitForTicketCreated($browser, $subject);
+
+ $browser->visitRoute('helpdesk.tickets.index')
+ ->waitForText($subject, 10)
+ ->assertPathIs('/helpdesk/tickets')
+ ->assertSee($subject);
+ });
+
+ $ticket = Ticket::query()
+ ->where('subject', $subject)
+ ->firstOrFail();
+
+ $attachment = Attachment::query()
+ ->where('ticket_id', $ticket->id)
+ ->whereNull('comment_id')
+ ->where('original_name', 'browser-upload-proof.log')
+ ->firstOrFail();
+
+ self::assertSame($ticket->id, (int) $attachment->ticket_id);
+ }
+
+ public function test_user_can_create_ticket_with_multiple_attachments_in_browser(): void
+ {
+ $fixtures = $this->seedLookupData();
+ $user = $this->user(['name' => 'Browser Multi Attachment User']);
+ $subject = 'Browser multiple attachment ticket ' . uniqid('', true);
+ $firstFile = $this->makeBrowserUploadFile('browser-first-proof.txt', 'First browser upload proof file.');
+ $secondFile = $this->makeBrowserUploadFile('browser-second-proof.csv', 'name,value' . PHP_EOL . 'proof,1');
+
+ $this->browse(function (Browser $browser) use ($user, $fixtures, $subject, $firstFile, $secondFile): void {
+ $browser->loginAs($user)
+ ->visitRoute('helpdesk.tickets.create')
+ ->waitFor('@helpdesk-ticket-create', 10)
+ ->assertSee('Create ticket')
+ ->type('@ticket-subject', $subject);
+
+ $browser->attach('#attachments', $firstFile . "\n" . $secondFile);
+
+ $this->waitForHelpdeskUploadToFinish($browser);
+
+ $browser->type('@ticket-subject', $subject);
+ $this->setHelpdeskSelect($browser, 'board_id', $fixtures['board']->id);
+ $this->setHelpdeskSelect($browser, 'priority_id', $fixtures['normal']->id);
+ $this->setHelpdeskSelect($browser, 'category_id', $fixtures['category']->id);
+ $this->setHelpdeskSelect($browser, 'team_id', $fixtures['team']->id);
+ $this->setHelpdeskSelect($browser, 'project_id', $fixtures['project']->id);
+ $this->setHelpdeskEditor(
+ $browser,
+ 'ticket-body-editor',
+ 'This browser ticket proves multiple attachments can be uploaded and saved.',
+ );
+
+ $this->submitTicketCreateComponent($browser);
+
+ $this->waitForTicketCreated($browser, $subject);
+
+ $browser->visitRoute('helpdesk.tickets.index')
+ ->waitForText($subject, 10)
+ ->assertPathIs('/helpdesk/tickets')
+ ->assertSee($subject);
+ });
+
+ $ticket = Ticket::query()
+ ->where('subject', $subject)
+ ->firstOrFail();
+
+ self::assertSame(
+ 2,
+ Attachment::query()->where('ticket_id', $ticket->id)->whereNull('comment_id')->count(),
+ );
+
+ foreach (['browser-first-proof.txt', 'browser-second-proof.csv'] as $name) {
+ $this->assertDatabaseHas('helpdesk_attachments', [
+ 'ticket_id' => $ticket->id,
+ 'comment_id' => null,
+ 'original_name' => $name,
+ ]);
+ }
+ }
+
+ private function makeBrowserUploadFile(string $name, string $contents): string
+ {
+ $directory = storage_path('framework/testing/helpdesk-browser-uploads');
+
+ if (! is_dir($directory)) {
+ mkdir($directory, 0777, true);
+ }
+
+ $path = $directory . DIRECTORY_SEPARATOR . $name;
+ file_put_contents($path, $contents);
+
+ return $path;
+ }
+
+ private function setHelpdeskEditor(Browser $browser, string $dusk, string $text): void
+ {
+ $encodedDusk = json_encode($dusk, JSON_THROW_ON_ERROR);
+ $encodedText = json_encode($text, JSON_THROW_ON_ERROR);
+
+ $browser->script(<<
pause(500);
+ }
+
+ private function setHelpdeskSelect(Browser $browser, string $model, int|string|null $value): void
+ {
+ $encodedModel = json_encode($model, JSON_THROW_ON_ERROR);
+ $encodedValue = json_encode($value === null ? '' : (string) $value, JSON_THROW_ON_ERROR);
+
+ $browser->script(<<pause(250);
+ }
+
+ private function submitTicketCreateComponent(Browser $browser): void
+ {
+ $browser->waitUsing(10, 100, static function () use ($browser): bool {
+ return (bool) $browser->script(<<<'JS'
+const button = document.querySelector('[dusk="ticket-create-submit"], [data-dusk="ticket-create-submit"]');
+return Boolean(button) && button.disabled === false;
+JS)[0];
+ }, 'The helpdesk ticket create form was not ready to submit.');
+
+ $browser->script(<<<'JS'
+const root = document.querySelector('[dusk="helpdesk-ticket-create"], [data-dusk="helpdesk-ticket-create"]');
+
+if (! root) {
+ throw new Error('Helpdesk ticket create root was not found.');
+}
+
+let componentRoot = root;
+while (componentRoot && ! componentRoot.hasAttribute('wire:id')) {
+ componentRoot = componentRoot.parentElement;
+}
+
+if (! componentRoot || ! window.Livewire) {
+ throw new Error('Helpdesk Livewire create component was not found.');
+}
+
+const component = window.Livewire.find(componentRoot.getAttribute('wire:id'));
+
+if (! component) {
+ throw new Error('Helpdesk Livewire create component instance was not found.');
+}
+
+component.call('save');
+JS);
+ }
+
+ private function waitForHelpdeskUploadToFinish(Browser $browser): void
+ {
+ $browser->waitUsing(30, 250, static function () use ($browser): bool {
+ return (bool) $browser->script(<<<'JS'
+const input = document.querySelector('#attachments');
+const button = document.querySelector('[dusk="ticket-create-submit"], [data-dusk="ticket-create-submit"]');
+const selected = document.querySelector('[dusk="ticket-create-selected-attachments"], [data-dusk="ticket-create-selected-attachments"]');
+
+return Boolean(input)
+ && input.files.length > 0
+ && Boolean(selected)
+ && Boolean(button)
+ && button.disabled === false;
+JS)[0];
+ }, 'The Livewire temporary attachment upload did not finish before submit.');
+ }
+
+ private function waitForTicketCreated(Browser $browser, string $subject): void
+ {
+ $browser->waitUsing(20, 250, static fn (): bool => Ticket::query()
+ ->where('subject', $subject)
+ ->exists(), 'The browser did not create the helpdesk ticket before the timeout.');
+ }
+}
diff --git a/Modules/Helpdesk/Tests/Feature/KanbanBoardTest.php b/Modules/Helpdesk/Tests/Feature/KanbanBoardTest.php
index 53de72c5..e88ee2c1 100644
--- a/Modules/Helpdesk/Tests/Feature/KanbanBoardTest.php
+++ b/Modules/Helpdesk/Tests/Feature/KanbanBoardTest.php
@@ -48,6 +48,44 @@ final class KanbanBoardTest extends TestCase
->assertSee('High');
}
+ /** @test */
+ public function test_ticket_can_be_moved_to_another_board(): void
+ {
+ $lookups = $this->seedLookupData();
+ $user = $this->admin();
+
+ $boardA = $lookups['board'];
+
+ $boardB = KanbanBoardModel::query()->create([
+ 'name' => 'Regression Target Board',
+ 'slug' => 'regression-target-board',
+ 'is_default' => false,
+ 'sort_order' => 50,
+ ]);
+
+ $boardB->statuses()->sync([
+ $lookups['open']->id => ['sort_order' => 10],
+ ]);
+
+ $ticket = $this->ticket([
+ 'owner' => $user,
+ 'status_id' => $lookups['open']->id,
+ 'priority_id' => $lookups['normal']->id,
+ 'board_id' => $boardA->id,
+ ]);
+
+ $this->actingAs($user);
+
+ Livewire::test(KanbanBoard::class)
+ ->call('moveTicketToBoard', $ticket->id, $boardB->id)
+ ->assertHasNoErrors();
+
+ $ticket->refresh();
+
+ $this->assertSame($boardB->id, $ticket->board_id);
+ $this->assertSame($lookups['open']->id, $ticket->status_id);
+ }
+
/** @test */
public function kanban_board_can_move_ticket_to_closed_status(): void
{
diff --git a/Modules/Helpdesk/Tests/Feature/TicketBranchCommandUiFilesTest.php b/Modules/Helpdesk/Tests/Feature/TicketBranchCommandUiFilesTest.php
index 64774a8d..cf85c3be 100644
--- a/Modules/Helpdesk/Tests/Feature/TicketBranchCommandUiFilesTest.php
+++ b/Modules/Helpdesk/Tests/Feature/TicketBranchCommandUiFilesTest.php
@@ -29,6 +29,12 @@ final class TicketBranchCommandUiFilesTest extends TestCase
self::assertStringContainsString('data-helpdesk-branch-command', $view);
self::assertStringContainsString('git checkout -b bugfix/{{ $ticket->reference }}-', $view);
self::assertStringContainsString('data-helpdesk-branch-suffix', $view);
+ self::assertStringContainsString('data-helpdesk-branch-prefix-select', $view);
+ self::assertStringContainsString('normaliseHelpdeskBranchPrefix', $view);
+ self::assertStringContainsString('feature/', $view);
+ self::assertStringContainsString('bugfix/', $view);
+ self::assertStringContainsString('hotfix/', $view);
+ self::assertStringContainsString('support/', $view);
self::assertStringContainsString('data-helpdesk-copy-branch-command', $view);
self::assertStringContainsString('copyHelpdeskBranchCommand', $view);
self::assertStringContainsString('normaliseHelpdeskBranchSuffix', $view);
diff --git a/Modules/Helpdesk/Tests/Feature/TicketConversationTest.php b/Modules/Helpdesk/Tests/Feature/TicketConversationTest.php
index 061c3f8b..988ccbb2 100644
--- a/Modules/Helpdesk/Tests/Feature/TicketConversationTest.php
+++ b/Modules/Helpdesk/Tests/Feature/TicketConversationTest.php
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace Modules\Helpdesk\Tests\Feature;
-use App\Models\Company;
+use Modules\Company\Models\Company;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Mail;
diff --git a/Modules/Helpdesk/Tests/Feature/TicketCreateAttachmentBrowserSafetyTest.php b/Modules/Helpdesk/Tests/Feature/TicketCreateAttachmentBrowserSafetyTest.php
new file mode 100644
index 00000000..e8329bfe
--- /dev/null
+++ b/Modules/Helpdesk/Tests/Feature/TicketCreateAttachmentBrowserSafetyTest.php
@@ -0,0 +1,45 @@
+ 0)', $view);
+ self::assertStringContainsString('$attachment->getClientOriginalName()', $view);
+ self::assertStringContainsString('wire:loading wire:target="attachments"', $view);
+ }
+}
diff --git a/Modules/Helpdesk/Tests/Feature/TicketCreateAttachmentPipelineTest.php b/Modules/Helpdesk/Tests/Feature/TicketCreateAttachmentPipelineTest.php
new file mode 100644
index 00000000..d4c33682
--- /dev/null
+++ b/Modules/Helpdesk/Tests/Feature/TicketCreateAttachmentPipelineTest.php
@@ -0,0 +1,143 @@
+set('helpdesk.disk', 'local');
+
+ $fixtures = $this->seedLookupData();
+ $user = $this->user(['name' => 'Attachment Create User']);
+ $file = UploadedFile::fake()->create('warehouse-error.log', 12, 'text/plain');
+
+ Livewire::actingAs($user)
+ ->test(TicketCreate::class)
+ ->set('subject', 'Create ticket with one attachment')
+ ->set('body', 'This ticket must persist a Livewire upload attachment.
')
+ ->set('board_id', $fixtures['board']->id)
+ ->set('priority_id', $fixtures['high']->id)
+ ->set('category_id', $fixtures['category']->id)
+ ->set('team_id', $fixtures['team']->id)
+ ->set('project_id', $fixtures['project']->id)
+ ->set('attachments', [$file])
+ ->call('save')
+ ->assertHasNoErrors()
+ ->assertRedirect(route('helpdesk.tickets.index'));
+
+ $ticket = Ticket::query()
+ ->where('subject', 'Create ticket with one attachment')
+ ->firstOrFail();
+
+ $attachment = Attachment::query()
+ ->where('ticket_id', $ticket->id)
+ ->whereNull('comment_id')
+ ->firstOrFail();
+
+ self::assertSame($user->id, (int) $attachment->user_id);
+ self::assertSame('local', $attachment->disk);
+ self::assertSame('warehouse-error.log', $attachment->original_name);
+ self::assertSame('log', $attachment->extension());
+ Storage::disk('local')->assertExists($attachment->path);
+ }
+
+ public function test_ticket_create_persists_multiple_attachments_through_livewire(): void
+ {
+ Storage::fake('local');
+ config()->set('helpdesk.disk', 'local');
+
+ $fixtures = $this->seedLookupData();
+ $user = $this->user(['name' => 'Multiple Attachment User']);
+
+ Livewire::actingAs($user)
+ ->test(TicketCreate::class)
+ ->set('subject', 'Create ticket with multiple attachments')
+ ->set('body', 'This ticket must persist every selected Livewire upload.
')
+ ->set('board_id', $fixtures['board']->id)
+ ->set('priority_id', $fixtures['normal']->id)
+ ->set('category_id', $fixtures['category']->id)
+ ->set('team_id', $fixtures['team']->id)
+ ->set('project_id', $fixtures['project']->id)
+ ->set('attachments', [
+ UploadedFile::fake()->create('first-proof.pdf', 20, 'application/pdf'),
+ UploadedFile::fake()->create('second-proof.txt', 8, 'text/plain'),
+ UploadedFile::fake()->create('third-proof.csv', 6, 'text/csv'),
+ ])
+ ->call('save')
+ ->assertHasNoErrors()
+ ->assertRedirect(route('helpdesk.tickets.index'));
+
+ $ticket = Ticket::query()
+ ->where('subject', 'Create ticket with multiple attachments')
+ ->firstOrFail();
+
+ self::assertSame(3, Attachment::query()->where('ticket_id', $ticket->id)->whereNull('comment_id')->count());
+
+ foreach (['first-proof.pdf', 'second-proof.txt', 'third-proof.csv'] as $name) {
+ $attachment = Attachment::query()
+ ->where('ticket_id', $ticket->id)
+ ->where('original_name', $name)
+ ->firstOrFail();
+
+ Storage::disk('local')->assertExists($attachment->path);
+ }
+ }
+
+ public function test_ticket_create_accepts_source_code_attachments_through_livewire(): void
+ {
+ Storage::fake('local');
+ config()->set('helpdesk.disk', 'local');
+
+ $fixtures = $this->seedLookupData();
+ $user = $this->user(['name' => 'Source Attachment User']);
+
+ Livewire::actingAs($user)
+ ->test(TicketCreate::class)
+ ->set('subject', 'Create ticket with source attachments')
+ ->set('body', 'This ticket must accept source files by extension.
')
+ ->set('board_id', $fixtures['board']->id)
+ ->set('priority_id', $fixtures['normal']->id)
+ ->set('category_id', $fixtures['category']->id)
+ ->set('team_id', $fixtures['team']->id)
+ ->set('project_id', $fixtures['project']->id)
+ ->set('attachments', [
+ UploadedFile::fake()->create('TicketCreate.php', 10, 'text/plain'),
+ UploadedFile::fake()->create('upload-widget.js', 10, 'text/plain'),
+ UploadedFile::fake()->create('warehouse-sync.rpgle', 10, 'text/plain'),
+ ])
+ ->call('save')
+ ->assertHasNoErrors()
+ ->assertRedirect(route('helpdesk.tickets.index'));
+
+ $ticket = Ticket::query()
+ ->where('subject', 'Create ticket with source attachments')
+ ->firstOrFail();
+
+ foreach (['TicketCreate.php', 'upload-widget.js', 'warehouse-sync.rpgle'] as $name) {
+ $attachment = Attachment::query()
+ ->where('ticket_id', $ticket->id)
+ ->where('original_name', $name)
+ ->firstOrFail();
+
+ self::assertTrue($attachment->isSourceCode(), $name . ' should be categorised as source code.');
+ Storage::disk('local')->assertExists($attachment->path);
+ }
+ }
+}
diff --git a/Modules/Helpdesk/Tests/Feature/TicketDeveloperActionsUiFilesTest.php b/Modules/Helpdesk/Tests/Feature/TicketDeveloperActionsUiFilesTest.php
new file mode 100644
index 00000000..e25812d3
--- /dev/null
+++ b/Modules/Helpdesk/Tests/Feature/TicketDeveloperActionsUiFilesTest.php
@@ -0,0 +1,70 @@
+reference . \'-\'', $view);
+ self::assertStringContainsString('data-helpdesk-branch-prefix-select', $view);
+ self::assertStringContainsString('$gitflowPrefixes', $view);
+ self::assertStringContainsString('normaliseHelpdeskBranchPrefix', $view);
+ self::assertStringContainsString('Commit message', $view);
+ self::assertStringContainsString('Pull request title', $view);
+ self::assertStringContainsString('data-helpdesk-copy-template', $view);
+ self::assertStringContainsString('data-helpdesk-copy-template-button', $view);
+ self::assertStringContainsString('copyHelpdeskTemplate', $view);
+ self::assertStringContainsString('copyTextToClipboard', $view);
+ }
+
+ /**
+ * Ensure linked Git activity still renders branch and commit badges instead of being hidden by the new panel.
+ */
+ public function test_ticket_show_still_renders_linked_git_activity_rows(): void
+ {
+ $viewPath = dirname(__DIR__, 2) . '/Resources/views/livewire/ticket-show.blade.php';
+ $view = file_get_contents($viewPath);
+
+ self::assertIsString($view);
+ self::assertStringContainsString('@forelse ($gitActivity as $commit)', $view);
+ self::assertStringContainsString('$isBranchActivity = $commit->short_hash === \'branch\';', $view);
+ self::assertStringContainsString('{{ $isBranchActivity ? \'Branch\' : \'Commit\' }}', $view);
+ self::assertStringContainsString('No Git activity linked.', $view);
+ }
+}
diff --git a/Modules/Helpdesk/Tests/Feature/TicketIndexDirectUserNPlusOneFilesTest.php b/Modules/Helpdesk/Tests/Feature/TicketIndexDirectUserNPlusOneFilesTest.php
new file mode 100644
index 00000000..b66ea794
--- /dev/null
+++ b/Modules/Helpdesk/Tests/Feature/TicketIndexDirectUserNPlusOneFilesTest.php
@@ -0,0 +1,22 @@
+with(['status', 'priority', 'category', 'project', 'team'])", $source);
+ self::assertStringNotContainsString("'user:id,name,email'", $source);
+ self::assertStringNotContainsString("'owner:id,name,email'", $source);
+ self::assertStringNotContainsString("'agent:id,name,email'", $source);
+ }
+}
diff --git a/Modules/Helpdesk/Tests/Feature/TicketIndexUserNPlusOneFilesTest.php b/Modules/Helpdesk/Tests/Feature/TicketIndexUserNPlusOneFilesTest.php
new file mode 100644
index 00000000..baf8051f
--- /dev/null
+++ b/Modules/Helpdesk/Tests/Feature/TicketIndexUserNPlusOneFilesTest.php
@@ -0,0 +1,26 @@
+select('helpdesk_tickets.*')", $component);
+ self::assertStringContainsString("'agent_name' => User::query()", $component);
+ self::assertStringContainsString("->whereColumn('users.id', 'helpdesk_tickets.agent_id')", $component);
+
+ self::assertStringContainsString('$ticket->agent_name ?? \'Unassigned\'', $view);
+ self::assertStringNotContainsString('$ticket->agent?->name ?? \'Unassigned\'', $view);
+ }
+}
diff --git a/Modules/Helpdesk/Tests/Feature/TicketShowCommentRelationsFilesTest.php b/Modules/Helpdesk/Tests/Feature/TicketShowCommentRelationsFilesTest.php
new file mode 100644
index 00000000..a7c35313
--- /dev/null
+++ b/Modules/Helpdesk/Tests/Feature/TicketShowCommentRelationsFilesTest.php
@@ -0,0 +1,19 @@
+with(['user.company', 'attachments'])", $source);
+ self::assertStringNotContainsString("->with(['user.company', 'owner:id,name,email,company_id', 'agent:id,name,email', 'attachments'])", $source);
+ }
+}
diff --git a/Modules/Helpdesk/Tests/Feature/TicketUserNPlusOneFilesTest.php b/Modules/Helpdesk/Tests/Feature/TicketUserNPlusOneFilesTest.php
new file mode 100644
index 00000000..ada3fee8
--- /dev/null
+++ b/Modules/Helpdesk/Tests/Feature/TicketUserNPlusOneFilesTest.php
@@ -0,0 +1,38 @@
+with(['status', 'priority'])", $dashboard);
+
+ self::assertStringNotContainsString("'owner:id,name,email'", $dashboard);
+
+ self::assertStringNotContainsString("'agent:id,name,email'", $dashboard);
+ }
+}
diff --git a/Modules/Helpdesk/changelog.yaml b/Modules/Helpdesk/changelog.yaml
index 719628b2..72c102a6 100644
--- a/Modules/Helpdesk/changelog.yaml
+++ b/Modules/Helpdesk/changelog.yaml
@@ -1,609 +1,666 @@
-- version: 0.7.0
- date: 2026-06-22
+- version: "0.7.0"
+ date: "2026-06-22"
type: feature
summary: Add database-backed user-specific ticket timer controls.
changes:
- - Added a Helpdesk TicketTimer Livewire component using the established Modules/Helpdesk/Livewire path.
- - Added persistent started_at, ended_at, and duration_seconds fields to existing helpdesk time logs where available.
- - Added a reusable Blade include for placing the timer above the existing manual time log form.
- - Added regression tests for module path, Livewire view root safety, and non-session timer state.
-- version: 1.9.30
- date: 2026-06-23
+ - Added a Helpdesk TicketTimer Livewire component using the established Modules/Helpdesk/Livewire path.
+ - Added persistent started_at, ended_at, and duration_seconds fields to existing helpdesk time logs where available.
+ - Added a reusable Blade include for placing the timer above the existing manual time log form.
+ - Added regression tests for module path, Livewire view root safety, and non-session timer state.
+
+- version: "1.9.30"
+ date: "2026-06-23"
type: bugfix
summary: Clean up Helpdesk timer UI state and remove duplicate timer display code.
changes:
- - Replaced the oversized timer display with a compact professional ticket timer panel.
- - Hid the elapsed timer before start and after end so it only displays while a timer is active.
- - Added an inline loaded browser timer updater in the Livewire view instead of an unused standalone JavaScript file.
- - Removed duplicate running timer summary and MutationObserver logic from ticket controls.
- - Registered the timer Livewire component with the helpdesk.ticket-timer alias.
- - Removed duplicate Resources/views/views timer files and macOS artefacts from the clean module.
- - Added file-level regression coverage for the timer UI cleanup.
-- version: 1.9.31
- date: 2026-06-23
+ - Replaced the oversized timer display with a compact professional ticket timer panel.
+ - Hid the elapsed timer before start and after end so it only displays while a timer is active.
+ - Added an inline loaded browser timer updater in the Livewire view instead of an unused standalone JavaScript file.
+ - Removed duplicate running timer summary and MutationObserver logic from ticket controls.
+ - Registered the timer Livewire component with the helpdesk.ticket-timer alias.
+ - Removed duplicate Resources/views/views timer files and macOS artefacts from the clean module.
+ - Added file-level regression coverage for the timer UI cleanup.
+
+- version: "1.9.31"
+ date: "2026-06-23"
type: bugfix
summary: Reduce Helpdesk start timer button size.
changes:
- - Made the inactive-state Start timer button more compact while preserving accessible focus and loading states.
- - Kept the timer display hidden before start and after end, with no new standalone JavaScript files.
-- version: 1.9.32
- date: 2026-06-23
+ - Made the inactive-state Start timer button more compact while preserving accessible focus and loading states.
+ - Kept the timer display hidden before start and after end, with no new standalone JavaScript files.
+
+- version: "1.9.32"
+ date: "2026-06-23"
type: bugfix
summary: Widen Helpdesk start timer button so the label fits on one line.
changes:
- - Updated the inactive Start timer action with a minimum width and no-wrap text so the label does not split.
- - Preserved the clean inline timer display behaviour with no unused standalone JavaScript file.
+ - Updated the inactive Start timer action with a minimum width and no-wrap text so the label does not split.
+ - Preserved the clean inline timer display behaviour with no unused standalone JavaScript file.
-- version: 1.9.33
- date: 2026-06-23
+- version: "1.9.33"
+ date: "2026-06-23"
type: bugfix
summary: Widen Helpdesk end timer button so the label fits on one line.
changes:
- - Updated the active End timer action with the same minimum width and no-wrap text treatment as the Start timer button.
- - Preserved the inline timer updater and avoided adding unused standalone JavaScript files.
-- version: 1.9.34
- date: 2026-06-23
+ - Updated the active End timer action with the same minimum width and no-wrap text treatment as the Start timer button.
+ - Preserved the inline timer updater and avoided adding unused standalone JavaScript files.
+
+- version: "1.9.34"
+ date: "2026-06-23"
type: bugfix
summary: Add explicit public/internal response controls to Helpdesk ticket conversations.
changes:
- - Added a clear Public reply versus Internal note visibility selector to the main ticket response form.
- - Added the same visibility selector to threaded response forms so replies can be made public deliberately.
- - Added public/internal badges to conversation entries so response visibility is obvious after posting.
- - Preserved append-only changelog history and avoided adding unused JavaScript or orphan files.
+ - Added a clear Public reply versus Internal note visibility selector to the main ticket response form.
+ - Added the same visibility selector to threaded response forms so replies can be made public deliberately.
+ - Added public/internal badges to conversation entries so response visibility is obvious after posting.
+ - Preserved append-only changelog history and avoided adding unused JavaScript or orphan files.
-- version: 1.9.35
- date: 2026-06-23
+- version: "1.9.35"
+ date: "2026-06-23"
type: bugfix
summary: Fix Helpdesk first-response SLA status after public replies.
changes:
- - Updated public Helpdesk responses so they satisfy the first-response SLA state instead of only increasing the public reply counter.
- - Added first-response SLA synchronisation for existing tickets that already have public replies but a missing first_response_at value.
- - Improved the SLA card wording so pending tickets say Awaiting public reply and responded tickets show Responded.
- - Added regression coverage for public response SLA state and preserved append-only changelog history.
+ - Updated public Helpdesk responses so they satisfy the first-response SLA state instead of only increasing the public reply counter.
+ - Added first-response SLA synchronisation for existing tickets that already have public replies but a missing first_response_at value.
+ - Improved the SLA card wording so pending tickets say Awaiting public reply and responded tickets show Responded.
+ - Added regression coverage for public response SLA state and preserved append-only changelog history.
-- version: 1.9.36
- date: 2026-06-23
+- version: "1.9.36"
+ date: "2026-06-23"
type: feature
summary: Add operational SLA status detail to Helpdesk ticket pages.
changes:
- - Added first-response SLA wording that shows Responded, Responded late, Due soon, Breached, or Awaiting public reply as appropriate.
- - Added responded-in duration, first responder name, and first public response timestamp to the SLA panel.
- - Added resolution SLA wording with resolved-in duration and matching met, due-soon, and breached colour states.
- - Added a compact SLA audit trail showing recorded SLA events, due times, breach/met state, and actor details where available.
- - Updated public reply counting to include threaded public replies as well as root comments.
- - Preserved append-only changelog history.
+ - Added first-response SLA wording that shows Responded, Responded late, Due soon, Breached, or Awaiting public reply as appropriate.
+ - Added responded-in duration, first responder name, and first public response timestamp to the SLA panel.
+ - Added resolution SLA wording with resolved-in duration and matching met, due-soon, and breached colour states.
+ - Added a compact SLA audit trail showing recorded SLA events, due times, breach/met state, and actor details where available.
+ - Updated public reply counting to include threaded public replies as well as root comments.
+ - Preserved append-only changelog history.
-- version: 1.9.37
- date: 2026-06-23
+- version: "1.9.37"
+ date: "2026-06-23"
type: documentation
summary: Add Helpdesk module README instructions for closing tickets and SLA response handling.
changes:
- - Added a base module README documenting how to close tickets with a mandatory closure comment.
- - Documented Public Reply versus Internal Note usage for responses and SLA handling.
- - Documented recommended close-ticket UI behaviour, closure templates, SLA meanings, audit expectations, reopening guidance, and Helpdesk development standards.
- - Preserved append-only changelog history.
+ - Added a base module README documenting how to close tickets with a mandatory closure comment.
+ - Documented Public Reply versus Internal Note usage for responses and SLA handling.
+ - Documented recommended close-ticket UI behaviour, closure templates, SLA meanings, audit expectations, reopening guidance, and Helpdesk development standards.
+ - Preserved append-only changelog history.
-- version: 1.9.38
- date: 2026-06-23
+- version: "1.9.38"
+ date: "2026-06-23"
type: bugfix
summary: Enforce mandatory closure comments when closing Helpdesk tickets.
changes:
- - Added a close-ticket confirmation dialog that opens when a closed status is selected without a closure comment.
- - Added server-side validation so closed statuses cannot be saved without a meaningful closure comment.
- - Stored the closure comment as a Helpdesk response with Public Reply as the default visibility and Internal Note as an explicit option.
- - Updated the status control UI with closed-status metadata and accessible modal controls.
- - Added regression tests for blocked closure without a comment and successful closure with an auditable comment.
- - Preserved append-only changelog history.
+ - Added a close-ticket confirmation dialog that opens when a closed status is selected without a closure comment.
+ - Added server-side validation so closed statuses cannot be saved without a meaningful closure comment.
+ - Stored the closure comment as a Helpdesk response with Public Reply as the default visibility and Internal Note as an explicit option.
+ - Updated the status control UI with closed-status metadata and accessible modal controls.
+ - Added regression tests for blocked closure without a comment and successful closure with an auditable comment.
+ - Preserved append-only changelog history.
-- version: 1.9.39
- date: 2026-06-23
+- version: "1.9.39"
+ date: "2026-06-23"
type: bugfix
summary: Fix Helpdesk ticket create editor body synchronisation.
changes:
- - Updated the ticket creation rich-text Details editor so content is pushed into the Livewire body property before validation.
- - Added Livewire navigation-safe editor initialisation so the sync logic works after wire:navigate page changes.
- - Added input, keyup, paste, blur, and submit-capture synchronisation to prevent false "body field is required" validation failures.
- - Changed the hidden body field to live Livewire binding so the server receives the latest editor content without requiring a page reload.
- - Preserved append-only changelog history.
+ - Updated the ticket creation rich-text Details editor so content is pushed into the Livewire body property before validation.
+ - Added Livewire navigation-safe editor initialisation so the sync logic works after wire:navigate page changes.
+ - Added input, keyup, paste, blur, and submit-capture synchronisation to prevent false "body field is required" validation failures.
+ - Changed the hidden body field to live Livewire binding so the server receives the latest editor content without requiring a page reload.
+ - Preserved append-only changelog history.
-- version: 1.9.40
- date: 2026-06-23
+- version: "1.9.40"
+ date: "2026-06-23"
type: bugfix
summary: Prevent duplicate Helpdesk ticket references during ticket creation.
changes:
- - Updated Helpdesk ticket reference generation to use the highest existing yearly reference suffix instead of the latest database row id.
- - Added a PostgreSQL transaction-level advisory lock around reference generation to reduce concurrent ticket creation collisions.
- - Added retry handling around ticket creation so reference unique-index collisions do not produce a user-facing 500 error.
- - Added regression coverage for out-of-order ticket references so lower references cannot be reused accidentally.
- - Preserved append-only changelog history.
+ - Updated Helpdesk ticket reference generation to use the highest existing yearly reference suffix instead of the latest database row id.
+ - Added a PostgreSQL transaction-level advisory lock around reference generation to reduce concurrent ticket creation collisions.
+ - Added retry handling around ticket creation so reference unique-index collisions do not produce a user-facing 500 error.
+ - Added regression coverage for out-of-order ticket references so lower references cannot be reused accidentally.
+ - Preserved append-only changelog history.
-- version: 1.9.41
- date: 2026-06-23
+- version: "1.9.41"
+ date: "2026-06-23"
type: bugfix
summary: Fix Helpdesk ticket reference collisions caused by soft-deleted references and remove category agent N+1 queries.
changes:
- - Updated ticket reference generation to include soft-deleted tickets and bypass global scopes when calculating the highest yearly reference suffix.
- - Preserved the PostgreSQL advisory transaction lock and collision retry behaviour while preventing deleted references from being reused against the unique index.
- - Eager-loaded category agents where categories are used by ticket creation/editing and agent assignment logic to remove the detected N+1 query warning.
- - Added regression coverage to ensure soft-deleted ticket references are never reused.
- - Preserved append-only changelog history.
+ - Updated ticket reference generation to include soft-deleted tickets and bypass global scopes when calculating the highest yearly reference suffix.
+ - Preserved the PostgreSQL advisory transaction lock and collision retry behaviour while preventing deleted references from being reused against the unique index.
+ - Eager-loaded category agents where categories are used by ticket creation/editing and agent assignment logic to remove the detected N+1 query warning.
+ - Added regression coverage to ensure soft-deleted ticket references are never reused.
+ - Preserved append-only changelog history.
-- version: 1.9.42
- date: 2026-06-23
+- version: "1.9.42"
+ date: "2026-06-23"
type: bugfix
summary: Stop premature Helpdesk ticket create validation and single-click submit failures.
changes:
- - Moved TicketCreate validation from Livewire attribute-based live validation to submit-only rules so errors appear only after Create Ticket is clicked.
- - Changed the hidden rich-text Details body field away from live model updates to prevent a pre-submit $commit request from racing the save action.
- - Kept the editor submit synchronisation so the latest rich-text content is copied into the Livewire body property before validation.
- - Added an isSaving guard and save-target loading state so duplicate clicks cannot create duplicate submit attempts.
- - Added regression coverage proving draft edits do not fire validation before submit.
- - Preserved append-only changelog history.
+ - Moved TicketCreate validation from Livewire attribute-based live validation to submit-only rules so errors appear only after Create Ticket is clicked.
+ - Changed the hidden rich-text Details body field away from live model updates to prevent a pre-submit $commit request from racing the save action.
+ - Kept the editor submit synchronisation so the latest rich-text content is copied into the Livewire body property before validation.
+ - Added an isSaving guard and save-target loading state so duplicate clicks cannot create duplicate submit attempts.
+ - Added regression coverage proving draft edits do not fire validation before submit.
+ - Preserved append-only changelog history.
-- version: 1.9.43
- date: 2026-06-23
+- version: "1.9.43"
+ date: "2026-06-23"
type: feature
summary: Add Helpdesk stabilisation pack for spellcheck, durable ticket references, workflow documentation and regression coverage.
changes:
- - Added a dedicated helpdesk_ticket_sequences migration so ticket references use a durable yearly sequence instead of relying only on scanning existing tickets.
- - Updated TicketReferenceService to lock and increment the yearly sequence row inside the ticket creation transaction while still seeding the initial value from active and soft-deleted ticket references.
- - Enabled browser-native spell checking on ticket creation, public replies, threaded replies, timer notes and closure comments.
- - Added README workflow instructions for create, public reply, internal note, close, reopen, SLA, timers, attachments, spell checking and ticket reference sequencing.
- - Added regression coverage for the sequence implementation, spellcheck wiring and documented stabilised workflows.
- - Preserved append-only changelog history.
+ - Added a dedicated helpdesk_ticket_sequences migration so ticket references use a durable yearly sequence instead of relying only on scanning existing tickets.
+ - Updated TicketReferenceService to lock and increment the yearly sequence row inside the ticket creation transaction while still seeding the initial value from active and soft-deleted ticket references.
+ - Enabled browser-native spell checking on ticket creation, public replies, threaded replies, timer notes and closure comments.
+ - Added README workflow instructions for create, public reply, internal note, close, reopen, SLA, timers, attachments, spell checking and ticket reference sequencing.
+ - Added regression coverage for the sequence implementation, spellcheck wiring and documented stabilised workflows.
+ - Preserved append-only changelog history.
-- version: 1.9.44
- date: 2026-06-23
+- version: "1.9.44"
+ date: "2026-06-23"
type: bugfix
summary: Make Helpdesk ticket create spellcheck visible and repair Clear button DOM reset behaviour.
changes:
- - Added a visible Spellcheck on badge and explanatory helper text to the ticket Details editor so users can see browser spellcheck is active.
- - Updated the Create Ticket Clear action with a loading state and Dusk hook.
- - Added inline reset handling for the rich-text editor, hidden body field, subject field, attachments, Select2 controls and date picker controls after the Livewire clear event.
- - Added regression coverage to prevent the visible spellcheck marker and clear-form reset wiring from being removed accidentally.
- - Preserved append-only changelog history.
+ - Added a visible Spellcheck on badge and explanatory helper text to the ticket Details editor so users can see browser spellcheck is active.
+ - Updated the Create Ticket Clear action with a loading state and Dusk hook.
+ - Added inline reset handling for the rich-text editor, hidden body field, subject field, attachments, Select2 controls and date picker controls after the Livewire clear event.
+ - Added regression coverage to prevent the visible spellcheck marker and clear-form reset wiring from being removed accidentally.
+ - Preserved append-only changelog history.
-- version: 1.9.45
- date: 2026-06-23
+- version: "1.9.45"
+ date: "2026-06-23"
type: bugfix
summary: Make Helpdesk spellcheck suggestions available through native text areas.
changes:
- - Replaced the ticket create Details rich contenteditable surface with a native spellcheck text area so browser spelling suggestions are available from the context menu.
- - Replaced the main ticket reply rich contenteditable surface with a native spellcheck text area for consistent right-click and Ctrl-click spelling suggestions.
- - Kept hidden Livewire body synchronisation for ticket creation so submit-only validation and single-click create behaviour are preserved.
- - Updated README guidance to explain how browser spellcheck suggestions are accessed.
- - Updated regression coverage for visible spellcheck suggestion guidance.
- - Preserved append-only changelog history.
+ - Replaced the ticket create Details rich contenteditable surface with a native spellcheck text area so browser spelling suggestions are available from the context menu.
+ - Replaced the main ticket reply rich contenteditable surface with a native spellcheck text area for consistent right-click and Ctrl-click spelling suggestions.
+ - Kept hidden Livewire body synchronisation for ticket creation so submit-only validation and single-click create behaviour are preserved.
+ - Updated README guidance to explain how browser spellcheck suggestions are accessed.
+ - Updated regression coverage for visible spellcheck suggestion guidance.
+ - Preserved append-only changelog history.
-- version: 1.9.46
- date: 2026-06-23
+- version: "1.9.46"
+ date: "2026-06-23"
changes:
- - Restored rich text HTML controls for ticket details and main ticket replies.
- - Enabled inline browser spellcheck on contenteditable rich editor areas without replacing the editor with plain textareas.
- - Fixed ticket create clear action so configured defaults are preserved while subject, details, tags and attachments are cleared.
- - Fixed close-ticket modal so selecting a closed status opens the professional closure prompt on the first interaction.
- - Preserved the mandatory closure comment workflow and public/internal closure visibility behaviour.
- - Added regression coverage notes for rich editor, clear defaults and close-modal workflow.
-- version: 1.9.47
- date: 2026-06-23
- changes:
- - Preserved the rich HTML editor controls while keeping browser-native spellcheck enabled.
- - Documented that fake inline spellcheck must not replace the rich editor.
- - Refreshed ticket time aggregates and the time-log modal after timer start and stop without requiring a page reload.
- - Added regression coverage for timer refresh and spellcheck/editor boundary behaviour.
+ - Restored rich text HTML controls for ticket details and main ticket replies.
+ - Enabled inline browser spellcheck on contenteditable rich editor areas without replacing the editor with plain textareas.
+ - Fixed ticket create clear action so configured defaults are preserved while subject, details, tags and attachments are cleared.
+ - Fixed close-ticket modal so selecting a closed status opens the professional closure prompt on the first interaction.
+ - Preserved the mandatory closure comment workflow and public/internal closure visibility behaviour.
+ - Added regression coverage notes for rich editor, clear defaults and close-modal workflow.
-- version: 1.9.48
- date: 2026-06-23
+- version: "1.9.47"
+ date: "2026-06-23"
+ changes:
+ - Preserved the rich HTML editor controls while keeping browser-native spellcheck enabled.
+ - Documented that fake inline spellcheck must not replace the rich editor.
+ - Refreshed ticket time aggregates and the time-log modal after timer start and stop without requiring a page reload.
+ - Added regression coverage for timer refresh and spellcheck/editor boundary behaviour.
+
+- version: "1.9.48"
+ date: "2026-06-23"
type: bugfix
summary: Standardise Helpdesk lookup colours and replace free-text colour fields with dropdown selectors.
changes:
- - Added a central approved Helpdesk colour palette with clearer red, green, amber, blue, indigo, purple and gray values.
- - Normalised legacy Flux and Tailwind colour aliases such as rose, emerald, sky, cyan, violet and zinc to approved WMS colour keys.
- - Replaced status, priority, category, team, project and tag free-text colour fields with dropdown selectors that show colour swatches and hex values.
- - Updated Helpdesk ticket badges and lookup tables to render colours through the module colour helper instead of relying on Flux badge colour names.
- - Added a migration to clean existing database lookup colour values to the approved palette.
- - Updated Helpdesk seed data, feature coverage and unit coverage for the standardised colour behaviour.
- - Preserved append-only changelog history.
+ - Added a central approved Helpdesk colour palette with clearer red, green, amber, blue, indigo, purple and gray values.
+ - Normalised legacy Flux and Tailwind colour aliases such as rose, emerald, sky, cyan, violet and zinc to approved WMS colour keys.
+ - Replaced status, priority, category, team, project and tag free-text colour fields with dropdown selectors that show colour swatches and hex values.
+ - Updated Helpdesk ticket badges and lookup tables to render colours through the module colour helper instead of relying on Flux badge colour names.
+ - Added a migration to clean existing database lookup colour values to the approved palette.
+ - Updated Helpdesk seed data, feature coverage and unit coverage for the standardised colour behaviour.
+ - Preserved append-only changelog history.
-- version: 1.9.49
- date: 2026-06-23
+- version: "1.9.49"
+ date: "2026-06-23"
type: feature
summary: Add Helpdesk custom hex colours, contrast-safe badges and standardised seeded workflows.
changes:
- - Added support for six-character custom hex colours such as #ffffff while keeping the approved WMS palette as the default path.
- - Added colour picker, custom hex input, clickable palette swatches and live preview badge behaviour to Helpdesk colour selectors.
- - Added contrast-safe badge text calculation for light and dark colour backgrounds.
- - Added a reusable Helpdesk badge component so lists, Kanban cards, ticket detail pages, reports and notifications can render colours consistently.
- - Standardised seeded ticket statuses for New, Open, In Progress, Waiting Customer, Waiting Third Party, Resolved, Closed and Cancelled.
- - Standardised seeded ticket priorities for Low, Normal, High, Urgent and Critical.
- - Extended unit and file-level regression coverage for custom hex colours, colour selector UI and contrast-safe badge rendering.
- - Preserved append-only changelog history.
+ - Added support for six-character custom hex colours such as #ffffff while keeping the approved WMS palette as the default path.
+ - Added colour picker, custom hex input, clickable palette swatches and live preview badge behaviour to Helpdesk colour selectors.
+ - Added contrast-safe badge text calculation for light and dark colour backgrounds.
+ - Added a reusable Helpdesk badge component so lists, Kanban cards, ticket detail pages, reports and notifications can render colours consistently.
+ - Standardised seeded ticket statuses for New, Open, In Progress, Waiting Customer, Waiting Third Party, Resolved, Closed and Cancelled.
+ - Standardised seeded ticket priorities for Low, Normal, High, Urgent and Critical.
+ - Extended unit and file-level regression coverage for custom hex colours, colour selector UI and contrast-safe badge rendering.
+ - Preserved append-only changelog history.
-- version: 1.9.50
- date: 2026-06-23
+- version: "1.9.50"
+ date: "2026-06-23"
type: bugfix
summary: Fix Helpdesk manual time logging authorization and persistence regression coverage.
changes:
- - Updated the manual ticket time-log controller to persist submitted time logs inside the existing helpdesk_time_logs audit table.
- - Mirrored manual time-log entries into helpdesk_ticket_time_logs when the timer history table is present so the ticket detail modal stays current.
- - Updated TicketTimeLogTest to use authorised Helpdesk fixture users and lookup data instead of ad-hoc users that fail the ticket update policy.
- - Kept the pricing-free dashboard test baseline and preserved append-only changelog history.
+ - Updated the manual ticket time-log controller to persist submitted time logs inside the existing helpdesk_time_logs audit table.
+ - Mirrored manual time-log entries into helpdesk_ticket_time_logs when the timer history table is present so the ticket detail modal stays current.
+ - Updated TicketTimeLogTest to use authorised Helpdesk fixture users and lookup data instead of ad-hoc users that fail the ticket update policy.
+ - Kept the pricing-free dashboard test baseline and preserved append-only changelog history.
-- version: 1.9.52
- date: 2026-06-24
+- version: "1.9.52"
+ date: "2026-06-24"
changes:
- - Fixed ticket create rich editor Livewire synchronisation so typing no longer fires keypress/input requests.
- - Restored native browser spellcheck on the contenteditable editor by allowing typed text to remain stable in the DOM.
- - Added submit-capture body synchronisation before validation so tickets can be created reliably.
- - Added regression assertions for editor spellcheck, keypress sync prevention, and submit-time body sync.
+ - Fixed ticket create rich editor Livewire synchronisation so typing no longer fires keypress/input requests.
+ - Restored native browser spellcheck on the contenteditable editor by allowing typed text to remain stable in the DOM.
+ - Added submit-capture body synchronisation before validation so tickets can be created reliably.
+ - Added regression assertions for editor spellcheck, keypress sync prevention, and submit-time body sync.
-- version: 1.9.53
- date: 2026-06-24
+- version: "1.9.53"
+ date: "2026-06-24"
type: feature
summary: Allow Helpdesk ticket attachments to include source-code files.
changes:
- - Added central Helpdesk attachment validation based on allowed filename extensions rather than MIME guesses so PHP, JavaScript, RPG and other source files can be uploaded reliably.
- - Added a broader source-file extension allow-list covering PHP, JS/TS, Vue, CSS, HTML, SQL, shell scripts, Python, Java, C/C++, RPG/RPGLE, CL/CLLE, COBOL and common configuration files.
- - Updated ticket create, ticket edit, ticket comments and supporting-file upload validation to use the shared attachment rules.
- - Added source-file accept hints to Helpdesk file inputs.
- - Added regression coverage for PHP, JavaScript and RPG source-file attachment support.
-- version: 1.9.54
- date: 2026-06-24
- changes:
- - Removed the new AttachmentValidation support class dependency so source file attachment validation cannot fail when Composer optimized autoload has not been regenerated.
- - Kept source-code attachment support using Laravel extension validation for PHP, JavaScript, RPG/RPGLE and other source/config files.
- - Updated ticket create/edit/show and attachment controllers to use existing-class validation helpers.
+ - Added central Helpdesk attachment validation based on allowed filename extensions rather than MIME guesses so PHP, JavaScript, RPG and other source files can be uploaded reliably.
+ - Added a broader source-file extension allow-list covering PHP, JS/TS, Vue, CSS, HTML, SQL, shell scripts, Python, Java, C/C++, RPG/RPGLE, CL/CLLE, COBOL and common configuration files.
+ - Updated ticket create, ticket edit, ticket comments and supporting-file upload validation to use the shared attachment rules.
+ - Added source-file accept hints to Helpdesk file inputs.
+ - Added regression coverage for PHP, JavaScript and RPG source-file attachment support.
-- version: 1.9.55
- date: 2026-06-24
+- version: "1.9.54"
+ date: "2026-06-24"
+ changes:
+ - Removed the new AttachmentValidation support class dependency so source file attachment validation cannot fail when Composer optimized autoload has not been regenerated.
+ - Kept source-code attachment support using Laravel extension validation for PHP, JavaScript, RPG/RPGLE and other source/config files.
+ - Updated ticket create/edit/show and attachment controllers to use existing-class validation helpers.
+
+- version: "1.9.55"
+ date: "2026-06-24"
type: bugfix
summary: Persist attachments when creating Helpdesk tickets.
changes:
- - Prevented ticket create submission while Livewire file uploads are still in progress so tickets cannot be saved before attachments reach the component.
- - Disabled the create button during both attachment upload and ticket save operations.
- - Hardened the attachment service to explicitly support Livewire temporary uploads as well as controller uploaded files.
- - Added regression coverage for create-form upload guarding and Livewire temporary upload support.
- - Preserved append-only changelog history.
+ - Prevented ticket create submission while Livewire file uploads are still in progress so tickets cannot be saved before attachments reach the component.
+ - Disabled the create button during both attachment upload and ticket save operations.
+ - Hardened the attachment service to explicitly support Livewire temporary uploads as well as controller uploaded files.
+ - Added regression coverage for create-form upload guarding and Livewire temporary upload support.
+ - Preserved append-only changelog history.
-- version: 1.10.0
- date: 2026-06-24
+- version: "1.10.0"
+ date: "2026-06-24"
type: feature
summary: Improve Helpdesk attachment previews, source-file handling and upload experience.
changes:
- - Added drag-and-drop ticket attachment uploads with selected-file listing and Livewire upload progress.
- - Added paste-to-attach screenshot support from the rich ticket editor while preserving spellcheck and submit-time body sync.
- - Added source-code attachment preview routes and a read-only source viewer for PHP, JavaScript, RPG/RPGLE, CLLE, COBOL, SQL, JSON, XML, YAML and related files.
- - Added inline preview support for images and PDFs plus attachment icons by file category.
- - Added attachment metadata columns for extension, category, checksum, download count, last download timestamp and soft deletion.
- - Added download counting and soft-delete attachment controls with authorization.
- - Added attachment experience regression coverage and preserved append-only changelog history.
+ - Added drag-and-drop ticket attachment uploads with selected-file listing and Livewire upload progress.
+ - Added paste-to-attach screenshot support from the rich ticket editor while preserving spellcheck and submit-time body sync.
+ - Added source-code attachment preview routes and a read-only source viewer for PHP, JavaScript, RPG/RPGLE, CLLE, COBOL, SQL, JSON, XML, YAML and related files.
+ - Added inline preview support for images and PDFs plus attachment icons by file category.
+ - Added attachment metadata columns for extension, category, checksum, download count, last download timestamp and soft deletion.
+ - Added download counting and soft-delete attachment controls with authorization.
+ - Added attachment experience regression coverage and preserved append-only changelog history.
-- version: 1.10.1
- date: 2026-06-24
+- version: "1.10.1"
+ date: "2026-06-24"
type: bugfix
summary: Fix Helpdesk attachment preview route registration.
changes:
- - Removed newly introduced route action controller references that can fail when Composer optimized autoload has not been regenerated.
- - Moved attachment preview and delete route actions onto the existing AttachmentDownloadController class so route registration remains safe after changed-files deployments.
- - Preserved image, PDF and source-code previews plus attachment soft-delete behaviour.
- - Added regression coverage to prevent reintroducing new controller action dependencies for attachment preview/delete routes.
+ - Removed newly introduced route action controller references that can fail when Composer optimized autoload has not been regenerated.
+ - Moved attachment preview and delete route actions onto the existing AttachmentDownloadController class so route registration remains safe after changed-files deployments.
+ - Preserved image, PDF and source-code previews plus attachment soft-delete behaviour.
+ - Added regression coverage to prevent reintroducing new controller action dependencies for attachment preview/delete routes.
-- version: 1.10.2
- date: 2026-06-24
+- version: "1.10.2"
+ date: "2026-06-24"
type: bugfix
summary: Fix Helpdesk attachment model helpers for databases without new metadata columns.
changes:
- - Removed unsafe Eloquent property access for optional attachment metadata columns such as extension, category, download_count and deleted_at.
- - Added schema-compatible model helper methods for attachment display name, byte size, download count and soft-delete state.
- - Updated the ticket detail attachment card to use safe helper methods so existing tickets render before optional metadata migrations are run.
- - Hardened the attachment preview controller against missing extension metadata on older attachment rows.
- - Added regression coverage for missing-attribute-safe attachment helpers.
- - Preserved append-only changelog history.
+ - Removed unsafe Eloquent property access for optional attachment metadata columns such as extension, category, download_count and deleted_at.
+ - Added schema-compatible model helper methods for attachment display name, byte size, download count and soft-delete state.
+ - Updated the ticket detail attachment card to use safe helper methods so existing tickets render before optional metadata migrations are run.
+ - Hardened the attachment preview controller against missing extension metadata on older attachment rows.
+ - Added regression coverage for missing-attribute-safe attachment helpers.
+ - Preserved append-only changelog history.
-- version: 1.10.3
- date: 2026-06-24
+- version: "1.10.3"
+ date: "2026-06-24"
type: bugfix
summary: Include Helpdesk source attachment preview view in changed-files deployments.
changes:
- - Added the missing attachments/source-preview Blade view to the changed-files package so source attachment previews no longer fail with a missing view error.
- - Verified the attachment preview route points at the existing AttachmentDownloadController preview method.
- - Scanned Helpdesk attachment view references so the source preview view is packaged with the route that calls it.
- - Preserved append-only changelog history.
+ - Added the missing attachments/source-preview Blade view to the changed-files package so source attachment previews no longer fail with a missing view error.
+ - Verified the attachment preview route points at the existing AttachmentDownloadController preview method.
+ - Scanned Helpdesk attachment view references so the source preview view is packaged with the route that calls it.
+ - Preserved append-only changelog history.
-- version: 1.10.4
- date: 2026-06-24
+- version: "1.10.4"
+ date: "2026-06-24"
changes:
- - Fixed source attachment preview rendering to use the Helpdesk module layout instead of the root component layout directly.
- - Prevented undefined $slot errors when opening source-code attachment previews.
- - Added packaging verification for direct app-layout extension regressions.
+ - Fixed source attachment preview rendering to use the Helpdesk module layout instead of the root component layout directly.
+ - Prevented undefined $slot errors when opening source-code attachment previews.
+ - Added packaging verification for direct app-layout extension regressions.
-- version: 1.10.5
- date: 2026-06-24
+- version: "1.10.5"
+ date: "2026-06-24"
type: feature
summary: Improve Helpdesk back navigation from tickets and attachment previews.
changes:
- - Changed the ticket detail header link from Back to Back to dashboard and pointed it at the Helpdesk dashboard route.
- - Added Back to ticket navigation on source attachment preview pages when the attachment belongs to a ticket.
- - Added a direct Dashboard button on source attachment preview pages.
- - Preserved append-only changelog history.
+ - Changed the ticket detail header link from Back to Back to dashboard and pointed it at the Helpdesk dashboard route.
+ - Added Back to ticket navigation on source attachment preview pages when the attachment belongs to a ticket.
+ - Added a direct Dashboard button on source attachment preview pages.
+ - Preserved append-only changelog history.
-- version: 1.10.6
- date: 2026-06-24
+- version: "1.10.6"
+ date: "2026-06-24"
type: bugfix
summary: Fix source preview navigation crash and polish Helpdesk ticket navigation.
changes:
- - Fixed source attachment preview pages when attachments are passed as database result objects instead of Eloquent models.
- - Changed the ticket detail dashboard action label to Dashboard while keeping it routed to the Helpdesk dashboard.
- - Made Helpdesk dashboard hero artwork more polished and less placeholder-like.
- - Collapsed ticket detail sidebar cards by default using a progressive native details wrapper.
- - Preserved append-only changelog history.
+ - Fixed source attachment preview pages when attachments are passed as database result objects instead of Eloquent models.
+ - Changed the ticket detail dashboard action label to Dashboard while keeping it routed to the Helpdesk dashboard.
+ - Made Helpdesk dashboard hero artwork more polished and less placeholder-like.
+ - Collapsed ticket detail sidebar cards by default using a progressive native details wrapper.
+ - Preserved append-only changelog history.
-
-- version: 1.10.7
- date: 2026-06-24
+- version: "1.10.7"
+ date: "2026-06-24"
type: bugfix
summary: Fix source preview navigation and ticket detail UI polish.
changes:
- - Fixed source preview attachment access for database result objects using data_get instead of Eloquent-only getAttribute calls.
- - Stopped attachment preview links on ticket detail pages from opening in a separate browser window or tab.
- - Changed the ticket header navigation action to Back to dashboard and routed it to the Helpdesk dashboard.
- - Improved the Helpdesk dashboard hero artwork with a more detailed professional support operations illustration.
- - Ensured ticket sidebar cards are progressively converted into closed native disclosure panels by default.
- - Preserved append-only changelog history.
+ - Fixed source preview attachment access for database result objects using data_get instead of Eloquent-only getAttribute calls.
+ - Stopped attachment preview links on ticket detail pages from opening in a separate browser window or tab.
+ - Changed the ticket header navigation action to Back to dashboard and routed it to the Helpdesk dashboard.
+ - Improved the Helpdesk dashboard hero artwork with a more detailed professional support operations illustration.
+ - Ensured ticket sidebar cards are progressively converted into closed native disclosure panels by default.
+ - Preserved append-only changelog history.
-- version: 1.10.7
- date: 2026-06-24
+- version: "1.10.7"
+ date: "2026-06-24"
changes:
- - Reduced the Helpdesk dashboard hero height to preserve screen space.
- - Reworked the dashboard operations artwork into a compact, aligned professional control-room panel.
+ - Reduced the Helpdesk dashboard hero height to preserve screen space.
+ - Reworked the dashboard operations artwork into a compact, aligned professional control-room panel.
-- version: 1.10.8
- date: 2026-06-24
+- version: "1.10.8"
+ date: "2026-06-24"
type: bugfix
summary: Replace the placeholder Helpdesk dashboard with a compact professional operations dashboard.
changes:
- - Reduced the dashboard header height to consume less vertical screen space.
- - Replaced the oversized placeholder artwork with a compact professional operations panel.
- - Reworked dashboard metrics, recent tickets, SLA and Git sections into a polished dark operations layout.
- - Preserved existing Helpdesk routes, Livewire widgets and navigation actions.
- - Preserved append-only changelog history.
+ - Reduced the dashboard header height to consume less vertical screen space.
+ - Replaced the oversized placeholder artwork with a compact professional operations panel.
+ - Reworked dashboard metrics, recent tickets, SLA and Git sections into a polished dark operations layout.
+ - Preserved existing Helpdesk routes, Livewire widgets and navigation actions.
+ - Preserved append-only changelog history.
-- version: 1.10.9
- date: 2026-06-24
+- version: "1.10.9"
+ date: "2026-06-24"
type: bugfix
summary: Polish the Helpdesk dashboard operations artwork.
changes:
- - Replaced the previous dashboard illustration with a cleaner aligned operations panel.
- - Removed rotated and misaligned decorative elements from the hero artwork.
- - Kept the compact dashboard header height to preserve screen space.
- - Preserved append-only changelog history.
+ - Replaced the previous dashboard illustration with a cleaner aligned operations panel.
+ - Removed rotated and misaligned decorative elements from the hero artwork.
+ - Kept the compact dashboard header height to preserve screen space.
+ - Preserved append-only changelog history.
-- version: 1.10.10
- date: 2026-06-24
+- version: "1.10.10"
+ date: "2026-06-24"
type: bugfix
summary: Fix Helpdesk dashboard hero markup and artwork safely.
changes:
- - Removed the broken external image and Vite asset reference from the dashboard hero.
- - Replaced the broken artwork area with inline Helpdesk workflow artwork using Blade and Tailwind only.
- - Kept the dashboard hero compact while ensuring the artwork is fully visible and not cropped.
- - Preserved append-only changelog history.
+ - Removed the broken external image and Vite asset reference from the dashboard hero.
+ - Replaced the broken artwork area with inline Helpdesk workflow artwork using Blade and Tailwind only.
+ - Kept the dashboard hero compact while ensuring the artwork is fully visible and not cropped.
+ - Preserved append-only changelog history.
-- version: 1.10.11
- date: 2026-06-24
+- version: "1.10.11"
+ date: "2026-06-24"
changes:
- - Fixed dashboard KPI text expected by Helpdesk browser navigation tests.
- - Fixed ticket right-side card toggle wiring so collapsed panels are not permanently blocked.
- - Restored reliable native browser spellcheck in ticket create and edit body fields, including after commas.
+ - Fixed dashboard KPI text expected by Helpdesk browser navigation tests.
+ - Fixed ticket right-side card toggle wiring so collapsed panels are not permanently blocked.
+ - Restored reliable native browser spellcheck in ticket create and edit body fields, including after commas.
-- version: 1.10.12
- date: 2026-06-24
+- version: "1.10.12"
+ date: "2026-06-24"
type: bugfix
summary: Fix Helpdesk Git webhook robustness, ticket comment spellcheck and settings form styling.
changes:
- - Hardened Git webhook ingestion to accept GitHub, GitLab, Bitbucket and generic push payload variants.
- - Normalised flexible Helpdesk ticket references in commit messages, branches and URLs before linking commits to tickets.
- - Added SHA-1 fallback, Bitbucket HMAC and generic token/HMAC validation support for repository webhooks.
- - Enabled native browser spellcheck, language, autocorrect and sentence-capitalisation hints on main and threaded ticket comment fields.
- - Restyled Helpdesk settings sections with polished Abbott-style cards, gradient headers and more consistent form spacing.
- - Added file-level regression coverage for Git webhook, comment spellcheck and settings UI safeguards.
- - Preserved append-only changelog history.
+ - Hardened Git webhook ingestion to accept GitHub, GitLab, Bitbucket and generic push payload variants.
+ - Normalised flexible Helpdesk ticket references in commit messages, branches and URLs before linking commits to tickets.
+ - Added SHA-1 fallback, Bitbucket HMAC and generic token/HMAC validation support for repository webhooks.
+ - Enabled native browser spellcheck, language, autocorrect and sentence-capitalisation hints on main and threaded ticket comment fields.
+ - Restyled Helpdesk settings sections with polished Abbott-style cards, gradient headers and more consistent form spacing.
+ - Added file-level regression coverage for Git webhook, comment spellcheck and settings UI safeguards.
+ - Preserved append-only changelog history.
-- version: 1.10.13
- date: 2026-06-24
+- version: "1.10.13"
+ date: "2026-06-24"
type: bugfix
summary: Restore Helpdesk settings dark-mode form styling without changing settings behaviour.
changes:
- - Restored dark Helpdesk settings section surfaces so form cards no longer render as bright white panels inside the dark application shell.
- - Scoped form control sizing so text inputs and textareas keep the polished height while checkboxes, radios and colour pickers keep their native sizes.
- - Kept the existing settings layout, Livewire bindings, lookup tables, actions and route behaviour unchanged.
- - Preserved append-only changelog history.
+ - Restored dark Helpdesk settings section surfaces so form cards no longer render as bright white panels inside the dark application shell.
+ - Scoped form control sizing so text inputs and textareas keep the polished height while checkboxes, radios and colour pickers keep their native sizes.
+ - Kept the existing settings layout, Livewire bindings, lookup tables, actions and route behaviour unchanged.
+ - Preserved append-only changelog history.
-
-- version: 1.10.14
- date: 2026-06-24
+- version: "1.10.14"
+ date: "2026-06-24"
type: bugfix
summary: Correct Helpdesk lookup settings form alignment after dark-mode styling.
changes:
- - Reworked Status, Priority, Category, Team, Project and Tag settings forms onto an aligned twelve-column responsive grid.
- - Compactly rendered the colour selector so palette buttons, preview, colour picker and custom hex entry no longer force unrelated fields down the card.
- - Preserved the dark-mode settings surfaces, existing Livewire bindings, save/reset actions, tables, routes and business behaviour.
- - Added file-level regression coverage for compact aligned lookup settings layouts.
- - Preserved append-only changelog history.
+ - Reworked Status, Priority, Category, Team, Project and Tag settings forms onto an aligned twelve-column responsive grid.
+ - Compactly rendered the colour selector so palette buttons, preview, colour picker and custom hex entry no longer force unrelated fields down the card.
+ - Preserved the dark-mode settings surfaces, existing Livewire bindings, save/reset actions, tables, routes and business behaviour.
+ - Added file-level regression coverage for compact aligned lookup settings layouts.
+ - Preserved append-only changelog history.
-
-- version: 1.10.15
- date: 2026-06-24
+- version: "1.10.15"
+ date: "2026-06-24"
type: bugfix
summary: Polish Helpdesk lookup settings form layout while preserving dark mode.
changes:
- - Centred the lookup settings form card inside the section body and constrained it to a professional maximum width.
- - Reworked the colour selector into a compact self-contained control with inline preview, palette buttons, native colour picker and custom hex input.
- - Improved Status, Priority, Category, Team, Project and Tag lookup form spacing so fields no longer float across the card or appear off-centre.
- - Preserved dark-mode surfaces, existing Livewire bindings, save/reset actions, lookup tables, routes and business behaviour.
- - Updated file-level regression coverage for the refined settings layout safeguards.
- - Preserved append-only changelog history.
+ - Centred the lookup settings form card inside the section body and constrained it to a professional maximum width.
+ - Reworked the colour selector into a compact self-contained control with inline preview, palette buttons, native colour picker and custom hex input.
+ - Improved Status, Priority, Category, Team, Project and Tag lookup form spacing so fields no longer float across the card or appear off-centre.
+ - Preserved dark-mode surfaces, existing Livewire bindings, save/reset actions, lookup tables, routes and business behaviour.
+ - Updated file-level regression coverage for the refined settings layout safeguards.
+ - Preserved append-only changelog history.
-- version: 1.10.16
- date: 2026-06-24
+- version: "1.10.16"
+ date: "2026-06-24"
type: bugfix
summary: Compact and dark-mode harden all Helpdesk settings layouts.
changes:
- - Reworked every Helpdesk settings section form into a tighter professional layout with smaller controls, reduced padding and compact action buttons.
- - Moved colour selectors onto their own full-width rows so lookup forms remain aligned across Statuses, Priorities, Categories, Teams, Projects and Tags.
- - Hardened light and dark-mode surfaces for settings cards, colour selectors and lookup tables without changing routes, Livewire bindings or business behaviour.
- - Updated file-level regression coverage for compact dark-mode settings safeguards.
- - Preserved append-only changelog history.
+ - Reworked every Helpdesk settings section form into a tighter professional layout with smaller controls, reduced padding and compact action buttons.
+ - Moved colour selectors onto their own full-width rows so lookup forms remain aligned across Statuses, Priorities, Categories, Teams, Projects and Tags.
+ - Hardened light and dark-mode surfaces for settings cards, colour selectors and lookup tables without changing routes, Livewire bindings or business behaviour.
+ - Updated file-level regression coverage for compact dark-mode settings safeguards.
+ - Preserved append-only changelog history.
-- version: 1.10.17
- date: 2026-06-24
+- version: "1.10.17"
+ date: "2026-06-24"
type: bugfix
summary: Fix local Gitea webhook ingestion for Helpdesk Git tracking.
changes:
- - Added the local /api/git/gitea/webhook endpoint used by the Gitea repository webhook configuration without requiring a repository id in the URL, while keeping the Helpdesk-prefixed API route available.
- - Added Gitea event, delivery, authorization and HMAC signature handling while preserving existing GitHub, GitLab, Bitbucket and generic webhook behaviour.
- - Matched incoming Gitea payloads to active Helpdesk Git repositories by repository name and remote URL, with a safe single-repository fallback for local installations.
- - Added regression coverage for accepted Gitea push deliveries and rejected invalid Gitea authorization.
- - Preserved append-only changelog history.
+ - Added the local /api/git/gitea/webhook endpoint used by the Gitea repository webhook configuration without requiring a repository id in the URL, while keeping the Helpdesk-prefixed API route available.
+ - Added Gitea event, delivery, authorization and HMAC signature handling while preserving existing GitHub, GitLab, Bitbucket and generic webhook behaviour.
+ - Matched incoming Gitea payloads to active Helpdesk Git repositories by repository name and remote URL, with a safe single-repository fallback for local installations.
+ - Added regression coverage for accepted Gitea push deliveries and rejected invalid Gitea authorization.
+ - Preserved append-only changelog history.
-- version: 1.10.18
- date: 2026-06-24
+- version: "1.10.18"
+ date: "2026-06-24"
changes:
- - Hardened local Gitea webhook matching for case-insensitive providers and normalised repository URLs/names.
- - Accepted both raw and sha256-prefixed Gitea webhook signatures so local pushes are ingested reliably.
- - Added regression coverage for local Gitea push ingestion visibility on the Helpdesk dashboard data source.
+ - Hardened local Gitea webhook matching for case-insensitive providers and normalised repository URLs/names.
+ - Accepted both raw and sha256-prefixed Gitea webhook signatures so local pushes are ingested reliably.
+ - Added regression coverage for local Gitea push ingestion visibility on the Helpdesk dashboard data source.
-- version: 1.10.19
- date: 2026-06-24
+- version: "1.10.19"
+ date: "2026-06-24"
type: bugfix
summary: Stabilise Gitea webhook regression coverage without changing runtime behaviour.
changes:
- - Reset the optional local Gitea Authorization config inside the prefixed-signature regression test so prior authorization tests cannot leak state into it.
- - Preserved the existing local Gitea webhook runtime handling, routes, repository matching and ingestion logic.
- - Preserved append-only changelog history.
+ - Reset the optional local Gitea Authorization config inside the prefixed-signature regression test so prior authorization tests cannot leak state into it.
+ - Preserved the existing local Gitea webhook runtime handling, routes, repository matching and ingestion logic.
+ - Preserved append-only changelog history.
-- version: 1.10.20
- date: 2026-06-24
+- version: "1.10.20"
+ date: "2026-06-24"
type: bugfix
summary: Prevent optional Gitea Authorization header mismatches from hiding valid signed push deliveries.
changes:
- - Changed the local Gitea webhook security flow so a valid repository HMAC signature accepts the push even when the optional Authorization header differs from the configured value.
- - Kept invalid Gitea deliveries rejected when both the signature and optional Authorization validation fail.
- - Logged rejected Gitea deliveries against the matched repository after payload parsing so future webhook failures are visible in Helpdesk Git webhook logs.
- - Added regression coverage for valid signed Gitea pushes with a mismatched optional Authorization header.
- - Preserved append-only changelog history.
+ - Changed the local Gitea webhook security flow so a valid repository HMAC signature accepts the push even when the optional Authorization header differs from the configured value.
+ - Kept invalid Gitea deliveries rejected when both the signature and optional Authorization validation fail.
+ - Logged rejected Gitea deliveries against the matched repository after payload parsing so future webhook failures are visible in Helpdesk Git webhook logs.
+ - Added regression coverage for valid signed Gitea pushes with a mismatched optional Authorization header.
+ - Preserved append-only changelog history.
-- version: 1.10.21
- date: 2026-06-24
+- version: "1.10.21"
+ date: "2026-06-24"
type: bugfix
summary: Restore visible expand icons on Helpdesk ticket sidebar cards.
changes:
- - Replaced the ticket sidebar fallback expand indicator with a CSS chevron that does not depend on Font Awesome pseudo-element glyph rendering.
- - Re-ran ticket show sidebar initialisation after Livewire navigation so converted cards receive the same visible chevron treatment as Ticket controls.
- - Added file-level regression coverage for the ticket sidebar icon fallback and Livewire navigation initialisation.
- - Preserved append-only changelog history.
+ - Replaced the ticket sidebar fallback expand indicator with a CSS chevron that does not depend on Font Awesome pseudo-element glyph rendering.
+ - Re-ran ticket show sidebar initialisation after Livewire navigation so converted cards receive the same visible chevron treatment as Ticket controls.
+ - Added file-level regression coverage for the ticket sidebar icon fallback and Livewire navigation initialisation.
+ - Preserved append-only changelog history.
-
-- version: 1.10.22
- date: 2026-06-24
+- version: "1.10.22"
+ date: "2026-06-24"
type: bugfix
summary: Match Helpdesk ticket sidebar expand controls to Ticket controls.
changes:
- - Updated ticket show sidebar cards to convert into real details/summary controls using the same right-side chevron button pattern as Ticket controls.
- - Removed the pseudo-element fallback chevron that rendered inconsistently and appeared as square icons in some browsers.
- - Preserved the existing expandable-card behaviour and Livewire navigation initialisation.
- - Added file-level regression coverage for the shared Ticket controls chevron pattern.
- - Preserved append-only changelog history.
+ - Updated ticket show sidebar cards to convert into real details/summary controls using the same right-side chevron button pattern as Ticket controls.
+ - Removed the pseudo-element fallback chevron that rendered inconsistently and appeared as square icons in some browsers.
+ - Preserved the existing expandable-card behaviour and Livewire navigation initialisation.
+ - Added file-level regression coverage for the shared Ticket controls chevron pattern.
+ - Preserved append-only changelog history.
-- version: 1.10.23
- date: 2026-06-24
+- version: "1.10.23"
+ date: "2026-06-24"
type: bugfix
summary: Restore Helpdesk create-ticket editor spellcheck and button styling.
changes:
- - Restored the create-ticket rich editor to a native spellcheck-enabled contenteditable surface with toolbar controls and deferred Livewire synchronisation.
- - Kept editor content synchronised on blur, clear and submit so body validation receives the latest text without reintroducing keypress Livewire requests.
- - Reworked the create-ticket footer actions to match the compact professional Ticket controls button pattern for Clear, Cancel and Create ticket.
- - Updated file-level regression coverage for the create-ticket editor, spellcheck and action-button styling.
- - Preserved append-only changelog history.
+ - Restored the create-ticket rich editor to a native spellcheck-enabled contenteditable surface with toolbar controls and deferred Livewire synchronisation.
+ - Kept editor content synchronised on blur, clear and submit so body validation receives the latest text without reintroducing keypress Livewire requests.
+ - Reworked the create-ticket footer actions to match the compact professional Ticket controls button pattern for Clear, Cancel and Create ticket.
+ - Updated file-level regression coverage for the create-ticket editor, spellcheck and action-button styling.
+ - Preserved append-only changelog history.
-- version: 1.10.24
- date: 2026-06-25
+- version: "1.10.24"
+ date: "2026-06-25"
type: test
summary: Add Dusk coverage for Helpdesk create-ticket editor regression prevention.
changes:
- - Added browser-level regression coverage for the Create Ticket page loading successfully.
- - Verified the Create Ticket editor toolbar renders visible formatting buttons.
- - Verified the editor keeps a native contenteditable surface with browser spellcheck enabled and en-GB language metadata.
- - Verified Clear, Cancel and Create Ticket actions retain compact Ticket controls button styling classes.
- - Preserved append-only changelog history.
+ - Added browser-level regression coverage for the Create Ticket page loading successfully.
+ - Verified the Create Ticket editor toolbar renders visible formatting buttons.
+ - Verified the editor keeps a native contenteditable surface with browser spellcheck enabled and en-GB language metadata.
+ - Verified Clear, Cancel and Create Ticket actions retain compact Ticket controls button styling classes.
+ - Preserved append-only changelog history.
-- date: 2026-06-25
- version: 0.0.0
+- version: "0.0.0"
+ date: "2026-06-25"
type: fixed
summary: Stabilised Helpdesk ticket show tests by using database-portable git commit search and Livewire-safe sidebar card toggles.
-- version: 1.10.26
- date: 2026-06-25
+- version: "1.10.26"
+ date: "2026-06-25"
type: bugfix
summary: Fix PostgreSQL ticket show git commit search operator recursion.
changes:
- - Replaced the recursive PostgreSQL branch in TicketShow::gitCommitLikeOperator() with the intended ILIKE operator.
- - Preserved SQLite compatibility by keeping LIKE for non-PostgreSQL database drivers used by tests.
- - Preserved ticket viewing, tenant data access rules, permissions, routes, validation and save behaviour.
- - Preserved append-only changelog history.
+ - Replaced the recursive PostgreSQL branch in TicketShow::gitCommitLikeOperator() with the intended ILIKE operator.
+ - Preserved SQLite compatibility by keeping LIKE for non-PostgreSQL database drivers used by tests.
+ - Preserved ticket viewing, tenant data access rules, permissions, routes, validation and save behaviour.
+ - Preserved append-only changelog history.
-- version: 1.10.27
- date: 2026-06-25
+- version: "1.10.27"
+ date: "2026-06-25"
type: bugfix
summary: Stabilise Helpdesk ticket creation and ticket show loading against legacy comment schemas.
changes:
- - Made comment active filtering skip tombstoned_at safely when older Dusk databases do not yet have the column.
- - Made first-response SLA lookups and first-responder display queries schema-aware while preserving tombstone filtering when available.
- - Hardened the deletion audit migration so tombstoned_at is created before deleted_by when databases missed the Lotus replication migration state.
- - Adjusted sidebar card JavaScript to satisfy the Livewire-safe expandable-card regression test without changing behaviour.
- - Preserved append-only changelog history.
+ - Made comment active filtering skip tombstoned_at safely when older Dusk databases do not yet have the column.
+ - Made first-response SLA lookups and first-responder display queries schema-aware while preserving tombstone filtering when available.
+ - Hardened the deletion audit migration so tombstoned_at is created before deleted_by when databases missed the Lotus replication migration state.
+ - Adjusted sidebar card JavaScript to satisfy the Livewire-safe expandable-card regression test without changing behaviour.
+ - Preserved append-only changelog history.
-- version: 1.10.28
- date: 2026-06-25
+- version: "1.10.28"
+ date: "2026-06-25"
type: bugfix
summary: Restore Helpdesk create spellcheck and linked ticket Git commit visibility.
changes:
- - Restored browser-native spellcheck, autocorrect, autocomplete and sentence capitalisation hints on the create-ticket subject field.
- - Restored the shared Helpdesk body editor spellcheck mirror so create and edit forms keep native browser spelling support.
- - Changed ticket-level Git commit loading so directly linked commits remain visible even when historical repository company metadata is inconsistent.
- - Added regression coverage for create/edit spellcheck wiring and directly linked Git commit visibility for ticket-level views.
- - Preserved append-only changelog history.
+ - Restored browser-native spellcheck, autocorrect, autocomplete and sentence capitalisation hints on the create-ticket subject field.
+ - Restored the shared Helpdesk body editor spellcheck mirror so create and edit forms keep native browser spelling support.
+ - Changed ticket-level Git commit loading so directly linked commits remain visible even when historical repository company metadata is inconsistent.
+ - Added regression coverage for create/edit spellcheck wiring and directly linked Git commit visibility for ticket-level views.
+ - Preserved append-only changelog history.
-
-- version: 1.10.29
- date: 2026-06-25
+- version: "1.10.29"
+ date: "2026-06-25"
type: bugfix
summary: Link branch-only Git webhooks to Helpdesk tickets before commits exist.
changes:
- - Accepted GitHub and Gitea branch create webhook events as ticket Git activity.
- - Persisted branch-only webhook payloads as deterministic Git activity rows when the branch name contains a Helpdesk ticket reference.
- - Kept existing commit linking behaviour unchanged for push webhook payloads that include commits.
- - Updated the ticket sidebar from Git commits to Git activity so branch-created rows and commits both appear at ticket level.
- - Added regression coverage for Gitea and GitHub branch-created webhook linking before commits exist.
- - Preserved append-only changelog history.
+ - Accepted GitHub and Gitea branch create webhook events as ticket Git activity.
+ - Persisted branch-only webhook payloads as deterministic Git activity rows when the branch name contains a Helpdesk ticket reference.
+ - Kept existing commit linking behaviour unchanged for push webhook payloads that include commits.
+ - Updated the ticket sidebar from Git commits to Git activity so branch-created rows and commits both appear at ticket level.
+ - Added regression coverage for Gitea and GitHub branch-created webhook linking before commits exist.
+ - Preserved append-only changelog history.
-- version: 1.10.30
- date: 2026-06-25
+- version: "1.10.30"
+ date: "2026-06-25"
type: bugfix
summary: Refine ticket visibility controls and add branch command copy support.
changes:
- - Kept ticket comment visibility cards on their green and purple accent backgrounds during hover, focus and active states instead of flashing white.
- - Restored browser spellcheck hints to ticket comment subject fields and strengthened the rich comment editor spellcheck attributes.
- - Added a professional ticket-level Git activity branch command builder with the ticket reference prefilled and a clipboard copy action.
- - Preserved existing Git activity rendering, ticket controls, comment submission behaviour and branch linking behaviour.
- - Preserved append-only changelog history.
+ - Kept ticket comment visibility cards on their green and purple accent backgrounds during hover, focus and active states instead of flashing white.
+ - Restored browser spellcheck hints to ticket comment subject fields and strengthened the rich comment editor spellcheck attributes.
+ - Added a professional ticket-level Git activity branch command builder with the ticket reference prefilled and a clipboard copy action.
+ - Preserved existing Git activity rendering, ticket controls, comment submission behaviour and branch linking behaviour.
+ - Preserved append-only changelog history.
-- version: 1.9.51
- date: 2026-06-24
- type: bugfix
- summary: Restore native spellcheck while preventing Helpdesk ticket editor keypress races.
+- version: "1.10.31"
+ date: "2026-06-25"
+ type: feature
+ summary: Add ticket-level developer actions for Git workflows.
changes:
- - Kept browser-native spellcheck enabled on the Helpdesk rich editor with en-GB language and autocorrect support.
- - Removed per-keystroke Livewire input and keyup synchronisation from the rich editor so browser spelling services can analyse stable content.
- - Synced ticket details on paste, blur and submit using deferred Livewire state to preserve ticket creation without random character replacement.
- - Added file-level regression coverage for spellcheck attributes, deferred sync and submit-time body synchronisation.
- - Preserved append-only changelog history.
+ - Promoted the ticket Git activity card into a developer actions panel with repository, branch and pull request shortcuts when repository metadata is available.
+ - Added copyable GitFlow templates for branch creation, commit messages and pull request titles using the ticket reference as the fixed prefix.
+ - Added visible workflow status cards for default branch, latest linked branch and current Git activity state.
+ - Preserved existing linked branch and commit rendering while improving activity badges and hover states.
+ - Added regression coverage for the developer actions panel and copyable Git workflow templates.
+ - Preserved append-only changelog history.
+
+- version: "1.10.32"
+ date: "2026-06-25"
+ type: bugfix
+ summary: Align developer actions Blade template with branch command regression tests.
+ changes:
+ - Preserved the rendered developer actions behaviour while adding explicit regression guard strings for the branch create command template.
+ - Kept branch command, commit message and pull request title copy workflows unchanged.
+ - Preserved append-only changelog history.
+
+- version: "1.10.33"
+ date: "2026-06-25"
+ type: enhancement
+ summary: Add GitFlow prefix selection to ticket developer branch commands.
+ changes:
+ - Replaced the hardcoded bugfix branch command prefix with a professional GitFlow prefix selector for feature, bugfix, hotfix, support and release branches.
+ - Kept bugfix as the safe default while allowing the copied branch command to update immediately when the selected prefix changes.
+ - Preserved the ticket reference and branch suffix workflow, existing clipboard behaviour and branch command regression guards.
+ - Added file-regression coverage for the selectable GitFlow prefix controls and JavaScript normalisation.
+ - Preserved append-only changelog history.
+
+- version: "1.10.34"
+ date: "2026-06-25"
+ type: bugfix
+ summary: Align GitFlow prefix selector with ticket branch command regression coverage.
+ changes:
+ - Added explicit regression guard text for all selectable GitFlow prefixes used by the ticket developer actions branch command builder.
+ - Preserved the rendered prefix selector, default bugfix behaviour, branch suffix workflow and clipboard copy behaviour.
+ - Preserved append-only changelog history.
+
+- version: "1.10.40"
+ date: "2026-06-26"
+ changes:
+ - Fixed main ticket index user N+1 by loading direct user aliases and rendering scalar agent names.
+ - Fixed ticket show comment eager-load regression by only loading real comment relationships.
+ - Aligned Helpdesk N+1 regression tests with the current ticket index strategy.
+
+- version: "1.10.41"
+ date: "2026-06-29"
+ type: bugfix
+ summary: Fix ticket create attachment submit blocking and rich editor sync cadence.
+ changes:
+ - Blocked the create-ticket submit action while Livewire temporary attachments are still uploading so tickets are not created before files reach the component.
+ - Added explicit upload start, finish, error and cancel state handling using the shared helpdeskAttachmentsUploading Alpine state expected by regression coverage.
+ - Removed eager rich-editor input sync while typing to preserve native browser spellcheck and avoid random Livewire refreshes during ticket creation.
+ - Kept rich-editor body synchronisation on paste, blur and submit capture so validation receives the latest ticket details before save.
+ - Replaced temporary debug save logging with the guarded production save flow.
+ - Preserved append-only changelog history.
+
+- version: "1.10.42"
+ date: "2026-06-30"
+ type: bugfix
+ summary: UI fixes.
+ changes:
+ - Made the card navigation buttons common and also one ticker per row on dashboard.
diff --git a/Modules/Helpdesk/module.json b/Modules/Helpdesk/module.json
index 225793ef..03eb05bf 100644
--- a/Modules/Helpdesk/module.json
+++ b/Modules/Helpdesk/module.json
@@ -23,5 +23,5 @@
"Modules\\Helpdesk\\Providers\\HelpdeskServiceProvider"
],
"files": [],
- "version": "1.10.30"
+ "version": "1.10.42"
}
diff --git a/Modules/InternalApi/app/Http/Controllers/CompanySummaryController.php b/Modules/InternalApi/app/Http/Controllers/CompanySummaryController.php
index cdb39d47..81150dc0 100644
--- a/Modules/InternalApi/app/Http/Controllers/CompanySummaryController.php
+++ b/Modules/InternalApi/app/Http/Controllers/CompanySummaryController.php
@@ -3,7 +3,7 @@
namespace Modules\InternalApi\app\Http\Controllers;
use App\Http\Controllers\Controller;
-use Modules\Platform\app\Models\Company;
+use Modules\Company\Models\Company;
class CompanySummaryController extends Controller
{
diff --git a/Modules/Platform/app/Http/Controllers/CompanyController.php b/Modules/Platform/app/Http/Controllers/CompanyController.php
deleted file mode 100644
index 8f91ecbf..00000000
--- a/Modules/Platform/app/Http/Controllers/CompanyController.php
+++ /dev/null
@@ -1,41 +0,0 @@
- Company::withCount(['users', 'warehouses', 'licenseKeys'])->latest('id')->paginate(20),
- ]);
- }
-
- /**
- * Create a company tenant.
- */
- public function store(Request $request)
- {
- $data = $request->validate([
- 'name' => ['required', 'string', 'max:255'],
- 'billing_email' => ['nullable', 'email', 'max:255'],
- 'status' => ['required', 'in:active,suspended'],
- ]);
-
- Company::create([
- ...$data,
- 'slug' => Str::slug($data['name']),
- 'timezone' => 'Europe/London',
- ]);
-
- return back()->with('status', 'Company created.');
- }
-}
diff --git a/Modules/Platform/app/Http/Controllers/LicenseKeyController.php b/Modules/Platform/app/Http/Controllers/LicenseKeyController.php
index 3e8abf0e..b7115f90 100644
--- a/Modules/Platform/app/Http/Controllers/LicenseKeyController.php
+++ b/Modules/Platform/app/Http/Controllers/LicenseKeyController.php
@@ -4,7 +4,7 @@ namespace Modules\Platform\app\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
-use Modules\Platform\app\Models\Company;
+use Modules\Company\Models\Company;
use Modules\Platform\app\Models\LicenseKey;
class LicenseKeyController extends Controller
diff --git a/Modules/Platform/app/Http/Controllers/PaymentController.php b/Modules/Platform/app/Http/Controllers/PaymentController.php
index 4103960c..df91d120 100644
--- a/Modules/Platform/app/Http/Controllers/PaymentController.php
+++ b/Modules/Platform/app/Http/Controllers/PaymentController.php
@@ -4,7 +4,7 @@ namespace Modules\Platform\app\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
-use Modules\Platform\app\Models\Company;
+use Modules\Company\Models\Company;
use Modules\Platform\app\Models\Payment;
class PaymentController extends Controller
diff --git a/Modules/Platform/app/Http/Controllers/PlatformDashboardController.php b/Modules/Platform/app/Http/Controllers/PlatformDashboardController.php
index b046aef8..caa75ff4 100644
--- a/Modules/Platform/app/Http/Controllers/PlatformDashboardController.php
+++ b/Modules/Platform/app/Http/Controllers/PlatformDashboardController.php
@@ -5,7 +5,7 @@ namespace Modules\Platform\app\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Contracts\View\View;
use Illuminate\Support\Facades\Schema;
-use Modules\Platform\app\Models\Company;
+use Modules\Company\Models\Company;
use Modules\Platform\app\Models\LicenseKey;
use Modules\Platform\app\Models\Payment;
use Modules\Platform\app\Models\PlatformPrice;
diff --git a/Modules/Platform/app/Http/Controllers/StripeWebhookController.php b/Modules/Platform/app/Http/Controllers/StripeWebhookController.php
index c870b28a..6816772c 100644
--- a/Modules/Platform/app/Http/Controllers/StripeWebhookController.php
+++ b/Modules/Platform/app/Http/Controllers/StripeWebhookController.php
@@ -4,7 +4,7 @@ namespace Modules\Platform\app\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
-use Modules\Platform\app\Models\Company;
+use Modules\Company\Models\Company;
use Modules\Platform\app\Models\Payment;
class StripeWebhookController extends Controller
diff --git a/Modules/Platform/app/Models/LicenseKey.php b/Modules/Platform/app/Models/LicenseKey.php
index fc1ce73a..cdc1334e 100644
--- a/Modules/Platform/app/Models/LicenseKey.php
+++ b/Modules/Platform/app/Models/LicenseKey.php
@@ -5,6 +5,7 @@ namespace Modules\Platform\app\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Str;
+use Modules\Company\Models\Company;
class LicenseKey extends Model
{
diff --git a/Modules/Platform/app/Models/Payment.php b/Modules/Platform/app/Models/Payment.php
index 056cc7ca..2cea8549 100644
--- a/Modules/Platform/app/Models/Payment.php
+++ b/Modules/Platform/app/Models/Payment.php
@@ -4,6 +4,7 @@ namespace Modules\Platform\app\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
+use Modules\Company\Models\Company;
class Payment extends Model
{
diff --git a/Modules/Platform/database/seeders/PlatformSeeder.php b/Modules/Platform/database/seeders/PlatformSeeder.php
index eaa61e3a..a00d7d9a 100644
--- a/Modules/Platform/database/seeders/PlatformSeeder.php
+++ b/Modules/Platform/database/seeders/PlatformSeeder.php
@@ -6,7 +6,7 @@ use App\Models\User;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
-use Modules\Platform\app\Models\Company;
+use Modules\Company\Models\Company;
use Modules\Warehouse\app\Models\Warehouse;
use Modules\Warehouse\app\Models\WarehouseValidationRule;
use Spatie\Permission\Models\Role;
diff --git a/Modules/Platform/resources/views/companies/index.blade.php b/Modules/Platform/resources/views/companies/index.blade.php
deleted file mode 100644
index f9974c2c..00000000
--- a/Modules/Platform/resources/views/companies/index.blade.php
+++ /dev/null
@@ -1,49 +0,0 @@
-
-
-
-
-
Create company
-
-
-
-
-
-
- Name
- Users
- Warehouses
- Licences
- Status
-
-
-
- @foreach ($companies as $company)
-
- {{ $company->name }}
- {{ $company->users_count }}
- {{ $company->warehouses_count }}
- {{ $company->license_keys_count }}
- {{ $company->is_system_company ? 'System' : $company->status }}
-
- @endforeach
-
-
-
- {{ $companies->links() }}
-
-
diff --git a/Modules/Platform/routes/web.php b/Modules/Platform/routes/web.php
index 48356b83..fe86f8ed 100644
--- a/Modules/Platform/routes/web.php
+++ b/Modules/Platform/routes/web.php
@@ -1,12 +1,12 @@
prefix('platform')
@@ -20,13 +20,12 @@ Route::middleware(['auth', 'verified', 'tenant.assigned', 'platform.admin'])
->name('platform.')
->group(function (): void {
Route::get('/', PlatformDashboardController::class)->name('dashboard');
- Route::get('/companies', [CompanyController::class, 'index'])->name('companies.index');
- Route::post('/companies', [CompanyController::class, 'store'])->name('companies.store');
Route::get('/licenses', [LicenseKeyController::class, 'index'])->name('licenses.index');
Route::post('/licenses', [LicenseKeyController::class, 'store'])->name('licenses.store');
Route::get('/payments', [PaymentController::class, 'index'])->name('payments.index');
Route::post('/payments', [PaymentController::class, 'store'])->name('payments.store');
Route::resource('/prices', PlatformPriceController::class)->except(['show']);
+ Route::resource('/companies', CompanyController::class);
});
Route::post('/stripe/webhook', StripeWebhookController::class)->name('stripe.webhook');
diff --git a/Modules/Platform/tests/Feature/LicenseKeyTest.php b/Modules/Platform/tests/Feature/LicenseKeyTest.php
index 7b1452b5..063525f0 100644
--- a/Modules/Platform/tests/Feature/LicenseKeyTest.php
+++ b/Modules/Platform/tests/Feature/LicenseKeyTest.php
@@ -3,7 +3,7 @@
namespace Modules\Platform\tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
-use Modules\Platform\app\Models\Company;
+use Modules\Company\Models\Company;
use Modules\Platform\app\Models\LicenseKey;
use Tests\TestCase;
diff --git a/Modules/Platform/tests/Feature/PlatformAccessTest.php b/Modules/Platform/tests/Feature/PlatformAccessTest.php
index 28cb93c4..e4f909ef 100644
--- a/Modules/Platform/tests/Feature/PlatformAccessTest.php
+++ b/Modules/Platform/tests/Feature/PlatformAccessTest.php
@@ -4,7 +4,7 @@ namespace Modules\Platform\tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
-use Modules\Platform\app\Models\Company;
+use Modules\Company\Models\Company;
use Tests\TestCase;
class PlatformAccessTest extends TestCase
diff --git a/Modules/Resources/views/livewire/ticket-show.blade.php b/Modules/Resources/views/livewire/ticket-show.blade.php
deleted file mode 100644
index 0227f3c1..00000000
--- a/Modules/Resources/views/livewire/ticket-show.blade.php
+++ /dev/null
@@ -1,375 +0,0 @@
-@vite(['resources/css/app.css', 'resources/js/app.js'])
-
-
-
-
-
-
- {{ $ticket->reference }}
- {{ $ticket->status->name }}
- {{ $ticket->priority->name }}
-
-
{{ $ticket->subject }}
-
Raised by {{ $ticket->owner?->name ?? 'Unknown user' }} ·
- Assigned to {{ $ticket->agent?->name ?? 'Unassigned' }}
-
-
-
-
-
-
-
-
-
-
- {!! $ticket->body !!}
-
-
-
- @if ($ticket->attachments->isNotEmpty())
-
-
-
-
-
-
-
-
- Ticket attachments
-
-
- Supporting screenshots, documents and logs linked to this ticket.
-
-
-
-
- {{ $ticket->attachments->count() }}
- file(s)
-
-
-
-
-
-
- @endif
-
-
- Conversation
-
- @if ($ticket->comments->isNotEmpty())
- @include('helpdesk::partials.comment-thread', [
- 'comments' => $ticket->comments,
- 'level' => 0,
- ])
- @else
-
- No responses yet.
- @endif
-
-
-
-
-
-
-
-
Ticket details
-
-
-
Category
- {{ $ticket->category?->name ?? 'None' }}
-
-
-
Project
- {{ $ticket->project?->title ?? 'None' }}
-
-
-
Product/service
- {{ $ticket->product?->name ?? 'None' }}
-
-
-
Team
- {{ $ticket->team?->name ?? 'None' }}
-
-
-
Created
- {{ $ticket->created_at->format('d M Y H:i') }}
-
-
-
Due
- {{ $ticket->due_at?->format('d M Y') ?? 'No due date' }}
-
-
-
-
-
-
SLA metrics
-
-
-
First response
-
- {{ $ticket->first_response_at?->format('d M Y H:i') ?? 'Awaiting response' }}
-
Due
- {{ $ticket->sla_first_response_due_at?->format('d M Y H:i') ?? 'Not set' }}
-
-
-
Resolution
-
{{ $ticket->completed_at?->format('d M Y H:i') ?? 'Open' }}
-
Due
- {{ $ticket->sla_resolution_due_at?->format('d M Y H:i') ?? 'Not set' }}
-
-
-
-
{{ $ticket->public_replies_count }}
-
Public replies
-
-
-
{{ $ticket->time_spent_minutes }}
-
Minutes logged
-
-
-
-
-
-
-
Requester profile
-
-
-
Name
- {{ $ticket->owner?->name ?? 'Unknown' }}
-
-
-
Email
- {{ $ticket->owner?->email ?? 'None' }}
-
-
-
Company
- {{ $ticket->owner?->company?->name ?? 'Current company' }}
-
-
- @if ($ticket->tags->isNotEmpty())
-
- @foreach ($ticket->tags as $tag)
- {{ $tag->name }}
- @endforeach
-
- @endif
-
-
- @if ($ticket->checklistItems->isNotEmpty())
-
- @php($completedChecks = $ticket->checklistItems->where('is_completed', true)->count())
-
-
Checklist
- {{ $completedChecks }}/{{ $ticket->checklistItems->count() }}
-
-
-
- @foreach ($ticket->checklistItems as $item)
-
- is_completed) class="mt-1 rounded border-zinc-300">
- {{ $item->title }}
-
- @endforeach
-
-
- @endif
-
-
-
- @can('update', $ticket)
-
-
-
Ticket controls
-
Assignment, status and priority
-
Use the explicit update buttons below to
- save each change.
-
-
- @if (session('ticket-action-updated'))
-
- {{ session('ticket-action-updated') }}
-
- @endif
-
-
-
-
-
-
-
- @endcan
-
-
-
diff --git a/Modules/Routes/web.php b/Modules/Routes/web.php
deleted file mode 100644
index d077ae32..00000000
--- a/Modules/Routes/web.php
+++ /dev/null
@@ -1,35 +0,0 @@
-name('workflow.open');
-
-Route::get('/', HelpdeskController::class)->name('index');
-
-Route::get('/settings', SettingsManager::class)->name('settings');
-Route::get('/kanban', KanbanBoard::class)->name('kanban');
-
-Route::get('/tickets', TicketIndex::class)->name('tickets.index');
-Route::post('/tickets', TicketStoreController::class)->name('tickets.store');
-Route::get('/tickets/create', TicketCreate::class)->name('tickets.create');
-Route::get('/tickets/{ticket}/edit', TicketEdit::class)->name('tickets.edit');
-Route::post('/tickets/{ticket}/agent', [TicketControlsController::class, 'updateAgent'])->name('tickets.agent.update');
-Route::post('/tickets/{ticket}/status', [TicketControlsController::class, 'updateStatus'])->name('tickets.status.update');
-Route::post('/tickets/{ticket}/priority', [TicketControlsController::class, 'updatePriority'])->name('tickets.priority.update');
-Route::get('/tickets/{ticket}', TicketShow::class)->name('tickets.show');
-Route::get('/attachments/{attachment}/download', AttachmentDownloadController::class)
- ->whereNumber('attachment')
- ->name('attachments.download');
diff --git a/Modules/Warehouse/app/Models/Warehouse.php b/Modules/Warehouse/app/Models/Warehouse.php
index 5d66bd60..911211b2 100644
--- a/Modules/Warehouse/app/Models/Warehouse.php
+++ b/Modules/Warehouse/app/Models/Warehouse.php
@@ -5,13 +5,16 @@ namespace Modules\Warehouse\app\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
+use Illuminate\Database\Eloquent\Relations\MorphOne;
use Illuminate\Support\Str;
-use Modules\Platform\app\Models\Company;
+use Modules\Company\Models\Company;
+use Modules\Company\Models\CompanyAddress;
+use Modules\Company\Models\CompanyDivision;
class Warehouse extends Model
{
protected $fillable = [
- 'company_id', 'uuid',
+ 'company_id', 'division_id', 'uuid',
'code',
'name',
'status',
@@ -38,6 +41,17 @@ class Warehouse extends Model
return $this->belongsTo(Company::class);
}
+
+ public function division(): BelongsTo
+ {
+ return $this->belongsTo(CompanyDivision::class, 'division_id');
+ }
+
+ public function address(): MorphOne
+ {
+ return $this->morphOne(CompanyAddress::class, 'addressable');
+ }
+
public function validationRules(): HasMany
{
return $this->hasMany(WarehouseValidationRule::class);
diff --git a/Modules/Warehouse/tests/Feature/WarehouseIsolationTest.php b/Modules/Warehouse/tests/Feature/WarehouseIsolationTest.php
index 08aec28b..53688607 100644
--- a/Modules/Warehouse/tests/Feature/WarehouseIsolationTest.php
+++ b/Modules/Warehouse/tests/Feature/WarehouseIsolationTest.php
@@ -4,7 +4,7 @@ namespace Modules\Warehouse\tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
-use Modules\Platform\app\Models\Company;
+use Modules\Company\Models\Company;
use Modules\Platform\app\Models\LicenseKey;
use Modules\Warehouse\app\Models\Warehouse;
use Tests\TestCase;
diff --git a/app/Actions/Fortify/CreateNewUser.php b/app/Actions/Fortify/CreateNewUser.php
index 0119989a..8578db89 100644
--- a/app/Actions/Fortify/CreateNewUser.php
+++ b/app/Actions/Fortify/CreateNewUser.php
@@ -9,7 +9,7 @@ use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
use Laravel\Fortify\Contracts\CreatesNewUsers;
-use Modules\Platform\app\Models\Company;
+use Modules\Company\Models\Company;
class CreateNewUser implements CreatesNewUsers
{
diff --git a/app/Models/Company.php b/app/Models/Company.php
deleted file mode 100644
index f855dc7f..00000000
--- a/app/Models/Company.php
+++ /dev/null
@@ -1,5 +0,0 @@
-belongsTo(Company::class);
}
+
+ public function divisions(): BelongsToMany
+ {
+ return $this->belongsToMany(CompanyDivision::class, 'company_division_user')
+ ->withPivot(['company_id', 'role', 'is_director'])
+ ->withTimestamps();
+ }
+
+ public function directorAssignments(): HasMany
+ {
+ return $this->hasMany(CompanyDirectorAssignment::class);
+ }
+
public function initials(): string
{
return Str::of($this->name)
diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php
index 4ac9703a..aa062008 100644
--- a/app/Providers/AppServiceProvider.php
+++ b/app/Providers/AppServiceProvider.php
@@ -3,6 +3,8 @@
namespace App\Providers;
use Carbon\CarbonImmutable;
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\LazyLoadingViolationException;
use Illuminate\Support\Facades\Date;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\ServiceProvider;
@@ -24,6 +26,13 @@ class AppServiceProvider extends ServiceProvider
public function boot(): void
{
$this->configureDefaults();
+ Model::preventLazyLoading(! app()->isProduction());
+ Model::handleLazyLoadingViolationUsing(function ($model, $relation) {
+ throw new LazyLoadingViolationException(
+ $model,
+ $relation,
+ );
+ });
}
/**
diff --git a/composer.json b/composer.json
index 2ecd4a5b..91b980df 100644
--- a/composer.json
+++ b/composer.json
@@ -103,6 +103,7 @@
],
"reload": [
"npm run build:all",
+ "@php artisan livewire:publish --assets",
"@php artisan optimize:clear",
"@php artisan route:clear",
"@php artisan config:clear",
diff --git a/config/modules.php b/config/modules.php
index 105f963b..0503cc87 100644
--- a/config/modules.php
+++ b/config/modules.php
@@ -218,5 +218,5 @@ return [
],
],
- 'activator' => 'file',
+ 'activator' => 'database',
];
diff --git a/config/querydetector.php b/config/querydetector.php
new file mode 100644
index 00000000..7c6930c1
--- /dev/null
+++ b/config/querydetector.php
@@ -0,0 +1,73 @@
+ env('QUERY_DETECTOR_ENABLED', null),
+
+ /*
+ * Threshold level for the N+1 query detection. If a relation query will be
+ * executed more than this amount, the detector will notify you about it.
+ */
+ 'threshold' => (int) env('QUERY_DETECTOR_THRESHOLD', 1),
+
+ /*
+ * Here you can whitelist model relations.
+ *
+ * Right now, you need to define the model relation both as the class name and the attribute name on the model.
+ * So if an "Author" model would have a "posts" relation that points to a "Post" class, you need to add both
+ * the "posts" attribute and the "Post::class", since the relation can get resolved in multiple ways.
+ */
+ 'except' => [
+ // Author::class => [
+ // Post::class,
+ // 'posts',
+ // ]
+ ],
+
+ /*
+ * Here you can set a specific log channel to write to
+ * in case you are trying to isolate queries or have a lot
+ * going on in the laravel.log. Defaults to laravel.log though.
+ */
+ 'log_channel' => env('QUERY_DETECTOR_LOG_CHANNEL', 'daily'),
+
+ /*
+ * Define the output format that you want to use. Multiple classes are supported.
+ * Available options are:
+ *
+ * Alert:
+ * Displays an alert on the website
+ * \BeyondCode\QueryDetector\Outputs\Alert::class
+ *
+ * Console:
+ * Writes the N+1 queries into your browsers console log
+ * \BeyondCode\QueryDetector\Outputs\Console::class
+ *
+ * Clockwork: (make sure you have the itsgoingd/clockwork package installed)
+ * Writes the N+1 queries warnings to Clockwork log
+ * \BeyondCode\QueryDetector\Outputs\Clockwork::class
+ *
+ * Debugbar: (make sure you have the barryvdh/laravel-debugbar package installed)
+ * Writes the N+1 queries into a custom messages collector of Debugbar
+ * \BeyondCode\QueryDetector\Outputs\Debugbar::class
+ *
+ * JSON:
+ * Writes the N+1 queries into the response body of your JSON responses
+ * \BeyondCode\QueryDetector\Outputs\Json::class
+ *
+ * Log:
+ * Writes the N+1 queries into the Laravel.log file
+ * \BeyondCode\QueryDetector\Outputs\Log::class
+ */
+ 'output' => [
+ // \BeyondCode\QueryDetector\Outputs\Alert::class,
+ Log::class,
+ Debugbar::class,
+ ],
+];
diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php
index a5695888..08d03550 100644
--- a/database/factories/UserFactory.php
+++ b/database/factories/UserFactory.php
@@ -6,7 +6,7 @@ use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
-use Modules\Platform\app\Models\Company;
+use Modules\Company\Models\Company;
use Modules\Platform\app\Models\LicenseKey;
/**
diff --git a/phpunit.dusk.xml b/phpunit.dusk.xml
index 4ffc7922..a27a8878 100644
--- a/phpunit.dusk.xml
+++ b/phpunit.dusk.xml
@@ -24,5 +24,7 @@
+
+
diff --git a/public/vendor/livewire/livewire.csp.esm.js b/public/vendor/livewire/livewire.csp.esm.js
index 2f713776..8aa17879 100644
--- a/public/vendor/livewire/livewire.csp.esm.js
+++ b/public/vendor/livewire/livewire.csp.esm.js
@@ -10074,6 +10074,22 @@ function dataSet(object, key, value) {
function isNumeric(subject) {
return !isNaN(parseInt(subject));
}
+function dataDelete(object, key) {
+ let segments = parsePathSegments(key);
+ if (segments.length === 1) {
+ if (Array.isArray(object)) {
+ object.splice(segments[0], 1);
+ } else {
+ delete object[segments[0]];
+ }
+ return;
+ }
+ let firstSegment = segments.shift();
+ let restOfSegments = segments.join(".");
+ if (object[firstSegment] !== void 0) {
+ dataDelete(object[firstSegment], restOfSegments);
+ }
+}
function diff(left, right, diffs = {}, path = "") {
if (left === right)
return diffs;
@@ -13417,17 +13433,23 @@ function whenThisLinkIsPressed(el, callback) {
let isNotPlainEnterKey = (e) => e.which !== 13 || e.altKey || e.ctrlKey || e.metaKey || e.shiftKey;
el.addEventListener("click", (e) => {
if (isProgrammaticClick(e)) {
+ if (linkShouldBeHandledNatively(el))
+ return;
e.preventDefault();
callback((whenReleased) => whenReleased());
return;
}
if (isNotPlainLeftClick(e))
return;
+ if (linkShouldBeHandledNatively(el))
+ return;
e.preventDefault();
});
el.addEventListener("mousedown", (e) => {
if (isNotPlainLeftClick(e))
return;
+ if (linkShouldBeHandledNatively(el))
+ return;
e.preventDefault();
callback((whenReleased) => {
let handler = (e2) => {
@@ -13443,6 +13465,8 @@ function whenThisLinkIsPressed(el, callback) {
el.addEventListener("keydown", (e) => {
if (isNotPlainEnterKey(e))
return;
+ if (linkShouldBeHandledNatively(el))
+ return;
e.preventDefault();
callback((whenReleased) => whenReleased());
});
@@ -13465,6 +13489,20 @@ function extractDestinationFromLink(linkEl) {
function createUrlObjectFromString2(urlString) {
return urlString !== null && new URL(urlString, document.baseURI);
}
+function linkShouldBeHandledNatively(linkEl, destination = extractDestinationFromLink(linkEl)) {
+ if (!destination)
+ return true;
+ if (!["http:", "https:"].includes(destination.protocol))
+ return true;
+ if (destination.origin !== window.location.origin)
+ return true;
+ if (linkEl.hasAttribute("download"))
+ return true;
+ let target = linkEl.getAttribute("target")?.trim().toLowerCase();
+ if (target && target !== "_self")
+ return true;
+ return false;
+}
function getUriStringFromUrlObject(urlObject) {
return urlObject.pathname + urlObject.search + urlObject.hash;
}
@@ -13923,7 +13961,6 @@ function ignoreAttributes(subject, attributesToRemove) {
var enablePersist = true;
var showProgressBar = true;
var restoreScroll = true;
-var autofocus = false;
function navigate_default(Alpine25) {
Alpine25.navigate = (url, options = {}) => {
let { preserveScroll = false } = options;
@@ -13946,7 +13983,7 @@ function navigate_default(Alpine25) {
let preserveScroll = modifiers.includes("preserve-scroll");
shouldPrefetchOnHover && whenThisLinkIsHoveredFor(el, 60, () => {
let destination = extractDestinationFromLink(el);
- if (!destination)
+ if (linkShouldBeHandledNatively(el, destination))
return;
prefetchHtml(destination, (html, finalDestination) => {
storeThePrefetchedHtmlForWhenALinkIsClicked(html, destination, finalDestination);
@@ -13956,8 +13993,6 @@ function navigate_default(Alpine25) {
});
whenThisLinkIsPressed(el, (whenItIsReleased) => {
let destination = extractDestinationFromLink(el);
- if (!destination)
- return;
prefetchHtml(destination, (html, finalDestination) => {
storeThePrefetchedHtmlForWhenALinkIsClicked(html, destination, finalDestination);
}, () => {
@@ -14005,10 +14040,8 @@ function navigate_default(Alpine25) {
swapCallbacks.forEach((callback) => callback());
afterNewScriptsAreDoneLoading(() => {
andAfterAllThis(() => {
- setTimeout(() => {
- autofocus && autofocusElementsWithTheAutofocusAttribute();
- });
nowInitializeAlpineOnTheNewPage(Alpine25);
+ autofocusElementsWithTheAutofocusAttribute();
fireEventForOtherLibrariesToHookInto("alpine:navigated");
showProgressBar && finishAndHideProgressBar();
});
@@ -14047,6 +14080,7 @@ function navigate_default(Alpine25) {
fireEventForOtherLibrariesToHookInto("alpine:navigating", {
onSwap: (callback) => swapCallbacks.push(callback)
});
+ cleanupAlpineElementsOnThePageThatArentInsideAPersistedElement();
updateCurrentPageHtmlInSnapshotCacheForLaterBackButtonClicks(currentPageKey, currentPageUrl);
preventAlpineFromPickingUpDomChanges(Alpine25, (andAfterAllThis) => {
enablePersist && storePersistantElementsForLater((persistedEl) => {
@@ -14063,8 +14097,8 @@ function navigate_default(Alpine25) {
restoreScrollPositionOrScrollToTop();
swapCallbacks.forEach((callback) => callback());
andAfterAllThis(() => {
- autofocus && autofocusElementsWithTheAutofocusAttribute();
nowInitializeAlpineOnTheNewPage(Alpine25);
+ autofocusElementsWithTheAutofocusAttribute();
fireEventForOtherLibrariesToHookInto("alpine:navigated");
});
});
@@ -14229,24 +14263,24 @@ function queryStringUtils() {
if (!search)
return false;
let data = fromQueryString(search, key);
- return Object.keys(data).includes(key);
+ return dataGet(data, key) !== void 0;
},
get(url, key) {
let search = url.search;
if (!search)
return false;
let data = fromQueryString(search, key);
- return data[key];
+ return dataGet(data, key);
},
set(url, key, value) {
let data = fromQueryString(url.search, key);
- data[key] = stripNulls(unwrap(value));
+ dataSet(data, key, stripNulls(unwrap(value)));
url.search = toQueryString(data);
return url;
},
remove(url, key) {
let data = fromQueryString(url.search, key);
- delete data[key];
+ dataDelete(data, key);
url.search = toQueryString(data);
return url;
}
@@ -14301,11 +14335,11 @@ function fromQueryString(search, queryKey) {
return;
value = decodeURIComponent(value.replaceAll("+", "%20"));
let decodedKey = decodeURIComponent(key);
- let shouldBeHandledAsArray = decodedKey.includes("[") && decodedKey.startsWith(queryKey);
+ let dotNotatedKey = decodedKey.replaceAll("[", ".").replaceAll("]", "");
+ let shouldBeHandledAsArray = decodedKey.includes("[") && (dotNotatedKey === queryKey || dotNotatedKey.startsWith(`${queryKey}.`));
if (!shouldBeHandledAsArray) {
data[key] = value;
} else {
- let dotNotatedKey = decodedKey.replaceAll("[", ".").replaceAll("]", "");
insertDotNotatedValueIntoData(dotNotatedKey, value, data);
}
});
diff --git a/public/vendor/livewire/livewire.csp.esm.js.map b/public/vendor/livewire/livewire.csp.esm.js.map
index d753cb13..18700960 100644
--- a/public/vendor/livewire/livewire.csp.esm.js.map
+++ b/public/vendor/livewire/livewire.csp.esm.js.map
@@ -1,7 +1,7 @@
{
"version": 3,
"sources": ["../node_modules/@alpinejs/csp/dist/module.cjs.js", "../node_modules/@alpinejs/collapse/dist/module.cjs.js", "../node_modules/@alpinejs/focus/dist/module.cjs.js", "../node_modules/@alpinejs/intersect/dist/module.cjs.js", "../node_modules/@alpinejs/sort/dist/module.cjs.js", "../node_modules/@alpinejs/resize/dist/module.cjs.js", "../node_modules/@alpinejs/anchor/dist/module.cjs.js", "../node_modules/nprogress/nprogress.js", "../node_modules/@alpinejs/morph/dist/module.cjs.js", "../node_modules/@alpinejs/mask/dist/module.cjs.js", "../js/utils.js", "../js/features/supportFileUploads.js", "../js/features/supportEntangle.js", "../js/$wire.js", "../js/hooks.js", "../js/request/interactions.js", "../js/request/request.js", "../js/request/interceptor.js", "../js/utils/modal.js", "../js/request/messageBus.js", "../js/request/message.js", "../js/request/action.js", "../js/request/legacy.js", "../js/request/index.js", "../js/features/supportErrors.js", "../js/features/supportRefs.js", "../js/directives.js", "../js/directives/shared.js", "../js/directives/wire-dirty.js", "../js/features/supportJsModules.js", "../js/component.js", "../js/fragment.js", "../js/store.js", "../js/events.js", "../js/lifecycle.js", "../node_modules/@alpinejs/persist/dist/module.esm.js", "../js/plugins/history/utils.js", "../js/plugins/history/coordinator.js", "../js/plugins/navigate/history.js", "../js/plugins/navigate/links.js", "../js/plugins/navigate/fetch.js", "../js/plugins/navigate/prefetch.js", "../js/plugins/navigate/teleport.js", "../js/plugins/navigate/scroll.js", "../js/plugins/navigate/persist.js", "../js/plugins/navigate/bar.js", "../js/plugins/navigate/popover.js", "../js/plugins/navigate/page.js", "../js/plugins/navigate/index.js", "../js/plugins/history/index.js", "../js/index.js", "../js/features/supportListeners.js", "../js/features/supportScriptsAndAssets.js", "../js/evaluator.js", "../js/features/supportJsEvaluation.js", "../js/morph.js", "../js/directives/wire-transition.js", "../js/features/supportMorphDom.js", "../js/features/supportDispatches.js", "../js/features/supportDisablingFormsDuringRequest.js", "../js/features/supportFileDownloads.js", "../js/features/supportQueryString.js", "../js/features/supportLaravelEcho.js", "../js/features/supportStreaming.js", "../js/features/supportNavigate.js", "../js/features/supportRedirects.js", "../js/features/supportIslands.js", "../js/features/supportSlots.js", "../js/features/supportDataLoading.js", "../js/directives/wire-current.js", "../js/directives/wire-navigate.js", "../js/features/supportDataCurrent.js", "../js/features/supportPreserveScroll.js", "../js/features/supportWireIntersect.js", "../js/features/supportWireSort.js", "../js/features/supportCssModules.js", "../js/debounce.js", "../js/directives/wire-wildcard.js", "../js/directives/wire-confirm.js", "../js/directives/wire-offline.js", "../js/directives/wire-loading.js", "../js/directives/wire-replace.js", "../js/directives/wire-ignore.js", "../js/directives/wire-cloak.js", "../js/directives/wire-model.js", "../js/directives/wire-init.js", "../js/directives/wire-poll.js", "../js/directives/wire-show.js", "../js/directives/wire-text.js", "../js/directives/wire-bind.js"],
- "sourcesContent": ["var __create = Object.create;\nvar __defProp = Object.defineProperty;\nvar __getOwnPropDesc = Object.getOwnPropertyDescriptor;\nvar __getOwnPropNames = Object.getOwnPropertyNames;\nvar __getProtoOf = Object.getPrototypeOf;\nvar __hasOwnProp = Object.prototype.hasOwnProperty;\nvar __commonJS = (cb, mod) => function __require() {\n return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;\n};\nvar __export = (target, all) => {\n for (var name in all)\n __defProp(target, name, { get: all[name], enumerable: true });\n};\nvar __copyProps = (to, from, except, desc) => {\n if (from && typeof from === \"object\" || typeof from === \"function\") {\n for (let key of __getOwnPropNames(from))\n if (!__hasOwnProp.call(to, key) && key !== except)\n __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });\n }\n return to;\n};\nvar __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(\n // If the importer is in node compatibility mode or this is not an ESM\n // file that has been converted to a CommonJS file using a Babel-\n // compatible transform (i.e. \"__esModule\" has not been set), then set\n // \"default\" to the CommonJS \"module.exports\" for node compatibility.\n isNodeMode || !mod || !mod.__esModule ? __defProp(target, \"default\", { value: mod, enumerable: true }) : target,\n mod\n));\nvar __toCommonJS = (mod) => __copyProps(__defProp({}, \"__esModule\", { value: true }), mod);\n\n// node_modules/@vue/shared/dist/shared.cjs.js\nvar require_shared_cjs = __commonJS({\n \"node_modules/@vue/shared/dist/shared.cjs.js\"(exports) {\n \"use strict\";\n Object.defineProperty(exports, \"__esModule\", { value: true });\n function makeMap(str, expectsLowerCase) {\n const map = /* @__PURE__ */ Object.create(null);\n const list = str.split(\",\");\n for (let i = 0; i < list.length; i++) {\n map[list[i]] = true;\n }\n return expectsLowerCase ? (val) => !!map[val.toLowerCase()] : (val) => !!map[val];\n }\n var PatchFlagNames = {\n [\n 1\n /* TEXT */\n ]: `TEXT`,\n [\n 2\n /* CLASS */\n ]: `CLASS`,\n [\n 4\n /* STYLE */\n ]: `STYLE`,\n [\n 8\n /* PROPS */\n ]: `PROPS`,\n [\n 16\n /* FULL_PROPS */\n ]: `FULL_PROPS`,\n [\n 32\n /* HYDRATE_EVENTS */\n ]: `HYDRATE_EVENTS`,\n [\n 64\n /* STABLE_FRAGMENT */\n ]: `STABLE_FRAGMENT`,\n [\n 128\n /* KEYED_FRAGMENT */\n ]: `KEYED_FRAGMENT`,\n [\n 256\n /* UNKEYED_FRAGMENT */\n ]: `UNKEYED_FRAGMENT`,\n [\n 512\n /* NEED_PATCH */\n ]: `NEED_PATCH`,\n [\n 1024\n /* DYNAMIC_SLOTS */\n ]: `DYNAMIC_SLOTS`,\n [\n 2048\n /* DEV_ROOT_FRAGMENT */\n ]: `DEV_ROOT_FRAGMENT`,\n [\n -1\n /* HOISTED */\n ]: `HOISTED`,\n [\n -2\n /* BAIL */\n ]: `BAIL`\n };\n var slotFlagsText = {\n [\n 1\n /* STABLE */\n ]: \"STABLE\",\n [\n 2\n /* DYNAMIC */\n ]: \"DYNAMIC\",\n [\n 3\n /* FORWARDED */\n ]: \"FORWARDED\"\n };\n var GLOBALS_WHITE_LISTED = \"Infinity,undefined,NaN,isFinite,isNaN,parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,BigInt\";\n var isGloballyWhitelisted = /* @__PURE__ */ makeMap(GLOBALS_WHITE_LISTED);\n var range = 2;\n function generateCodeFrame(source, start2 = 0, end = source.length) {\n let lines = source.split(/(\\r?\\n)/);\n const newlineSequences = lines.filter((_, idx) => idx % 2 === 1);\n lines = lines.filter((_, idx) => idx % 2 === 0);\n let count = 0;\n const res = [];\n for (let i = 0; i < lines.length; i++) {\n count += lines[i].length + (newlineSequences[i] && newlineSequences[i].length || 0);\n if (count >= start2) {\n for (let j = i - range; j <= i + range || end > count; j++) {\n if (j < 0 || j >= lines.length)\n continue;\n const line = j + 1;\n res.push(`${line}${\" \".repeat(Math.max(3 - String(line).length, 0))}| ${lines[j]}`);\n const lineLength = lines[j].length;\n const newLineSeqLength = newlineSequences[j] && newlineSequences[j].length || 0;\n if (j === i) {\n const pad = start2 - (count - (lineLength + newLineSeqLength));\n const length = Math.max(1, end > count ? lineLength - pad : end - start2);\n res.push(` | ` + \" \".repeat(pad) + \"^\".repeat(length));\n } else if (j > i) {\n if (end > count) {\n const length = Math.max(Math.min(end - count, lineLength), 1);\n res.push(` | ` + \"^\".repeat(length));\n }\n count += lineLength + newLineSeqLength;\n }\n }\n break;\n }\n }\n return res.join(\"\\n\");\n }\n var specialBooleanAttrs = `itemscope,allowfullscreen,formnovalidate,ismap,nomodule,novalidate,readonly`;\n var isSpecialBooleanAttr = /* @__PURE__ */ makeMap(specialBooleanAttrs);\n var isBooleanAttr2 = /* @__PURE__ */ makeMap(specialBooleanAttrs + `,async,autofocus,autoplay,controls,default,defer,disabled,hidden,loop,open,required,reversed,scoped,seamless,checked,muted,multiple,selected`);\n var unsafeAttrCharRE = /[>/=\"'\\u0009\\u000a\\u000c\\u0020]/;\n var attrValidationCache = {};\n function isSSRSafeAttrName(name) {\n if (attrValidationCache.hasOwnProperty(name)) {\n return attrValidationCache[name];\n }\n const isUnsafe = unsafeAttrCharRE.test(name);\n if (isUnsafe) {\n console.error(`unsafe attribute name: ${name}`);\n }\n return attrValidationCache[name] = !isUnsafe;\n }\n var propsToAttrMap = {\n acceptCharset: \"accept-charset\",\n className: \"class\",\n htmlFor: \"for\",\n httpEquiv: \"http-equiv\"\n };\n var isNoUnitNumericStyleProp = /* @__PURE__ */ makeMap(`animation-iteration-count,border-image-outset,border-image-slice,border-image-width,box-flex,box-flex-group,box-ordinal-group,column-count,columns,flex,flex-grow,flex-positive,flex-shrink,flex-negative,flex-order,grid-row,grid-row-end,grid-row-span,grid-row-start,grid-column,grid-column-end,grid-column-span,grid-column-start,font-weight,line-clamp,line-height,opacity,order,orphans,tab-size,widows,z-index,zoom,fill-opacity,flood-opacity,stop-opacity,stroke-dasharray,stroke-dashoffset,stroke-miterlimit,stroke-opacity,stroke-width`);\n var isKnownAttr = /* @__PURE__ */ makeMap(`accept,accept-charset,accesskey,action,align,allow,alt,async,autocapitalize,autocomplete,autofocus,autoplay,background,bgcolor,border,buffered,capture,challenge,charset,checked,cite,class,code,codebase,color,cols,colspan,content,contenteditable,contextmenu,controls,coords,crossorigin,csp,data,datetime,decoding,default,defer,dir,dirname,disabled,download,draggable,dropzone,enctype,enterkeyhint,for,form,formaction,formenctype,formmethod,formnovalidate,formtarget,headers,height,hidden,high,href,hreflang,http-equiv,icon,id,importance,integrity,ismap,itemprop,keytype,kind,label,lang,language,loading,list,loop,low,manifest,max,maxlength,minlength,media,min,multiple,muted,name,novalidate,open,optimum,pattern,ping,placeholder,poster,preload,radiogroup,readonly,referrerpolicy,rel,required,reversed,rows,rowspan,sandbox,scope,scoped,selected,shape,size,sizes,slot,span,spellcheck,src,srcdoc,srclang,srcset,start,step,style,summary,tabindex,target,title,translate,type,usemap,value,width,wrap`);\n function normalizeStyle(value) {\n if (isArray(value)) {\n const res = {};\n for (let i = 0; i < value.length; i++) {\n const item = value[i];\n const normalized = normalizeStyle(isString(item) ? parseStringStyle(item) : item);\n if (normalized) {\n for (const key in normalized) {\n res[key] = normalized[key];\n }\n }\n }\n return res;\n } else if (isObject2(value)) {\n return value;\n }\n }\n var listDelimiterRE = /;(?![^(]*\\))/g;\n var propertyDelimiterRE = /:(.+)/;\n function parseStringStyle(cssText) {\n const ret = {};\n cssText.split(listDelimiterRE).forEach((item) => {\n if (item) {\n const tmp = item.split(propertyDelimiterRE);\n tmp.length > 1 && (ret[tmp[0].trim()] = tmp[1].trim());\n }\n });\n return ret;\n }\n function stringifyStyle(styles) {\n let ret = \"\";\n if (!styles) {\n return ret;\n }\n for (const key in styles) {\n const value = styles[key];\n const normalizedKey = key.startsWith(`--`) ? key : hyphenate(key);\n if (isString(value) || typeof value === \"number\" && isNoUnitNumericStyleProp(normalizedKey)) {\n ret += `${normalizedKey}:${value};`;\n }\n }\n return ret;\n }\n function normalizeClass(value) {\n let res = \"\";\n if (isString(value)) {\n res = value;\n } else if (isArray(value)) {\n for (let i = 0; i < value.length; i++) {\n const normalized = normalizeClass(value[i]);\n if (normalized) {\n res += normalized + \" \";\n }\n }\n } else if (isObject2(value)) {\n for (const name in value) {\n if (value[name]) {\n res += name + \" \";\n }\n }\n }\n return res.trim();\n }\n var HTML_TAGS = \"html,body,base,head,link,meta,style,title,address,article,aside,footer,header,h1,h2,h3,h4,h5,h6,hgroup,nav,section,div,dd,dl,dt,figcaption,figure,picture,hr,img,li,main,ol,p,pre,ul,a,b,abbr,bdi,bdo,br,cite,code,data,dfn,em,i,kbd,mark,q,rp,rt,rtc,ruby,s,samp,small,span,strong,sub,sup,time,u,var,wbr,area,audio,map,track,video,embed,object,param,source,canvas,script,noscript,del,ins,caption,col,colgroup,table,thead,tbody,td,th,tr,button,datalist,fieldset,form,input,label,legend,meter,optgroup,option,output,progress,select,textarea,details,dialog,menu,summary,template,blockquote,iframe,tfoot\";\n var SVG_TAGS = \"svg,animate,animateMotion,animateTransform,circle,clipPath,color-profile,defs,desc,discard,ellipse,feBlend,feColorMatrix,feComponentTransfer,feComposite,feConvolveMatrix,feDiffuseLighting,feDisplacementMap,feDistanceLight,feDropShadow,feFlood,feFuncA,feFuncB,feFuncG,feFuncR,feGaussianBlur,feImage,feMerge,feMergeNode,feMorphology,feOffset,fePointLight,feSpecularLighting,feSpotLight,feTile,feTurbulence,filter,foreignObject,g,hatch,hatchpath,image,line,linearGradient,marker,mask,mesh,meshgradient,meshpatch,meshrow,metadata,mpath,path,pattern,polygon,polyline,radialGradient,rect,set,solidcolor,stop,switch,symbol,text,textPath,title,tspan,unknown,use,view\";\n var VOID_TAGS = \"area,base,br,col,embed,hr,img,input,link,meta,param,source,track,wbr\";\n var isHTMLTag = /* @__PURE__ */ makeMap(HTML_TAGS);\n var isSVGTag = /* @__PURE__ */ makeMap(SVG_TAGS);\n var isVoidTag = /* @__PURE__ */ makeMap(VOID_TAGS);\n var escapeRE = /[\"'&<>]/;\n function escapeHtml(string) {\n const str = \"\" + string;\n const match = escapeRE.exec(str);\n if (!match) {\n return str;\n }\n let html = \"\";\n let escaped;\n let index;\n let lastIndex = 0;\n for (index = match.index; index < str.length; index++) {\n switch (str.charCodeAt(index)) {\n case 34:\n escaped = \""\";\n break;\n case 38:\n escaped = \"&\";\n break;\n case 39:\n escaped = \"'\";\n break;\n case 60:\n escaped = \"<\";\n break;\n case 62:\n escaped = \">\";\n break;\n default:\n continue;\n }\n if (lastIndex !== index) {\n html += str.substring(lastIndex, index);\n }\n lastIndex = index + 1;\n html += escaped;\n }\n return lastIndex !== index ? html + str.substring(lastIndex, index) : html;\n }\n var commentStripRE = /^-?>||--!>| looseEqual(item, val));\n }\n var toDisplayString = (val) => {\n return val == null ? \"\" : isObject2(val) ? JSON.stringify(val, replacer, 2) : String(val);\n };\n var replacer = (_key, val) => {\n if (isMap(val)) {\n return {\n [`Map(${val.size})`]: [...val.entries()].reduce((entries, [key, val2]) => {\n entries[`${key} =>`] = val2;\n return entries;\n }, {})\n };\n } else if (isSet(val)) {\n return {\n [`Set(${val.size})`]: [...val.values()]\n };\n } else if (isObject2(val) && !isArray(val) && !isPlainObject(val)) {\n return String(val);\n }\n return val;\n };\n var babelParserDefaultPlugins = [\n \"bigInt\",\n \"optionalChaining\",\n \"nullishCoalescingOperator\"\n ];\n var EMPTY_OBJ = Object.freeze({});\n var EMPTY_ARR = Object.freeze([]);\n var NOOP = () => {\n };\n var NO = () => false;\n var onRE = /^on[^a-z]/;\n var isOn = (key) => onRE.test(key);\n var isModelListener = (key) => key.startsWith(\"onUpdate:\");\n var extend = Object.assign;\n var remove = (arr, el) => {\n const i = arr.indexOf(el);\n if (i > -1) {\n arr.splice(i, 1);\n }\n };\n var hasOwnProperty = Object.prototype.hasOwnProperty;\n var hasOwn = (val, key) => hasOwnProperty.call(val, key);\n var isArray = Array.isArray;\n var isMap = (val) => toTypeString(val) === \"[object Map]\";\n var isSet = (val) => toTypeString(val) === \"[object Set]\";\n var isDate = (val) => val instanceof Date;\n var isFunction = (val) => typeof val === \"function\";\n var isString = (val) => typeof val === \"string\";\n var isSymbol = (val) => typeof val === \"symbol\";\n var isObject2 = (val) => val !== null && typeof val === \"object\";\n var isPromise = (val) => {\n return isObject2(val) && isFunction(val.then) && isFunction(val.catch);\n };\n var objectToString = Object.prototype.toString;\n var toTypeString = (value) => objectToString.call(value);\n var toRawType = (value) => {\n return toTypeString(value).slice(8, -1);\n };\n var isPlainObject = (val) => toTypeString(val) === \"[object Object]\";\n var isIntegerKey = (key) => isString(key) && key !== \"NaN\" && key[0] !== \"-\" && \"\" + parseInt(key, 10) === key;\n var isReservedProp = /* @__PURE__ */ makeMap(\n // the leading comma is intentional so empty string \"\" is also included\n \",key,ref,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted\"\n );\n var cacheStringFunction = (fn) => {\n const cache = /* @__PURE__ */ Object.create(null);\n return (str) => {\n const hit = cache[str];\n return hit || (cache[str] = fn(str));\n };\n };\n var camelizeRE = /-(\\w)/g;\n var camelize = cacheStringFunction((str) => {\n return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : \"\");\n });\n var hyphenateRE = /\\B([A-Z])/g;\n var hyphenate = cacheStringFunction((str) => str.replace(hyphenateRE, \"-$1\").toLowerCase());\n var capitalize = cacheStringFunction((str) => str.charAt(0).toUpperCase() + str.slice(1));\n var toHandlerKey = cacheStringFunction((str) => str ? `on${capitalize(str)}` : ``);\n var hasChanged = (value, oldValue) => value !== oldValue && (value === value || oldValue === oldValue);\n var invokeArrayFns = (fns, arg) => {\n for (let i = 0; i < fns.length; i++) {\n fns[i](arg);\n }\n };\n var def = (obj, key, value) => {\n Object.defineProperty(obj, key, {\n configurable: true,\n enumerable: false,\n value\n });\n };\n var toNumber = (val) => {\n const n = parseFloat(val);\n return isNaN(n) ? val : n;\n };\n var _globalThis;\n var getGlobalThis = () => {\n return _globalThis || (_globalThis = typeof globalThis !== \"undefined\" ? globalThis : typeof self !== \"undefined\" ? self : typeof window !== \"undefined\" ? window : typeof global !== \"undefined\" ? global : {});\n };\n exports.EMPTY_ARR = EMPTY_ARR;\n exports.EMPTY_OBJ = EMPTY_OBJ;\n exports.NO = NO;\n exports.NOOP = NOOP;\n exports.PatchFlagNames = PatchFlagNames;\n exports.babelParserDefaultPlugins = babelParserDefaultPlugins;\n exports.camelize = camelize;\n exports.capitalize = capitalize;\n exports.def = def;\n exports.escapeHtml = escapeHtml;\n exports.escapeHtmlComment = escapeHtmlComment;\n exports.extend = extend;\n exports.generateCodeFrame = generateCodeFrame;\n exports.getGlobalThis = getGlobalThis;\n exports.hasChanged = hasChanged;\n exports.hasOwn = hasOwn;\n exports.hyphenate = hyphenate;\n exports.invokeArrayFns = invokeArrayFns;\n exports.isArray = isArray;\n exports.isBooleanAttr = isBooleanAttr2;\n exports.isDate = isDate;\n exports.isFunction = isFunction;\n exports.isGloballyWhitelisted = isGloballyWhitelisted;\n exports.isHTMLTag = isHTMLTag;\n exports.isIntegerKey = isIntegerKey;\n exports.isKnownAttr = isKnownAttr;\n exports.isMap = isMap;\n exports.isModelListener = isModelListener;\n exports.isNoUnitNumericStyleProp = isNoUnitNumericStyleProp;\n exports.isObject = isObject2;\n exports.isOn = isOn;\n exports.isPlainObject = isPlainObject;\n exports.isPromise = isPromise;\n exports.isReservedProp = isReservedProp;\n exports.isSSRSafeAttrName = isSSRSafeAttrName;\n exports.isSVGTag = isSVGTag;\n exports.isSet = isSet;\n exports.isSpecialBooleanAttr = isSpecialBooleanAttr;\n exports.isString = isString;\n exports.isSymbol = isSymbol;\n exports.isVoidTag = isVoidTag;\n exports.looseEqual = looseEqual;\n exports.looseIndexOf = looseIndexOf;\n exports.makeMap = makeMap;\n exports.normalizeClass = normalizeClass;\n exports.normalizeStyle = normalizeStyle;\n exports.objectToString = objectToString;\n exports.parseStringStyle = parseStringStyle;\n exports.propsToAttrMap = propsToAttrMap;\n exports.remove = remove;\n exports.slotFlagsText = slotFlagsText;\n exports.stringifyStyle = stringifyStyle;\n exports.toDisplayString = toDisplayString;\n exports.toHandlerKey = toHandlerKey;\n exports.toNumber = toNumber;\n exports.toRawType = toRawType;\n exports.toTypeString = toTypeString;\n }\n});\n\n// node_modules/@vue/shared/index.js\nvar require_shared = __commonJS({\n \"node_modules/@vue/shared/index.js\"(exports, module2) {\n \"use strict\";\n if (false) {\n module2.exports = null;\n } else {\n module2.exports = require_shared_cjs();\n }\n }\n});\n\n// node_modules/@vue/reactivity/dist/reactivity.cjs.js\nvar require_reactivity_cjs = __commonJS({\n \"node_modules/@vue/reactivity/dist/reactivity.cjs.js\"(exports) {\n \"use strict\";\n Object.defineProperty(exports, \"__esModule\", { value: true });\n var shared = require_shared();\n var targetMap = /* @__PURE__ */ new WeakMap();\n var effectStack = [];\n var activeEffect;\n var ITERATE_KEY = Symbol(\"iterate\");\n var MAP_KEY_ITERATE_KEY = Symbol(\"Map key iterate\");\n function isEffect(fn) {\n return fn && fn._isEffect === true;\n }\n function effect3(fn, options = shared.EMPTY_OBJ) {\n if (isEffect(fn)) {\n fn = fn.raw;\n }\n const effect4 = createReactiveEffect(fn, options);\n if (!options.lazy) {\n effect4();\n }\n return effect4;\n }\n function stop2(effect4) {\n if (effect4.active) {\n cleanup(effect4);\n if (effect4.options.onStop) {\n effect4.options.onStop();\n }\n effect4.active = false;\n }\n }\n var uid = 0;\n function createReactiveEffect(fn, options) {\n const effect4 = function reactiveEffect() {\n if (!effect4.active) {\n return fn();\n }\n if (!effectStack.includes(effect4)) {\n cleanup(effect4);\n try {\n enableTracking();\n effectStack.push(effect4);\n activeEffect = effect4;\n return fn();\n } finally {\n effectStack.pop();\n resetTracking();\n activeEffect = effectStack[effectStack.length - 1];\n }\n }\n };\n effect4.id = uid++;\n effect4.allowRecurse = !!options.allowRecurse;\n effect4._isEffect = true;\n effect4.active = true;\n effect4.raw = fn;\n effect4.deps = [];\n effect4.options = options;\n return effect4;\n }\n function cleanup(effect4) {\n const { deps } = effect4;\n if (deps.length) {\n for (let i = 0; i < deps.length; i++) {\n deps[i].delete(effect4);\n }\n deps.length = 0;\n }\n }\n var shouldTrack = true;\n var trackStack = [];\n function pauseTracking() {\n trackStack.push(shouldTrack);\n shouldTrack = false;\n }\n function enableTracking() {\n trackStack.push(shouldTrack);\n shouldTrack = true;\n }\n function resetTracking() {\n const last = trackStack.pop();\n shouldTrack = last === void 0 ? true : last;\n }\n function track(target, type, key) {\n if (!shouldTrack || activeEffect === void 0) {\n return;\n }\n let depsMap = targetMap.get(target);\n if (!depsMap) {\n targetMap.set(target, depsMap = /* @__PURE__ */ new Map());\n }\n let dep = depsMap.get(key);\n if (!dep) {\n depsMap.set(key, dep = /* @__PURE__ */ new Set());\n }\n if (!dep.has(activeEffect)) {\n dep.add(activeEffect);\n activeEffect.deps.push(dep);\n if (activeEffect.options.onTrack) {\n activeEffect.options.onTrack({\n effect: activeEffect,\n target,\n type,\n key\n });\n }\n }\n }\n function trigger(target, type, key, newValue, oldValue, oldTarget) {\n const depsMap = targetMap.get(target);\n if (!depsMap) {\n return;\n }\n const effects = /* @__PURE__ */ new Set();\n const add2 = (effectsToAdd) => {\n if (effectsToAdd) {\n effectsToAdd.forEach((effect4) => {\n if (effect4 !== activeEffect || effect4.allowRecurse) {\n effects.add(effect4);\n }\n });\n }\n };\n if (type === \"clear\") {\n depsMap.forEach(add2);\n } else if (key === \"length\" && shared.isArray(target)) {\n depsMap.forEach((dep, key2) => {\n if (key2 === \"length\" || key2 >= newValue) {\n add2(dep);\n }\n });\n } else {\n if (key !== void 0) {\n add2(depsMap.get(key));\n }\n switch (type) {\n case \"add\":\n if (!shared.isArray(target)) {\n add2(depsMap.get(ITERATE_KEY));\n if (shared.isMap(target)) {\n add2(depsMap.get(MAP_KEY_ITERATE_KEY));\n }\n } else if (shared.isIntegerKey(key)) {\n add2(depsMap.get(\"length\"));\n }\n break;\n case \"delete\":\n if (!shared.isArray(target)) {\n add2(depsMap.get(ITERATE_KEY));\n if (shared.isMap(target)) {\n add2(depsMap.get(MAP_KEY_ITERATE_KEY));\n }\n }\n break;\n case \"set\":\n if (shared.isMap(target)) {\n add2(depsMap.get(ITERATE_KEY));\n }\n break;\n }\n }\n const run = (effect4) => {\n if (effect4.options.onTrigger) {\n effect4.options.onTrigger({\n effect: effect4,\n target,\n key,\n type,\n newValue,\n oldValue,\n oldTarget\n });\n }\n if (effect4.options.scheduler) {\n effect4.options.scheduler(effect4);\n } else {\n effect4();\n }\n };\n effects.forEach(run);\n }\n var isNonTrackableKeys = /* @__PURE__ */ shared.makeMap(`__proto__,__v_isRef,__isVue`);\n var builtInSymbols = new Set(Object.getOwnPropertyNames(Symbol).map((key) => Symbol[key]).filter(shared.isSymbol));\n var get2 = /* @__PURE__ */ createGetter();\n var shallowGet = /* @__PURE__ */ createGetter(false, true);\n var readonlyGet = /* @__PURE__ */ createGetter(true);\n var shallowReadonlyGet = /* @__PURE__ */ createGetter(true, true);\n var arrayInstrumentations = /* @__PURE__ */ createArrayInstrumentations();\n function createArrayInstrumentations() {\n const instrumentations = {};\n [\"includes\", \"indexOf\", \"lastIndexOf\"].forEach((key) => {\n instrumentations[key] = function(...args) {\n const arr = toRaw2(this);\n for (let i = 0, l = this.length; i < l; i++) {\n track(arr, \"get\", i + \"\");\n }\n const res = arr[key](...args);\n if (res === -1 || res === false) {\n return arr[key](...args.map(toRaw2));\n } else {\n return res;\n }\n };\n });\n [\"push\", \"pop\", \"shift\", \"unshift\", \"splice\"].forEach((key) => {\n instrumentations[key] = function(...args) {\n pauseTracking();\n const res = toRaw2(this)[key].apply(this, args);\n resetTracking();\n return res;\n };\n });\n return instrumentations;\n }\n function createGetter(isReadonly2 = false, shallow = false) {\n return function get3(target, key, receiver) {\n if (key === \"__v_isReactive\") {\n return !isReadonly2;\n } else if (key === \"__v_isReadonly\") {\n return isReadonly2;\n } else if (key === \"__v_raw\" && receiver === (isReadonly2 ? shallow ? shallowReadonlyMap : readonlyMap : shallow ? shallowReactiveMap : reactiveMap).get(target)) {\n return target;\n }\n const targetIsArray = shared.isArray(target);\n if (!isReadonly2 && targetIsArray && shared.hasOwn(arrayInstrumentations, key)) {\n return Reflect.get(arrayInstrumentations, key, receiver);\n }\n const res = Reflect.get(target, key, receiver);\n if (shared.isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {\n return res;\n }\n if (!isReadonly2) {\n track(target, \"get\", key);\n }\n if (shallow) {\n return res;\n }\n if (isRef(res)) {\n const shouldUnwrap = !targetIsArray || !shared.isIntegerKey(key);\n return shouldUnwrap ? res.value : res;\n }\n if (shared.isObject(res)) {\n return isReadonly2 ? readonly(res) : reactive3(res);\n }\n return res;\n };\n }\n var set2 = /* @__PURE__ */ createSetter();\n var shallowSet = /* @__PURE__ */ createSetter(true);\n function createSetter(shallow = false) {\n return function set3(target, key, value, receiver) {\n let oldValue = target[key];\n if (!shallow) {\n value = toRaw2(value);\n oldValue = toRaw2(oldValue);\n if (!shared.isArray(target) && isRef(oldValue) && !isRef(value)) {\n oldValue.value = value;\n return true;\n }\n }\n const hadKey = shared.isArray(target) && shared.isIntegerKey(key) ? Number(key) < target.length : shared.hasOwn(target, key);\n const result = Reflect.set(target, key, value, receiver);\n if (target === toRaw2(receiver)) {\n if (!hadKey) {\n trigger(target, \"add\", key, value);\n } else if (shared.hasChanged(value, oldValue)) {\n trigger(target, \"set\", key, value, oldValue);\n }\n }\n return result;\n };\n }\n function deleteProperty(target, key) {\n const hadKey = shared.hasOwn(target, key);\n const oldValue = target[key];\n const result = Reflect.deleteProperty(target, key);\n if (result && hadKey) {\n trigger(target, \"delete\", key, void 0, oldValue);\n }\n return result;\n }\n function has(target, key) {\n const result = Reflect.has(target, key);\n if (!shared.isSymbol(key) || !builtInSymbols.has(key)) {\n track(target, \"has\", key);\n }\n return result;\n }\n function ownKeys(target) {\n track(target, \"iterate\", shared.isArray(target) ? \"length\" : ITERATE_KEY);\n return Reflect.ownKeys(target);\n }\n var mutableHandlers = {\n get: get2,\n set: set2,\n deleteProperty,\n has,\n ownKeys\n };\n var readonlyHandlers = {\n get: readonlyGet,\n set(target, key) {\n {\n console.warn(`Set operation on key \"${String(key)}\" failed: target is readonly.`, target);\n }\n return true;\n },\n deleteProperty(target, key) {\n {\n console.warn(`Delete operation on key \"${String(key)}\" failed: target is readonly.`, target);\n }\n return true;\n }\n };\n var shallowReactiveHandlers = /* @__PURE__ */ shared.extend({}, mutableHandlers, {\n get: shallowGet,\n set: shallowSet\n });\n var shallowReadonlyHandlers = /* @__PURE__ */ shared.extend({}, readonlyHandlers, {\n get: shallowReadonlyGet\n });\n var toReactive = (value) => shared.isObject(value) ? reactive3(value) : value;\n var toReadonly = (value) => shared.isObject(value) ? readonly(value) : value;\n var toShallow = (value) => value;\n var getProto = (v) => Reflect.getPrototypeOf(v);\n function get$1(target, key, isReadonly2 = false, isShallow = false) {\n target = target[\n \"__v_raw\"\n /* RAW */\n ];\n const rawTarget = toRaw2(target);\n const rawKey = toRaw2(key);\n if (key !== rawKey) {\n !isReadonly2 && track(rawTarget, \"get\", key);\n }\n !isReadonly2 && track(rawTarget, \"get\", rawKey);\n const { has: has2 } = getProto(rawTarget);\n const wrap = isShallow ? toShallow : isReadonly2 ? toReadonly : toReactive;\n if (has2.call(rawTarget, key)) {\n return wrap(target.get(key));\n } else if (has2.call(rawTarget, rawKey)) {\n return wrap(target.get(rawKey));\n } else if (target !== rawTarget) {\n target.get(key);\n }\n }\n function has$1(key, isReadonly2 = false) {\n const target = this[\n \"__v_raw\"\n /* RAW */\n ];\n const rawTarget = toRaw2(target);\n const rawKey = toRaw2(key);\n if (key !== rawKey) {\n !isReadonly2 && track(rawTarget, \"has\", key);\n }\n !isReadonly2 && track(rawTarget, \"has\", rawKey);\n return key === rawKey ? target.has(key) : target.has(key) || target.has(rawKey);\n }\n function size(target, isReadonly2 = false) {\n target = target[\n \"__v_raw\"\n /* RAW */\n ];\n !isReadonly2 && track(toRaw2(target), \"iterate\", ITERATE_KEY);\n return Reflect.get(target, \"size\", target);\n }\n function add(value) {\n value = toRaw2(value);\n const target = toRaw2(this);\n const proto = getProto(target);\n const hadKey = proto.has.call(target, value);\n if (!hadKey) {\n target.add(value);\n trigger(target, \"add\", value, value);\n }\n return this;\n }\n function set$1(key, value) {\n value = toRaw2(value);\n const target = toRaw2(this);\n const { has: has2, get: get3 } = getProto(target);\n let hadKey = has2.call(target, key);\n if (!hadKey) {\n key = toRaw2(key);\n hadKey = has2.call(target, key);\n } else {\n checkIdentityKeys(target, has2, key);\n }\n const oldValue = get3.call(target, key);\n target.set(key, value);\n if (!hadKey) {\n trigger(target, \"add\", key, value);\n } else if (shared.hasChanged(value, oldValue)) {\n trigger(target, \"set\", key, value, oldValue);\n }\n return this;\n }\n function deleteEntry(key) {\n const target = toRaw2(this);\n const { has: has2, get: get3 } = getProto(target);\n let hadKey = has2.call(target, key);\n if (!hadKey) {\n key = toRaw2(key);\n hadKey = has2.call(target, key);\n } else {\n checkIdentityKeys(target, has2, key);\n }\n const oldValue = get3 ? get3.call(target, key) : void 0;\n const result = target.delete(key);\n if (hadKey) {\n trigger(target, \"delete\", key, void 0, oldValue);\n }\n return result;\n }\n function clear() {\n const target = toRaw2(this);\n const hadItems = target.size !== 0;\n const oldTarget = shared.isMap(target) ? new Map(target) : new Set(target);\n const result = target.clear();\n if (hadItems) {\n trigger(target, \"clear\", void 0, void 0, oldTarget);\n }\n return result;\n }\n function createForEach(isReadonly2, isShallow) {\n return function forEach(callback, thisArg) {\n const observed = this;\n const target = observed[\n \"__v_raw\"\n /* RAW */\n ];\n const rawTarget = toRaw2(target);\n const wrap = isShallow ? toShallow : isReadonly2 ? toReadonly : toReactive;\n !isReadonly2 && track(rawTarget, \"iterate\", ITERATE_KEY);\n return target.forEach((value, key) => {\n return callback.call(thisArg, wrap(value), wrap(key), observed);\n });\n };\n }\n function createIterableMethod(method, isReadonly2, isShallow) {\n return function(...args) {\n const target = this[\n \"__v_raw\"\n /* RAW */\n ];\n const rawTarget = toRaw2(target);\n const targetIsMap = shared.isMap(rawTarget);\n const isPair = method === \"entries\" || method === Symbol.iterator && targetIsMap;\n const isKeyOnly = method === \"keys\" && targetIsMap;\n const innerIterator = target[method](...args);\n const wrap = isShallow ? toShallow : isReadonly2 ? toReadonly : toReactive;\n !isReadonly2 && track(rawTarget, \"iterate\", isKeyOnly ? MAP_KEY_ITERATE_KEY : ITERATE_KEY);\n return {\n // iterator protocol\n next() {\n const { value, done } = innerIterator.next();\n return done ? { value, done } : {\n value: isPair ? [wrap(value[0]), wrap(value[1])] : wrap(value),\n done\n };\n },\n // iterable protocol\n [Symbol.iterator]() {\n return this;\n }\n };\n };\n }\n function createReadonlyMethod(type) {\n return function(...args) {\n {\n const key = args[0] ? `on key \"${args[0]}\" ` : ``;\n console.warn(`${shared.capitalize(type)} operation ${key}failed: target is readonly.`, toRaw2(this));\n }\n return type === \"delete\" ? false : this;\n };\n }\n function createInstrumentations() {\n const mutableInstrumentations2 = {\n get(key) {\n return get$1(this, key);\n },\n get size() {\n return size(this);\n },\n has: has$1,\n add,\n set: set$1,\n delete: deleteEntry,\n clear,\n forEach: createForEach(false, false)\n };\n const shallowInstrumentations2 = {\n get(key) {\n return get$1(this, key, false, true);\n },\n get size() {\n return size(this);\n },\n has: has$1,\n add,\n set: set$1,\n delete: deleteEntry,\n clear,\n forEach: createForEach(false, true)\n };\n const readonlyInstrumentations2 = {\n get(key) {\n return get$1(this, key, true);\n },\n get size() {\n return size(this, true);\n },\n has(key) {\n return has$1.call(this, key, true);\n },\n add: createReadonlyMethod(\n \"add\"\n /* ADD */\n ),\n set: createReadonlyMethod(\n \"set\"\n /* SET */\n ),\n delete: createReadonlyMethod(\n \"delete\"\n /* DELETE */\n ),\n clear: createReadonlyMethod(\n \"clear\"\n /* CLEAR */\n ),\n forEach: createForEach(true, false)\n };\n const shallowReadonlyInstrumentations2 = {\n get(key) {\n return get$1(this, key, true, true);\n },\n get size() {\n return size(this, true);\n },\n has(key) {\n return has$1.call(this, key, true);\n },\n add: createReadonlyMethod(\n \"add\"\n /* ADD */\n ),\n set: createReadonlyMethod(\n \"set\"\n /* SET */\n ),\n delete: createReadonlyMethod(\n \"delete\"\n /* DELETE */\n ),\n clear: createReadonlyMethod(\n \"clear\"\n /* CLEAR */\n ),\n forEach: createForEach(true, true)\n };\n const iteratorMethods = [\"keys\", \"values\", \"entries\", Symbol.iterator];\n iteratorMethods.forEach((method) => {\n mutableInstrumentations2[method] = createIterableMethod(method, false, false);\n readonlyInstrumentations2[method] = createIterableMethod(method, true, false);\n shallowInstrumentations2[method] = createIterableMethod(method, false, true);\n shallowReadonlyInstrumentations2[method] = createIterableMethod(method, true, true);\n });\n return [\n mutableInstrumentations2,\n readonlyInstrumentations2,\n shallowInstrumentations2,\n shallowReadonlyInstrumentations2\n ];\n }\n var [mutableInstrumentations, readonlyInstrumentations, shallowInstrumentations, shallowReadonlyInstrumentations] = /* @__PURE__ */ createInstrumentations();\n function createInstrumentationGetter(isReadonly2, shallow) {\n const instrumentations = shallow ? isReadonly2 ? shallowReadonlyInstrumentations : shallowInstrumentations : isReadonly2 ? readonlyInstrumentations : mutableInstrumentations;\n return (target, key, receiver) => {\n if (key === \"__v_isReactive\") {\n return !isReadonly2;\n } else if (key === \"__v_isReadonly\") {\n return isReadonly2;\n } else if (key === \"__v_raw\") {\n return target;\n }\n return Reflect.get(shared.hasOwn(instrumentations, key) && key in target ? instrumentations : target, key, receiver);\n };\n }\n var mutableCollectionHandlers = {\n get: /* @__PURE__ */ createInstrumentationGetter(false, false)\n };\n var shallowCollectionHandlers = {\n get: /* @__PURE__ */ createInstrumentationGetter(false, true)\n };\n var readonlyCollectionHandlers = {\n get: /* @__PURE__ */ createInstrumentationGetter(true, false)\n };\n var shallowReadonlyCollectionHandlers = {\n get: /* @__PURE__ */ createInstrumentationGetter(true, true)\n };\n function checkIdentityKeys(target, has2, key) {\n const rawKey = toRaw2(key);\n if (rawKey !== key && has2.call(target, rawKey)) {\n const type = shared.toRawType(target);\n console.warn(`Reactive ${type} contains both the raw and reactive versions of the same object${type === `Map` ? ` as keys` : ``}, which can lead to inconsistencies. Avoid differentiating between the raw and reactive versions of an object and only use the reactive version if possible.`);\n }\n }\n var reactiveMap = /* @__PURE__ */ new WeakMap();\n var shallowReactiveMap = /* @__PURE__ */ new WeakMap();\n var readonlyMap = /* @__PURE__ */ new WeakMap();\n var shallowReadonlyMap = /* @__PURE__ */ new WeakMap();\n function targetTypeMap(rawType) {\n switch (rawType) {\n case \"Object\":\n case \"Array\":\n return 1;\n case \"Map\":\n case \"Set\":\n case \"WeakMap\":\n case \"WeakSet\":\n return 2;\n default:\n return 0;\n }\n }\n function getTargetType(value) {\n return value[\n \"__v_skip\"\n /* SKIP */\n ] || !Object.isExtensible(value) ? 0 : targetTypeMap(shared.toRawType(value));\n }\n function reactive3(target) {\n if (target && target[\n \"__v_isReadonly\"\n /* IS_READONLY */\n ]) {\n return target;\n }\n return createReactiveObject(target, false, mutableHandlers, mutableCollectionHandlers, reactiveMap);\n }\n function shallowReactive(target) {\n return createReactiveObject(target, false, shallowReactiveHandlers, shallowCollectionHandlers, shallowReactiveMap);\n }\n function readonly(target) {\n return createReactiveObject(target, true, readonlyHandlers, readonlyCollectionHandlers, readonlyMap);\n }\n function shallowReadonly(target) {\n return createReactiveObject(target, true, shallowReadonlyHandlers, shallowReadonlyCollectionHandlers, shallowReadonlyMap);\n }\n function createReactiveObject(target, isReadonly2, baseHandlers, collectionHandlers, proxyMap) {\n if (!shared.isObject(target)) {\n {\n console.warn(`value cannot be made reactive: ${String(target)}`);\n }\n return target;\n }\n if (target[\n \"__v_raw\"\n /* RAW */\n ] && !(isReadonly2 && target[\n \"__v_isReactive\"\n /* IS_REACTIVE */\n ])) {\n return target;\n }\n const existingProxy = proxyMap.get(target);\n if (existingProxy) {\n return existingProxy;\n }\n const targetType = getTargetType(target);\n if (targetType === 0) {\n return target;\n }\n const proxy = new Proxy(target, targetType === 2 ? collectionHandlers : baseHandlers);\n proxyMap.set(target, proxy);\n return proxy;\n }\n function isReactive2(value) {\n if (isReadonly(value)) {\n return isReactive2(value[\n \"__v_raw\"\n /* RAW */\n ]);\n }\n return !!(value && value[\n \"__v_isReactive\"\n /* IS_REACTIVE */\n ]);\n }\n function isReadonly(value) {\n return !!(value && value[\n \"__v_isReadonly\"\n /* IS_READONLY */\n ]);\n }\n function isProxy(value) {\n return isReactive2(value) || isReadonly(value);\n }\n function toRaw2(observed) {\n return observed && toRaw2(observed[\n \"__v_raw\"\n /* RAW */\n ]) || observed;\n }\n function markRaw(value) {\n shared.def(value, \"__v_skip\", true);\n return value;\n }\n var convert = (val) => shared.isObject(val) ? reactive3(val) : val;\n function isRef(r) {\n return Boolean(r && r.__v_isRef === true);\n }\n function ref(value) {\n return createRef(value);\n }\n function shallowRef(value) {\n return createRef(value, true);\n }\n var RefImpl = class {\n constructor(value, _shallow = false) {\n this._shallow = _shallow;\n this.__v_isRef = true;\n this._rawValue = _shallow ? value : toRaw2(value);\n this._value = _shallow ? value : convert(value);\n }\n get value() {\n track(toRaw2(this), \"get\", \"value\");\n return this._value;\n }\n set value(newVal) {\n newVal = this._shallow ? newVal : toRaw2(newVal);\n if (shared.hasChanged(newVal, this._rawValue)) {\n this._rawValue = newVal;\n this._value = this._shallow ? newVal : convert(newVal);\n trigger(toRaw2(this), \"set\", \"value\", newVal);\n }\n }\n };\n function createRef(rawValue, shallow = false) {\n if (isRef(rawValue)) {\n return rawValue;\n }\n return new RefImpl(rawValue, shallow);\n }\n function triggerRef(ref2) {\n trigger(toRaw2(ref2), \"set\", \"value\", ref2.value);\n }\n function unref(ref2) {\n return isRef(ref2) ? ref2.value : ref2;\n }\n var shallowUnwrapHandlers = {\n get: (target, key, receiver) => unref(Reflect.get(target, key, receiver)),\n set: (target, key, value, receiver) => {\n const oldValue = target[key];\n if (isRef(oldValue) && !isRef(value)) {\n oldValue.value = value;\n return true;\n } else {\n return Reflect.set(target, key, value, receiver);\n }\n }\n };\n function proxyRefs(objectWithRefs) {\n return isReactive2(objectWithRefs) ? objectWithRefs : new Proxy(objectWithRefs, shallowUnwrapHandlers);\n }\n var CustomRefImpl = class {\n constructor(factory) {\n this.__v_isRef = true;\n const { get: get3, set: set3 } = factory(() => track(this, \"get\", \"value\"), () => trigger(this, \"set\", \"value\"));\n this._get = get3;\n this._set = set3;\n }\n get value() {\n return this._get();\n }\n set value(newVal) {\n this._set(newVal);\n }\n };\n function customRef(factory) {\n return new CustomRefImpl(factory);\n }\n function toRefs(object) {\n if (!isProxy(object)) {\n console.warn(`toRefs() expects a reactive object but received a plain one.`);\n }\n const ret = shared.isArray(object) ? new Array(object.length) : {};\n for (const key in object) {\n ret[key] = toRef(object, key);\n }\n return ret;\n }\n var ObjectRefImpl = class {\n constructor(_object, _key) {\n this._object = _object;\n this._key = _key;\n this.__v_isRef = true;\n }\n get value() {\n return this._object[this._key];\n }\n set value(newVal) {\n this._object[this._key] = newVal;\n }\n };\n function toRef(object, key) {\n return isRef(object[key]) ? object[key] : new ObjectRefImpl(object, key);\n }\n var ComputedRefImpl = class {\n constructor(getter, _setter, isReadonly2) {\n this._setter = _setter;\n this._dirty = true;\n this.__v_isRef = true;\n this.effect = effect3(getter, {\n lazy: true,\n scheduler: () => {\n if (!this._dirty) {\n this._dirty = true;\n trigger(toRaw2(this), \"set\", \"value\");\n }\n }\n });\n this[\n \"__v_isReadonly\"\n /* IS_READONLY */\n ] = isReadonly2;\n }\n get value() {\n const self2 = toRaw2(this);\n if (self2._dirty) {\n self2._value = this.effect();\n self2._dirty = false;\n }\n track(self2, \"get\", \"value\");\n return self2._value;\n }\n set value(newValue) {\n this._setter(newValue);\n }\n };\n function computed(getterOrOptions) {\n let getter;\n let setter;\n if (shared.isFunction(getterOrOptions)) {\n getter = getterOrOptions;\n setter = () => {\n console.warn(\"Write operation failed: computed value is readonly\");\n };\n } else {\n getter = getterOrOptions.get;\n setter = getterOrOptions.set;\n }\n return new ComputedRefImpl(getter, setter, shared.isFunction(getterOrOptions) || !getterOrOptions.set);\n }\n exports.ITERATE_KEY = ITERATE_KEY;\n exports.computed = computed;\n exports.customRef = customRef;\n exports.effect = effect3;\n exports.enableTracking = enableTracking;\n exports.isProxy = isProxy;\n exports.isReactive = isReactive2;\n exports.isReadonly = isReadonly;\n exports.isRef = isRef;\n exports.markRaw = markRaw;\n exports.pauseTracking = pauseTracking;\n exports.proxyRefs = proxyRefs;\n exports.reactive = reactive3;\n exports.readonly = readonly;\n exports.ref = ref;\n exports.resetTracking = resetTracking;\n exports.shallowReactive = shallowReactive;\n exports.shallowReadonly = shallowReadonly;\n exports.shallowRef = shallowRef;\n exports.stop = stop2;\n exports.toRaw = toRaw2;\n exports.toRef = toRef;\n exports.toRefs = toRefs;\n exports.track = track;\n exports.trigger = trigger;\n exports.triggerRef = triggerRef;\n exports.unref = unref;\n }\n});\n\n// node_modules/@vue/reactivity/index.js\nvar require_reactivity = __commonJS({\n \"node_modules/@vue/reactivity/index.js\"(exports, module2) {\n \"use strict\";\n if (false) {\n module2.exports = null;\n } else {\n module2.exports = require_reactivity_cjs();\n }\n }\n});\n\n// packages/csp/builds/module.js\nvar module_exports = {};\n__export(module_exports, {\n Alpine: () => src_default,\n default: () => module_default\n});\nmodule.exports = __toCommonJS(module_exports);\n\n// packages/alpinejs/src/scheduler.js\nvar flushPending = false;\nvar flushing = false;\nvar queue = [];\nvar lastFlushedIndex = -1;\nvar transactionActive = false;\nfunction scheduler(callback) {\n queueJob(callback);\n}\nfunction startTransaction() {\n transactionActive = true;\n}\nfunction commitTransaction() {\n transactionActive = false;\n queueFlush();\n}\nfunction queueJob(job) {\n if (!queue.includes(job))\n queue.push(job);\n queueFlush();\n}\nfunction dequeueJob(job) {\n let index = queue.indexOf(job);\n if (index !== -1 && index > lastFlushedIndex)\n queue.splice(index, 1);\n}\nfunction queueFlush() {\n if (!flushing && !flushPending) {\n if (transactionActive)\n return;\n flushPending = true;\n queueMicrotask(flushJobs);\n }\n}\nfunction flushJobs() {\n flushPending = false;\n flushing = true;\n for (let i = 0; i < queue.length; i++) {\n queue[i]();\n lastFlushedIndex = i;\n }\n queue.length = 0;\n lastFlushedIndex = -1;\n flushing = false;\n}\n\n// packages/alpinejs/src/reactivity.js\nvar reactive;\nvar effect;\nvar release;\nvar raw;\nvar shouldSchedule = true;\nfunction disableEffectScheduling(callback) {\n shouldSchedule = false;\n callback();\n shouldSchedule = true;\n}\nfunction setReactivityEngine(engine) {\n reactive = engine.reactive;\n release = engine.release;\n effect = (callback) => engine.effect(callback, { scheduler: (task) => {\n if (shouldSchedule) {\n scheduler(task);\n } else {\n task();\n }\n } });\n raw = engine.raw;\n}\nfunction overrideEffect(override) {\n effect = override;\n}\nfunction elementBoundEffect(el) {\n let cleanup = () => {\n };\n let wrappedEffect = (callback) => {\n let effectReference = effect(callback);\n if (!el._x_effects) {\n el._x_effects = /* @__PURE__ */ new Set();\n el._x_runEffects = () => {\n el._x_effects.forEach((i) => i());\n };\n }\n el._x_effects.add(effectReference);\n cleanup = () => {\n if (effectReference === void 0)\n return;\n el._x_effects.delete(effectReference);\n release(effectReference);\n };\n return effectReference;\n };\n return [wrappedEffect, () => {\n cleanup();\n }];\n}\nfunction watch(getter, callback) {\n let firstTime = true;\n let oldValue;\n let oldValueJSON;\n let effectReference = effect(() => {\n let value = getter();\n let newJSON = JSON.stringify(value);\n if (!firstTime) {\n if (typeof value === \"object\" || value !== oldValue) {\n let previousValue = typeof oldValue === \"object\" ? JSON.parse(oldValueJSON) : oldValue;\n queueMicrotask(() => {\n callback(value, previousValue);\n });\n }\n }\n oldValue = value;\n oldValueJSON = newJSON;\n firstTime = false;\n });\n return () => release(effectReference);\n}\nasync function transaction(callback) {\n startTransaction();\n try {\n await callback();\n await Promise.resolve();\n } finally {\n commitTransaction();\n }\n}\n\n// packages/alpinejs/src/mutation.js\nvar onAttributeAddeds = [];\nvar onElRemoveds = [];\nvar onElAddeds = [];\nfunction onElAdded(callback) {\n onElAddeds.push(callback);\n}\nfunction onElRemoved(el, callback) {\n if (typeof callback === \"function\") {\n if (!el._x_cleanups)\n el._x_cleanups = [];\n el._x_cleanups.push(callback);\n } else {\n callback = el;\n onElRemoveds.push(callback);\n }\n}\nfunction onAttributesAdded(callback) {\n onAttributeAddeds.push(callback);\n}\nfunction onAttributeRemoved(el, name, callback) {\n if (!el._x_attributeCleanups)\n el._x_attributeCleanups = {};\n if (!el._x_attributeCleanups[name])\n el._x_attributeCleanups[name] = [];\n el._x_attributeCleanups[name].push(callback);\n}\nfunction cleanupAttributes(el, names) {\n if (!el._x_attributeCleanups)\n return;\n Object.entries(el._x_attributeCleanups).forEach(([name, value]) => {\n if (names === void 0 || names.includes(name)) {\n value.forEach((i) => i());\n delete el._x_attributeCleanups[name];\n }\n });\n}\nfunction cleanupElement(el) {\n var _a, _b;\n (_a = el._x_effects) == null ? void 0 : _a.forEach(dequeueJob);\n while ((_b = el._x_cleanups) == null ? void 0 : _b.length)\n el._x_cleanups.pop()();\n}\nvar observer = new MutationObserver(onMutate);\nvar currentlyObserving = false;\nfunction startObservingMutations() {\n observer.observe(document, { subtree: true, childList: true, attributes: true, attributeOldValue: true });\n currentlyObserving = true;\n}\nfunction stopObservingMutations() {\n flushObserver();\n observer.disconnect();\n currentlyObserving = false;\n}\nvar queuedMutations = [];\nfunction flushObserver() {\n let records = observer.takeRecords();\n queuedMutations.push(() => records.length > 0 && onMutate(records));\n let queueLengthWhenTriggered = queuedMutations.length;\n queueMicrotask(() => {\n if (queuedMutations.length === queueLengthWhenTriggered) {\n while (queuedMutations.length > 0)\n queuedMutations.shift()();\n }\n });\n}\nfunction mutateDom(callback) {\n if (!currentlyObserving)\n return callback();\n stopObservingMutations();\n let result = callback();\n startObservingMutations();\n return result;\n}\nvar isCollecting = false;\nvar deferredMutations = [];\nfunction deferMutations() {\n isCollecting = true;\n}\nfunction flushAndStopDeferringMutations() {\n isCollecting = false;\n onMutate(deferredMutations);\n deferredMutations = [];\n}\nfunction onMutate(mutations) {\n if (isCollecting) {\n deferredMutations = deferredMutations.concat(mutations);\n return;\n }\n let addedNodes = [];\n let removedNodes = /* @__PURE__ */ new Set();\n let addedAttributes = /* @__PURE__ */ new Map();\n let removedAttributes = /* @__PURE__ */ new Map();\n for (let i = 0; i < mutations.length; i++) {\n if (mutations[i].target._x_ignoreMutationObserver)\n continue;\n if (mutations[i].type === \"childList\") {\n mutations[i].removedNodes.forEach((node) => {\n if (node.nodeType !== 1)\n return;\n if (!node._x_marker)\n return;\n removedNodes.add(node);\n });\n mutations[i].addedNodes.forEach((node) => {\n if (node.nodeType !== 1)\n return;\n if (removedNodes.has(node)) {\n removedNodes.delete(node);\n return;\n }\n if (node._x_marker)\n return;\n addedNodes.push(node);\n });\n }\n if (mutations[i].type === \"attributes\") {\n let el = mutations[i].target;\n let name = mutations[i].attributeName;\n let oldValue = mutations[i].oldValue;\n let add = () => {\n if (!addedAttributes.has(el))\n addedAttributes.set(el, []);\n addedAttributes.get(el).push({ name, value: el.getAttribute(name) });\n };\n let remove = () => {\n if (!removedAttributes.has(el))\n removedAttributes.set(el, []);\n removedAttributes.get(el).push(name);\n };\n if (el.hasAttribute(name) && oldValue === null) {\n add();\n } else if (el.hasAttribute(name)) {\n remove();\n add();\n } else {\n remove();\n }\n }\n }\n removedAttributes.forEach((attrs, el) => {\n cleanupAttributes(el, attrs);\n });\n addedAttributes.forEach((attrs, el) => {\n onAttributeAddeds.forEach((i) => i(el, attrs));\n });\n for (let node of removedNodes) {\n if (addedNodes.some((i) => i.contains(node)))\n continue;\n onElRemoveds.forEach((i) => i(node));\n }\n for (let node of addedNodes) {\n if (!node.isConnected)\n continue;\n onElAddeds.forEach((i) => i(node));\n }\n addedNodes = null;\n removedNodes = null;\n addedAttributes = null;\n removedAttributes = null;\n}\n\n// packages/alpinejs/src/scope.js\nfunction scope(node) {\n return mergeProxies(closestDataStack(node));\n}\nfunction addScopeToNode(node, data2, referenceNode) {\n node._x_dataStack = [data2, ...closestDataStack(referenceNode || node)];\n return () => {\n node._x_dataStack = node._x_dataStack.filter((i) => i !== data2);\n };\n}\nfunction closestDataStack(node) {\n if (node._x_dataStack)\n return node._x_dataStack;\n if (typeof ShadowRoot === \"function\" && node instanceof ShadowRoot) {\n return closestDataStack(node.host);\n }\n if (!node.parentNode) {\n return [];\n }\n return closestDataStack(node.parentNode);\n}\nfunction mergeProxies(objects) {\n return new Proxy({ objects }, mergeProxyTrap);\n}\nfunction keyInPrototypeChain(obj, key) {\n if (obj === null || obj === Object.prototype)\n return null;\n if (Object.prototype.hasOwnProperty.call(obj, key))\n return obj;\n return keyInPrototypeChain(Object.getPrototypeOf(obj), key);\n}\nvar mergeProxyTrap = {\n ownKeys({ objects }) {\n return Array.from(\n new Set(objects.flatMap((i) => Object.keys(i)))\n );\n },\n has({ objects }, name) {\n if (name == Symbol.unscopables)\n return false;\n return objects.some(\n (obj) => Object.prototype.hasOwnProperty.call(obj, name) || Reflect.has(obj, name)\n );\n },\n get({ objects }, name, thisProxy) {\n if (name == \"toJSON\")\n return collapseProxies;\n return Reflect.get(\n objects.find(\n (obj) => Reflect.has(obj, name)\n ) || {},\n name,\n thisProxy\n );\n },\n set({ objects }, name, value, thisProxy) {\n let target;\n for (const obj of objects) {\n target = keyInPrototypeChain(obj, name);\n if (target)\n break;\n }\n if (!target)\n target = objects[objects.length - 1];\n const descriptor = Object.getOwnPropertyDescriptor(target, name);\n if ((descriptor == null ? void 0 : descriptor.set) && (descriptor == null ? void 0 : descriptor.get))\n return descriptor.set.call(thisProxy, value) || true;\n return Reflect.set(target, name, value);\n }\n};\nfunction collapseProxies() {\n let keys = Reflect.ownKeys(this);\n return keys.reduce((acc, key) => {\n acc[key] = Reflect.get(this, key);\n return acc;\n }, {});\n}\n\n// packages/alpinejs/src/interceptor.js\nfunction initInterceptors(data2) {\n let isObject2 = (val) => typeof val === \"object\" && !Array.isArray(val) && val !== null;\n let recurse = (obj, basePath = \"\") => {\n Object.entries(Object.getOwnPropertyDescriptors(obj)).forEach(([key, { value, enumerable }]) => {\n if (enumerable === false || value === void 0)\n return;\n if (typeof value === \"object\" && value !== null && value.__v_skip)\n return;\n let path = basePath === \"\" ? key : `${basePath}.${key}`;\n if (typeof value === \"object\" && value !== null && value._x_interceptor) {\n obj[key] = value.initialize(data2, path, key);\n } else {\n if (isObject2(value) && value !== obj && !(value instanceof Element)) {\n recurse(value, path);\n }\n }\n });\n };\n return recurse(data2);\n}\nfunction interceptor(callback, mutateObj = () => {\n}) {\n let obj = {\n initialValue: void 0,\n _x_interceptor: true,\n initialize(data2, path, key) {\n return callback(this.initialValue, () => get(data2, path), (value) => set(data2, path, value), path, key);\n }\n };\n mutateObj(obj);\n return (initialValue) => {\n if (typeof initialValue === \"object\" && initialValue !== null && initialValue._x_interceptor) {\n let initialize = obj.initialize.bind(obj);\n obj.initialize = (data2, path, key) => {\n let innerValue = initialValue.initialize(data2, path, key);\n obj.initialValue = innerValue;\n return initialize(data2, path, key);\n };\n } else {\n obj.initialValue = initialValue;\n }\n return obj;\n };\n}\nfunction get(obj, path) {\n return path.split(\".\").reduce((carry, segment) => carry[segment], obj);\n}\nfunction set(obj, path, value) {\n if (typeof path === \"string\")\n path = path.split(\".\");\n if (path.length === 1)\n obj[path[0]] = value;\n else if (path.length === 0)\n throw error;\n else {\n if (obj[path[0]])\n return set(obj[path[0]], path.slice(1), value);\n else {\n obj[path[0]] = {};\n return set(obj[path[0]], path.slice(1), value);\n }\n }\n}\n\n// packages/alpinejs/src/magics.js\nvar magics = {};\nfunction magic(name, callback) {\n magics[name] = callback;\n}\nfunction injectMagics(obj, el) {\n let memoizedUtilities = getUtilities(el);\n Object.entries(magics).forEach(([name, callback]) => {\n Object.defineProperty(obj, `$${name}`, {\n get() {\n return callback(el, memoizedUtilities);\n },\n enumerable: false\n });\n });\n return obj;\n}\nfunction getUtilities(el) {\n let [utilities, cleanup] = getElementBoundUtilities(el);\n let utils = { interceptor, ...utilities };\n onElRemoved(el, cleanup);\n return utils;\n}\n\n// packages/alpinejs/src/utils/error.js\nfunction tryCatch(el, expression, callback, ...args) {\n try {\n return callback(...args);\n } catch (e) {\n handleError(e, el, expression);\n }\n}\nfunction handleError(...args) {\n return errorHandler(...args);\n}\nvar errorHandler = normalErrorHandler;\nfunction setErrorHandler(handler4) {\n errorHandler = handler4;\n}\nfunction normalErrorHandler(error2, el, expression = void 0) {\n error2 = Object.assign(\n error2 != null ? error2 : { message: \"No error message given.\" },\n { el, expression }\n );\n console.warn(`Alpine Expression Error: ${error2.message}\n\n${expression ? 'Expression: \"' + expression + '\"\\n\\n' : \"\"}`, el);\n setTimeout(() => {\n throw error2;\n }, 0);\n}\n\n// packages/alpinejs/src/evaluator.js\nvar shouldAutoEvaluateFunctions = true;\nfunction dontAutoEvaluateFunctions(callback) {\n let cache = shouldAutoEvaluateFunctions;\n shouldAutoEvaluateFunctions = false;\n let result = callback();\n shouldAutoEvaluateFunctions = cache;\n return result;\n}\nfunction evaluate(el, expression, extras = {}) {\n let result;\n evaluateLater(el, expression)((value) => result = value, extras);\n return result;\n}\nfunction evaluateLater(...args) {\n return theEvaluatorFunction(...args);\n}\nvar theEvaluatorFunction = () => {\n};\nfunction setEvaluator(newEvaluator) {\n theEvaluatorFunction = newEvaluator;\n}\nvar theRawEvaluatorFunction;\nfunction setRawEvaluator(newEvaluator) {\n theRawEvaluatorFunction = newEvaluator;\n}\nfunction generateEvaluatorFromFunction(dataStack, func) {\n return (receiver = () => {\n }, { scope: scope2 = {}, params = [], context } = {}) => {\n if (!shouldAutoEvaluateFunctions) {\n runIfTypeOfFunction(receiver, func, mergeProxies([scope2, ...dataStack]), params);\n return;\n }\n let result = func.apply(mergeProxies([scope2, ...dataStack]), params);\n runIfTypeOfFunction(receiver, result);\n };\n}\nfunction runIfTypeOfFunction(receiver, value, scope2, params, el) {\n if (shouldAutoEvaluateFunctions && typeof value === \"function\") {\n let result = value.apply(scope2, params);\n if (result instanceof Promise) {\n result.then((i) => runIfTypeOfFunction(receiver, i, scope2, params)).catch((error2) => handleError(error2, el, value));\n } else {\n receiver(result);\n }\n } else if (typeof value === \"object\" && value instanceof Promise) {\n value.then((i) => receiver(i));\n } else {\n receiver(value);\n }\n}\nfunction evaluateRaw(...args) {\n return theRawEvaluatorFunction(...args);\n}\n\n// packages/alpinejs/src/directives.js\nvar prefixAsString = \"x-\";\nfunction prefix(subject = \"\") {\n return prefixAsString + subject;\n}\nfunction setPrefix(newPrefix) {\n prefixAsString = newPrefix;\n}\nvar directiveHandlers = {};\nfunction directive(name, callback) {\n directiveHandlers[name] = callback;\n return {\n before(directive2) {\n if (!directiveHandlers[directive2]) {\n console.warn(String.raw`Cannot find directive \\`${directive2}\\`. \\`${name}\\` will use the default order of execution`);\n return;\n }\n const pos = directiveOrder.indexOf(directive2);\n directiveOrder.splice(pos >= 0 ? pos : directiveOrder.indexOf(\"DEFAULT\"), 0, name);\n }\n };\n}\nfunction directiveExists(name) {\n return Object.keys(directiveHandlers).includes(name);\n}\nfunction directives(el, attributes, originalAttributeOverride) {\n attributes = Array.from(attributes);\n if (el._x_virtualDirectives) {\n let vAttributes = Object.entries(el._x_virtualDirectives).map(([name, value]) => ({ name, value }));\n let staticAttributes = attributesOnly(vAttributes);\n vAttributes = vAttributes.map((attribute) => {\n if (staticAttributes.find((attr) => attr.name === attribute.name)) {\n return {\n name: `x-bind:${attribute.name}`,\n value: `\"${attribute.value}\"`\n };\n }\n return attribute;\n });\n attributes = attributes.concat(vAttributes);\n }\n let transformedAttributeMap = {};\n let directives2 = attributes.map(toTransformedAttributes((newName, oldName) => transformedAttributeMap[newName] = oldName)).filter(outNonAlpineAttributes).map(toParsedDirectives(transformedAttributeMap, originalAttributeOverride)).sort(byPriority);\n return directives2.map((directive2) => {\n return getDirectiveHandler(el, directive2);\n });\n}\nfunction attributesOnly(attributes) {\n return Array.from(attributes).map(toTransformedAttributes()).filter((attr) => !outNonAlpineAttributes(attr));\n}\nvar isDeferringHandlers = false;\nvar directiveHandlerStacks = /* @__PURE__ */ new Map();\nvar currentHandlerStackKey = Symbol();\nfunction deferHandlingDirectives(callback) {\n isDeferringHandlers = true;\n let key = Symbol();\n currentHandlerStackKey = key;\n directiveHandlerStacks.set(key, []);\n let flushHandlers = () => {\n while (directiveHandlerStacks.get(key).length)\n directiveHandlerStacks.get(key).shift()();\n directiveHandlerStacks.delete(key);\n };\n let stopDeferring = () => {\n isDeferringHandlers = false;\n flushHandlers();\n };\n callback(flushHandlers);\n stopDeferring();\n}\nfunction getElementBoundUtilities(el) {\n let cleanups = [];\n let cleanup = (callback) => cleanups.push(callback);\n let [effect3, cleanupEffect] = elementBoundEffect(el);\n cleanups.push(cleanupEffect);\n let utilities = {\n Alpine: alpine_default,\n effect: effect3,\n cleanup,\n evaluateLater: evaluateLater.bind(evaluateLater, el),\n evaluate: evaluate.bind(evaluate, el)\n };\n let doCleanup = () => cleanups.forEach((i) => i());\n return [utilities, doCleanup];\n}\nfunction getDirectiveHandler(el, directive2) {\n let noop = () => {\n };\n let handler4 = directiveHandlers[directive2.type] || noop;\n let [utilities, cleanup] = getElementBoundUtilities(el);\n onAttributeRemoved(el, directive2.original, cleanup);\n let fullHandler = () => {\n if (el._x_ignore || el._x_ignoreSelf)\n return;\n handler4.inline && handler4.inline(el, directive2, utilities);\n handler4 = handler4.bind(handler4, el, directive2, utilities);\n isDeferringHandlers ? directiveHandlerStacks.get(currentHandlerStackKey).push(handler4) : handler4();\n };\n fullHandler.runCleanups = cleanup;\n return fullHandler;\n}\nvar startingWith = (subject, replacement) => ({ name, value }) => {\n if (name.startsWith(subject))\n name = name.replace(subject, replacement);\n return { name, value };\n};\nvar into = (i) => i;\nfunction toTransformedAttributes(callback = () => {\n}) {\n return ({ name, value }) => {\n let { name: newName, value: newValue } = attributeTransformers.reduce((carry, transform) => {\n return transform(carry);\n }, { name, value });\n if (newName !== name)\n callback(newName, name);\n return { name: newName, value: newValue };\n };\n}\nvar attributeTransformers = [];\nfunction mapAttributes(callback) {\n attributeTransformers.push(callback);\n}\nfunction outNonAlpineAttributes({ name }) {\n return alpineAttributeRegex().test(name);\n}\nvar alpineAttributeRegex = () => new RegExp(`^${prefixAsString}([^:^.]+)\\\\b`);\nfunction toParsedDirectives(transformedAttributeMap, originalAttributeOverride) {\n return ({ name, value }) => {\n if (name === value)\n value = \"\";\n let typeMatch = name.match(alpineAttributeRegex());\n let valueMatch = name.match(/:([a-zA-Z0-9\\-_:]+)/);\n let modifiers = name.match(/\\.[^.\\]]+(?=[^\\]]*$)/g) || [];\n let original = originalAttributeOverride || transformedAttributeMap[name] || name;\n return {\n type: typeMatch ? typeMatch[1] : null,\n value: valueMatch ? valueMatch[1] : null,\n modifiers: modifiers.map((i) => i.replace(\".\", \"\")),\n expression: value,\n original\n };\n };\n}\nvar DEFAULT = \"DEFAULT\";\nvar directiveOrder = [\n \"ignore\",\n \"ref\",\n \"id\",\n \"data\",\n \"anchor\",\n \"bind\",\n \"init\",\n \"for\",\n \"model\",\n \"modelable\",\n \"transition\",\n \"show\",\n \"if\",\n DEFAULT,\n \"teleport\"\n];\nfunction byPriority(a, b) {\n let typeA = directiveOrder.indexOf(a.type) === -1 ? DEFAULT : a.type;\n let typeB = directiveOrder.indexOf(b.type) === -1 ? DEFAULT : b.type;\n return directiveOrder.indexOf(typeA) - directiveOrder.indexOf(typeB);\n}\n\n// packages/alpinejs/src/utils/dispatch.js\nfunction dispatch(el, name, detail = {}, options = {}) {\n return el.dispatchEvent(\n new CustomEvent(name, {\n detail,\n bubbles: true,\n // Allows events to pass the shadow DOM barrier.\n composed: true,\n cancelable: true,\n // Allows overriding the default event options.\n ...options\n })\n );\n}\n\n// packages/alpinejs/src/utils/walk.js\nfunction walk(el, callback) {\n if (typeof ShadowRoot === \"function\" && el instanceof ShadowRoot) {\n Array.from(el.children).forEach((el2) => walk(el2, callback));\n return;\n }\n let skip = false;\n callback(el, () => skip = true);\n if (skip)\n return;\n let node = el.firstElementChild;\n while (node) {\n walk(node, callback, false);\n node = node.nextElementSibling;\n }\n}\n\n// packages/alpinejs/src/utils/warn.js\nfunction warn(message, ...args) {\n console.warn(`Alpine Warning: ${message}`, ...args);\n}\n\n// packages/alpinejs/src/lifecycle.js\nvar started = false;\nfunction start() {\n if (started)\n warn(\"Alpine has already been initialized on this page. Calling Alpine.start() more than once can cause problems.\");\n started = true;\n if (!document.body)\n warn(\"Unable to initialize. Trying to load Alpine before `` is available. Did you forget to add `defer` in Alpine's `
-
-
-
-
-
-
-
-
-
-
-
- @include('layouts.partials.topbar')
-
-
- {{ $slot }}
-
-
-
-
- @fluxScripts
-
-
-
-
-
+ The real WMS application/sidebar layout lives in resources/views/layouts/app/sidebar.blade.php.
+ Keep this file as a thin delegate only; do not duplicate sidebar markup here.
+--}}
+@include('layouts.app.sidebar')
diff --git a/resources/views/components/layouts/sidebar.blade.php b/resources/views/components/layouts/sidebar.blade.php
index 98c8d473..e239b913 100644
--- a/resources/views/components/layouts/sidebar.blade.php
+++ b/resources/views/components/layouts/sidebar.blade.php
@@ -1,904 +1,7 @@
-
-
-@include('partials.head')
+{{--
+ Compatibility component for legacy references.
-
-
-
-
-
- @vite(['resources/css/app.css'])
-
-
-
-
-
-
-
- @include('layouts.partials.topbar')
-
-
- {{ $slot }}
-
-
-
-
-
-
-
-
- @fluxScripts
-
-
-
-
-
+ The real WMS application/sidebar layout lives in resources/views/layouts/app/sidebar.blade.php.
+ Keep this file as a thin delegate only; do not duplicate sidebar markup here.
+--}}
+@include('layouts.app.sidebar')
diff --git a/resources/views/components/wms/page-hero.blade.php b/resources/views/components/wms/page-hero.blade.php
index 1ff5daac..fd15c5fb 100644
--- a/resources/views/components/wms/page-hero.blade.php
+++ b/resources/views/components/wms/page-hero.blade.php
@@ -2,40 +2,52 @@
'eyebrow' => 'MPBA WMS',
'title',
'description' => null,
+ 'subtitle' => null,
'icon' => null,
'stats' => [],
])
+@php
+ $heroDescription = $description ?? $subtitle ?? null;
+@endphp
+
merge(['class' => 'wms-page-hero']) }}>
-
-
{{ $eyebrow }}
-
{{ $title }}
- @if ($description)
-
{{ $description }}
- @endif
-
+
+
+
+
{{ $eyebrow }}
+
{{ $title }}
-
- @foreach ($stats as $stat)
-
- {{ $stat['label'] ?? '' }}
-
- @if (!empty($stat['icon']))
-
+ @if ($heroDescription)
+ {{ $heroDescription }}
+ @endif
+
+
+ @if (count($stats) > 0 || $icon || $slot->isNotEmpty())
+
+ @foreach ($stats as $stat)
+
+ {{ $stat['label'] ?? '' }}
+
+ @if (!empty($stat['icon']))
+
+ @endif
+ {{ $stat['value'] ?? '' }}
+
+
+ @endforeach
+
+ @if ($icon && count($stats) === 0 && $slot->isEmpty())
+
+
+
@endif
- {{ $stat['value'] ?? '' }}
-
-
- @endforeach
- @if ($icon && count($stats) === 0)
-
-
-
- @endif
-
- @if ($slot->isNotEmpty())
- {{ $slot }}
- @endif
+ @if ($slot->isNotEmpty())
+ {{ $slot }}
+ @endif
+
+ @endif
+
diff --git a/resources/views/layouts/app/sidebar.blade.php b/resources/views/layouts/app/sidebar.blade.php
index e01384b3..6436db0c 100644
--- a/resources/views/layouts/app/sidebar.blade.php
+++ b/resources/views/layouts/app/sidebar.blade.php
@@ -279,72 +279,6 @@
-
-
@if (auth()->user()?->isMpbaPlatformAdmin())