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' } },