diff --git a/src/Http/Controllers/TicketController.php b/src/Http/Controllers/TicketController.php index f5d50bc..7fb1d80 100644 --- a/src/Http/Controllers/TicketController.php +++ b/src/Http/Controllers/TicketController.php @@ -24,6 +24,13 @@ use Inertia\Response; class TicketController extends Controller { + /** + * The sidebar's views, and the only values `?filter=` means anything as. + * Index.vue's `visibleViews` draws exactly these; `savePrefs()` validates + * against them; `index()` normalises anything else to `all` (#1054). + */ + private const VIEW_FILTERS = ['all', 'mine', 'unassigned', 'pending', 'resolved', 'merged']; + private function agentGroupIds(): array { return TicketingAgentAccess::where('user_id', Auth::id()) @@ -112,6 +119,18 @@ class TicketController extends Controller $request->merge(['filter' => $pref['filter']]); } + // #1054 — a `filter` the sidebar has no view for used to fall through to + // the All Open branch and then be echoed straight back in `props.filters`, + // so `?filter=closed` looked like a real view that just happened to be + // showing open tickets. There is no Closed view anywhere in the UI; + // anything unrecognised is All Open, and now says so. The list is the + // one `savePrefs()` validates against and the one Index.vue draws. + // Only rewrites a filter that was actually asked for: an absent one keeps + // meaning "no view chosen", which is what leaves `filter` out of the URL. + if ($request->filled('filter') && ! in_array($request->filter, self::VIEW_FILTERS, true)) { + $request->merge(['filter' => 'all']); + } + // If no groups exist at all, render a first-run / bootstrap state $totalGroups = TicketingGroup::count(); if ($totalGroups === 0) { @@ -181,10 +200,20 @@ class TicketController extends Controller if ($request->filled('project_id')) { $query->where('project_id', $request->project_id); } + // #1054 — the work queues scope by `$closedSlugs`, the same list the + // counts above use. They used to spell it `['resolved', 'closed']` + // by hand, which predates the `merged` status: a correctly merged + // ticket stayed in Unassigned as pickable work while the sidebar + // count next to it — already reading `$closedSlugs` — excluded it, + // so the view disagreed with its own header (4 vs 5). Scoping by + // STATUS and not by `merged_into_id` is deliberate: #938's + // inconsistent rows (pointer set, status still open) are live work + // until somebody repairs them, and hiding them from the queue would + // hide the rows that most need a human. if ($request->filter === 'mine') { - $query->where('assigned_to', $user->id)->whereNotIn('status', ['resolved', 'closed']); + $query->where('assigned_to', $user->id)->whereNotIn('status', $closedSlugs); } elseif ($request->filter === 'unassigned') { - $query->whereNull('assigned_to')->whereNotIn('status', ['resolved', 'closed']); + $query->whereNull('assigned_to')->whereNotIn('status', $closedSlugs); } elseif ($request->filter === 'pending') { $query->where('status', 'pending'); } elseif ($request->filter === 'resolved') { @@ -630,7 +659,7 @@ class TicketController extends Controller { $validated = $request->validate([ 'view_mode' => 'sometimes|in:list,kanban', - 'filter' => 'sometimes|in:all,mine,unassigned,pending,resolved,merged', + 'filter' => 'sometimes|in:'.implode(',', self::VIEW_FILTERS), ]); if (empty($validated)) {