crd job + resume list
This commit is contained in:
@@ -187,4 +187,8 @@ export const Pages = {
|
|||||||
fines: "/financial/fines",
|
fines: "/financial/fines",
|
||||||
createFine: "/financial/create-fine",
|
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 Shop from '@/pages/setting/Shop'
|
||||||
import Banners from '@/pages/setting/Banners'
|
import Banners from '@/pages/setting/Banners'
|
||||||
import SiteSetting from '@/pages/setting/SiteSetting'
|
import SiteSetting from '@/pages/setting/SiteSetting'
|
||||||
|
import JobsList from '@/pages/jobs/List'
|
||||||
|
import Resumes from '@/pages/jobs/Resume'
|
||||||
|
|
||||||
const MainRouter: FC = () => {
|
const MainRouter: FC = () => {
|
||||||
|
|
||||||
@@ -141,6 +143,9 @@ const MainRouter: FC = () => {
|
|||||||
<Route path={Pages.setting.shop} element={<Shop />} />
|
<Route path={Pages.setting.shop} element={<Shop />} />
|
||||||
<Route path={Pages.setting.banners} element={<Banners />} />
|
<Route path={Pages.setting.banners} element={<Banners />} />
|
||||||
<Route path={Pages.setting.siteSetting} element={<SiteSetting />} />
|
<Route path={Pages.setting.siteSetting} element={<SiteSetting />} />
|
||||||
|
|
||||||
|
<Route path={Pages.jobs.list} element={<JobsList />} />
|
||||||
|
<Route path={Pages.jobs.resumes} element={<Resumes />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user