Skip to content
25 changes: 20 additions & 5 deletions src/app/admin/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ export default function AdminJobs() {
const [expiredJobData, setExpiredJobData] = useState<null | IJob[]>(null);

const setExpiredJobs = async (jobs: IJob[]) => {
if (!Array.isArray(jobs)) {
console.error("Expected jobs to be an array but received:", typeof jobs);
return;
}
console.log("Setting Expired");
const jobsToExpire = jobs.filter((job) => job.jobStatus !== "expired" && isMoreThanThirtyDaysAgo(job.approvedDate));

for (const job of jobsToExpire) {
Expand All @@ -29,16 +34,28 @@ export default function AdminJobs() {

const fetchData = async () => {
try {
const response = await fetch("/api/jobs");
const response = await fetch("/api/jobs?jobStatus=approved&admin=true");
if (!response.ok) {
console.error("Failed to fetch jobs:", response.status, response.statusText);
return;
}
const result: IJob[] = await response.json();
//const result: IJob[] = [];

// Check and update expired jobs
await setExpiredJobs(result);

// Fetch all job statuses in parallel
const jobStatuses = ["pending", "approved", "rejected", "expired"];
const responses = await Promise.all(jobStatuses.map((status) => fetch(`/api/jobs?jobStatus=${status}`)));
const responses = await Promise.all(
jobStatuses.map((status) => fetch(`/api/jobs?jobStatus=${status}&admin=true`)),
);

// Check if any of the parallel requests failed
const failedResponses = responses.filter((res) => !res.ok);
if (failedResponses.length > 0) {
console.error("Some job status requests failed:", failedResponses);
return;
}

const [incomingData, liveData, completeData, expiredData] = await Promise.all(responses.map((res) => res.json()));

Expand Down Expand Up @@ -114,7 +131,6 @@ export default function AdminJobs() {
throw new Error("Failed to update job status");
}

// Show toast notification based on status
if (status === "approved") {
toast({
title: "Job Approved",
Expand Down Expand Up @@ -162,7 +178,6 @@ export default function AdminJobs() {
switch (status) {
case "approved":
setLiveJobData((prev) => (prev ? [...prev, updatedJob] : [updatedJob]));
setExpiredJobData((prev) => (prev ? prev.filter((job) => job._id !== jobId) : []));
break;
case "rejected":
setCompleteJobData((prev) => (prev ? [...prev, updatedJob] : [updatedJob]));
Expand Down
8 changes: 4 additions & 4 deletions src/app/api/__test__/AdminPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,22 +125,22 @@ describe("Admin Jobs Page", () => {
ok: true,
json: async () => fakeJobs,
});
} else if (url === "/api/jobs?jobStatus=pending") {
} else if (url === "/api/jobs?jobStatus=pending&admin=true") {
return Promise.resolve({
ok: true,
json: async () => pendingJobs,
});
} else if (url === "/api/jobs?jobStatus=approved") {
} else if (url === "/api/jobs?jobStatus=approved&admin=true") {
return Promise.resolve({
ok: true,
json: async () => approvedJobs,
});
} else if (url === "/api/jobs?jobStatus=rejected") {
} else if (url === "/api/jobs?jobStatus=rejected&admin=true") {
return Promise.resolve({
ok: true,
json: async () => rejectedJobs,
});
} else if (url === "/api/jobs?jobStatus=expired") {
} else if (url === "/api/jobs?jobStatus=expired&admin=true") {
return Promise.resolve({
ok: true,
json: async () => expiredJobs,
Expand Down
21 changes: 18 additions & 3 deletions src/app/api/jobs/[jobId]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const DELETE = withApiAuth(
}

//Deleting based on _id
const result = await Job.findByIdAndDelete(jobId);
await Job.findByIdAndDelete(jobId);

return NextResponse.json({ message: "Deleted successfully" }, { status: 200 });
} catch (error) {
Expand All @@ -49,7 +49,7 @@ export const PUT = withApiAuth(
const jobId = req.nextUrl.pathname.split("/").pop();

const requestData = await req.json();
const { previousStatus, newStatus, ...jobData } = requestData;
const { previousStatus, newStatus, isRenewal, ...jobData } = requestData;

// Handle both cases - with and without status transition data
const hasStatusTransition = previousStatus !== undefined && newStatus !== undefined;
Expand All @@ -76,6 +76,21 @@ export const PUT = withApiAuth(
return NextResponse.json({ error: "Insufficient permissions to update this job" }, { status: 403 });
}

// Handle job renewal
if (isRenewal) {
const updatedJob = await Job.findByIdAndUpdate(
jobId,
{
jobStatus: JobStatus.approved,
postDate: new Date(),
approvedDate: new Date(),
modifiedDate: new Date(),
},
{ new: true },
);
return NextResponse.json({ message: "Job renewed successfully", job: updatedJob });
}

const updatedJob = {
...jobData,
jobStatus: hasStatusTransition
Expand All @@ -92,7 +107,7 @@ export const PUT = withApiAuth(

await Job.findByIdAndUpdate(jobId, updatedJob, { new: true });
return NextResponse.json({ message: "Job updated successfully" });
} catch (error: any) {
} catch (error) {
console.error("PUT Error:", error);
return NextResponse.json({ message: "Error updating job: ", error }, { status: 500 });
}
Expand Down
11 changes: 7 additions & 4 deletions src/app/api/jobs/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,12 @@ export const GET = withApiAuth(
await connectDB();

const { searchParams } = new URL(req.url);
const page = Math.max(parseInt(searchParams.get("page") || "1", 10), 1);
const limit = Math.max(parseInt(searchParams.get("limit") || "10", 10), 1);
const skip = (page - 1) * limit; // This will always be >= 0
const isAdminRequest = searchParams.get("admin") === "true";

// Only apply pagination for non-admin requests
const page = isAdminRequest ? 1 : Math.max(parseInt(searchParams.get("page") || "1", 10), 1);
const limit = isAdminRequest ? 0 : Math.max(parseInt(searchParams.get("limit") || "10", 10), 1);
const skip = isAdminRequest ? 0 : (page - 1) * limit; // This will always be >= 0

// Filter parameters
const employmentFilters = searchParams.getAll("employmentType");
Expand Down Expand Up @@ -53,7 +56,7 @@ export const GET = withApiAuth(
};

// Filter out expired jobs that are already approved
if (statusFilter == "approved") {
if (statusFilter == "approved" && !isAdminRequest) {
filter.approvedDate = {
$gte: getThirtyDaysAgo(),
};
Expand Down
12 changes: 11 additions & 1 deletion src/app/dashboard/DashboardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@ export default function DashboardPage({ organizationName }: DashboardProps) {
}
}, [user, isLoaded]);

const handleJobRenewed = (renewedJob: IJob) => {
setUserJobs((prevJobs) => {
// Update the job in the array
const updatedJobs = prevJobs.map((job) => (job._id === renewedJob._id ? renewedJob : job));
return updatedJobs;
});
};

if (loading) {
return (
<div className="w-full">
Expand Down Expand Up @@ -136,7 +144,9 @@ export default function DashboardPage({ organizationName }: DashboardProps) {
<div className="text-2xl font-semibold mb-4">Expired Applications</div>
<div className="flex flex-col gap-4">
{expiredJobs.length > 0 ? (
expiredJobs.map((job, index) => <OrgCard key={index} job={job} types={["expired"]} />)
expiredJobs.map((job, index) => (
<OrgCard key={index} job={job} types={["expired"]} onJobRenewed={handleJobRenewed} />
))
) : (
<div className="py-4 px-5 rounded-md bg-[#f7f7f7] text-gray-500">
No expired applications available
Expand Down
38 changes: 38 additions & 0 deletions src/components/JobCard/AdminCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import JobCardModal from "./JobCardModal";
import { useRouter } from "next/navigation";
import RejectButton from "@/components/RejectButton";
import Link from "next/link";
import { useToast } from "@chakra-ui/react";

interface JobCardProps {
job: IJob;
Expand All @@ -26,6 +27,7 @@ export default function AdminCard({ job, onUpdateJob, innerRef }: JobCardProps)
const router = useRouter();
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
const toast = useToast();

// Check localStorage for dismissed state on component mount
useEffect(() => {
Expand Down Expand Up @@ -68,6 +70,42 @@ export default function AdminCard({ job, onUpdateJob, innerRef }: JobCardProps)
if (onUpdateJob) {
await onUpdateJob(job._id, "approved", new Date());
}
} else if (selectedAction === "renew") {
setIsLoading("approve");
try {
const response = await fetch(`/api/jobs/${job._id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
isRenewal: true,
previousStatus: job.jobStatus,
newStatus: "approved",
}),
});

if (!response.ok) {
throw new Error("Failed to renew job");
}

const data = await response.json();

// Update the job status in the UI
if (onUpdateJob) {
await onUpdateJob(job._id, "approved", new Date());
}
} catch (error) {
console.error("Error renewing job:", error);
toast({
title: "Error",
description: "Failed to renew job. Please try again.",
status: "error",
duration: 5000,
isClosable: true,
position: "top-right",
});
}
}
setIsLoading(null);
closeModal();
Expand Down
71 changes: 69 additions & 2 deletions src/components/JobCard/OrgCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ import {
ModalCloseButton,
useDisclosure,
Text,
useToast,
} from "@chakra-ui/react";
import { FiEdit, FiMessageSquare } from "react-icons/fi";
import { FiEdit, FiMessageSquare, FiRefreshCw } from "react-icons/fi";
import { useRouter } from "next/navigation";
import JobDateInfo, { JobDateKind } from "./JobDateInfo";
import { isMoreThanThirtyDaysAgo } from "@/lib/utils";
Expand All @@ -25,6 +26,7 @@ export interface OrgCardProps extends ComponentProps<"div"> {
className?: string;
job: IJob;
types: JobDateKind[];
onJobRenewed?: (job: IJob) => void;
}

function getJobDate(job: IJob, type: JobDateKind) {
Expand Down Expand Up @@ -53,16 +55,68 @@ function getJobDate(job: IJob, type: JobDateKind) {
}

export const OrgCard = forwardRef<HTMLDivElement, OrgCardProps>(
({ children, className, job, types, ...props }, ref) => {
({ children, className, job, types, onJobRenewed, ...props }, ref) => {
const { isOpen, onOpen, onClose } = useDisclosure();
const isActuallyExpired = isMoreThanThirtyDaysAgo(job.approvedDate);
const router = useRouter();
const toast = useToast();

function handleEditApplicationButton(e: React.ChangeEvent<any>) {
e.preventDefault();
router.push(`/jobform?jobId=${job._id}&returnURL=/dashboard`);
}

async function handleRenewJob(e: React.ChangeEvent<any>) {
e.preventDefault();
try {
const response = await fetch(`/api/jobs/${job._id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
isRenewal: true,
previousStatus: job.jobStatus,
newStatus: "approved",
}),
});

if (!response.ok) {
throw new Error("Failed to renew job");
}

const data = await response.json();

// Show success toast
toast({
title: "Job Renewed",
description: `Successfully renewed "${job.title}"`,
status: "success",
duration: 5000,
isClosable: true,
position: "top-right",
});

// Call the callback if provided
if (onJobRenewed) {
onJobRenewed(data.job);
} else {
// Fallback to page refresh if no callback provided
window.location.reload();
}
} catch (error) {
console.error("Error renewing job:", error);
toast({
title: "Error",
description: "Failed to renew job. Please try again.",
status: "error",
duration: 5000,
isClosable: true,
position: "top-right",
});
}
}

return (
<>
<div
Expand Down Expand Up @@ -132,6 +186,19 @@ export const OrgCard = forwardRef<HTMLDivElement, OrgCardProps>(
</Button>
)}
</div>
{isActuallyExpired && (
<Button
aria-label="Renew Job"
size={{ base: "xs", md: "sm" }}
colorScheme="green"
variant="outline"
onClick={handleRenewJob}
className="flex flex-row items-center gap-1 sm:gap-2"
>
<FiRefreshCw className="text-sm sm:text-base" />
Renew
</Button>
)}
<Button
aria-label="Edit Application"
size={{ base: "xs", md: "sm" }}
Expand Down