@@ -12,7 +12,17 @@ import {
1212 Plus ,
1313 Users ,
1414 CheckSquare ,
15- HardHat
15+ HardHat ,
16+ ChevronDown ,
17+ X ,
18+ Loader2 ,
19+ Trash2 ,
20+ ShieldCheck ,
21+ Zap ,
22+ Droplets ,
23+ Trash ,
24+ MapPin ,
25+ Building
1626} from "lucide-react" ;
1727import { toast , ToastContainer } from "@/components/Toast" ;
1828import { supabase } from "@/src/lib/supabase" ;
@@ -57,10 +67,19 @@ export default function Workers({ onViewReports }: { onViewReports?: () => void
5767 const [ selectedWorker , setSelectedWorker ] = useState < WorkerAccount | null > ( null ) ;
5868 const profileCardRef = useRef < HTMLDivElement > ( null ) ;
5969
60- // Assign Department popup
70+ // Assign Department state
6171 const [ assigningWorker , setAssigningWorker ] = useState < WorkerAccount | null > ( null ) ;
62- const [ selectedNewDept , setSelectedNewDept ] = useState ( "" ) ;
63- const assignCardRef = useRef < HTMLDivElement > ( null ) ;
72+ const [ assigning , setAssigning ] = useState ( false ) ;
73+ const dropdownRef = useRef < HTMLDivElement > ( null ) ;
74+
75+ const DEPARTMENTS = [
76+ { name : "Roads & Infrastructure" , icon : MapPin , color : "#f97316" , desc : "Highway, bridges, and local road maintenance." } ,
77+ { name : "Sanitation & Waste" , icon : Trash , color : "#10b981" , desc : "Waste collection and city cleaning services." } ,
78+ { name : "Lighting" , icon : Zap , color : "#fbbf24" , desc : "Street light maintenance and installation." } ,
79+ { name : "Electrical" , icon : ShieldCheck , color : "#6366f1" , desc : "Grid management and public power systems." } ,
80+ { name : "Water Supply" , icon : Droplets , color : "#3b82f6" , desc : "Clean water distribution and pipeline repairs." } ,
81+ { name : "Planning & Development" , icon : Building , color : "#ec4899" , desc : "Urban planning and building permits." } ,
82+ ] ;
6483
6584 const fetchWorkers = async ( ) => {
6685 setLoading ( true ) ;
@@ -129,8 +148,23 @@ export default function Workers({ onViewReports }: { onViewReports?: () => void
129148 . on ( 'postgres_changes' , { event : '*' , schema : 'public' , table : 'profiles' } , ( ) => {
130149 void fetchWorkers ( ) ;
131150 } )
151+ . on ( 'postgres_changes' , { event : '*' , schema : 'public' , table : 'worker_profiles' } , ( ) => {
152+ void fetchWorkers ( ) ;
153+ } )
132154 . subscribe ( ) ;
133- return ( ) => { void supabase . removeChannel ( channel ) ; } ;
155+
156+ // Handle click outside for dropdown
157+ const handleClickOutside = ( e : MouseEvent ) => {
158+ if ( dropdownRef . current && ! dropdownRef . current . contains ( e . target as Node ) ) {
159+ setAssigningWorker ( null ) ;
160+ }
161+ } ;
162+ document . addEventListener ( "mousedown" , handleClickOutside ) ;
163+
164+ return ( ) => {
165+ void supabase . removeChannel ( channel ) ;
166+ document . removeEventListener ( "mousedown" , handleClickOutside ) ;
167+ } ;
134168 } , [ ] ) ;
135169
136170 const filtered = useMemo ( ( ) => {
@@ -197,46 +231,47 @@ export default function Workers({ onViewReports }: { onViewReports?: () => void
197231 }
198232 } ;
199233
200- // Assign Dept open animation
201- useEffect ( ( ) => {
202- if ( assigningWorker && assignCardRef . current ) {
203- gsap . fromTo ( assignCardRef . current ,
204- { opacity : 0 , scale : 0.88 , y : 20 } ,
205- { opacity : 1 , scale : 1 , y : 0 , duration : 0.3 , ease : "back.out(1.5)" }
206- ) ;
207- }
208- } , [ assigningWorker ] ) ;
209234
210- const handleCloseAssignModal = ( ) => {
211- if ( assignCardRef . current ) {
212- gsap . to ( assignCardRef . current , {
213- opacity : 0 , scale : 0.9 , y : 20 , duration : 0.25 , ease : "power2.in" ,
214- onComplete : ( ) => {
215- setAssigningWorker ( null ) ;
216- setSelectedNewDept ( "" ) ;
235+
236+ const handleAssignDept = async ( workerId : string , department : string ) => {
237+ setAssigning ( true ) ;
238+ try {
239+ const { data : { session } } = await supabase . auth . getSession ( ) ;
240+ if ( ! session ) {
241+ toast . error ( "Session expired." ) ;
242+ return ;
243+ }
244+
245+ const apiUrl = process . env . NEXT_PUBLIC_API_URL || "https://api.vihaan.perkkk.dev" ;
246+ const response = await fetch ( `${ apiUrl } /api/admin/workers` , {
247+ method : "PATCH" ,
248+ headers : {
249+ "Content-Type" : "application/json" ,
250+ "Authorization" : `Bearer ${ session . access_token } `
217251 } ,
252+ body : JSON . stringify ( {
253+ worker_id : workerId ,
254+ department : department
255+ } )
218256 } ) ;
219- } else {
257+
258+ if ( ! response . ok ) {
259+ throw new Error ( "Failed to update department" ) ;
260+ }
261+
262+ toast . success ( `Assigned ${ department } to worker` ) ;
220263 setAssigningWorker ( null ) ;
264+ void fetchWorkers ( ) ; // Refresh
265+ } catch ( e : any ) {
266+ console . error ( "Assignment failed:" , e ) ;
267+ toast . error ( `Error: ${ e . message || "Failed to assign department" } ` ) ;
268+ } finally {
269+ setAssigning ( false ) ;
221270 }
222271 } ;
223272
224- const handleAssignDept = ( ) => {
225- if ( ! assigningWorker || ! selectedNewDept ) return ;
226-
227- setWorkers ( prev => prev . map ( w => {
228- if ( w . id === assigningWorker . id ) {
229- // Prevent duplicates
230- const updatedCats = w . categories . includes ( selectedNewDept )
231- ? w . categories
232- : [ ...w . categories , selectedNewDept ] ;
233- return { ...w , categories : updatedCats } ;
234- }
235- return w ;
236- } ) ) ;
237-
238- toast . success ( `Assigned ${ selectedNewDept } to ${ assigningWorker . name } ` ) ;
239- handleCloseAssignModal ( ) ;
273+ const handleOpenAssign = ( worker : WorkerAccount ) => {
274+ setAssigningWorker ( prev => prev ?. id === worker . id ? null : worker ) ;
240275 } ;
241276
242277
@@ -434,9 +469,96 @@ export default function Workers({ onViewReports }: { onViewReports?: () => void
434469 ) ) }
435470 </ div >
436471
437- < div className = { styles . acActions } >
472+ < div className = { styles . acActions } style = { { position : 'relative' } } >
438473 < button className = { styles . acBtn } onClick = { ( ) => setSelectedWorker ( auth ) } > View Person</ button >
439- < button className = { styles . acBtn } onClick = { ( ) => setAssigningWorker ( auth ) } > Assign Department</ button >
474+ < button className = { styles . acBtn } onClick = { ( ) => handleOpenAssign ( auth ) } >
475+ { assigningWorker ?. id === auth . id ? 'Close' : 'Assign Department' }
476+ < ChevronDown size = { 14 } style = { { marginLeft : 6 , transform : assigningWorker ?. id === auth . id ? 'rotate(180deg)' : 'none' , transition : 'transform 0.3s' } } />
477+ </ button >
478+
479+ { assigningWorker ?. id === auth . id && (
480+ < div
481+ ref = { dropdownRef }
482+ style = { {
483+ position : 'absolute' ,
484+ top : '100%' ,
485+ right : 0 ,
486+ zIndex : 1000 ,
487+ width : '320px' ,
488+ marginTop : '8px' ,
489+ background : 'rgba(15, 15, 20, 0.98)' ,
490+ backdropFilter : 'blur(20px)' ,
491+ border : '1px solid rgba(255,255,255,0.12)' ,
492+ borderRadius : '16px' ,
493+ padding : '10px' ,
494+ boxShadow : '0 20px 50px rgba(0,0,0,0.6), 0 0 0 1px rgba(249,115,22,0.15)' ,
495+ animation : 'dropdownFadeIn 0.3s cubic-bezier(0.23, 1, 0.32, 1)'
496+ } }
497+ >
498+ < style dangerouslySetInnerHTML = { { __html : `
499+ @keyframes dropdownFadeIn {
500+ from { opacity: 0; transform: translateY(-10px) scale(0.98); }
501+ to { opacity: 1; transform: translateY(0) scale(1); }
502+ }
503+ ` } } />
504+ < div style = { { padding : '8px 10px' , fontSize : '11px' , fontWeight : 700 , color : 'rgba(255,255,255,0.4)' , textTransform : 'uppercase' , letterSpacing : '0.08em' , display : 'flex' , justifyContent : 'space-between' , alignItems : 'center' } } >
505+ Choose Department
506+ < button onClick = { ( ) => setAssigningWorker ( null ) } style = { { background : 'none' , border : 'none' , color : 'rgba(255,255,255,0.3)' , cursor : 'pointer' } } >
507+ < X size = { 14 } />
508+ </ button >
509+ </ div >
510+ < div style = { { maxHeight : '340px' , overflowY : 'auto' , padding : '4px' } } className = "noScrollbar" >
511+ { DEPARTMENTS . map ( ( dept ) => {
512+ const Icon = dept . icon ;
513+ const isCurrent = auth . categories . includes ( dept . name ) ;
514+ return (
515+ < button
516+ key = { dept . name }
517+ onClick = { ( ) => ! isCurrent && handleAssignDept ( auth . id , dept . name ) }
518+ disabled = { assigning }
519+ style = { {
520+ width : '100%' ,
521+ display : 'flex' ,
522+ alignItems : 'center' ,
523+ gap : '12px' ,
524+ padding : '12px' ,
525+ borderRadius : '12px' ,
526+ background : isCurrent ? 'rgba(249,115,22,0.08)' : 'transparent' ,
527+ border : isCurrent ? '1px solid rgba(249,115,22,0.15)' : '1px solid transparent' ,
528+ cursor : assigning ? 'wait' : isCurrent ? 'default' : 'pointer' ,
529+ transition : 'all 0.2s ease' ,
530+ textAlign : 'left' ,
531+ marginBottom : '4px'
532+ } }
533+ onMouseEnter = { ( e ) => { if ( ! isCurrent ) e . currentTarget . style . background = 'rgba(255,255,255,0.05)' ; } }
534+ onMouseLeave = { ( e ) => { if ( ! isCurrent ) e . currentTarget . style . background = 'transparent' ; } }
535+ >
536+ < div style = { {
537+ width : '36px' , height : '36px' , borderRadius : '10px' ,
538+ background : `${ dept . color } 15` , border : `1px solid ${ dept . color } 30` ,
539+ display : 'flex' , alignItems : 'center' , justifyContent : 'center' ,
540+ color : dept . color , flexShrink : 0
541+ } } >
542+ < Icon size = { 18 } />
543+ </ div >
544+ < div style = { { flex : 1 } } >
545+ < div style = { { fontSize : '13px' , fontWeight : 600 , color : isCurrent ? '#f97316' : '#fff' , display : 'flex' , alignItems : 'center' , gap : '6px' } } >
546+ { dept . name }
547+ { isCurrent && < ShieldCheck size = { 12 } /> }
548+ </ div >
549+ < div style = { { fontSize : '11px' , color : 'rgba(255,255,255,0.4)' , marginTop : '2px' , lineHeight : 1.3 } } > { dept . desc } </ div >
550+ </ div >
551+ </ button >
552+ ) ;
553+ } ) }
554+ </ div >
555+ { assigning && (
556+ < div style = { { position : 'absolute' , inset : 0 , background : 'rgba(0,0,0,0.4)' , borderRadius : '16px' , display : 'flex' , alignItems : 'center' , justifyContent : 'center' , zIndex : 10 } } >
557+ < Loader2 size = { 24 } className = "animate-spin text-orange-500" />
558+ </ div >
559+ ) }
560+ </ div >
561+ ) }
440562 </ div >
441563 </ div >
442564 ) ) }
@@ -662,107 +784,6 @@ export default function Workers({ onViewReports }: { onViewReports?: () => void
662784 document . body
663785 ) }
664786
665- { /* Assign Department Modal Portal */ }
666- { assigningWorker && typeof document !== 'undefined' && createPortal (
667- < div
668- onClick = { handleCloseAssignModal }
669- style = { {
670- position : 'fixed' , inset : 0 ,
671- background : 'rgba(0,0,0,0.55)' ,
672- backdropFilter : 'blur(4px)' ,
673- WebkitBackdropFilter : 'blur(4px)' ,
674- zIndex : 9999 ,
675- display : 'flex' , alignItems : 'center' , justifyContent : 'center' ,
676- } }
677- >
678- < div
679- ref = { assignCardRef }
680- onClick = { e => e . stopPropagation ( ) }
681- style = { {
682- width : '420px' ,
683- background : 'rgba(14,14,18,0.95)' ,
684- backdropFilter : 'blur(40px)' ,
685- WebkitBackdropFilter : 'blur(40px)' ,
686- border : '1px solid rgba(255,255,255,0.1)' ,
687- borderRadius : '20px' ,
688- padding : '24px' ,
689- boxShadow : '0 24px 80px rgba(0,0,0,0.8), 0 0 0 1px rgba(249,115,22,0.08)' ,
690- } }
691- >
692- < h2 style = { { fontSize : '1.25rem' , fontWeight : 700 , color : '#fafafa' , marginBottom : '6px' } } > Assign Department</ h2 >
693- < p style = { { fontSize : '0.85rem' , color : 'rgba(255,255,255,0.4)' , marginBottom : '20px' } } >
694- Assign a new department to < span style = { { color : '#f97316' , fontWeight : 600 } } > { assigningWorker . name } </ span >
695- </ p >
696-
697- < div style = { { marginBottom : '24px' } } >
698- < label style = { { display : 'block' , fontSize : '0.7rem' , color : 'rgba(255,255,255,0.4)' , fontWeight : 600 , textTransform : 'uppercase' , letterSpacing : '0.05em' , marginBottom : '8px' } } >
699- Select Department
700- </ label >
701- < select
702- value = { selectedNewDept }
703- onChange = { ( e ) => setSelectedNewDept ( e . target . value ) }
704- style = { {
705- width : '100%' ,
706- background : 'rgba(255,255,255,0.05)' ,
707- border : '1px solid rgba(255,255,255,0.1)' ,
708- borderRadius : '12px' ,
709- padding : '12px 16px' ,
710- color : '#fafafa' ,
711- fontSize : '0.9rem' ,
712- outline : 'none' ,
713- cursor : 'pointer' ,
714- appearance : 'none' ,
715- } }
716- >
717- < option value = "" style = { { background : '#121216' } } > Choose a department...</ option >
718- < option value = "Roads & Infrastructure" style = { { background : '#121216' } } > Roads & Infrastructure </ option >
719- < option value = "Sanitation & Waste" style = { { background : '#121216' } } > Sanitation & Waste </ option >
720- < option value = "Lighting" style = { { background : '#121216' } } > Lighting</ option >
721- < option value = "Electrical" style = { { background : '#121216' } } > Electrical</ option >
722- < option value = "Water Supply" style = { { background : '#121216' } } > Water Supply</ option >
723- </ select >
724- </ div >
725-
726- < div style = { { display : 'flex' , gap : '12px' , justifyContent : 'flex-end' } } >
727- < button
728- onClick = { handleCloseAssignModal }
729- style = { {
730- padding : '10px 20px' ,
731- background : 'rgba(255,255,255,0.06)' ,
732- border : '1px solid rgba(255,255,255,0.1)' ,
733- borderRadius : '10px' ,
734- color : 'rgba(255,255,255,0.6)' ,
735- fontSize : '0.85rem' ,
736- fontWeight : 600 ,
737- cursor : 'pointer' ,
738- } }
739- >
740- Cancel
741- </ button >
742- < button
743- onClick = { handleAssignDept }
744- disabled = { ! selectedNewDept }
745- style = { {
746- padding : '10px 24px' ,
747- background : selectedNewDept ? '#f97316' : 'rgba(249,115,22,0.3)' ,
748- border : 'none' ,
749- borderRadius : '10px' ,
750- color : selectedNewDept ? '#fff' : 'rgba(255,255,255,0.5)' ,
751- fontSize : '0.85rem' ,
752- fontWeight : 600 ,
753- cursor : selectedNewDept ? 'pointer' : 'not-allowed' ,
754- transition : 'all 0.2s' ,
755- boxShadow : selectedNewDept ? '0 8px 20px rgba(249,115,22,0.2)' : 'none' ,
756- } }
757- >
758- Assign Department
759- </ button >
760- </ div >
761- </ div >
762- </ div > ,
763- document . body
764- ) }
765-
766787 </ div >
767788 ) ;
768789}
0 commit comments