From d751e36ae81fa6ac869e8b78666813eb84d2d00c Mon Sep 17 00:00:00 2001 From: josh Date: Mon, 30 Mar 2026 23:35:33 -0400 Subject: [PATCH] Fix My Tickets and queue filter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - My Tickets: exclude RESOLVED and CLOSED, show active tickets only - Queue filter: cascading Category > Type > Item picker — each leaf is a distinct queue (e.g. TheWrightServer > Automation > Backup vs Sync) - Server: support typeId and itemId as ticket list filter params Co-Authored-By: Claude Sonnet 4.6 --- client/src/pages/Dashboard.tsx | 196 +++++++++++++++++++++++++++++---- client/src/pages/MyTickets.tsx | 16 ++- server/src/routes/tickets.ts | 6 +- 3 files changed, 190 insertions(+), 28 deletions(-) diff --git a/client/src/pages/Dashboard.tsx b/client/src/pages/Dashboard.tsx index e41f154..2c51437 100644 --- a/client/src/pages/Dashboard.tsx +++ b/client/src/pages/Dashboard.tsx @@ -1,13 +1,13 @@ import { useState, useEffect, useCallback } from 'react' import { Link } from 'react-router-dom' -import { Search } from 'lucide-react' +import { Search, ChevronRight, X } from 'lucide-react' import { formatDistanceToNow } from 'date-fns' import api from '../api/client' import Layout from '../components/Layout' import SeverityBadge from '../components/SeverityBadge' import StatusBadge from '../components/StatusBadge' import Avatar from '../components/Avatar' -import { Ticket, TicketStatus, Category } from '../types' +import { Ticket, TicketStatus, Category, CTIType, Item } from '../types' const STATUSES: { value: TicketStatus | ''; label: string }[] = [ { value: '', label: 'All Statuses' }, @@ -20,41 +20,97 @@ const STATUSES: { value: TicketStatus | ''; label: string }[] = [ const selectClass = 'bg-gray-800 border border-gray-700 text-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent' +// Queue label built from whatever CTI level is selected +function queueLabel( + category: Category | null, + type: CTIType | null, + item: Item | null, +): string { + if (item && type && category) return `${category.name} › ${type.name} › ${item.name}` + if (type && category) return `${category.name} › ${type.name}` + if (category) return category.name + return '' +} + export default function Dashboard() { const [tickets, setTickets] = useState([]) - const [categories, setCategories] = useState([]) const [loading, setLoading] = useState(true) const [search, setSearch] = useState('') const [status, setStatus] = useState('') const [severity, setSeverity] = useState('') - const [categoryId, setCategoryId] = useState('') + + // CTI queue filter state + const [categories, setCategories] = useState([]) + const [types, setTypes] = useState([]) + const [items, setItems] = useState([]) + const [selectedCategory, setSelectedCategory] = useState(null) + const [selectedType, setSelectedType] = useState(null) + const [selectedItem, setSelectedItem] = useState(null) + const [showQueueFilter, setShowQueueFilter] = useState(false) useEffect(() => { api.get('/cti/categories').then((r) => setCategories(r.data)) }, []) + const handleCategorySelect = (cat: Category) => { + setSelectedCategory(cat) + setSelectedType(null) + setSelectedItem(null) + setTypes([]) + setItems([]) + api.get('/cti/types', { params: { categoryId: cat.id } }).then((r) => setTypes(r.data)) + } + + const handleTypeSelect = (type: CTIType) => { + setSelectedType(type) + setSelectedItem(null) + setItems([]) + api.get('/cti/items', { params: { typeId: type.id } }).then((r) => setItems(r.data)) + } + + const handleItemSelect = (item: Item) => { + setSelectedItem(item) + setShowQueueFilter(false) + } + + const clearQueue = () => { + setSelectedCategory(null) + setSelectedType(null) + setSelectedItem(null) + setTypes([]) + setItems([]) + } + + // Derive the most specific filter param + const queueParams: Record = {} + if (selectedItem) queueParams.itemId = selectedItem.id + else if (selectedType) queueParams.typeId = selectedType.id + else if (selectedCategory) queueParams.categoryId = selectedCategory.id + const fetchTickets = useCallback(() => { setLoading(true) - const params: Record = {} + const params: Record = { ...queueParams } if (status) params.status = status if (severity) params.severity = severity - if (categoryId) params.categoryId = categoryId if (search) params.search = search api .get('/tickets', { params }) .then((r) => setTickets(r.data)) .finally(() => setLoading(false)) - }, [status, severity, categoryId, search]) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [status, severity, search, selectedCategory, selectedType, selectedItem]) useEffect(() => { const t = setTimeout(fetchTickets, 300) return () => clearTimeout(t) }, [fetchTickets]) + const activeQueue = queueLabel(selectedCategory, selectedType, selectedItem) + return ( {/* Filters */} -
+
- + {/* Queue picker */} +
+ + + {showQueueFilter && ( +
+ {/* Categories */} +
+

+ Category +

+
+ {categories.map((cat) => ( + + ))} +
+
+ + {/* Types */} +
+

+ Type +

+
+ {!selectedCategory ? ( +

Select category

+ ) : types.length === 0 ? ( +

No types

+ ) : ( + types.map((type) => ( + + )) + )} +
+
+ + {/* Items */} +
+

+ Item +

+
+ {!selectedType ? ( +

Select type

+ ) : items.length === 0 ? ( +

No items

+ ) : ( + items.map((item) => ( + + )) + )} +
+
+
+ )} +
{/* Ticket list */} @@ -118,7 +273,6 @@ export default function Dashboard() { to={`/tickets/${ticket.displayId}`} className="flex items-center gap-4 bg-gray-900 border border-gray-800 rounded-lg px-4 py-3 hover:border-blue-500/50 hover:bg-gray-900/80 transition-all group" > - {/* Severity stripe */}
{ if (!user) return - api - .get('/tickets', { params: { assigneeId: user.id } }) - .then((r) => setTickets(r.data)) + // Only show active tickets — OPEN and IN_PROGRESS + Promise.all([ + api.get('/tickets', { params: { assigneeId: user.id, status: 'OPEN' } }), + api.get('/tickets', { params: { assigneeId: user.id, status: 'IN_PROGRESS' } }), + ]) + .then(([openRes, inProgressRes]) => { + const combined = [...openRes.data, ...inProgressRes.data] + combined.sort((a, b) => a.severity - b.severity || new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()) + setTickets(combined) + }) .finally(() => setLoading(false)) }, [user]) @@ -27,7 +34,7 @@ export default function MyTickets() {
Loading...
) : tickets.length === 0 ? (
- No tickets assigned to you + No active tickets assigned to you
) : (
@@ -37,7 +44,6 @@ export default function MyTickets() { to={`/tickets/${ticket.displayId}`} className="flex items-center gap-4 bg-gray-900 border border-gray-800 rounded-lg px-4 py-3 hover:border-blue-500/50 transition-all group" > - {/* Severity stripe */}
{ - const { status, severity, assigneeId, categoryId, search } = req.query + const { status, severity, assigneeId, categoryId, typeId, itemId, search } = req.query const where: Record = {} if (status) where.status = status if (severity) where.severity = Number(severity) if (assigneeId) where.assigneeId = assigneeId - if (categoryId) where.categoryId = categoryId + if (itemId) where.itemId = itemId + else if (typeId) where.typeId = typeId + else if (categoryId) where.categoryId = categoryId if (search) { where.OR = [ { title: { contains: search as string, mode: 'insensitive' } },