diff --git a/src/app/(routes)/admin/jobs/[jobId]/layout.tsx b/src/app/(routes)/admin/jobs/[jobId]/layout.tsx new file mode 100644 index 00000000..02c5be14 --- /dev/null +++ b/src/app/(routes)/admin/jobs/[jobId]/layout.tsx @@ -0,0 +1,9 @@ +interface Props { + children: React.ReactNode; +} + +const AdminJobLayout = ({ children }: Props) => { + return
{children}
; +}; + +export default AdminJobLayout; diff --git a/src/app/(routes)/admin/jobs/[jobId]/page.tsx b/src/app/(routes)/admin/jobs/[jobId]/page.tsx new file mode 100644 index 00000000..c853d710 --- /dev/null +++ b/src/app/(routes)/admin/jobs/[jobId]/page.tsx @@ -0,0 +1,758 @@ +"use client"; +import React, { useState, useEffect } from "react"; +import { JobDetailFC } from "@/helpers/recruiter/types"; +import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos"; +import PersonIcon from "@mui/icons-material/Person"; +import { Button } from "@/components/ui/button"; +import Link from "next/link"; +import { Separator } from "@/components/ui/separator"; +import { getJafDetails } from "@/helpers/recruiter/api"; +import { JAFdetailsFC } from "@/helpers/recruiter/types"; +import { patchJobData } from "@/helpers/recruiter/api"; +import { patchSalaryData } from "@/helpers/recruiter/api"; +import { + CategorySelectList, + GenderSelectList, +} from "@/components/Recruiters/jobEdit"; +import { fetchEachJob } from "@/helpers/api"; +import Cookies from "js-cookie"; +import Loader from "@/components/Loader/loader"; +import toast from "react-hot-toast"; +import JobCoordinatorForm from "@/components/Admin/AddForms"; + +const JobDetailPage = ({ params }: { params: { jobId: string } }) => { + const [job, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [editMode, setEditMode] = useState(false); + const [formData, setFormData] = useState(null); + const [jafDetails, setJafDetails] = useState(); + + useEffect(() => { + const fetchData = async () => { + try { + const [jobDetailData, jafDetailsData] = await Promise.all([ + fetchEachJob(Cookies.get("accessToken"), params.jobId), + getJafDetails(), + ]); + + setJafDetails((prev) => jafDetailsData); + setData(jobDetailData); + setFormData(jobDetailData); + } catch (error) { + toast.error("Error fetching data"); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [params.jobId]); + + const handleEditClick = () => { + if (editMode) { + handleSubmit(); + } + setEditMode(!editMode); + }; + + const handleChange = (e) => { + const { name, value } = e.target; + setFormData((prev) => ({ ...prev, [name]: value })); + }; + + const handleSubmit = async () => { + const c1 = await patchJobData(job.id, formData); + if (true) { + formData.salaries.map((salary, index) => patchSalaryData(salary)); + } + setEditMode(false); + window.location.reload(); + }; + + const addNewTest = () => { + const newTest = { type: "", duration: 0 }; + setFormData((prev) => ({ + ...prev, + selectionProcedure: { + ...prev.selectionProcedure, + tests: [...prev.selectionProcedure.tests, newTest], + }, + })); + }; + + const addNewInterview = () => { + const newInterview = { type: "", duration: 0 }; + setFormData((prev) => ({ + ...prev, + selectionProcedure: { + ...prev.selectionProcedure, + interviews: [...prev.selectionProcedure.interviews, newInterview], + }, + })); + }; + + return ( +
+ {loading && ( +
+
+ +
+
+ )} + {job && ( +
+
+
+
+ Role + {editMode ? ( + + ) : ( + {job.role} + )} +
+
+ Activity + {job.active ? "Active" : "Inactive"} +
+
+ Current Status + {job.currentStatus} +
+
+ + + + {!job.active && ( + + )} +
+
+
+
Skills
+
+ {editMode ? ( + + ) : ( + job.skills + )} +
+
+
+
Location
{" "} + {editMode ? ( + + ) : ( +
{job.location}
+ )} +
+
+
Offer letter release
{" "} + {editMode ? ( + + ) : ( +
+ {new Date(job.offerLetterReleaseDate).toLocaleString()} +
+ )} +
+
+
Joining Date
{" "} + {editMode ? ( + + ) : ( +
{new Date(job.joiningDate).toLocaleString()}
+ )} +
+
+
Duration
{" "} + {editMode ? ( + + ) : ( +
{job.duration}
+ )} +
+
+
Attachment
{" "} + {editMode ? ( + + ) : ( +
{job.attachment}
+ )} +
+
+
+
+ {job.selectionProcedure && ( +
+
+ Selection Procedure +
+ +
+
+
Selection mode
{" "} + {editMode ? ( + + ) : ( +
{job.selectionProcedure.selectionMode}
+ )} +
+
+
+ Shortlist from Resume +
{" "} + {editMode ? ( + { + setFormData((prev) => ({ + ...prev, + selectionProcedure: { + ...prev.selectionProcedure, + shortlistFromResume: e.target.checked, + }, + })); + }} + /> + ) : ( +
+ {job.selectionProcedure.shortlistFromResume + ? "YES" + : "NO"} +
+ )} +
+
+
Group Discussion
{" "} + {editMode ? ( + { + setFormData((prev) => ({ + ...prev, + selectionProcedure: { + ...prev.selectionProcedure, + groupDiscussion: e.target.checked, + }, + })); + }} + /> + ) : ( +
+ {job.selectionProcedure.groupDiscussion ? "YES" : "NO"} +
+ )} +
+
+
Number of members
{" "} + {editMode ? ( + { + setFormData((prev) => ({ + ...prev, + selectionProcedure: { + ...prev.selectionProcedure, + requirements: { + ...prev.selectionProcedure.requirements, + numberOfMembers: e.target.value, + }, + }, + })); + }} + /> + ) : ( +
+ {job.selectionProcedure.requirements?.numberOfMembers!} +
+ )} +
+
+ +
+
+
Tests
+
    + {editMode + ? formData.selectionProcedure.tests.map((test, index) => ( +
  • + Test Type: + +
    + Test Duration: + { + const updatedTests = + formData.selectionProcedure.tests.map( + (t, i) => + i === index + ? { + ...t, + duration: Number(e.target.value), + } + : t + ); + setFormData((prev) => ({ + ...prev, + selectionProcedure: { + ...prev.selectionProcedure, + tests: updatedTests, + }, + })); + }} + /> +
  • + )) + : job.selectionProcedure.tests.map((p, index) => ( +
  • + {Object.entries(p).map(([key, value]) => ( + + {key} : {value} +
    +
    + ))} +
  • + ))} +
