diff --git a/src/Component/ui/Url.tsx b/src/Component/ui/Url.tsx index e120bd8..9c3c134 100644 --- a/src/Component/ui/Url.tsx +++ b/src/Component/ui/Url.tsx @@ -22,8 +22,8 @@ const Url: React.FC = ({ style, ariaLabel = "URL display", }) => { - const valid = isValidDomain(domain); - const [Domain, setDomain] = React.useState(domain); + const valid = isValidDomain(domain || ""); + const [domainState, setDomainState] = React.useState(domain || ""); return (
@@ -39,8 +39,8 @@ const Url: React.FC = ({
setDomain(e.target.value)} + value={domainState} + onChange={(e) => setDomainState(e.target.value)} className="Domain p-2 rounded-r-lg border w-[80%]" style={{ color: "var(--cd-text)", diff --git a/src/features/AddMember/v1/Component/Administrative_MetaData.tsx b/src/features/AddMember/v1/Component/Administrative_MetaData.tsx index 36ccf86..a11e8df 100644 --- a/src/features/AddMember/v1/Component/Administrative_MetaData.tsx +++ b/src/features/AddMember/v1/Component/Administrative_MetaData.tsx @@ -1,5 +1,4 @@ import MemberShip_Status from "./MemberShip_Status"; -import AccessLevel from "./AccessLevel"; const Administrative_MetaData = () => { return ( diff --git a/src/features/AddMember/v1/Component/MemberShip_Status.tsx b/src/features/AddMember/v1/Component/MemberShip_Status.tsx index 6d720a9..8e7a573 100644 --- a/src/features/AddMember/v1/Component/MemberShip_Status.tsx +++ b/src/features/AddMember/v1/Component/MemberShip_Status.tsx @@ -1,4 +1,3 @@ -import { useState } from "react"; import { useFormContext } from "react-hook-form"; import type { MemberFormValues } from "../Validator/AddMember.Validator"; diff --git a/src/features/AddMember/v1/Sections/ProfessionalDetails.tsx b/src/features/AddMember/v1/Sections/ProfessionalDetails.tsx index a85d20f..6d16a1b 100644 --- a/src/features/AddMember/v1/Sections/ProfessionalDetails.tsx +++ b/src/features/AddMember/v1/Sections/ProfessionalDetails.tsx @@ -3,7 +3,6 @@ import { IoBag } from "react-icons/io5"; import { Input } from "../../../../Component/ui/Input"; import DropDown from "../../../../Component/ui/DropDown"; import { Roles } from "../Constant/Role.constant"; -import { SkillColor } from "../Constant/Skill.constant"; import { useFormContext } from "react-hook-form"; import type { MemberFormValues } from "../Validator/AddMember.Validator"; import { theme } from "@/theme"; @@ -27,11 +26,6 @@ const ProfessionalDetails = () => { } }; - const getSkillColor = (skill: string) => { - const index = skills.indexOf(skill); - return SkillColor[index % SkillColor.length]; - }; - return (
new Promise((resolve) => setTimeout(resolve, ms)); + +// ========================================== +// COMPANY PROFILE HOOKS +// ========================================== + +export function useCompanyProfile() { + const profile = useHiringStore((state) => state.companyProfile); + return useQuery({ + queryKey: ["hiring-company-profile"], + queryFn: async () => { + await delay(300); + return profile; + }, + }); +} + +export function useUpdateCompanyProfile() { + const queryClient = useQueryClient(); + const updateProfile = useHiringStore((state) => state.updateCompanyProfile); + + return useMutation({ + mutationFn: async (patch: Parameters[0]) => { + await delay(500); + updateProfile(patch); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["hiring-company-profile"] }); + }, + }); +} + +// ========================================== +// JOBS HOOKS +// ========================================== + +export function useJobs(filters?: { search?: string; department?: string; status?: string }) { + const jobs = useHiringStore((state) => state.jobs); + + return useQuery({ + queryKey: ["hiring-jobs", filters], + queryFn: async () => { + await delay(400); + + return jobs.filter((job) => { + if (filters?.search) { + const q = filters.search.toLowerCase(); + const matchTitle = job.title.toLowerCase().includes(q); + const matchDept = job.department.toLowerCase().includes(q); + const matchManager = job.hiringManager.toLowerCase().includes(q); + const matchId = job.id.toLowerCase().includes(q); + if (!matchTitle && !matchDept && !matchManager && !matchId) return false; + } + + if (filters?.department && filters.department !== "all") { + if (job.department.toLowerCase() !== filters.department.toLowerCase()) return false; + } + + if (filters?.status && filters.status !== "all") { + if (job.status.toLowerCase() !== filters.status.toLowerCase()) return false; + } + + return true; + }); + }, + }); +} + +export function useJobDetail(id: string | undefined) { + const jobs = useHiringStore((state) => state.jobs); + + return useQuery({ + queryKey: ["hiring-job", id], + queryFn: async () => { + await delay(300); + const job = jobs.find((j) => j.id === id); + if (!job) throw new Error("Job not found"); + return job; + }, + enabled: !!id, + }); +} + +export function useCreateJob() { + const queryClient = useQueryClient(); + const addJob = useHiringStore((state) => state.addJob); + + return useMutation({ + mutationFn: async (payload: Parameters[0]) => { + await delay(500); + return addJob(payload); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["hiring-jobs"] }); + }, + }); +} + +export function useUpdateJob() { + const queryClient = useQueryClient(); + const updateJob = useHiringStore((state) => state.updateJob); + + return useMutation({ + mutationFn: async ({ id, patch }: { id: string; patch: Parameters[1] }) => { + await delay(400); + updateJob(id, patch); + }, + onSuccess: (_, variables) => { + queryClient.invalidateQueries({ queryKey: ["hiring-jobs"] }); + queryClient.invalidateQueries({ queryKey: ["hiring-job", variables.id] }); + }, + }); +} + +export function useDeleteJob() { + const queryClient = useQueryClient(); + const deleteJob = useHiringStore((state) => state.deleteJob); + + return useMutation({ + mutationFn: async (id: string) => { + await delay(400); + deleteJob(id); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["hiring-jobs"] }); + }, + }); +} + +// ========================================== +// APPLICANTS HOOKS +// ========================================== + +export function useApplicants(jobId: string | undefined) { + const applicants = useHiringStore((state) => state.applicants); + + return useQuery({ + queryKey: ["hiring-applicants", jobId], + queryFn: async () => { + await delay(400); + return applicants.filter((a) => a.jobId === jobId); + }, + enabled: !!jobId, + }); +} + +export function useApplicantDetail(id: string | undefined) { + const applicants = useHiringStore((state) => state.applicants); + + return useQuery({ + queryKey: ["hiring-applicant", id], + queryFn: async () => { + await delay(300); + const app = applicants.find((a) => a.id === id); + if (!app) throw new Error("Applicant not found"); + return app; + }, + enabled: !!id, + }); +} + +export function useApplyToJob() { + const queryClient = useQueryClient(); + const applyToJob = useHiringStore((state) => state.applyToJob); + + return useMutation({ + mutationFn: async ({ jobId, payload }: { jobId: string; payload: Parameters[1] }) => { + await delay(600); + return applyToJob(jobId, payload); + }, + onSuccess: (_, variables) => { + queryClient.invalidateQueries({ queryKey: ["hiring-applicants", variables.jobId] }); + queryClient.invalidateQueries({ queryKey: ["hiring-jobs"] }); + }, + }); +} + +export function useUpdateApplicantStatus() { + const queryClient = useQueryClient(); + const updateStatus = useHiringStore((state) => state.updateApplicantStatus); + + return useMutation({ + mutationFn: async ({ id, status, user }: { id: string; status: ApplicantStatus; user: string }) => { + await delay(400); + updateStatus(id, status, user); + }, + onSuccess: (_, variables) => { + queryClient.invalidateQueries({ queryKey: ["hiring-applicant", variables.id] }); + queryClient.invalidateQueries({ queryKey: ["hiring-applicants"] }); + }, + }); +} + +export function useAddRecruiterNote() { + const queryClient = useQueryClient(); + const addNote = useHiringStore((state) => state.addRecruiterNote); + + return useMutation({ + mutationFn: async ({ applicantId, note }: { applicantId: string; note: Parameters[1] }) => { + await delay(300); + addNote(applicantId, note); + }, + onSuccess: (_, variables) => { + queryClient.invalidateQueries({ queryKey: ["hiring-applicant", variables.applicantId] }); + }, + }); +} + +// ========================================== +// MAIL HOOKS +// ========================================== + +export function useMailLogs(applicantId: string | undefined) { + const mailLogs = useHiringStore((state) => state.mailLogs); + + return useQuery({ + queryKey: ["hiring-mail-logs", applicantId], + queryFn: async () => { + await delay(300); + return mailLogs.filter((m) => m.applicantId === applicantId); + }, + enabled: !!applicantId, + }); +} + +export function useSendMail() { + const queryClient = useQueryClient(); + const sendMail = useHiringStore((state) => state.sendMail); + + return useMutation({ + mutationFn: async ({ applicantId, mail }: { applicantId: string; mail: Parameters[1] }) => { + await delay(500); + sendMail(applicantId, mail); + }, + onSuccess: (_, variables) => { + queryClient.invalidateQueries({ queryKey: ["hiring-mail-logs", variables.applicantId] }); + queryClient.invalidateQueries({ queryKey: ["hiring-applicant", variables.applicantId] }); + }, + }); +} + +// ========================================== +// AUDIT LOGS HOOKS +// ========================================== + +export function useAuditLogs(jobId: string | undefined) { + const auditLogs = useHiringStore((state) => state.auditLogs); + + return useQuery({ + queryKey: ["hiring-audit-logs", jobId], + queryFn: async () => { + await delay(300); + return auditLogs.filter((a) => a.jobId === jobId); + }, + enabled: !!jobId, + }); +} + +// ========================================== +// MODERATION HOOKS +// ========================================== + +export function useModerationJobs() { + const jobs = useHiringStore((state) => state.jobs); + + return useQuery({ + queryKey: ["hiring-moderation-jobs"], + queryFn: async () => { + await delay(400); + // Moderation queue has pending status jobs (e.g. draft, paused, or we can seed a specific pending job, or just filter jobs with 'draft' or 'paused') + return jobs; + }, + }); +} + +export function useModerateJob() { + const queryClient = useQueryClient(); + const moderateJob = useHiringStore((state) => state.moderateJob); + + return useMutation({ + mutationFn: async ({ jobId, action, user }: { jobId: string; action: "approve" | "reject"; user: string }) => { + await delay(500); + moderateJob(jobId, action, user); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["hiring-jobs"] }); + queryClient.invalidateQueries({ queryKey: ["hiring-moderation-jobs"] }); + }, + }); +} diff --git a/src/features/Hiring/v1/Pages/ApplicantDetailsPage.tsx b/src/features/Hiring/v1/Pages/ApplicantDetailsPage.tsx new file mode 100644 index 0000000..e42c0cb --- /dev/null +++ b/src/features/Hiring/v1/Pages/ApplicantDetailsPage.tsx @@ -0,0 +1,420 @@ +import React, { useState } from "react"; +import { useTheme } from "@/theme"; +import { useNavigate, useParams } from "react-router-dom"; +import { useApplicantDetail, useUpdateApplicantStatus, useAddRecruiterNote, useJobDetail } from "../Hooks/useHiring"; +import { ApplicantStatus } from "../Types/Hiring.types"; +import Button from "@/Component/ui/Button"; +import { FiCalendar, FiMail, FiArrowLeft, FiPlus, FiChevronRight } from "react-icons/fi"; +import { MdCheckCircle } from "react-icons/md"; + +const ApplicantDetailsPage = () => { + const { theme } = useTheme(); + const navigate = useNavigate(); + const { id: jobId, applicantId } = useParams<{ id: string; applicantId: string }>(); + + // Fetch applicant + const { data: applicant, isLoading } = useApplicantDetail(applicantId); + const { data: job } = useJobDetail(jobId); + const updateStatusMutation = useUpdateApplicantStatus(); + const addNoteMutation = useAddRecruiterNote(); + + // Note composition state + const [noteContent, setNoteContent] = useState(""); + const [noteRec, setNoteRec] = useState<"strong_hire" | "hire" | "neutral" | "no_hire">("hire"); + const [showNoteForm, setShowNoteForm] = useState(false); + + if (isLoading) { + return ( +
+
+ Loading candidate profile file... +
+
+ ); + } + + if (!applicant) { + return ( +
+ Candidate file not found. +
+ ); + } + + const handleMoveStage = (status: ApplicantStatus) => { + updateStatusMutation.mutate({ id: applicant.id, status, user: "Sarah Jenkins" }); + }; + + const handleAddNote = (e: React.FormEvent) => { + e.preventDefault(); + if (!noteContent.trim()) return; + + addNoteMutation.mutate( + { + applicantId: applicant.id, + note: { + author: "Sarah Jenkins", + content: noteContent, + recommendation: noteRec, + avatar: "/avatars/sarah.jpg", + }, + }, + { + onSuccess: () => { + setNoteContent(""); + setShowNoteForm(false); + }, + } + ); + }; + + const getNextStage = (status: ApplicantStatus): ApplicantStatus => { + const sequence: ApplicantStatus[] = ["applied", "screening", "technical", "interview", "hr", "offer", "hired"]; + const idx = sequence.indexOf(status); + if (idx !== -1 && idx < sequence.length - 1) return sequence[idx + 1]; + return status; + }; + + const nextStage = getNextStage(applicant.status); + + return ( +
+
+ {/* Top bar */} +
+
+ +
+
+

+ {applicant.name} +

+ + {applicant.matchScore}% MATCH + +
+ + Applied for {job?.title || "Position"} • Applied {applicant.appliedDate} + +
+
+ +
+
+
+ + {/* Layout Grid */} +
+ {/* LEFT: CANDIDATE INFO & METRIC */} +
+ {/* Profile Summary Card */} +
+
+ {applicant.name[0]} +
+
+

{applicant.name}

+ San Francisco, CA +
+ +
+
+ Experience + {applicant.experience} +
+
+ Education + RISD BFA +
+
+
+ + {/* Evaluation Score Card */} +
+
+

Evaluation Matrix

+
+
+ Technical Fit + {applicant.rating.technical || "Pending"}/10 +
+
+ Culture Fit + {applicant.rating.culture || "Pending"}/10 +
+
+ Communication + {applicant.rating.communication || "Pending"}/10 +
+
+
+ +
+ Rating + {applicant.rating.rating} +
+
+ + {/* Skills & tags */} +
+

Skills Tags

+
+ {applicant.skills.map((skill) => ( + + {skill} + + ))} +
+
+
+ + {/* MIDDLE: TIMELINES & NOTES */} +
+ {/* Reviewer Feedback Panel */} +
+
+

+ Internal Reviewer Feedback +

+ +
+ + {/* Note Submission Form */} + {showNoteForm && ( +
+