diff --git a/admin/src/controllers/candidate/index.ts b/admin/src/controllers/candidate/index.ts index a0aaf34..86d0096 100644 --- a/admin/src/controllers/candidate/index.ts +++ b/admin/src/controllers/candidate/index.ts @@ -1,6 +1,5 @@ import getAllCandidatesController from './getCandidates.controller'; import getCandidateByIdController from './viewProfile.controller'; -import searchCandidatesController from './search.controller'; import candidateBlockUnblockController from './blockUnblock.controller'; import { IDependency } from '../../frameworks/types/dependency'; @@ -9,7 +8,6 @@ export = (dependencies: IDependency) => { return { getAllCandidatesController: getAllCandidatesController(dependencies), getCandidateByIdController: getCandidateByIdController(dependencies), - searchCandidatesController: searchCandidatesController(dependencies), candidateBlockUnblockController: candidateBlockUnblockController(dependencies), }; }; diff --git a/admin/src/controllers/index.ts b/admin/src/controllers/index.ts index b3cca23..15b6300 100644 --- a/admin/src/controllers/index.ts +++ b/admin/src/controllers/index.ts @@ -4,6 +4,7 @@ import recruiterControllers from './recruiter'; import dashboardControllers from './admin'; import membershipControllers from './membership'; import paymentControllers from './payment'; +import searchControllers from './search'; export { candidateControllers, @@ -12,4 +13,5 @@ export { dashboardControllers, membershipControllers, paymentControllers, + searchControllers }; diff --git a/admin/src/controllers/search/index.ts b/admin/src/controllers/search/index.ts new file mode 100644 index 0000000..f9d3f55 --- /dev/null +++ b/admin/src/controllers/search/index.ts @@ -0,0 +1,8 @@ +import searchController from "./search.controller" +import { IDependency } from '../../frameworks/types/dependency'; + +export = (dependencies: IDependency) => { + return { + searchController: searchController(dependencies), + }; +}; diff --git a/admin/src/controllers/candidate/search.controller.ts b/admin/src/controllers/search/search.controller.ts similarity index 55% rename from admin/src/controllers/candidate/search.controller.ts rename to admin/src/controllers/search/search.controller.ts index 98639f6..cdd8955 100644 --- a/admin/src/controllers/candidate/search.controller.ts +++ b/admin/src/controllers/search/search.controller.ts @@ -3,21 +3,22 @@ import { IDependency } from '../../frameworks/types/dependency'; export = (dependencies: IDependency) => { const { - useCases: { searchCandidatesUseCase }, + useCases: { searchUseCase }, } = dependencies; return async (req: Request, res: Response) => { + const { type: resourceType } = req.params; const { searchKey } = req.query; - - const { candidates, numberOfPages } = await searchCandidatesUseCase(dependencies).execute( - searchKey, + const { result, numberOfPages } = await searchUseCase(dependencies).execute( + resourceType, + searchKey as string, Number(req.params.page) || 1, Number(req.params.limit) || 4, ); - + res.status(200).json({ - message: 'candidates fetched successfully', - data: { candidates, numberOfPages }, + message: 'search results fetched successfully', + data: { result, numberOfPages }, }); }; }; diff --git a/admin/src/frameworks/database/models/job.ts b/admin/src/frameworks/database/models/job.ts index 6045e90..bafbd10 100644 --- a/admin/src/frameworks/database/models/job.ts +++ b/admin/src/frameworks/database/models/job.ts @@ -1,7 +1,7 @@ import mongoose from 'mongoose'; import { IJob } from '../../types/job'; -export type JobDocument = mongoose.Document & Omit; +export type IJobDocument = mongoose.Document & Omit; const jobSchema = new mongoose.Schema( { @@ -39,8 +39,8 @@ const jobSchema = new mongoose.Schema( }, ); -interface JobModel extends mongoose.Model { - buildJob(attributes: IJob): JobDocument; +interface JobModel extends mongoose.Model { + buildJob(attributes: IJob): IJobDocument; } jobSchema.statics.buildJob = (attributes: IJob) => { @@ -48,4 +48,4 @@ jobSchema.statics.buildJob = (attributes: IJob) => { return new JobModel({ ...rest, _id: jobId }); }; -export const JobModel = mongoose.model('Job', jobSchema); +export const JobModel = mongoose.model('Job', jobSchema); diff --git a/admin/src/frameworks/express/routes/candidate.ts b/admin/src/frameworks/express/routes/candidate.ts index 50a9844..371b053 100644 --- a/admin/src/frameworks/express/routes/candidate.ts +++ b/admin/src/frameworks/express/routes/candidate.ts @@ -8,7 +8,6 @@ export const candidateRouter = (dependencies: IDependency) => { const candidateController = candidateControllers(dependencies); // candidate - router.get('/candidates/search/:page/:limit', candidateController.searchCandidatesController); router.get('/candidates/:page/:limit', candidateController.getAllCandidatesController); router.get('/view-profile/:userId', candidateController.getCandidateByIdController); router.put('/block-unblock/:userId', candidateController.candidateBlockUnblockController); diff --git a/admin/src/frameworks/express/routes/index.ts b/admin/src/frameworks/express/routes/index.ts index ed69451..fc37111 100644 --- a/admin/src/frameworks/express/routes/index.ts +++ b/admin/src/frameworks/express/routes/index.ts @@ -8,6 +8,7 @@ import { membershipPlanRouter } from './membershipplan'; import { jobRouter } from './job'; import { paymentRouter } from './payment'; import { IDependency } from '../../types/dependency'; +import { searchRouter } from './search'; export const routes = (dependencies: IDependency) => { const router = express.Router(); @@ -18,6 +19,7 @@ export const routes = (dependencies: IDependency) => { const membershipPlan = membershipPlanRouter(dependencies); const job = jobRouter(dependencies); const payment = paymentRouter(dependencies); + const search = searchRouter(dependencies); // checkCurrentUser extract current user from jwt, if user is present add it to req.currentUser // In admin service every routes are protected for admin. @@ -31,6 +33,7 @@ export const routes = (dependencies: IDependency) => { router.use('/membership', membershipPlan); router.use('/job', job); router.use('/payment', payment); + router.use('/search', search); return router; }; diff --git a/admin/src/frameworks/express/routes/recruiter.ts b/admin/src/frameworks/express/routes/recruiter.ts index 60cabdb..1ae4df5 100644 --- a/admin/src/frameworks/express/routes/recruiter.ts +++ b/admin/src/frameworks/express/routes/recruiter.ts @@ -8,7 +8,6 @@ export const recruiterRouter = (dependencies: IDependency) => { const recruiterController = recruiterControllers(dependencies); router.get('/recruiters/:page/:limit', recruiterController.getAllRecruitersController); - router.get('/recruiters/search/:page/:limit', recruiterController.searchRecruitersController); router.get('/view-profile/:userId', recruiterController.getRecruiterByIdController); router.put('/block-unblock/:userId', recruiterController.recruiterBlockUnblockController); diff --git a/admin/src/frameworks/express/routes/search.ts b/admin/src/frameworks/express/routes/search.ts new file mode 100644 index 0000000..1b3adc0 --- /dev/null +++ b/admin/src/frameworks/express/routes/search.ts @@ -0,0 +1,13 @@ +import express, { Router } from 'express'; +import { IDependency } from '../../types/dependency'; +import { searchControllers } from '../../../controllers'; + +export const searchRouter = (dependencies: IDependency) => { + const router: Router = express.Router(); + + const searchController = searchControllers(dependencies); + + router.get('/:type/:page/:limit/', searchController.searchController); + + return router; +}; diff --git a/admin/src/frameworks/repositories/mongo/candidate.repository.ts b/admin/src/frameworks/repositories/mongo/candidate.repository.ts index d1a99cc..62f447f 100644 --- a/admin/src/frameworks/repositories/mongo/candidate.repository.ts +++ b/admin/src/frameworks/repositories/mongo/candidate.repository.ts @@ -44,7 +44,7 @@ export = { searchKey: string, skip: number, limit: number, - ): Promise => { + ): Promise => { return await CandidateModel.find({ name: { @@ -57,7 +57,6 @@ export = { }, getCountOfSearchedCandidates: async (searchKey: string): Promise => { - return await CandidateModel.countDocuments({ name: { $regex: new RegExp(searchKey, 'i') } }); }, }; diff --git a/admin/src/frameworks/repositories/mongo/job.repository.ts b/admin/src/frameworks/repositories/mongo/job.repository.ts index a044cde..d6f6f57 100644 --- a/admin/src/frameworks/repositories/mongo/job.repository.ts +++ b/admin/src/frameworks/repositories/mongo/job.repository.ts @@ -1,13 +1,14 @@ -import { JobDocument, JobModel } from '../../database/models'; +import { IJobDocument, JobModel } from '../../database/models'; import { IJob } from '../../types/job'; +const JOB_SELECT_FIELDS = ['title', 'companyLocation', 'isActive']; export = { - createJob: async (jobData: IJob): Promise => { + createJob: async (jobData: IJob): Promise => { const jobObject = JobModel.buildJob(jobData); return await jobObject.save(); }, - blockUnblock: async (jobId: string): Promise => { + blockUnblock: async (jobId: string): Promise => { const job = await JobModel.findById(jobId); if (!job) throw new Error('job not found'); @@ -16,17 +17,17 @@ export = { return await job.save(); }, - updateJob: async (jobId: string, data: Partial): Promise => { + updateJob: async (jobId: string, data: Partial): Promise => { const updatedJob = await JobModel.findOneAndUpdate({ jobId: jobId }, { $set: data }, { new: true }); return updatedJob; }, - getById: async (jobId: string): Promise => { + getById: async (jobId: string): Promise => { return await JobModel.findById(jobId); }, - deleteJob: async (jobId: string): Promise => { + deleteJob: async (jobId: string): Promise => { const deletedJob = await JobModel.findByIdAndUpdate( jobId, { $set: { isDeleted: true } }, @@ -37,11 +38,26 @@ export = { return deletedJob; }, - getAllJobs: async (skip: number, limit: number): Promise => { - return await JobModel.find({}).skip(skip).limit(limit); + getAllJobs: async (skip: number, limit: number): Promise => { + return await JobModel.find({}).select(JOB_SELECT_FIELDS).skip(skip).limit(limit); }, getCountOfJobs: async (): Promise => { return await JobModel.countDocuments(); }, + + searchJobs: async (searchKey: string, skip: number, limit: number): Promise => { + return await JobModel.find({ + title: { + $regex: new RegExp(searchKey, 'i'), + }, + }) + .select(JOB_SELECT_FIELDS) + .skip(skip) + .limit(limit); + }, + + getCountOfSearchedJobs: async (searchKey: string): Promise => { + return await JobModel.countDocuments({ title: { $regex: new RegExp(searchKey, 'i') } }); + }, }; diff --git a/admin/src/frameworks/repositories/mongo/membership.repository.ts b/admin/src/frameworks/repositories/mongo/membership.repository.ts index 9c4fb1d..6ca27c9 100644 --- a/admin/src/frameworks/repositories/mongo/membership.repository.ts +++ b/admin/src/frameworks/repositories/mongo/membership.repository.ts @@ -1,6 +1,8 @@ import { IMembershipPlanData } from '../../../entities/membership-plan'; import { IMembershipPlansDocument, MembershipPlansModel } from '../../database/models'; +const MEMBERSHIP_SELECT_FIELDS = ["name", "price","isActive"] + export = { createMembershipPlan: async ( membershipPlanData: IMembershipPlanData, @@ -58,11 +60,25 @@ export = { const membershipPlans = await MembershipPlansModel.find({}) .skip(skip) .limit(limit) - .select(['name', 'price']); + .select(MEMBERSHIP_SELECT_FIELDS); return membershipPlans; }, getCountOfMembershipPlans: async (): Promise => { return await MembershipPlansModel.countDocuments(); }, + + searchMembershipPlans: async (searchKey: string, skip: number, limit: number): Promise => { + return await MembershipPlansModel.find({ + name: { + $regex: new RegExp(searchKey, 'i'), + }, + }).select(MEMBERSHIP_SELECT_FIELDS) + .skip(skip) + .limit(limit); + }, + + getCountOfSearchedMembershipPlans: async (searchKey: string): Promise => { + return await MembershipPlansModel.countDocuments({ name: { $regex: new RegExp(searchKey, 'i') } }); + }, }; diff --git a/admin/src/frameworks/repositories/mongo/payment.repository.ts b/admin/src/frameworks/repositories/mongo/payment.repository.ts index eca0b3a..5b323e1 100644 --- a/admin/src/frameworks/repositories/mongo/payment.repository.ts +++ b/admin/src/frameworks/repositories/mongo/payment.repository.ts @@ -43,19 +43,6 @@ export = { } }, - getAllPayments: async (skip: number, limit: number): Promise => { - const payments = await PaymentModel.find({}) - .skip(skip) - .limit(limit) - .populate('candidateId', ['name', 'email']) - .populate('membershipPlanId', ['name', 'price']); - return payments; - }, - - getCountOfPayments: async (): Promise => { - return await PaymentModel.countDocuments(); - }, - // populate graph data for admin getGraphData: async () => { const monthlyPayments = await PaymentModel.aggregate([ @@ -96,4 +83,110 @@ export = { return monthlyPayments; }, + + getAllPayments: async (skip: number, limit: number): Promise => { + const payments = await PaymentModel.find({}) + .skip(skip) + .limit(limit) + .populate('candidateId', ['name', 'email']) + .populate('membershipPlanId', ['name', 'price']); + return payments; + }, + + getCountOfPayments: async (): Promise => { + return await PaymentModel.countDocuments(); + }, + + searchPayments: async ( + searchKey: string, + skip: number, + limit: number, + ): Promise => { + return await PaymentModel.aggregate([ + { + $lookup: { + from: 'candidates', + foreignField: '_id', + localField: 'candidateId', + as: 'candidate', + }, + }, + { + $lookup: { + from: 'membershipplans', + foreignField: '_id', + localField: 'membershipPlanId', + as: 'membershipPlan', + }, + }, + { + $unwind: { + path: "$candidate", + preserveNullAndEmptyArrays: true + } + }, + { + $unwind: { + path: "$membershipPlan", + preserveNullAndEmptyArrays: true + } + }, + { + $project: { + candidateId: { + _id: "$candidate._id", + name: "$candidate.name", + email: "$candidate.email", + }, + membershipPlanId: { + _id: "$membershipPlan._id", + name: "$membershipPlan.name", + price: "$membershipPlan.price", + }, + createdAt: 1, + updatedAt: 1, + }, + }, + { + $match: { + 'candidateId.name': { + $regex: new RegExp(searchKey, 'i'), + }, + }, + }, + { $skip: skip }, + { $limit: limit }, + ]); + }, + + getCountOfSearchedPayments: async (searchKey: string): Promise => { + const result = await PaymentModel.aggregate([ + { + $lookup: { + from: 'candidates', + foreignField: '_id', + localField: 'candidateId', + as: 'candidate', + }, + }, + { + $unwind: { + path: "$candidate", + preserveNullAndEmptyArrays: true + } + }, + { + $match: { + 'candidate.name': { + $regex: new RegExp(searchKey, 'i'), + }, + }, + }, + { + $count: "totalCount" + } + ]); + + return result[0]?.totalCount || 0; + }, }; diff --git a/admin/src/frameworks/repositories/mongo/recruiter.repository.ts b/admin/src/frameworks/repositories/mongo/recruiter.repository.ts index 38fee6a..c325e27 100644 --- a/admin/src/frameworks/repositories/mongo/recruiter.repository.ts +++ b/admin/src/frameworks/repositories/mongo/recruiter.repository.ts @@ -44,7 +44,7 @@ export = { searchKey: string, skip: number, limit: number, - ): Promise => { + ): Promise => { return await RecruiterModel.find({ name: { $regex: new RegExp(searchKey, 'i'), diff --git a/admin/src/useCases/candidate/index.ts b/admin/src/useCases/candidate/index.ts index c808621..352acfa 100644 --- a/admin/src/useCases/candidate/index.ts +++ b/admin/src/useCases/candidate/index.ts @@ -1,11 +1,9 @@ import blockUnblockCandidateUseCase from './blockunblock'; import getCandidateProfileByuserIdUseCase from './getCandidate'; import getAllCandidatesUseCase from './getCandidates'; -import searchCandidatesUseCase from './search'; export { blockUnblockCandidateUseCase, getCandidateProfileByuserIdUseCase, getAllCandidatesUseCase, - searchCandidatesUseCase, }; diff --git a/admin/src/useCases/candidate/search.ts b/admin/src/useCases/candidate/search.ts deleted file mode 100644 index 757b1c9..0000000 --- a/admin/src/useCases/candidate/search.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { IDependency } from '../../frameworks/types/dependency'; - -export = (dependencies: IDependency) => { - const { - repositories: { candidateRepository }, - } = dependencies; - - if (!candidateRepository) { - throw new Error('candidateRepository should exist in dependencies'); - } - - const execute = async (searchKey: string, page: number, limit: number) => { - // pagination - const skip = (page - 1) * limit; - - const candidates = await candidateRepository.searchCandidates(searchKey, skip, limit); - const candidatesCount = await candidateRepository.getCountOfSearchedCandidates(searchKey); - const numberOfPages = Math.ceil(candidatesCount / limit); - - return { candidates, numberOfPages }; - }; - - return { execute }; -}; diff --git a/admin/src/useCases/index.ts b/admin/src/useCases/index.ts index afd0fd9..30c198c 100644 --- a/admin/src/useCases/index.ts +++ b/admin/src/useCases/index.ts @@ -9,3 +9,5 @@ export * from './payment'; export * from './membership'; export * from './dashboard'; + +export * from './search'; diff --git a/admin/src/useCases/recruiter/index.ts b/admin/src/useCases/recruiter/index.ts index f96153d..d9eba21 100644 --- a/admin/src/useCases/recruiter/index.ts +++ b/admin/src/useCases/recruiter/index.ts @@ -1,11 +1,9 @@ import blockUnblockRecruiterUseCase from './blockUnblock'; import getRecruiterProfileByuserIdUseCase from './getRecruiter'; import getAllRecruitersUseCase from './getRecruiters'; -import searchRecruitersUseCase from './search'; export { blockUnblockRecruiterUseCase, getRecruiterProfileByuserIdUseCase, getAllRecruitersUseCase, - searchRecruitersUseCase, }; diff --git a/admin/src/useCases/recruiter/search.ts b/admin/src/useCases/recruiter/search.ts deleted file mode 100644 index 6b09bb4..0000000 --- a/admin/src/useCases/recruiter/search.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { IDependency } from '../../frameworks/types/dependency'; - -export = (dependencies: IDependency) => { - const { - repositories: { recruiterRepository }, - } = dependencies; - - if (!recruiterRepository) { - throw new Error('recruiterRepository should exist in dependencies'); - } - - const execute = async (searchKey: string, page: number, limit: number) => { - // pagination - const skip = (page - 1) * limit; - - const recruiters = await recruiterRepository.searchRecruiters(searchKey, skip, limit); - const recruitersCount = await recruiterRepository.getCountOfSearchedRecruiters(searchKey); - const numberOfPages = Math.ceil(recruitersCount / limit); - return { recruiters, numberOfPages }; - }; - - return { execute }; -}; diff --git a/admin/src/useCases/search/index.ts b/admin/src/useCases/search/index.ts new file mode 100644 index 0000000..493f846 --- /dev/null +++ b/admin/src/useCases/search/index.ts @@ -0,0 +1,3 @@ +import searchUseCase from './search'; + +export { searchUseCase }; diff --git a/admin/src/useCases/search/search.ts b/admin/src/useCases/search/search.ts new file mode 100644 index 0000000..ca1d741 --- /dev/null +++ b/admin/src/useCases/search/search.ts @@ -0,0 +1,69 @@ +import { BadRequestError } from '@abijobportal/common'; +import { IDependency } from '../../frameworks/types/dependency'; + +const SEARCH_RESOURCE_TYPES = Object.freeze({ + CANDIDATE: 'candidate', + RECRUITER: 'recruiter', + JOBS: 'jobs', + PAYMENTS: 'payments', + PLANS: 'palns', +}); + +export = (dependencies: IDependency) => { + const { + repositories: { + recruiterRepository, + candidateRepository, + jobRepository, + membershipRepository, + paymentRepository, + }, + } = dependencies; + + if (!recruiterRepository) { + throw new Error('recruiterRepository should exist in dependencies'); + } + + const execute = async (resourceType: string, searchKey: string, page: number, limit: number) => { + // pagination + const skip = (page - 1) * limit; + + let result; + let count: number = 0; + switch (resourceType) { + case SEARCH_RESOURCE_TYPES.CANDIDATE: { + result = await candidateRepository.searchCandidates(searchKey, skip, limit); + count = await candidateRepository.getCountOfSearchedCandidates(searchKey); + break; + } + case SEARCH_RESOURCE_TYPES.RECRUITER: { + result = await recruiterRepository.searchRecruiters(searchKey, skip, limit); + count = await recruiterRepository.getCountOfSearchedRecruiters(searchKey); + break; + } + case SEARCH_RESOURCE_TYPES.JOBS: { + result = await jobRepository.searchJobs(searchKey, skip, limit); + count = await jobRepository.getCountOfSearchedJobs(searchKey); + break; + } + case SEARCH_RESOURCE_TYPES.PAYMENTS: { + result = await paymentRepository.searchPayments(searchKey, skip, limit); + count = await paymentRepository.getCountOfSearchedPayments(searchKey); + break; + } + case SEARCH_RESOURCE_TYPES.PLANS: { + result = await membershipRepository.searchMembershipPlans(searchKey, skip, limit); + count = await membershipRepository.getCountOfSearchedMembershipPlans(searchKey); + break; + } + + default: + throw new BadRequestError('Invalid resoutce type'); + } + + const numberOfPages = Math.ceil(count / limit); + return { result, numberOfPages }; + }; + + return { execute }; +}; diff --git a/client/.dockerignore b/client/.dockerignore index b512c09..b7dab5e 100644 --- a/client/.dockerignore +++ b/client/.dockerignore @@ -1 +1,2 @@ -node_modules \ No newline at end of file +node_modules +build \ No newline at end of file diff --git a/client/src/axios/apiMethods/admin-service/candidates.ts b/client/src/axios/apiMethods/admin-service/candidates.ts index c818b3d..6c66294 100644 --- a/client/src/axios/apiMethods/admin-service/candidates.ts +++ b/client/src/axios/apiMethods/admin-service/candidates.ts @@ -1,6 +1,5 @@ import adminApiUrlConfig from "../../../config/apiUrlsConfig/adminServiceApiUrlConfig"; import { IResponse } from "../../../types/api"; -import { ISearch } from "../../../types/Job"; import makeApiCall from "../../apiCalls"; @@ -8,10 +7,6 @@ export const getAllCandidatesApi = async (page: number, limit: number): Promise< return await makeApiCall("get", adminApiUrlConfig.getAllCandidatesUrl(page, limit)); }; -export const searchCandidatesApi = async (searchData: ISearch, page: number, limit: number): Promise => { - return await makeApiCall("get", adminApiUrlConfig.searchCandidatesUrl(searchData, page, limit)); -}; - export const blockUnblockCandidateApi = async ( userId: string ): Promise => { diff --git a/client/src/axios/apiMethods/admin-service/recruiters.ts b/client/src/axios/apiMethods/admin-service/recruiters.ts index 97262b7..fa008d1 100644 --- a/client/src/axios/apiMethods/admin-service/recruiters.ts +++ b/client/src/axios/apiMethods/admin-service/recruiters.ts @@ -1,6 +1,5 @@ import adminApiUrlConfig from "../../../config/apiUrlsConfig/adminServiceApiUrlConfig"; import { IResponse } from "../../../types/api"; -import { ISearch } from "../../../types/Job"; import makeApiCall from "../../apiCalls"; export const getAllRecruitersApi = async (page: number, limit: number): Promise => { @@ -10,13 +9,6 @@ export const getAllRecruitersApi = async (page: number, limit: number): Promise< ); }; -export const searchRecruitersApi = async (searchData: ISearch, page: number, limit: number): Promise => { - return await makeApiCall( - "get", - adminApiUrlConfig.searchRecruitersUrl( searchData, page, limit) - ); -}; - export const blockUnblockRecruiterApi = async ( userId: string ): Promise => { diff --git a/client/src/axios/apiMethods/admin-service/search.ts b/client/src/axios/apiMethods/admin-service/search.ts new file mode 100644 index 0000000..eec4e97 --- /dev/null +++ b/client/src/axios/apiMethods/admin-service/search.ts @@ -0,0 +1,9 @@ + +import adminApiUrlConfig from "../../../config/apiUrlsConfig/adminServiceApiUrlConfig"; +import { IResponse } from "../../../types/api"; +import makeApiCall from "../../apiCalls"; + + +export const searchApi = async (searchKey: string, resourceType: string ,page: number, limit: number): Promise => { + return await makeApiCall("get", adminApiUrlConfig.getSearchResultsUrl(searchKey, resourceType, page, limit)); +}; diff --git a/client/src/axios/apiMethods/jobs-service/jobs.ts b/client/src/axios/apiMethods/jobs-service/jobs.ts index f5a4f71..b1faf2c 100644 --- a/client/src/axios/apiMethods/jobs-service/jobs.ts +++ b/client/src/axios/apiMethods/jobs-service/jobs.ts @@ -1,7 +1,7 @@ import jobApiUrlConfig from "../../../config/apiUrlsConfig/jobApiUrlConfig"; import { IResponse } from "../../../types/api"; -import { IFilter, IJob, ISearch } from "../../../types/Job"; +import { IFilter, IJob } from "../../../types/Job"; import makeApiCall from "../../apiCalls"; @@ -19,12 +19,6 @@ export const filterJobsApi = async (filterData: IFilter, page: number, limit: nu return await makeApiCall("post", jobApiUrlConfig.filterJobsUrl(page,limit), filterData); }; -// Common for candidate and recruiter -export const serachJobsApi = async (searchData: ISearch, page: number, limit: number): Promise => { - return await makeApiCall("post", jobApiUrlConfig.searchJobsUrl(page,limit), searchData); -}; - - // Candidate export const getAJobCandidateApi = async (id: string): Promise => { return await makeApiCall("get", jobApiUrlConfig.getAJobCandidateUrl(id)); diff --git a/client/src/axios/apiMethods/premium-plans-service/admin.ts b/client/src/axios/apiMethods/premium-plans-service/admin.ts index 814df4e..b0f090a 100644 --- a/client/src/axios/apiMethods/premium-plans-service/admin.ts +++ b/client/src/axios/apiMethods/premium-plans-service/admin.ts @@ -19,5 +19,4 @@ export const getAMembershipPlanApi = async (membershipPlanId: any): Promise => { return await makeApiCall("get", adminApiUrlConfig.getAllMembershipPlansUrl(page, limit)); - }; diff --git a/client/src/components/filterSearch/SearchBar.tsx b/client/src/components/filterSearch/SearchBar.tsx index 06ef61c..577911a 100644 --- a/client/src/components/filterSearch/SearchBar.tsx +++ b/client/src/components/filterSearch/SearchBar.tsx @@ -2,9 +2,10 @@ import { useEffect, useState } from "react"; interface SearchBarProps { onSearch: (searchTerm: string) => void; + placeholder: string } -const SearchBar: React.FC = ({ onSearch }) => { +const SearchBar: React.FC = ({ placeholder, onSearch }) => { const [searchKey, setSearchKey] = useState(""); const [debouncedSearchKey, setDebouncedSearchKey] = useState(""); @@ -12,9 +13,9 @@ const SearchBar: React.FC = ({ onSearch }) => { useEffect(() => { const timer = setTimeout(() => { setDebouncedSearchKey(searchKey); - }, 800); // 800ms debounce time + }, 500); // 500 ms debounce time - // Clear timer if searchKey changes before 800ms + // Clear timer if searchKey changes before 500 ms return () => clearTimeout(timer); }, [searchKey]); @@ -26,8 +27,8 @@ const SearchBar: React.FC = ({ onSearch }) => { return ( setSearchKey(e.target.value.trim())} /> ); diff --git a/client/src/config/apiUrlsConfig/adminServiceApiUrlConfig.ts b/client/src/config/apiUrlsConfig/adminServiceApiUrlConfig.ts index 13c4abd..b85ca23 100644 --- a/client/src/config/apiUrlsConfig/adminServiceApiUrlConfig.ts +++ b/client/src/config/apiUrlsConfig/adminServiceApiUrlConfig.ts @@ -1,5 +1,4 @@ -import { ISearch } from "../../types/Job"; - +const ADMIN_URL = `admin`; const JOB_ADMIN_URL = `admin/job`; // job management in admin service const RECRUITER_ADMIN_URL = `admin/recruiter`; // recruiter management in admin service const CANDIDATE_ADMIN_URL = `admin/candidate`; @@ -18,9 +17,6 @@ const adminApiUrlConfig = { // Recruiter getAllRecruitersUrl: (page: number, limit: number) => `${RECRUITER_ADMIN_URL}/recruiters/${page}/${limit}`, - searchRecruitersUrl: (searchData: ISearch, page: number, limit: number) => - `${RECRUITER_ADMIN_URL}/recruiters/search/${page}/${limit}?searchKey=${searchData.searchKey}`, - blockUnblockRecruiterUrl: (userId: string) => `${RECRUITER_ADMIN_URL}/block-unblock/${userId}`, viewRecruiterProfileDetailsUrl: (userId: string) => @@ -29,9 +25,6 @@ const adminApiUrlConfig = { // Candidate getAllCandidatesUrl: (page: number, limit: number) => `${CANDIDATE_ADMIN_URL}/candidates/${page}/${limit}`, - searchCandidatesUrl: (searchData: ISearch, page: number, limit: number) => - `${CANDIDATE_ADMIN_URL}/candidates/search/${page}/${limit}?searchKey=${searchData.searchKey}`, - blockUnblockCandidateUrl: (userId: string) => `${CANDIDATE_ADMIN_URL}/block-unblock/${userId}`, viewCandidateProfileDetailsUrl: (userId: string) => @@ -51,9 +44,13 @@ const adminApiUrlConfig = { getAllPaymentsUrl: (page: number, limit: number) => `${ADMIN_PAYMENT_URL}/payments/${page}/${limit}`, - // dashboard + // Dashboard getAllCardsDetailsUrl: `${DASHBOARD_ADMIN_URL}/cards-data`, getGraphDataUrl: `${DASHBOARD_ADMIN_URL}/graph-data`, + + // Search + getSearchResultsUrl: (searchKey: string, resourceType: string, page: number, limit: number) => + `${ADMIN_URL}/search/${resourceType}/${page}/${limit}?searchKey=${searchKey}`, }; export default adminApiUrlConfig; diff --git a/client/src/pages/admin/UsersListPage.tsx b/client/src/pages/admin/UsersListPage.tsx index 1a8cd64..44a734a 100644 --- a/client/src/pages/admin/UsersListPage.tsx +++ b/client/src/pages/admin/UsersListPage.tsx @@ -5,19 +5,19 @@ import { notify } from "../../utils/toastMessage"; import { blockUnblockCandidateApi, getAllCandidatesApi, - searchCandidatesApi, } from "../../axios/apiMethods/admin-service/candidates"; import { blockUnblockRecruiterApi, getAllRecruitersApi, - searchRecruitersApi, } from "../../axios/apiMethods/admin-service/recruiters"; +import { searchApi } from "../../axios/apiMethods/admin-service/search"; + import { IResponse } from "../../types/api"; import { IUserData } from "../../types/user"; import { swal } from "../../utils/swal"; import Table from "../../components/table/Table"; -import { ROLES } from "../../utils/constants"; +import { ROLES, SEARCH_RESOURCE_TYPES } from "../../utils/constants"; import SearchBar from "../../components/filterSearch/SearchBar"; function UsersListPage() { @@ -26,7 +26,6 @@ function UsersListPage() { const [numberOfPages, setNumberOfPages] = useState(1); const [currentPage, setCurrentPage] = useState(1); const [searchKey, setSearchKey] = useState(""); - const urlPath = useLocation() const locationUrl = useLocation(); const isCandidateUrl: boolean = locationUrl.pathname.includes( @@ -36,7 +35,7 @@ function UsersListPage() { const USERS_PER_PAGE: number = 2; const fetchUsers = async (currentPage: number) => { - let usersData: IResponse | null = null; + let usersData: IResponse | [] = []; if (isCandidateUrl) { if (!searchKey) { console.log("no search key"); @@ -44,34 +43,32 @@ function UsersListPage() { currentPage, USERS_PER_PAGE ); - } else { - console.log("yes search key"); - - usersData = await searchCandidatesApi( - {searchKey}, + setUsersData(usersData.data.candidates); + } else { + usersData = await searchApi( + searchKey, + SEARCH_RESOURCE_TYPES.CANDIDATE, currentPage, USERS_PER_PAGE ); + setUsersData(usersData.data.result); } - - if (usersData) { - setUsersData(usersData.data.candidates); - } + } else { if (!searchKey) { usersData = await getAllRecruitersApi( currentPage, USERS_PER_PAGE ); + setUsersData(usersData.data.recruiters); } else { - usersData = await searchRecruitersApi( - {searchKey}, + usersData = await searchApi( + searchKey, + SEARCH_RESOURCE_TYPES.RECRUITER, currentPage, USERS_PER_PAGE ); - } - if (usersData) { - setUsersData(usersData.data.recruiters); + setUsersData(usersData.data.result); } } @@ -124,7 +121,7 @@ function UsersListPage() { if (user.id === userId) { return { ...user, - isActive: updatedUser.data.isActive, + isActive: updatedUser?.data.isActive, }; } @@ -194,8 +191,8 @@ function UsersListPage() { ? "Candidates Management" : " Recruiters Management"} -
- +
+
([]); - const [numberOfPages, setNumberOfPages] = useState(0); + const [numberOfPages, setNumberOfPages] = useState(1); + const [currentPage, setCurrentPage] = useState(1); + const [searchKey, setSearchKey] = useState(""); const JOBS_PER_PAGE: number = 2; const fetchJobs = async (currentPage: number) => { - const jobsData: IResponse = await getAllJobsAdminApi(currentPage, JOBS_PER_PAGE); - setJobsData(jobsData.data.jobs); + let jobsData: IResponse | [] = []; + if (!searchKey) { + jobsData = await getAllJobsAdminApi(currentPage, JOBS_PER_PAGE); + setJobsData(jobsData.data.jobs); + } else { + jobsData = await searchApi( + searchKey, + SEARCH_RESOURCE_TYPES.JOBS, + currentPage, + JOBS_PER_PAGE + ); + setJobsData(jobsData.data.result); + } + + if (jobsData) { setNumberOfPages(jobsData.data.numberOfPages); - + } }; + // Reset to page 1 when starting a new search useEffect(() => { - fetchJobs(1); // Fetch initial data for the first page - }, []); + setCurrentPage(1); + }, [searchKey]); + useEffect(() => { + fetchJobs(1); // Fetch initial data for the first page + }, [searchKey, currentPage]); const viewJobDetails = async (jobId: string): Promise => { navigate(`/admin/job/viewJobDetails/${jobId}`); @@ -110,12 +133,17 @@ function JobsManagementPage() { }, ]; - return (

Jobs Management

+
+ +
- +
{jobs.length > 0 ? ( <> @@ -70,7 +79,9 @@ function AllJobsPage() { ) : (
-

No jobs are listed yet

+

+ No jobs are listed yet +

)} diff --git a/client/src/pages/payment/MembershipsListPage.tsx b/client/src/pages/payment/MembershipsListPage.tsx new file mode 100644 index 0000000..1a4b100 --- /dev/null +++ b/client/src/pages/payment/MembershipsListPage.tsx @@ -0,0 +1,247 @@ +import { Modal } from "react-responsive-modal"; +import "react-responsive-modal/styles.css"; + +import { useEffect, useState } from "react"; + +import { notify } from "../../utils/toastMessage"; +import { ErrorMessage, Field, Form, Formik } from "formik"; +import * as Yup from "yup"; +import { + createMembershipPlanApi, + getAllMembershipPlansApi, +} from "../../axios/apiMethods/premium-plans-service/admin"; +import Table from "../../components/table/Table"; +import SearchBar from "../../components/filterSearch/SearchBar"; +import { IResponse } from "../../types/api"; +import { SEARCH_RESOURCE_TYPES } from "../../utils/constants"; +import { searchApi } from "../../axios/apiMethods/admin-service/search"; + +// Formik validation schema using Yup +const validationSchema = Yup.object({ + name: Yup.string().required("Name is required"), + price: Yup.string().required("Price is required"), + description: Yup.string().required("Description is required"), + features: Yup.string().required("Features are required"), +}); + +// Formik initialValues +const initialValues = { + name: "", + price: 0, + description: "", + features: "", +}; + +function MembershipsListPage() { + const [membershipPlansData, setMembershipPlansData] = useState([]); + const [numberOfPages, setNumberOfPages] = useState(1); + const [currentPage, setCurrentPage] = useState(1); + const [searchKey, setSearchKey] = useState(""); + + const PLANS_PER_PAGE: number = 2; + + const columns = [ + { Header: "Name", accessor: "name" }, + { Header: "Price", accessor: "price" }, + ]; + + const fetchMembershipPlans = async (currentPage: number) => { + let membershipPlans: IResponse | [] = []; + if (!searchKey) { + membershipPlans = await getAllMembershipPlansApi( + currentPage, + PLANS_PER_PAGE + ); + + setMembershipPlansData(membershipPlans.data.membershipPlans); + } else { + membershipPlans = await searchApi( + searchKey, + SEARCH_RESOURCE_TYPES.PLANS, + currentPage, + PLANS_PER_PAGE + ); + setMembershipPlansData(membershipPlans.data.result); + } + setNumberOfPages(membershipPlans.data.numberOfPages); + }; + + // Reset to page 1 when starting a new search + useEffect(() => { + setCurrentPage(1); + }, [searchKey]); + + useEffect(() => { + fetchMembershipPlans(1); // Fetch initial data for the first page + }, [searchKey, currentPage]); + + const [isModalOpen, setIsModalOpen] = useState(false); + + // Handle modal open/close + const handleModalOpen = () => setIsModalOpen(true); + const handleModalClose = () => setIsModalOpen(false); + + // Handle creating premium membership plan + const handleCreatePremium = async (values: any) => { + const arr: Array = []; + + values.features = values.features.split(",").map((element: string) => { + arr.push(element.trim()); + return element; + }); + const membershipPlans = await createMembershipPlanApi(values); + if (membershipPlans) { + notify(membershipPlans.message, "success"); + } + setMembershipPlansData([...membershipPlansData, membershipPlans.data]); + handleModalClose(); + }; + + return ( +
+
+ Premium Membership Plans +
+ +
+ + + +
+
+ + ); +} + +interface IModal { + initialValues: typeof initialValues; + validationSchema: typeof validationSchema; + handleCreatePremium: (values: typeof initialValues) => void; + handleModalClose: () => void; + isModalOpen: boolean; +} +const ModalComponent = ({ + initialValues, + validationSchema, + handleCreatePremium, + handleModalClose, + isModalOpen, +}: IModal) => { + return ( + +
+

+ Create Premium Membership +

+ { + handleCreatePremium(values); + }} + > +
+ {/* Form fields */} +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ + {/* Buttons */} +
+ + +
+ +
+
+
+ ); +}; + +export default MembershipsListPage; diff --git a/client/src/pages/payment/PaymentSuccessFul.tsx b/client/src/pages/payment/PaymentSuccessFul.tsx index 4f99bc5..89cf626 100644 --- a/client/src/pages/payment/PaymentSuccessFul.tsx +++ b/client/src/pages/payment/PaymentSuccessFul.tsx @@ -2,20 +2,15 @@ import React, { useEffect } from "react"; import { Link } from "react-router-dom"; import CheckmarkSvg from "../../assets/payment/wired-flat-37-approve-checked-simple (3).gif"; import { getCandidateProfileApi } from "../../axios/apiMethods/profile-service/candidate"; -import { useDispatch, useSelector } from "react-redux"; -import { RootState } from "../../redux/reducer"; +import { useDispatch } from "react-redux"; import { setMyProfileData } from "../../redux/slice/user"; const PaymentSuccessFul: React.FC = () => { const dispatch = useDispatch(); - const candidateData: any = useSelector( - (state: RootState) => state.userReducer.authData - ); + useEffect(() => { (async () => { - const candidateProfileData = await getCandidateProfileApi( - candidateData?.id - ); + const candidateProfileData = await getCandidateProfileApi(); dispatch(setMyProfileData(candidateProfileData?.data)); })(); diff --git a/client/src/pages/payment/PaymentsListPage.tsx b/client/src/pages/payment/PaymentsListPage.tsx index 8222451..3748c38 100644 --- a/client/src/pages/payment/PaymentsListPage.tsx +++ b/client/src/pages/payment/PaymentsListPage.tsx @@ -1,10 +1,17 @@ import { useEffect, useState } from 'react'; import Table from '../../components/table/Table'; import { getAllPaymentsApi } from '../../axios/apiMethods/payment-service/admin'; +import SearchBar from '../../components/filterSearch/SearchBar'; +import { IResponse } from '../../types/api'; +import { SEARCH_RESOURCE_TYPES } from '../../utils/constants'; +import { searchApi } from '../../axios/apiMethods/admin-service/search'; const PaymentsListPage = () => { const [paymentsData, setPaymentsData] = useState([]); - const [numberOfPages, setNumberOfPages] = useState(0); + const [numberOfPages, setNumberOfPages] = useState(1); + const [currentPage, setCurrentPage] = useState(1); + const [searchKey, setSearchKey] = useState(""); + const PAYMENTS_PER_PAGE: number = 2; @@ -16,20 +23,34 @@ const PaymentsListPage = () => { ]; const fetchPayments = async (currentPage: number) => { - const paymentList = await getAllPaymentsApi(currentPage, PAYMENTS_PER_PAGE); // Adjusted page size to match rowsPerPage - setPaymentsData(paymentList.data.payments); + let paymentList: IResponse | null = null; + if(!searchKey){ + paymentList = await getAllPaymentsApi(currentPage, PAYMENTS_PER_PAGE); // Adjusted page size to match rowsPerPage + setPaymentsData(paymentList.data.payments); + }else{ + paymentList = await searchApi(searchKey, SEARCH_RESOURCE_TYPES.PAYMENTS, currentPage, PAYMENTS_PER_PAGE); // Adjusted page size to match rowsPerPage + setPaymentsData(paymentList.data.result); + } setNumberOfPages(paymentList.data.numberOfPages); }; + // Reset to page 1 when starting a new search + useEffect(() => { + setCurrentPage(1); + }, [searchKey]); + useEffect(() => { fetchPayments(1); // Fetch initial data for the first page - }, []); + }, [searchKey, currentPage]); return (

Payments Management

+
+ +
([]); - const [numberOfPages, setNumberOfPages] = useState(0); - - const PLANS_PER_PAGE: number = 2; - - const columns = [ - { Header: "Name", accessor: "name" }, - { Header: "Price", accessor: "price" }, - ]; - - const fetchMembershipPlans = async (currentPage: number) => { - const membershipPlans = await getAllMembershipPlansApi( - currentPage, - PLANS_PER_PAGE - ); - setMembershipPlansData(membershipPlans.data.membershipPlans); - setNumberOfPages(membershipPlans.data.numberOfPages); - }; - - useEffect(() => { - fetchMembershipPlans(1); // Fetch initial data for the first page - }, []); - - const [modalIsOpen, setModalIsOpen] = useState(false); - - // Handle modal open/close - const handleModalOpen = () => setModalIsOpen(true); - const handleModalClose = () => setModalIsOpen(false); - - // Handle creating premium membership plan - const handleCreatePremium = async (values: any) => { - const arr: Array = []; - - values.features = values.features.split(",").map((element: string) => { - arr.push(element.trim()); - return element; - }); - const membershipPlans = await createMembershipPlanApi(values); - if (membershipPlans) { - notify(membershipPlans.message, "success"); - } - setMembershipPlansData([...membershipPlansData, membershipPlans.data]); - handleModalClose(); - }; - - // Formik initialValues - const initialValues = { - name: "", - price: 0, - description: "", - features: "", - }; - - // Formik validation schema using Yup - const validationSchema = Yup.object({ - name: Yup.string().required("Name is required"), - price: Yup.string().required("Price is required"), - description: Yup.string().required("Description is required"), - features: Yup.string().required("Features are required"), - }); - - return ( -
-
- Premium Membership Plans -
-
- -
- - -
-

- Create Premium Membership -

- { - handleCreatePremium(values); - }} - > -
- {/* Form fields */} -
- - - -
- -
- - - -
- -
- - - -
- -
- - - -
- - {/* Buttons */} -
- - -
- -
-
-
-
- - ); -} - -export default PremiumMembershipPage; diff --git a/client/src/routes/AdminRoutes.tsx b/client/src/routes/AdminRoutes.tsx index b4d3d00..717a4ab 100644 --- a/client/src/routes/AdminRoutes.tsx +++ b/client/src/routes/AdminRoutes.tsx @@ -21,8 +21,8 @@ const ViewJobDetailsPage = lazy( () => import("../pages/job/admin/ViewJobDetailsPage") ); -const PremiumMembershipPage = lazy( - () => import("../pages/payment/PremiumMembershipPage") +const MembershipsListPage = lazy( + () => import("../pages/payment/MembershipsListPage") ); const PaymentsListPage = lazy( @@ -93,7 +93,7 @@ function AdminRoutes() { /> } + element={} /> } /> diff --git a/client/src/types/Job.ts b/client/src/types/Job.ts index 58c47d0..7ab5dac 100644 --- a/client/src/types/Job.ts +++ b/client/src/types/Job.ts @@ -55,7 +55,3 @@ export interface IFilter { companyLocation: string; employmentType: string; } - -export interface ISearch { - searchKey: string; -} \ No newline at end of file diff --git a/client/src/utils/constants.ts b/client/src/utils/constants.ts index dca8414..13f1eb8 100644 --- a/client/src/utils/constants.ts +++ b/client/src/utils/constants.ts @@ -18,3 +18,12 @@ export const ROLES = Object.freeze({ CANDIDATE: "candidate", RECRUITER: "recruiter", }); + + +export const SEARCH_RESOURCE_TYPES = Object.freeze({ + CANDIDATE: 'candidate', + RECRUITER: 'recruiter', + JOBS: 'jobs', + PAYMENTS: 'payments', + PLANS: 'palns', +}); \ No newline at end of file