+ {editMode && ( + + )} +
+ + + +
+
Interviews
+
    + {editMode + ? formData.selectionProcedure.interviews.map( + (interview, index) => ( +
  • + Interview Type: + +
    + Interview Duration: + { + const updatedInterviews = + formData.selectionProcedure.interviews.map( + (i, j) => + j === index + ? { ...i, duration: e.target.value } + : i + ); + setFormData((prev) => ({ + ...prev, + selectionProcedure: { + ...prev.selectionProcedure, + interviews: updatedInterviews, + }, + })); + }} + /> +
  • + ) + ) + : job.selectionProcedure.interviews.map((p, index) => ( +
  • + {Object.entries(p).map(([key, value]) => ( + + {key} : {value} +
    +
    + ))} +
  • + ))} +
+ {editMode && ( + + )} +
+
+
+
+
+ Other requirements :{" "} + {editMode ? ( + { + setFormData((prev) => ({ + ...prev, + selectionProcedure: { + ...prev.selectionProcedure, + requirements: { + ...prev.selectionProcedure.requirements, + otherRequirements: e.target.value, + }, + }, + })); + }} + /> + ) : ( +
+ {job.selectionProcedure.requirements?.otherRequirements} +
+ )} +
+
+
+
+ )} +
+
Job Coordinators
+
+ {job.jobCoordinators?.map((coordinator, index) => ( +
+
+ +
+ {coordinator.tpcMember.user.name} +
+
+
+ Role : + {coordinator.tpcMember.role} +
+
+ Department : + {coordinator.tpcMember.department} +
+
+ Email : + {coordinator.tpcMember.user.email} +
+
+ Contact : + {coordinator.tpcMember.user.contact} +
+
+
+ ))} + +
+
+
+
Salaries
+ {job.salaries?.map((salary, salaryIndex) => ( +
+
+
+
Base Salary
{" "} + {editMode ? ( + { + const updatedSalaries = formData.salaries.map( + (s, i) => + i === salaryIndex + ? { ...s, baseSalary: e.target.value } + : s + ); + setFormData((prev) => ({ + ...prev, + salaries: updatedSalaries, + })); + }} + /> + ) : ( +
{salary.baseSalary}
+ )} +
+
+
CTC
{" "} + {editMode ? ( + { + const updatedSalaries = formData.salaries.map( + (s, i) => + i === salaryIndex + ? { ...s, totalCTC: e.target.value } + : s + ); + setFormData((prev) => ({ + ...prev, + salaries: updatedSalaries, + })); + }} + /> + ) : ( +
{salary.totalCTC}
+ )} +
+
+
Take Home Salary
{" "} + {editMode ? ( + { + const updatedSalaries = formData.salaries.map( + (s, i) => + i === salaryIndex + ? { ...s, takeHomeSalary: e.target.value } + : s + ); + setFormData((prev) => ({ + ...prev, + salaries: updatedSalaries, + })); + }} + /> + ) : ( +
{salary.takeHomeSalary}
+ )} +
+
+
Gross Salary
{" "} + {editMode ? ( + { + const updatedSalaries = formData.salaries.map( + (s, i) => + i === salaryIndex + ? { ...s, grossSalary: e.target.value } + : s + ); + setFormData((prev) => ({ + ...prev, + salaries: updatedSalaries, + })); + }} + /> + ) : ( +
{salary.grossSalary}
+ )} +
+
+
+ Other Compensations +
{" "} + {editMode ? ( + { + const updatedSalaries = formData.salaries.map( + (s, i) => + i === salaryIndex + ? { ...s, otherCompensations: e.target.value } + : s + ); + setFormData((prev) => ({ + ...prev, + salaries: updatedSalaries, + })); + }} + /> + ) : ( +
{salary.otherCompensations}
+ )} +
+
+ {/* Genders */} +
+

Genders

+ {editMode ? ( + + ) : ( +
+ {salary.genders?.map((gender, genderIndex) => ( +
+ {editMode ? null : ( +
+ {gender} +
+ )} +
+ ))} +
+ )} +
+ + {/* Categories */} +
+

Categories

