diff --git a/.github/workflows/supabase-waitlisted-cron.yml b/.github/workflows/supabase-waitlisted-cron.yml deleted file mode 100644 index 65a0b9f..0000000 --- a/.github/workflows/supabase-waitlisted-cron.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: Supabase Waitlisted Cron - -on: - schedule: - - cron: '0 * * * *' # every hour - -jobs: - call-edge-function: - runs-on: ubuntu-latest - steps: - - name: Call update-waitlisted Edge Function - run: | - curl -X POST "https://dbuyxpovejdakzveiprx.supabase.co/functions/v1/update-waitlisted" \ - -H "Authorization: Bearer ${{ secrets.SUPABASE_ANON_KEY }}" \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..7a73a41 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/app/(app)/[username]/page.tsx b/app/(app)/[username]/page.tsx deleted file mode 100644 index 5d439e8..0000000 --- a/app/(app)/[username]/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -//import AboutPage from "./students/students-profile"; - -export default function Page() { - //return ; -} diff --git a/app/(app)/[username]/students/modals/students-view-cert.tsx b/app/(app)/[username]/students/modals/students-view-cert.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/app/(app)/[username]/students/modals/students-view-portfolio.tsx b/app/(app)/[username]/students/modals/students-view-portfolio.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/app/(app)/[username]/students/students-profile.tsx b/app/(app)/[username]/students/students-profile.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/app/(app)/[username]/students/tabs/skills-tab.tsx b/app/(app)/[username]/students/tabs/skills-tab.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/app/(app)/[username]/students/tabs/students-ratings-tab.tsx b/app/(app)/[username]/students/tabs/students-ratings-tab.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/app/(app)/admin/coordinators/dashboard/page.tsx b/app/(app)/admin/coordinators/dashboard/page.tsx index 53c8936..e316c65 100644 --- a/app/(app)/admin/coordinators/dashboard/page.tsx +++ b/app/(app)/admin/coordinators/dashboard/page.tsx @@ -14,85 +14,36 @@ import { } from "lucide-react" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" + import MuiPopover from "@mui/material/Popover" import MuiButton from "@mui/material/Button" -import { motion } from "framer-motion" - -const statsCards = [ - { - title: "Total Students", - value: "245", - change: "+5%", - trend: "up", - icon: GraduationCap, - color: "from-blue-500 to-cyan-500", - bgColor: "from-blue-50 to-cyan-50", - sub: "from last month", - }, - { - title: "Hired Students", - value: "87", - change: "+12%", - trend: "up", - icon: Users, - color: "from-emerald-500 to-teal-500", - bgColor: "from-emerald-50 to-teal-50", - sub: "from last month", - }, - { - title: "In Progress", - value: "124", - change: "-3%", - trend: "down", - icon: FileText, - color: "from-orange-500 to-red-500", - bgColor: "from-orange-50 to-red-50", - sub: "from last month", - }, - { - title: "Pending Reports", - value: "12", - change: "+2%", - trend: "up", - icon: BarChart3, - color: "from-purple-500 to-pink-500", - bgColor: "from-purple-50 to-pink-50", - sub: "from last month", - }, -] export default function AdminDashboard() { const [dateRange, setDateRange] = useState("This Month") + const [anchorEl, setAnchorEl] = useState(null) - const handlePopoverOpen = (event: React.MouseEvent) => setAnchorEl(event.currentTarget) - const handlePopoverClose = () => setAnchorEl(null) + const handlePopoverOpen = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget) + } + const handlePopoverClose = () => { + setAnchorEl(null) + } const open = Boolean(anchorEl) return ( -
- +
+
-

Dashboard

-

IT Department Admin Dashboard

+

Dashboard

+

IT Department Admin Dashboard

+ {/* MUI Popover for date range */} } endIcon={} > @@ -140,216 +91,170 @@ export default function AdminDashboard() {
+
-
+
- {statsCards.map((stat, index) => ( - - -
- - - {stat.title} - -
- + + + Total Students + + + +
245
+
+ + 5% from last month +
+
+
+ + + Hired Students + + + +
87
+
+ + 12% from last month +
+
+
+ + + In Progress + + + +
124
+
+ + 3% from last month +
+
+
+ + + Pending Reports + + + +
12
+
+ + 2% from last month +
+
+
+
+ + + + Overview + Students + Reports + + +
+ + + Student Placement Trends + Monthly student placements over the past year + + +
+

Chart: Monthly student placements

+
+
+ + + Student Status + Breakdown by placement status -
{stat.value}
-
- {stat.trend === "up" ? ( - - ) : ( - - )} - {stat.change} {stat.sub} +
+

Chart: Student status distribution

- - ))} -
- - - - - Overview - - - Students - - - Reports - - - -
- - - - Student Placement Trends - - Monthly student placements over the past year - - - -
-

Chart: Monthly student placements

-
-
-
-
- - - - Student Status - Breakdown by placement status - - -
-

Chart: Student status distribution

-
-
-
-
-
-
- - - - Recent Activities - Latest department activities - - -
- {[1, 2, 3, 4, 5].map((i) => ( - -
- -
-
-

- Student placement updated -

-

2 hours ago

-
-
- ))} -
-
-
-
- - - - Top Companies - Companies hiring the most IT students - - -
- {[1, 2, 3, 4, 5].map((i) => ( - -
-
-
-

- Tech Company {i} -

-

{15 - i * 2} students hired

-
-
- -
- ))} -
-
-
-
-
- - - - -
- -
-
- Student Analytics - Detailed student performance and placement metrics -
+ +
+ + + Recent Activities + Latest department activities -
-

Student analytics content

+
+ {[1, 2, 3, 4, 5].map((i) => ( +
+
+ +
+
+

Student placement updated

+

2 hours ago

+
+
+ ))}
- - - - - - -
- -
-
- Department Reports - Generated reports and statistics for IT department -
+ + + Top Companies + Companies hiring the most IT students -
-

Reports content

+
+ {[1, 2, 3, 4, 5].map((i) => ( +
+
+
+
+

Tech Company {i}

+

{15 - i * 2} students hired

+
+
+ +
+ ))}
- +
+
+ + + + Student Analytics + Detailed student performance and placement metrics + + +
+

Student analytics content

+
+
+
+
+ + + + Department Reports + Generated reports and statistics for IT department + + +
+

Reports content

+
+
+
) } - diff --git a/app/(app)/admin/coordinators/layout.tsx b/app/(app)/admin/coordinators/layout.tsx index 16eb222..fa01b0c 100644 --- a/app/(app)/admin/coordinators/layout.tsx +++ b/app/(app)/admin/coordinators/layout.tsx @@ -1,23 +1,11 @@ "use client" import type React from "react" -import { useState, useEffect, Suspense } from "react" + +import { useState, useEffect, useMemo, Suspense } from "react" import Link from "next/link" -import { usePathname} from "next/navigation" -import { - BarChart3, - Users, - LogOut, - Menu, - ChevronDown, - Search, - Bell, - Flag, - Settings, - Shield, - User, - X, -} from "lucide-react" +import { usePathname, useRouter } from "next/navigation" +import { BarChart3, Users, LogOut, Menu, ChevronDown, Search, Bell, Flag, Settings } from "lucide-react" import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" @@ -29,10 +17,8 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" +import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet" import { Badge } from "@/components/ui/badge" -import { motion, AnimatePresence } from "framer-motion" -import { cn } from "@/lib/utils" -import { useSession, signOut } from "next-auth/react" interface NavItem { title: string @@ -42,570 +28,298 @@ interface NavItem { submenu?: { title: string; href: string; badge?: number }[] } -const navItems: NavItem[] = [ - { - title: "Dashboard", - href: "/admin/coordinators/dashboard", - icon: BarChart3, - }, - { - title: "Student Management", - href: "/admin/coordinators/students", - icon: Users, - badge: 3, - }, - { - title: "Report Management", - href: "#", - icon: Flag, - badge: 12, - submenu: [ - { title: "Reported Employers", href: "/admin/coordinators/reports/employers", badge: 5 }, - { title: "Reported Companies", href: "/admin/coordinators/reports/companies", badge: 3 }, - { title: "Reported Listings", href: "/admin/coordinators/reports/listings", badge: 2 }, - { title: "Reported Students", href: "/admin/coordinators/reports/students", badge: 2 }, - ], - }, -] +export default function AdminLayout({ + children, +}: { + children: React.ReactNode +}) { + const pathname = usePathname() + const router = useRouter() + const [openSubmenu, setOpenSubmenu] = useState(null) -const sidebarVariants = { - expanded: { - width: 280, - transition: { - type: "spring", - stiffness: 400, - damping: 40, - mass: 1, + const navItems: NavItem[] = useMemo(() => [ + { + title: "Dashboard", + href: "/admin/coordinators/dashboard", + icon: BarChart3, }, - }, - collapsed: { - width: 80, - transition: { - type: "spring", - stiffness: 400, - damping: 40, - mass: 1, - }, - }, -} - -const contentVariants = { - expanded: { - opacity: 1, - x: 0, - transition: { - delay: 0.1, - duration: 0.3, - ease: "easeOut", + { + title: "Student Management", + href: "/admin/coordinators/students", + icon: Users, + badge: 3, }, - }, - collapsed: { - opacity: 0, - x: -10, - transition: { - duration: 0.2, - ease: "easeIn", + { + title: "Report Management", + href: "#", + icon: Flag, + badge: 12, + submenu: [ + { title: "Reported Employers", href: "/admin/coordinators/reports/employers", badge: 5 }, + { title: "Reported Companies", href: "/admin/coordinators/reports/companies", badge: 3 }, + { title: "Reported Listings", href: "/admin/coordinators/reports/listings", badge: 2 }, + { title: "Reported Students", href: "/admin/coordinators/reports/students", badge: 2 }, + ], }, - }, -} - -function MobileNavigation({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) { - return ( - - {isOpen && ( - <> - - -
-
-
-
- -
-
-

Coordinator

-

Portal

-
-
- -
-
- -
-
-
- - )} -
- ) -} - -function NavContent({ minimized = false, onItemClick }: { minimized?: boolean; onItemClick?: () => void }) { - const pathname = usePathname() - // const router = useRouter() // Remove unused router - const [openSubmenu, setOpenSubmenu] = useState(null) - const { data: session } = useSession() + ], []) useEffect(() => { const currentSubmenu = navItems.find((item) => item.submenu?.some((subItem) => pathname === subItem.href)) if (currentSubmenu) { setOpenSubmenu(currentSubmenu.title) } - }, [pathname]) + }, [pathname, navItems]) const toggleSubmenu = (title: string) => { - if (minimized) return - setOpenSubmenu(openSubmenu === title ? null : title) + if (openSubmenu === title) { + setOpenSubmenu(null) + } else { + setOpenSubmenu(title) + } } - const isActive = (href: string) => pathname === href - const isSubmenuActive = (submenu: { title: string; href: string }[]) => submenu.some((item) => pathname === item.href) + const isActive = (href: string) => { + return pathname === href + } - const handleLogout = async () => { - await signOut({ callbackUrl: "/admin/login" }) + const isSubmenuActive = (submenu: { title: string; href: string }[]) => { + return submenu.some((item) => pathname === item.href) } - return ( + const handleLogout = () => { + console.log("Logging out...") + + router.push("/login") + } + + const NavContent = () => (
- +
+
+
+ + + JS - - {!minimized && ( - -

- {(session?.user && typeof session.user === "object" && "firstName" in session.user - ? (session.user as { firstName?: string }).firstName - : "")} - {" "} - {(session?.user && typeof session.user === "object" && "lastName" in session.user - ? (session.user as { lastName?: string }).lastName - : "")} -

-

- {(() => { - const role = (session?.user && typeof session.user === "object" && "role" in session.user - ? (session.user as { role?: string }).role - : ""); - return role ? role.charAt(0).toUpperCase() + role.slice(1) : ""; - })()} -

-

- {(() => { - const dept = (session?.user && typeof session.user === "object" && "department" in session.user - ? (session.user as { department?: string }).department - : ""); - if (!dept) return ""; - if (dept.includes("Information Technology")) return "BSIT Department"; - if (dept.includes("Business Administration")) return "BSBA Department"; - if (dept.includes("Hospitality Management")) return "BSHM Department"; - if (dept.includes("Tourism Management")) return "BSTM Department"; - if (dept.includes("ABM")) return "ABM Department"; - if (dept.includes("HUMSS")) return "HUMSS Department"; - if (dept.includes("IT Mobile app and Web Development")) return "ICT Department"; - - return dept; - })()} -

-
- )} -
- - {!minimized && ( - - - - )} - +
+

Jane Smith

+

IT Department

+
+
) -} - -export default function AdminLayout({ children }: { children: React.ReactNode }) { - const [sidebarCollapsed, setSidebarCollapsed] = useState(false) - const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false) - const { data: session } = useSession() - - const handleLogout = async () => { - await signOut({ callbackUrl: "/admin/login" }) - } return ( - -
- setMobileSidebarOpen(false)} /> - - -
- - {!sidebarCollapsed && ( - -
- -
-
-

Coordinator

-

Portal

-
-
- )} -
- {sidebarCollapsed && ( -
- -
- )} -
- -
- -
+
+ {/* Sidebar for desktop */} +
+ +
-
- -
- + {/* Mobile sidebar */} + + + + + + + + -
-
-
-
- -
- - -
-
-
- - - - - - - - -
-

- {`${(session?.user as { firstName?: string })?.firstName ?? ""} ${(session?.user as { lastName?: string })?.lastName ?? ""}`.trim()} -

-

- {(session?.user as { username?: string })?.username ?? ""} -

-
-
- - - - - Profile - - - - Settings - - - - - Log out - -
-
+ {/* Main content */} +
+ {/* Header */} +
+
+
+
+ +
-
-
-
- - -
-
-
-
-
- } +
+ + + + + + + - {children} - - + +
+

Jane Smith

+

jane.smith@example.com

