Skip to content

Commit 85a9979

Browse files
committed
Merge branch 'main' of https://github.com/MrPanda009/vihaan
2 parents 52ee785 + 7950337 commit 85a9979

4 files changed

Lines changed: 340 additions & 154 deletions

File tree

frontend/app/admin/Workers.tsx

Lines changed: 162 additions & 141 deletions
Original file line numberDiff line numberDiff line change
@@ -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";
1727
import { toast, ToastContainer } from "@/components/Toast";
1828
import { 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
}

frontend/app/admin/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ import Authorities from "./Authorities";
1818
import Workers from "./Workers";
1919
import Reports from "./Reports";
2020
import WorkerReports from "./WorkerReports";
21-
import { toast } from "@/components/Toast";
2221
import { supabase } from "@/src/lib/supabase";
22+
import { toast } from "@/components/Toast";
2323

2424
type AdminStats = {
2525
totalComplaints: number;

0 commit comments

Comments
 (0)