1+ 'use client' ;
2+
3+ import { useEffect , useMemo , useState } from 'react' ;
4+ import { Button } from '@comp/ui/button' ;
5+ import {
6+ Dialog ,
7+ DialogContent ,
8+ DialogDescription ,
9+ DialogFooter ,
10+ DialogHeader ,
11+ DialogTitle ,
12+ } from '@comp/ui/dialog' ;
13+ import { Label } from '@comp/ui/label' ;
14+ import { Select , SelectContent , SelectItem , SelectTrigger , SelectValue } from '@comp/ui/select' ;
15+ import { TaskStatus } from '@db' ;
16+ import { Loader2 } from 'lucide-react' ;
17+ import { useParams , useRouter } from 'next/navigation' ;
18+ import { apiClient } from '@/lib/api-client' ;
19+ import { toast } from 'sonner' ;
20+ import { TaskStatusIndicator } from './TaskStatusIndicator' ;
21+
22+ interface BulkTaskStatusChangeModalProps {
23+ open : boolean ;
24+ selectedTaskIds : string [ ] ;
25+ onOpenChange : ( open : boolean ) => void ;
26+ onSuccess ?: ( ) => void ;
27+ }
28+
29+ export function BulkTaskStatusChangeModal ( {
30+ open,
31+ onOpenChange,
32+ selectedTaskIds,
33+ onSuccess,
34+ } : BulkTaskStatusChangeModalProps ) {
35+ const router = useRouter ( ) ;
36+ const params = useParams < { orgId : string } > ( ) ;
37+ const orgIdParam = Array . isArray ( params . orgId ) ? params . orgId [ 0 ] : params . orgId ;
38+
39+ const statusOptions = useMemo ( ( ) => Object . values ( TaskStatus ) as TaskStatus [ ] , [ ] ) ;
40+ const defaultStatus = statusOptions [ 0 ] ;
41+
42+ const [ status , setStatus ] = useState < TaskStatus > ( defaultStatus ) ;
43+ const [ isSubmitting , setIsSubmitting ] = useState ( false ) ;
44+ const selectedCount = selectedTaskIds . length ;
45+ const isSingular = selectedCount === 1 ;
46+
47+ useEffect ( ( ) => {
48+ if ( open ) {
49+ setStatus ( defaultStatus ) ;
50+ }
51+ } , [ defaultStatus , open ] ) ;
52+
53+ const handleMove = async ( ) => {
54+ if ( ! orgIdParam || selectedTaskIds . length === 0 ) {
55+ return ;
56+ }
57+
58+ try {
59+ setIsSubmitting ( true ) ;
60+ const payload = {
61+ taskIds : selectedTaskIds ,
62+ status,
63+ ...( status === TaskStatus . done ? { reviewDate : new Date ( ) . toISOString ( ) } : { } ) ,
64+ } ;
65+
66+ const response = await apiClient . patch < { updatedCount : number } > (
67+ '/v1/tasks/bulk' ,
68+ payload ,
69+ orgIdParam ,
70+ ) ;
71+
72+ if ( response . error ) {
73+ throw new Error ( response . error ) ;
74+ }
75+
76+ const updatedCount = response . data ?. updatedCount ?? selectedTaskIds . length ;
77+ toast . success ( `Updated ${ updatedCount } task${ updatedCount === 1 ? '' : 's' } ` ) ;
78+ onSuccess ?.( ) ;
79+ onOpenChange ( false ) ;
80+ router . refresh ( ) ;
81+ } catch ( error ) {
82+ console . error ( 'Failed to bulk update task status' , error ) ;
83+ const message = error instanceof Error ? error . message : 'Failed to update tasks' ;
84+ toast . error ( message ) ;
85+ } finally {
86+ setIsSubmitting ( false ) ;
87+ }
88+ } ;
89+
90+ return (
91+ < Dialog open = { open } onOpenChange = { onOpenChange } >
92+ < DialogContent >
93+ < DialogHeader >
94+ < DialogTitle > Bulk Status Update</ DialogTitle >
95+ < DialogDescription >
96+ { `${ selectedCount } item${ isSingular ? '' : 's' } ${
97+ isSingular ? 'is' : 'are'
98+ } selected. Are you sure you want to change the status?`}
99+ </ DialogDescription >
100+ </ DialogHeader >
101+
102+ < div className = "space-y-2 flex flex-row items-center gap-4" >
103+ < Label htmlFor = "task-status" > Status</ Label >
104+ < Select value = { status } onValueChange = { ( value ) => setStatus ( value as TaskStatus ) } >
105+ < SelectTrigger id = "task-status" >
106+ < SelectValue placeholder = "Select status" />
107+ </ SelectTrigger >
108+ < SelectContent >
109+ { statusOptions . map ( ( option ) => (
110+ < SelectItem key = { option } value = { option } >
111+ < div className = "flex items-center gap-2" >
112+ < TaskStatusIndicator status = { option } />
113+ < span className = "capitalize" > { option . replace ( '_' , ' ' ) } </ span >
114+ </ div >
115+ </ SelectItem >
116+ ) ) }
117+ </ SelectContent >
118+ </ Select >
119+ </ div >
120+
121+ < DialogFooter className = "flex justify-end gap-2" >
122+ < Button variant = "outline" onClick = { ( ) => onOpenChange ( false ) } >
123+ Cancel
124+ </ Button >
125+ < Button onClick = { handleMove } disabled = { isSubmitting } >
126+ { isSubmitting ? < Loader2 className = "h-4 w-4 animate-spin" /> : 'Change Status' }
127+ </ Button >
128+ </ DialogFooter >
129+ </ DialogContent >
130+ </ Dialog >
131+ ) ;
132+ }
0 commit comments