+
+
+ + + Profile + + + Settings + + + + Logout + +
+
-
-
+
+
+ + {/* Main content area */} +
+
+ +
+
+
+
+
+ } + > + {children} + +
+
- +
) } - - diff --git a/app/(app)/admin/coordinators/students/components/ojt-progress-tab.tsx b/app/(app)/admin/coordinators/students/components/ojt-progress-tab.tsx deleted file mode 100644 index d899e84..0000000 --- a/app/(app)/admin/coordinators/students/components/ojt-progress-tab.tsx +++ /dev/null @@ -1,698 +0,0 @@ -"use client" - -import type React from "react" -import { useEffect, useState } from "react" -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" -import { Badge } from "@/components/ui/badge" -import { Progress } from "@/components/ui/progress" -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" -import { - ChevronRight, - Building2, - Calendar, - Clock, - FileText, - CheckCircle, - AlertCircle, - ChevronLeft, - Sparkles, - Target, -} from "lucide-react" -import { AiOutlineFileSearch } from "react-icons/ai" -import { motion } from "framer-motion" -import { FaRegCalendarCheck } from "react-icons/fa" -import { FaCircleCheck } from "react-icons/fa6" -import { IoCloseCircle } from "react-icons/io5" -import { LuNotebookPen } from "react-icons/lu" -import { MdEventNote } from "react-icons/md" -import { Checkbox } from "@/components/ui/checkbox" -import { HiMiniClipboardDocumentList } from "react-icons/hi2" -import Image from "next/image" - -const REQUIRED_DOCUMENTS = [ - "Application Letter", - "Resume/CV", - "School Endorsement Letter", - "Medical Certificate", - "Insurance Certificate", - "Parent's Consent Form", - "Company Acceptance Letter", - "Training Plan", - "Weekly Time Records", - "Monthly Progress Reports", - "Final Evaluation Form", - "Certificate of Completion", -] - -interface Application { - id: string - companyName: string - jobTitle?: string - status: string - dateApplied: string - companyLogo?: string - company_name?: string -} - -interface WeeklyActivity { - week: number - activities: string - hoursLogged: number - date: string -} - -interface Document { - name: string - status: "Submitted" | "Pending" | "Approved" | "Rejected" - dateSubmitted?: string -} - -export interface OJTStudent { - course: string - id: string - name: string - email: string - isHired: boolean - applicationsSent?: number - applications?: Application[] - companyName?: string - startDate?: string - ojtStatus?: "In Progress" | "Completed" | "On Hold" - hoursCompleted?: number - requiredHours?: number - weeklyActivities?: WeeklyActivity[] - supervisorFeedback?: string - documents?: Document[] -} - -function getStatusBadge(status: string) { - let displayStatus = status - switch (status?.toLowerCase()) { - case "new": - displayStatus = "Pending" - break - case "shortlisted": - displayStatus = "Under Review" - break - case "interview scheduled": - displayStatus = "Interview Schedule" - break - case "waitlisted": - displayStatus = "Interviewed" - break - case "hired": - displayStatus = "Hired" - break - case "rejected": - displayStatus = "Rejected" - break - default: - displayStatus = status - } - - const variants: Record = { - Pending: "secondary", - "Under Review": "secondary", - "Interview Schedule": "default", - Interviewed: "default", - Hired: "default", - Rejected: "destructive", - "In Progress": "default", - Completed: "secondary", - "On Hold": "secondary", - Submitted: "secondary", - Approved: "secondary", - PendingDoc: "secondary", - } - - const lucideIcons: Record = { - Pending: , - "Under Review": , - "Interview Schedule": , - Interviewed: , - Hired: , - Rejected: , - Submitted: , - Approved: , - PendingDoc: , - } - - const colorMap: Record = { - Interviewed: { - base: "bg-indigo-100 text-indigo-700 border border-indigo-200", - hover: "hover:bg-indigo-200 hover:text-indigo-900", - }, - "Interview Schedule": { - base: "bg-purple-100 text-purple-700 border border-purple-200", - hover: "hover:bg-purple-200 hover:text-purple-900", - }, - "Under Review": { - base: "bg-amber-100 text-amber-700 border border-amber-200", - hover: "hover:bg-amber-200 hover:text-amber-900", - }, - Hired: { - base: "bg-emerald-100 text-emerald-700 border border-emerald-200", - hover: "hover:bg-emerald-200 hover:text-emerald-900", - }, - Pending: { - base: "bg-orange-100 text-orange-700 border border-orange-200", - hover: "hover:bg-orange-200 hover:text-orange-900", - }, - Rejected: { - base: "", - hover: "", - }, - } - - const color = colorMap[displayStatus] || { base: "", hover: "" } - const Icon = lucideIcons[displayStatus] || null - - return ( - - {Icon} - {displayStatus} - - ) -} - -export default function OJTProgressTab({ - student, -}: { - student: OJTStudent -}) { - const [page, setPage] = useState(1) - const perPage = 5 - const [companyLogoUrl, setCompanyLogoUrl] = useState(null) - - const applications = student.applications || [] - const hasHired = applications.some((app) => app.status && app.status.toLowerCase() === "hired") - const hiredApplication = applications.find((app) => app.status && app.status.toLowerCase() === "hired") - - const displayCompanyName = - hiredApplication?.company_name || - hiredApplication?.companyName || - student.companyName || - hiredApplication?.jobTitle || - "No Company Assigned" - - const effectiveOjtStatus = hasHired ? "hired" : student.ojtStatus - - useEffect(() => { - async function fetchLogo() { - const logoPath = hiredApplication?.companyLogo - if (logoPath) { - const res = await fetch( - `/api/employers/get-signed-url?bucket=company.logo&path=${encodeURIComponent(logoPath)}`, - ) - const data = await res.json() - if (data.signedUrl) setCompanyLogoUrl(data.signedUrl) - else setCompanyLogoUrl(null) - } else { - setCompanyLogoUrl(null) - } - } - fetchLogo() - }, [hiredApplication?.companyLogo]) - - function calculateWeekdaysBetween(startDate: Date, endDate: Date) { - let count = 0 - for (const current = new Date(startDate); current <= endDate; current.setDate(current.getDate() + 1)) { - const day = current.getDay() - if (day !== 0 && day !== 6) count++ - } - return count - } - - let calculatedHoursCompleted = student.hoursCompleted - - if (hasHired) { - const appliedDateStr = hiredApplication?.dateApplied || student.startDate - if (appliedDateStr) { - const startDate = new Date(appliedDateStr) - const today = new Date() - const weekdays = calculateWeekdaysBetween(startDate, today) - calculatedHoursCompleted = weekdays * 8 - } - } - - let requiredHours = 0 - if (student && student.course && typeof student.course === "string") { - const course = student.course.toLowerCase() - if ( - course.includes("information technology") || - course.includes("abm") || - course.includes("humss") || - course.includes("it mobile app") || - course.includes("web development") - ) { - requiredHours = 486 - } else { - requiredHours = 600 - } - } - - requiredHours = requiredHours || 0 - const totalPages = Math.ceil(applications.length / perPage) - const pagedApplications = applications.slice((page - 1) * perPage, page * perPage) - - function getVisiblePages(currentPage: number, totalPages: number) { - const delta = 2 - const range = [] - const rangeWithDots = [] - - for (let i = Math.max(2, currentPage - delta); i <= Math.min(totalPages - 1, currentPage + delta); i++) { - range.push(i) - } - - if (currentPage - delta > 2) { - rangeWithDots.push(1, "…") - } else { - rangeWithDots.push(1) - } - - rangeWithDots.push(...range) - - if (currentPage + delta < totalPages - 1) { - rangeWithDots.push("…", totalPages) - } else if (totalPages > 1) { - rangeWithDots.push(totalPages) - } - - return rangeWithDots - } - - const visiblePages = getVisiblePages(page, totalPages) - const progressPercentage = - student.isHired && calculatedHoursCompleted && requiredHours ? (calculatedHoursCompleted / requiredHours) * 100 : 0 - - const [docs, setDocs] = useState( - REQUIRED_DOCUMENTS.map((name) => { - const found = student.documents?.find((d) => d.name === name) - return found || { name, status: "Pending" } - }), - ) - - useEffect(() => { - setDocs( - REQUIRED_DOCUMENTS.map((name) => { - const found = student.documents?.find((d) => d.name === name) - return found || { name, status: "Pending" } - }), - ) - }, [student.documents]) - - function handleToggleDoc(index: number, checked: boolean | "indeterminate") { - setDocs((prev) => - prev.map((doc, i) => - i === index - ? { - ...doc, - status: checked === true ? "Submitted" : "Pending", - } - : doc, - ), - ) - } - - return ( -
- {effectiveOjtStatus?.toLowerCase() !== "hired" ? ( -
- - - - - Status: Not Hired Yet - - - -
- - Applications Sent: - - {student.applicationsSent} - -
-
-

- - OJT logs will be visible once the student is placed. -

-
-
-
- - - - - - Companies Applied To - - - -
- {applications.length === 0 ? ( -
- -
- This student hasn't applied for any companies yet -
-
- ) : ( - <> - - - - - Company Name - - Job Title - Status - - Date Applied - - - - - {pagedApplications.map((application, idx) => { - const key = application.id ? String(application.id) : `row-${idx}` - return ( - - {application.companyName} - {application.jobTitle || "-"} - - {getStatusBadge(application.status)} - - - {application.dateApplied ? new Date(application.dateApplied).toLocaleDateString() : "-"} - - - ) - })} - -
- -
-
- - -
- {visiblePages.map((p, idx) => ( -
- {p === "…" ? ( - - ) : ( - - )} -
- ))} -
- - -
-
- Page {page} of {totalPages} -
-
- - )} -
-
-
-
- ) : ( -
- {/* Company Information Card*/} -
-
-
-
-
- {companyLogoUrl ? ( - {displayCompanyName - ) : ( -
- {displayCompanyName?.charAt(0) || "?"} -
- )} -
- - {hiredApplication?.jobTitle && displayCompanyName - ? `${hiredApplication.jobTitle} at ${displayCompanyName}` - : displayCompanyName} - - {hiredApplication?.jobTitle && ( - Student's current Job Position - )} -
-
-
- -
- -
-
- - Start Date: - - {hiredApplication?.dateApplied - ? new Date(hiredApplication.dateApplied).toLocaleDateString() - : student.startDate - ? new Date(student.startDate).toLocaleDateString() - : "N/A"} - -
- -
- OJT Status: - {getStatusBadge(student.ojtStatus || "N/A")} -
- -
- - Hours: - - {calculatedHoursCompleted}/{requiredHours} - -
-
-
-
- -
-
-
- Completed: {calculatedHoursCompleted} hours - Required: {requiredHours > 0 ? requiredHours + " hours" : "N/A"} -
- -
{progressPercentage.toFixed(1)}% Complete
-
-
-
- - {/* Supervisor Feedback */} - {student.supervisorFeedback && ( - - - Supervisor Feedback - - -

- {student.supervisorFeedback} -

-
-
- )} - - {/* Document Checklist - Using purple theme */} - -
-
- - Document Submission Progress -
- {(() => { - const submittedCount = docs.filter( - (doc) => doc.status === "Submitted" || doc.status === "Approved", - ).length - const progressPercentage = docs.length > 0 ? (submittedCount / docs.length) * 100 : 0 - - return ( - <> -
- - Submitted: {submittedCount} of {docs.length} - - {progressPercentage.toFixed(0)}% Complete -
-
-
-
- - ) - })()} -
- - - {(() => { - const submittedCount = docs.filter( - (doc) => doc.status === "Submitted" || doc.status === "Approved", - ).length - const progressPercentage = docs.length > 0 ? (submittedCount / docs.length) * 100 : 0 - - return ( -
- {progressPercentage === 100 && ( -
- - All documents submitted! 🎉 -
- )} - -
-
-

- - Document Name -

-
- {docs.map((doc, index) => ( -
- - {doc.name} - - {(doc.status === "Submitted" || doc.status === "Approved") && ( - - )} -
- ))} -
-
- -
-

- - Submitted? -

-
- {docs.map((doc, index) => ( -
- handleToggleDoc(index, checked)} - className="w-5 h-5 data-[state=checked]:bg-purple-500 data-[state=checked]:border-purple-500 cursor-pointer" - /> -
- ))} -
-
-
-
- ) - })()} -
- -
- )} -
- ) -} - -export function OJTProgressWrapper({ - ojtStudent, - ojtApplicationsLoading, -}: { - ojtStudent: OJTStudent - ojtApplicationsLoading: boolean -}) { - return ( -
- {ojtApplicationsLoading ? ( -
-
- Loading -
- Loading OJT progress... -
- ) : ( - - )} -
- ) -} diff --git a/app/(app)/admin/coordinators/students/components/studentDetails.tsx b/app/(app)/admin/coordinators/students/components/studentDetails.tsx deleted file mode 100644 index 7230f4b..0000000 --- a/app/(app)/admin/coordinators/students/components/studentDetails.tsx +++ /dev/null @@ -1,582 +0,0 @@ -import { Badge as UIBadge } from "@/components/ui/badge" -import { CheckCircle, MessageSquare, Sparkles } from "lucide-react" -import { Button } from "@/components/ui/button" -import { DialogFooter, Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog" -import Tooltip from "@mui/material/Tooltip" -import { motion } from "framer-motion" -import { HiRocketLaunch } from "react-icons/hi2" -import { RiProgress6Fill, RiCheckboxCircleFill } from "react-icons/ri" -import { FaMagnifyingGlass, FaStar } from "react-icons/fa6" -import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs" -import { useState, useEffect } from "react" -import { Loader2 } from "lucide-react" -import { TbBulb } from "react-icons/tb" -import { MdMarkEmailRead, MdEditCalendar } from "react-icons/md" -import { LiaBusinessTimeSolid } from "react-icons/lia" -import { AiFillCloseCircle } from "react-icons/ai" -import { Checkbox } from "@/components/ui/checkbox" -import OJTProgressTab, { OJTStudent } from "./ojt-progress-tab" - -import { PiWarningCircleBold } from "react-icons/pi" - -import Lottie from "lottie-react" -import ojtTrackAnimation from "@/../public/animations/ojt-track.json" -import { VisuallyHidden } from "@radix-ui/react-visually-hidden" - - -type TimelineItem = { - name: string - position: string - update: string - time: string - icon: string - application_id: string - type: string - created_at: string - message: string - category?: string - job_title?: string -} - -type OJTApplication = { - id: string | number - companyName?: string - company_name?: string - status?: string - created_at?: string - applied_at?: string - job_title?: string - company_logo_image_path?: string -} - -type StudentInfo = { - email: string - name: string - studentId: string - status: string - course?: string - year?: string | number - company?: string - employer?: string - progress: number - profile_img?: string | null - section?: string | null - id?: string | number - application_id?: string - student_id?: string | number -} - -function getStatusDisplay(status: string) { - switch (status.toLowerCase()) { - case "new": - return { - label: "Not Applied", - bg: "bg-orange-100", - text: "text-orange-700", - border: "border-orange-200", - icon: , - } - case "shortlisted": - case "waitlisted": - return { - label: "Seeking Jobs", - bg: "bg-yellow-100", - text: "text-yellow-700", - border: "border-yellow-200", - icon: , - } - case "interview scheduled": - return { - label: "In Progress", - bg: "bg-blue-100", - text: "text-blue-700", - border: "border-blue-200", - icon: , - } - case "hired": - return { - label: "Hired", - bg: "bg-green-100", - text: "text-green-700", - border: "border-green-200", - icon: , - } - case "rejected": - return { - label: "", - bg: "", - text: "", - border: "", - icon: null, - } - default: - return { - label: status.charAt(0).toUpperCase() + status.slice(1), - bg: "bg-gray-100", - text: "text-gray-700", - border: "border-gray-200", - icon: null, - } - } -} - -function StatusBadge({ status }: { status: string }) { - const display = getStatusDisplay(status) - if (!display.label) return null - let tooltip = "Indicates the highest progress reached in the application process." - if (display.label === "Not Applied") { - tooltip = "This student hasn't applied for any jobs yet." - } else if (display.label === "Seeking Jobs") { - tooltip = "This student has applied for some jobs and is awaiting updates." - } else if (display.label === "In Progress") { - tooltip = "This student has an application currently in progress." - } else if (display.label === "Hired") { - tooltip = "This student has been hired for an OJT placement." - } else if (display.label === "Rejected") { - tooltip = "This student's application was not successful." - } - return ( - - - - {display.icon} - {display.label} - - - - ) -} - -function formatTimelineDate(dateString: string) { - if (!dateString) return "" - const date = new Date(dateString) - if (isNaN(date.getTime())) return dateString - const monthNames = [ - "January", "February", "March", "April", "May", "June", - "July", "August", "September", "October", "November", "December" - ] - const month = monthNames[date.getMonth()] - const day = date.getDate().toString().padStart(2, '0') - const year = date.getFullYear() - let hours = date.getHours() - const minutes = date.getMinutes().toString().padStart(2, '0') - const ampm = hours >= 12 ? "PM" : "AM" - hours = hours % 12 - if (hours === 0) hours = 12 - return `${month} ${day} ${year}, ${hours}:${minutes} ${ampm}` -} - -function getTimelineIcon(type: string) { - switch (type?.toLowerCase()) { - case "new": - case "applied": - case "applied-for": - return - case "shortlisted": - return - case "interview scheduled": - case "interview": - return - case "waitlisted": - return - case "hired": - return - case "rejected": - return - default: - return - } -} - -function getTimelineDescription(item: TimelineItem) { - let position = "" - if (item.job_title && item.job_title.trim() !== "") { - position = `for the position ${item.job_title}` - } else if (item.position && item.position.trim() !== "") { - position = `for the position ${item.position}` - } - if (!position && item.update) { - const match = item.update.match(/for the position ([^.]*)/i) - if (match && match[1]) { - position = `for the position ${match[1]}` - } - } - switch (item.type?.toLowerCase()) { - case "new": - return `Application submitted ${position}.` - case "applied-for": - return `Application submitted ${position}.` - case "shortlisted": - return `Shortlisted ${position}.` - case "waitlisted": - return `Placed on the waitlist ${position}.` - case "interview scheduled": - case "interview": - return `Interview scheduled ${position}.` - case "hired": - return `Hired ${position}.` - case "rejected": - return `Not selected ${position}.` - case "completed": - return `Internship completed ${position}.` - default: - return item.update || "" - } -} - -export default function StudentDetailsModalContent({ - student, - onClose, -}: { - student: StudentInfo - onClose: () => void -}) { - const [tab, setTab] = useState("info") - const [, setAvatarUrl] = useState(null) - const [timeline, setTimeline] = useState([]) - const [timelineLoading, setTimelineLoading] = useState(false) - const [timelineFilterOptions, setTimelineFilterOptions] = useState<{ value: string, label: string }[]>([]) - const [filterModalOpen, setFilterModalOpen] = useState(false) - const [selectedPositions, setSelectedPositions] = useState([]) - const [selectedStatuses, setSelectedStatuses] = useState([]) - const [ojtApplications, setOjtApplications] = useState([]) - - useEffect(() => { - async function fetchAvatar() { - if (student.profile_img) { - const res = await fetch("/api/students/get-signed-url", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - bucket: "user.avatars", - path: student.profile_img, - }), - }) - const data = await res.json() - if (data.signedUrl) setAvatarUrl(data.signedUrl) - else setAvatarUrl(null) - } else { - setAvatarUrl(null) - } - } - fetchAvatar() - }, [student.profile_img, student]) - - useEffect(() => { - if (tab === "timeline" && student && student.student_id) { - setTimelineLoading(true) - fetch(`/api/superadmin/coordinators/fetchTimeline?student_id=${student.student_id}`) - .then(res => res.json()) - .then(data => { - setTimeline(Array.isArray(data) ? data : []) - setTimelineLoading(false) - }) - .catch(() => setTimelineLoading(false)) - } - }, [tab, student.student_id, student]) - - useEffect(() => { - if (tab === "ojt" && student && student.student_id) { - fetch(`/api/superadmin/coordinators/fetchOJTProgress?student_id=${student.student_id}`) - .then(res => res.json()) - .then(data => { - setOjtApplications(Array.isArray(data) ? data : []) - }) - .catch(() => { - }) - } - }, [tab, student.student_id, student]) - - useEffect(() => { - if (tab === "timeline" && timeline.length > 0) { - const positions = Array.from(new Set(timeline.map(item => item.position).filter(Boolean))) - const statuses = Array.from(new Set(timeline.map(item => item.type).filter(Boolean))) - setTimelineFilterOptions([ - { value: "all", label: "All activities" }, - ...positions.map(pos => ({ value: `position:${pos}`, label: pos })), - ...statuses.map(st => ({ value: `status:${st}`, label: getStatusDisplay(st).label || st })), - ]) - } - }, [timeline, tab]) - - const positionOptions = timelineFilterOptions.filter(opt => opt.value.startsWith("position:")) - const statusOptions = timelineFilterOptions.filter(opt => opt.value.startsWith("status:")) - - let filteredTimeline = timeline - if (selectedPositions.length > 0 || selectedStatuses.length > 0) { - filteredTimeline = timeline.filter(item => - (selectedPositions.length === 0 || selectedPositions.includes(item.position)) || - (selectedStatuses.length === 0 || selectedStatuses.includes(item.type)) - ) - } - - function handlePositionToggle(pos: string) { - setSelectedPositions(prev => - prev.includes(pos) ? prev.filter(p => p !== pos) : [...prev, pos] - ) - } - function handleStatusToggle(st: string) { - setSelectedStatuses(prev => - prev.includes(st) ? prev.filter(s => s !== st) : [...prev, st] - ) - } - function clearFilters() { - setSelectedPositions([]) - setSelectedStatuses([]) - setFilterModalOpen(false) - } - - const ojtStudent: OJTStudent = { - id: String(student.id ?? student.student_id ?? ""), - name: student.name, - email: student.email, - isHired: (student.status?.toLowerCase() === "hired"), - companyName: - student.company || - ojtApplications.find(app => app.companyName || app.company_name)?.companyName || - ojtApplications.find(app => app.companyName || app.company_name)?.company_name || - "", - applicationsSent: ojtApplications.length, - applications: ojtApplications.map(app => ({ - id: String(app.id), - companyName: app.companyName || app.company_name || "-", - jobTitle: app.job_title || "-", - status: app.status || "", - dateApplied: app.applied_at || app.created_at || "", - companyLogo: app.company_logo_image_path || "", - })), - course: student.course || "" - } - - if (!student) return null - return ( -
- - - Student Details - - - - - - - OJT Progress - - - Full Timeline - - - Status Breakdown - - - - -
- {tab === "ojt" && ojtApplications.length === 0 ? ( -
-
- -
- - Fetching OJT Progress... - -
- ) : ( - - )} -
-
- -

Student Progress Timeline

-
- - - - - - - Timeline Filters - - -
- -
-
By Job Position
-
- {positionOptions.map(opt => ( - - ))} -
-
-
-
By Status
-
- {statusOptions.map(opt => ( - - ))} -
-
-
- - - -
-
- - {(selectedPositions.length === 0 && selectedStatuses.length === 0) - ? "All activities" - : [ - ...selectedPositions.map(p => `Job: ${p}`), - ...selectedStatuses.map(s => `Status: ${getStatusDisplay(s).label || s}`) - ].join(", ") - } - -
-
-
- {timelineLoading ? ( -
- - Loading timeline... -
- ) : filteredTimeline.length === 0 ? ( -
No timeline activity yet
- ) : ( - filteredTimeline.map((item, idx) => ( -
-
- {getTimelineIcon(item.type)} -
-
-
- {item.update} - {idx === 0 && ( - - - Latest - - - )} -
-
{formatTimelineDate(item.time)}
-
- {getTimelineDescription(item)} -
-
-
- )) - )} -
-
- -
-
- Coordinator Guidance:
- - Review the student's progress and activity logs above. Use this timeline to monitor application milestones, follow up with employers, and provide timely support to help the student succeed in their placement journey. - -
-
-
-
- -

Status Breakdown

-
-
- - Not Applied -
-
- - Seeking Jobs -
-
- - In Progress -
-
- - Hired -
-
- - Rejected -
-
-
-
- - - - -
- ) -} diff --git a/app/(app)/admin/coordinators/students/page.tsx b/app/(app)/admin/coordinators/students/page.tsx index 996b806..caa2353 100644 --- a/app/(app)/admin/coordinators/students/page.tsx +++ b/app/(app)/admin/coordinators/students/page.tsx @@ -1,7 +1,9 @@ "use client" import Image from "next/image" + import type React from "react" -import { useState, useRef, useEffect } from "react" + +import { useState, useRef } from "react" import { Search, Filter, @@ -9,6 +11,9 @@ import { MoreHorizontal, Eye, MessageSquare, + Clock, + CheckCircle, + AlertCircle, Edit, Upload, FileText, @@ -16,11 +21,7 @@ import { Check, Info, AlertTriangle, - Loader2, - Building2, } from "lucide-react" -import { FiCalendar } from "react-icons/fi" -import { HiOutlineUserGroup } from "react-icons/hi2" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" @@ -41,30 +42,17 @@ import { Progress } from "@/components/ui/progress" import { Avatar } from "@/components/ui/avatar" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" -import { motion } from "framer-motion" -import { HiRocketLaunch } from "react-icons/hi2" -import { RiProgress6Fill } from "react-icons/ri" -import { FaMagnifyingGlass } from "react-icons/fa6" -import Tooltip from "@mui/material/Tooltip" -import StudentDetailsModalContent from "./components/studentDetails" -import { PiWarningCircleBold } from "react-icons/pi" -import { LuGraduationCap } from "react-icons/lu" interface Student { id: number name: string studentId: string - email: string + course: string year: number - status: string + status: "not_hired" | "in_progress" | "hired" | "finished" progress: number company?: string employer?: string - course?: string - profile_img?: string | null - section?: string | null - application_id?: string - student_id?: string | number } interface BulkUploadPreviewData { @@ -96,61 +84,77 @@ export default function StudentManagement() { errors: string[] }>({ total: 0, successful: 0, failed: 0, errors: [] }) const fileInputRef = useRef(null) - const [students, setStudents] = useState([]) - const [isLoading, setIsLoading] = useState(false) - useEffect(() => { - setIsLoading(true) - fetch("/api/superadmin/coordinators/fetchDeptStudents") - .then((res) => res.json()) - .then((data) => { - setIsLoading(false) - if (Array.isArray(data)) { - setStudents( - data.map((row) => { - let studentId = "No Student ID" - const email = String(row.email ?? "") - if (email.endsWith("@alabang.sti.edu.ph")) { - const match = email.match(/\.(\d+)@alabang\.sti\.edu\.ph$/) - if (match && match[1]) { - studentId = `02000-${match[1]}` - } - } - return { - id: row.id, - name: [row.first_name, row.last_name].filter(Boolean).join(" "), - studentId, - email: row.email || "", - year: row.year || "", - status: row.status || "New", - progress: row.progress || 0, - company: row.company || "", - employer: row.employer || "", - course: row.course || "", - profile_img: row.profile_img || null, - section: row.section || null, - application_id: row.application_id || "", - student_id: row.id, - } - }) - ) - } - }) - }, []) + // Mock data + const students: Student[] = [ + { + id: 1, + name: "Alex Johnson", + studentId: "2023-IT-0001", + course: "BSIT", + year: 4, + status: "hired", + progress: 100, + company: "Tech Solutions Inc.", + employer: "John Smith", + }, + { + id: 2, + name: "Maria Garcia", + studentId: "2023-IT-0002", + course: "BSIT", + year: 4, + status: "in_progress", + progress: 65, + company: "Digital Innovations", + employer: "Sarah Williams", + }, + { + id: 3, + name: "James Wilson", + studentId: "2023-IT-0003", + course: "BSIT", + year: 3, + status: "not_hired", + progress: 10, + }, + { + id: 4, + name: "Emily Davis", + studentId: "2023-IT-0004", + course: "BSIT", + year: 4, + status: "finished", + progress: 100, + company: "WebTech Solutions", + employer: "Michael Brown", + }, + { + id: 5, + name: "Robert Martinez", + studentId: "2023-IT-0005", + course: "BSIT", + year: 3, + status: "in_progress", + progress: 45, + company: "Innovative Systems", + employer: "Jennifer Lee", + }, + ] const filteredStudents = students.filter((student) => { const matchesSearch = student.name.toLowerCase().includes(searchQuery.toLowerCase()) || student.studentId.toLowerCase().includes(searchQuery.toLowerCase()) || - (student.course ?? "").toLowerCase().includes(searchQuery.toLowerCase()) || + student.course.toLowerCase().includes(searchQuery.toLowerCase()) || (student.company && student.company.toLowerCase().includes(searchQuery.toLowerCase())) const matchesTab = activeTab === "all" || - (activeTab === "not_hired" && student.status.toLowerCase() === "not_hired") || - (activeTab === "in_progress" && student.status.toLowerCase() === "in_progress") || - (activeTab === "hired" && student.status.toLowerCase() === "hired") || - (activeTab === "finished" && student.status.toLowerCase() === "finished") + (activeTab === "not_hired" && student.status === "not_hired") || + (activeTab === "in_progress" && student.status === "in_progress") || + (activeTab === "hired" && student.status === "hired") || + (activeTab === "finished" && student.status === "finished") return matchesSearch && matchesTab }) @@ -162,15 +166,31 @@ export default function StudentManagement() { const handleEditStatus = (student: Student) => { setSelectedStudent(student) - setSelectedStatus(student.status) + setSelectedStatus(mapStatusToEditOptions(student.status)) setIsEditStatusDialogOpen(true) } + const mapStatusToEditOptions = (status: string): string => { + switch (status) { + case "not_hired": + return "not-started" + case "in_progress": + return "in-progress" + case "hired": + case "finished": + return "completed" + default: + return "not-started" + } + } + const updateStudentStatus = () => { if (!selectedStudent) return + // In a real application, you would call an API to update the student status console.log(`Updating student ${selectedStudent.name} status to ${selectedStatus}`) + // Close the dialog setIsEditStatusDialogOpen(false) } @@ -187,6 +207,7 @@ export default function StudentManagement() { const file = e.target.files?.[0] if (file) { setSelectedFile(file) + // Mock parsing CSV file setTimeout(() => { const mockPreviewData: BulkUploadPreviewData[] = [ { @@ -252,6 +273,7 @@ export default function StudentManagement() { const file = files[0] if (file.type === "text/csv" || file.name.endsWith(".csv")) { setSelectedFile(file) + // Mock parsing CSV file (same as handleFileChange) setTimeout(() => { const mockPreviewData: BulkUploadPreviewData[] = [ { @@ -309,6 +331,7 @@ export default function StudentManagement() { const handleUpload = () => { setUploadStep("uploading") + // Simulate upload progress let progress = 0 const interval = setInterval(() => { progress += 10 @@ -317,6 +340,7 @@ export default function StudentManagement() { if (progress >= 100) { clearInterval(interval) + // Mock results const validRecords = previewData.filter((record) => record.isValid) const invalidRecords = previewData.filter((record) => !record.isValid) @@ -346,8 +370,10 @@ export default function StudentManagement() { } const downloadTemplate = () => { + // Create CSV content const csvContent = "studentId,name,course,year,status\n2023-IT-XXXX,Student Name,BSIT,3,not_hired" + // Create a blob and download const blob = new Blob([csvContent], { type: "text/csv" }) const url = URL.createObjectURL(blob) const a = document.createElement("a") @@ -360,522 +386,499 @@ export default function StudentManagement() { } return ( -
- {isLoading ? ( -
-
-
Fetching users...
+
+
+
+

Student Management

+

Manage IT department students and track their progress

- ) : ( - <> - -
-

Student Management

-

Manage IT department students and track their progress

-
-
- - + +
+
+ + + + IT Department Students + View and manage all students in the IT department + + +
+
+
+ + setSearchQuery(e.target.value)} + /> +
+
- +
+ + + + All + Not Hired + In Progress + Hired + Finished + + + + + + + + + + + + + + + + + +
+
+ + {/* View Student Dialog */} + + + + Student Details + Detailed information about the student and their progress. + + {selectedStudent && ( +
+
+ + Student + +
+

{selectedStudent.name}

+

{selectedStudent.studentId}

+
+ +
+
+
- - - - IT Department Students - - View and manage all students in the IT department - - - -
-
-
- - setSearchQuery(e.target.value)} - /> -
- +
+
+ +

{selectedStudent.course}

+
+
+ +

{selectedStudent.year}

+
+ {selectedStudent.company && ( +
+ +

{selectedStudent.company}

+ )} + {selectedStudent.employer && ( +
+ +

{selectedStudent.employer}

+
+ )} +
+ +
+ +
+
+ {selectedStudent.progress}% Complete +
+
- - - - All - - - Not Hired - - - In Progress - - - Hired - - - Finished - - - - - - - - - - - - - - - - - - - - - +
- {/* View Student Dialog */} - - - {selectedStudent && ( -
- -
- - - - - -
-
-

{selectedStudent.name}

- - - -
-

{selectedStudent.studentId}

-
- - - Course: - {selectedStudent.course} - - - - Year: - {selectedStudent.year} - - - - Section: - {selectedStudent.section} - -
-
-
- {selectedStudent.company && ( - - - {selectedStudent.company} - - )} +
+

Timeline

+
+
+
+
+
+
+
+
+

Application Submitted

+

Jan 15, 2023

- -
- setIsViewDialogOpen(false)} - />
-
- )} - -
- - {/* Edit Status Dialog */} - - - - Update Student Status - Change the current status of the student. - -
- {selectedStudent && ( - <> -
- - Student - -
-

{selectedStudent.name}

-

{selectedStudent.studentId}

+
+
+
+
+
+
+
+

Interview Scheduled

+

Jan 20, 2023

-
- - +
+
+
+
= 50 ? "bg-blue-500" : "bg-gray-300"}`} + > + {selectedStudent.progress >= 50 ? ( + + ) : ( + + )} +
+
- - )} -
- - - - - -
- - {/* Bulk Upload Dialog */} - - - - Bulk Upload Students - Upload multiple student records at once using a CSV file. - - - {uploadStep === "select" && ( -
-
fileInputRef.current?.click()} - > -
- -

Upload CSV File

-

- Drag and drop your CSV file here, or click to browse +

+

Placement Confirmed

+

+ {selectedStudent.progress >= 50 ? "Feb 1, 2023" : "Pending"}

- -
- - - - CSV Format - - Your CSV file should include the following columns: studentId, name, course, year, status. - - - -
- )} - - {uploadStep === "preview" && ( -
-
+ {selectedStudent.progress === 100 ? ( + + ) : ( + + )} +
+
-

File Preview

+

Internship Completed

- {selectedFile?.name} ({previewData.length} records) + {selectedStudent.progress === 100 ? "May 30, 2023" : "Pending"}

-
- - {previewData.some((record) => !record.isValid) && ( - - - Validation Errors - - Some records have validation errors. Please fix them before uploading. - - - )} - -
- - - - Student ID - Name - Course - Year - Status - Valid - - - - {previewData.map((record, index) => ( - - {record.studentId || "-"} - {record.name} - {record.course} - {record.year} - {record.status} - - {record.isValid ? ( - - ) : ( -
- - {record.errors && record.errors.length > 0 && ( -
-
    - {record.errors.map((error, i) => ( -
  • {error}
  • - ))} -
-
- )} -
- )} -
-
- ))} -
-
+
+
+
+ )} + + + + + +
+ + {/* Edit Status Dialog */} + + + + Update Student Status + Change the current status of the student. + +
+ {selectedStudent && ( + <> +
+ + Student + +
+

{selectedStudent.name}

+

{selectedStudent.studentId}

- )} +
+ + +
+ + )} +
+ + + + +
+
+ + {/* Bulk Upload Dialog */} + + + + Bulk Upload Students + Upload multiple student records at once using a CSV file. + + + {uploadStep === "select" && ( +
+
fileInputRef.current?.click()} + > +
+ +

Upload CSV File

+

+ Drag and drop your CSV file here, or click to browse +

+ + +
+
- {uploadStep === "uploading" && ( -
-
-

Uploading Students...

- -

{uploadProgress}% complete

-
+ + + CSV Format + + Your CSV file should include the following columns: studentId, name, course, year, status. + + + +
+ )} + + {uploadStep === "preview" && ( +
+
+
+

File Preview

+

+ {selectedFile?.name} ({previewData.length} records) +

+ +
+ + {previewData.some((record) => !record.isValid) && ( + + + Validation Errors + + Some records have validation errors. Please fix them before uploading. + + )} - {uploadStep === "results" && ( -
-
- {uploadResults.failed === 0 ? ( -
- -
- ) : ( -
- -
- )} +
+ + + + Student ID + Name + Course + Year + Status + Valid + + + + {previewData.map((record, index) => ( + + {record.studentId || "-"} + {record.name} + {record.course} + {record.year} + {record.status} + + {record.isValid ? ( + + ) : ( +
+ + {record.errors && record.errors.length > 0 && ( +
+
    + {record.errors.map((error, i) => ( +
  • {error}
  • + ))} +
+
+ )} +
+ )} +
+
+ ))} +
+
+
+
+ )} -

Upload Complete

-

- {uploadResults.successful} of {uploadResults.total} records were successfully uploaded -

-
+ {uploadStep === "uploading" && ( +
+
+

Uploading Students...

+ +

{uploadProgress}% complete

+
+
+ )} -
- - -

{uploadResults.total}

-

Total Records

-
-
- - -

{uploadResults.successful}

-

Successful

-
-
- - -

{uploadResults.failed}

-

Failed

-
-
+ {uploadStep === "results" && ( +
+
+ {uploadResults.failed === 0 ? ( +
+ +
+ ) : ( +
+
+ )} - {uploadResults.failed > 0 && ( - - - Upload Errors - -

The following errors occurred during upload:

-
    - {uploadResults.errors.slice(0, 3).map((error, index) => ( -
  • {error}
  • - ))} - {uploadResults.errors.length > 3 &&
  • ...and {uploadResults.errors.length - 3} more errors
  • } -
-
-
- )} -
+

Upload Complete

+

+ {uploadResults.successful} of {uploadResults.total} records were successfully uploaded +

+
+ +
+ + +

{uploadResults.total}

+

Total Records

+
+
+ + +

{uploadResults.successful}

+

Successful

+
+
+ + +

{uploadResults.failed}

+

Failed

+
+
+
+ + {uploadResults.failed > 0 && ( + + + Upload Errors + +

The following errors occurred during upload:

+
    + {uploadResults.errors.slice(0, 3).map((error, index) => ( +
  • {error}
  • + ))} + {uploadResults.errors.length > 3 &&
  • ...and {uploadResults.errors.length - 3} more errors
  • } +
+
+
)} +
+ )} - - {uploadStep === "select" && ( - - )} + + {uploadStep === "select" && ( + + )} - {uploadStep === "preview" && ( - <> - - - - )} + {uploadStep === "preview" && ( + <> + + + + )} - {uploadStep === "uploading" && } + {uploadStep === "uploading" && } - {uploadStep === "results" && ( - <> - - - - )} - - -
- - )} + {uploadStep === "results" && ( + <> + + + + )} + + +
) } @@ -890,48 +893,45 @@ function StudentsTable({ onEditStatus: (student: Student) => void }) { return ( - +
- - Student ID - Name - School Email - Year - Status - Company - Actions + + Student ID + Name + Course + Year + Status + Progress + Company + Actions {students.length === 0 ? ( - + No students found ) : ( - students.map((student, idx) => ( - - {student.studentId} - {student.name} - {student.email} - {student.year} - + students.map((student) => ( + + {student.studentId} + {student.name} + {student.course} + {student.year} + - {student.company || "-"} - + +
+ + {student.progress}% +
+
+ {student.company || "-"} +
- +
) } - - -function getStatusDisplay(status: string) { - switch (status.toLowerCase()) { - case "new": - return { - label: "Not Applied", - bg: "bg-orange-100", - text: "text-orange-700", - border: "border-orange-200", - icon: , - } - case "shortlisted": - case "waitlisted": - return { - label: "Seeking Jobs", - bg: "bg-yellow-100", - text: "text-yellow-700", - border: "border-yellow-200", - icon: , - } - case "interview scheduled": - return { - label: "In Progress", - bg: "bg-blue-100", - text: "text-blue-700", - border: "border-blue-200", - icon: , - } - case "hired": - return { - label: "Hired", - bg: "bg-green-100", - text: "text-green-700", - border: "border-green-200", - icon: , - } - case "rejected": - return { - label: "", - bg: "", - text: "", - border: "", - icon: null, - } - default: - return { - label: status.charAt(0).toUpperCase() + status.slice(1), - bg: "bg-gray-100", - text: "text-gray-700", - border: "border-gray-200", - icon: null, - } - } -} - function StatusBadge({ status }: { status: string }) { - const display = getStatusDisplay(status) - if (!display.label) return null - let tooltip = "Indicates the highest progress reached in the application process." - if (display.label === "Not Applied") { - tooltip = "This student hasn't applied for any jobs yet." - } else if (display.label === "Seeking Jobs") { - tooltip = "This student has applied for some jobs and is awaiting updates." - } else if (display.label === "In Progress") { - tooltip = "This student has an application currently in progress." - } else if (display.label === "Hired") { - tooltip = "This student has been hired for an OJT placement." - } else if (display.label === "Rejected") { - tooltip = "This student's application was not successful." - } - return ( - - - - {display.icon} - {display.label} - - - - ) -} - -function ProfileImage({ profile_img, name }: { profile_img?: string | null, name: string }) { - const [avatarUrl, setAvatarUrl] = useState(null) - const [loading, setLoading] = useState(false) - - useEffect(() => { - let ignore = false - async function fetchAvatar() { - if (profile_img) { - setLoading(true) - try { - const res = await fetch("/api/students/get-signed-url", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - bucket: "user.avatars", - path: profile_img, - }), - }) - const data = await res.json() - if (!ignore) setAvatarUrl(data.signedUrl || null) - } catch { - if (!ignore) setAvatarUrl(null) - } finally { - if (!ignore) setLoading(false) - } - } else { - setAvatarUrl(null) - } - } - fetchAvatar() - return () => { ignore = true } - }, [profile_img]) - - if (loading) { + if (status === "not_hired") { return ( -
- -
+ + + Not Hired + ) - } - if (avatarUrl) { + } else if (status === "in_progress") { return ( - {name} + + + In Progress + ) - } - if (profile_img === null) { + } else if (status === "hired") { return ( -
- -
+ + + Hired + + ) + } else if (status === "finished") { + return ( + + + Finished + ) } - return ( -
- - - - -
- ) + return null } - diff --git a/app/(app)/admin/login/page.tsx b/app/(app)/admin/login/page.tsx deleted file mode 100644 index dbe8047..0000000 --- a/app/(app)/admin/login/page.tsx +++ /dev/null @@ -1,357 +0,0 @@ -"use client" - -import type React from "react" -import { useState } from "react" -import { useRouter } from "next/navigation" -import Link from "next/link" -import { Eye, EyeOff, Lock, User, Shield, Zap, Users } from "lucide-react" -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { Label } from "@/components/ui/label" -import { Card, CardContent } from "@/components/ui/card" -import { Alert, AlertDescription } from "@/components/ui/alert" -import { motion, AnimatePresence } from "framer-motion" -import { cn } from "@/lib/utils" -import { MdQuestionMark } from "react-icons/md" -import { signIn } from "next-auth/react" -import Image from "next/image" - -export default function LoginPage() { - const router = useRouter() - const [username, setUsername] = useState("") - const [password, setPassword] = useState("") - const [showPassword, setShowPassword] = useState(false) - const [error, setError] = useState("") - const [isLoading, setIsLoading] = useState(false) - - - const handleLogin = async (e: React.FormEvent) => { - e.preventDefault() - setError("") - setIsLoading(true) - - const res = await signIn("admin-login", { - redirect: false, - username, - password, - }) - - if (res?.error) { - setError(res.error) - setIsLoading(false) - return - } - - if (res?.ok) { - const { getSession } = await import("next-auth/react") - const session = await getSession() - if (session?.user?.role === "superadmin") { - router.push("/admin/superadmin/dashboard") - } else { - router.push("/admin/coordinators/dashboard") - } - } - } - - const features = [ - { - icon: Users, - title: "Student Progress", - description: "Easily monitor and track OJT progress for every student", - color: "from-blue-500 to-cyan-500", - }, - { - icon: Zap, - title: "Employer Connect", - description: "Seamlessly contact and coordinate with partner employers", - color: "from-purple-500 to-pink-500", - }, - { - icon: Shield, - title: "Report Management", - description: "Manage reports and keep the school and students safe in one place", - color: "from-emerald-500 to-teal-500", - }, - ] - - return ( -
- -
-
- -
- -
- {/* Header */} - -
- Seekr Logo -
-
-

- Welcome to the -
- - Seekr Admin Portal - -

-

- Manage your organization with powerful tools and insights -

-
-
- - {/* Features */} -
- {features.map((feature, index) => ( - -
- -
-
-

{feature.title}

-

{feature.description}

-
-
- ))} -
- - {/* Footer */} - -

© {new Date().getFullYear()} Seekr Admin System

-
-
- - - {/* Right side - Login form */} -
- - {/* Mobile header */} -
-
- Seekr Logo -
-
- - {/* Form header */} -
-

Welcome back!

-

Sign in to access your admin dashboard

-
- - {/* Login form */} - - - - {error && ( - - - {error} - - - )} - - -
- - -
- - setUsername(e.target.value)} - required - /> -
-
- - -
- -
-
- - setPassword(e.target.value)} - required - /> - -
-
- - Forgot password? - -
-
- - - - - - - - - -
- - {/* Help Desk */} - -
-
- -
-
-

Don't have an account yet?

-
-

- Please contact your administrator to create your admin account. -

-
-
-
-
-
-
- - -
-
-
- ) -} - diff --git a/app/(app)/admin/superadmin/account-management/admins/page.tsx b/app/(app)/admin/superadmin/account-management/admins/page.tsx index 047559f..3abbab1 100644 --- a/app/(app)/admin/superadmin/account-management/admins/page.tsx +++ b/app/(app)/admin/superadmin/account-management/admins/page.tsx @@ -1,51 +1,41 @@ "use client" -import { useState, useEffect } from "react" -import { Plus, Search, Filter, Download, MoreHorizontal, Edit, Trash, Archive, Eye, User } from "lucide-react" +import React, { useState } from "react" + +import { Plus, Search, Filter, Download, MoreHorizontal, Edit, Trash, Archive, Eye } from "lucide-react" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { - Dialog, - DialogContent, + Dialog as UIDialog, + DialogContent as UIDialogContent, DialogDescription, DialogFooter, DialogHeader, - DialogTitle, + DialogTitle as UIDialogTitle, DialogTrigger, } from "@/components/ui/dialog" import { Label } from "@/components/ui/label" import { Badge } from "@/components/ui/badge" -import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" -import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from "@/components/ui/alert-dialog" -import { motion } from "framer-motion" -import { cn } from "@/lib/utils" -import toast from 'react-hot-toast' -import ExcelJS from "exceljs" - -interface Coordinator { - id: number - username: string - first_name: string - middle_name: string - last_name: string - suffix?: string - department: string - status: "active" | "inactive" | "archived" - created_at?: string -} + +import Table from "@mui/material/Table" +import TableBody from "@mui/material/TableBody" +import TableCell from "@mui/material/TableCell" +import TableContainer from "@mui/material/TableContainer" +import TableHead from "@mui/material/TableHead" +import TableRow from "@mui/material/TableRow" +import Paper from "@mui/material/Paper" +import Menu from "@mui/material/Menu" +import MenuItem from "@mui/material/MenuItem" +import IconButton from "@mui/material/IconButton" +import Dialog from "@mui/material/Dialog" +import DialogTitle from "@mui/material/DialogTitle" +import DialogContent from "@mui/material/DialogContent" +import DialogActions from "@mui/material/DialogActions" +import Typography from "@mui/material/Typography" + interface Admin { id: number @@ -74,7 +64,6 @@ export default function AdminsManagement() { firstName: "", middleName: "", lastName: "", - suffix: "", department: "", }) const [editFormData, setEditFormData] = useState({ @@ -85,60 +74,75 @@ export default function AdminsManagement() { department: "", status: "", }) - const [adminList, setAdminList] = useState([]) - const [isCreating, setIsCreating] = useState(false) - const [fieldErrors, setFieldErrors] = useState({ - username: "", - password: "", - firstName: "", - lastName: "", - department: "", - }) - const [page, setPage] = useState(1) - const pageSize = 7 - const [isLoading, setIsLoading] = useState(false) - - useEffect(() => { - async function fetchAdmins() { - setIsLoading(true) - const res = await fetch("/api/superadmin/fetchUsers") - setIsLoading(false) - if (!res.ok) return - const { coordinators } = await res.json() - if (Array.isArray(coordinators)) { - setAdminList( - coordinators.map((c: Coordinator) => ({ - id: c.id, - username: c.username, - name: { - first: c.first_name, - middle: c.middle_name, - last: c.last_name + (c.suffix && c.suffix !== "none" ? `, ${c.suffix}` : ""), - }, - department: c.department, - status: c.status, - createdAt: c.created_at ? c.created_at.split("T")[0] : "", - })) - ) - } - } - fetchAdmins() - }, []) + const [adminList, setAdminList] = useState([ + { + id: 1, + username: "johndoe", + name: { + first: "John", + middle: "A", + last: "Doe", + }, + department: "IT", + status: "active", + createdAt: "2023-01-15", + }, + { + id: 2, + username: "janesmith", + name: { + first: "Jane", + middle: "B", + last: "Smith", + }, + department: "HR", + status: "active", + createdAt: "2023-02-20", + }, + { + id: 3, + username: "mikebrown", + name: { + first: "Mike", + middle: "C", + last: "Brown", + }, + department: "Marketing", + status: "inactive", + createdAt: "2023-03-10", + }, + { + id: 4, + username: "sarahjones", + name: { + first: "Sarah", + middle: "D", + last: "Jones", + }, + department: "Finance", + status: "archived", + createdAt: "2023-04-05", + }, + { + id: 5, + username: "robertwilson", + name: { + first: "Robert", + middle: "E", + last: "Wilson", + }, + department: "IT", + status: "active", + createdAt: "2023-05-12", + }, + ]) const filteredAdmins = adminList.filter((admin) => { - const search = searchQuery.trim().toLowerCase() - const nameParts = [ - admin.name.first, - admin.name.middle, - admin.name.last, - `${admin.name.first} ${admin.name.last}`, - `${admin.name.first} ${admin.name.middle} ${admin.name.last}`, - `${admin.name.last} ${admin.name.first}`, - ].map(s => s.toLowerCase()) const matchesSearch = - admin.username.toLowerCase().includes(search) || - admin.department.toLowerCase().includes(search) || - nameParts.some(part => part.includes(search)) + admin.username.toLowerCase().includes(searchQuery.toLowerCase()) || + admin.name.first.toLowerCase().includes(searchQuery.toLowerCase()) || + admin.name.last.toLowerCase().includes(searchQuery.toLowerCase()) || + admin.department.toLowerCase().includes(searchQuery.toLowerCase()) const matchesTab = activeTab === "all" || @@ -149,13 +153,6 @@ export default function AdminsManagement() { return matchesSearch && matchesTab }) - const pageCount = Math.ceil(filteredAdmins.length / pageSize) - const paginatedAdmins = filteredAdmins.slice((page - 1) * pageSize, page * pageSize) - - useEffect(() => { - if (page > pageCount && pageCount > 0) setPage(pageCount) - }, [pageCount, page]) - const handleViewAdmin = (admin: Admin) => { setSelectedAdmin(admin) setIsViewDialogOpen(true) @@ -179,139 +176,89 @@ export default function AdminsManagement() { setIsDeleteDialogOpen(true) } - const handleArchiveAdmin = async (admin: Admin) => { - const isArchived = admin.status === "archived" - const newStatus = isArchived ? "active" : "archived" - await toast.promise( - fetch("/api/superadmin/actions", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ id: admin.id, status: newStatus }), - }).then(res => { - if (!res.ok) throw new Error() - setAdminList(list => - list.map(item => - item.id === admin.id ? { ...item, status: newStatus } : item - ) - ) - }), - { - loading: isArchived ? "Unarchiving..." : "Archiving...", - success: isArchived ? "Admin unarchived." : "Admin archived.", - error: isArchived ? "Failed to unarchive admin." : "Failed to archive admin.", - }, - { position: "bottom-right" } - ) + const handleArchiveAdmin = (admin: Admin) => { + const updatedAdmins = adminList.map((item) => { + if (item.id === admin.id) { + return { + ...item, + status: item.status === "archived" ? "active" : "archived" as "active" | "archived", + } + } + return item + }) + setAdminList(updatedAdmins) } - const handleCreateAdmin = async () => { - const errors = { - username: !formData.username ? "Username is required" : "", - password: !formData.password ? "Password is required" : "", - firstName: !formData.firstName ? "First Name is required" : "", - lastName: !formData.lastName ? "Last Name is required" : "", - department: !formData.department ? "Department is required" : "", - } - setFieldErrors(errors) - if (Object.values(errors).some(Boolean)) { + const handleInputChange = (e: React.ChangeEvent) => { + const { id, value } = e.target + setFormData({ + ...formData, + [id]: value, + }) + } + + const handleEditInputChange = (e: React.ChangeEvent) => { + const { id, value } = e.target + setEditFormData({ + ...editFormData, + [id]: value, + }) + } + + const handleSelectChange = (value: string) => { + setFormData({ + ...formData, + department: value, + }) + } + + const handleEditSelectChange = (field: string, value: string) => { + setEditFormData({ + ...editFormData, + [field]: value, + }) + } + + const handleCreateAdmin = () => { + if (!formData.username || !formData.password || !formData.firstName || !formData.lastName || !formData.department) { + alert("Please fill in all required fields") return } - setIsCreating(true) - try { - const res = await fetch("/api/superadmin/create-new", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - username: formData.username, - password: formData.password, - firstName: formData.firstName, - middleName: formData.middleName, - lastName: formData.lastName, - suffix: formData.suffix, - department: formData.department, - }), - }) - if (!res.ok) { - if (res.status === 409) { - setFieldErrors(f => ({ ...f, username: "Username already exists" })) - } else { - const errorData = await res.json().catch(() => null) - if (errorData && errorData.error && errorData.error.toLowerCase().includes("username already exists")) { - setFieldErrors(f => ({ ...f, username: "Username already exists" })) - } else { - toast.error('Failed to create OJT Coordinator.', { - position: 'bottom-right', - duration: 5000, - style: { - border: '1px solid #dc2626', - padding: '16px', - color: '#dc2626', - }, - iconTheme: { - primary: '#dc2626', - secondary: '#FEF2F2', - }, - }) - } - } - setIsCreating(false) - return - } - const { coordinator } = await res.json() - let newId = coordinator.id - if (adminList.some(a => a.id === newId)) { - newId = Math.max(0, ...adminList.map(a => a.id)) + 1 - } - const newAdmin: Admin = { - id: newId, - username: coordinator.username, - name: { - first: coordinator.first_name, - middle: coordinator.middle_name, - last: coordinator.last_name + (coordinator.suffix && coordinator.suffix !== "none" ? `, ${coordinator.suffix}` : ""), - }, - department: coordinator.department, - status: coordinator.status, - createdAt: coordinator.created_at ? coordinator.created_at.split("T")[0] : new Date().toISOString().split("T")[0], - } - setAdminList([...adminList, newAdmin]) - setFormData({ - username: "", - password: "", - firstName: "", - middleName: "", - lastName: "", - suffix: "", - department: "", - }) - setIsCreateDialogOpen(false) - toast.success('OJT Coordinator created successfully!', { - position: 'bottom-right', - duration: 5000, - style: { - border: '1px solid #2563eb', - padding: '16px', - color: '#2563eb', - - }, - iconTheme: { - primary: '#2563eb', - secondary: '#EFF6FF', - }, - }) - } finally { - setIsCreating(false) + + const newAdmin: Admin = { + id: adminList.length > 0 ? Math.max(...adminList.map((admin) => admin.id)) + 1 : 1, + username: formData.username, + name: { + first: formData.firstName, + middle: formData.middleName, + last: formData.lastName, + }, + department: formData.department, + status: "active", + createdAt: new Date().toISOString().split("T")[0], } + + setAdminList([...adminList, newAdmin]) + + setFormData({ + username: "", + password: "", + firstName: "", + middleName: "", + lastName: "", + department: "", + }) + setIsCreateDialogOpen(false) + + alert("Admin created successfully!") } const handleUpdateAdmin = () => { - if ( - !selectedAdmin || - !editFormData.username || - !editFormData.firstName || - !editFormData.lastName || - !editFormData.department - ) { + if (!selectedAdmin) return + + + if (!editFormData.username || !editFormData.firstName || !editFormData.lastName || !editFormData.department) { + alert("Please fill in all required fields") return } @@ -326,10 +273,9 @@ export default function AdminsManagement() { last: editFormData.lastName, }, department: editFormData.department, - status: (["active", "inactive", "archived"].includes(editFormData.status) ? editFormData.status : "active") as - | "active" - | "inactive" - | "archived", + status: (["active", "inactive", "archived"].includes(editFormData.status) + ? editFormData.status + : "active") as "active" | "inactive" | "archived", } } return admin @@ -337,46 +283,33 @@ export default function AdminsManagement() { setAdminList(updatedAdmins) setIsEditDialogOpen(false) + alert("Admin updated successfully!") } const confirmDeleteAdmin = () => { if (!selectedAdmin) return + const updatedAdmins = adminList.filter((admin) => admin.id !== selectedAdmin.id) setAdminList(updatedAdmins) setIsDeleteDialogOpen(false) + alert("Admin deleted successfully!") } - const exportAdmins = async () => { - const workbook = new ExcelJS.Workbook() - const worksheet = workbook.addWorksheet("Admins") - worksheet.columns = [ - { header: "ID", key: "id", width: 10 }, - { header: "Username", key: "username", width: 20 }, - { header: "First Name", key: "first", width: 20 }, - { header: "Middle Name", key: "middle", width: 20 }, - { header: "Last Name", key: "last", width: 20 }, - { header: "Department", key: "department", width: 25 }, - { header: "Status", key: "status", width: 15 }, - { header: "Created At", key: "createdAt", width: 18 }, - ] - filteredAdmins.forEach((admin) => { - worksheet.addRow({ - id: admin.id, - username: admin.username, - first: admin.name.first, - middle: admin.name.middle, - last: admin.name.last, - department: admin.department, - status: admin.status, - createdAt: admin.createdAt, - }) - }) - const buffer = await workbook.xlsx.writeBuffer() - const blob = new Blob([buffer], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" }) + const exportAdmins = () => { + + const headers = "ID,Username,First Name,Middle Name,Last Name,Department,Status,Created At\n" + const csvContent = filteredAdmins + .map( + (admin) => + `${admin.id},${admin.username},${admin.name.first},${admin.name.middle},${admin.name.last},${admin.department},${admin.status},${admin.createdAt}`, + ) + .join("\n") + + const blob = new Blob([headers + csvContent], { type: "text/csv" }) const url = URL.createObjectURL(blob) const a = document.createElement("a") a.href = url - a.download = "ojt_coordinators.xlsx" + a.download = "admins.csv" document.body.appendChild(a) a.click() document.body.removeChild(a) @@ -384,416 +317,229 @@ export default function AdminsManagement() { } return ( -
- {/* Header */} - +
+
-

OJT Coordinator Management

-

Manage administrator accounts and permissions across the platform.

+

Admin Management

+

Manage administrator accounts and permissions

- - - - - - - - Create New OJT Coordinator Account - - Fill in the details to create a new coordinator account. - - -
- {/* Username */} -
- -
- { - setFormData({ ...formData, username: e.target.value }) - setFieldErrors(f => ({ ...f, username: "" })) - }} - /> - {fieldErrors.username && ( -
{fieldErrors.username}
- )} +
+ + + + + + + Create New Admin Account + Fill in the details to create a new administrator account. + +
+
+ +
-
- {/* Password */} -
- -
+
+ { - setFormData({ ...formData, password: e.target.value }) - setFieldErrors(f => ({ ...f, password: "" })) - }} + onChange={handleInputChange} /> - {fieldErrors.password && ( -
{fieldErrors.password}
- )}
-
- -
- -
-
- { - setFormData({ ...formData, firstName: e.target.value }) - setFieldErrors(f => ({ ...f, firstName: "" })) - }} - /> - {fieldErrors.firstName && ( -
{fieldErrors.firstName}
- )} -
+
+ +
+
+ + setFormData({ ...formData, middleName: e.target.value })} + onChange={handleInputChange} /> -
- { - setFormData({ ...formData, lastName: e.target.value }) - setFieldErrors(f => ({ ...f, lastName: "" })) - }} - /> - {fieldErrors.lastName && ( -
{fieldErrors.lastName}
- )} -
-
-
- {/* Department */} -
- -
- +
+
+ + - {fieldErrors.department && ( -
{fieldErrors.department}
- )}
-
- - + + + + +
+
+ + + + Admin Accounts + View and manage all administrator accounts in the system + + +
+
+
+ + setSearchQuery(e.target.value)} + /> +
+ -
+
+ - - -
- - - {/* Main Card */} - - - - OJT Coordinator Accounts - - View and manage all administrator accounts in the system - - - - {isLoading ? ( -
-
-
Fetching users...
-
- ) : ( - <> - {/* Search and Filter Bar */} -
-
-
- - setSearchQuery(e.target.value)} - /> -
- -
- -
+
+
- {/* Tabs */} - - - - All ({adminList.length}) - - - Active ({adminList.filter((a) => a.status === "active").length}) - - - Inactive ({adminList.filter((a) => a.status === "inactive").length}) - - - Archived ({adminList.filter((a) => a.status === "archived").length}) - - - - {/* Table */} -
- - - - Username - Name - Department - Status - Created At - Actions - - - - {paginatedAdmins.length === 0 ? ( - - -
- -

No admin accounts found

-

Try adjusting your search or filter criteria

-
-
-
- ) : ( - paginatedAdmins.map((admin, index) => ( - - {admin.username} - - {admin.name.first} {admin.name.last} - - {admin.department} - - - - {admin.createdAt} - - - - - - - handleViewAdmin(admin)} className="rounded-xl py-3"> - - View Details - - handleEditAdmin(admin)} className="rounded-xl py-3"> - - Edit Admin - - handleArchiveAdmin(admin)} className="rounded-xl py-3"> - - {admin.status === "archived" ? "Unarchive" : "Archive"} - - handleDeleteAdmin(admin)} - className="rounded-xl py-3 text-red-600 focus:text-red-600 focus:bg-red-50" - > - - Delete - - - - - - )) - )} -
-
- {/* Pagination Controls */} - {pageCount > 1 && ( -
- - - Page {page} of {pageCount} - - -
- )} -
-
- - )} - - - - - {/* View Dialog */} - - + + + All + Active + Inactive + Archived + + + + + + + + + + + + + + + + + + {/* View Admin Dialog */} + + - OJT Coordinator Details - - Detailed information about the admin account. - + Admin Details + Detailed information about the admin account. {selectedAdmin && ( -
+
- -
{selectedAdmin.username}
+ +
{selectedAdmin.username}
- -
+ +
{selectedAdmin.name.first} {selectedAdmin.name.middle} {selectedAdmin.name.last}
- -
{selectedAdmin.department}
+ +
{selectedAdmin.department}
- +
- -
{selectedAdmin.createdAt}
+ +
{selectedAdmin.createdAt}
)} - - -
+ + - {/* Edit Dialog */} - - + {/* Edit Admin Dialog */} + + - Edit Admin Account - - Update the details of this administrator account. - + Edit Admin Account + Update the details of this administrator account. -
+
-
-
-
-
-
-
- - - -
+ + {/* Delete Confirmation Dialog */} - - - - Are you absolutely sure? - - This action cannot be undone. This will permanently delete the admin account and remove all associated - data from our servers. - - - - Cancel - - Delete Account - - - - + setIsDeleteDialogOpen(false)}> + Are you sure you want to delete this admin? + + + This action cannot be undone. This will permanently delete the admin account and remove all associated data. + + + + + + +
) } -function StatusBadge({ status }: { status: string }) { - const variants = { - active: "bg-emerald-100 text-emerald-700 border-emerald-200 hover:bg-emerald-100", - inactive: "bg-red-100 text-red-700 border-red-200 hover:bg-red-100", - archived: "bg-orange-100 text-orange-700 border-orange-200 hover:bg-orange-100", +function AdminsTable({ + admins, + onViewAdmin, + onEditAdmin, + onDeleteAdmin, + onArchiveAdmin, +}: { + admins: Admin[] + onViewAdmin: (admin: Admin) => void + onEditAdmin: (admin: Admin) => void + onDeleteAdmin: (admin: Admin) => void + onArchiveAdmin: (admin: Admin) => void +}) { + const [anchorEls, setAnchorEls] = React.useState<{ [key: number]: HTMLElement | null }>({}) + + const handleMenuOpen = (event: React.MouseEvent, id: number) => { + setAnchorEls((prev) => ({ ...prev, [id]: event.currentTarget })) + } + + const handleMenuClose = (id: number) => { + setAnchorEls((prev) => ({ ...prev, [id]: null })) } return ( - - {status.charAt(0).toUpperCase() + status.slice(1)} - + + + + + Username + Name + Department + Status + Created At + Actions + + + + {admins.length === 0 ? ( + + + No admin accounts found + + + ) : ( + admins.map((admin) => ( + + {admin.username} + + {admin.name.first} {admin.name.last} + + {admin.department} + + + + {admin.createdAt} + + handleMenuOpen(e, admin.id)} + > + + + handleMenuClose(admin.id)} + anchorOrigin={{ + vertical: "bottom", + horizontal: "right", + }} + transformOrigin={{ + vertical: "top", + horizontal: "right", + }} + > + { + handleMenuClose(admin.id) + onViewAdmin(admin) + }} + > + + View + + { + handleMenuClose(admin.id) + onEditAdmin(admin) + }} + > + + Edit + + {admin.status !== "archived" ? ( + { + handleMenuClose(admin.id) + onArchiveAdmin(admin) + }} + > + + Archive + + ) : ( + { + handleMenuClose(admin.id) + onArchiveAdmin(admin) + }} + > + + Unarchive + + )} + { + handleMenuClose(admin.id) + onDeleteAdmin(admin) + }} + style={{ color: "#dc2626" }} + > + + Delete + + + + + )) + )} + +
+
) } + +function StatusBadge({ status }: { status: string }) { + if (status === "active") { + return Active + } else if (status === "inactive") { + return ( + + Inactive + + ) + } else if (status === "archived") { + return Archived + } + return null +} diff --git a/app/(app)/admin/superadmin/account-management/companies/page.tsx b/app/(app)/admin/superadmin/account-management/companies/page.tsx index ed6e908..9c58af1 100644 --- a/app/(app)/admin/superadmin/account-management/companies/page.tsx +++ b/app/(app)/admin/superadmin/account-management/companies/page.tsx @@ -1,6 +1,6 @@ "use client" -import { useState, useEffect } from "react" +import { useState } from "react" import { Search, Download, @@ -22,20 +22,26 @@ import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { Badge } from "@/components/ui/badge" -import FormControl from "@mui/material/FormControl" -import InputLabel from "@mui/material/InputLabel" -import Select from "@mui/material/Select" + +import Table from "@mui/material/Table" +import TableBody from "@mui/material/TableBody" +import TableCell from "@mui/material/TableCell" +import TableContainer from "@mui/material/TableContainer" +import TableHead from "@mui/material/TableHead" +import TableRow from "@mui/material/TableRow" +import Paper from "@mui/material/Paper" +import Avatar from "@mui/material/Avatar" +import Menu from "@mui/material/Menu" import MenuItem from "@mui/material/MenuItem" +import IconButton from "@mui/material/IconButton" import Dialog from "@mui/material/Dialog" import DialogTitle from "@mui/material/DialogTitle" import DialogContent from "@mui/material/DialogContent" import DialogActions from "@mui/material/DialogActions" import Typography from "@mui/material/Typography" -import { motion } from "framer-motion" -import { cn } from "@/lib/utils" -import { PiWarningFill } from "react-icons/pi" -import { HiBadgeCheck } from "react-icons/hi" -import { Tooltip } from "@mui/material" +import FormControl from "@mui/material/FormControl" +import InputLabel from "@mui/material/InputLabel" +import Select from "@mui/material/Select" interface Company { id: number @@ -51,93 +57,22 @@ interface Company { website: string employeesCount: number description: string + contactPerson: string contactEmail: string contactPhone: string address: string verified: boolean - suite_unit_floor?: string - business_park_landmark?: string - building_name?: string - country_code?: string - contact_number?: string - logoPath?: string } export default function CompaniesManagement() { const [searchQuery, setSearchQuery] = useState("") - const [activeTab, setActiveTab] = useState("all") + const [activeTab, setActiveTab] = useState("active") const [isViewDialogOpen, setIsViewDialogOpen] = useState(false) const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false) const [selectedCompany, setSelectedCompany] = useState(null) const [selectedIndustry, setSelectedIndustry] = useState("all") const [selectedSize, setSelectedSize] = useState("all") const [menuAnchors, setMenuAnchors] = useState<{ [key: number]: HTMLElement | null }>({}) - const [companies, setCompanies] = useState([]) - const [isLoading, setIsLoading] = useState(false) - const [employerCount, setEmployerCount] = useState(null) - - useEffect(() => { - setIsLoading(true) - fetch("/api/superadmin/fetchUsers?allCompanies=true") - .then(res => res.json()) - .then(res => { - if (Array.isArray(res.companies)) { - setCompanies( - res.companies.map((c: Record, idx: number) => ({ - id: typeof c.id === "number" ? c.id : idx + 1, - companyId: typeof c.company_id === "string" - ? c.company_id - : typeof c.id === "number" - ? c.id.toString() - : "", - name: typeof c.company_name === "string" ? c.company_name : "", - email: typeof c.contact_email === "string" ? c.contact_email : "", - phone: - (typeof c.country_code === "string" && c.country_code - ? "+" + c.country_code + " " - : "") + - (typeof c.contact_number === "string" ? c.contact_number : ""), - country_code: typeof c.country_code === "string" ? c.country_code : "", - contact_number: typeof c.contact_number === "string" ? c.contact_number : "", - industry: typeof c.company_industry === "string" ? c.company_industry : "", - size: typeof c.company_size === "string" ? c.company_size : "small", - status: typeof c.status === "string" ? c.status : "active", - registrationDate: - typeof c.created_at === "string" - ? new Date(c.created_at).toISOString().slice(0, 10) - : "", - location: typeof c.company_branch === "string" ? c.company_branch : "", - website: typeof c.company_website === "string" ? c.company_website : "", - employeesCount: typeof c.employees_count === "number" ? c.employees_count : 0, - description: typeof c.description === "string" ? c.description : "", - contactEmail: typeof c.contact_email === "string" ? c.contact_email : "", - contactPhone: - (typeof c.country_code === "string" && c.country_code - ? "+" + c.country_code + " " - : "") + - (typeof c.contact_number === "string" ? c.contact_number : ""), - address: typeof c.exact_address === "string" ? c.exact_address : "", - suite_unit_floor: typeof c.suite_unit_floor === "string" ? c.suite_unit_floor : "", - business_park_landmark: typeof c.business_park_landmark === "string" ? c.business_park_landmark : "", - building_name: typeof c.building_name === "string" ? c.building_name : "", - verified: c.verify_status === "full", - logoPath: typeof c.company_logo_image_path === "string" ? c.company_logo_image_path : undefined, - })) - ) - } - }) - .finally(() => setIsLoading(false)) - }, []) - - useEffect(() => { - if (selectedCompany && selectedCompany.name) { - fetch(`/api/superadmin/fetchUsers?countEmployersByCompany=true`) - .then(res => res.json()) - .then(res => { - if (typeof res.count === "number") setEmployerCount(res.count) - }) - } - }, [selectedCompany]) const handleMenuOpen = (event: React.MouseEvent, id: number) => { setMenuAnchors((prev) => ({ ...prev, [id]: event.currentTarget })) @@ -147,6 +82,157 @@ export default function CompaniesManagement() { setMenuAnchors((prev) => ({ ...prev, [id]: null })) } + // Mock data + const companies: Company[] = [ + { + id: 1, + companyId: "COM-001", + name: "Tech Solutions Inc.", + email: "info@techsolutions.com", + phone: "+63 2 8123 4567", + industry: "Information Technology", + size: "medium", + status: "active", + registrationDate: "2020-03-15", + location: "Manila", + website: "www.techsolutions.com", + employeesCount: 120, + description: + "Tech Solutions Inc. is a leading IT services company specializing in software development and cloud solutions.", + contactPerson: "John Smith", + contactEmail: "john.smith@techsolutions.com", + contactPhone: "+63 912 345 6789", + address: "123 Tech Avenue, Makati City, Metro Manila", + verified: true, + }, + { + id: 2, + companyId: "COM-002", + name: "Digital Innovations", + email: "contact@digitalinnovations.com", + phone: "+63 2 8234 5678", + industry: "Information Technology", + size: "small", + status: "active", + registrationDate: "2021-05-20", + location: "Cebu", + website: "www.digitalinnovations.com", + employeesCount: 45, + description: + "Digital Innovations is a digital transformation company helping businesses adopt modern technologies.", + contactPerson: "Sarah Williams", + contactEmail: "sarah.williams@digitalinnovations.com", + contactPhone: "+63 923 456 7890", + address: "456 Digital Street, Cebu City", + verified: true, + }, + { + id: 3, + companyId: "COM-003", + name: "WebTech Solutions", + email: "hello@webtech.com", + phone: "+63 2 8345 6789", + industry: "Web Development", + size: "small", + status: "inactive", + registrationDate: "2019-01-10", + location: "Manila", + website: "www.webtech.com", + employeesCount: 30, + description: + "WebTech Solutions specializes in web development and digital marketing services for small to medium businesses.", + contactPerson: "Michael Brown", + contactEmail: "michael.brown@webtech.com", + contactPhone: "+63 934 567 8901", + address: "789 Web Lane, Quezon City, Metro Manila", + verified: true, + }, + { + id: 4, + companyId: "COM-004", + name: "Innovative Systems", + email: "info@innovativesystems.com", + phone: "+63 2 8456 7890", + industry: "Software Development", + size: "medium", + status: "suspended", + registrationDate: "2018-11-05", + location: "Davao", + website: "www.innovativesystems.com", + employeesCount: 75, + description: + "Innovative Systems develops custom software solutions for various industries including healthcare and finance.", + contactPerson: "Jennifer Lee", + contactEmail: "jennifer.lee@innovativesystems.com", + contactPhone: "+63 945 678 9012", + address: "101 Innovation Road, Davao City", + verified: false, + }, + { + id: 5, + companyId: "COM-005", + name: "Future Technologies", + email: "contact@futuretechnologies.com", + phone: "+63 2 8567 8901", + industry: "Artificial Intelligence", + size: "small", + status: "pending", + registrationDate: "2022-01-20", + location: "Makati", + website: "www.futuretechnologies.com", + employeesCount: 25, + description: + "Future Technologies is a startup focused on artificial intelligence and machine learning applications.", + contactPerson: "David Wilson", + contactEmail: "david.wilson@futuretechnologies.com", + contactPhone: "+63 956 789 0123", + address: "202 Future Street, Makati City, Metro Manila", + verified: false, + }, + { + id: 6, + companyId: "COM-006", + name: "Global Finance Corp", + email: "info@globalfinance.com", + phone: "+63 2 8678 9012", + industry: "Finance", + size: "large", + status: "active", + registrationDate: "2015-08-15", + location: "Makati", + website: "www.globalfinance.com", + employeesCount: 350, + description: + "Global Finance Corp provides financial services and solutions to businesses and individuals across the Philippines.", + contactPerson: "Maria Rodriguez", + contactEmail: "maria.rodriguez@globalfinance.com", + contactPhone: "+63 967 890 1234", + address: "303 Finance Tower, Makati City, Metro Manila", + verified: true, + }, + { + id: 7, + companyId: "COM-007", + name: "Healthcare Innovations", + email: "contact@healthcareinnovations.com", + phone: "+63 2 8789 0123", + industry: "Healthcare", + size: "large", + status: "active", + registrationDate: "2017-04-10", + location: "Manila", + website: "www.healthcareinnovations.com", + employeesCount: 280, + description: + "Healthcare Innovations develops technology solutions for hospitals, clinics, and healthcare providers.", + contactPerson: "Robert Chen", + contactEmail: "robert.chen@healthcareinnovations.com", + contactPhone: "+63 978 901 2345", + address: "404 Health Plaza, Manila", + verified: true, + }, + ] + const industries = Array.from(new Set(companies.map((company) => company.industry))) const sizes = [ { value: "small", label: "Small (1-50)" }, @@ -186,10 +272,14 @@ export default function CompaniesManagement() { } const confirmDeleteCompany = () => { + // In a real application, you would call an API to delete the company + console.log(`Deleting company with ID: ${selectedCompany?.id}`) setIsDeleteDialogOpen(false) + // Then refresh the company list } const exportCompanies = () => { + // In a real application, you would generate a CSV or Excel file const header = "Company ID,Name,Email,Phone,Industry,Size,Status,Location,Employees,Verified\n" const csv = filteredCompanies .map( @@ -210,181 +300,137 @@ export default function CompaniesManagement() { } return ( -
- +
+
-

Company Management

-

View, export, and manage company records

+

Company Management

+

View, export, and manage company records

+
+
+
- - +
- - - - Company Records - - Comprehensive list of all companies in the system - - - - {isLoading ? ( -
-
-
Fetching companies...
+ + + Company Records + Comprehensive list of all companies in the system + + +
+
+
+ + setSearchQuery(e.target.value)} + />
- ) : ( - <> -
-
-
- - setSearchQuery(e.target.value)} - /> -
-
- - Industry - - - - Company Size - - -
-
-
+
+ + Industry + + + + Company Size + + +
+
+
- - - - All ({companies.length}) - - - Active ({companies.filter((c) => c.status === "active").length}) - - - Inactive ({companies.filter((c) => c.status === "inactive").length}) - - - Suspended ({companies.filter((c) => c.status === "suspended").length}) - - - Pending ({companies.filter((c) => c.status === "pending").length}) - - - - - - - - - - - - - - - - - - - - )} -
-
- + + + All + Active + Inactive + Suspended + Pending + + + + + + + + + + + + + + + + + + + + {/* View Company Dialog */} setIsViewDialogOpen(false)} maxWidth="md" fullWidth> Company Details @@ -394,11 +440,17 @@ export default function CompaniesManagement() { {selectedCompany && (
- + + +

{selectedCompany.name}

- + {selectedCompany.verified && ( + + Verified + + )}

{selectedCompany.companyId}

@@ -406,83 +458,79 @@ export default function CompaniesManagement() {
-
-

Company Information

-
-
-
- - - Email: {selectedCompany.email} - -
-
- - - Phone:{" "} - {selectedCompany.country_code - ? `+${selectedCompany.country_code} ` - : ""} - {selectedCompany.contact_number || selectedCompany.phone} - -
-
- - - Website:{" "} - - {selectedCompany.website} - - -
-
- - - Location: {selectedCompany.location} - -
+
+
+
+ + + Email: {selectedCompany.email} +
-
-
- - - Industry: {selectedCompany.industry} - -
-
- - - Size: {getSizeLabel(selectedCompany.size)} - -
- {employerCount !== null && ( -
- - - Number of registered employers: {employerCount} - -
- )} -
- - - Registration Date: {selectedCompany.registrationDate} - -
+
+ + + Phone: {selectedCompany.phone} + +
+
+ + + Website:{" "} + + {selectedCompany.website} + + +
+
+ + + Location: {selectedCompany.location} +
- +
+
+ + + Industry: {selectedCompany.industry} + +
+
+ + + Size: {getSizeLabel(selectedCompany.size)} ({selectedCompany.employeesCount}{" "} + employees) + +
+
+ + + Registration Date: {selectedCompany.registrationDate} + +
+
+
+ +
+

Company Description

+

{selectedCompany.description}

-
+

Contact Information

+
+ + + Contact Person: {selectedCompany.contactPerson} + +
@@ -497,25 +545,11 @@ export default function CompaniesManagement() { Contact Phone: {selectedCompany.contactPhone}
-
-
-
-
- - Address -
-
-
- Exact Address: - Montillano St, Muntinlupa City, 1780 Metro Manila -
-
- Landmark: - Lianas Abandoned Cuh -
-
- Building Name: - STI College +
+ + + Address: {selectedCompany.address} +
@@ -531,6 +565,7 @@ export default function CompaniesManagement() {
+ {/* Delete Confirmation Dialog */} setIsDeleteDialogOpen(false)}> Are you sure you want to delete this company? @@ -552,6 +587,7 @@ export default function CompaniesManagement() { ) } +// MUI Table and Menu for CompaniesTable function CompaniesTable({ companies, onViewCompany, @@ -568,144 +604,125 @@ function CompaniesTable({ handleMenuClose: (id: number) => void }) { return ( -
- - - - - - - - - - - - + +
NameIndustryLocationStatusVerifiedActions
+ + + Company ID + Name + Industry + Size + Location + Status + Verified + Actions + + + {companies.length === 0 ? ( - - - + + + No companies found + + ) : ( - companies.map((company, index) => ( - - - - - - - - + + View Details + + { + handleMenuClose(company.id) + // Add edit logic here if needed + }} + > + + Edit + + { + handleMenuClose(company.id) + onDeleteCompany(company) + }} + style={{ color: "#dc2626" }} + > + + Delete + + + + )) )} - -
-
- -

No companies found

-

Try adjusting your search or filter criteria

-
-
{company.name} - {company.industry.charAt(0).toUpperCase() + company.industry.slice(1)} - {company.location} + companies.map((company) => ( + + {company.companyId} + {company.name} + {company.industry} + {getSizeLabel(company.size)} + {company.location} + - - - - - {menuAnchors[company.id] && ( -
+ + handleMenuClose(company.id)} + anchorOrigin={{ + vertical: "bottom", + horizontal: "right", + }} + transformOrigin={{ + vertical: "top", + horizontal: "right", + }} + > + { + handleMenuClose(company.id) + onViewCompany(company) }} - onMouseLeave={() => handleMenuClose(company.id)} > - - - -
- )} -
-
+ + + ) } function StatusBadge({ status }: { status: string }) { if (status === "active") { - return ( - - Active - - ) + return Active } else if (status === "inactive") { return ( - + Inactive ) } else if (status === "suspended") { - return ( - - Suspended - - ) + return Suspended } else if (status === "pending") { return ( - + Pending ) @@ -713,78 +730,6 @@ function StatusBadge({ status }: { status: string }) { return null } -function CompanyVerifyBadge({ verified }: { verified: boolean }) { - if (verified) { - return ( - - - - - Full - - - - ) - } else { - return ( - - - - - Basic - - - - ) - } -} - -function CompanyVerifyBadgeModal({ verified }: { verified: boolean }) { - if (verified) { - return ( - - - - - Fully Verified - - - - ) - } else { - return ( - - - - - Not Verified - - - - ) - } -} - function getSizeLabel(size: string): string { switch (size) { case "small": @@ -799,47 +744,3 @@ function getSizeLabel(size: string): string { return size } } - -function CompanyLogo({ logoPath, alt }: { logoPath?: string; alt?: string }) { - const [logoUrl, setLogoUrl] = useState(null) - const [loading, setLoading] = useState(false) - - useEffect(() => { - if (!logoPath) { - setLogoUrl(null) - setLoading(false) - return - } - setLoading(true) - fetch(`/api/employers/get-signed-url?bucket=company.logo&path=${encodeURIComponent(logoPath)}`) - .then(res => res.json()) - .then(res => { - if (res.signedUrl) setLogoUrl(res.signedUrl) - else setLogoUrl(null) - }) - .catch(() => setLogoUrl(null)) - .finally(() => setLoading(false)) - }, [logoPath]) - - if (loading) { - return ( -
-
-
- ) - } - - if (logoUrl) { - return ( -
- {/* eslint-disable-next-line @next/next/no-img-element */} - {alt -
- ) - } - return ( -
- -
- ) -} diff --git a/app/(app)/admin/superadmin/account-management/employers/components/employer-details.tsx b/app/(app)/admin/superadmin/account-management/employers/components/employer-details.tsx deleted file mode 100644 index 035e97a..0000000 --- a/app/(app)/admin/superadmin/account-management/employers/components/employer-details.tsx +++ /dev/null @@ -1,213 +0,0 @@ -import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog" -import { Button } from "@/components/ui/button" -import { Label } from "@/components/ui/label" -import Avatar from "@mui/material/Avatar" -import { User, Mail, Phone, Building, Calendar, Briefcase, MapPin } from "lucide-react" -import { PiSubtitlesBold } from "react-icons/pi" -import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs" -import { useEffect, useState } from "react" -import Image from "next/image" - -interface Employer { - id: number - employerId: string - name: string - email: string - phone: string - company: string - position: string - status: "active" | "inactive" | "suspended" | "pending" - registrationDate: string - location: string - employeesCount: number - description: string - verify_status?: string - job_title?: string - company_branch?: string - username?: string - company_website?: string - company_admin?: boolean - profile_img?: string -} - -export function EmployerDetailsModal({ - open, - onOpenChange, - employer, - onEdit, -}: { - open: boolean - onOpenChange: (open: boolean) => void - employer: Employer | null - onEdit?: () => void -}) { - const [avatarUrl, setAvatarUrl] = useState(null) - const [avatarLoading, setAvatarLoading] = useState(false) - - useEffect(() => { - let isMounted = true - setAvatarUrl(null) - if (employer?.profile_img) { - setAvatarLoading(true) - fetch(`/api/employers/get-signed-url?bucket=user.avatars&path=${encodeURIComponent(employer.profile_img)}`) - .then(res => res.json()) - .then(data => { - if (isMounted) setAvatarUrl(data.signedUrl || null) - }) - .catch(() => { - if (isMounted) setAvatarUrl(null) - }) - .finally(() => { - if (isMounted) setAvatarLoading(false) - }) - } else { - setAvatarLoading(false) - setAvatarUrl(null) - } - return () => { isMounted = false } - }, [employer?.profile_img]) - - return ( - - - - Employer Details - - Comprehensive information about the employer. - - - {employer && ( - - - Employer Details - Company Details - - -
-
- - {avatarLoading ? ( - - ) : avatarUrl ? ( - Avatar setAvatarLoading(false)} - /> - ) : ( - - )} - -
-

- {(() => { - const nameParts = employer.name.split(" ") - const firstName = nameParts[0] ? nameParts[0].charAt(0).toUpperCase() + nameParts[0].slice(1).toLowerCase() : "" - const lastName = nameParts.length > 1 ? nameParts[nameParts.length - 1].charAt(0).toUpperCase() + nameParts[nameParts.length - 1].slice(1).toLowerCase() : "" - return `${firstName} ${lastName}`.trim() - })()} - {employer.company_admin && ( - - Company Admin - - )} -

-

{employer.username || "-"}

-
-
-
-
-
-
-
- - - Email: {employer.email} - -
-
- - - Phone: {employer.phone} - -
-
- - - Position: {employer.position.charAt(0).toUpperCase() + employer.position.slice(1)} - -
-
- - - Job Title: {employer.job_title || "-"} - -
-
-
-
- - - Registration Date: {employer.registrationDate} - -
-
-
-
-
- -
-
-
-
- - - Company: {employer.company} - -
-
- - - Branch: {employer.company_branch || "-"} - -
-
- - - Employees: {employer.employeesCount} - -
-
-
-
- -

{employer.company_website}

-
-
- -

{employer.description}

-
-
-
-
-
-
- )} - - - - -
-
- ) -} - diff --git a/app/(app)/admin/superadmin/account-management/employers/page.tsx b/app/(app)/admin/superadmin/account-management/employers/page.tsx index 861b3d5..bbc23cf 100644 --- a/app/(app)/admin/superadmin/account-management/employers/page.tsx +++ b/app/(app)/admin/superadmin/account-management/employers/page.tsx @@ -1,6 +1,6 @@ "use client" -import { useState, useEffect } from "react" +import { useState } from "react" import { Search, Download, @@ -8,29 +8,41 @@ import { Eye, Trash, Edit, + Mail, + Phone, + Building, + Calendar, User, + Briefcase, + MapPin, } from "lucide-react" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog" +import Button from "@mui/material/Button" +import { Input } from "@mui/material" +import Dialog from "@mui/material/Dialog" +import DialogActions from "@mui/material/DialogActions" +import DialogContent from "@mui/material/DialogContent" +import DialogContentText from "@mui/material/DialogContentText" +import DialogTitle from "@mui/material/DialogTitle" +import Avatar from "@mui/material/Avatar" +import Select from "@mui/material/Select" +import MenuItem from "@mui/material/MenuItem" +import InputLabel from "@mui/material/InputLabel" +import FormControl from "@mui/material/FormControl" +import Table from "@mui/material/Table" +import TableBody from "@mui/material/TableBody" +import TableCell from "@mui/material/TableCell" +import TableContainer from "@mui/material/TableContainer" +import TableHead from "@mui/material/TableHead" +import TableRow from "@mui/material/TableRow" +import Paper from "@mui/material/Paper" +import IconButton from "@mui/material/IconButton" +import Menu from "@mui/material/Menu" +import ListItemIcon from "@mui/material/ListItemIcon" +import ListItemText from "@mui/material/ListItemText" import { Badge } from "@/components/ui/badge" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" -import { motion } from "framer-motion" -import { cn } from "@/lib/utils" -import { HiBadgeCheck } from "react-icons/hi" -import { LuBadgeCheck } from "react-icons/lu" -import { Tooltip } from "@mui/material" -import { EmployerDetailsModal } from "./components/employer-details" -import { PiWarningFill } from "react-icons/pi" +import { Label } from "@/components/ui/label" interface Employer { id: number @@ -42,164 +54,130 @@ interface Employer { position: string status: "active" | "inactive" | "suspended" | "pending" registrationDate: string + industry: string location: string + website: string employeesCount: number description: string - verify_status?: string - job_title?: string - company_branch?: string - username?: string - company_website?: string - profile_img?: string -} - -interface ApiEmployer { - profile_img: string - id: string - first_name?: string - middle_name?: string - last_name?: string - suffix?: string - country_code?: string - phone?: string - email?: string - company_email?: string - company_name?: string - company_branch?: string - company_role?: string - company_industry?: string - verify_status?: string - created_at?: string - job_title?: string - username?: string - company_website?: string -} - -function StatusBadge({ status }: { status: string }) { - let label = "" - let badgeClass = "" - let icon = null - let badgeContent = null - - if (status === "full") { - label = "Full" - badgeClass = - "bg-blue-100 text-blue-700 border-blue-200 flex items-center gap-1" - icon = - badgeContent = ( - - {icon}{label} - - ) - } else if (status === "basic") { - label = "Basic" - badgeClass = - "bg-orange-100 text-orange-700 border-orange-200 flex items-center gap-1" - icon = - badgeContent = ( - - {icon}{label} - - ) - } else if (status === "standard") { - label = "Standard" - badgeClass = - "bg-violet-100 text-violet-700 border-violet-200 flex items-center gap-1" - icon = - badgeContent = ( - - {icon}{label} - - ) - } else if (status === "active") { - label = "Active" - badgeClass = - "bg-green-100 text-green-700 border-green-200 flex items-center gap-1" - badgeContent = {label} - } else if (status === "inactive") { - label = "Inactive" - badgeClass = - "bg-gray-100 text-gray-700 border-gray-200 flex items-center gap-1" - badgeContent = {label} - } else if (status === "suspended") { - label = "Suspended" - badgeClass = - "bg-red-100 text-red-700 border-red-200 flex items-center gap-1" - badgeContent = {label} - } else { - label = status.charAt(0).toUpperCase() + status.slice(1) - badgeClass = - "bg-gray-100 text-gray-700 border-gray-200 flex items-center gap-1" - badgeContent = {label} - } - - return ( - - {badgeContent} - - ) } export default function EmployersManagement() { const [searchQuery, setSearchQuery] = useState("") - const [activeTab, setActiveTab] = useState("all") + const [activeTab, setActiveTab] = useState("active") const [isViewDialogOpen, setIsViewDialogOpen] = useState(false) const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false) const [selectedEmployer, setSelectedEmployer] = useState(null) const [selectedIndustry, setSelectedIndustry] = useState("all") const [selectedLocation, setSelectedLocation] = useState("all") - const [menuAnchorEl, setMenuAnchorEl] = useState(null) - const [menuEmployerId, setMenuEmployerId] = useState(null) - const [page, setPage] = useState(1) - const pageSize = 7 - const [employers, setEmployers] = useState([]) - const [isLoading, setIsLoading] = useState(false) + // Mock data + const employers: Employer[] = [ + { + id: 1, + employerId: "EMP-001", + name: "John Smith", + email: "john.smith@techsolutions.com", + phone: "+63 912 345 6789", + company: "Tech Solutions Inc.", + position: "HR Manager", + status: "active", + registrationDate: "2022-03-15", + industry: "Information Technology", + location: "Manila", + website: "www.techsolutions.com", + employeesCount: 120, + description: + "Tech Solutions Inc. is a leading IT services company specializing in software development and cloud solutions.", + }, + { + id: 2, + employerId: "EMP-002", + name: "Sarah Williams", + email: "sarah.williams@digitalinnovations.com", + phone: "+63 923 456 7890", + company: "Digital Innovations", + position: "Talent Acquisition Specialist", + status: "active", + registrationDate: "2022-05-20", + industry: "Information Technology", + location: "Cebu", + website: "www.digitalinnovations.com", + employeesCount: 85, + description: + "Digital Innovations is a digital transformation company helping businesses adopt modern technologies.", + }, + { + id: 3, + employerId: "EMP-003", + name: "Michael Brown", + email: "michael.brown@webtech.com", + phone: "+63 934 567 8901", + company: "WebTech Solutions", + position: "Recruitment Manager", + status: "inactive", + registrationDate: "2022-01-10", + industry: "Web Development", + location: "Manila", + website: "www.webtech.com", + employeesCount: 45, + description: + "WebTech Solutions specializes in web development and digital marketing services for small to medium businesses.", + }, + { + id: 4, + employerId: "EMP-004", + name: "Jennifer Lee", + email: "jennifer.lee@innovativesystems.com", + phone: "+63 945 678 9012", + company: "Innovative Systems", + position: "HR Director", + status: "suspended", + registrationDate: "2021-11-05", + industry: "Software Development", + location: "Davao", + website: "www.innovativesystems.com", + employeesCount: 60, + description: + "Innovative Systems develops custom software solutions for various industries including healthcare and finance.", + }, + { + id: 5, + employerId: "EMP-005", + name: "David Wilson", + email: "david.wilson@futuretechnologies.com", + phone: "+63 956 789 0123", + company: "Future Technologies", + position: "Recruitment Specialist", + status: "pending", + registrationDate: "2023-01-20", + industry: "Artificial Intelligence", + location: "Makati", + website: "www.futuretechnologies.com", + employeesCount: 30, + description: + "Future Technologies is a startup focused on artificial intelligence and machine learning applications.", + }, + { + id: 6, + employerId: "EMP-006", + name: "Maria Rodriguez", + email: "maria.rodriguez@globalfinance.com", + phone: "+63 967 890 1234", + company: "Global Finance Corp", + position: "Talent Manager", + status: "active", + registrationDate: "2022-08-15", + industry: "Finance", + location: "Makati", + website: "www.globalfinance.com", + employeesCount: 200, + description: + "Global Finance Corp provides financial services and solutions to businesses and individuals across the Philippines.", + }, + ] - useEffect(() => { - setIsLoading(true) - const statuses = ["active", "inactive", "suspended"] - fetch("/api/superadmin/fetchUsers?employers=true") - .then((res) => res.json()) - .then(async (res) => { - if (Array.isArray(res.employers)) { - setEmployers( - res.employers.map((e: ApiEmployer, idx: number) => ({ - id: idx + 1, - employerId: e.id, - name: [e.first_name, e.middle_name, e.last_name, e.suffix].filter(Boolean).join(" "), - email: e.email || e.company_email || "", - phone: (e.country_code ? "+" + e.country_code + " " : "") + (e.phone || ""), - company: e.company_name || "", - position: e.company_role || "", - status: statuses[idx % statuses.length] as "active" | "inactive" | "suspended", - registrationDate: e.created_at ? new Date(e.created_at).toISOString().slice(0, 10) : "", - industry: e.company_industry || "", - location: e.company_branch || "", - company_website: e.company_website || "", - employeesCount: 0, - description: "", - verify_status: e.verify_status ?? "", - job_title: e.job_title || "", - company_branch: e.company_branch || "", - username: e.username || "", - profile_img: e.profile_img || "", - })), - ) - } - }) - .finally(() => setIsLoading(false)) - }, []) - - const locations = Array.from( - new Set(employers.map((employer) => employer.location).filter((v) => v && v !== "")) - ) + const industries = Array.from(new Set(employers.map((employer) => employer.industry))) + const locations = Array.from(new Set(employers.map((employer) => employer.location))) const filteredEmployers = employers.filter((employer) => { const matchesSearch = @@ -215,28 +193,14 @@ export default function EmployersManagement() { (activeTab === "pending" && employer.status === "pending") || activeTab === "all" + const matchesIndustry = selectedIndustry === "all" || employer.industry === selectedIndustry const matchesLocation = selectedLocation === "all" || employer.location === selectedLocation - return matchesSearch && matchesTab && matchesLocation + return matchesSearch && matchesTab && matchesIndustry && matchesLocation }) - const pageCount = Math.ceil(filteredEmployers.length / pageSize) - const paginatedEmployers = filteredEmployers.slice((page - 1) * pageSize, page * pageSize) - - const fetchCompanyDetails = async (employerId: string) => { - const res = await fetch(`/api/superadmin/fetchUsers?employers=true&employerId=${employerId}`) - const data = await res.json() - return data.companies?.[0] - } - - const handleViewEmployer = async (employer: Employer) => { - const company = await fetchCompanyDetails(employer.employerId) - setSelectedEmployer({ - ...employer, - ...(company || {}), - company_website: company?.company_website || employer.company_website || "", - profile_img: employer.profile_img, - }) + const handleViewEmployer = (employer: Employer) => { + setSelectedEmployer(employer) setIsViewDialogOpen(true) } @@ -246,15 +210,19 @@ export default function EmployersManagement() { } const confirmDeleteEmployer = () => { + // In a real application, you would call an API to delete the employer + console.log(`Deleting employer with ID: ${selectedEmployer?.id}`) setIsDeleteDialogOpen(false) + // Then refresh the employer list } const exportEmployers = () => { + // In a real application, you would generate a CSV or Excel file const header = "Employer ID,Name,Email,Phone,Company,Position,Status,Industry,Location\n" const csv = filteredEmployers .map( (employer) => - `${employer.employerId},${employer.name},${employer.email},${employer.phone},${employer.company},${employer.position},${employer.status},${employer.location}`, + `${employer.employerId},${employer.name},${employer.email},${employer.phone},${employer.company},${employer.position},${employer.status},${employer.industry},${employer.location}`, ) .join("\n") @@ -270,227 +238,231 @@ export default function EmployersManagement() { } return ( -
- +
+
-

Employer Management

-

View, export, and manage employer records

+

Employer Management

+

View, export, and manage employer records

+
+
+
- - +
- - - - Employer Records - - Comprehensive list of all employers in the system - - - - {isLoading ? ( -
-
-
Fetching users...
+ + + Employer Records + Comprehensive list of all employers in the system + + +
+
+
+ + setSearchQuery(e.target.value)} + />
- ) : ( - <> -
-
-
- - setSearchQuery(e.target.value)} - /> -
-
- - -
+
+ + Industry + + + + Location + + +
+
+
+ + + + All + Active + Inactive + Suspended + Pending + + + + + + + + + + + + + + + + + + + + + {/* View Employer Dialog */} + setIsViewDialogOpen(false)}> + Employer Details + + Comprehensive information about the employer. + {selectedEmployer && ( +
+
+ + + +
+

{selectedEmployer.name}

+

{selectedEmployer.employerId}

+
+
+
- - - - All ({employers.length}) - - - Active ({employers.filter((e) => e.status === "active").length}) - - - Inactive ({employers.filter((e) => e.status === "inactive").length}) - - - Suspended ({employers.filter((e) => e.status === "suspended").length}) - - - Pending ({employers.filter((e) => e.status === "pending").length}) - - - - - - - - - - - - - - - - - - - {pageCount > 1 && ( -
- - - Page {page} of {pageCount} +
+
+
+ + + Email: {selectedEmployer.email} + +
+
+ + + Phone: {selectedEmployer.phone} -
- )} - - )} - - - +
+ + + Company: {selectedEmployer.company} + +
+
+ + + Position: {selectedEmployer.position} + +
+
+
+
+ + + Registration Date: {selectedEmployer.registrationDate} + +
+
+ + + Industry: {selectedEmployer.industry} + +
+
+ + + Location: {selectedEmployer.location} + +
+
+ + + Employees: {selectedEmployer.employeesCount} + +
+
+
- {}} - /> +
+ +

{selectedEmployer.website}

+
+ +
+ +

{selectedEmployer.description}

+
+
+ )} + + + + + +
- - - - Are you sure you want to delete this employer? - - This action cannot be undone. This will permanently delete the employer record and remove all associated data from the system. - - - - - - + {/* Delete Confirmation Dialog */} + setIsDeleteDialogOpen(false)}> + Are you sure you want to delete this employer? + + + This action cannot be undone. This will permanently delete the employer record and remove all associated + data from the system. + + + + +
) @@ -500,134 +472,138 @@ function EmployersTable({ employers, onViewEmployer, onDeleteEmployer, - menuAnchorEl, - setMenuAnchorEl, - menuEmployerId, - setMenuEmployerId, }: { - employers: (Employer & { verify_status?: string; profile_img?: string })[] - onViewEmployer: (employer: Employer & { profile_img?: string }) => void + employers: Employer[] + onViewEmployer: (employer: Employer) => void onDeleteEmployer: (employer: Employer) => void - menuAnchorEl: null | HTMLElement - setMenuAnchorEl: (el: null | HTMLElement) => void - menuEmployerId: number | null - setMenuEmployerId: (id: number | null) => void }) { - const handleFetchCompany = async (employerId: string) => { - const res = await fetch(`/api/superadmin/fetchUsers?employers=true&employerId=${employerId}`) - const data = await res.json() - return data.companies?.[0] + const [menuAnchorEl, setMenuAnchorEl] = useState(null) + const [menuEmployerId, setMenuEmployerId] = useState(null) + + const handleMenuOpen = (event: React.MouseEvent, employerId: number) => { + setMenuAnchorEl(event.currentTarget) + setMenuEmployerId(employerId) + } + + const handleMenuClose = () => { + setMenuAnchorEl(null) + setMenuEmployerId(null) } return ( -
- - - - {/* Avatar column removed */} - - - - - - - - - - + +
NameEmailCompanyPositionVerificationStatusActions
+ + + Employer ID + Name + Email + Phone + Company + Position + Status + Location + Actions + + + {employers.length === 0 ? ( - - - + + + No employers found + + ) : ( - employers.map((employer, index) => ( - - {/* Avatar cell removed */} - - - - - - - - + + + + Delete + + + + )) )} - -
-
- -

No employers found

-

Try adjusting your search or filter criteria

-
-
{employer.name}{employer.email}{employer.company.charAt(0).toUpperCase() + employer.company.slice(1)}{employer.position.charAt(0).toUpperCase() + employer.position.slice(1)} - - + employers.map((employer) => ( + + {employer.employerId} + {employer.name} + {employer.email} + {employer.phone} + {employer.company} + {employer.position} + - - - {menuEmployerId === employer.id && menuAnchorEl && ( -
{ + onViewEmployer(employer) + handleMenuClose() + }} + > + + + + View Details + + { + // Add edit logic here if needed + handleMenuClose() }} - onMouseLeave={() => { - setMenuAnchorEl(null) - setMenuEmployerId(null) + > + + + + Edit + + { + onDeleteEmployer(employer) + handleMenuClose() }} > - - - -
- )} -
-
+ + + ) } + +function StatusBadge({ status }: { status: string }) { + if (status === "active") { + return Active + } else if (status === "inactive") { + return ( + + Inactive + + ) + } else if (status === "suspended") { + return Suspended + } else if (status === "pending") { + return ( + + Pending + + ) + } + return null +} diff --git a/app/(app)/admin/superadmin/account-management/students/page.tsx b/app/(app)/admin/superadmin/account-management/students/page.tsx index 0f1fb32..34719ab 100644 --- a/app/(app)/admin/superadmin/account-management/students/page.tsx +++ b/app/(app)/admin/superadmin/account-management/students/page.tsx @@ -1,6 +1,6 @@ "use client" -import { useState, useEffect } from "react" +import { useState } from "react" import { Search, Download, @@ -29,7 +29,16 @@ import { Label } from "@/components/ui/label" import { Badge } from "@/components/ui/badge" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" + +// MUI imports import MuiAvatar from "@mui/material/Avatar" +import MuiTable from "@mui/material/Table" +import MuiTableBody from "@mui/material/TableBody" +import MuiTableCell from "@mui/material/TableCell" +import MuiTableContainer from "@mui/material/TableContainer" +import MuiTableHead from "@mui/material/TableHead" +import MuiTableRow from "@mui/material/TableRow" +import MuiPaper from "@mui/material/Paper" import MuiDialog from "@mui/material/Dialog" import MuiDialogTitle from "@mui/material/DialogTitle" import MuiDialogContent from "@mui/material/DialogContent" @@ -39,19 +48,15 @@ import MuiButton from "@mui/material/Button" import MuiMenu from "@mui/material/Menu" import MuiMenuItem from "@mui/material/MenuItem" import MuiIconButton from "@mui/material/IconButton" -import { motion } from "framer-motion" -import { cn } from "@/lib/utils" -import Image from "next/image" interface Student { - id: string + id: number studentId: string name: string email: string phone: string course: string - year: string - section: string + year: number status: "active" | "inactive" | "graduated" | "on_leave" enrollmentDate: string gender: string @@ -68,72 +73,103 @@ export default function StudentsManagement() { const [selectedStudent, setSelectedStudent] = useState(null) const [selectedDepartment, setSelectedDepartment] = useState("all") const [selectedYear, setSelectedYear] = useState("all") - const [page, setPage] = useState(1) - const pageSize = 7 - const [students, setStudents] = useState([]) - const [profileImgUrl, setProfileImgUrl] = useState(null) - const [avatarLoading, setAvatarLoading] = useState(false) - const [personalEmail, setPersonalEmail] = useState("") - const [personalPhone, setPersonalPhone] = useState("") - const [username, setUsername] = useState("") - const [isLoading, setIsLoading] = useState(false) - useEffect(() => { - async function fetchStudents() { - setIsLoading(true) - const res = await fetch("/api/superadmin/fetchUsers?students=1", { method: "GET" }) - setIsLoading(false) - if (!res.ok) return - const { students: apiStudents } = await res.json() - console.log("Frontend received students:", apiStudents) - if (Array.isArray(apiStudents)) { - setStudents( - apiStudents.map((s: Record) => { - let studentId = "No Student ID" - const email = String(s.email ?? "") - if (email.endsWith("@alabang.sti.edu.ph")) { - const match = email.match(/\.(\d+)@alabang\.sti\.edu\.ph$/) - if (match && match[1]) { - studentId = `02000-${match[1]}` - } - } - return { - id: typeof s.id === "string" ? s.id : String(s.id), - studentId, - name: `${String(s.first_name ?? "")} ${String(s.last_name ?? "")}`.trim(), - email, - phone: "", - course: String(s.course ?? ""), - year: s.year ? String(s.year) : "", - section: String(s.section ?? ""), - status: "active", - enrollmentDate: s.created_at ? String(s.created_at) : "", - gender: "", - address: String(s.address ?? ""), - dateOfBirth: "", - department: "", - } - }) as unknown as Student[] - ) - } - } - fetchStudents() - }, []) + // Mock data + const students: Student[] = [ + { + id: 1, + studentId: "2023-IT-0001", + name: "Alex Johnson", + email: "alex.johnson@example.com", + phone: "+63 912 345 6789", + course: "BSIT", + year: 4, + status: "active", + enrollmentDate: "2020-06-15", + gender: "Male", + address: "123 Main St, Manila", + dateOfBirth: "2002-03-15", + department: "IT", + }, + { + id: 2, + studentId: "2023-IT-0002", + name: "Maria Garcia", + email: "maria.garcia@example.com", + phone: "+63 923 456 7890", + course: "BSIT", + year: 4, + status: "active", + enrollmentDate: "2020-06-15", + gender: "Female", + address: "456 Oak Ave, Quezon City", + dateOfBirth: "2002-05-22", + department: "IT", + }, + { + id: 3, + studentId: "2023-BUS-0001", + name: "James Wilson", + email: "james.wilson@example.com", + phone: "+63 934 567 8901", + course: "BSBA", + year: 3, + status: "active", + enrollmentDate: "2021-06-10", + gender: "Male", + address: "789 Pine St, Makati", + dateOfBirth: "2003-01-10", + department: "Business", + }, + { + id: 4, + studentId: "2022-IT-0015", + name: "Emily Davis", + email: "emily.davis@example.com", + phone: "+63 945 678 9012", + course: "BSIT", + year: 2, + status: "inactive", + enrollmentDate: "2022-06-20", + gender: "Female", + address: "101 Cedar Rd, Pasig", + dateOfBirth: "2004-07-30", + department: "IT", + }, + { + id: 5, + studentId: "2021-ENG-0022", + name: "Robert Martinez", + email: "robert.martinez@example.com", + phone: "+63 956 789 0123", + course: "BSCE", + year: 3, + status: "on_leave", + enrollmentDate: "2021-06-15", + gender: "Male", + address: "202 Maple Dr, Taguig", + dateOfBirth: "2003-11-05", + department: "Engineering", + }, + { + id: 6, + studentId: "2020-IT-0008", + name: "Sophia Lee", + email: "sophia.lee@example.com", + phone: "+63 967 890 1234", + course: "BSIT", + year: 4, + status: "graduated", + enrollmentDate: "2020-06-10", + gender: "Female", + address: "303 Birch Ln, Mandaluyong", + dateOfBirth: "2002-09-18", + department: "IT", + }, + ] - const departments = Array.from( - new Set( - students - .map((student) => student.department) - .filter((dept) => dept && dept !== "") - ) - ) - const years = Array.from( - new Set( - students - .map((student) => student.year) - .filter((year) => year && year !== "") - ) - ) + const departments = Array.from(new Set(students.map((student) => student.department))) + const years = Array.from(new Set(students.map((student) => student.year))) const filteredStudents = students.filter((student) => { const matchesSearch = @@ -150,55 +186,14 @@ export default function StudentsManagement() { activeTab === "all" const matchesDepartment = selectedDepartment === "all" || student.department === selectedDepartment - const matchesYear = selectedYear === "all" || student.year === selectedYear + const matchesYear = selectedYear === "all" || student.year.toString() === selectedYear + return matchesSearch && matchesTab && matchesDepartment && matchesYear }) - const pageCount = Math.ceil(filteredStudents.length / pageSize) - const paginatedStudents = filteredStudents.slice((page - 1) * pageSize, page * pageSize) - - const handleViewStudent = async (student: Student) => { + const handleViewStudent = (student: Student) => { setSelectedStudent(student) setIsViewDialogOpen(true) - setProfileImgUrl(null) - setAvatarLoading(false) - setPersonalEmail("") - setPersonalPhone("") - setUsername("") - if (student.id && student.id !== "") { - try { - const res = await fetch(`/api/superadmin/fetchUsers?studentId=${encodeURIComponent(student.id)}`) - if (res.ok) { - const { profile_img, contact_info, username: uname } = await res.json() - if (profile_img) { - setAvatarLoading(true) - const signedUrlRes = await fetch("/api/students/get-signed-url", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ bucket: "user.avatars", path: profile_img }), - }) - if (signedUrlRes.ok) { - const { signedUrl } = await signedUrlRes.json() - setProfileImgUrl(signedUrl) - } - setAvatarLoading(false) - } - if (contact_info) { - let info = contact_info - if (typeof info === "string") { - try { info = JSON.parse(info) } catch {} - } - if (info && typeof info === "object") { - if (info.email) setPersonalEmail(info.email) - if (info.countryCode && info.phone) setPersonalPhone(`+${info.countryCode} ${info.phone}`) - } - } - if (uname) setUsername(uname) - } - } catch { - setAvatarLoading(false) - } - } } const handleDeleteStudent = (student: Student) => { @@ -207,15 +202,19 @@ export default function StudentsManagement() { } const confirmDeleteStudent = () => { + // In a real application, you would call an API to delete the student + console.log(`Deleting student with ID: ${selectedStudent?.id}`) setIsDeleteDialogOpen(false) + // Then refresh the student list } const exportStudents = () => { - const header = "Student ID,Name,Email,Phone,Course,Year,Status\n" + // In a real application, you would generate a CSV or Excel file + const header = "Student ID,Name,Email,Phone,Course,Year,Status,Department\n" const csv = filteredStudents .map( (student) => - `${student.studentId},${student.name},${student.email},${student.phone},${student.course},${student.year},${student.status}`, + `${student.studentId},${student.name},${student.email},${student.phone},${student.course},${student.year},${student.status},${student.department}`, ) .join("\n") @@ -230,262 +229,132 @@ export default function StudentsManagement() { URL.revokeObjectURL(url) } - function formatJoinDate(dateString: string) { - if (!dateString) return "" - const date = new Date(dateString) - return date.toLocaleString("en-US", { - year: "numeric", - month: "long", - day: "numeric", - hour: "numeric", - minute: "2-digit", - hour12: true, - }) - } - return ( -
- +
+
-

Student Management

-

View, export, and manage student records

+

Student Management

+

View, export, and manage student records

- - +
+ +
+
- - - - Student Records - - Comprehensive list of all students in the system - - - - {isLoading ? ( -
-
-
Fetching users...
+ + + Student Records + Comprehensive list of all students in the system + + +
+
+
+ + setSearchQuery(e.target.value)} + />
- ) : ( - <> -
-
-
- - setSearchQuery(e.target.value)} - /> -
-
- - -
-
-
+
+ + +
+
+
- - - - All ({students.length}) - - - Active ({students.filter((s) => s.status === "active").length}) - - - Inactive ({students.filter((s) => s.status === "inactive").length}) - - - Graduated ({students.filter((s) => s.status === "graduated").length}) - - - On Leave ({students.filter((s) => s.status === "on_leave").length}) - - - - - - - - - - - - - - - - - - - {pageCount > 1 && ( -
- - - Page {page} of {pageCount} - - -
- )} - - )} -
-
- + + + All + Active + Inactive + Graduated + On Leave + + + + + + + + + + + + + + + + + + + {/* View Student Dialog */} - + - Student Details - - Comprehensive information about the student. - + Student Details + Comprehensive information about the student. - {!selectedStudent ? ( -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ) : ( + {selectedStudent && (
- {avatarLoading ? ( - - ) : profileImgUrl ? ( - Avatar setAvatarLoading(false)} - /> - ) : ( - - )} +

{selectedStudent.name}

{selectedStudent.studentId}

- {username && ( -
Username: {username}
- )}
@@ -497,25 +366,21 @@ export default function StudentsManagement() {
- School Email: {selectedStudent.email} + Email: {selectedStudent.email} + +
+
+ + + Phone: {selectedStudent.phone} + +
+
+ + + Course: {selectedStudent.course}
- {personalEmail && ( -
- - - Personal Email: {personalEmail} - -
- )} - {personalPhone && ( -
- - - Phone: {personalPhone} - -
- )}
@@ -525,21 +390,27 @@ export default function StudentsManagement() {
- + - Course: {selectedStudent.course} + Enrollment Date: {selectedStudent.enrollmentDate}
- + - Section: {selectedStudent.section} + Gender: {selectedStudent.gender}
- Join Date: {formatJoinDate(selectedStudent.enrollmentDate)} + Date of Birth: {selectedStudent.dateOfBirth} + +
+
+ + + Department: {selectedStudent.department}
@@ -552,12 +423,10 @@ export default function StudentsManagement() {
)} - - +
@@ -595,10 +464,11 @@ function StudentsTable({ onViewStudent: (student: Student) => void onDeleteStudent: (student: Student) => void }) { + // State for menu anchor and selected student for menu const [menuAnchorEl, setMenuAnchorEl] = useState(null) - const [menuStudentId, setMenuStudentId] = useState(null) + const [menuStudentId, setMenuStudentId] = useState(null) - const handleMenuOpen = (event: React.MouseEvent, studentId: string) => { + const handleMenuOpen = (event: React.MouseEvent, studentId: number) => { setMenuAnchorEl(event.currentTarget) setMenuStudentId(studentId) } @@ -609,53 +479,49 @@ function StudentsTable({ } return ( -
- - - - - - - - - - - - - + + + + + Student ID + Name + Email + Phone + Course + Year + Status + Department + Actions + + + {students.length === 0 ? ( - - - + + + No students found + + ) : ( - students.map((student, index) => ( - - - - - - - - - + + )) )} - -
Student IDNameSchool EmailCourseYearSectionActions
-
- -

No students found

-

Try adjusting your search or filter criteria

-
-
{student.studentId}{student.name}{student.email}{student.course}{student.year}{student.section} + students.map((student) => ( + + {student.studentId} + {student.name} + {student.email} + {student.phone} + {student.course} + {student.year} + + + + {student.department} + handleMenuOpen(e, student.id)} > - + { handleMenuClose() + // Add edit logic here if needed }} > @@ -699,36 +566,37 @@ function StudentsTable({ Delete -
-
+ + + ) } function StatusBadge({ status }: { status: string }) { - const variants = { - active: "bg-emerald-100 text-emerald-700 border-emerald-200 hover:bg-emerald-100", - inactive: "bg-red-100 text-red-700 border-red-200 hover:bg-red-100", - graduated: "bg-blue-100 text-blue-800 border-blue-200 hover:bg-blue-100", - on_leave: "bg-orange-100 text-orange-700 border-orange-200 hover:bg-orange-100", + if (status === "active") { + return Active + } else if (status === "inactive") { + return ( + + Inactive + + ) + } else if (status === "graduated") { + return ( + + Graduated + + ) + } else if (status === "on_leave") { + return ( + + On Leave + + ) } - - let label = status.charAt(0).toUpperCase() + status.slice(1) - if (status === "on_leave") label = "On Leave" - return ( - - {label} - - ) + return null } - diff --git a/app/(app)/admin/superadmin/dashboard/page.tsx b/app/(app)/admin/superadmin/dashboard/page.tsx index 1b6a6bd..11b8dc2 100644 --- a/app/(app)/admin/superadmin/dashboard/page.tsx +++ b/app/(app)/admin/superadmin/dashboard/page.tsx @@ -8,94 +8,36 @@ import { TrendingUp, ArrowUpRight, ArrowDownRight, - Clock, - CheckCircle, - AlertTriangle, - Zap, + } from "lucide-react" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" -import { Badge } from "@/components/ui/badge" import { useState, useEffect } from "react" -import { motion } from "framer-motion" -import { cn } from "@/lib/utils" -import supabase from "@/lib/supabase" +import Switch from "@mui/material/Switch" +import { styled } from "@mui/material/styles" +import { RiRobot2Fill } from "react-icons/ri" +import Tooltip from "@mui/material/Tooltip" +import { createClient } from "@supabase/supabase-js" -const statsCards = [ - { - title: "Total Users", - value: "2,853", - change: "+12%", - trend: "up", - icon: Users, - color: "from-blue-500 to-cyan-500", - bgColor: "from-blue-50 to-cyan-50", - }, - { - title: "Active Employers", - value: "432", - change: "+8%", - trend: "up", - icon: Building2, - color: "from-emerald-500 to-teal-500", - bgColor: "from-emerald-50 to-teal-50", - }, - { - title: "Companies", - value: "1,234", - change: "+18%", - trend: "up", - icon: FileText, - color: "from-purple-500 to-pink-500", - bgColor: "from-purple-50 to-pink-50", - }, - { - title: "Pending Reports", - value: "24", - change: "-4%", - trend: "down", - icon: BarChart3, - color: "from-orange-500 to-red-500", - bgColor: "from-orange-50 to-red-50", - }, -] +const supabase = createClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! +) -const recentActivities = [ - { - id: 1, - type: "user", - title: "New admin account created", - description: "John Smith created a new admin account", - time: "2 hours ago", - icon: Users, - color: "from-blue-500 to-cyan-500", +const PurpleSwitch = styled(Switch)({ + "& .MuiSwitch-switchBase.Mui-checked": { + color: "#a21caf", }, - { - id: 2, - type: "company", - title: "Company verification completed", - description: "TechCorp Inc. has been verified", - time: "4 hours ago", - icon: CheckCircle, - color: "from-emerald-500 to-teal-500", + "& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track": { + backgroundColor: "#a21caf", }, - { - id: 3, - type: "report", - title: "New bug report submitted", - description: "Critical issue reported in job application system", - time: "6 hours ago", - icon: AlertTriangle, - color: "from-orange-500 to-red-500", + "& .MuiSwitch-track": { + backgroundColor: "#e9d5ff", }, -] -const topEmployers = [ - { id: 1, name: "TechCorp Inc.", listings: 18, growth: "+15%" }, - { id: 2, name: "InnovateLab", listings: 16, growth: "+12%" }, - { id: 3, name: "DataSystems", listings: 14, growth: "+8%" }, - { id: 4, name: "CloudTech", listings: 12, growth: "+22%" }, - { id: 5, name: "StartupHub", listings: 10, growth: "+5%" }, -] + "& .MuiSwitch-thumb": { + backgroundColor: "#a21caf", + }, +}) export default function Dashboard() { const [showFeedback, setShowFeedback] = useState(false) @@ -114,6 +56,7 @@ export default function Dashboard() { setShowFeedback(data.show_feedback_button) setSettingId(data.id) } else { + const { data: inserted } = await supabase .from("site_settings") .insert([{ show_feedback_button: true }]) @@ -153,329 +96,217 @@ export default function Dashboard() { } return ( -
- {/* Header */} - +
+
-

Dashboard

-

Welcome back! Here's what's happening with your platform today.

+

Dashboard

+

Overview of system performance and key metrics

- - -
- -
-
-
-

Testing Mode

-

Enable feedback collection

-
-
+
- {/* Stats Cards */}
- {statsCards.map((stat, index) => ( - - -
- - - {stat.title} - -
- + + + Total Users + + + +
2,853
+
+ + 12% from last month +
+
+
+ + + Active Employers + + + +
432
+
+ + 8% from last month +
+
+
+ + + Companies + + + +
1,234
+
+ + 18% from last month +
+
+
+ + + Pending Reports + + + +
24
+
+ + 4% from last month +
+
+
+
+ + + + Overview + Analytics + Reports + + +
+ + + User Registration Trends + Monthly user registrations over the past year + + +
+

Chart: Monthly user registrations

+
+
+ + + User Distribution + Breakdown by user type -
{stat.value}
-
- {stat.trend === "up" ? ( - - ) : ( - - )} - {stat.change} from last month +
+

Chart: User distribution

- - ))} -
- - {/* Main Content Tabs */} - - - - Overview - - - Analytics - - - Reports - - - - -
- {/* Chart Placeholder */} - - - - User Registration Trends - - Monthly user registrations over the past year - - - -
-
- -

Chart: Monthly user registrations

-

Interactive chart will be displayed here

-
-
-
-
-
- - {/* Recent Activities */} - - - - Recent Activities - Latest system activities - - -
- {recentActivities.map((activity, index) => ( - -
- -
-
-

- {activity.title} -

-

{activity.description}

-
- -

{activity.time}

-
-
-
- ))} -
-
-
-
-
- {/* Top Employers */} - - - - Top Employers - Most active employers on the platform - - -
- {topEmployers.map((employer, index) => ( - -
-
- -
-
-

- {employer.name} -

-

{employer.listings} job listings

-
-
-
- - {employer.growth} - - -
-
- ))} -
-
-
-
- - {/* Company Verification Status */} - - - - Company Verification - Verification status overview - - -
-
- Pending - 7 -
-
- -
-
- -
-
- Verified This Month - 15 -
-
- -
-
- -
-
- Rejected - 2 -
-
- +
+ + + Recent Activities + Latest system activities + + +
+ {[1, 2, 3, 4, 5].map((i) => ( +
+
+ +
+
+

New admin account created

+

2 hours ago

+
-
-
-
- -
- - - - - + ))} +
+ + + - Advanced Analytics - Detailed platform analytics and insights + Top Employers + Most active employers on the platform -
-
- -

Advanced Analytics Dashboard

-

Comprehensive analytics will be displayed here

-
+
+ {[1, 2, 3, 4, 5].map((i) => ( +
+
+
+
+

Company {i}

+

{20 - i * 2} job listings

+
+
+ +
+ ))}
- - - - - - + - System Reports - Generated reports and statistics + Pending Company Verification + Companies awaiting verification -
-
- -

System Reports

-

Detailed reports and statistics will be displayed here

+
+
+

Total Pending

+

7

+
+
+
+
+
+

Verified This Month

+

15

+
+
+
+
+
+

Rejected

+

2

+
+
+
- +
+ + + + + Advanced Analytics + Detailed platform analytics and insights + + +
+

Advanced analytics content

+
+
+
+
+ + + + System Reports + Generated reports and statistics + + +
+

Reports content

+
+
+
diff --git a/app/(app)/admin/superadmin/hiring/opportunities/page.tsx b/app/(app)/admin/superadmin/hiring/opportunities/page.tsx index d217367..97a7b7e 100644 --- a/app/(app)/admin/superadmin/hiring/opportunities/page.tsx +++ b/app/(app)/admin/superadmin/hiring/opportunities/page.tsx @@ -1,6 +1,6 @@ "use client" -import { useState, useEffect } from "react" +import { useState } from "react" import { Plus, Search, @@ -16,8 +16,6 @@ import { XCircle, Clock, Users, - DollarSign, - MapPin, } from "lucide-react" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Button } from "@/components/ui/button" @@ -46,10 +44,6 @@ import MuiPaper from "@mui/material/Paper" import MuiMenu from "@mui/material/Menu" import MuiMenuItem from "@mui/material/MenuItem" import MuiIconButton from "@mui/material/IconButton" -import { motion } from "framer-motion" -import { cn } from "@/lib/utils" -import toast from "react-hot-toast" -import { Tooltip } from "@mui/material" interface CareerOpportunity { id: number @@ -64,7 +58,6 @@ interface CareerOpportunity { description: string requirements: string[] responsibilities: string[] - salaryRange?: string // add this } export default function CareerOpportunities() { @@ -77,47 +70,142 @@ export default function CareerOpportunities() { const [newOpportunity, setNewOpportunity] = useState>({ title: "", department: "", - location: "STI College Alabang", + location: "", type: "full-time", + status: "draft", description: "", requirements: [], responsibilities: [], }) - const [opportunities, setOpportunities] = useState([]) - const [isLoading, setIsLoading] = useState(false) - const [isCreating, setIsCreating] = useState(false) - const [isUpdating, setIsUpdating] = useState(false) - async function fetchOpportunities() { - setIsLoading(true) - const res = await fetch("/api/superadmin/careers/fetch") - setIsLoading(false) - if (!res.ok) return - const { data } = await res.json() - if (Array.isArray(data)) { - setOpportunities( - data.map((item: Record, idx: number): CareerOpportunity => ({ - id: idx + 1, - title: typeof item.position_title === "string" ? item.position_title : "", - department: typeof item.department === "string" ? item.department : "", - location: typeof item.campus === "string" ? item.campus : "STI College Alabang", - type: (item.employment_type as CareerOpportunity["type"]) || "full-time", - status: "active", - postedAt: typeof item.posted_date === "string" ? new Date(item.posted_date).toISOString().split("T")[0] : "", - closingDate: "", - applicants: 0, - description: typeof item.job_description === "string" ? item.job_description : "", - requirements: Array.isArray(item.requirements) ? item.requirements as string[] : [], - responsibilities: Array.isArray(item.responsibilities) ? item.responsibilities as string[] : [], - salaryRange: typeof item.salary_range === "string" ? item.salary_range : undefined, - })) - ) - } - } - - useEffect(() => { - fetchOpportunities() - }, []) + // Mock data + const opportunities: CareerOpportunity[] = [ + { + id: 1, + title: "Software Developer", + department: "IT", + location: "Manila", + type: "full-time", + status: "active", + postedAt: "2023-05-15", + closingDate: "2023-06-15", + applicants: 12, + description: + "We are looking for a skilled software developer to join our IT team. The ideal candidate will have experience in web development and be proficient in JavaScript frameworks.", + requirements: [ + "Bachelor's degree in Computer Science or related field", + "2+ years of experience in web development", + "Proficiency in JavaScript, React, and Node.js", + "Experience with database systems like MySQL or MongoDB", + ], + responsibilities: [ + "Develop and maintain web applications", + "Collaborate with cross-functional teams", + "Troubleshoot and debug applications", + "Implement security and data protection measures", + ], + }, + { + id: 2, + title: "Data Analyst", + department: "Analytics", + location: "Cebu", + type: "full-time", + status: "active", + postedAt: "2023-05-20", + closingDate: "2023-06-20", + applicants: 8, + description: + "We are seeking a data analyst to help interpret data and turn it into information which can offer ways to improve our business, make it more efficient, and increase profits.", + requirements: [ + "Bachelor's degree in Statistics, Mathematics, or related field", + "Experience with data visualization tools", + "Proficiency in SQL and Excel", + "Knowledge of Python or R for data analysis", + ], + responsibilities: [ + "Collect and analyze data to identify patterns and trends", + "Create reports and dashboards", + "Collaborate with teams to implement data-driven strategies", + "Monitor performance metrics", + ], + }, + { + id: 3, + title: "UI/UX Designer", + department: "Design", + location: "Manila", + type: "part-time", + status: "active", + postedAt: "2023-05-25", + closingDate: "2023-06-25", + applicants: 15, + description: + "We are looking for a UI/UX Designer to turn our software into easy-to-use products for our clients. You will be responsible for the user interface design and user experience.", + requirements: [ + "Bachelor's degree in Design, Computer Science, or related field", + "Portfolio demonstrating UI/UX projects", + "Proficiency in design software like Figma or Adobe XD", + "Understanding of user-centered design principles", + ], + responsibilities: [ + "Create user flows, wireframes, and prototypes", + "Conduct user research and testing", + "Collaborate with developers to implement designs", + "Ensure consistency in design elements", + ], + }, + { + id: 4, + title: "Network Administrator", + department: "IT", + location: "Davao", + type: "full-time", + status: "closed", + postedAt: "2023-04-10", + closingDate: "2023-05-10", + applicants: 6, + description: + "We are seeking a Network Administrator to maintain and optimize our company's network infrastructure. The ideal candidate will have experience in network security and troubleshooting.", + requirements: [ + "Bachelor's degree in IT, Computer Science, or related field", + "Network certification (CCNA, CompTIA Network+)", + "Experience with network hardware and software", + "Knowledge of security protocols and procedures", + ], + responsibilities: [ + "Maintain network infrastructure and security", + "Monitor network performance and troubleshoot issues", + "Implement and manage network hardware and software", + "Provide technical support to staff", + ], + }, + { + id: 5, + title: "IT Intern", + department: "IT", + location: "Manila", + type: "internship", + status: "draft", + postedAt: "", + closingDate: "", + applicants: 0, + description: + "We are offering an internship opportunity for IT students to gain practical experience in a professional environment. Interns will work on real projects under the guidance of experienced professionals.", + requirements: [ + "Currently enrolled in a Computer Science or IT-related program", + "Basic knowledge of programming languages", + "Eagerness to learn and grow", + "Good communication skills", + ], + responsibilities: [ + "Assist in software development projects", + "Help with technical support tasks", + "Learn about IT operations in a corporate environment", + "Participate in team meetings and discussions", + ], + }, + ] const filteredOpportunities = opportunities.filter((opportunity) => { const matchesSearch = @@ -144,8 +232,9 @@ export default function CareerOpportunities() { setNewOpportunity({ title: opportunity.title, department: opportunity.department, - location: "STI College Alabang", + location: opportunity.location, type: opportunity.type, + status: opportunity.status, description: opportunity.description, requirements: opportunity.requirements, responsibilities: opportunity.responsibilities, @@ -167,159 +256,61 @@ export default function CareerOpportunities() { }) } - async function handleCreateOpportunity(e: React.FormEvent) { - e.preventDefault() - setIsCreating(true) - const payload = { - title: newOpportunity.title, - department: newOpportunity.department, - type: newOpportunity.type, - description: newOpportunity.description, - requirements: newOpportunity.requirements, - responsibilities: newOpportunity.responsibilities, - } - const res = await fetch("/api/superadmin/careers", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(payload), - }) - setIsCreating(false) - if (res.ok) { - setIsCreateDialogOpen(false) - setNewOpportunity({ - title: "", - department: "", - location: "STI College Alabang", - type: "full-time", - description: "", - requirements: [], - responsibilities: [], - }) - toast.success('Career opportunity created successfully!', { - position: 'bottom-right', - duration: 5000, - style: { - border: '1px solid #2563eb', - padding: '16px', - color: '#2563eb', - }, - iconTheme: { - primary: '#2563eb', - secondary: '#EFF6FF', - }, - }) - fetchOpportunities() - } else { - toast.error('Failed to create career opportunity.', { - position: 'bottom-right', - duration: 5000, - style: { - border: '1px solid #dc2626', - padding: '16px', - color: '#dc2626', - }, - iconTheme: { - primary: '#dc2626', - secondary: '#FEF2F2', - }, - }) - } - } - - async function handleEditOpportunitySubmit(e: React.FormEvent) { - e.preventDefault() - if (!selectedOpportunity) return - setIsUpdating(true) - const payload = { - id: selectedOpportunity.id, - title: newOpportunity.title, - department: newOpportunity.department, - type: newOpportunity.type, - description: newOpportunity.description, - requirements: newOpportunity.requirements, - responsibilities: newOpportunity.responsibilities, - } - const res = await fetch("/api/superadmin/careers", { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(payload), - }) - setIsUpdating(false) - if (res.ok) { - setIsEditDialogOpen(false) - toast.success('Career opportunity updated successfully!', { - position: 'bottom-right', - duration: 5000, - style: { - border: '1px solid #2563eb', - padding: '16px', - color: '#2563eb', - }, - iconTheme: { - primary: '#2563eb', - secondary: '#EFF6FF', - }, - }) - fetchOpportunities() - } else { - toast.error('Failed to update career opportunity.', { - position: 'bottom-right', - duration: 5000, - style: { - border: '1px solid #dc2626', - padding: '16px', - color: '#dc2626', - }, - iconTheme: { - primary: '#dc2626', - secondary: '#FEF2F2', - }, - }) - } - } - return ( - - +
+
-

Career Opportunities

-

Manage job listings and career opportunities

+

Career Opportunities

+

Manage job listings and career opportunities

- - - - - -
+
+ + + + + Create New Career Opportunity Fill in the details to create a new job listing.
- {/* Job Title & Job Type side by side */}
setNewOpportunity({ ...newOpportunity, title: e.target.value })} /> +
+
+ + setNewOpportunity({ ...newOpportunity, department: e.target.value })} + /> +
+
+ + setNewOpportunity({ ...newOpportunity, location: e.target.value })} + /> +
+
@@ -329,7 +320,7 @@ export default function CareerOpportunities() { setNewOpportunity({ ...newOpportunity, type: value }) } > - + @@ -340,34 +331,25 @@ export default function CareerOpportunities() {
- {/* Department only */} -
- - setNewOpportunity({ ...newOpportunity, department: e.target.value })} - /> -
- {/* Location (read-only, uneditable) with tooltip */}
-
- - + - -
-
- + +
+
+
- - - - Job Listings - - View and manage all career opportunities in the system - - - - {isLoading ? ( -
-
-
Fetching opportunities...
+ + + Job Listings + View and manage all career opportunities in the system + + +
+
+
+ + setSearchQuery(e.target.value)} + />
- ) : ( - <> -
-
-
- - setSearchQuery(e.target.value)} - /> -
- -
-
- - - - All - - - Active - - - Closed - - - Draft - - - - - - - - - - - - - - - - - )} - - - + +
+
+ + + + All + Active + Closed + Draft + + + + + + + + + + + + + + +
+
{/* View Opportunity Dialog */} @@ -545,27 +479,13 @@ export default function CareerOpportunities() {
{selectedOpportunity.department} - {selectedOpportunity.salaryRange && ( - <> - - - - {(() => { - const [min, max] = selectedOpportunity.salaryRange.split(",").map(s => s.trim()) - if (min && max) return `₱${min} - ₱${max}` - if (min) return `₱${min}` - return "" - })()} - - - )} - {selectedOpportunity.location}
+
@@ -594,43 +514,26 @@ export default function CareerOpportunities() {

{selectedOpportunity.description}

-
-
-

Requirements

-
    - {( - Array.isArray(selectedOpportunity.requirements) - ? selectedOpportunity.requirements.flatMap((req: string) => - typeof req === "string" ? req.split(",") : [] - ) - : typeof selectedOpportunity.requirements === "string" - ? (selectedOpportunity.requirements as string).split(",") - : [] - ).map((req: string, index: number) => ( -
  • - {req.trim()} -
  • - ))} -
-
-
-

Responsibilities

-
    - {( - Array.isArray(selectedOpportunity.responsibilities) - ? selectedOpportunity.responsibilities.flatMap((resp: string) => - typeof resp === "string" ? resp.split(",") : [] - ) - : typeof selectedOpportunity.responsibilities === "string" - ? (selectedOpportunity.responsibilities as string).split(",") - : [] - ).map((resp: string, index: number) => ( -
  • - {resp.trim()} -
  • - ))} -
-
+
+

Requirements

+
    + {selectedOpportunity.requirements.map((req, index) => ( +
  • + {req} +
  • + ))} +
+
+ +
+

Responsibilities

+
    + {selectedOpportunity.responsibilities.map((resp, index) => ( +
  • + {resp} +
  • + ))} +
)} @@ -639,7 +542,6 @@ export default function CareerOpportunities() { Close +
+ + + +
+

Team Feedback

+
+
+
+ + JD + +
+
John Doe
+
Technical Lead • May 14, 2025
+
+
+

+ Strong technical skills. Good understanding of React and TypeScript. Would be a good fit for our team. +

+
+
+
- {(() => { - const status = application.status.toLowerCase() - if (status === "new" || status === "shortlisted") { - return ( - <> - - - - - - {}}> - - View Profile - - { - window.location.href = `/employers/jobs/job-listings?job=${application.job_id}`; - }}> - - View Job Listing - - {status === "new" && ( - { - e.preventDefault() - if (setIsModalOpen) setIsModalOpen(false) - setTimeout(() => { - window.dispatchEvent(new CustomEvent("__openInterviewModal")) - }, 200) - }} - > - - Schedule Interview - - )} - { - e.preventDefault() - setIsModalOpen?.(false) - setTimeout(() => setShowSendOfferModal?.(true), 200) - }} - > - - Send Offer - - - - - - ) - } - if (status === "shortlisted") { - return ( - <> - - - - - ) - } - if (status === "interview" || status === "interview scheduled") { - return ( - <> - - - - - - {}}> - - View Profile - - { - window.location.href = `/employers/jobs/job-listings?job=${application.job_id}`; -}}> - - View Job Listing - - { - e.preventDefault(); - if (setIsModalOpen) setIsModalOpen(false); - setTimeout(() => { if (onOpenMarkDoneModal) onOpenMarkDoneModal(); }, 200); -}}> - - Mark as Done - - - - - - ) - } - if (status === "waitlisted") { - return ( - <> - - - - - - {}}> - - View Profile - - { - window.location.href = `/employers/jobs/job-listings?job=${application.job_id}`; - }}> - - View Job Listing - - - - - - ) - } - if (status === "rejected") { - return ( - <> - - - - ) - } - - return ( - <> - - - ) - })()} + + + {application.status.toLowerCase() === "new" && ( + + )}
-
- {(() => { - const status = application.status.toLowerCase() - if (status === "new") { - return ( - <> - - - - ) - } - if (status === "shortlisted") { - return ( - <> - - - - ) - } - if (status === "interview" || status === "interview scheduled") { - return ( - <> - - - - ) - } - if (status === "waitlisted") { - return ( - <> - - - - ) - } - if (status === "hired") { - return null - } - if (status === "rejected") { - return null - } - return null - })()} - {(() => { - const status = application.status.toLowerCase() - if (status === "hired") { - return ( - - ) - } - return null - })()} +
+ {["new", "interview"].includes(application.status.toLowerCase()) ? ( + + ) : ( + + )}
) } - -function getMatchMessage(percent: number) { - if (percent >= 70) return "Great fit for this job" - if (percent >= 40) return "Somewhat matches the requirements" - return "Low skill match for this job" -} - -function normalizeFiles(arr: (string | { name: string; url: string })[] = []): { name: string; url: string }[] { - return arr.map(item => { - if (typeof item === "string") { - const name = item.split("/").pop() || item - return { name, url: item } - } - return item - }) -} diff --git a/app/(app)/employers/jobs/applications/components/recruiter-application-tracker.tsx b/app/(app)/employers/jobs/applications/components/recruiter-application-tracker.tsx index 739ada1..c8dc592 100644 --- a/app/(app)/employers/jobs/applications/components/recruiter-application-tracker.tsx +++ b/app/(app)/employers/jobs/applications/components/recruiter-application-tracker.tsx @@ -1,209 +1,42 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ "use client" import type React from "react" import { useState, useRef, useEffect } from "react" -import { useSearchParams } from "next/navigation" import { Search, Calendar, MapPin, + ChevronDown, FileText, + Bookmark, Filter, MoreHorizontal, ArrowUpRight, ChevronRight, - ChevronLeft, - RefreshCw, - ChevronDown, - ChevronUp, - BarChart2, - CalendarDays, - CheckCircle + Briefcase, + CheckCircle, } from "lucide-react" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Badge } from "@/components/ui/badge" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { RecruiterApplicationDetailsModal } from "./recruiter-application-details" -import InterviewScheduleModal from "./modals/interview-schedule" -import SendOfferModal from "./modals/send-offer" -import FilterModal from "./modals/filter-modal" import { toast } from "react-toastify" import Avatar from "@mui/material/Avatar" -import { motion } from "framer-motion" -import { - MdStars, - MdOutlineEditCalendar, - MdRestore -} from "react-icons/md" -import { TbClockQuestion, TbUserSearch } from "react-icons/tb" -import { Menu, MenuItem } from "@mui/material" -import { Briefcase, User, Calendar as CalendarIcon } from "lucide-react" -import Tooltip from "@mui/material/Tooltip" -import { IoIosCloseCircleOutline } from "react-icons/io" -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog" -import { RiCalendarScheduleLine, RiEmotionSadLine } from "react-icons/ri" -import { LuCalendarCog } from "react-icons/lu" -import { FaHandHoldingUsd, FaRegCalendarTimes, FaUserCheck } from "react-icons/fa" -import { FaCalendarCheck, FaHandHoldingDollar, FaRegFolderOpen } from "react-icons/fa6" -import { calculateSkillsMatch } from "../../../../../../lib/match-utils" -import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" -import { BsMailbox2Flag } from "react-icons/bs" -import { LuSquarePen } from "react-icons/lu" -import { TbMailStar } from "react-icons/tb" -import { useRouter } from "next/navigation" - -type JobPosting = { - job_title?: string - work_type?: string - remote_options?: string - pay_amount?: string - pay_type?: string - perks_and_benefits?: string[] - location?: string -} - -type AnswersMap = Record; type Applicant = { - skills: string[] - contactInfo: { email: string; phone: string; socials: never[]; countryCode: string } application_id: string job_id: string job_title?: string status?: string - first_name?: string + first_name?: string last_name?: string address?: string experience_years?: string - applied_at?: string - student_id?: string - profile_image_url?: string - job_postings?: JobPosting - course?: string - year?: string - application_answers?: AnswersMap - pay_amount?: string - pay_type?: string - work_type?: string - remote_options?: string - perks_and_benefits?: string[] - location?: string - is_invited?: boolean -} - -function Pagination({ - totalPages = 1, - currentPage = 1, - onPageChange, -}: { - totalPages?: number - currentPage?: number - onPageChange?: (page: number) => void -}) { - const handlePageChange = (page: number) => { - if (page >= 1 && page <= totalPages) { - onPageChange?.(page) - } - } - - const getVisiblePages = () => { - const delta = 2 - const range = [] - const rangeWithDots = [] - - for (let i = Math.max(2, currentPage - delta); i <= Math.min(totalPages - 1, currentPage + delta); i++) { - range.push(i) - } - - if (currentPage - delta > 2) { - rangeWithDots.push(1, "…") - } else { - rangeWithDots.push(1) - } - - rangeWithDots.push(...range) - - if (currentPage + delta < totalPages - 1) { - rangeWithDots.push("…", totalPages) - } else if (totalPages > 1) { - rangeWithDots.push(totalPages) - } - - return rangeWithDots - } - - const visiblePages = getVisiblePages() - - return ( - -
-
- -
- {visiblePages.map((page, index) => ( -
- {page === "…" ? ( - - ) : ( - - )} -
- ))} -
- -
-
- Page {currentPage} of {totalPages} -
-
- ) -} - -const capitalize = (str?: string) => { - if (!str) return "" - if (str.toLowerCase() === "offer_sent") return "Offer Sent" - return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase() } export default function RecruiterApplicationTracker() { - - const searchParams = useSearchParams() const [selectedApplication, setSelectedApplication] = useState(null) const [isModalOpen, setIsModalOpen] = useState(false) const scrollContainerRef = useRef(null) @@ -214,170 +47,24 @@ export default function RecruiterApplicationTracker() { const [selectedJob, setSelectedJob] = useState<{ id: string; title: string } | null>(null) const [tab, setTab] = useState("all") const [selectedApplicant, setSelectedApplicant] = useState(null) - const [page, setPage] = useState(1) - const limit = 5 - const [loading, setLoading] = useState(true) - const [loadingJobSelection, setLoadingJobSelection] = useState(false) - const [interviewModalOpen, setInterviewModalOpen] = useState(false) - const [interviewApplicant, setInterviewApplicant] = useState(null) - const [companyName, setCompanyName] = useState(undefined) - const [employerId, setEmployerId] = useState(undefined) - const [editInterviewMode, setEditInterviewMode] = useState(false) - type InterviewData = { - date?: string - time?: string - location?: string - [key: string]: unknown - } - const [editInterviewData, setEditInterviewData] = useState(null) - const [sendOfferModalOpen, setSendOfferModalOpen] = useState(false) - const [offerApplicant, setOfferApplicant] = useState(null) - const [offerInitialData, setOfferInitialData] = useState(null) - type RecentActivity = { - name: string - position: string - update: string - time: string - icon?: string - iconBg?: string - application_id: string - } - const [recentActivity, setRecentActivity] = useState(null) - const [jobSkillsMap, setJobSkillsMap] = useState>({}) - const [search, setSearch] = useState("") - const [refreshingApplicants, setRefreshingApplicants] = useState(false) - const [sortBy, setSortBy] = useState<"match" | "date">("date") - const [sortDir, setSortDir] = useState<"desc" | "asc">("desc") - const [filterModalOpen, setFilterModalOpen] = useState(false) - const [filters, setFilters] = useState({}) - const allSkills = Array.from(new Set(applicants.flatMap(a => a.skills || []))) as string[] -const allLocations = Array.from( - new Set( - applicants - .map(a => a.address?.split(",")[0].trim()) - .filter((loc): loc is string => Boolean(loc)) - ) -) -const allCourses = Array.from( - new Set(applicants.map(a => a.course).filter((c): c is string => Boolean(c))) -) -const allYears = Array.from( - new Set(applicants.map(a => a.year).filter((y): y is string => Boolean(y))) -) -const allDegrees = ["Associate", "Bachelor’s", "Master’s", "Doctorate"] - - useEffect(() => { - setLoading(true) - const preSelectedJobId = searchParams?.get('jobId') - const preSelectedApplicantId = searchParams?.get('applicantId') - if (preSelectedJobId) { - setLoadingJobSelection(true) - } - fetch("/api/employers/applications") .then(res => res.json()) - .then(async data => { - - let employerIdFromSession: string | undefined - try { - const sessionRes = await fetch("/api/auth/session") - const sessionData = await sessionRes.json() - employerIdFromSession = sessionData?.user?.employerId - setEmployerId(employerIdFromSession) - } catch {} - if (employerIdFromSession) { - fetch(`/api/employers/colleagues/fetchCompanyName?employer_id=${employerIdFromSession}`) - .then(res => res.json()) - .then(companyData => { - if (companyData.company_name) setCompanyName(companyData.company_name) - }) - } - - const applicants: Applicant[] = (data.applicants as Applicant[]) || [] - const applicantsWithProfileImg = await Promise.all( - applicants.map(async (a) => { - if (!a.student_id) return a - try { - const res = await fetch(`/api/employers/applications/getStudentDetails?student_id=${a.student_id}`, { - method: "GET", - headers: { "Content-Type": "application/json" } - }) - const details = await res.json() - - // console.log("getStudentDetails for", a.student_id, "->", details) - let profile_image_url = "" - let course = a.course - let year = a.year - if (details && details.profile_img) { - const imgPath = details.profile_img - const signedUrlRes = await fetch("/api/students/get-signed-url", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - bucket: "user.avatars", - path: imgPath - }) - }) - const signedUrlData = await signedUrlRes.json() - if (signedUrlData && signedUrlData.signedUrl) { - profile_image_url = signedUrlData.signedUrl - } - } - if (details && details.course) course = details.course - if (details && details.year) year = details.year - return { ...a, profile_image_url, course, year } - } catch {} - return a - }) - ) - setApplicants(applicantsWithProfileImg) + .then(data => { + setApplicants((data.applicants as Applicant[]) || []) const jobs = Array.from( new Map( - applicantsWithProfileImg.map((a) => [ + ((data.applicants as Applicant[]) || []).map((a) => [ a.job_id, { id: a.job_id, title: a.job_title || "Job Posting" }, ]) ).values() ) - const allJobPostings = [{ id: "all", title: "All Job Postings" }, ...jobs] - setJobPostings(allJobPostings) - - if (preSelectedJobId) { - const preSelectedJob = allJobPostings.find(job => job.id === preSelectedJobId) - if (preSelectedJob) { - setSelectedJob(preSelectedJob) - } else { - setSelectedJob({ id: "all", title: "All Job Postings" }) - } - setLoadingJobSelection(false) - } else { - setSelectedJob({ id: "all", title: "All Job Postings" }) - } - - if (preSelectedApplicantId) { - const preSelectedApplicant = applicantsWithProfileImg.find(app => app.application_id === preSelectedApplicantId) - if (preSelectedApplicant) { - let answers = undefined - if (preSelectedApplicant?.application_answers && Array.isArray(preSelectedApplicant.application_answers)) { - answers = {} - } else if (preSelectedApplicant?.application_answers && typeof preSelectedApplicant.application_answers === "object") { - answers = preSelectedApplicant.application_answers - } - setSelectedApplicant(preSelectedApplicant ? { ...preSelectedApplicant, application_answers: answers } : null) - setIsModalOpen(true) - } - } - - setLoading(false) - setLoadingJobSelection(false) - }) - .catch(() => { - setLoading(false) - setLoadingJobSelection(false) + setJobPostings([{ id: "all", title: "All Job Postings" }, ...jobs]) + setSelectedJob({ id: "all", title: "All Job Postings" }) }) - }, [searchParams]) + }, []) useEffect(() => { if (selectedJob?.id === "all") { @@ -387,76 +74,6 @@ const allDegrees = ["Associate", "Bachelor’s", "Master’s", "Doctorate"] } }, [selectedJob, applicants]) - useEffect(() => { - let filtered = applicants - if (selectedJob?.id !== "all") { - filtered = filtered.filter(a => a.job_id === selectedJob?.id) - } - if (search.trim() !== "") { - const s = search.trim().toLowerCase() - filtered = filtered.filter(a => { - const name = `${a.first_name || ""} ${a.last_name || ""}`.toLowerCase() - const skills = Array.isArray(a.skills) ? a.skills.join(" ").toLowerCase() : "" - const experience = (a.experience_years || "").toLowerCase() - return ( - name.includes(s) || - skills.includes(s) || - experience.includes(s) - ) - }) - } - if (filters.status && filters.status.length > 0) { - filtered = filtered.filter(a => filters.status.includes(capitalize(a.status))) - } - if (filters.experience && filters.experience.length > 0) { - filtered = filtered.filter(a => - filters.experience.some((exp: string) => { - if (exp === "No experience") return (a.experience_years || "").toLowerCase().includes("no experience") - if (exp === "0-1 years") return /^0-1/.test(a.experience_years || "") - if (exp === "2-3 years") return /^2-3/.test(a.experience_years || "") - if (exp === "4-5 years") return /^4-5/.test(a.experience_years || "") - if (exp === "6+ years") return /^6/.test(a.experience_years || "") || (a.experience_years && parseInt(a.experience_years) >= 6) - return false - }) - ) - } - if (filters.skills && filters.skills.length > 0) { - filtered = filtered.filter(a => - Array.isArray(a.skills) && filters.skills.every((s: string) => a.skills.includes(s)) - ) - } - if (filters.location && filters.location.length > 0) { - filtered = filtered.filter(a => - filters.location.includes(a.address ? a.address.split(",")[0].trim() : "") - ) - } - if (filters.course && filters.course.length > 0) { - filtered = filtered.filter(a => filters.course.includes(a.course)) - } - if (filters.year && filters.year.length > 0) { - filtered = filtered.filter(a => filters.year.includes(a.year)) - } - if (filters.degree && filters.degree.length > 0) { - } - if (filters.dateFrom) { - filtered = filtered.filter(a => a.applied_at && new Date(a.applied_at) >= new Date(filters.dateFrom)) - } - if (filters.dateTo) { - filtered = filtered.filter(a => a.applied_at && new Date(a.applied_at) <= new Date(filters.dateTo)) - } - if (filters.showInvitedOnly) { - filtered = filtered.filter(a => a.is_invited) - } - setFilteredApplicants(filtered) - }, [search, applicants, selectedJob, filters]) - - function getTabStatus(applicantStatus: string | undefined) { - if (!applicantStatus) return "" - const status = capitalize(applicantStatus) - if (status === "Interview scheduled" || status === "Interview Scheduled") return "Interview" - return status - } - useEffect(() => { const handleScroll = () => { if (scrollContainerRef.current && scrollContainerRef.current.scrollTop > 50) { @@ -475,269 +92,17 @@ const allDegrees = ["Associate", "Bachelor’s", "Master’s", "Doctorate"] const handleViewDetails = (id: string, e: React.MouseEvent) => { e.stopPropagation() const applicant = applicants.find(a => a.application_id === id) - - let answers: AnswersMap | undefined = undefined - if (applicant?.application_answers && Array.isArray(applicant.application_answers)) { - answers = {} - } else if (applicant?.application_answers && typeof applicant.application_answers === "object") { - answers = applicant.application_answers as AnswersMap - } - setSelectedApplicant(applicant ? { ...applicant, application_answers: answers } : null) + setSelectedApplicant(applicant || null) setIsModalOpen(true) } const handleInviteToInterview = (id: string, e: React.MouseEvent) => { e.stopPropagation() - const applicant = applicants.find(a => a.application_id === id) - setInterviewApplicant(applicant || null) - setInterviewModalOpen(true) - } - - const handleReschedInterview = async (applicant: Applicant, e: React.MouseEvent) => { - e.stopPropagation() - setInterviewApplicant(applicant) - setEditInterviewMode(true) - let interviewData = null - try { - const res = await fetch(`/api/employers/applications/postInterviewSched?application_id=${applicant.application_id}`) - if (res.ok) { - const text = await res.text() - if (text) { - interviewData = JSON.parse(text).data || null - } - } - } catch {} - setEditInterviewData(interviewData) - setInterviewModalOpen(true) + toast.success("Interview invitation sent successfully!") } const totalApplicants = filteredApplicants.length - const newApplicants = filteredApplicants.filter(a => capitalize(a.status) === "New").length - - useEffect(() => { - setPage(1) - }, [selectedJob, tab, applicants]) - - const updateApplicantStatus = async (application_id: string, action: "shortlist" | "reject") => { - try { - const res = await fetch("/api/employers/applications/actions", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ application_id, action }), - }) - const data = await res.json() - if (res.ok && data.status) { - setApplicants(prev => - prev.map(app => - app.application_id === application_id - ? { ...app, status: capitalize(data.status) } - : app - ) - ) - toast.success( - action === "shortlist" - ? "Applicant shortlisted!" - : "Applicant rejected." - ) - } else { - toast.error(data.error || "Failed to update status") - } - } catch { - toast.error("Failed to update status") - } - } - - useEffect(() => { - const fetchRecentActivity = () => { - fetch("/api/employers/applications/activity") - .then(res => res.json()) - .then(data => { - if (Array.isArray(data)) setRecentActivity(data) - else if (Array.isArray(data.activity)) setRecentActivity(data.activity) - }) - .catch(() => setRecentActivity([])) - } - fetchRecentActivity() - const interval = setInterval(fetchRecentActivity, 30000) - return () => clearInterval(interval) - }, []) - - const iconMap: Record = { - new: { icon: , iconBg: "bg-yellow-500" }, - shortlisted: { icon: , iconBg: "bg-cyan-500" }, - interview: { icon: , iconBg: "bg-purple-500" }, - offer: { icon: , iconBg: "bg-yellow-500" }, - offer_sent: { icon: , iconBg: "bg-lime-400" }, - offer_updated: { icon: , iconBg: "bg-amber-600" }, - waitlisted: { icon: , iconBg: "bg-blue-500" }, - rejected: { icon: , iconBg: "bg-red-500" }, - hired: { icon: , iconBg: "bg-green-700" }, - } - - useEffect(() => { - async function fetchJobSkills() { - const jobs = applicants.map(a => a.job_id) - const uniqueJobs = Array.from(new Set(jobs)) - const skillsMap: Record = {} - await Promise.all( - uniqueJobs.map(async (jobId) => { - if (!jobId) return - try { - const res = await fetch(`/api/jobs/${jobId}/skills`) - const data = await res.json() - if (Array.isArray(data.skills)) { - skillsMap[jobId] = data.skills - } - } catch {} - }) - ) - setJobSkillsMap(skillsMap) - } - if (applicants.length > 0) fetchJobSkills() - }, [applicants]) - - function sortByAppliedAtDesc(applicants: Applicant[]) { - return [...applicants].sort((a, b) => { - const aDate = a.applied_at ? new Date(a.applied_at).getTime() : 0 - const bDate = b.applied_at ? new Date(b.applied_at).getTime() : 0 - return bDate - aDate - }) - } - function sortApplicants(applicants: Applicant[]) { - if (sortBy === "match") { - const sorted = [...applicants].sort((a, b) => - (calculateSkillsMatch(b.skills || [], jobSkillsMap[b.job_id] || []) - - calculateSkillsMatch(a.skills || [], jobSkillsMap[a.job_id] || [])) - ) - if (sortDir === "asc") sorted.reverse() - return sorted - } - const sorted = sortByAppliedAtDesc(applicants) - if (sortDir === "asc") sorted.reverse() - return sorted - } - - const refreshApplicants = async () => { - setRefreshingApplicants(true) - try { - const res = await fetch("/api/employers/applications") - const data = await res.json() - - let employerIdFromSession: string | undefined - try { - const sessionRes = await fetch("/api/auth/session") - const sessionData = await sessionRes.json() - employerIdFromSession = sessionData?.user?.employerId - setEmployerId(employerIdFromSession) - } catch {} - if (employerIdFromSession) { - fetch(`/api/employers/colleagues/fetchCompanyName?employer_id=${employerIdFromSession}`) - .then(res => res.json()) - .then(companyData => { - if (companyData.company_name) setCompanyName(companyData.company_name) - }) - } - - const applicants: Applicant[] = (data.applicants as Applicant[]) || [] - const applicantsWithProfileImg = await Promise.all( - applicants.map(async (a) => { - if (!a.student_id) return a - try { - const res = await fetch(`/api/employers/applications/getStudentDetails?student_id=${a.student_id}`, { - method: "GET", - headers: { "Content-Type": "application/json" } - }) - const details = await res.json() - - let profile_image_url = "" - let course = a.course - let year = a.year - if (details && details.profile_img) { - const imgPath = details.profile_img - const signedUrlRes = await fetch("/api/students/get-signed-url", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - bucket: "user.avatars", - path: imgPath - }) - }) - const signedUrlData = await signedUrlRes.json() - if (signedUrlData && signedUrlData.signedUrl) { - profile_image_url = signedUrlData.signedUrl - } - } - if (details && details.course) course = details.course - if (details && details.year) year = details.year - return { ...a, profile_image_url, course, year } - } catch {} - return a - }) - ) - setApplicants(applicantsWithProfileImg) - const jobs = Array.from( - new Map( - applicantsWithProfileImg.map((a) => [ - a.job_id, - { id: a.job_id, title: a.job_title || "Job Posting" }, - ]) - ).values() - ) - setJobPostings([{ id: "all", title: "All Job Postings" }, ...jobs]) - if (!selectedJob) { - setSelectedJob({ id: "all", title: "All Job Postings" }) - } - toast.success("Applicants refreshed successfully!") - } catch (error) { - console.log(error) - toast.error("Failed to refresh applicants") - } finally { - setRefreshingApplicants(false) - } - } - - const activeFilterCount = - (filters.status?.length || 0) + - (filters.experience?.length || 0) + - (filters.skills?.length || 0) + - (filters.location?.length || 0) + - (filters.course?.length || 0) + - (filters.year?.length || 0) + - (filters.degree?.length || 0) + - (filters.dateFrom ? 1 : 0) + - (filters.dateTo ? 1 : 0) + - (filters.showInvitedOnly ? 1 : 0) - - const handleViewOffer = async (applicant: Applicant) => { - setOfferApplicant(applicant) - setSendOfferModalOpen(true) - setOfferInitialData(null) - if (!applicant?.application_id) return - try { - const res = await fetch(`/api/employers/applications/postJobOffer/getJobOffer?application_id=${applicant.application_id}`) - const data = await res.json() - if (data && data.offer) { - setOfferInitialData({ - ...data.offer, - job_postings: applicant.job_postings, - applicant_name: `${applicant.first_name || ''} ${applicant.last_name || ''}`.trim(), - job_title: applicant.job_title || '', - company_name: companyName, - employer_id: employerId, - student_id: applicant.student_id, - application_id: applicant.application_id, - }) - } - } catch (e) { - console.log(e) - } - } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const handleOfferSent = async (application_id: string) => { - await refreshApplicants() - - } + const newApplicants = filteredApplicants.filter(a => a.status === "new").length return ( <> @@ -760,34 +125,26 @@ const allDegrees = ["Associate", "Bachelor’s", "Master’s", "Doctorate"] >
-

Applicant Tracking

-

+

Applicant Tracking

+

Manage and review all applicants for your job postings

- + {jobPostings.map((job) => ( + + ))} +
@@ -797,7 +154,7 @@ const allDegrees = ["Associate", "Bachelor’s", "Master’s", "Doctorate"]

{totalApplicants}

-

New Applicants

+

New Today

{newApplicants}

@@ -811,24 +168,13 @@ const allDegrees = ["Associate", "Bachelor’s", "Master’s", "Doctorate"]
-
+
setSearch(e.target.value)} - onKeyDown={e => { - if (e.key === "Enter") { - setSearch(e.currentTarget.value) - } - }} /> - @@ -839,597 +185,117 @@ const allDegrees = ["Associate", "Bachelor’s", "Master’s", "Doctorate"]
-
+ Your Applicants - -
- + - {(loading || refreshingApplicants) ? ( -
-
- -
- - {loadingJobSelection ? "Selecting your job posting..." : "Fetching applicants, please wait..."} - -
- ) : ( - { setTab(v); setPage(1); }} className="w-full"> + - + All - - {filteredApplicants.length} - - + New - - {filteredApplicants.filter(a => capitalize(a.status) === "New").length} - - - Shortlisted - - {filteredApplicants.filter(a => capitalize(a.status) === "Shortlisted").length} - + + Under Review - + Interview - - {filteredApplicants.filter(a => getTabStatus(a.status) === "Interview").length} - - - - Waitlisted - - {filteredApplicants.filter(a => { - const status = a.status ? a.status.toLowerCase() : "" - return status === "waitlisted" || status === "offer_sent" - }).length} - - - Hired - - {filteredApplicants.filter(a => a.status && a.status.toLowerCase() === "hired").length} - + + Invited - + Rejected - - {filteredApplicants.filter(a => capitalize(a.status) === "Rejected").length} - -
-
- - { - (() => { - if (tab === "all") return filteredApplicants.length - if (tab === "new") return filteredApplicants.filter(a => capitalize(a.status) === "New").length - if (tab === "shortlisted") return filteredApplicants.filter(a => capitalize(a.status) === "Shortlisted").length - if (tab === "interview") return filteredApplicants.filter(a => getTabStatus(a.status) === "Interview").length - if (tab === "invited") return filteredApplicants.filter(a => capitalize(a.status) === "Invited").length - if (tab === "waitlisted") return filteredApplicants.filter(a => capitalize(a.status) === "Waitlisted").length - if (tab === "rejected") return filteredApplicants.filter(a => capitalize(a.status) === "Rejected").length - if (tab === "hired") return filteredApplicants.filter(a => a.status && a.status.toLowerCase() === "hired").length - return 0 - })() - } applicants - -
- Sort by - - - - - - { - if (sortBy === "date") { - setSortDir(sortDir === "desc" ? "asc" : "desc") - } else { - setSortBy("date") - setSortDir("desc") - } - }} - > - - Date Applied - {sortBy === "date" && ( - sortDir === "desc" - ? - : - )} - - { - if (sortBy === "match") { - setSortDir(sortDir === "desc" ? "asc" : "desc") - } else { - setSortBy("match") - setSortDir("desc") - } - }} - > - - Match Score - {sortBy === "match" && ( - sortDir === "desc" - ? - : - )} - - - -
-
-
- - -
+
+ Sort by: + +
- { - (() => { - const filtered = sortApplicants(filteredApplicants) - const totalPages = Math.max(1, Math.ceil(filtered.length / limit)) - const paginated = filtered.slice((page - 1) * limit, page * limit) - if ( - filters.showInvitedOnly && - activeFilterCount === 1 && - filtered.length === 0 - ) { - return ( -
- -
No invited applicants
-
No applicants came from your invitation link or email.
-
- ) - } - if (activeFilterCount > 0 && filtered.length === 0) { - return ( -
- -
No candidates match your filters
-
Try adjusting your filters to see more applicants.
-
- ) - } - if (filtered.length === 0) { - return ( -
- -
No applicants yet
-
You'll see applicants here once they apply
-
- ) - } - return ( - <> - {paginated.map(app => - setSelectedApplication(app.application_id)} - handleViewDetails={handleViewDetails} - handleInviteToInterview={handleInviteToInterview} - handleReschedInterview={handleReschedInterview} - onShortlist={async () => await updateApplicantStatus(app.application_id, "shortlist")} - onReject={async () => await updateApplicantStatus(app.application_id, "reject")} - setOfferApplicant={setOfferApplicant} - setSendOfferModalOpen={setSendOfferModalOpen} - matchScore={calculateSkillsMatch(app.skills || [], jobSkillsMap[app.job_id] || [])} - handleViewOffer={handleViewOffer} - setApplicants={setApplicants} - /> - )} - {totalPages > 1 && ( - - )} - - ) - })() - } + {filteredApplicants.map(app => + setSelectedApplication(app.application_id)} + handleViewDetails={handleViewDetails} + handleInviteToInterview={handleInviteToInterview} + /> + )}
- { - (() => { - const filtered = sortApplicants(filteredApplicants.filter(a => capitalize(a.status) === "New")) - const totalPages = Math.max(1, Math.ceil(filtered.length / limit)) - const paginated = filtered.slice((page - 1) * limit, page * limit) - if (filtered.length === 0) { - return ( -
- -
No new applicants
-
Check back soon for new applications
-
- ) - } - return ( - <> - {paginated.map(app => - setSelectedApplication(app.application_id)} - handleViewDetails={handleViewDetails} - handleInviteToInterview={handleInviteToInterview} - handleReschedInterview={handleReschedInterview} - onShortlist={async () => await updateApplicantStatus(app.application_id, "shortlist")} - onReject={async () => await updateApplicantStatus(app.application_id, "reject")} - setOfferApplicant={setOfferApplicant} - setSendOfferModalOpen={setSendOfferModalOpen} - matchScore={calculateSkillsMatch(app.skills || [], jobSkillsMap[app.job_id] || [])} - handleViewOffer={handleViewOffer} - setApplicants={setApplicants} - /> - )} - {totalPages > 1 && ( - - )} - - ) - })() - } + {filteredApplicants.filter(a => a.status === "new").map(app => + setSelectedApplication(app.application_id)} + handleViewDetails={handleViewDetails} + handleInviteToInterview={handleInviteToInterview} + /> + )}
- - { - (() => { - const filtered = sortApplicants(filteredApplicants.filter(a => capitalize(a.status) === "Shortlisted")) - const totalPages = Math.max(1, Math.ceil(filtered.length / limit)) - const paginated = filtered.slice((page - 1) * limit, page * limit) - if (filtered.length === 0) { - return ( -
- -
No shortlisted applicants
-
Shortlisted applicants will appear here
-
- ) - } - return ( - <> - {paginated.map(app => - setSelectedApplication(app.application_id)} - handleViewDetails={handleViewDetails} - handleInviteToInterview={handleInviteToInterview} - handleReschedInterview={handleReschedInterview} - onShortlist={async () => await updateApplicantStatus(app.application_id, "shortlist")} - onReject={async () => await updateApplicantStatus(app.application_id, "reject")} - setOfferApplicant={setOfferApplicant} - setSendOfferModalOpen={setSendOfferModalOpen} - matchScore={calculateSkillsMatch(app.skills || [], jobSkillsMap[app.job_id] || [])} - handleViewOffer={handleViewOffer} - setApplicants={setApplicants} - /> - )} - {totalPages > 1 && ( - - )} - - ) - })() - } + + {filteredApplicants.filter(a => a.status === "review").map(app => + setSelectedApplication(app.application_id)} + handleViewDetails={handleViewDetails} + handleInviteToInterview={handleInviteToInterview} + /> + )} - { - (() => { - const filtered = sortApplicants(filteredApplicants.filter(a => getTabStatus(a.status) === "Interview")) - const totalPages = Math.max(1, Math.ceil(filtered.length / limit)) - const paginated = filtered.slice((page - 1) * limit, page * limit) - if (filtered.length === 0) { - return ( -
- -
No interviews scheduled
-
Invite applicants to interview
-
- ) - } - return ( - <> - {paginated.map(app => - setSelectedApplication(app.application_id)} - handleViewDetails={handleViewDetails} - handleInviteToInterview={handleInviteToInterview} - handleReschedInterview={handleReschedInterview} - onShortlist={async () => await updateApplicantStatus(app.application_id, "shortlist")} - onReject={async () => await updateApplicantStatus(app.application_id, "reject")} - setOfferApplicant={setOfferApplicant} - setSendOfferModalOpen={setSendOfferModalOpen} - matchScore={calculateSkillsMatch(app.skills || [], jobSkillsMap[app.job_id] || [])} - handleViewOffer={handleViewOffer} - setApplicants={setApplicants} - /> - )} - {totalPages > 1 && ( - - )} - - ) - })() - } + {filteredApplicants.filter(a => a.status === "interview").map(app => + setSelectedApplication(app.application_id)} + handleViewDetails={handleViewDetails} + handleInviteToInterview={handleInviteToInterview} + /> + )}
- { - (() => { - const filtered = sortApplicants(filteredApplicants.filter(a => capitalize(a.status) === "Invited")) - const totalPages = Math.max(1, Math.ceil(filtered.length / limit)) - const paginated = filtered.slice((page - 1) * limit, page * limit) - if (filtered.length === 0) { - return ( -
- -
No invitations sent
-
Invite applicants to interview
-
- ) - } - return ( - <> - {paginated.map(app => - setSelectedApplication(app.application_id)} - handleViewDetails={handleViewDetails} - handleInviteToInterview={handleInviteToInterview} - handleReschedInterview={handleReschedInterview} - onShortlist={async () => await updateApplicantStatus(app.application_id, "shortlist")} - onReject={async () => await updateApplicantStatus(app.application_id, "reject")} - setOfferApplicant={setOfferApplicant} - setSendOfferModalOpen={setSendOfferModalOpen} - matchScore={calculateSkillsMatch(app.skills || [], jobSkillsMap[app.job_id] || [])} - handleViewOffer={handleViewOffer} - setApplicants={setApplicants} - /> - )} - {totalPages > 1 && ( - - )} - - ) - })() - } + {filteredApplicants.filter(a => a.status === "invited").map(app => + setSelectedApplication(app.application_id)} + handleViewDetails={handleViewDetails} + handleInviteToInterview={handleInviteToInterview} + /> + )}
- { - (() => { - const filtered = sortApplicants(filteredApplicants.filter(a => capitalize(a.status) === "Rejected")) - const totalPages = Math.max(1, Math.ceil(filtered.length / limit)) - const paginated = filtered.slice((page - 1) * limit, page * limit) - if (filtered.length === 0) { - return ( -
- -
No rejected applicants
-
Rejected applicants will appear here
-
- ) - } - return ( - <> - {paginated.map(app => - setSelectedApplication(app.application_id)} - handleViewDetails={handleViewDetails} - handleInviteToInterview={handleInviteToInterview} - handleReschedInterview={handleReschedInterview} - onShortlist={async () => await updateApplicantStatus(app.application_id, "shortlist")} - onReject={async () => await updateApplicantStatus(app.application_id, "reject")} - setOfferApplicant={setOfferApplicant} - setSendOfferModalOpen={setSendOfferModalOpen} - matchScore={calculateSkillsMatch(app.skills || [], jobSkillsMap[app.job_id] || [])} - handleViewOffer={handleViewOffer} - setApplicants={setApplicants} - /> - )} - {totalPages > 1 && ( - - )} - - ) - })() - } -
- - { - (() => { - const filtered = sortApplicants(filteredApplicants.filter(a => { - const status = a.status ? a.status.toLowerCase() : "" - return status === "waitlisted" || status === "offer_sent" - })) - const totalPages = Math.max(1, Math.ceil(filtered.length / limit)) - const paginated = filtered.slice((page - 1) * limit, page * limit) - if (filtered.length === 0) { - return ( -
- -
No waitlisted applicants
-
Waitlisted applicants will appear here
-
- ) - } - return ( - <> - {paginated.map(app => - setSelectedApplication(app.application_id)} - handleViewDetails={handleViewDetails} - handleInviteToInterview={handleInviteToInterview} - handleReschedInterview={handleReschedInterview} - onShortlist={async () => await updateApplicantStatus(app.application_id, "shortlist")} - onReject={async () => await updateApplicantStatus(app.application_id, "reject")} - setOfferApplicant={setOfferApplicant} - setSendOfferModalOpen={setSendOfferModalOpen} - matchScore={calculateSkillsMatch(app.skills || [], jobSkillsMap[app.job_id] || [])} - handleViewOffer={handleViewOffer} - setApplicants={setApplicants} - /> - )} - {totalPages > 1 && ( - - )} - - ) - })() - } -
- - { - (() => { - const filtered = sortApplicants(filteredApplicants.filter(a => a.status && a.status.toLowerCase() === "hired")) - const totalPages = Math.max(1, Math.ceil(filtered.length / limit)) - const paginated = filtered.slice((page - 1) * limit, page * limit) - if (filtered.length === 0) { - return ( -
- -
No hired applicants
-
Hired applicants will appear here
-
- ) - } - return ( - <> - {paginated.map(app => - setSelectedApplication(app.application_id)} - handleViewDetails={handleViewDetails} - handleInviteToInterview={handleInviteToInterview} - handleReschedInterview={handleReschedInterview} - onShortlist={async () => await updateApplicantStatus(app.application_id, "shortlist")} - onReject={async () => await updateApplicantStatus(app.application_id, "reject")} - setOfferApplicant={setOfferApplicant} - setSendOfferModalOpen={setSendOfferModalOpen} - matchScore={calculateSkillsMatch(app.skills || [], jobSkillsMap[app.job_id] || [])} - handleViewOffer={handleViewOffer} - setApplicants={setApplicants} - /> - )} - {totalPages > 1 && ( - - )} - - ) - })() - } + {filteredApplicants.filter(a => a.status === "rejected").map(app => + setSelectedApplication(app.application_id)} + handleViewDetails={handleViewDetails} + handleInviteToInterview={handleInviteToInterview} + /> + )}
- )}
@@ -1441,49 +307,52 @@ const allDegrees = ["Associate", "Bachelor’s", "Master’s", "Doctorate"]
- {recentActivity === null || recentActivity.length === 0 ? ( -
- -
No recent activity
-
- ) : ( - recentActivity.slice(0, 6).map((update, index) => { - let iconKey = (update.icon || '').toLowerCase() - if (iconKey === "offer sent" || iconKey === "offer_sent") iconKey = "offer_sent" - const iconInfo = iconMap[iconKey] || { icon: , iconBg: 'bg-blue-200' } - return ( -
{ - if (update.application_id) { - const applicant = applicants.find(a => a.application_id === update.application_id) - setSelectedApplicant(applicant || null) - setIsModalOpen(true) - } - }} - > -
-
- {iconInfo.icon} -
- {update.time && update.time.includes("hour") && index < 1 ? ( - - ) : null} -
-
-

{update.name}

-

{update.position}

-

{update.update}

-

{formatActivityDate(update.time)}

-
- + {[ + { + name: "Alex Johnson", + position: "Frontend Developer", + update: "Submitted application", + time: "2 hours ago", + icon: , + iconBg: "bg-green-500", + }, + { + name: "Sarah Williams", + position: "Frontend Developer", + update: "Accepted interview invitation", + time: "1 day ago", + icon: , + iconBg: "bg-blue-500", + }, + { + name: "Michael Chen", + position: "Frontend Developer", + update: "Completed technical assessment", + time: "2 days ago", + icon: , + iconBg: "bg-yellow-500", + }, + ].map((update, index) => ( +
+
+
+ {update.icon}
- ) - }) - )} + {update.time.includes("hours") && index < 1 ? ( + + ) : null} +
+
+

{update.name}

+

{update.position}

+

{update.update}

+

{update.time}

+
+ +
+ ))}
@@ -1543,7 +412,7 @@ const allDegrees = ["Associate", "Bachelor’s", "Master’s", "Doctorate"] {candidate.match} Match

{candidate.title}

-

{candidate.experience}

+

{candidate.experience} experience

{candidate.skills.map((skill, i) => ( @@ -1571,80 +440,9 @@ const allDegrees = ["Associate", "Bachelor’s", "Master’s", "Doctorate"]
- { setInterviewModalOpen(false); setEditInterviewMode(false); setEditInterviewData(null); }} - initial={editInterviewMode && editInterviewData ? { - ...editInterviewData, - application_id: interviewApplicant?.application_id, - student_id: interviewApplicant?.student_id, - employer_id: employerId, - company_name: companyName - } : interviewApplicant ? { - application_id: interviewApplicant.application_id, - student_id: interviewApplicant.student_id, - employer_id: employerId, - company_name: companyName - } : undefined} - editMode={editInterviewMode} - onInterviewScheduled={(application_id) => { - setApplicants(prev => prev.map(app => app.application_id === application_id ? { ...app, status: "Interview scheduled" } : app)) - }} - /> - { setSendOfferModalOpen(false); setOfferApplicant(null); setOfferInitialData(null); }} - initial={offerInitialData || (offerApplicant ? { - application_id: offerApplicant.application_id, - student_id: offerApplicant.student_id, - employer_id: employerId, - company_name: companyName, - applicant_name: `${offerApplicant.first_name || ''} ${offerApplicant.last_name || ''}`.trim(), - job_title: offerApplicant.job_title || '', - job_postings: offerApplicant.job_postings - } : undefined)} - editMode={!!(offerInitialData && offerInitialData.id)} - onOfferSent={handleOfferSent} - /> - - - setFilterModalOpen(false)} - onApply={setFilters} - skills={allSkills} - locations={allLocations} - courses={allCourses} - years={allYears} - degrees={allDegrees} - initial={filters} />
@@ -1653,648 +451,108 @@ const allDegrees = ["Associate", "Bachelor’s", "Master’s", "Doctorate"] function ApplicantCard({ applicant, + selected, setSelected, handleViewDetails, handleInviteToInterview, - handleReschedInterview, - onShortlist, - onReject, - setOfferApplicant, - setSendOfferModalOpen, - matchScore, - handleViewOffer, - setApplicants }: { applicant: Applicant selected: boolean setSelected: () => void handleViewDetails: (id: string, e: React.MouseEvent) => void handleInviteToInterview: (id: string, e: React.MouseEvent) => void - handleReschedInterview: (applicant: Applicant, e: React.MouseEvent) => void - onShortlist: () => Promise - onReject: () => Promise - setOfferApplicant: (a: Applicant) => void - setSendOfferModalOpen: (open: boolean) => void - matchScore: number - handleViewOffer?: (a: Applicant) => Promise - setApplicants: React.Dispatch> }) { - const formattedLocation = applicant.address - ? applicant.address.split(",")[0].trim() - : "" - - function formatAppliedAt(dateString?: string) { - if (!dateString) return "" - let date: Date | null = null - if (typeof dateString === 'string' && dateString.includes('T')) { - date = new Date(dateString) - } else if (typeof dateString === 'string') { - date = new Date(Date.parse(dateString)) - } - if (!date || isNaN(date.getTime())) return "Invalid date" - const now = new Date() - const diffMs = now.getTime() - date.getTime() - const diffMins = Math.floor(diffMs / 60000) - const diffHours = Math.floor(diffMs / 3600000) - - - - const diffDays = Math.floor(diffMs / 86400000) - const diffWeeks = Math.floor(diffDays / 7) - - if (diffMins < 1) return "just now" - if (diffMins < 60) return `${diffMins} minute${diffMins === 1 ? "" : "s"} ago` - if ( diffHours < 24) return `${diffHours} hour${diffHours === 1 ? "" : "s"} ago` - if (diffDays < 7) return `${diffDays} day${diffDays === 1 ? "" : "s"} ago` - if (diffWeeks < 4) return `${diffWeeks} week${diffWeeks === 1 ? "" : "s"} ago` - const month = (date.getMonth() + 1).toString().padStart(2, '0') - const day = date.getDate().toString().padStart(2, '0') - const year = date.getFullYear().toString().slice(-2) - const hours = date.getHours().toString().padStart(2, '0') - const minutes = date.getMinutes().toString().padStart(2, '0') - return `Date ${month} ${day} ${year} ${hours}:${minutes}` - } - - const formattedAppliedAt = applicant.applied_at ? formatAppliedAt(applicant.applied_at) : "" - - const [anchorEl, setAnchorEl] = useState(null) - const open = Boolean(anchorEl) - const [loadingShortlist, setLoadingShortlist] = useState(false) - const [loadingReject, setLoadingReject] = useState(false) - const [rejectOpen, setRejectOpen] = useState(false) - const [cancelInterviewOpen, setCancelInterviewOpen] = useState(false) - const [markDoneOpen, setMarkDoneOpen] = useState(false) - const [markDoneLoading, setMarkDoneLoading] = useState(false) - const router = useRouter() - - const handleReject = async () => { - setLoadingReject(true) - await onReject() - - setLoadingReject(false) - setRejectOpen(false) - } - - const handleCancelInterview = async () => { - setLoadingShortlist(true) - await onShortlist() - setLoadingShortlist(false) - setCancelInterviewOpen(false) - } - - async function handleMarkAsDone() { - setMarkDoneLoading(true) - try { - const res = await fetch("/api/employers/applications/actions", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ application_id: applicant.application_id, action: "waitlist" }), - }) - const data = await res.json() - if (res.ok && data.status) { - applicant.status = "Waitlisted" - setApplicants(prev => - prev.map(app => - app.application_id === applicant.application_id - ? { ...app, status: "Waitlisted" } - : app - ) - ) - } - setMarkDoneOpen(false) - setMarkDoneLoading(false) - } finally { - - } - } - - function getMatchTooltip(score: number) { - if (score >= 70) return "Strong match for this job" - if (score >= 40) return "Partial match for this job" - return "Low skill match for this job" - } - return ( - <> - {/* Reject Dialog */} - - - -
- -
- Are you sure? - - This will reject {applicant.first_name}'s application. -
- - We know it's never easy to say no to talented people. - -
-
- - - - -
-
- - {/* Cancel Interview Dialog */} - - - -
- -
- Cancel Interview? - - This will move {applicant.first_name}'s application back to Shortlisted. -
- - Are you sure you want to cancel this interview? - -
-
- - - - -
-
- - {/* Mark as Done Dialog */} - - -
-
-
- -
- - Mark Interview as Finished? - - - This will move {applicant.first_name}'s application to Waitlisted status.
- - Are you sure you want to mark this interview as finished? - -
-
-
- - -
-
-
-
- - setSelected()} >
- - {!applicant.profile_image_url && (applicant.first_name?.charAt(0) || "A")} + + {applicant.first_name?.charAt(0) || "A"}

{applicant.first_name} {applicant.last_name}

- {applicant.is_invited && ( - - - - - Invited - - - - )} - - - {capitalize(applicant.status) === "Interview scheduled" ? "Interview Scheduled" : (capitalize(applicant.status) || "New")} - - - {capitalize(applicant.status) === "Interview scheduled" && null} + {applicant.status || "New"}

Applied for {applicant.job_title || "Job"}

-
-
+
+
- {formattedLocation} + {applicant.address}
-
+
- - {applicant.experience_years === "No experience" - ? "No experience" - : `${applicant.experience_years} experience`} - + {applicant.experience_years} experience
-
+
- Applied at {formattedAppliedAt} + Applied
-
- - - - = 70 - ? "bg-green-100 text-green-700 hover:bg-green-200 hover:text-green-800" - : matchScore >= 40 - ? "bg-orange-100 text-orange-700 hover:bg-orange-200 hover:text-orange-800" - : "bg-red-100 text-red-700 hover:bg-red-200 hover:text-red-800" - ) - + " whitespace-nowrap min-w-[70px]" - } - style={{ cursor: "pointer-events-none" }} - > - {matchScore}% Match - - - - -
- - setAnchorEl(null)} - anchorOrigin={{ vertical: "bottom", horizontal: "right" }} - transformOrigin={{ vertical: "top", horizontal: "right" }} - slotProps={{ - paper: { - sx: { minWidth: 180, borderRadius: 2, boxShadow: 2, p: 0.5 } - } - }} - > - { - setAnchorEl(null) - router.push(`/employers/jobs/job-listings?job=${applicant.job_id}&tab=overview`) - }}> - - View Job Listing - - setAnchorEl(null)}> - - View Profile - - {(() => { - const status = capitalize(applicant.status) - if (status === "New" || status === "Shortlisted") { - return [ - { - setAnchorEl(null) - handleInviteToInterview(applicant.application_id, e) - }} - > - - Set Interview - , - { setAnchorEl(null); setOfferApplicant({ ...applicant, job_postings: applicant.job_postings }); setSendOfferModalOpen(true); }}> - - Send Offer - - ] - } - if (status === "Interview" || status === "Interview scheduled") { - return [ - { setAnchorEl(null); setMarkDoneOpen(true); }}> - - Mark as Done - , - { setAnchorEl(null); setOfferApplicant({ ...applicant, job_postings: applicant.job_postings }); setSendOfferModalOpen(true); }}> - - Send Offer - , - { setAnchorEl(null); setRejectOpen(true); }}> - - Reject - - ] - } - if (status === "Offer Sent") { - return [ - { - setAnchorEl(null) - if (handleViewOffer) { - await handleViewOffer(applicant) - } else { - setOfferApplicant({ ...applicant, job_postings: applicant.job_postings }) - setSendOfferModalOpen(true) - } - }}> - - View Offer - - ] - } - return null - })()} - -
+
+ Match + +
- {capitalize(applicant.status) === "New" && ( + {applicant.status === "new" ? ( <> - - - - )} - {capitalize(applicant.status) === "Rejected" && ( - <> - - - { - e.stopPropagation() - setLoadingShortlist(true) - await onShortlist() - setLoadingShortlist(false) - }} - style={{ padding: "6px 12px", borderRadius: "6px", fontWeight: 500 }} - > - - Restore - - - - )} - {capitalize(applicant.status) === "Hired" && ( - <> - - - )} - {(capitalize(applicant.status) === "Shortlisted" || capitalize(applicant.status) === "Interview scheduled" || capitalize(applicant.status) === "Interview" || capitalize(applicant.status) === "Invited" || capitalize(applicant.status) === "Waitlisted") && ( - <> - {capitalize(applicant.status) === "Shortlisted" && ( - - )} - {(capitalize(applicant.status) === "Interview scheduled") && ( - - )} - {capitalize(applicant.status) === "Waitlisted" && ( - - )} - - {(capitalize(applicant.status) === "Interview scheduled") && ( - - - - )} - {(capitalize(applicant.status) !== "Interview scheduled") && ( - - )} - )} - {capitalize(applicant.status) === "Offer Sent" && ( + ) : ( <> - + {(applicant.status === "review") && ( + + )} )}
- {capitalize(applicant.status) === "New" + {applicant.status === "new" ? "New application" - : capitalize(applicant.status) === "Review" + : applicant.status === "review" ? "In review" - : capitalize(applicant.status) === "Interview Scheduled" + : applicant.status === "interview" ? "Interview phase" - : capitalize(applicant.status) === "Interview scheduled" || capitalize(applicant.status) === "Interview Scheduled" - ? "Interview phase" - : capitalize(applicant.status) === "Invited" + : applicant.status === "invited" ? "Invitation sent" - : capitalize(applicant.status) === "Shortlisted" - ? "Shortlisted" - : capitalize(applicant.status) === "Waitlisted" - ? "Waitlisted" - : capitalize(applicant.status) === "Offer Sent" - ? "Offer Sent" - : capitalize(applicant.status) === "Hired" - ? "Hired" - : "Rejected"} + : "Not selected"}
- - +
) } - -function formatActivityDate(dateString?: string) { - if (!dateString) return "" - let date: Date | null = null - if (typeof dateString === 'string' && dateString.includes('T')) { - date = new Date(dateString) - } else if (typeof dateString === 'string') { - date = new Date(Date.parse(dateString)) - } - if (!date || isNaN(date.getTime())) return "Invalid date" - const monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] - const month = monthNames[date.getMonth()] - const day = date.getDate().toString().padStart(2, '0') - const year = date.getFullYear().toString() - return `${month} ${day} ${year}` -} diff --git a/app/(app)/employers/jobs/applications/components/tabs/note-tab.tsx b/app/(app)/employers/jobs/applications/components/tabs/note-tab.tsx deleted file mode 100644 index bbdd832..0000000 --- a/app/(app)/employers/jobs/applications/components/tabs/note-tab.tsx +++ /dev/null @@ -1,171 +0,0 @@ -import Avatar from "@mui/material/Avatar" -import { Button } from "@/components/ui/button" -import { Separator } from "@/components/ui/separator" -import { Edit, Trash2 } from "lucide-react" -import { LuNotebookPen } from "react-icons/lu" -import type React from "react" - -type RecruiterNote = { - employer_name: string - job_title: string - date_added: string - note: string - profile_img?: string | null - isEmployer?: boolean -} - -export default function NoteTab({ - notes, - employerName, - editMode, - editNoteIdx, - editNoteText, - loading, - newNote, - setEditMode, - setEditNoteIdx, - setEditNoteText, - handleEditNote, - handleSaveEditNote, - handleDeleteNote, - setNewNote, - handleAddNote -}: { - notes: RecruiterNote[] - employerName: string - editMode: boolean - editNoteIdx: number | null - editNoteText: string - loading: boolean - newNote: string - setEditMode: (v: boolean) => void - setEditNoteIdx: (v: number | null) => void - setEditNoteText: (v: string) => void - handleEditNote: (idx: number) => void - handleSaveEditNote: () => void - handleDeleteNote: (idx: number) => void - setNewNote: (v: string) => void - handleAddNote: (note?: Partial) => void -}) { - function formatNoteDate(dateString?: string) { - if (!dateString) return "" - const date = new Date(dateString) - if (isNaN(date.getTime())) return dateString - const monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] - const month = monthNames[date.getMonth()] - const day = date.getDate().toString().padStart(2, '0') - const year = date.getFullYear().toString() - return `${month} ${day} ${year}` - } - - const employerNotes = notes.filter(n => n.isEmployer !== false) - - function handleAddEmployerNote() { - handleAddNote({ - note: newNote, - job_title: "", - date_added: new Date().toISOString(), - profile_img: undefined, - employer_name: employerName, - isEmployer: true - }) - } - - return ( - <> -
-

Recruiter Notes

-
- {employerNotes.length === 0 &&

No notes yet.

} - {employerNotes.map((n, idx) => { - const isOwnNote = n.employer_name === employerName - return ( -
-
- {n.profile_img ? ( - - ) : ( - - {(n.employer_name || "R")[0]} - - )} -
-
{n.employer_name}
-
{n.job_title} • {formatNoteDate(n.date_added)}
-
-
- {editMode && editNoteIdx === idx ? ( -
- - -
- - ) -} diff --git a/app/(app)/employers/jobs/applications/components/tabs/questions-tab.tsx b/app/(app)/employers/jobs/applications/components/tabs/questions-tab.tsx deleted file mode 100644 index 328d76a..0000000 --- a/app/(app)/employers/jobs/applications/components/tabs/questions-tab.tsx +++ /dev/null @@ -1,111 +0,0 @@ -"use client" - -import React, { useEffect, useState } from "react" -import { TbReportSearch } from "react-icons/tb" - -type Question = { - id: string - job_id: string - question: string -} - -type AnswersMap = Record; - -type QuestionsTabProps = { - jobId: string - answers?: AnswersMap -} - -export default function QuestionsTab({ jobId, answers }: QuestionsTabProps) { - const [questions, setQuestions] = useState([]) - const [loading, setLoading] = useState(true) - const [error, setError] = useState(null) - - useEffect(() => { - if (!jobId) { - setQuestions([]) - setLoading(false) - setError("No job ID provided") - return - } - setLoading(true) - setError(null) - fetch(`/api/employers/applications/getQuestions?job_id=${jobId}`) - .then(res => { - if (!res.ok) throw new Error(`API error: ${res.status}`) - return res.json() - }) - .then(data => { - setQuestions(data.questions || []) - setLoading(false) - }) - .catch((err) => { - setError(err.message) - setLoading(false) - }) - }, [jobId]) - - if (loading) { - return ( -
- -
- ) - } - - if (error) { - return
Error: {error}
- } - - if (!questions.length) { - return ( -
- - - No application questions for this job. -
- ) - } - - return ( -
-

Application Questions

-
- {questions.map((q, idx) => { - const answer: string | string[] | undefined = answers?.[q.id] - let displayAnswer: React.ReactNode - if (Array.isArray(answer)) { - displayAnswer = answer.length > 0 - ? ( -
- {answer.map((ans, i) => ( - {ans} - ))} -
- ) - : No answer - } else if (typeof answer === "string" && answer.trim() !== "") { - displayAnswer = {answer} - } else { - displayAnswer = No answer - } - return ( -
-
- {idx + 1}. - {q.question} -
-
- Answer: -
{displayAnswer}
-
-
- ) - })} -
-
- ) -} diff --git a/app/(app)/employers/jobs/applications/components/tabs/resume-tab.tsx b/app/(app)/employers/jobs/applications/components/tabs/resume-tab.tsx deleted file mode 100644 index a189ff8..0000000 --- a/app/(app)/employers/jobs/applications/components/tabs/resume-tab.tsx +++ /dev/null @@ -1,294 +0,0 @@ -import { FileText, Download, ExternalLink, Eye } from "lucide-react" -import { Button } from "@/components/ui/button" -import { useState } from "react" - -export default function ResumeTab({ - resumeUrl, - resume, - documents, - achievements = [], - portfolio = [] -}: { - resumeUrl?: string | null - resume?: string - documents: { name: string; date: string; size: string }[] - achievements?: (string | { name: string; url: string })[] - portfolio?: (string | { name: string; url: string })[] -}) { - const [previewUrl, setPreviewUrl] = useState(resumeUrl && typeof resumeUrl === "string" && resumeUrl.length > 0 ? resumeUrl : null) - const [previewLabel, setPreviewLabel] = useState("Resume Preview") - - function handlePreview(url: string, label: string) { - setPreviewUrl(url) - setPreviewLabel(label) - } - - return ( -
-

Candidate Documents

-
- {previewUrl ? ( - <> - {(() => { - const ext = previewLabel.split('.').pop()?.toLowerCase() || "" - if (ext === "pdf") { - return ( -