crd job + resume list

This commit is contained in:
hamid zarghami
2025-10-04 10:46:58 +03:30
parent 7db97eed6d
commit afc90b3156
8 changed files with 498 additions and 0 deletions
+4
View File
@@ -187,4 +187,8 @@ export const Pages = {
fines: "/financial/fines",
createFine: "/financial/create-fine",
},
jobs: {
list: "/jobs/list",
resumes: "/jobs/resumes",
},
};
+112
View File
@@ -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
+132
View File
@@ -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
+36
View File
@@ -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),
});
};
+22
View File
@@ -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;
};
+60
View File
@@ -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;
};
+5
View File
@@ -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>