+ {editMode ? ( + + ) : ( +
+ {salary.categories?.map((category, categoryIndex) => ( +
+ {editMode ? null : ( +
+ {category} +
+ )} +
+ ))} +
+ )} +
+ + +
+ ))} +
+
+ )} +
+ ); +}; + +export default JobDetailPage; diff --git a/src/app/(routes)/admin/jobs/events/[jobId]/layout.tsx b/src/app/(routes)/admin/jobs/events/[jobId]/layout.tsx new file mode 100644 index 00000000..02c5be14 --- /dev/null +++ b/src/app/(routes)/admin/jobs/events/[jobId]/layout.tsx @@ -0,0 +1,9 @@ +interface Props { + children: React.ReactNode; +} + +const AdminJobLayout = ({ children }: Props) => { + return
{children}
; +}; + +export default AdminJobLayout; diff --git a/src/app/(routes)/admin/jobs/events/[jobId]/page.tsx b/src/app/(routes)/admin/jobs/events/[jobId]/page.tsx new file mode 100644 index 00000000..b85473a5 --- /dev/null +++ b/src/app/(routes)/admin/jobs/events/[jobId]/page.tsx @@ -0,0 +1,66 @@ +"use client"; +import React from "react"; +import { useState, useEffect } from "react"; +import { JobEvents } from "@/components/Admin/JobEvents"; +import { fetchJobEvents } from "@/helpers/api"; +import { EventFC } from "@/helpers/recruiter/types"; +import { Button } from "@/components/ui/button"; +import { AddEvent } from "@/components/Admin/JobEvents"; +import Loader from "@/components/Loader/loader"; +import toast from "react-hot-toast"; + +const EventsPage = ({ params }: { params: { jobId: string } }) => { + const [events, setData] = useState<[EventFC]>(null); + const [loading, setLoading] = useState(true); + const [addEventForm, setAddEventForm] = useState(false); + + useEffect(() => { + const fetchData = async () => { + try { + const jsonData = await fetchJobEvents(params.jobId); + setData(jsonData); + } catch (error) { + toast.error("Some error occured"); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [params.jobId]); + return ( +
+

+ Events And Applications +

+ {addEventForm && ( + + )} +
+ +
+ {loading && ( +
+ +
+ )} + {events && ( +
+ +
+ )} +
+ ); +}; + +export default EventsPage; diff --git a/src/app/(routes)/admin/jobs/page.tsx b/src/app/(routes)/admin/jobs/page.tsx index 63ac7e65..b6302367 100644 --- a/src/app/(routes)/admin/jobs/page.tsx +++ b/src/app/(routes)/admin/jobs/page.tsx @@ -10,30 +10,48 @@ import { useMaterialReactTable, type MRT_Row, createMRTColumnHelper, -} from 'material-react-table'; +} from "material-react-table"; import Table from "@/components/NewTableComponent/Table"; +import { useEffect, useState } from "react"; +import Loader from "@/components/Loader/loader"; -const hiddenColumns = ['id','season.id','company.id','recruiter.id','recruiter.user.id']; +const hiddenColumns = [ + "id", + "season.id", + "company.id", + "recruiter.id", + "recruiter.user.id", +]; -const JobPage = async () => { +const JobPage = () => { const columnHelper = createMRTColumnHelper(); - const columns = generateColumns(recruitmentDTO) - console.log(columns) - const AllJobs = await fetchAllJobs(Cookies.get("accessToken"),undefined); - console.log(AllJobs) + const columns = generateColumns(recruitmentDTO); + const [loading, setLoading] = useState(true); + const [allJobs, setAllJobs] = useState(); const visibleColumns = columns.filter( - (column:any) => !hiddenColumns.includes(column?.accessorKey) + (column: any) => !hiddenColumns.includes(column?.accessorKey) ); + + useEffect(() => { + const getData = async () => { + const data = await fetchAllJobs(Cookies.get("accessToken"), undefined); + setAllJobs(data); + setLoading(false); + }; + getData(); + }, []); + return (

Jobs

- {AllJobs && ( - + {loading && ( +
+ +
+ )} + {allJobs && ( +
)} diff --git a/src/components/Admin/AddForms.tsx b/src/components/Admin/AddForms.tsx new file mode 100644 index 00000000..7e2d7ebe --- /dev/null +++ b/src/components/Admin/AddForms.tsx @@ -0,0 +1,120 @@ +import React, { useEffect, useState } from "react"; +import { JobCoordinatorFC, TPCMember } from "./types"; +import PersonIcon from "@mui/icons-material/Person"; +import Select from "react-select"; +import { Button } from "../ui/button"; +import { fetchTpcMembers, postJobCoordinator } from "@/helpers/api"; +import { CircularProgress } from "@mui/material"; +import toast from "react-hot-toast"; + +const JobCoordinatorForm = ({ jobId }: { jobId: string }) => { + const [tpcMembers, setTpcMembers] = useState(); + const [role, setRole] = useState("PRIMARY"); + const [loading, setLoading] = useState(true); + const options = tpcMembers?.map((member) => ({ + label: `${member.user.name} ${member.user.email}`, + value: member.id, + })); + const jobRoleOptions = [ + { + label: "PRIMARY", + value: "PRIMARY", + }, + { + label: "SECONDARY", + value: "SECONDARY", + }, + ]; + const [coordinator, setCoordinator] = useState(); + + const submitData = async () => { + await postJobCoordinator([ + { + jobId: jobId, + tpcMemberId: coordinator.id, + role: role, + }, + ]); + toast.success("Added! Reload to see changes"); + }; + + useEffect(() => { + const fetchData = async () => { + const data = await fetchTpcMembers(); + console.log(data); + setTpcMembers(data); + setLoading(false); + }; + fetchData(); + }, []); + + return ( +
+ {loading ? ( + + ) : ( + <> +
+ +
+ + { + setRole(newValue.value); + }} + /> + +
+ )} + + )} +
+ ); +}; + +export default JobCoordinatorForm; diff --git a/src/components/Admin/JobEvents.tsx b/src/components/Admin/JobEvents.tsx new file mode 100644 index 00000000..d6971b31 --- /dev/null +++ b/src/components/Admin/JobEvents.tsx @@ -0,0 +1,589 @@ +"use client"; +import React from "react"; +import { useState, useEffect } from "react"; +import { EventFC, ApplicationFC, SalaryFC } from "@/helpers/recruiter/types"; +import { CircularProgress, Modal, Typography } from "@mui/material"; +import VerifiedIcon from "@mui/icons-material/Verified"; +import { + addEvent, + fetchEventById, + getResumeFile, + getStudentSalaryOffers, + postOnCampusOffer, + promoteStudent, +} from "@/helpers/api"; +import { Button } from "../ui/button"; +import toast from "react-hot-toast"; +import Select from "react-select"; +import { Unstable_NumberInput as NumberInput } from "@mui/base/Unstable_NumberInput"; +import Table from "../NewTableComponent/Table"; +import generateColumns from "../NewTableComponent/ColumnMapping"; + +const typeOptions = [ + "POLL", + "PPT", + "INTERVIEW", + "TEST", + "APPLICATION", + "COMPLETED", +]; + +const options = typeOptions.map((option) => ({ + value: option, + label: option, +})); + +export const AddEvent = ({ + open, + setOpen, + jobId, +}: { + open: boolean; + setOpen: (open: boolean) => void; + jobId: string; +}) => { + const [formValues, setFormValues] = useState({ + jobId: jobId, + roundNumber: 0, + type: "", + metadata: "", + startDateTime: "", + endDateTime: "", + visibleToRecruiter: false, + }); + + const handleClose = () => setOpen(false); + + const handleChange = (e: React.ChangeEvent) => { + const { name, value, type, checked } = e.currentTarget; + setFormValues({ + ...formValues, + [name]: type === "checkbox" ? checked : value, + }); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + try { + await addEvent([formValues]); + toast.success("Successfully added"); + window.location.reload(); + } catch { + toast.error("Some Error Occured"); + } + }; + + return ( + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+ ); +}; + +const PromoteStudent = ({ + open, + students, + onClose, + events, +}: { + open: boolean; + students: any[]; + onClose: () => void; + events: EventFC[]; +}) => { + const [roundNumber, setRoundNumber] = useState(0); + const studentIds = students.map((student) => student.id); + const [eventId, setEventId] = useState(); + useEffect(() => { + const setCurrentEvent = events.forEach((event) => { + if (event.roundNumber == roundNumber) { + setEventId(event.id); + return event; + } + }); + setCurrentEvent; + }, [roundNumber]); + + const updateEvent = async () => { + await promoteStudent({ studentIds }, eventId); + toast.success("Successfully promoted!"); + onClose(); + }; + + return ( + +
+
+ Promote / Demote students :{" "} + {students.map((student) => `${student.name}, `)} to
+ + { + setRoundNumber(newValue); + }} + max={events.length} + min={0} + slotProps={{ + input: { + className: + "bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500", + }, + }} + placeholder="0" + required + /> + +
+
+
+ ); +}; + +const MakeJobOfferModal = ({ + open, + students, + onClose, + events, + lastEvent, +}: { + open: boolean; + students: any[]; + onClose: () => void; + events: EventFC[]; + lastEvent: EventFC; +}) => { + const studentIds = students.map((student) => student.id); + const [salaries, setSalaries] = useState(); + const columns = generateColumns([ + { + select: "", + baseSalary: 0, + totalCTC: 0, + takeHomeSalary: 0, + grossSalary: 0, + otherCompensations: 0, + salaryPeriod: "string", + job: { + role: "string", + company: { + name: "string", + }, + }, + }, + ]); + + const makeOffer = async (salaryId: string) => { + await postOnCampusOffer([ + { + salaryId: salaryId, + studentId: studentIds[0], + status: "ACCEPTED", + }, + ]); + toast.success("Made a successful offer!"); + window.location.reload(); + }; + + useEffect(() => { + const fetchSalaries = async () => { + const salaries = await getStudentSalaryOffers( + lastEvent.job.id, + studentIds[0] + ); + const newSalaries = salaries.map((salary) => ({ + select: ( + + ), + ...salary, + })); + setSalaries(newSalaries); + }; + fetchSalaries(); + }, []); + + return ( + +
+
+ + Select Salary + + {salaries ? ( +
+
+ + ) : ( +
+ +
+ )} + + + + ); +}; + +export const JobEvents = ({ events }: { events: [EventFC] }) => { + const [eventId, setEventId] = useState(null); + + const changeApplications = (eventId: string) => { + setEventId(eventId); + }; + + return ( +
+
+
+ + + + + + + + + + + {events.map((event, index) => ( + { + changeApplications(event.id); + }} + > + + + + + + + ))} + +
+ Round Number + + Type + + Meta Data + + Start Date + + End Date +
+ {event.roundNumber} + {event.type}{event.metadata} + {new Date(event.startDateTime).toLocaleString()} + + {new Date(event.endDateTime).toLocaleString()} +
+
+

Applications

+ {eventId ? ( +
+ +
+ ) : ( +
Select Event to view more
+ )} +
+ ); +}; + +export const Applications = ({ + eventId, + events, +}: { + eventId: string; + events: EventFC[]; +}) => { + const [applications, setApplications] = useState<[ApplicationFC]>(null); + var lastEvent: EventFC; + events.forEach((event) => { + if (lastEvent) { + if (lastEvent.roundNumber < event.roundNumber) { + lastEvent = event; + } + } else { + lastEvent = event; + } + }); + const [loading, setLoading] = useState(true); + const [promoteStudents, setPromoteStudents] = useState([]); + const [seed, setSeed] = useState(0); + + useEffect(() => { + setLoading(true); + setApplications(null); + const fetchData = async () => { + try { + const jsonData: EventFC = await fetchEventById(eventId); + setApplications(jsonData.applications); + } catch (error) { + toast.error("Some error occured"); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [eventId, seed]); + + return ( +
+ {lastEvent.id == eventId && promoteStudents.length > 0 ? ( + 0} + students={promoteStudents} + onClose={() => { + setPromoteStudents([]); + setSeed(seed + 1); + }} + events={events} + lastEvent={lastEvent} + /> + ) : ( + 0} + students={promoteStudents} + onClose={() => { + setPromoteStudents([]); + setSeed(seed + 1); + }} + events={events} + /> + )} + {loading && ( +
+ +
+ )} + {applications && ( + + + + + + + + + + + + {applications.map((application, index) => ( + + + + + + + + ))} + +
+ Roll Number + + Name + + Email + + Resume + + Promote +
+ {application.student.rollNo} + {application.student.user.name}{application.student.user.email} getResumeFile(application.resume.filepath)} + > + {application.resume.filepath}{" "} + {application.resume.verified && } + + {lastEvent.id == eventId ? ( + + ) : ( + + )} +
+ )} +
+ ); +}; diff --git a/src/components/Admin/types.tsx b/src/components/Admin/types.tsx new file mode 100644 index 00000000..c7188f6b --- /dev/null +++ b/src/components/Admin/types.tsx @@ -0,0 +1,27 @@ +export interface JobCoordinatorFC { + id: string; + role: string; + tpcMember: { + id: string; + department: string; + role: string; + user: { + id: string; + email: string; + name: string; + contact: string; + }; + }; +} + +export interface TPCMember { + id: string; + department: string; + role: string; + user: { + id: string; + name: string; + email: string; + contact: string; + }; +} diff --git a/src/components/NewTableComponent/NewJobModal.tsx b/src/components/NewTableComponent/NewJobModal.tsx new file mode 100644 index 00000000..9e02fa82 --- /dev/null +++ b/src/components/NewTableComponent/NewJobModal.tsx @@ -0,0 +1,789 @@ +"use client"; +import React, { useState, useEffect } from "react"; +import { JobDetailFC } from "@/helpers/recruiter/types"; +import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos"; +import PersonIcon from "@mui/icons-material/Person"; +import { Button } from "@/components/ui/button"; +import Link from "next/link"; +import { Separator } from "@/components/ui/separator"; +import { getJafDetails } from "@/helpers/recruiter/api"; +import { JAFdetailsFC } from "@/helpers/recruiter/types"; +import { patchJobData } from "@/helpers/recruiter/api"; +import { patchSalaryData } from "@/helpers/recruiter/api"; +import { + CategorySelectList, + GenderSelectList, +} from "@/components/Recruiters/jobEdit"; +import { fetchEachJob } from "@/helpers/api"; +import Cookies from "js-cookie"; +import Modal from "@mui/material/Modal"; +import { CircularProgress } from "@mui/material"; +import toast from "react-hot-toast"; + +const JobModal = ({ + jobID, + open, + setOpen, +}: { + jobID: string; + open: boolean; + setOpen: (open: boolean) => void; +}) => { + if (!open) { + return null; + } + + const [job, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [editMode, setEditMode] = useState(false); + const [formData, setFormData] = useState(null); + const [jafDetails, setJafDetails] = useState(); + const handleClose = () => setOpen(false); + + useEffect(() => { + const fetchData = async () => { + try { + const [jobDetailData, jafDetailsData] = await Promise.all([ + fetchEachJob(Cookies.get("accessToken"), jobID), + getJafDetails(), + ]); + + setJafDetails((prev) => jafDetailsData); + setData(jobDetailData); + setFormData(jobDetailData); + } catch (error) { + toast.error("Error Fetching data"); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [jobID]); + + const handleEditClick = () => { + if (editMode) { + handleSubmit(); + } + setEditMode(!editMode); + }; + + const handleChange = (e) => { + const { name, value } = e.target; + setFormData((prev) => ({ ...prev, [name]: value })); + }; + + const handleSubmit = async () => { + const c1 = await patchJobData(job.id, formData); + if (true) { + formData.salaries.map((salary, index) => patchSalaryData(salary)); + } + setEditMode(false); + window.location.reload(); + }; + + const addNewTest = () => { + const newTest = { type: "", duration: 0 }; + setFormData((prev) => ({ + ...prev, + selectionProcedure: { + ...prev.selectionProcedure, + tests: [...prev.selectionProcedure.tests, newTest], + }, + })); + }; + + const addNewInterview = () => { + const newInterview = { type: "", duration: 0 }; + setFormData((prev) => ({ + ...prev, + selectionProcedure: { + ...prev.selectionProcedure, + interviews: [...prev.selectionProcedure.interviews, newInterview], + }, + })); + }; + + return ( + +
+ {loading && ( +
+
+ +
+
+ )} + {job && ( +
+
+
+
+ Role + {editMode ? ( + + ) : ( + {job.role} + )} +
+
+ Activity + {job.active ? "Active" : "Inactive"} +
+
+ Current Status + {job.currentStatus} +
+
+ + + + {!job.active && ( + + )} +
+
+
+
Skills
+
+ {editMode ? ( + + ) : ( + job.skills + )} +
+
+
+
Location
{" "} + {editMode ? ( + + ) : ( +
{job.location}
+ )} +
+
+
+ Offer letter release +
{" "} + {editMode ? ( + + ) : ( +
+ {new Date(job.offerLetterReleaseDate).toLocaleString()} +
+ )} +
+
+
Joining Date
{" "} + {editMode ? ( + + ) : ( +
{new Date(job.joiningDate).toLocaleString()}
+ )} +
+
+
Duration
{" "} + {editMode ? ( + + ) : ( +
{job.duration}
+ )} +
+
+
Attachment
{" "} + {editMode ? ( + + ) : ( +
{job.attachment}
+ )} +
+
+
+
+ {job.selectionProcedure && ( +
+
+ Selection Procedure +
+ +
+
+
Selection mode
{" "} + {editMode ? ( + + ) : ( +
{job.selectionProcedure.selectionMode}
+ )} +
+
+
+ Shortlist from Resume +
{" "} + {editMode ? ( + { + setFormData((prev) => ({ + ...prev, + selectionProcedure: { + ...prev.selectionProcedure, + shortlistFromResume: e.target.checked, + }, + })); + }} + /> + ) : ( +
+ {job.selectionProcedure.shortlistFromResume + ? "YES" + : "NO"} +
+ )} +
+
+
Group Discussion
{" "} + {editMode ? ( + { + setFormData((prev) => ({ + ...prev, + selectionProcedure: { + ...prev.selectionProcedure, + groupDiscussion: e.target.checked, + }, + })); + }} + /> + ) : ( +
+ {job.selectionProcedure.groupDiscussion ? "YES" : "NO"} +
+ )} +
+
+
Number of members
{" "} + {editMode ? ( + { + setFormData((prev) => ({ + ...prev, + selectionProcedure: { + ...prev.selectionProcedure, + requirements: { + ...prev.selectionProcedure.requirements, + numberOfMembers: e.target.value, + }, + }, + })); + }} + /> + ) : ( +
+ {job.selectionProcedure.requirements.numberOfMembers} +
+ )} +
+
+ +
+
+
Tests
+
    + {editMode + ? formData.selectionProcedure.tests.map( + (test, index) => ( +
  • + Test Type: + +
    + Test Duration: + { + const updatedTests = + formData.selectionProcedure.tests.map( + (t, i) => + i === index + ? { + ...t, + duration: Number( + e.target.value + ), + } + : t + ); + setFormData((prev) => ({ + ...prev, + selectionProcedure: { + ...prev.selectionProcedure, + tests: updatedTests, + }, + })); + }} + /> +
  • + ) + ) + : job.selectionProcedure.tests.map((p, index) => ( +
  • + {Object.entries(p).map(([key, value]) => ( + + {key} : {value} +
    +
    + ))} +
  • + ))} +
+ {editMode && ( + + )} +
+ + + +
+
Interviews
+
    + {editMode + ? formData.selectionProcedure.interviews.map( + (interview, index) => ( +
  • + Interview Type: + +
    + Interview Duration: + { + const updatedInterviews = + formData.selectionProcedure.interviews.map( + (i, j) => + j === index + ? { ...i, duration: e.target.value } + : i + ); + setFormData((prev) => ({ + ...prev, + selectionProcedure: { + ...prev.selectionProcedure, + interviews: updatedInterviews, + }, + })); + }} + /> +
  • + ) + ) + : job.selectionProcedure.interviews.map((p, index) => ( +
  • + {Object.entries(p).map(([key, value]) => ( + + {key} : {value} +
    +
    + ))} +
  • + ))} +
+ {editMode && ( + + )} +
+
+
+
+
+ + Other requirements : + {" "} + {editMode ? ( + { + setFormData((prev) => ({ + ...prev, + selectionProcedure: { + ...prev.selectionProcedure, + requirements: { + ...prev.selectionProcedure.requirements, + otherRequirements: e.target.value, + }, + }, + })); + }} + /> + ) : ( +
+ { + job.selectionProcedure.requirements + .otherRequirements + } +
+ )} +
+
+
+
+ )} +
+
Job Coordinators
+
+ {job.jobCoordinators?.map((coordinator, index) => ( +
+
+ +
+ {coordinator.tpcMember.user.name} +
+
+
+ Role : + {coordinator.tpcMember.role} +
+
+ Department : + {coordinator.tpcMember.department} +
+
+ Email : + {coordinator.tpcMember.user.email} +
+
+ Contact : + {coordinator.tpcMember.user.contact} +
+
+
+ ))} +
+
+
+
Salaries
+ {job.salaries?.map((salary, salaryIndex) => ( +
+
+
+
Base Salary
{" "} + {editMode ? ( + { + const updatedSalaries = formData.salaries.map( + (s, i) => + i === salaryIndex + ? { ...s, baseSalary: e.target.value } + : s + ); + setFormData((prev) => ({ + ...prev, + salaries: updatedSalaries, + })); + }} + /> + ) : ( +
{salary.baseSalary}
+ )} +
+
+
CTC
{" "} + {editMode ? ( + { + const updatedSalaries = formData.salaries.map( + (s, i) => + i === salaryIndex + ? { ...s, totalCTC: e.target.value } + : s + ); + setFormData((prev) => ({ + ...prev, + salaries: updatedSalaries, + })); + }} + /> + ) : ( +
{salary.totalCTC}
+ )} +
+
+
Take Home Salary
{" "} + {editMode ? ( + { + const updatedSalaries = formData.salaries.map( + (s, i) => + i === salaryIndex + ? { ...s, takeHomeSalary: e.target.value } + : s + ); + setFormData((prev) => ({ + ...prev, + salaries: updatedSalaries, + })); + }} + /> + ) : ( +
{salary.takeHomeSalary}
+ )} +
+
+
Gross Salary
{" "} + {editMode ? ( + { + const updatedSalaries = formData.salaries.map( + (s, i) => + i === salaryIndex + ? { ...s, grossSalary: e.target.value } + : s + ); + setFormData((prev) => ({ + ...prev, + salaries: updatedSalaries, + })); + }} + /> + ) : ( +
{salary.grossSalary}
+ )} +
+
+
+ Other Compensations +
{" "} + {editMode ? ( + { + const updatedSalaries = formData.salaries.map( + (s, i) => + i === salaryIndex + ? { ...s, otherCompensations: e.target.value } + : s + ); + setFormData((prev) => ({ + ...prev, + salaries: updatedSalaries, + })); + }} + /> + ) : ( +
{salary.otherCompensations}
+ )} +
+
+ {/* Genders */} +
+

