diff --git a/app/admin/attendance/page.tsx b/app/admin/attendance/page.tsx new file mode 100644 index 00000000..6ea0e58d --- /dev/null +++ b/app/admin/attendance/page.tsx @@ -0,0 +1,167 @@ +"use client" + +import { useState, useEffect } from "react" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" +import { Badge } from "@/components/ui/badge" +import { createClient } from "@/lib/supabase/client" +import { Clock, Download, Calendar } from "lucide-react" +import { Button } from "@/components/ui/button" + +type AttendanceLog = { + id: string + user_id: string + check_in: string + check_out: string | null + total_hours: number | null + profiles: { + first_name: string | null + last_name: string | null + email: string + } | null +} + +export default function AdminAttendancePage() { + const [logs, setLogs] = useState([]) + const [loading, setLoading] = useState(true) + const supabase = createClient() + + useEffect(() => { + const fetchLogs = async () => { + try { + const { data, error } = await supabase + .from("attendance_logs") + .select(` + *, + profiles ( + first_name, + last_name, + email + ) + `) + .order("check_in", { ascending: false }) + .limit(50) + + if (error) throw error + setLogs(data as any) // Type casting simpler for now + } catch (err) { + console.error("Error fetching logs:", err) + } finally { + setLoading(false) + } + } + + fetchLogs() + }, [supabase]) + + const getStatusBadge = (log: AttendanceLog) => { + if (!log.check_out) { + return ( + + + Online + + ) + } + return Completed + } + + return ( +
+ {/* Background Pattern */} +
+ + + + + + + + + +
+ +
+ +
+

+ Attendance +

+

Monitor staff work hours and presence

