fix: move /settings route before /{ticket} wildcard; fix null tickets on bootstrap

Route ordering caused /settings to be captured by the /{ticket} wildcard,
returning 404. Also pass safe empty pagination object when no groups exist
to prevent TypeError reading .total on null. Closes #2 #3.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joel Wedemire
2026-04-09 21:31:50 -07:00
parent a66f61c638
commit 1bc34ff16f
2 changed files with 35 additions and 30 deletions

View File

@@ -61,7 +61,7 @@ class TicketController extends Controller
$totalGroups = TicketingGroup::count(); $totalGroups = TicketingGroup::count();
if ($totalGroups === 0) { if ($totalGroups === 0) {
return Inertia::render('Ticketing/Index', [ return Inertia::render('Ticketing/Index', [
'tickets' => null, 'tickets' => ['data' => [], 'total' => 0, 'per_page' => 20, 'current_page' => 1, 'last_page' => 1],
'groups' => [], 'groups' => [],
'priorities' => [], 'priorities' => [],
'projects' => [], 'projects' => [],

View File

@@ -11,46 +11,51 @@ Route::middleware(['web', 'auth', 'app.access:ticketing'])
->name('ticketing.') ->name('ticketing.')
->group(function () { ->group(function () {
// ── Visible to anyone with module access (ticketing.view minimum) ────── // ── Fixed paths first (before any /{ticket} wildcards) ────────────────
Route::get('/my-tickets', [TicketController::class, 'myTickets'])->name('my-tickets'); Route::get('/my-tickets', [TicketController::class, 'myTickets'])->name('my-tickets');
Route::get('/attachments/{attachment}', [TicketAttachmentController::class, 'show'])->name('attachments.show'); Route::get('/attachments/{attachment}', [TicketAttachmentController::class, 'show'])->name('attachments.show');
// ── Create/view tickets ────────────────────────────────────────────────
Route::middleware('permission:ticketing.view,ticketing.manage')->group(function () { Route::middleware('permission:ticketing.view,ticketing.manage')->group(function () {
Route::get('/create', [TicketController::class, 'create'])->name('create'); Route::get('/create', [TicketController::class, 'create'])->name('create');
Route::post('/', [TicketController::class, 'store']) ->name('store'); Route::post('/', [TicketController::class, 'store']) ->name('store');
Route::get('/{ticket}', [TicketController::class, 'show']) ->name('show');
}); });
// ── Requires manage permission (agents / staff) ────────────────────────
Route::middleware('permission:ticketing.manage')->group(function () { Route::middleware('permission:ticketing.manage')->group(function () {
Route::get('/', [TicketController::class, 'index']) ->name('index'); Route::get('/', [TicketController::class, 'index'])->name('index');
Route::get('/{ticket}/edit', [TicketController::class, 'edit']) ->name('edit'); });
Route::put('/{ticket}', [TicketController::class, 'update'])->name('update');
// ── Settings (must be before /{ticket} wildcard) ──────────────────────
Route::middleware('permission:ticketing.settings')->group(function () {
Route::get('/settings', [TicketingSettingsController::class, 'index']) ->name('settings');
Route::post('/settings/groups', [TicketingSettingsController::class, 'storeGroup']) ->name('settings.groups.store');
Route::put('/settings/groups/{group}', [TicketingSettingsController::class, 'updateGroup']) ->name('settings.groups.update');
Route::post('/settings/agents', [TicketingSettingsController::class, 'storeAgent']) ->name('settings.agents.store');
Route::delete('/settings/agents/{access}', [TicketingSettingsController::class, 'destroyAgent']) ->name('settings.agents.destroy');
Route::post('/settings/priorities', [TicketingSettingsController::class, 'storePriority']) ->name('settings.priorities.store');
Route::put('/settings/priorities/{priority}', [TicketingSettingsController::class, 'updatePriority']) ->name('settings.priorities.update');
Route::delete('/settings/priorities/{priority}', [TicketingSettingsController::class, 'destroyPriority']) ->name('settings.priorities.destroy');
Route::post('/settings/projects', [TicketingSettingsController::class, 'storeProject']) ->name('settings.projects.store');
Route::put('/settings/projects/{project}', [TicketingSettingsController::class, 'updateProject']) ->name('settings.projects.update');
Route::delete('/settings/projects/{project}', [TicketingSettingsController::class, 'destroyProject']) ->name('settings.projects.destroy');
Route::post('/settings/email', [TicketingSettingsController::class, 'storeEmailConnection'])->name('settings.email.store');
Route::put('/settings/email/{connection}', [TicketingSettingsController::class, 'updateEmailConnection'])->name('settings.email.update');
Route::delete('/settings/email/{connection}', [TicketingSettingsController::class, 'destroyEmailConnection'])->name('settings.email.destroy');
});
// ── Wildcard /{ticket} routes (last, after all fixed paths) ──────────
Route::middleware('permission:ticketing.view,ticketing.manage')->group(function () {
Route::get('/{ticket}', [TicketController::class, 'show'])->name('show');
});
Route::middleware('permission:ticketing.manage')->group(function () {
Route::get('/{ticket}/edit', [TicketController::class, 'edit']) ->name('edit');
Route::put('/{ticket}', [TicketController::class, 'update']) ->name('update');
Route::delete('/{ticket}', [TicketController::class, 'destroy'])->name('destroy'); Route::delete('/{ticket}', [TicketController::class, 'destroy'])->name('destroy');
}); });
// ── Messages and attachments (view or manage) ──────────────────────────
Route::middleware('permission:ticketing.view,ticketing.manage')->group(function () { Route::middleware('permission:ticketing.view,ticketing.manage')->group(function () {
Route::post('/{ticket}/messages', [TicketMessageController::class, 'store']) ->name('messages.store'); Route::post('/{ticket}/messages', [TicketMessageController::class, 'store']) ->name('messages.store');
Route::post('/{ticket}/attachments', [TicketAttachmentController::class, 'store'])->name('attachments.store'); Route::post('/{ticket}/attachments', [TicketAttachmentController::class, 'store'])->name('attachments.store');
}); });
// ── Requires settings permission ───────────────────────────────────────
Route::middleware('permission:ticketing.settings')->group(function () {
Route::get('/settings', [TicketingSettingsController::class, 'index']) ->name('settings');
Route::post('/settings/groups', [TicketingSettingsController::class, 'storeGroup']) ->name('settings.groups.store');
Route::put('/settings/groups/{group}', [TicketingSettingsController::class, 'updateGroup']) ->name('settings.groups.update');
Route::post('/settings/agents', [TicketingSettingsController::class, 'storeAgent']) ->name('settings.agents.store');
Route::delete('/settings/agents/{access}', [TicketingSettingsController::class, 'destroyAgent']) ->name('settings.agents.destroy');
Route::post('/settings/priorities', [TicketingSettingsController::class, 'storePriority']) ->name('settings.priorities.store');
Route::put('/settings/priorities/{priority}', [TicketingSettingsController::class, 'updatePriority']) ->name('settings.priorities.update');
Route::delete('/settings/priorities/{priority}', [TicketingSettingsController::class, 'destroyPriority']) ->name('settings.priorities.destroy');
Route::post('/settings/projects', [TicketingSettingsController::class, 'storeProject']) ->name('settings.projects.store');
Route::put('/settings/projects/{project}', [TicketingSettingsController::class, 'updateProject']) ->name('settings.projects.update');
Route::delete('/settings/projects/{project}', [TicketingSettingsController::class, 'destroyProject']) ->name('settings.projects.destroy');
Route::post('/settings/email', [TicketingSettingsController::class, 'storeEmailConnection'])->name('settings.email.store');
Route::put('/settings/email/{connection}', [TicketingSettingsController::class, 'updateEmailConnection'])->name('settings.email.update');
Route::delete('/settings/email/{connection}', [TicketingSettingsController::class, 'destroyEmailConnection'])->name('settings.email.destroy');
});
}); });