diff --git a/docs/documentation.md b/docs/documentation.md
index cff9114..8ed81ef 100644
--- a/docs/documentation.md
+++ b/docs/documentation.md
@@ -184,7 +184,7 @@ Event region and city options live in `src/lib/eventOptions.ts`.
- Supports filters for `employmentType`, `compensationType`, `organizationIndustry`, and `jobStatus`.
- Non-admin requests are paginated with `page` and `limit`.
- Admin requests use `admin=true` and return all matching records.
-- Approved non-admin results include all approved jobs, regardless of approval date.
+- Approved non-admin results include only jobs approved within the last 30 days.
- Sorting prioritizes `memberJob` first, then approved date or post date.
`POST /api/jobs`
@@ -362,18 +362,18 @@ Shows the authenticated user's jobs. Organization users can:
- Edit jobs
- View rejection feedback
-- Reopen resolved jobs
+- Renew expired jobs
- Resolve approved jobs
### Job Admin Dashboard
File: `src/components/jobs/pages/JobsAdminPage.tsx`
-Shows pending, approved, rejected, and resolved jobs. Admins can:
+Shows pending, approved, rejected, and expired jobs. Admins can:
- Approve jobs
- Reject jobs with a reason
-- Reopen or resolve jobs through status actions
+- Renew expired jobs or resolve approved jobs through status actions
- Refresh dashboard data
- Navigate to user management
diff --git a/docs/owner-user-guide.md b/docs/owner-user-guide.md
index afedc47..5b65d91 100644
--- a/docs/owner-user-guide.md
+++ b/docs/owner-user-guide.md
@@ -62,17 +62,17 @@ Approved jobs appear on the public job board. Rejected jobs stay available to th
- Pending: waiting for Spokes admin review.
- Approved: visible on the public job board.
- Rejected: not public; the organization can view feedback.
-- Resolved: no longer active or visible on the public job board.
+- Expired: no longer active or visible on the public job board.
-Jobs stay active until the posting organization resolves or deletes them.
+Approved jobs expire automatically after 30 days. The admin dashboard checks for expired jobs when it loads and once per hour while open.
-### Edit, Reopen, or Resolve Jobs
+### Edit, Renew, or Resolve Jobs
Organizations can use `/jobs/manage` to manage their own jobs.
- Edit: updates the job and sends it back to pending review.
-- Reopen: sends a resolved job back for review.
-- Resolve Job: marks an approved job as resolved so it is no longer active.
+- Renew: sends an expired job back for review and starts a new 30-day period after approval.
+- Resolve Job: marks an approved job as expired so it is no longer active.
- View Feedback: shows the admin's rejection reason.
## Managing Events
@@ -236,7 +236,7 @@ Owners can usually handle:
- Updating user organizations
- Deleting users
- Deleting organizations
-- Reopening or resolving jobs from the organization dashboard
+- Renewing or resolving jobs from the organization dashboard
Owners should ask a technical maintainer for:
diff --git a/src/app/api/__test__/AdminPage.test.tsx b/src/app/api/__test__/AdminPage.test.tsx
index 17303cf..ec9fdb9 100644
--- a/src/app/api/__test__/AdminPage.test.tsx
+++ b/src/app/api/__test__/AdminPage.test.tsx
@@ -89,6 +89,7 @@ describe("Admin Jobs Page", () => {
detailURL: "http://example.com/live",
userId: "user2",
modifiedDate: new Date().toISOString(),
+ approvedDate: new Date().toISOString(),
},
{
_id: "3",
@@ -190,8 +191,8 @@ describe("Admin Jobs Page", () => {
});
expect(screen.getByText("Org2")).toBeInTheDocument();
- // Click on Resolved Jobs tab
- fireEvent.click(screen.getByText(/Resolved Jobs/i));
+ // Click on Expired Jobs tab
+ fireEvent.click(screen.getByText(/Expired Jobs/i));
// Check for expired jobs
await waitFor(() => {
diff --git a/src/app/api/__test__/JobsApi.test.ts b/src/app/api/__test__/JobsApi.test.ts
index 0722f7e..2fcfd01 100644
--- a/src/app/api/__test__/JobsApi.test.ts
+++ b/src/app/api/__test__/JobsApi.test.ts
@@ -186,7 +186,7 @@ describe("Jobs API", () => {
expect(Job.find).not.toHaveBeenCalled();
});
- test("GET defaults public job listings to all approved jobs", async () => {
+ test("GET defaults public job listings to approved jobs from the last 30 days", async () => {
mockAuth.userId = null as any;
mockAuth.role = "job_seeker";
const limit = jest.fn().mockResolvedValue([]);
@@ -202,7 +202,10 @@ describe("Jobs API", () => {
);
expect(response.status).toBe(200);
- expect(Job.find).toHaveBeenCalledWith({ jobStatus: "approved" });
+ expect(Job.find).toHaveBeenCalledWith({
+ jobStatus: "approved",
+ approvedDate: { $gte: expect.any(Date) },
+ });
});
test("GET job detail hides private jobs from anonymous users", async () => {
@@ -221,7 +224,7 @@ describe("Jobs API", () => {
expect(result.message).toBe("Job not found");
});
- test("GET job detail keeps an approved legacy job public without an approval date", async () => {
+ test("GET job detail hides an approved job without an approval date", async () => {
mockAuth.userId = null as any;
mockAuth.role = "job_seeker";
(Job.findById as jest.Mock).mockResolvedValue({
@@ -232,7 +235,7 @@ describe("Jobs API", () => {
const response = await GET_JOB({ nextUrl: { pathname: "/api/jobs/job-1" } } as any, {});
- expect(response.status).toBe(200);
+ expect(response.status).toBe(404);
});
test("allows an organization to resolve its job", async () => {
diff --git a/src/app/api/jobs/[jobId]/route.ts b/src/app/api/jobs/[jobId]/route.ts
index 6cf64d0..8603ee9 100644
--- a/src/app/api/jobs/[jobId]/route.ts
+++ b/src/app/api/jobs/[jobId]/route.ts
@@ -3,6 +3,7 @@ import { NextRequest, NextResponse } from "next/server";
import Job from "@/database/jobSchema";
import { withApiAuth } from "@/lib/auth";
import { JobStatus } from "@/database/jobSchema";
+import { isExpired } from "@/lib/utils";
const mutableJobFields = [
"organizationIndustry",
@@ -171,7 +172,7 @@ export const GET = withApiAuth(
return NextResponse.json({ message: "Job not found" }, { status: 404 });
}
- const isPubliclyVisible = job.jobStatus === JobStatus.approved;
+ const isPubliclyVisible = job.jobStatus === JobStatus.approved && !isExpired(job.jobStatus, job.approvedDate);
const canViewPrivateJob = auth.role === "spokes_admin" || (auth.userId && job.userId === auth.userId);
if (!isPubliclyVisible && !canViewPrivateJob) {
diff --git a/src/app/api/jobs/recent/route.ts b/src/app/api/jobs/recent/route.ts
index 137afc3..eef11ee 100644
--- a/src/app/api/jobs/recent/route.ts
+++ b/src/app/api/jobs/recent/route.ts
@@ -3,6 +3,7 @@ import { NextRequest, NextResponse } from "next/server";
import Job from "@/database/jobSchema";
import { ObjectId } from "mongodb";
import { withApiAuth } from "@/lib/auth";
+import { getThirtyDaysAgo } from "@/lib/utils";
export const POST = withApiAuth(
async (req: NextRequest) => {
@@ -21,6 +22,7 @@ export const POST = withApiAuth(
const recentJobs = await Job.find({
_id: { $in: jobIdArray },
jobStatus: "approved",
+ approvedDate: { $gte: getThirtyDaysAgo() },
});
return NextResponse.json(recentJobs, { status: 200 });
} catch (error) {
diff --git a/src/app/api/jobs/route.ts b/src/app/api/jobs/route.ts
index 7a6cde1..877d6e5 100644
--- a/src/app/api/jobs/route.ts
+++ b/src/app/api/jobs/route.ts
@@ -4,6 +4,7 @@ import Job from "@/database/jobSchema";
import User from "@/database/userSchema";
import { withApiAuth } from "@/lib/auth";
import { resolveOrganizationName } from "@/lib/organizations";
+import { getThirtyDaysAgo } from "@/lib/utils";
// no filters: GET /api/jobs?page=1&limit=10
// filter by employment type: GET /api/jobs?employmentType=full-time&employment=part-time
@@ -72,6 +73,10 @@ export const GET = withApiAuth(
filter.jobStatus = "approved";
}
+ if (!isAdminRequest) {
+ filter.approvedDate = { $gte: getThirtyDaysAgo() };
+ }
+
if (statusFilter === "approved") {
sort.approvedDate = -1;
} else {
diff --git a/src/app/dashboard/DashboardPage.tsx b/src/app/dashboard/DashboardPage.tsx
index 4b851cc..3ae759a 100644
--- a/src/app/dashboard/DashboardPage.tsx
+++ b/src/app/dashboard/DashboardPage.tsx
@@ -139,13 +139,15 @@ export default function DashboardPage({ organizationName, membershipStatus }: Da
);
}
- const liveJobs = userJobs.filter((job) => job.jobStatus.toLowerCase() === "approved" && !isExpired(job.jobStatus));
+ const liveJobs = userJobs.filter(
+ (job) => job.jobStatus.toLowerCase() === "approved" && !isExpired(job.jobStatus, job.approvedDate),
+ );
const pendingJobs = userJobs.filter(
(job) => job.jobStatus.toLowerCase() === "pending" || job.jobStatus.toLowerCase() === "rejected",
);
- const resolvedJobs = userJobs.filter((job) => isExpired(job.jobStatus));
+ const expiredJobs = userJobs.filter((job) => isExpired(job.jobStatus, job.approvedDate));
return (
@@ -170,7 +172,7 @@ export default function DashboardPage({ organizationName, membershipStatus }: Da
))
@@ -192,14 +194,14 @@ export default function DashboardPage({ organizationName, membershipStatus }: Da