crd job + resume list
This commit is contained in:
@@ -187,4 +187,8 @@ export const Pages = {
|
||||
fines: "/financial/fines",
|
||||
createFine: "/financial/create-fine",
|
||||
},
|
||||
jobs: {
|
||||
list: "/jobs/list",
|
||||
resumes: "/jobs/resumes",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
import { type FC, useState } from 'react'
|
||||
import { useGetJobs, useDeleteJob } from './hooks/useJobsData'
|
||||
import { type Job } from './types/Types'
|
||||
import PageLoading from '../../components/PageLoading'
|
||||
import Error from '../../components/Error'
|
||||
import PaginationUi from '../../components/PaginationUi'
|
||||
import Td from '../../components/Td'
|
||||
import TrashWithConfrim from '../../components/TrashWithConfrim'
|
||||
import ModalCreateJob from './components/ModalCreateJob'
|
||||
|
||||
const JobsList: FC = () => {
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const { data: jobsData, isLoading, error, refetch } = useGetJobs(currentPage)
|
||||
const deleteJobMutation = useDeleteJob();
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex justify-center items-center min-h-[400px]">
|
||||
<PageLoading />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex justify-center items-center min-h-[400px]">
|
||||
<Error errorText="خطا در بارگذاری شغلها" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const handleDeleteJob = async (jobId: string) => {
|
||||
await deleteJobMutation.mutateAsync(jobId);
|
||||
refetch()
|
||||
};
|
||||
|
||||
const jobs = jobsData?.results?.jobs || [];
|
||||
const pager = jobsData?.results?.pager;
|
||||
|
||||
const getJobTypeText = (type: string) => {
|
||||
switch (type) {
|
||||
case 'fullTime': return 'تمام وقت';
|
||||
case 'partTime': return 'پاره وقت';
|
||||
case 'contract': return 'قراردادی';
|
||||
default: return type;
|
||||
}
|
||||
};
|
||||
|
||||
const truncateText = (text: string, maxLength: number = 100) => {
|
||||
return text.length > maxLength ? text.substring(0, maxLength) + '...' : text;
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ModalCreateJob />
|
||||
<div className='relative overflow-x-auto rounded-3xl mt-5 w-full'>
|
||||
<table className='w-full text-sm'>
|
||||
<thead className='thead'>
|
||||
<tr>
|
||||
<Td text={'عنوان شغل'} />
|
||||
<Td text={'توضیحات'} />
|
||||
<Td text={'مکان'} />
|
||||
<Td text={'نوع شغل'} />
|
||||
<Td text={'تاریخ ایجاد'} />
|
||||
<Td text={'عملیات'} />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{jobs.length === 0 ? (
|
||||
<tr className='tr'>
|
||||
<td colSpan={6} className="text-center py-8 text-gray-500">
|
||||
هیچ شغلی یافت نشد
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
jobs.map((job: Job) => (
|
||||
<tr key={job._id} className='tr'>
|
||||
<Td text={job.title} />
|
||||
<Td text={truncateText(job.description)} />
|
||||
<Td text={job.location} />
|
||||
<Td text="">
|
||||
<span className="px-2 py-1 rounded-full text-xs bg-blue-100 text-blue-800">
|
||||
{getJobTypeText(job.type)}
|
||||
</span>
|
||||
</Td>
|
||||
<Td text={new Date(job.createdAt).toLocaleDateString('fa-IR')} />
|
||||
<Td text="">
|
||||
<div className="flex items-center gap-2">
|
||||
<TrashWithConfrim
|
||||
onDelete={() => handleDeleteJob(job._id)}
|
||||
isLoading={deleteJobMutation.isPending}
|
||||
/>
|
||||
</div>
|
||||
</Td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<PaginationUi
|
||||
pager={pager}
|
||||
onPageChange={(page) => {
|
||||
setCurrentPage(page);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default JobsList
|
||||
@@ -0,0 +1,132 @@
|
||||
import { type FC, useState } from 'react'
|
||||
import { useGetResumes } from './hooks/useJobsData'
|
||||
import { type Resume } from './types/Types'
|
||||
import PageLoading from '@/components/PageLoading'
|
||||
import Error from '@/components/Error'
|
||||
import PaginationUi from '@/components/PaginationUi'
|
||||
import Td from '@/components/Td'
|
||||
import StatusWithText from '@/components/StatusWithText'
|
||||
import { ArrowLeft2 } from 'iconsax-react'
|
||||
|
||||
const Resumes: FC = () => {
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const { data, isLoading, error } = useGetResumes(currentPage);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex justify-center items-center min-h-[400px]">
|
||||
<PageLoading />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex justify-center items-center min-h-[400px]">
|
||||
<Error errorText="خطا در بارگذاری رزومهها" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const resumes = data?.results?.resumes || [];
|
||||
const pager = data?.results?.pager;
|
||||
|
||||
const getStatusVariant = (status: string) => {
|
||||
switch (status) {
|
||||
case 'Accepted': return 'success';
|
||||
case 'Rejected': return 'error';
|
||||
case 'Pending': return 'warning';
|
||||
default: return 'warning';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
switch (status) {
|
||||
case 'Accepted': return 'تایید شده';
|
||||
case 'Rejected': return 'رد شده';
|
||||
case 'Pending': return 'در انتظار بررسی';
|
||||
default: return status;
|
||||
}
|
||||
};
|
||||
|
||||
const truncateText = (text: string, maxLength: number = 50) => {
|
||||
return text.length > maxLength ? text.substring(0, maxLength) + '...' : text;
|
||||
};
|
||||
|
||||
const handleDownloadResume = (resumeUrl: string, fullName: string) => {
|
||||
const link = document.createElement('a');
|
||||
link.href = resumeUrl;
|
||||
link.download = `${fullName}_resume.pdf`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='relative overflow-x-auto rounded-3xl mt-5 w-full'>
|
||||
<table className='w-full text-sm'>
|
||||
<thead className='thead'>
|
||||
<tr>
|
||||
<Td text={'نام کامل'} />
|
||||
<Td text={'نام پدر'} />
|
||||
<Td text={'محل تولد'} />
|
||||
<Td text={'تاریخ تولد'} />
|
||||
<Td text={'عنوان شغل'} />
|
||||
<Td text={'وضعیت'} />
|
||||
<Td text={'تاریخ ارسال'} />
|
||||
<Td text={'عملیات'} />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{resumes.length === 0 ? (
|
||||
<tr className='tr'>
|
||||
<td colSpan={8} className="text-center py-8 text-gray-500">
|
||||
هیچ رزومهای یافت نشد
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
resumes.map((resume: Resume) => (
|
||||
<tr key={resume._id} className='tr'>
|
||||
<Td text={resume.fullName} />
|
||||
<Td text={resume.fatherName} />
|
||||
<Td text={resume.placeOfBirth} />
|
||||
<Td text={new Date(resume.birthday).toLocaleDateString('fa-IR')} />
|
||||
<Td text={truncateText(resume.job.title)} />
|
||||
<Td text="">
|
||||
<StatusWithText
|
||||
variant={getStatusVariant(resume.status)}
|
||||
text={getStatusText(resume.status)}
|
||||
/>
|
||||
</Td>
|
||||
<Td text={new Date(resume.createdAt).toLocaleDateString('fa-IR')} />
|
||||
<Td text="">
|
||||
<div
|
||||
onClick={() => handleDownloadResume(resume.resumeUrl, resume.fullName)}
|
||||
className="flex items-center gap-2 cursor-pointer">
|
||||
رزرومه
|
||||
<ArrowLeft2
|
||||
color='#8C90A3'
|
||||
size={16}
|
||||
className="cursor-pointer hover:text-blue-500"
|
||||
/>
|
||||
</div>
|
||||
</Td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<PaginationUi
|
||||
pager={pager}
|
||||
onPageChange={(page) => {
|
||||
setCurrentPage(page);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Resumes
|
||||
@@ -0,0 +1,127 @@
|
||||
import Button from '@/components/Button'
|
||||
import DefaulModal from '@/components/DefaulModal'
|
||||
import Input from '@/components/Input'
|
||||
import Select, { type ItemsSelectType } from '@/components/Select'
|
||||
import Textarea from '@/components/Textarea'
|
||||
import { useFormik } from 'formik'
|
||||
import { useState, type FC } from 'react'
|
||||
import * as Yup from 'yup'
|
||||
import type { CreateJobType } from '../types/Types'
|
||||
import { useCreateJob } from '../hooks/useJobsData'
|
||||
|
||||
const ModalCreateJob: FC = () => {
|
||||
|
||||
const createJobMutation = useCreateJob()
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
const jobTypeOptions: ItemsSelectType[] = [
|
||||
{ value: 'fullTime', label: 'تمام وقت' },
|
||||
{ value: 'partTime', label: 'پاره وقت' },
|
||||
{ value: 'contract', label: 'قراردادی' },
|
||||
]
|
||||
|
||||
const formik = useFormik<CreateJobType>({
|
||||
initialValues: {
|
||||
title: '',
|
||||
description: '',
|
||||
location: '',
|
||||
type: 'fullTime',
|
||||
},
|
||||
onSubmit: async (values) => {
|
||||
try {
|
||||
await createJobMutation.mutateAsync(values)
|
||||
setIsOpen(false)
|
||||
formik.resetForm()
|
||||
} catch (error) {
|
||||
console.error('خطا در ایجاد شغل:', error)
|
||||
}
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
title: Yup.string().required('عنوان شغل الزامی است'),
|
||||
description: Yup.string().required('توضیحات شغل الزامی است'),
|
||||
location: Yup.string().required('مکان شغل الزامی است'),
|
||||
type: Yup.string().required('نوع شغل الزامی است'),
|
||||
})
|
||||
})
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setIsOpen(false)
|
||||
formik.resetForm()
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='flex justify-end mt-5'>
|
||||
<Button
|
||||
label='افزودن شغل'
|
||||
onClick={() => setIsOpen(true)}
|
||||
className='w-fit'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DefaulModal
|
||||
open={isOpen}
|
||||
close={handleCloseModal}
|
||||
isHeader={true}
|
||||
title_header='افزودن شغل جدید'
|
||||
width={600}
|
||||
>
|
||||
<form onSubmit={formik.handleSubmit} className='space-y-4 w-[400px] mt-4'>
|
||||
<Input
|
||||
label='عنوان شغل'
|
||||
name='title'
|
||||
value={formik.values.title}
|
||||
onChange={formik.handleChange}
|
||||
error_text={formik.touched.title ? formik.errors.title : undefined}
|
||||
placeholder='عنوان شغل را وارد کنید'
|
||||
/>
|
||||
|
||||
<Textarea
|
||||
label='توضیحات شغل'
|
||||
name='description'
|
||||
value={formik.values.description}
|
||||
onChange={formik.handleChange}
|
||||
error_text={formik.touched.description ? formik.errors.description : undefined}
|
||||
placeholder='توضیحات شغل را وارد کنید'
|
||||
/>
|
||||
|
||||
<Input
|
||||
label='مکان'
|
||||
name='location'
|
||||
value={formik.values.location}
|
||||
onChange={formik.handleChange}
|
||||
error_text={formik.touched.location ? formik.errors.location : undefined}
|
||||
placeholder='مکان شغل را وارد کنید'
|
||||
/>
|
||||
|
||||
<Select
|
||||
label='نوع شغل'
|
||||
name='type'
|
||||
value={formik.values.type}
|
||||
onChange={formik.handleChange}
|
||||
error_text={formik.touched.type ? formik.errors.type : undefined}
|
||||
items={jobTypeOptions}
|
||||
placeholder='نوع شغل را انتخاب کنید'
|
||||
/>
|
||||
|
||||
<div className='flex justify-end gap-3 pt-4'>
|
||||
<Button
|
||||
type='button'
|
||||
label='لغو'
|
||||
onClick={handleCloseModal}
|
||||
className='w-fit bg-gray-100 text-gray-700 hover:bg-gray-200'
|
||||
/>
|
||||
<Button
|
||||
type='submit'
|
||||
label={createJobMutation.isPending ? 'در حال ذخیره...' : 'ذخیره'}
|
||||
disabled={createJobMutation.isPending}
|
||||
className='w-fit'
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</DefaulModal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModalCreateJob
|
||||
@@ -0,0 +1,36 @@
|
||||
import * as api from "../service/JobsService";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
export const useGetJobs = (page: number = 1) => {
|
||||
return useQuery({
|
||||
queryKey: ["jobs", page],
|
||||
queryFn: () => api.getJobs(page),
|
||||
});
|
||||
};
|
||||
|
||||
export const useCreateJob = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: api.createJob,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["jobs"] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteJob = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: api.deleteJob,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["jobs"] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetResumes = (page: number = 1) => {
|
||||
return useQuery({
|
||||
queryKey: ["resumes", page],
|
||||
queryFn: () => api.getResumes(page),
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import axios from "@/config/axios";
|
||||
import type { CreateJobType, GetJobsResponse } from "../types/Types";
|
||||
|
||||
export const getJobs = async (page: number = 1): Promise<GetJobsResponse> => {
|
||||
const { data } = await axios.get(`/jobs?page=${page}`);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const createJob = async (params: CreateJobType) => {
|
||||
const { data } = await axios.post("/admin/settings/jobs", params);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const deleteJob = async (jobId: string) => {
|
||||
const { data } = await axios.delete(`/admin/settings/jobs/${jobId}/delete`);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getResumes = async (page: number = 1) => {
|
||||
const { data } = await axios.get(`/admin/settings/jobs/resume?page=${page}`);
|
||||
return data;
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
export type CreateJobType = {
|
||||
title: string;
|
||||
description: string;
|
||||
location: string;
|
||||
type: "fullTime" | "partTime" | "contract";
|
||||
};
|
||||
|
||||
export type Job = {
|
||||
_id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
location: string;
|
||||
type: "fullTime" | "partTime" | "contract";
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type Pager = {
|
||||
page: number;
|
||||
limit: number;
|
||||
totalItems: number;
|
||||
totalPages: number;
|
||||
prevPage: boolean | null;
|
||||
nextPage: string | null;
|
||||
};
|
||||
|
||||
export type JobsResponse = {
|
||||
pager: Pager;
|
||||
jobs: Job[];
|
||||
};
|
||||
|
||||
export type GetJobsResponse = {
|
||||
status: number;
|
||||
success: boolean;
|
||||
results: JobsResponse;
|
||||
};
|
||||
|
||||
export type Resume = {
|
||||
_id: string;
|
||||
job: Job;
|
||||
fullName: string;
|
||||
fatherName: string;
|
||||
placeOfBirth: string;
|
||||
birthday: string;
|
||||
resumeUrl: string;
|
||||
status: "Pending" | "Accepted" | "Rejected";
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type ResumesResponse = {
|
||||
pager: Pager;
|
||||
resumes: Resume[];
|
||||
};
|
||||
|
||||
export type GetResumesResponse = {
|
||||
status: number;
|
||||
success: boolean;
|
||||
results: ResumesResponse;
|
||||
};
|
||||
@@ -58,6 +58,8 @@ import Pricing from '@/pages/setting/Pricing'
|
||||
import Shop from '@/pages/setting/Shop'
|
||||
import Banners from '@/pages/setting/Banners'
|
||||
import SiteSetting from '@/pages/setting/SiteSetting'
|
||||
import JobsList from '@/pages/jobs/List'
|
||||
import Resumes from '@/pages/jobs/Resume'
|
||||
|
||||
const MainRouter: FC = () => {
|
||||
|
||||
@@ -141,6 +143,9 @@ const MainRouter: FC = () => {
|
||||
<Route path={Pages.setting.shop} element={<Shop />} />
|
||||
<Route path={Pages.setting.banners} element={<Banners />} />
|
||||
<Route path={Pages.setting.siteSetting} element={<SiteSetting />} />
|
||||
|
||||
<Route path={Pages.jobs.list} element={<JobsList />} />
|
||||
<Route path={Pages.jobs.resumes} element={<Resumes />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user