1
1
import useTranslation from "next-translate/useTranslation" ;
2
2
import { useRouter } from "next/router" ;
3
3
import Loader from "react-spinners/ClipLoader" ;
4
- import { useState } from "react" ;
4
+ import { useState , useMemo } from "react" ;
5
5
6
6
import { ContextMenu } from "@radix-ui/themes" ;
7
7
import { getCookie } from "cookies-next" ;
@@ -10,7 +10,7 @@ import Link from "next/link";
10
10
import { useQuery } from "react-query" ;
11
11
import { useUser } from "../../store/session" ;
12
12
import { Popover , PopoverContent , PopoverTrigger } from "@/shadcn/ui/popover" ;
13
- import { CheckIcon , PlusCircle } from "lucide-react" ;
13
+ import { CheckIcon , Filter , PlusCircle , X } from "lucide-react" ;
14
14
import { Button } from "@/shadcn/ui/button" ;
15
15
import {
16
16
Command ,
@@ -32,6 +32,28 @@ async function getUserTickets(token: any) {
32
32
return res . json ( ) ;
33
33
}
34
34
35
+ // Add this new component for the filter badge
36
+ const FilterBadge = ( {
37
+ text,
38
+ onRemove,
39
+ } : {
40
+ text : string ;
41
+ onRemove : ( ) => void ;
42
+ } ) => (
43
+ < div className = "flex items-center gap-1 bg-accent rounded-md px-2 py-1 text-xs" >
44
+ < span > { text } </ span >
45
+ < button
46
+ onClick = { ( e ) => {
47
+ e . preventDefault ( ) ;
48
+ onRemove ( ) ;
49
+ } }
50
+ className = "hover:bg-muted rounded-full p-0.5"
51
+ >
52
+ < X className = "h-3 w-3" />
53
+ </ button >
54
+ </ div >
55
+ ) ;
56
+
35
57
export default function Tickets ( ) {
36
58
const router = useRouter ( ) ;
37
59
const { t } = useTranslation ( "peppermint" ) ;
@@ -47,7 +69,10 @@ export default function Tickets() {
47
69
const low = "bg-blue-100 text-blue-800" ;
48
70
const normal = "bg-green-100 text-green-800" ;
49
71
72
+ const [ filterSelected , setFilterSelected ] = useState ( ) ;
50
73
const [ selectedPriorities , setSelectedPriorities ] = useState < string [ ] > ( [ ] ) ;
74
+ const [ selectedStatuses , setSelectedStatuses ] = useState < string [ ] > ( [ ] ) ;
75
+ const [ selectedAssignees , setSelectedAssignees ] = useState < string [ ] > ( [ ] ) ;
51
76
52
77
const handlePriorityToggle = ( priority : string ) => {
53
78
setSelectedPriorities ( ( prev ) =>
@@ -57,14 +82,65 @@ export default function Tickets() {
57
82
) ;
58
83
} ;
59
84
85
+ const handleStatusToggle = ( status : string ) => {
86
+ setSelectedStatuses ( ( prev ) =>
87
+ prev . includes ( status )
88
+ ? prev . filter ( ( s ) => s !== status )
89
+ : [ ...prev , status ]
90
+ ) ;
91
+ } ;
92
+
93
+ const handleAssigneeToggle = ( assignee : string ) => {
94
+ setSelectedAssignees ( ( prev ) =>
95
+ prev . includes ( assignee )
96
+ ? prev . filter ( ( a ) => a !== assignee )
97
+ : [ ...prev , assignee ]
98
+ ) ;
99
+ } ;
100
+
60
101
const filteredTickets = data
61
- ? data . tickets . filter ( ( ticket ) =>
62
- selectedPriorities . length > 0
63
- ? selectedPriorities . includes ( ticket . priority )
64
- : true
65
- )
102
+ ? data . tickets . filter ( ( ticket ) => {
103
+ const priorityMatch =
104
+ selectedPriorities . length === 0 ||
105
+ selectedPriorities . includes ( ticket . priority ) ;
106
+ const statusMatch =
107
+ selectedStatuses . length === 0 ||
108
+ selectedStatuses . includes ( ticket . isComplete ? "closed" : "open" ) ;
109
+ const assigneeMatch =
110
+ selectedAssignees . length === 0 ||
111
+ selectedAssignees . includes ( ticket . assignedTo ?. name || "Unassigned" ) ;
112
+
113
+ return priorityMatch && statusMatch && assigneeMatch ;
114
+ } )
66
115
: [ ] ;
67
116
117
+ type FilterType = "priority" | "status" | "assignee" | null ;
118
+ const [ activeFilter , setActiveFilter ] = useState < FilterType > ( null ) ;
119
+ const [ filterSearch , setFilterSearch ] = useState ( "" ) ;
120
+
121
+ const filteredPriorities = useMemo ( ( ) => {
122
+ const priorities = [ "low" , "medium" , "high" ] ;
123
+ return priorities . filter ( ( priority ) =>
124
+ priority . toLowerCase ( ) . includes ( filterSearch . toLowerCase ( ) )
125
+ ) ;
126
+ } , [ filterSearch ] ) ;
127
+
128
+ const filteredStatuses = useMemo ( ( ) => {
129
+ const statuses = [ "open" , "closed" ] ;
130
+ return statuses . filter ( ( status ) =>
131
+ status . toLowerCase ( ) . includes ( filterSearch . toLowerCase ( ) )
132
+ ) ;
133
+ } , [ filterSearch ] ) ;
134
+
135
+ const filteredAssignees = useMemo ( ( ) => {
136
+ const assignees = data ?. tickets
137
+ . map ( ( t ) => t . assignedTo ?. name || "Unassigned" )
138
+ . filter ( ( name , index , self ) => self . indexOf ( name ) === index ) ;
139
+ return assignees ?. filter ( ( assignee ) =>
140
+ assignee . toLowerCase ( ) . includes ( filterSearch . toLowerCase ( ) )
141
+ ) ;
142
+ } , [ data ?. tickets , filterSearch ] ) ;
143
+
68
144
return (
69
145
< div >
70
146
{ status === "loading" && (
@@ -76,60 +152,221 @@ export default function Tickets() {
76
152
{ status === "success" && (
77
153
< div >
78
154
< div className = "flex flex-col" >
79
- < div className = "py-2 px-6 bg-gray-200 dark:bg-[#0A090C] border-b-[1px] flex flex-row items-center justify-between" >
80
- < div className = "flex flex-row items-center space-x-4" >
81
- < span className = "text-sm font-bold" > All Tickets</ span >
155
+ < div className = "py-2 px-3 bg-background border-b-[1px] flex flex-row items-center justify-between" >
156
+ < div className = "flex flex-row items-center gap-2" >
82
157
< Popover >
83
158
< PopoverTrigger asChild >
84
159
< Button
85
- variant = "outline "
160
+ variant = "ghost "
86
161
size = "sm"
87
- className = "h-6 bg-transparent border-dashed "
162
+ className = "h-6 bg-transparent"
88
163
>
89
- < PlusCircle className = "mr-2 h-4 w-4" />
90
- Priority
164
+ < Filter className = "mr-2 h-4 w-4" />
165
+ < span className = "hidden sm:block" > Filters </ span >
91
166
</ Button >
92
167
</ PopoverTrigger >
93
- < PopoverContent className = "w-[200px] p-0" align = "start" >
94
- < Command >
95
- < CommandInput placeholder = "Search priority..." />
96
- < CommandList >
97
- < CommandEmpty > No results found.</ CommandEmpty >
98
- < CommandGroup >
99
- { [ "low" , "medium" , "high" ] . map ( ( priority ) => (
168
+ < PopoverContent className = "w-[300px] p-0" align = "start" >
169
+ { ! activeFilter ? (
170
+ < Command >
171
+ < CommandInput placeholder = "Search filters..." />
172
+ < CommandList >
173
+ < CommandEmpty > No results found.</ CommandEmpty >
174
+ < CommandGroup >
175
+ < CommandItem
176
+ onSelect = { ( ) => setActiveFilter ( "priority" ) }
177
+ >
178
+ Priority
179
+ </ CommandItem >
180
+ < CommandItem
181
+ onSelect = { ( ) => setActiveFilter ( "status" ) }
182
+ >
183
+ Status
184
+ </ CommandItem >
185
+ < CommandItem
186
+ onSelect = { ( ) => setActiveFilter ( "assignee" ) }
187
+ >
188
+ Assigned To
189
+ </ CommandItem >
190
+ </ CommandGroup >
191
+ </ CommandList >
192
+ </ Command >
193
+ ) : activeFilter === "priority" ? (
194
+ < Command >
195
+ < CommandInput
196
+ placeholder = "Search priority..."
197
+ value = { filterSearch }
198
+ onValueChange = { setFilterSearch }
199
+ />
200
+ < CommandList >
201
+ < CommandEmpty > No priorities found.</ CommandEmpty >
202
+ < CommandGroup heading = "Priority" >
203
+ { filteredPriorities . map ( ( priority ) => (
204
+ < CommandItem
205
+ key = { priority }
206
+ onSelect = { ( ) => handlePriorityToggle ( priority ) }
207
+ >
208
+ < div
209
+ className = { cn (
210
+ "mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary" ,
211
+ selectedPriorities . includes ( priority )
212
+ ? "bg-primary text-primary-foreground"
213
+ : "opacity-50 [&_svg]:invisible"
214
+ ) }
215
+ >
216
+ < CheckIcon className = { cn ( "h-4 w-4" ) } />
217
+ </ div >
218
+ < span className = "capitalize" > { priority } </ span >
219
+ </ CommandItem >
220
+ ) ) }
221
+ </ CommandGroup >
222
+ < CommandSeparator />
223
+ < CommandGroup >
100
224
< CommandItem
101
- key = { priority }
102
- onSelect = { ( ) => handlePriorityToggle ( priority ) }
225
+ onSelect = { ( ) => {
226
+ setActiveFilter ( null ) ;
227
+ setFilterSearch ( "" ) ;
228
+ } }
229
+ className = "justify-center text-center"
103
230
>
104
- < div
105
- className = { cn (
106
- "mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary" ,
107
- selectedPriorities . includes ( priority )
108
- ? "bg-primary text-primary-foreground"
109
- : "opacity-50 [&_svg]:invisible"
110
- ) }
231
+ Back to filters
232
+ </ CommandItem >
233
+ </ CommandGroup >
234
+ </ CommandList >
235
+ </ Command >
236
+ ) : activeFilter === "status" ? (
237
+ < Command >
238
+ < CommandInput
239
+ placeholder = "Search status..."
240
+ value = { filterSearch }
241
+ onValueChange = { setFilterSearch }
242
+ />
243
+ < CommandList >
244
+ < CommandEmpty > No statuses found.</ CommandEmpty >
245
+ < CommandGroup heading = "Status" >
246
+ { filteredStatuses . map ( ( status ) => (
247
+ < CommandItem
248
+ key = { status }
249
+ onSelect = { ( ) => handleStatusToggle ( status ) }
111
250
>
112
- < CheckIcon className = { cn ( "h-4 w-4" ) } />
113
- </ div >
114
- < span className = "capitalize" > { priority } </ span >
251
+ < div
252
+ className = { cn (
253
+ "mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary" ,
254
+ selectedStatuses . includes ( status )
255
+ ? "bg-primary text-primary-foreground"
256
+ : "opacity-50 [&_svg]:invisible"
257
+ ) }
258
+ >
259
+ < CheckIcon className = { cn ( "h-4 w-4" ) } />
260
+ </ div >
261
+ < span className = "capitalize" > { status } </ span >
262
+ </ CommandItem >
263
+ ) ) }
264
+ </ CommandGroup >
265
+ < CommandSeparator />
266
+ < CommandGroup >
267
+ < CommandItem
268
+ onSelect = { ( ) => {
269
+ setActiveFilter ( null ) ;
270
+ setFilterSearch ( "" ) ;
271
+ } }
272
+ className = "justify-center text-center"
273
+ >
274
+ Back to filters
115
275
</ CommandItem >
116
- ) ) }
117
- </ CommandGroup >
118
- < >
276
+ </ CommandGroup >
277
+ </ CommandList >
278
+ </ Command >
279
+ ) : activeFilter === "assignee" ? (
280
+ < Command >
281
+ < CommandInput
282
+ placeholder = "Search assignee..."
283
+ value = { filterSearch }
284
+ onValueChange = { setFilterSearch }
285
+ />
286
+ < CommandList >
287
+ < CommandEmpty > No assignees found.</ CommandEmpty >
288
+ < CommandGroup heading = "Assigned To" >
289
+ { filteredAssignees ?. map ( ( name ) => (
290
+ < CommandItem
291
+ key = { name }
292
+ onSelect = { ( ) => handleAssigneeToggle ( name ) }
293
+ >
294
+ < div
295
+ className = { cn (
296
+ "mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary" ,
297
+ selectedAssignees . includes ( name )
298
+ ? "bg-primary text-primary-foreground"
299
+ : "opacity-50 [&_svg]:invisible"
300
+ ) }
301
+ >
302
+ < CheckIcon className = { cn ( "h-4 w-4" ) } />
303
+ </ div >
304
+ < span > { name } </ span >
305
+ </ CommandItem >
306
+ ) ) }
307
+ </ CommandGroup >
119
308
< CommandSeparator />
120
309
< CommandGroup >
121
310
< CommandItem
122
- onSelect = { ( ) => setSelectedPriorities ( [ ] ) }
311
+ onSelect = { ( ) => {
312
+ setActiveFilter ( null ) ;
313
+ setFilterSearch ( "" ) ;
314
+ } }
123
315
className = "justify-center text-center"
124
316
>
125
- Clear filters
317
+ Back to filters
126
318
</ CommandItem >
127
319
</ CommandGroup >
128
- </ >
129
- </ CommandList >
130
- </ Command >
320
+ </ CommandList >
321
+ </ Command >
322
+ ) : null }
131
323
</ PopoverContent >
132
324
</ Popover >
325
+
326
+ { /* Display selected filters */ }
327
+ < div className = "flex flex-wrap gap-2" >
328
+ { selectedPriorities . map ( ( priority ) => (
329
+ < FilterBadge
330
+ key = { `priority-${ priority } ` }
331
+ text = { `Priority: ${ priority } ` }
332
+ onRemove = { ( ) => handlePriorityToggle ( priority ) }
333
+ />
334
+ ) ) }
335
+
336
+ { selectedStatuses . map ( ( status ) => (
337
+ < FilterBadge
338
+ key = { `status-${ status } ` }
339
+ text = { `Status: ${ status } ` }
340
+ onRemove = { ( ) => handleStatusToggle ( status ) }
341
+ />
342
+ ) ) }
343
+
344
+ { selectedAssignees . map ( ( assignee ) => (
345
+ < FilterBadge
346
+ key = { `assignee-${ assignee } ` }
347
+ text = { `Assignee: ${ assignee } ` }
348
+ onRemove = { ( ) => handleAssigneeToggle ( assignee ) }
349
+ />
350
+ ) ) }
351
+
352
+ { /* Clear all filters button - only show if there are filters */ }
353
+ { ( selectedPriorities . length > 0 ||
354
+ selectedStatuses . length > 0 ||
355
+ selectedAssignees . length > 0 ) && (
356
+ < Button
357
+ variant = "ghost"
358
+ size = "sm"
359
+ className = "h-6 px-2 text-xs"
360
+ onClick = { ( ) => {
361
+ setSelectedPriorities ( [ ] ) ;
362
+ setSelectedStatuses ( [ ] ) ;
363
+ setSelectedAssignees ( [ ] ) ;
364
+ } }
365
+ >
366
+ Clear all
367
+ </ Button >
368
+ ) }
369
+ </ div >
133
370
</ div >
134
371
< div > </ div >
135
372
</ div >
0 commit comments