Genders

+ {editMode ? ( + + ) : ( +
+ {salary.genders?.map((gender, genderIndex) => ( +
+ {editMode ? null : ( +
+ {gender} +
+ )} +
+ ))} +
+ )} +
+ + {/* Categories */} +
+

Categories

+ {editMode ? ( + + ) : ( +
+ {salary.categories?.map((category, categoryIndex) => ( +
+ {editMode ? null : ( +
+ {category} +
+ )} +
+ ))} +
+ )} +
+ + +
+ ))} +
+
+ )} +
+
+ ); +}; + +export default JobModal; diff --git a/src/components/NewTableComponent/Table.jsx b/src/components/NewTableComponent/Table.jsx index f30eb6dd..6b4decfa 100644 --- a/src/components/NewTableComponent/Table.jsx +++ b/src/components/NewTableComponent/Table.jsx @@ -1,158 +1,164 @@ -'use client' +"use client"; import { - MaterialReactTable, - useMaterialReactTable, - createMRTColumnHelper, -} from 'material-react-table'; -import { useState } from 'react'; -import FileDownloadIcon from '@mui/icons-material/FileDownload'; -import { mkConfig, generateCsv, download } from 'export-to-csv'; -import { Box, Button } from '@mui/material'; -import { MenuItem } from '@mui/material'; -import StudentModal from './StudentModal'; -import JobModal from './JobModal'; -import PenaltyModal from './PenaltyModal'; -import RecruiterModal from './RecruiterModal' + MaterialReactTable, + useMaterialReactTable, + createMRTColumnHelper, +} from "material-react-table"; +import { useState } from "react"; +import FileDownloadIcon from "@mui/icons-material/FileDownload"; +import { mkConfig, generateCsv, download } from "export-to-csv"; +import { Box, Button } from "@mui/material"; +import { MenuItem } from "@mui/material"; +import StudentModal from "./StudentModal"; +import JobModal from "./NewJobModal"; +import PenaltyModal from "./PenaltyModal"; +import RecruiterModal from "./RecruiterModal"; +import Link from "next/link"; const csvConfig = mkConfig({ - fieldSeparator: ',', - decimalSeparator: '.', - useKeysAsHeaders: true, + fieldSeparator: ",", + decimalSeparator: ".", + useKeysAsHeaders: true, }); const Table = ({ data, columns, type }) => { - const [validationErrors, setValidationErrors] = useState({}); - const flattenObject = (obj, prefix = '') => { - return Object.keys(obj).reduce((acc, key) => { - const pre = prefix.length ? prefix + ' ' : ''; - if (typeof obj[key] === 'object' && obj[key] !== null) { - Object.assign(acc, flattenObject(obj[key], pre + key)); - } else { - acc[pre + key] = obj[key]; - } - return acc; - }, {}); - }; - const flattenData = (data) => { - return data.map(item => flattenObject(item)); - }; - const handleExportRows = (rows) => { - const rowData = rows.map((row) => row.original); - console.log(rowData) - const csv = generateCsv(csvConfig)(flattenData(rowData)); - download(csvConfig)(csv); - }; + const [validationErrors, setValidationErrors] = useState({}); + const flattenObject = (obj, prefix = "") => { + return Object.keys(obj).reduce((acc, key) => { + const pre = prefix.length ? prefix + " " : ""; + if (typeof obj[key] === "object" && obj[key] !== null) { + Object.assign(acc, flattenObject(obj[key], pre + key)); + } else { + acc[pre + key] = obj[key]; + } + return acc; + }, {}); + }; + const flattenData = (data) => { + return data.map((item) => flattenObject(item)); + }; + const handleExportRows = (rows) => { + const rowData = rows.map((row) => row.original); + console.log(rowData); + const csv = generateCsv(csvConfig)(flattenData(rowData)); + download(csvConfig)(csv); + }; - const handleExportData = () => { - console.log(flattenData) - const csv = generateCsv(csvConfig)(flattenData(data)); - download(csvConfig)(csv); - }; + const handleExportData = () => { + console.log(flattenData); + const csv = generateCsv(csvConfig)(flattenData(data)); + download(csvConfig)(csv); + }; - const [isModalOpen, setIsModalOpen] = useState(false); - const [open, setOpen] = useState(false); - const [id, setId] = useState(null); + const [isModalOpen, setIsModalOpen] = useState(false); + const [open, setOpen] = useState(false); + const [id, setId] = useState(null); - const handleOpenModal = () => setIsModalOpen(true); - const handleCloseModal = () => setIsModalOpen(false); + const handleOpenModal = () => setIsModalOpen(true); + const handleCloseModal = () => setIsModalOpen(false); - const table = useMaterialReactTable({ - columns, - data, - createDisplayMode: 'modal', - editDisplayMode: 'modal', - enableRowActions: true, - enableRowSelection: true, - columnFilterDisplayMode: 'popover', - paginationDisplayMode: 'pages', - enableGlobalFilterModes: true, - positionToolbarAlertBanner: 'bottom', - renderRowActionMenuItems: ({ row, table }) => ( - - {type === 'student' && ( - - Add Penalty - - )} - { - setId(row.original.id); - setOpen(true) - }}> - View {type} - - - ), - renderTopToolbarCustomActions: ({ table }) => ( - - - - - - - ), - renderEditRowDialogContent: ({ table, row, internalEditComponents }) => ( -
-
Penalty
- - -
- ), - }); + const table = useMaterialReactTable({ + columns, + data, + createDisplayMode: "modal", + editDisplayMode: "modal", + enableRowActions: true, + enableRowSelection: true, + columnFilterDisplayMode: "popover", + paginationDisplayMode: "pages", + enableGlobalFilterModes: true, + positionToolbarAlertBanner: "bottom", + renderRowActionMenuItems: ({ row, table }) => ( + + {type === "student" && ( + + Add Penalty + + )} + {type == "job" ? ( + + View {type} + + ) : ( + { + setId(row.original.id); + setOpen(true); + }} + > + View {type} + + )} + + ), + renderTopToolbarCustomActions: ({ table }) => ( + + + + + + + ), + renderEditRowDialogContent: ({ table, row, internalEditComponents }) => ( +
+
Penalty
+ + +
+ ), + }); - return ( - <> - {type === 'student' && ( - - )}{type == "job" && ( - - )} - {type == "recruiter" && ( - - )} -
- -
- {type === 'student' && ( - - )} - - ); + return ( + <> + {type === "student" && ( + + )} + {/* {type == "job" && } */} + {type == "recruiter" && ( + + )} +
+ +
+ {type === "student" && ( + + )} + + ); }; -export default Table; \ No newline at end of file +export default Table; diff --git a/src/components/Recruiters/jobEdit.tsx b/src/components/Recruiters/jobEdit.tsx index 7fb05a15..75b49f70 100644 --- a/src/components/Recruiters/jobEdit.tsx +++ b/src/components/Recruiters/jobEdit.tsx @@ -15,7 +15,7 @@ export const CategorySelectList = ({ ...formData.salaries[salaryIndex], categories: values, }; - const updatedSalaries = formData.salaries.map((salary, i) => { + const updatedSalaries = formData.salaries?.map((salary, i) => { if (i === salaryIndex) { return updatedSalary; } @@ -35,7 +35,7 @@ export const CategorySelectList = ({ return ( ({ + value={formData.salaries[salaryIndex].genders?.map((gender) => ({ value: gender, label: gender, }))} diff --git a/src/dto/SalaryDto.ts b/src/dto/SalaryDto.ts new file mode 100644 index 00000000..06b5d22d --- /dev/null +++ b/src/dto/SalaryDto.ts @@ -0,0 +1,38 @@ +const salaryDto = { + id: "string", + baseSalary: 0, + totalCTC: 0, + takeHomeSalary: 0, + grossSalary: 0, + otherCompensations: 0, + salaryPeriod: "string", + job: { + id: "string", + role: "string", + company: { + id: "string", + name: "string", + }, + season: { + id: "string", + year: "string", + type: "INTERN", + }, + salaries: [ + { + id: "string", + totalCTC: 0, + salaryPeriod: "string", + genders: ["MALE"], + programs: ["string"], + facultyApprovals: ["Astronomy, Astrophysics and Space Engineering"], + categories: ["GENERAL"], + minCPI: 0, + tenthMarks: 0, + twelthMarks: 0, + }, + ], + }, +}; + +export { salaryDto }; diff --git a/src/helpers/api.ts b/src/helpers/api.ts index 407468c6..c0dcd04d 100644 --- a/src/helpers/api.ts +++ b/src/helpers/api.ts @@ -286,25 +286,38 @@ export const fetchEachJob = async ( return json; }; -export const fetchJobEvents = async ( - accessToken: string | undefined, - jobId: any -) => { - if (!accessToken || accessToken === undefined) { - redirect(); - return; - } - const res = await fetch(`${url("/jobs")}/${jobId}/events`, { - next: { - tags: ["AllEvents"], - }, - headers: { - Authorization: `Bearer ${accessToken}`, +export const fetchJobEvents = async (jobId: any) => { + return apiCall(`/events`, { + queryParam: { + q: { + filterBy: { + job: { + id: { + eq: [jobId], + }, + }, + }, + }, }, }); +}; - const json = await res.json(); - return json; +export const fetchEventById = async (eventId: any) => { + return apiCall(`/events/${eventId}`); +}; + +export const addEvent = async (body: any) => { + return apiCall(`/events`, { + method: "POST", + body: body, + }); +}; + +export const promoteStudent = async (body: any, eventId: string) => { + return apiCall(`/events/${eventId}`, { + method: "PATCH", + body: body, + }); }; export const fetchRecruiterData = async ( @@ -315,7 +328,6 @@ export const fetchRecruiterData = async ( redirect(); return; } - console.log("filter", filter); const res = await fetch( filter ? url(`/recruiters?${filter}`) : url("/recruiters"), { @@ -343,3 +355,40 @@ export const patchResumeVerify = async (changes: ResumePatchData[]) => { body: changes, }); }; + +export const getStudentSalaryOffers = async ( + jobId: string, + studentId: string +) => { + return apiCall(`/on-campus-offers/salaries/${jobId}/student/${studentId}`); +}; + +export const postOnCampusOffer = async ( + body: { + salaryId: string; + studentId: string; + status: string; + }[] +) => { + return apiCall(`/on-campus-offers/`, { + method: "POST", + body: body, + }); +}; + +export const fetchTpcMembers = async () => { + return apiCall(`/tpc-members`); +}; + +export const postJobCoordinator = async ( + body: { + jobId: string; + tpcMemberId: string; + role: string; + }[] +) => { + return apiCall(`/jobs/coordinators`, { + method: "POST", + body: body, + }); +}; diff --git a/src/helpers/recruiter/types.ts b/src/helpers/recruiter/types.ts index 96123561..4ca85ad7 100644 --- a/src/helpers/recruiter/types.ts +++ b/src/helpers/recruiter/types.ts @@ -32,6 +32,7 @@ export interface EventFC { startDateTime: string; endDateTime: string; applications?: [ApplicationFC]; + job?: JobDetailFC; } export interface JAFdetailsFC { diff --git a/src/middleware.ts b/src/middleware.ts index dd0a0af3..1f342243 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -3,7 +3,14 @@ import type { NextRequest } from "next/server"; -const adminRoutes = ["/admin/company", "/admin/students", "/admin/job"]; +const adminRoutes = [ + "/admin/company", + "/admin/students", + "/admin/job", + /^\/admin\/jobs\/events\/[a-zA-Z0-9\-]+$/ +]; + + const studentRoutes = [ "/student/jobs", "/student/offCampus", @@ -11,8 +18,19 @@ const studentRoutes = [ "/student/interviewExperiences", "/student/profile", "/student/resumes", + /^\/student\/job\/[a-zA-Z0-9\-]+$/, + /^\/student\/job\/salary\/[a-zA-Z0-9\-]+$/ ]; -const recruiterRoutes = ["/recruiter/jaf", "/recruiter/prevjaf"]; + +const recruiterRoutes = [ + "/recruiter", + "/recruiter/jobs", + "/recruiter/events", + "/recruiter/profile", + /^\/recruiter\/jobs\/[a-zA-Z0-9\-]+$/, + /^\/recruiter\/events\/[a-zA-Z0-9\-]+$/ +]; + const facultyRoutes = ["/faculty", "/faculty/profile"]; export function middleware(request: NextRequest) { @@ -37,20 +55,28 @@ export function middleware(request: NextRequest) { if ( user?.role !== "ADMIN" && - adminRoutes.includes(request.nextUrl.pathname) + adminRoutes.some(route => + typeof route === "string" + ? request.nextUrl.pathname === route + : route.test(request.nextUrl.pathname)) ) { return NextResponse.redirect(new URL("/login", request.url)); } if ( user?.role !== "STUDENT" && - studentRoutes.includes(request.nextUrl.pathname) + studentRoutes.some(route => + typeof route === "string" + ? request.nextUrl.pathname === route + : route.test(request.nextUrl.pathname)) ) { return NextResponse.redirect(new URL("/login", request.url)); } if ( user?.role !== "RECRUITER" && - recruiterRoutes.includes(request.nextUrl.pathname) && - request.url.includes("/recruiter") + recruiterRoutes.some(route => + typeof route === "string" + ? request.nextUrl.pathname === route + : route.test(request.nextUrl.pathname)) ) { return NextResponse.redirect(new URL("/login", request.url)); }