+
+
+ +
+ +
+ + + + + + Attendance Logs + + Real-time log of staff checks in/out events. + + +
+ + + + Staff Member + Status + Check In + Check Out + Duration + + + + {loading ? ( + [1, 2, 3].map(i => ( + +
+
+
+
+
+ + )) + ) : logs.length === 0 ? ( + + + No logs found. + + + ) : ( + logs.map((log) => ( + + + {log.profiles?.first_name + ? `${log.profiles.first_name} ${log.profiles.last_name || ''}` + : log.profiles?.email || 'Unknown User' + } + + {getStatusBadge(log)} + + {new Date(log.check_in).toLocaleString()} + + + {log.check_out ? new Date(log.check_out).toLocaleString() : '—'} + + + {log.total_hours ? `${log.total_hours.toFixed(2)} hrs` : '—'} + + + )) + )} + +
+
+
+
+
+ ) +} diff --git a/app/admin/layout.tsx b/app/admin/layout.tsx index 9dc3d971..de9e12cc 100644 --- a/app/admin/layout.tsx +++ b/app/admin/layout.tsx @@ -5,6 +5,7 @@ import { Button } from "@/components/ui/button" import { Sidebar } from "@/components/admin/Sidebar" import { Code2, + Clock, LayoutDashboard, Users, FileText, @@ -102,6 +103,11 @@ const sidebarItems: SidebarGroupType[] = [ url: "/admin/certificates", icon: Award, }, + { + title: "Attendance", + url: "/admin/attendance", + icon: Clock, + }, ], }, { diff --git a/app/protected/layout.tsx b/app/protected/layout.tsx index 507ec883..6da124d5 100644 --- a/app/protected/layout.tsx +++ b/app/protected/layout.tsx @@ -1,6 +1,7 @@ 'use client' -import type React from "react" +import React from "react" import Link from "next/link" +import { useRouter } from "next/navigation" import { Button } from "@/components/ui/button" import { StudentSidebar } from "@/components/users/StudentSidebar" import { @@ -199,7 +200,7 @@ const sidebarItems: SidebarGroupType[] = [ { title: "Support", items: [ - + { title: "Help Center", url: "/protected/help", @@ -209,11 +210,32 @@ const sidebarItems: SidebarGroupType[] = [ }, ] +// Staff imports removed +// Staff Sidebar Items removed + export default function ProtectedLayout({ children }: { children: React.ReactNode }) { const { user, loading } = useAuth() - const { isChecking, isAuthorized } = useRoleProtection('student') + const { isAuthorized } = useRoleProtection('student') + + // Use state to handle client-side role check safely + const [role, setRole] = React.useState(null) + + const router = useRouter() + + React.useEffect(() => { + if (user) { + const userRole = user.user_metadata?.role || 'student' + + if (userRole === 'staff') { + router.push('/staff/dashboard') + return + } - if (loading || isChecking) { + setRole(userRole) + } + }, [user, router]) + + if (loading || role === null) { return (
@@ -221,7 +243,7 @@ export default function ProtectedLayout({ children }: { children: React.ReactNod ) } - if (!user || !isAuthorized) { + if (!user) { return (
@@ -235,9 +257,18 @@ export default function ProtectedLayout({ children }: { children: React.ReactNod ) } - const avatar = user?.user_metadata?.first_name?.[0]?.toUpperCase() || user?.email?.[0]?.toUpperCase() || "S" - const name = user?.user_metadata?.first_name || user?.email || "Student" - const email = user?.email || "student@codeunia.com" + const avatar = user?.user_metadata?.first_name?.[0]?.toUpperCase() || user?.email?.[0]?.toUpperCase() || "U" + const name = user?.user_metadata?.first_name || user?.email || "User" + const email = user?.email || "user@codeunia.com" + + // If somehow role is staff but didn't redirect yet (rare race condition), show spinner + if (role === 'staff') { + return ( +
+
+
+ ) + } return ( ([]) + const [historyLoading, setHistoryLoading] = useState(true) + const supabase = createClient() + + useEffect(() => { + if (!user) return + + const fetchHistory = async () => { + try { + const { data, error } = await supabase + .from("attendance_logs") + .select("*") + .eq("user_id", user.id) + .not("check_out", "is", null) + .order("check_in", { ascending: false }) + .limit(5) + + if (error) throw error + setHistory(data || []) + } catch (err) { + console.error("Error fetching history:", err) + } finally { + setHistoryLoading(false) + } + } + + fetchHistory() + }, [user, supabase]) + + if (loading) return null + + return ( +
+ {/* Header Section */} +
+

+ Staff Dashboard +

+

+ Welcome back, {user?.user_metadata?.first_name || 'Staff Member'}. + Manage your workflow and track your progress. +

+
+ + {/* Stats Row */} + + + {/* Bento Grid Layout */} +
+ + {/* Main Attendance Widget - Spans 2 cols, 2 rows */} +
+ +
+ + {/* Quick Actions - 1 col, 1 row */} +
+ +
+ + {/* Weekly Overview - 1 col, 1 row */} +
+ +
+ + {/* Recent Activity - Full width on mobile/tablet, bottom on desktop */} + + + + + Recent Activity Log + + + + {historyLoading ? ( +
+ {[1, 2, 3].map(i =>
)} +
+ ) : history.length === 0 ? ( +
+ +

No recent activity found

+
+ ) : ( +
+ {history.map(record => ( +
+
+
+ +
+
+
+ {new Date(record.check_in).toLocaleDateString()} +
+
+ {new Date(record.check_in).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} - + {record.check_out ? new Date(record.check_out).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : '...'} +
+
+
+
+
+ {record.total_hours ? `${record.total_hours.toFixed(2)}h` : '-'} +
+
+
+ ))} +
+ )} + + +
+
+ ) +} diff --git a/app/staff/layout.tsx b/app/staff/layout.tsx new file mode 100644 index 00000000..c9d500bb --- /dev/null +++ b/app/staff/layout.tsx @@ -0,0 +1,140 @@ +'use client' +import type React from "react" +import Link from "next/link" +import { Button } from "@/components/ui/button" +import { StaffSidebar, SidebarGroupType } from "@/components/staff/StaffSidebar" +import { + LayoutDashboard, + History, + User, + LogOut, + CalendarDays, + ClipboardList, + MessageSquare, + Bell, + FileText, + Book, + Settings +} from "lucide-react" +import { useAuth } from "@/lib/hooks/useAuth" + +const sidebarItems: SidebarGroupType[] = [ + { + title: "Main", + items: [ + { + title: "Dashboard", + url: "/staff/dashboard", + icon: LayoutDashboard, + }, + { + title: "My History", + url: "/staff/history", + icon: History, + }, + ], + }, + { + title: "Work", + items: [ + { + title: "Schedule", + url: "/staff/schedule", + icon: CalendarDays, + }, + { + title: "My Tasks", + url: "/staff/tasks", + icon: ClipboardList, + }, + ], + }, + { + title: "Communication", + items: [ + { + title: "Messages", + url: "/staff/messages", + icon: MessageSquare, + }, + { + title: "Announcements", + url: "/staff/announcements", + icon: Bell, + }, + ], + }, + { + title: "Resources", + items: [ + { + title: "Documents", + url: "/staff/documents", + icon: FileText, + }, + { + title: "Guidelines", + url: "/staff/guidelines", + icon: Book, + }, + ], + }, + { + title: "Account", + items: [ + { + title: "Profile", + url: "/protected/profile/view", + icon: User, + }, + { + title: "Settings", + url: "/staff/settings", + icon: Settings, + }, + ], + }, +] + +export default function StaffLayout({ children }: { children: React.ReactNode }) { + const { user, loading } = useAuth() + + if (loading) { + return ( +
+
+
+ ) + } + + if (!user) { + return ( +
+
+

Authentication Required

+

Please sign in to access the staff portal.

+ +
+
+ ) + } + + const avatar = user?.user_metadata?.first_name?.[0]?.toUpperCase() || user?.email?.[0]?.toUpperCase() || "S" + const name = user?.user_metadata?.first_name || "Staff Member" + const email = user?.email || "staff@codeunia.com" + + return ( + +
+ {children} +
+
+ ) +} diff --git a/components/header.tsx b/components/header.tsx index 4b71ffb7..6e23c5de 100644 --- a/components/header.tsx +++ b/components/header.tsx @@ -208,12 +208,12 @@ export default function Header() {
{/* Dashboard Link */} setIsMenuOpen(false)} > - Dashboard + {user?.user_metadata?.role === 'staff' ? 'Staff Dashboard' : 'Dashboard'} {/* Logout Button */} diff --git a/components/staff/AttendanceWidget.tsx b/components/staff/AttendanceWidget.tsx new file mode 100644 index 00000000..27ed72a3 --- /dev/null +++ b/components/staff/AttendanceWidget.tsx @@ -0,0 +1,195 @@ +"use client" + +import { useState, useEffect } from "react" +import { createClient } from "@/lib/supabase/client" +import { Button } from "@/components/ui/button" +import { Card, CardContent } from "@/components/ui/card" +import { useAuth } from "@/lib/hooks/useAuth" +import { Clock, PlayCircle, StopCircle, Loader2 } from "lucide-react" + +type AttendanceRecord = { + id: string + check_in: string + check_out: string | null +} + +export function AttendanceWidget() { + const { user } = useAuth() + const [loading, setLoading] = useState(true) + const [actionLoading, setActionLoading] = useState(false) + const [currentSession, setCurrentSession] = useState(null) + const [elapsedTime, setElapsedTime] = useState("00:00:00") + const [error, setError] = useState(null) + + const supabase = createClient() + + // Fetch current status + useEffect(() => { + if (!user) return + + const fetchStatus = async () => { + try { + setLoading(true) + const { data, error } = await supabase + .from("attendance_logs") + .select("*") + .eq("user_id", user.id) + .is("check_out", null) + .maybeSingle() + + if (error) throw error + setCurrentSession(data) + } catch (err: any) { + console.error("Error fetching attendance:", err) + setError(err.message) + } finally { + setLoading(false) + } + } + + fetchStatus() + }, [user, supabase]) + + // Timer for elapsed time + useEffect(() => { + if (!currentSession) { + setElapsedTime("00:00:00") + return + } + + const interval = setInterval(() => { + const start = new Date(currentSession.check_in).getTime() + const now = new Date().getTime() + const diff = now - start + + const hours = Math.floor(diff / (1000 * 60 * 60)) + const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)) + const seconds = Math.floor((diff % (1000 * 60)) / 1000) + + setElapsedTime( + `${hours.toString().padStart(2, "0")}:${minutes + .toString() + .padStart(2, "0")}:${seconds.toString().padStart(2, "0")}` + ) + }, 1000) + + return () => clearInterval(interval) + }, [currentSession]) + + const handleClockIn = async () => { + if (!user) return + setError(null) + try { + setActionLoading(true) + const { data, error } = await supabase + .from("attendance_logs") + .insert([{ user_id: user.id }]) + .select() + .single() + + if (error) throw error + setCurrentSession(data) + } catch (err: any) { + setError(err.message) + } finally { + setActionLoading(false) + } + } + + const handleClockOut = async () => { + if (!user || !currentSession) return + setError(null) + try { + setActionLoading(true) + const { error } = await supabase + .from("attendance_logs") + .update({ check_out: new Date().toISOString() }) + .eq("id", currentSession.id) + + if (error) throw error + setCurrentSession(null) + } catch (err: any) { + setError(err.message) + } finally { + setActionLoading(false) + } + } + + if (loading) { + return ( + + + + ) + } + + return ( + + {/* Ambient Background Glow */} +
+ + +
+
+

+ + Attendance Status +

+
+ + + + + + {currentSession ? 'Currently Working' : 'Not Checked In'} + +
+
+
+ +
+
+ {elapsedTime} +
+

+ Session Duration +

+
+ + {error && ( +
+ {error} +
+ )} + +
+ {!currentSession ? ( + + ) : ( + + )} + {currentSession && ( +

+ Started at {new Date(currentSession.check_in).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} +

+ )} +
+
+
+ ) +} diff --git a/components/staff/DashboardStats.tsx b/components/staff/DashboardStats.tsx new file mode 100644 index 00000000..86618e09 --- /dev/null +++ b/components/staff/DashboardStats.tsx @@ -0,0 +1,59 @@ +"use client" + +import { Card, CardContent } from "@/components/ui/card" +import { Clock, CheckCircle2, CalendarDays } from "lucide-react" + +export function DashboardStats() { + const stats = [ + { + label: "Weekly Hours", + value: "32.5", + subtext: "/ 40 hrs target", + icon: Clock, + color: "text-blue-400", + bg: "bg-blue-500/10", + border: "border-blue-500/20" + }, + { + label: "Attendance", + value: "98%", + subtext: "On time arrival", + icon: CheckCircle2, + color: "text-green-400", + bg: "bg-green-500/10", + border: "border-green-500/20" + }, + { + label: "Next Shift", + value: "Mon, 9:00", + subtext: "Regular Shift", + icon: CalendarDays, + color: "text-purple-400", + bg: "bg-purple-500/10", + border: "border-purple-500/20" + } + ] + + return ( +
+ {stats.map((stat, i) => ( + + +
+
+

{stat.label}

+
+ {stat.value} + {stat.subtext} +
+
+
+ +
+
+
+
+ ))} +
+ ) +} diff --git a/components/staff/QuickActions.tsx b/components/staff/QuickActions.tsx new file mode 100644 index 00000000..faf0ab81 --- /dev/null +++ b/components/staff/QuickActions.tsx @@ -0,0 +1,46 @@ +"use client" + +import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { + CalendarPlus, + FileText, + MessageSquarePlus, + UserCog, + HelpCircle, + FileBadge +} from "lucide-react" + +export function QuickActions() { + const actions = [ + { label: "Apply Leave", icon: CalendarPlus, variant: "default" as const }, + { label: "Submit Report", icon: FileText, variant: "secondary" as const }, + { label: "New Request", icon: MessageSquarePlus, variant: "secondary" as const }, + { label: "Update Profile", icon: UserCog, variant: "ghost" as const }, + { label: "View Policies", icon: FileBadge, variant: "ghost" as const }, + { label: "Get Help", icon: HelpCircle, variant: "ghost" as const }, + ] + + return ( + + + Quick Actions + + + {actions.map((action, i) => ( + + ))} + + + ) +} diff --git a/components/staff/StaffSidebar.tsx b/components/staff/StaffSidebar.tsx new file mode 100644 index 00000000..5751fcf2 --- /dev/null +++ b/components/staff/StaffSidebar.tsx @@ -0,0 +1,340 @@ +import React, { useState, useEffect } from "react"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { useSafeNavigation } from "@/lib/security/safe-navigation"; +import { + LogOut, + User, + ChevronDown, + Menu, + Settings, + ChevronLeft, + ChevronRight, +} from "lucide-react"; +import { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuItem, +} from "@/components/ui/dropdown-menu"; +import { + SidebarContent, + SidebarFooter, + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarHeader, + SidebarInset, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarProvider, +} from "@/components/ui/sidebar" +import { Button } from "@/components/ui/button" +import { Sheet, SheetContent, SheetTrigger, SheetTitle } from "@/components/ui/sheet" +import CodeuniaLogo from "../codeunia-logo"; + + +export type SidebarGroupType = { + title: string; + items: { + title: string; + url: string; + icon: React.ElementType; + }[]; +}; + + +interface StaffSidebarProps { + avatar: React.ReactNode; + name: string; + email: string; + sidebarItems: SidebarGroupType[]; + children: React.ReactNode; +} + +export function StaffSidebar({ avatar, name, email, sidebarItems, children }: StaffSidebarProps) { + const [mobileOpen, setMobileOpen] = useState(false); + const [sidebarCollapsed, setSidebarCollapsed] = useState(false); + const closeSidebar = () => setMobileOpen(false); + const toggleSidebar = () => setSidebarCollapsed(!sidebarCollapsed); + const pathname = usePathname(); + const { navigateTo } = useSafeNavigation(); + + // Lock background scroll when mobile sheet is open + useEffect(() => { + if (mobileOpen) { + const previousOverflow = document.body.style.overflow; + document.body.style.overflow = "hidden"; + return () => { + document.body.style.overflow = previousOverflow; + }; + } + }, [mobileOpen]); + + return ( + +
+ {/* Mobile Header Bar - Fixed at top */} +
+ + + + + + Staff Navigation Menu +
+ {/* mobile header */} +
+
+
+ +
+
+ Codeunia Staff + Staff Portal +
+
+
+ + {/* mobile navigation */} +
+ {sidebarItems.map((group) => ( +
+
+ {group.title} +
+
+ {group.items.map((item) => ( + + + {React.createElement(item.icon, { className: "size-5" })} + + {item.title} + + ))} +
+
+ ))} +
+ + {/* mobile footer */} +
+ + + + + + +
+
+ {avatar} +
+
+ {name} + {email} +
+
+
+ + + + + Profile + + + + + + Settings + + + + navigateTo("/auth/signin")} className="flex items-center gap-2 px-3 py-2 hover:bg-red-600/20 text-red-400 rounded-md cursor-pointer"> + + Log out + +
+
+
+
+
+
+ + {/* Mobile Logo/Title */} +
+
+ +
+ Codeunia Staff +
+
+ + {/* desktop sidebar */} + + + {/* main content */} +
+ {/* Add top padding on mobile to account for fixed header */} +
+ +
+ {children} +
+
+
+
+
+ ) +} diff --git a/components/staff/WeeklyOverview.tsx b/components/staff/WeeklyOverview.tsx new file mode 100644 index 00000000..ff2ecfcc --- /dev/null +++ b/components/staff/WeeklyOverview.tsx @@ -0,0 +1,52 @@ +"use client" + +import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card" + +export function WeeklyOverview() { + // Mock data - would ideally come from DB + const days = [ + { day: "Mon", hours: 8.5 }, + { day: "Tue", hours: 7.2 }, + { day: "Wed", hours: 8.0 }, + { day: "Thu", hours: 6.5 }, + { day: "Fri", hours: 2.3 }, // Current day partially filled + { day: "Sat", hours: 0 }, + { day: "Sun", hours: 0 }, + ] + + const maxHours = 10 + + return ( + + + Weekly Activity + + +
+ {days.map((d, i) => { + const height = (d.hours / maxHours) * 100 + return ( +
+
+
0 ? 'bg-blue-600 group-hover:bg-blue-500' : 'bg-transparent' + }`} + style={{ height: `${height}%` }} + >
+ + {/* Tooltip */} +
+ {d.hours} hrs +
+
+ 0 ? 'text-zinc-300' : 'text-zinc-600'}`}> + {d.day} + +
+ ) + })} +
+
+
+ ) +} diff --git a/components/user-icon.tsx b/components/user-icon.tsx index 9d694c9d..8a0e05ba 100644 --- a/components/user-icon.tsx +++ b/components/user-icon.tsx @@ -3,15 +3,15 @@ import { useRouter } from "next/navigation" import { createClient } from "@/lib/supabase/client" import { Button } from "@/components/ui/button" -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger } from "@/components/ui/dropdown-menu" -import {LogOut, Shield, UserCircle } from "lucide-react" +import { LogOut, Shield, UserCircle } from "lucide-react" import { useAuth } from "@/lib/hooks/useAuth" export function UserIcon() { @@ -55,7 +55,7 @@ export function UserIcon() { Profile */} - router.push("/protected")}> + router.push(user?.user_metadata?.role === 'staff' ? "/staff/dashboard" : "/protected")}> Dashboard diff --git a/middleware.ts b/middleware.ts index f23c462d..a805ce09 100644 --- a/middleware.ts +++ b/middleware.ts @@ -7,7 +7,7 @@ import { reservedUsernameEdgeService } from '@/lib/services/reserved-usernames-e async function awardDailyLoginPoints(userId: string, supabase: any) { try { const today = new Date().toISOString().split('T')[0]; - + // Check if user already got points today const { data: existing, error: checkError } = await supabase .from('user_activity_log') @@ -71,7 +71,7 @@ async function awardDailyLoginPoints(userId: string, supabase: any) { export async function middleware(req: NextRequest) { const res = NextResponse.next(); - + try { const supabase = createServerClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, @@ -130,8 +130,8 @@ export async function middleware(req: NextRequest) { ]; // Check if the current path is public first - const isPublicRoute = publicRoutes.some(route => - req.nextUrl.pathname.startsWith(route) || + const isPublicRoute = publicRoutes.some(route => + req.nextUrl.pathname.startsWith(route) || req.nextUrl.pathname.startsWith('/_next') || req.nextUrl.pathname.startsWith('/api/') || req.nextUrl.pathname.includes('.') @@ -145,14 +145,14 @@ export async function middleware(req: NextRequest) { // Handle username-based routing (only for non-public routes) const pathname = req.nextUrl.pathname; const usernameMatch = pathname.match(/^\/([^\/]+)$/); - + // Exclude admin routes from username routing const adminRoutes = ['/admin', '/admin/', '/admin/users', '/admin/tests', '/admin/events', '/admin/blog-posts', '/admin/certificates', '/admin/pending-payments', '/admin/reserved-usernames']; const isAdminRoute = adminRoutes.some(route => pathname.startsWith(route)); - + if (usernameMatch && !isAdminRoute) { const username = usernameMatch[1]; - + // Check if username is reserved try { const isReserved = await reservedUsernameEdgeService.isReservedUsername(username); @@ -166,7 +166,7 @@ export async function middleware(req: NextRequest) { return NextResponse.rewrite(new URL('/_not-found', req.url)); } } - + // For non-reserved usernames, treat as public profile route // This allows public access to user profiles return res; @@ -202,7 +202,7 @@ export async function middleware(req: NextRequest) { // User setup is not complete, redirect based on next step if (req.nextUrl.pathname !== '/setup' && req.nextUrl.pathname !== '/auth/confirm' && req.nextUrl.pathname !== '/auth/email-confirmation-required') { const redirectUrl = req.nextUrl.clone(); - + if (setupStatus.next_step === 'confirm_email') { // For email users who haven't confirmed, redirect to email confirmation page redirectUrl.pathname = '/auth/email-confirmation-required'; @@ -210,17 +210,52 @@ export async function middleware(req: NextRequest) { // For other incomplete setups, redirect to setup page redirectUrl.pathname = '/setup'; } - + return NextResponse.redirect(redirectUrl); } } else if (setupStatus && setupStatus.can_proceed) { - // If setup is complete and user is on setup page, redirect to dashboard + // Fetch user role for redirection + const { data: profile, error: profileError } = await supabase + .from('profiles') + .select('role') + .eq('id', user.id) + .single(); + + if (profileError) { + console.error('Middleware: Error fetching profile role:', profileError); + } else { + console.log('Middleware: Fetched profile:', profile); + } + + const role = profile?.role || 'student'; + console.log('Middleware: Detected role:', role, 'for user:', user.email, 'ID:', user.id); + + // If setup is complete and user is on setup page, redirect to appropriate dashboard if (req.nextUrl.pathname === '/setup') { const redirectUrl = req.nextUrl.clone(); - redirectUrl.pathname = '/protected/dashboard'; + redirectUrl.pathname = role === 'staff' ? '/staff/dashboard' : '/protected/dashboard'; return NextResponse.redirect(redirectUrl); } - + + // RBAC: Direct Access Control + // if (role === 'staff') { + // // If staff tries to access student dashboard, redirect to staff dashboard + // if (req.nextUrl.pathname.startsWith('/protected')) { + // console.log('Middleware: Redirecting staff to /staff/dashboard'); + // const redirectUrl = req.nextUrl.clone(); + // redirectUrl.pathname = '/staff/dashboard'; + // return NextResponse.redirect(redirectUrl); + // } + // } else { + // // If student (or non-staff) tries to access staff dashboard, redirect to student dashboard + // if (req.nextUrl.pathname.startsWith('/staff')) { + // // console.log('Middleware: Redirecting student to /protected/dashboard'); + // const redirectUrl = req.nextUrl.clone(); + // redirectUrl.pathname = '/protected/dashboard'; + // return NextResponse.redirect(redirectUrl); + // } + // } + // Award daily login points (only once per day) try { await awardDailyLoginPoints(user.id, supabase); @@ -231,7 +266,7 @@ export async function middleware(req: NextRequest) { // Apply production-grade cache headers before returning response let finalResponse = res; - + return finalResponse; } catch (error) { console.error('Middleware error:', error);