From 139175f9c78c4b953a47c09ccf2f8b565cc94ab0 Mon Sep 17 00:00:00 2001
From: Paribesh Nepal <100255987+Paribesh01@users.noreply.github.com>
Date: Thu, 31 Oct 2024 12:08:44 +0530
Subject: [PATCH] Feat/admin UI (#553)
* fix: deleted jobs were showing
* feat: jobs management page done
* feat: added new ui for the payment and recruiters
* fix: minor changes
* fix:fixed responsiveness
* fix:minor fix
* feat:removed payment
* fix:removed payments
* feat:added filters
* feat:added recruiters
* fix:minor change
* fix:minor changes
* fix:filter fixed
* fix: fixed responsiveness
* fix:minor change
---
prisma/schema.prisma | 19 +-
prisma/seed.ts | 38 ++-
src/actions/job.action.ts | 6 +
src/actions/user.profile.actions.ts | 36 +++
src/app/manage/{ => jobs}/page.tsx | 6 +-
src/app/manage/recruiters/page.tsx | 22 ++
src/components/DeleteDialog.tsx | 10 +-
src/components/JobManagement.tsx | 7 +-
src/components/JobManagementTable.tsx | 282 +++++++++++++++-------
src/components/ManageRecruiters.tsx | 181 ++++++++++++++
src/components/ToggleApproveJobButton.tsx | 10 +-
src/components/pagination-client.tsx | 4 +-
src/components/ui/pagination.tsx | 2 -
src/components/ui/paginator.tsx | 32 ++-
src/config/path.config.ts | 3 +-
src/lib/constant/app.constant.ts | 11 +-
src/lib/icons.ts | 4 +
src/types/jobs.types.ts | 1 +
src/types/recruiters.types.ts | 17 ++
19 files changed, 561 insertions(+), 130 deletions(-)
rename src/app/manage/{ => jobs}/page.tsx (87%)
create mode 100644 src/app/manage/recruiters/page.tsx
create mode 100644 src/components/ManageRecruiters.tsx
create mode 100644 src/types/recruiters.types.ts
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index d7350ec2..9a420bdb 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -25,12 +25,23 @@ model User {
project Project[]
resume String?
- oauthProvider OauthProvider? // Tracks OAuth provider (e.g., 'google')
- oauthId String?
-
+ oauthProvider OauthProvider? // Tracks OAuth provider (e.g., 'google')
+ oauthId String?
+ createdAt DateTime @default(now())
blockedByAdmin DateTime?
- onBoard Boolean @default(false)
+ onBoard Boolean @default(false)
bookmark Bookmark[]
+ companyId String? @unique
+ company Company? @relation(fields: [companyId], references: [id])
+}
+
+model Company {
+ id String @id @default(cuid())
+ companyName String
+ companyLogo String?
+ companyEmail String
+ companyBio String
+ user User?
}
enum OauthProvider {
diff --git a/prisma/seed.ts b/prisma/seed.ts
index 5aa0b4a8..e2ba7cdb 100644
--- a/prisma/seed.ts
+++ b/prisma/seed.ts
@@ -14,9 +14,21 @@ const prisma = new PrismaClient();
const users = [
{ id: '1', name: 'Jack', email: 'user@gmail.com' },
{ id: '2', name: 'Admin', email: 'admin@gmail.com', role: Role.ADMIN, onBoard: true },
- { id: '3', name: 'Hr', email: 'hr@gmail.com', role: Role.HR },
+ { id: '3', companyId: '1', name: 'Hr', email: 'hr@gmail.com', role: Role.HR, onBoard: true },
+ { id: '4', companyId: '2', name: 'John', email: 'john@gmail.com', role: Role.HR, onBoard: true },
+ { id: '5', companyId: '3', name: 'Jane', email: 'jane@gmail.com', role: Role.HR, onBoard: true },
+
+
];
+
+const companies = [
+ { id: '1', compnayEmail: "careers@techcorps.com", companyName: 'Tech Corp', companyBio: 'Leading tech solutions provider specializing in innovative web development.', companyLogo: '/main.svg' },
+ { id: '2', companyEmail: "careers@globalsolutions.com", companyName: 'Global Solutions', companyBio: 'Global Solutions offers comprehensive IT services for businesses worldwide.', companyLogo: '/main.svg' },
+ { id: '3', companyEmail: 'careers@innovatech.com', companyName: 'Innovatech', companyBio: 'Innovatech specializes in backend systems and cloud-based solutions.', companyLogo: '/main.svg' },
+]
+
+
let jobs = [
{
id: '1',
@@ -328,6 +340,7 @@ async function seedUsers() {
password: hashedPassword,
role: u.role || Role.USER,
emailVerified: new Date(),
+ companyId: u.companyId
},
});
console.log(`User created or updated: ${u.email}`);
@@ -340,6 +353,28 @@ async function seedUsers() {
console.error('Error seeding users:', error);
}
}
+async function seedCompanies() {
+ try {
+ await Promise.all(
+ companies.map(async (c) =>
+ prisma.company.upsert({
+ where: { id: c.id },
+ create: {
+ id: c.id,
+ companyName: c.companyName,
+ companyEmail: c.companyEmail ?? "default@example.com",
+ companyBio: c.companyBio,
+ companyLogo: c.companyLogo,
+ },
+ update: {},
+ })
+ )
+ );
+ console.log('✅ Company seed completed successfully');
+ } catch (error) {
+ console.error('Error seeding companies:', error);
+ }
+}
async function seedJobs() {
try {
@@ -401,6 +436,7 @@ async function seedJobs() {
}
async function main() {
+ await seedCompanies();
await seedUsers();
await seedJobs();
}
diff --git a/src/actions/job.action.ts b/src/actions/job.action.ts
index 659edb3e..b3b15ade 100644
--- a/src/actions/job.action.ts
+++ b/src/actions/job.action.ts
@@ -156,6 +156,7 @@ export const getAllJobs = withSession<
skills: true,
address: true,
workMode: true,
+ expired: true,
category: true,
minSalary: true,
maxSalary: true,
@@ -219,6 +220,7 @@ export const getRecommendedJobs = withServerActionAsyncCatcher<
maxSalary: true,
postedAt: true,
skills: true,
+ expired: true,
isVerifiedJob: true,
companyLogo: true,
},
@@ -252,6 +254,7 @@ export const getRecommendedJobs = withServerActionAsyncCatcher<
companyLogo: true,
minExperience: true,
maxExperience: true,
+ expired: true,
isVerifiedJob: true,
category: true,
},
@@ -294,6 +297,7 @@ export const getJobById = withServerActionAsyncCatcher<
minExperience: true,
maxExperience: true,
skills: true,
+ expired: true,
address: true,
workMode: true,
hasSalaryRange: true,
@@ -352,6 +356,7 @@ export const getRecentJobs = async () => {
minExperience: true,
maxExperience: true,
skills: true,
+ expired: true,
postedAt: true,
companyLogo: true,
type: true,
@@ -601,6 +606,7 @@ export async function GetBookmarkByUserId() {
minSalary: true,
maxSalary: true,
postedAt: true,
+ expired: true,
companyLogo: true,
},
},
diff --git a/src/actions/user.profile.actions.ts b/src/actions/user.profile.actions.ts
index 51128cda..d68e0857 100644
--- a/src/actions/user.profile.actions.ts
+++ b/src/actions/user.profile.actions.ts
@@ -317,3 +317,39 @@ export const getUserDetails = async () => {
return new ErrorHandler('Internal server error', 'DATABASE_ERROR');
}
};
+
+export const getUserRecruiters = async () => {
+ const auth = await getServerSession(authOptions);
+
+ if (!auth || !auth?.user?.id || auth?.user?.role !== 'ADMIN')
+ throw new ErrorHandler('Not Authorized', 'UNAUTHORIZED');
+ try {
+ const res = await prisma.user.findMany({
+ where: {
+ role: 'HR',
+ },
+ select: {
+ id: true,
+ email: true,
+ name: true,
+ createdAt: true,
+ _count: {
+ select: {
+ jobs: true,
+ },
+ },
+ company: {
+ select: {
+ companyName: true,
+ companyEmail: true,
+ },
+ },
+ },
+ });
+ return new SuccessResponse('Recruiter SuccessFully Fetched', 200, {
+ recruiters: res,
+ }).serialize();
+ } catch (_error) {
+ return new ErrorHandler('Internal server error', 'DATABASE_ERROR');
+ }
+};
diff --git a/src/app/manage/page.tsx b/src/app/manage/jobs/page.tsx
similarity index 87%
rename from src/app/manage/page.tsx
rename to src/app/manage/jobs/page.tsx
index afcddb1d..65d3819f 100644
--- a/src/app/manage/page.tsx
+++ b/src/app/manage/jobs/page.tsx
@@ -25,11 +25,7 @@ const ManageJob = async ({
redirect('/jobs');
}
const searchParamss = parsedData.data;
- return (
-
-
-
- );
+ return ;
};
export default ManageJob;
diff --git a/src/app/manage/recruiters/page.tsx b/src/app/manage/recruiters/page.tsx
new file mode 100644
index 00000000..160cefbf
--- /dev/null
+++ b/src/app/manage/recruiters/page.tsx
@@ -0,0 +1,22 @@
+import { getUserRecruiters } from '@/actions/user.profile.actions';
+import ManageRecruiters from '@/components/ManageRecruiters';
+
+import { options } from '@/lib/auth';
+import { getServerSession } from 'next-auth';
+import { redirect } from 'next/navigation';
+import React from 'react';
+
+const RecruitersPage = async () => {
+ const server = await getServerSession(options);
+ if (!server?.user) {
+ redirect('/api/auth/signin');
+ } else if (server.user.role !== 'ADMIN') {
+ redirect('/jobs');
+ }
+
+ const Recruiters = await getUserRecruiters();
+
+ return ;
+};
+
+export default RecruitersPage;
diff --git a/src/components/DeleteDialog.tsx b/src/components/DeleteDialog.tsx
index 873eb442..ff5713b6 100644
--- a/src/components/DeleteDialog.tsx
+++ b/src/components/DeleteDialog.tsx
@@ -4,6 +4,7 @@ import { Button } from './ui/button';
import { useToast } from './ui/use-toast';
import { toggleDeleteJobById } from '@/actions/job.action';
import { JobType } from '@/types/jobs.types';
+import icons from '@/lib/icons';
import {
Dialog,
DialogTrigger,
@@ -13,7 +14,6 @@ import {
DialogDescription,
DialogFooter,
} from './ui/dialog';
-import { ArchiveRestore, Trash } from 'lucide-react';
const JobDialog = ({ job }: { job: JobType }) => {
const [dialogOpen, setDialogOpen] = useState(false); // State to manage dialog visibility
@@ -42,7 +42,9 @@ const JobDialog = ({ job }: { job: JobType }) => {
role="button"
onClick={() => setDialogOpen(true)}
>
- {/* Icon for restoring the job */}
+
) : (
{
role="button"
onClick={() => setDialogOpen(true)}
>
- {/* Icon for deleting the job */}
+
)}
diff --git a/src/components/JobManagement.tsx b/src/components/JobManagement.tsx
index a0e6918d..53aaa29d 100644
--- a/src/components/JobManagement.tsx
+++ b/src/components/JobManagement.tsx
@@ -1,6 +1,5 @@
import React from 'react';
import { getAllJobs } from '@/actions/job.action';
-import JobManagementHeader from './JobManagementHeader';
import JobManagementTable from './JobManagementTable';
import { JobQuerySchemaType } from '@/lib/validators/jobs.validator';
@@ -13,10 +12,10 @@ const JobManagement = async ({
if (!jobs.status) {
return Error {jobs.message}
;
}
+
return (
-
-
-
+
+
);
};
diff --git a/src/components/JobManagementTable.tsx b/src/components/JobManagementTable.tsx
index 564a4035..e92a69a4 100644
--- a/src/components/JobManagementTable.tsx
+++ b/src/components/JobManagementTable.tsx
@@ -1,3 +1,4 @@
+'use client';
import {
Table,
TableBody,
@@ -6,107 +7,226 @@ import {
TableHeader,
TableRow,
} from './ui/table';
-import { Edit } from 'lucide-react';
-import { Badge } from '@/components/ui/badge';
+import { Plus, Search } from 'lucide-react';
-import { getAllJobsAdditonalType } from '@/types/jobs.types';
-import { ServerActionReturnType } from '@/types/api.types';
+import { JobType, getAllJobsAdditonalType } from '@/types/jobs.types';
import { JobQuerySchemaType } from '@/lib/validators/jobs.validator';
import { DEFAULT_PAGE, JOBS_PER_PAGE } from '@/config/app.config';
-import { Pagination, PaginationContent, PaginationItem } from './ui/pagination';
-import {
- PaginationNextButton,
- PaginationPreviousButton,
-} from './pagination-client';
-import APP_PATHS from '@/config/path.config';
-import { PaginationPages } from './ui/paginator';
+
import DeleteDialog from './DeleteDialog';
import ToggleApproveJobButton from './ToggleApproveJobButton';
+import { Input } from './ui/input';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from './ui/select';
+import { Button } from './ui/button';
+import Link from 'next/link';
+import { useState, useEffect } from 'react';
+import {
+ Pagination,
+ PaginationContent,
+ PaginationItem,
+ PaginationLink,
+ PaginationNext,
+ PaginationPrevious,
+} from './ui/pagination';
type props = {
searchParams: JobQuerySchemaType;
- jobs: ServerActionReturnType
;
+ jobs: getAllJobsAdditonalType | undefined;
};
const JobManagementTable = ({ jobs, searchParams }: props) => {
- if (!jobs.status) {
- return Error {jobs.message}
;
- }
+ const [filteredJobs, setFilteredJobs] = useState([]);
+ const [statusFilter, setStatusFilter] = useState('All');
+ const [orderFilter, setOrderFilter] = useState('latest');
+ const [currentPage, setCurrentPage] = useState(
+ searchParams.page || DEFAULT_PAGE
+ );
+ const [searchTerm, setSearchTerm] = useState('');
+
+ useEffect(() => {
+ if (jobs?.jobs) {
+ const filtered = jobs?.jobs.filter((job) => {
+ if (statusFilter === 'All') return true;
+ if (statusFilter === 'active')
+ return !job.deleted && !job.expired && job.isVerifiedJob;
+ if (statusFilter === 'deleted') return job.deleted;
+ if (statusFilter === 'closed')
+ return (job.expired || !job.isVerifiedJob) && !job.deleted;
+ return true;
+ });
+
+ const searched = filtered.filter((job) => {
+ const lowerCaseTitle = job.title.toLowerCase();
+ const lowerCaseCompanyName = job.companyName.toLowerCase();
+ return (
+ lowerCaseTitle.includes(searchTerm.toLowerCase()) ||
+ lowerCaseCompanyName.includes(searchTerm.toLowerCase())
+ );
+ });
+
+ const sorted = searched.sort((a, b) => a.title.localeCompare(b.title));
+
+ if (orderFilter === 'latest') {
+ sorted.sort(
+ (a, b) =>
+ new Date(b.postedAt).getTime() - new Date(a.postedAt).getTime()
+ );
+ } else {
+ sorted.sort(
+ (a, b) =>
+ new Date(a.postedAt).getTime() - new Date(b.postedAt).getTime()
+ );
+ }
+
+ setFilteredJobs(sorted);
+ }
+ }, [jobs?.jobs, statusFilter, orderFilter, searchTerm]);
const totalPages =
- Math.ceil((jobs.additional?.totalJobs || 0) / JOBS_PER_PAGE) ||
- DEFAULT_PAGE;
- const currentPage = searchParams.page || DEFAULT_PAGE;
+ Math.ceil(filteredJobs.length / JOBS_PER_PAGE) || DEFAULT_PAGE;
+ const startIndex = (currentPage - 1) * JOBS_PER_PAGE;
+ const currentJobs = filteredJobs.slice(
+ startIndex,
+ startIndex + JOBS_PER_PAGE
+ );
+
+ const handlePageChange = (pageNumber: number) => {
+ setCurrentPage(Math.min(Math.max(pageNumber, 1), totalPages));
+ };
+
return (
- <>
-
-
-
-
- Job Title
- JobType
- Location
- isVerified
- Actions
-
-
-
- {jobs.additional?.jobs?.map((job) => (
-
- {job?.title}
- {job?.workMode}
- {job?.city}
-
- {job.deleted ? (
- Deleted
- ) : (
+
+
+
Manage Jobs
+
+
+
+
+
+
+
+
+ setSearchTerm(e.target.value)}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Job Title
+ Company Name
+ Job Category
+ Job Type
+ Posted Date
+ Status
+ Verified
+ Action
+
+
+
+ {currentJobs.map((job) => (
+
+ {job.title}
+ {job.companyName}
+ {job.category}
+ {job.type}
+
+ {new Date(job.postedAt).toLocaleDateString()}
+
+
+
+ {job.deleted
+ ? 'Deleted'
+ : job.expired || !job.isVerifiedJob
+ ? 'Closed'
+ : 'Active'}
+
+
+
- )}
-
-
-
-
-
-
+
+
-
-
-
- ))}
-
-
-
-
- {totalPages ? (
-
-
+
+
+ ))}
+
+
+
+
+
+
+
+
+
+ handlePageChange(currentPage - 1)}>
+
- ) : null}
-
- {totalPages ? (
-
-
+ {[...Array(totalPages)].map((_, index) => (
+ handlePageChange(index + 1)}
+ >
+ {index + 1}
+
+ ))}
+ handlePageChange(currentPage + 1)}>
+
- ) : null}
-
-
+
+
+
- >
+
);
};
diff --git a/src/components/ManageRecruiters.tsx b/src/components/ManageRecruiters.tsx
new file mode 100644
index 00000000..7bf6251b
--- /dev/null
+++ b/src/components/ManageRecruiters.tsx
@@ -0,0 +1,181 @@
+'use client';
+import React, { useState } from 'react';
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from './ui/table';
+import { Search, Trash2 } from 'lucide-react';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from './ui/select';
+import { Input } from './ui/input';
+import { getAllRecruiters } from '@/types/recruiters.types';
+import { Button } from './ui/button';
+import {
+ Pagination,
+ PaginationContent,
+ PaginationItem,
+ PaginationLink,
+ PaginationNext,
+ PaginationPrevious,
+} from './ui/pagination';
+import { ServerActionReturnType } from '@/types/api.types';
+
+type props = {
+ recruiters: ServerActionReturnType;
+};
+
+const ManageRecruiters = ({ recruiters }: props) => {
+ const [currentPage, setCurrentPage] = useState(1);
+ const [searchTerm, setSearchTerm] = useState('');
+ const itemsPerPage = 10;
+
+ if (!recruiters.status) {
+ return Error {recruiters.message}
;
+ }
+
+ const recruiterList = recruiters.additional?.recruiters ?? [];
+
+ const filteredRecruiters = recruiterList.filter(
+ (recruiter) =>
+ recruiter.company?.companyName
+ .toLowerCase()
+ .includes(searchTerm.toLowerCase()) ||
+ recruiter.company?.companyEmail
+ .toLowerCase()
+ .includes(searchTerm.toLowerCase())
+ );
+
+ const totalPages = Math.ceil(filteredRecruiters.length / itemsPerPage);
+
+ const currentRecruiters = filteredRecruiters.slice(
+ (currentPage - 1) * itemsPerPage,
+ currentPage * itemsPerPage
+ );
+
+ const handlePageChange = (page: number) => {
+ if (page > 0 && page <= totalPages) {
+ setCurrentPage(page);
+ }
+ };
+ return (
+
+
+
Manage Recruiters
+
+
+
+
+
+ setSearchTerm(e.target.value)}
+ />
+
+
+
+
+
+
+
+
+
+
+
+ Company Name
+ Company Email
+ Jobs Posted
+ Created At
+ Action
+
+
+
+ {currentRecruiters.length ? (
+ currentRecruiters.map((recruiter) => (
+
+
+ {recruiter.company?.companyName}
+
+
+ {recruiter.company?.companyEmail}
+
+
+ {recruiter._count.jobs}
+
+
+ {new Date(recruiter.createdAt).toLocaleDateString()}
+
+
+
+
+
+ ))
+ ) : (
+
+
+ No Recruiters
+
+
+ )}
+
+
+
+
+
+
+
+
+
+ handlePageChange(currentPage - 1)}
+ className="border hover:border-blue-600 dark:bg-slate-400 dark:bg-opacity-5 dark:text-white text-black bg-slate-600 bg-opacity-15"
+ />
+
+ {[...Array(totalPages)].map((_, index) => (
+
+ handlePageChange(index + 1)}
+ className={`border hover:border-blue-600 dark:bg-slate-400 dark:bg-opacity-5 ${
+ currentPage === index + 1
+ ? 'bg-blue-600 text-white'
+ : 'text-black dark:text-white'
+ }`}
+ >
+ {index + 1}
+
+
+ ))}
+
+ handlePageChange(currentPage + 1)}
+ className="border hover:border-blue-600 dark:bg-slate-400 dark:bg-opacity-5 dark:text-white text-black bg-slate-600 bg-opacity-15"
+ />
+
+
+
+
+
+ );
+};
+
+export default ManageRecruiters;
diff --git a/src/components/ToggleApproveJobButton.tsx b/src/components/ToggleApproveJobButton.tsx
index 5f964b78..d12ef494 100644
--- a/src/components/ToggleApproveJobButton.tsx
+++ b/src/components/ToggleApproveJobButton.tsx
@@ -14,7 +14,7 @@ import { Button } from './ui/button';
import { useToast } from './ui/use-toast';
import { toggleApproveJob } from '@/actions/job.action';
import { JobType } from '@/types/jobs.types';
-import { Badge } from './ui/badge';
+import { Switch } from './ui/switch';
const ToggleApproveJobButton = ({ job }: { job: JobType }) => {
const { toast } = useToast();
@@ -41,13 +41,7 @@ const ToggleApproveJobButton = ({ job }: { job: JobType }) => {
return (