diff --git a/app/(app)/admin/superadmin/account-management/admins/page.tsx b/app/(app)/admin/superadmin/account-management/admins/page.tsx index 047559f..c0d54c4 100644 --- a/app/(app)/admin/superadmin/account-management/admins/page.tsx +++ b/app/(app)/admin/superadmin/account-management/admins/page.tsx @@ -125,15 +125,18 @@ export default function AdminsManagement() { fetchAdmins() }, []) + const safeLower = (str: string | null | undefined) => + str ? str.toLowerCase() : "" + const filteredAdmins = adminList.filter((admin) => { - const search = searchQuery.trim().toLowerCase() + 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}`, + 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) || diff --git a/app/(app)/admin/superadmin/account-management/students/page.tsx b/app/(app)/admin/superadmin/account-management/students/page.tsx index 0f1fb32..5d35d0b 100644 --- a/app/(app)/admin/superadmin/account-management/students/page.tsx +++ b/app/(app)/admin/superadmin/account-management/students/page.tsx @@ -45,8 +45,10 @@ import Image from "next/image" interface Student { id: string + uid: string studentId: string - name: string + first_name: string + last_name: string email: string phone: string course: string @@ -58,9 +60,11 @@ interface Student { address: string dateOfBirth: string department: string + } export default function StudentsManagement() { + const [isEditDialogOpen, setIsEditDialogOpen] = useState(false) const [searchQuery, setSearchQuery] = useState("") const [activeTab, setActiveTab] = useState("active") const [isViewDialogOpen, setIsViewDialogOpen] = useState(false) @@ -100,7 +104,8 @@ export default function StudentsManagement() { return { id: typeof s.id === "string" ? s.id : String(s.id), studentId, - name: `${String(s.first_name ?? "")} ${String(s.last_name ?? "")}`.trim(), + first_name: String(s.first_name ?? ""), + last_name: String(s.last_name ?? ""), email, phone: "", course: String(s.course ?? ""), @@ -135,12 +140,35 @@ export default function StudentsManagement() { ) ) + const handleEditStudent = (student: Student) => { + setSelectedStudent(student) + setIsEditDialogOpen(true) + } + + const confirmEditStudent = async (updatedStudent: Student) => { + try { + const res = await fetch(`/api/superadmin/updateStudent`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(updatedStudent), + }) + if (res.ok) { + setStudents((prev) => + prev.map((s) => (s.id === updatedStudent.id ? updatedStudent : s)) + ) + setIsEditDialogOpen(false) + } + } catch (error) { + console.error("Error updating student:", error) + } + } + const filteredStudents = students.filter((student) => { - const matchesSearch = - student.name.toLowerCase().includes(searchQuery.toLowerCase()) || - student.studentId.toLowerCase().includes(searchQuery.toLowerCase()) || - student.email.toLowerCase().includes(searchQuery.toLowerCase()) || - student.course.toLowerCase().includes(searchQuery.toLowerCase()) + const matchesSearch = + `${student.first_name} ${student.last_name}`.toLowerCase().includes(searchQuery.toLowerCase()) || + student.studentId.toLowerCase().includes(searchQuery.toLowerCase()) || + student.email.toLowerCase().includes(searchQuery.toLowerCase()) || + student.course.toLowerCase().includes(searchQuery.toLowerCase()) const matchesTab = (activeTab === "active" && student.status === "active") || @@ -215,7 +243,7 @@ export default function StudentsManagement() { const csv = filteredStudents .map( (student) => - `${student.studentId},${student.name},${student.email},${student.phone},${student.course},${student.year},${student.status}`, + `${student.studentId},${student.first_name} ${student.last_name},${student.email},${student.phone},${student.course},${student.year},${student.status}`, ) .join("\n") @@ -359,6 +387,7 @@ export default function StudentsManagement() { students={paginatedStudents} onViewStudent={handleViewStudent} onDeleteStudent={handleDeleteStudent} + onEditStudent={handleEditStudent} /> @@ -366,6 +395,7 @@ export default function StudentsManagement() { students={paginatedStudents} onViewStudent={handleViewStudent} onDeleteStudent={handleDeleteStudent} + onEditStudent={handleEditStudent} /> @@ -373,6 +403,7 @@ export default function StudentsManagement() { students={paginatedStudents} onViewStudent={handleViewStudent} onDeleteStudent={handleDeleteStudent} + onEditStudent={handleEditStudent} /> @@ -380,6 +411,7 @@ export default function StudentsManagement() { students={paginatedStudents} onViewStudent={handleViewStudent} onDeleteStudent={handleDeleteStudent} + onEditStudent={handleEditStudent} /> @@ -387,6 +419,7 @@ export default function StudentsManagement() { students={paginatedStudents} onViewStudent={handleViewStudent} onDeleteStudent={handleDeleteStudent} + onEditStudent={handleEditStudent} /> @@ -481,7 +514,7 @@ export default function StudentsManagement() { )}
-

{selectedStudent.name}

+

{selectedStudent.first_name} {selectedStudent.last_name}

{selectedStudent.studentId}

{username && (
Username: {username}
@@ -555,13 +588,155 @@ export default function StudentsManagement() { - + {/* Edit Student Dialog */} + + + + Edit Student + + Modify the student information below and save changes. + + + + {selectedStudent && ( +
+
+
+ + + setSelectedStudent({ ...selectedStudent, first_name: e.target.value }) + } + /> +
+
+ + + setSelectedStudent({ ...selectedStudent, last_name: e.target.value }) + } + /> +
+
+
+
+ + + setSelectedStudent({ ...selectedStudent, email: e.target.value }) + } + /> +
+
+ + + setSelectedStudent({ ...selectedStudent, course: e.target.value }) + } + /> +
+
+
+
+ + + setSelectedStudent({ ...selectedStudent, year: e.target.value }) + } + /> +
+
+ + + setSelectedStudent({ ...selectedStudent, section: e.target.value }) + } + /> +
+
+
+ )} + + + + + +
+
+ + {/* Delete Confirmation Dialog using MUI */} setIsDeleteDialogOpen(false)}> Are you sure you want to delete this student? @@ -582,6 +757,8 @@ export default function StudentsManagement() { + +
) } @@ -590,10 +767,12 @@ function StudentsTable({ students, onViewStudent, onDeleteStudent, + onEditStudent, }: { students: Student[] onViewStudent: (student: Student) => void onDeleteStudent: (student: Student) => void + onEditStudent: (student: Student) => void }) { const [menuAnchorEl, setMenuAnchorEl] = useState(null) const [menuStudentId, setMenuStudentId] = useState(null) @@ -643,7 +822,7 @@ function StudentsTable({ className="hover:bg-gray-50 transition-colors border-b border-gray-100 last:border-b-0" > {student.studentId} - {student.name} + {student.first_name} {student.last_name} {student.email} {student.course} {student.year} @@ -683,6 +862,7 @@ function StudentsTable({ { handleMenuClose() + onEditStudent(student) }} > @@ -730,5 +910,4 @@ function StatusBadge({ status }: { status: string }) { {label} ) -} - +} \ No newline at end of file diff --git a/app/(app)/students/jobs/applications/components/application-tracker.tsx b/app/(app)/students/jobs/applications/components/application-tracker.tsx index 53a1b61..89f5b5e 100644 --- a/app/(app)/students/jobs/applications/components/application-tracker.tsx +++ b/app/(app)/students/jobs/applications/components/application-tracker.tsx @@ -383,8 +383,10 @@ export default function ApplicationTrackerNoSidebar() { const [jobRatingCompanyImg, setJobRatingCompanyImg] = useState("") const [jobRatingRecruiterImg, setJobRatingRecruiterImg] = useState("") const [jobRatingRecruiterName, setJobRatingRecruiterName] = useState("") + const [jobRatingJobId, setJobRatingJobId] = useState("") async function handleOpenJobRatingModal(app: ApplicationData) { + setJobRatingJobId(app.job_postings?.id || "") let logo = "" let recruiterImg = "" let recruiterName = "" @@ -840,6 +842,7 @@ export default function ApplicationTrackerNoSidebar() { setIsJobRatingModalOpen(false)} + jobId={jobRatingJobId} jobTitle={jobRatingData?.jobTitle || ""} companyName={jobRatingData?.companyName || ""} recruiterProfileImg={jobRatingRecruiterImg} diff --git a/app/(app)/students/jobs/applications/components/job-rating-modal.tsx b/app/(app)/students/jobs/applications/components/job-rating-modal.tsx index 80dd5b9..6bb3d49 100644 --- a/app/(app)/students/jobs/applications/components/job-rating-modal.tsx +++ b/app/(app)/students/jobs/applications/components/job-rating-modal.tsx @@ -16,6 +16,7 @@ import { ConfettiStars } from "@/components/magicui/star" interface JobRatingModalProps { isOpen: boolean onClose: () => void + jobId: string jobTitle?: string companyName?: string recruiterProfileImg?: string @@ -34,6 +35,7 @@ interface RatingData { export function JobRatingModal({ isOpen, onClose, + jobId, jobTitle = "Software Engineer", companyName = "TechCorp", recruiterProfileImg, @@ -49,6 +51,32 @@ export function JobRatingModal({ const [recruiterImgUrl, setRecruiterImgUrl] = useState(null) const [companyLogoUrl, setCompanyLogoUrl] = useState(null) + // NEW: track if already rated + const [alreadyRated, setAlreadyRated] = useState(false) + const [loading, setLoading] = useState(true) + + useEffect(() => { + async function checkIfRated() { + if (!isOpen) return + setLoading(true) + try { + const res = await fetch(`/api/students/ratings?jobId=${jobId}`) + const data = await res.json() + if (Array.isArray(data) && data.length > 0) { + setAlreadyRated(true) + } else { + setAlreadyRated(false) + } + } catch (err) { + console.error("Failed to check rating:", err) + setAlreadyRated(false) + } finally { + setLoading(false) + } + } + checkIfRated() + }, [isOpen, jobId]) + useEffect(() => { async function fetchRecruiterImg() { if (!recruiterProfileImg) { @@ -115,11 +143,11 @@ export function JobRatingModal({ const handleCommentChange = (step: keyof RatingData, comment: string) => { setRatings((prev) => ({ ...prev, - [step]: { ...prev[step], comment }, + [step]: { ...prev[step], comment: comment.slice(0, 200) }, })) } - const handleNext = () => { + const handleNext = async () => { switch (currentStep) { case "intro": setCurrentStep("recruiter") @@ -131,6 +159,15 @@ export function JobRatingModal({ setCurrentStep("overall") break case "overall": + try { + await fetch("/api/students/ratings", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ jobId, ...ratings }), + }) + } catch (error) { + console.error("Failed to submit ratings:", error) + } setCurrentStep("complete") break case "complete": @@ -154,7 +191,13 @@ export function JobRatingModal({ } } - const StarRating = ({ rating, onRatingChange }: { rating: number; onRatingChange: (rating: number) => void }) => ( + const StarRating = ({ + rating, + onRatingChange, + }: { + rating: number + onRatingChange: (rating: number) => void + }) => (
{[1, 2, 3, 4, 5].map((star) => ( @@ -172,85 +217,46 @@ export function JobRatingModal({
) - useEffect(() => { - if (currentStep === "complete") { - } - }, [currentStep]) - - const getStepIcon = () => { - switch (currentStep) { - case "intro": - return - case "overall": - return - case "recruiter": - return recruiterImgUrl ? ( -
- Recruiter -
- ) : ( -
- -
- ) - case "company": - return companyLogoUrl ? ( -
- Company -
- ) : ( -
- {companyName?.charAt(0) || "C"} -
- ) - case "complete": - return ( -
- - -
- ) + const getStepContent = () => { + if (loading) { + return

Checking rating...

} - } - - const getStepTitle = () => { - switch (currentStep) { - case "intro": - return "Share Your Experience" - case "overall": - return "Overall Job Experience" - case "recruiter": - return "Recruiter Experience" - case "company": - return "Company Rating" - default: - return "" + if (alreadyRated) { + return ( +
+

+ You already rated this job +

+

+ Thanks for your feedback on {jobTitle} at{" "} + {companyName}. You can only submit one rating per job. +

+ +
+ ) } - } - const getStepContent = () => { + // ... existing step-based UI switch (currentStep) { case "intro": return (
-

{getStepTitle()}

+

+ Share Your Experience +

- Help others by rating your experience with the {jobTitle} position at {companyName} + Help others by rating your experience with the {jobTitle}{" "} + position at {companyName}

-

This is a multi-step rating that will only take a moment

+

+ This is a multi-step rating that will only take a moment +

- -
-
- ) - case "overall": - return ( -
-

How would you rate your overall experience with this position?

- handleRatingChange("overall", rating)} - /> -