Skip to content
Extraits de code Groupes Projets
Valider 116dfcea rédigé par Mohamed Lemine BAILLAHI's avatar Mohamed Lemine BAILLAHI
Parcourir les fichiers

Merge branch 'feature/HIR-23' into 'develop'

"feature/HIR-23" Add Job Status

Closes HIR-23

See merge request !19
parents 0c9ece03 69bd2374
Branches
1 requête de fusion!19"feature/HIR-23" Add Job Status
Pipeline #3760 réussi avec les étapes
in 7 minutes et 54 secondes
Affichage de
avec 1553 ajouts et 50 suppressions
import { CONFIG } from 'src/config-global';
import { RecruiterProfileView, UserProfileView } from 'src/shared/sections/user/view';
// ----------------------------------------------------------------------
export const metadata = { title: `User profile | Dashboard - ${CONFIG.site.name}` };
export default function Page() {
return <RecruiterProfileView />;
}
......@@ -6,6 +6,7 @@ const ROOTS = {
DASHBOARD: '/dashboard',
FREELANCERS: '/freelancer',
STATS: '/stats',
RECRUITER: '/recruiter',
};
// ----------------------------------------------------------------------
......@@ -98,5 +99,8 @@ export const paths = {
stats: {
root: `${ROOTS.STATS}`,
},
recruiter: {
root: `${ROOTS.DASHBOARD}${ROOTS.RECRUITER}`,
},
},
};
......@@ -96,7 +96,18 @@ Connaissance des bonnes pratiques de sécurité web<br>
`;
export const _freelancer = [...Array(12)].map((_, index) => {
const publish = index % 3 ? 'published' : 'draft';
const getRandomStatus = (indexF: number): string => {
switch (indexF % 3) {
case 0:
return 'publié';
case 1:
return 'en_cours';
case 2:
return 'non_disponible';
default:
return 'publié'; // This should never happen, but TypeScript wants a default
}
};
const salary = {
price: _mock.number.price(index),
......@@ -113,7 +124,7 @@ export const _freelancer = [...Array(12)].map((_, index) => {
return {
id: _mock.id(index),
salary,
publish,
publish: getRandomStatus(index),
company,
skills: ['JavaScript', 'Spring boot', 'Angular'],
content: CONTENT,
......
......@@ -50,54 +50,8 @@ export function JobDetailsToolbar({
>
Back
</Button>
<Box sx={{ flexGrow: 1 }} />
{/* {publish === 'published' && (
<Tooltip title="Go Live">
<IconButton component={RouterLink} href={liveLink}>
<Iconify icon="eva:external-link-fill" />
</IconButton>
</Tooltip>
)}
<Tooltip title="Edit">
<IconButton component={RouterLink} href={editLink}>
<Iconify icon="solar:pen-bold" />
</IconButton>
</Tooltip> */}
{/* <LoadingButton
color="inherit"
variant="contained"
loading={!publish}
loadingIndicator="Loading…"
endIcon={<Iconify icon="eva:arrow-ios-downward-fill" />}
onClick={popover.onOpen}
sx={{ textTransform: 'capitalize' }}
>
{publish}
</LoadingButton> */}
</Stack>
{/*
<CustomPopover open={popover.open} anchorEl={popover.anchorEl} onClose={popover.onClose}>
<MenuList>
{publishOptions.map((option) => (
<MenuItem
key={option.value}
selected={option.value === publish}
onClick={() => {
popover.onClose();
onChangePublish(option.value);
}}
>
{option.value === 'published' && <Iconify icon="eva:cloud-upload-fill" />}
{option.value === 'draft' && <Iconify icon="solar:file-text-bold" />}
{option.label}
</MenuItem>
))}
</MenuList>
</CustomPopover> */}
</>
);
}
......@@ -26,6 +26,7 @@ import { _roles, FREELANCER_BADGE_OPTIONS, FREELANCER_SKILL_OPTIONS } from 'src/
import { toast } from 'src/shared/components/snackbar';
import { Form, Field } from 'src/shared/components/hook-form';
import { FormLabel } from '@mui/material';
// ----------------------------------------------------------------------
......@@ -192,6 +193,17 @@ export function JobNewEditForm({ currentJob }: Props) {
}
/>
</Stack>
<Stack spacing={1.5}>
<FormLabel component="legend">Statut</FormLabel>
<Field.RadioGroup
name="status"
options={[
{ value: 'publié', label: 'Publié' },
{ value: 'en_cours', label: 'En cours' },
{ value: 'non_disponible', label: 'Non disponible' },
]}
/>
</Stack>
<Stack spacing={1.5}>
<Typography variant="subtitle2">Certifs</Typography>
<Field.Autocomplete
......
......@@ -55,8 +55,8 @@ export function JobDetailsView({ job }: Props) {
return (
<DashboardContent>
<JobDetailsToolbar
backLink={paths.dashboard.job.root}
editLink={paths.dashboard.job.edit(`${job?.id}`)}
backLink={paths.dashboard.freelancers.jobs}
editLink={paths.dashboard.freelancers.edit(`${job?.id}`)}
liveLink="#"
publish={publish || ''}
onChangePublish={handleChangePublish}
......
import type { IFreelancerCandidate } from 'src/types/freelancer';
import Box from '@mui/material/Box';
import Card from '@mui/material/Card';
import Stack from '@mui/material/Stack';
import Avatar from '@mui/material/Avatar';
import Tooltip from '@mui/material/Tooltip';
import Pagination from '@mui/material/Pagination';
import IconButton from '@mui/material/IconButton';
import ListItemText from '@mui/material/ListItemText';
import { varAlpha } from 'src/shared/theme/styles';
import { Iconify } from 'src/shared/components/iconify';
import { useEffect } from 'react';
// ----------------------------------------------------------------------
type Props = {
candidates: IFreelancerCandidate[];
};
export function JobDetailsCandidates({ candidates }: Props) {
useEffect(() => {
candidates.forEach((candidat) => {
console.log(candidat.name);
}, []);
});
return (
<>
<Box
gap={3}
display="grid"
gridTemplateColumns={{ xs: 'repeat(1, 1fr)', md: 'repeat(3, 1fr)' }}
>
{candidates.map((candidate) => (
<Card key={candidate.id} sx={{ p: 3, gap: 2, display: 'flex' }}>
<IconButton sx={{ position: 'absolute', top: 8, right: 8 }}>
<Iconify icon="eva:more-vertical-fill" />
</IconButton>
<Avatar alt={candidate.name} src={candidate.avatarUrl} sx={{ width: 48, height: 48 }} />
<Stack spacing={2}>
<ListItemText
primary={candidate.name}
secondary={candidate.role}
secondaryTypographyProps={{
mt: 0.5,
component: 'span',
typography: 'caption',
color: 'text.disabled',
}}
/>
<Stack spacing={1} direction="row">
<IconButton
size="small"
color="error"
sx={{
borderRadius: 1,
bgcolor: (theme) => varAlpha(theme.vars.palette.error.mainChannel, 0.08),
'&:hover': {
bgcolor: (theme) => varAlpha(theme.vars.palette.error.mainChannel, 0.16),
},
}}
>
<Iconify width={18} icon="solar:phone-bold" />
</IconButton>
<IconButton
size="small"
color="info"
sx={{
borderRadius: 1,
bgcolor: (theme) => varAlpha(theme.vars.palette.info.mainChannel, 0.08),
'&:hover': {
bgcolor: (theme) => varAlpha(theme.vars.palette.info.mainChannel, 0.16),
},
}}
>
<Iconify width={18} icon="solar:chat-round-dots-bold" />
</IconButton>
<IconButton
size="small"
color="primary"
sx={{
borderRadius: 1,
bgcolor: (theme) => varAlpha(theme.vars.palette.primary.mainChannel, 0.08),
'&:hover': {
bgcolor: (theme) => varAlpha(theme.vars.palette.primary.mainChannel, 0.16),
},
}}
>
<Iconify width={18} icon="fluent:mail-24-filled" />
</IconButton>
<Tooltip title="Download CV">
<IconButton
size="small"
color="secondary"
sx={{
borderRadius: 1,
bgcolor: (theme) => varAlpha(theme.vars.palette.secondary.mainChannel, 0.08),
'&:hover': {
bgcolor: (theme) =>
varAlpha(theme.vars.palette.secondary.mainChannel, 0.16),
},
}}
>
<Iconify width={18} icon="eva:cloud-download-fill" />
</IconButton>
</Tooltip>
</Stack>
</Stack>
</Card>
))}
</Box>
<Pagination count={10} sx={{ mt: { xs: 5, md: 8 }, mx: 'auto' }} />
</>
);
}
// import { IJobItem } from 'src/shared/types/freelancer';
import type { IFreelancerItem } from 'src/types/freelancer';
import Chip from '@mui/material/Chip';
import Card from '@mui/material/Card';
import Paper from '@mui/material/Paper';
import Stack from '@mui/material/Stack';
import Avatar from '@mui/material/Avatar';
import Grid from '@mui/material/Unstable_Grid2';
import Typography from '@mui/material/Typography';
import ListItemText from '@mui/material/ListItemText';
import { fDate } from 'src/utils/format-time';
import { fCurrency } from 'src/utils/format-number';
import { Iconify } from 'src/shared/components/iconify';
import { Markdown } from 'src/shared/components/markdown';
// ----------------------------------------------------------------------
type Props = {
job?: IFreelancerItem;
};
export function JobDetailsContent({ job }: Props) {
const renderContent = (
<Card sx={{ p: 3, gap: 3, display: 'flex', flexDirection: 'column' }}>
<Typography variant="h4">{job?.title}</Typography>
<Markdown children={job?.content} />
<Markdown children={job?.prestations} />
<Stack spacing={2}>
<Typography variant="h6">Certifications</Typography>
<Stack direction="row" alignItems="center" spacing={1}>
{job?.certifs.map((certif, index) => <Chip key={index} label={certif} variant="soft" />)}
</Stack>
</Stack>
<Markdown children={job?.exigences} />
</Card>
);
const renderOverview = (
<Card sx={{ p: 3, gap: 2, display: 'flex', flexDirection: 'column' }}>
{[
{
label: 'Date de publication',
value: fDate(job?.createdAt),
icon: <Iconify icon="solar:calendar-date-bold" />,
},
{
label: 'Salaire proposé',
value: job?.salary.negotiable ? 'Negotiable' : fCurrency(job?.salary.price),
icon: <Iconify icon="solar:wad-of-money-bold" />,
},
].map((item) => (
<Stack key={item.label} spacing={1.5} direction="row">
{item.icon}
<ListItemText
primary={item.label}
secondary={item.value}
primaryTypographyProps={{ typography: 'body2', color: 'text.secondary', mb: 0.5 }}
secondaryTypographyProps={{
component: 'span',
color: 'text.primary',
typography: 'subtitle2',
}}
/>
</Stack>
))}
</Card>
);
const renderCompany = (
<Paper variant="outlined" sx={{ p: 3, mt: 3, gap: 2, borderRadius: 2, display: 'flex' }}>
<Avatar
alt={job?.company.name}
src={job?.company.logo}
variant="rounded"
sx={{ width: 64, height: 64 }}
/>
<Stack spacing={1}>
<Typography variant="subtitle1">{job?.company.name}</Typography>
<Typography variant="body2">{job?.company.fullAddress}</Typography>
<Typography variant="body2">{job?.company.phoneNumber}</Typography>
</Stack>
</Paper>
);
return (
<Grid container spacing={3}>
<Grid xs={12} md={8}>
{renderContent}
</Grid>
<Grid xs={12} md={4}>
{renderOverview}
{renderCompany}
</Grid>
</Grid>
);
}
import type { StackProps } from '@mui/material/Stack';
import Box from '@mui/material/Box';
import Stack from '@mui/material/Stack';
import Button from '@mui/material/Button';
import Tooltip from '@mui/material/Tooltip';
import MenuList from '@mui/material/MenuList';
import MenuItem from '@mui/material/MenuItem';
import IconButton from '@mui/material/IconButton';
import LoadingButton from '@mui/lab/LoadingButton';
import { RouterLink } from 'src/routes/components';
import { Iconify } from 'src/shared/components/iconify';
import { usePopover, CustomPopover } from 'src/shared/components/custom-popover';
// ----------------------------------------------------------------------
type Props = StackProps & {
backLink: string;
editLink: string;
liveLink: string;
publish: string;
onChangePublish: (newValue: string) => void;
publishOptions: {
value: string;
label: string;
}[];
};
export function JobDetailsToolbar({
publish,
backLink,
editLink,
liveLink,
publishOptions,
onChangePublish,
sx,
...other
}: Props) {
const popover = usePopover();
return (
<>
<Stack spacing={1.5} direction="row" sx={{ mb: { xs: 3, md: 5 }, ...sx }} {...other}>
<Button
component={RouterLink}
href={backLink}
startIcon={<Iconify icon="eva:arrow-ios-back-fill" width={16} />}
>
Back
</Button>
<Box sx={{ flexGrow: 1 }} />
{/* {publish === 'published' && (
<Tooltip title="Go Live">
<IconButton component={RouterLink} href={liveLink}>
<Iconify icon="eva:external-link-fill" />
</IconButton>
</Tooltip>
)}
<Tooltip title="Edit">
<IconButton component={RouterLink} href={editLink}>
<Iconify icon="solar:pen-bold" />
</IconButton>
</Tooltip> */}
{/* <LoadingButton
color="inherit"
variant="contained"
loading={!publish}
loadingIndicator="Loading…"
endIcon={<Iconify icon="eva:arrow-ios-downward-fill" />}
onClick={popover.onOpen}
sx={{ textTransform: 'capitalize' }}
>
{publish}
</LoadingButton> */}
</Stack>
{/*
<CustomPopover open={popover.open} anchorEl={popover.anchorEl} onClose={popover.onClose}>
<MenuList>
{publishOptions.map((option) => (
<MenuItem
key={option.value}
selected={option.value === publish}
onClick={() => {
popover.onClose();
onChangePublish(option.value);
}}
>
{option.value === 'published' && <Iconify icon="eva:cloud-upload-fill" />}
{option.value === 'draft' && <Iconify icon="solar:file-text-bold" />}
{option.label}
</MenuItem>
))}
</MenuList>
</CustomPopover> */}
</>
);
}
import type { IFreelancerFilters } from 'src/types/freelancer';
import type { Theme, SxProps } from '@mui/material/styles';
import type { UseSetStateReturn } from 'src/hooks/use-set-state';
// ----------------------------------------------------------------------
type Props = {
totalResults: number;
sx?: SxProps<Theme>;
filters: UseSetStateReturn<IFreelancerFilters>;
};
export function JobFiltersResult({ filters, totalResults, sx }: Props) {
// const handleRemoveEmploymentTypes = (inputValue: string) => {
// const newValue = filters.state.employmentTypes.filter((item) => item !== inputValue);
// filters.setState({ employmentTypes: newValue });
// };
// const handleRemoveExperience = () => {
// filters.setState({ experience: 'all' });
// };
// const handleRemoveRoles = (inputValue: string) => {
// const newValue = filters.state.roles.filter((item) => item !== inputValue);
// filters.setState({ roles: newValue });
// };
// const handleRemoveLocations = (inputValue: string) => {
// const newValue = filters.state.locations.filter((item) => item !== inputValue);
// filters.setState({ locations: newValue });
// };
// const handleRemoveBenefits = (inputValue: string) => {
// const newValue = filters.state.benefits.filter((item) => item !== inputValue);
// filters.setState({ benefits: newValue });
// };
return (
<div>
<h1>test</h1>
</div>
);
}
import type { IFreelancerFilters } from 'src/types/freelancer';
import type { UseSetStateReturn } from 'src/hooks/use-set-state';
import { useCallback } from 'react';
import Box from '@mui/material/Box';
import Badge from '@mui/material/Badge';
import Drawer from '@mui/material/Drawer';
import Button from '@mui/material/Button';
import Divider from '@mui/material/Divider';
import IconButton from '@mui/material/IconButton';
import Typography from '@mui/material/Typography';
import Slider from '@mui/material/Slider';
import Chip from '@mui/material/Chip';
import Autocomplete from '@mui/material/Autocomplete';
import TextField from '@mui/material/TextField';
import Stack from '@mui/material/Stack';
import { Iconify } from 'src/shared/components/iconify';
import { Scrollbar } from 'src/shared/components/scrollbar';
// ----------------------------------------------------------------------
type Props = {
open: boolean;
canReset: boolean;
onOpen: () => void;
onClose: () => void;
filters: UseSetStateReturn<IFreelancerFilters>;
options: {
salary: [number, number];
totalViews: number;
certifs: string[];
skills: string[];
};
};
export function JobFilters({ open, canReset, onOpen, onClose, filters, options }: Props) {
const handleSalaryChange = useCallback(
(event: Event, newValue: number | number[]) => {
if (Array.isArray(newValue) && newValue.length === 2) {
filters.setState({ salary: newValue as [number, number] });
}
},
[filters]
);
const handleTotalViewsChange = useCallback(
(event: Event, newValue: number | number[]) => {
filters.setState({ totalViews: newValue as number });
},
[filters]
);
const handleCertifsChange = useCallback(
(event: React.SyntheticEvent, newValue: string[]) => {
filters.setState({ certifs: newValue });
},
[filters]
);
const handleSkillsChange = useCallback(
(event: React.SyntheticEvent, newValue: string[]) => {
filters.setState({ skills: newValue });
},
[filters]
);
const renderHead = (
<>
<Box display="flex" alignItems="center" sx={{ py: 2, pr: 1, pl: 2.5 }}>
<Typography variant="h6" sx={{ flexGrow: 1 }}>
Filters
</Typography>
<IconButton onClick={filters.onResetState}>
<Badge color="error" variant="dot" invisible={!canReset}>
<Iconify icon="solar:restart-bold" />
</Badge>
</IconButton>
<IconButton onClick={onClose}>
<Iconify icon="mingcute:close-line" />
</IconButton>
</Box>
<Divider sx={{ borderStyle: 'dashed' }} />
</>
);
const renderSalaryFilter = (
<Box>
<Typography variant="subtitle2" gutterBottom>
Salary Range
</Typography>
<Slider
value={filters.state.salary}
onChange={handleSalaryChange}
valueLabelDisplay="auto"
min={0}
max={2000} // Adjust this max value as needed
step={500}
marks={[
{ value: 0, label: '$0' },
{ value: 500, label: '500€' },
{ value: 1000, label: '1000€' },
{ value: 1500, label: '1500€' },
{ value: 2000, label: '2000€' },
]}
/>
<Box display="flex" justifyContent="space-between">
<Typography variant="caption">{filters.state.salary[0]}</Typography>
<Typography variant="caption">{filters.state.salary[1]}</Typography>
</Box>
</Box>
);
const renderTotalViewsFilter = (
<Box>
<Typography variant="subtitle2" gutterBottom>
Minimum Total Views
</Typography>
<Slider
value={filters.state.totalViews}
onChange={handleTotalViewsChange}
valueLabelDisplay="auto"
min={0}
max={options.totalViews}
/>
</Box>
);
const renderCertifsFilter = (
<Box>
<Typography variant="subtitle2" gutterBottom>
Certifications
</Typography>
<Autocomplete
multiple
options={options.certifs}
value={filters.state.certifs}
onChange={handleCertifsChange}
renderInput={(params) => <TextField {...params} variant="outlined" />}
renderTags={(value, getTagProps) =>
value.map((option, index) => (
<Chip variant="outlined" label={option} {...getTagProps({ index })} />
))
}
/>
</Box>
);
const renderSkillsFilter = (
<Box>
<Typography variant="subtitle2" gutterBottom>
Skills
</Typography>
<Autocomplete
multiple
options={options.skills}
value={filters.state.skills}
onChange={handleSkillsChange}
renderInput={(params) => <TextField {...params} variant="outlined" />}
renderTags={(value, getTagProps) =>
value.map((option, index) => (
<Chip variant="outlined" label={option} {...getTagProps({ index })} />
))
}
/>
</Box>
);
return (
<>
<Button
disableRipple
color="inherit"
endIcon={
<Badge color="error" variant="dot" invisible={!canReset}>
<Iconify icon="ic:round-filter-list" />
</Badge>
}
onClick={onOpen}
>
Filters
</Button>
<Drawer
anchor="right"
open={open}
onClose={onClose}
slotProps={{ backdrop: { invisible: true } }}
PaperProps={{ sx: { width: 320 } }}
>
{renderHead}
<Scrollbar sx={{ px: 2.5, py: 3 }}>
<Stack spacing={3}>
{renderSalaryFilter}
{renderTotalViewsFilter}
{renderCertifsFilter}
{renderSkillsFilter}
</Stack>
</Scrollbar>
</Drawer>
</>
);
}
import type { IFreelancerItem } from 'src/types/freelancer';
import React from 'react';
import Box from '@mui/material/Box';
import Link from '@mui/material/Link';
import Card from '@mui/material/Card';
import Typography from '@mui/material/Typography';
import ListItemText from '@mui/material/ListItemText';
import { paths } from 'src/routes/paths';
import { RouterLink } from 'src/routes/components';
import { Avatar, Button, ButtonProps, IconButton, MenuItem, MenuList } from '@mui/material';
import { CustomPopover, usePopover } from 'src/shared/components/custom-popover';
import { Iconify } from 'src/shared/components/iconify';
// ----------------------------------------------------------------------
function getTimeAgo(date: string | number | Date | null) {
if (date == null) return null;
const now = new Date();
const past = new Date(date);
const diffInSeconds = Math.floor((now.getTime() - past.getTime()) / 1000);
if (diffInSeconds < 60) return "à l'instant";
if (diffInSeconds < 3600) return `il y a ${Math.floor(diffInSeconds / 60)} minute(s)`;
if (diffInSeconds < 86400) return `il y a ${Math.floor(diffInSeconds / 3600)} heure(s)`;
if (diffInSeconds < 2592000) return `il y a ${Math.floor(diffInSeconds / 86400)} jour(s)`;
if (diffInSeconds < 31536000) return `il y a ${Math.floor(diffInSeconds / 2592000)} mois`;
return `il y a ${Math.floor(diffInSeconds / 31536000)} an(s)`;
}
const StatusButton = ({ status }: { status: string }) => {
let color: ButtonProps['color'];
let text;
let backgroundColor;
switch (status) {
case 'publié':
color = 'success';
text = 'Publié';
backgroundColor = 'success.main';
break;
case 'non_disponible':
color = 'warning';
text = 'En cours';
backgroundColor = 'warning.main';
break;
default:
color = 'error';
text = 'Pas encore disponible';
backgroundColor = 'error.main';
}
return (
<Button
variant="contained"
color={color}
size="small"
sx={{
pointerEvents: 'none',
position: 'absolute',
top: 10,
right: 37,
backgroundColor,
'&:hover': {
backgroundColor,
},
}}
>
{text}
</Button>
);
};
type Props = {
job: IFreelancerItem;
onView: () => void;
onEdit: () => void;
onDelete: () => void;
};
export function JobItem({ job, onView, onEdit, onDelete }: Props) {
// const timeAgo = getTimeAgo(job.createdAt);
const popover = usePopover();
const handleMoreClick = (event: React.MouseEvent<HTMLButtonElement>) => {
event.stopPropagation();
popover.onOpen(event);
};
const handleCardClick = (event: React.MouseEvent<HTMLDivElement>) => {
if (!popover.open) {
onView();
}
};
return (
<>
<Card
onClick={onView}
sx={{
cursor: 'pointer',
'&:hover': { bgcolor: 'action.hover' },
position: 'relative',
pt: 5,
}}
>
<IconButton onClick={handleMoreClick} sx={{ position: 'absolute', top: 8, right: 8 }}>
<Iconify icon="eva:more-vertical-fill" />
</IconButton>
<StatusButton status={job.publish as string} />
<Box sx={{ position: 'absolute', left: 16 }}>
<Avatar
src={job.company?.logo || '/path/to/default-logo.png'}
alt={job.company?.name || 'Company Logo'}
variant="rounded"
sx={{ width: 40, height: 40 }}
/>
</Box>
<Box sx={{ px: 10, pb: 3 }}>
<ListItemText
sx={{ mb: 1 }}
primary={
<Link
component={RouterLink}
sx={{ fontSize: '18px' }}
href={paths.dashboard.freelancers.details(job.id)}
color="inherit"
>
{job.title}
</Link>
}
/>
<Typography variant="body2" sx={{ color: 'text.secondary', mb: 1 }}>
{`${job.salary.price ? `${job.salary.price}€` : 'Salaire non spécifié'}${job.totalViews} vues`}
</Typography>
<Typography
variant="body2"
sx={{
color: 'text.secondary',
overflow: 'hidden',
textOverflow: 'ellipsis',
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
lineHeight: '1.5em',
height: '3em',
mb: 1,
}}
>
{job.content.slice(0, 200)}
</Typography>
<Typography variant="body2" sx={{ fontWeight: 'bold', mb: 1 }}>
{job.certifs.join(', ')}
</Typography>
<Typography variant="caption" sx={{ color: 'text.secondary', display: 'block' }}>
{`Il y a ${getTimeAgo(job.createdAt)} • Client #${job.id.slice(0, 8)}`}
</Typography>
</Box>
</Card>
<CustomPopover
open={popover.open}
anchorEl={popover.anchorEl}
onClose={popover.onClose}
slotProps={{ arrow: { placement: 'right-top' } }}
>
<MenuList>
<MenuItem
onClick={() => {
popover.onClose();
onView();
}}
>
<Iconify icon="solar:eye-bold" />
View
</MenuItem>
<MenuItem
onClick={() => {
popover.onClose();
onEdit();
}}
>
<Iconify icon="solar:pen-bold" />
Edit
</MenuItem>
<MenuItem
onClick={() => {
popover.onClose();
onDelete();
}}
sx={{ color: 'error.main' }}
>
<Iconify icon="solar:trash-bin-trash-bold" />
Delete
</MenuItem>
</MenuList>
</CustomPopover>
</>
);
}
import type { IFreelancerItem } from 'src/types/freelancer';
import { useCallback } from 'react';
import Box from '@mui/material/Box';
import Pagination, { paginationClasses } from '@mui/material/Pagination';
import { paths } from 'src/routes/paths';
import { useRouter } from 'src/routes/hooks';
import { JobItem } from './job-item';
// ----------------------------------------------------------------------
type Props = {
jobs: IFreelancerItem[];
};
export function JobList({ jobs }: Props) {
const router = useRouter();
const handleView = useCallback(
(id: string) => {
router.push(paths.dashboard.freelancers.details(id));
},
[router]
);
const handleEdit = useCallback(
(id: string) => {
router.push(paths.dashboard.freelancers.edit(id));
},
[router]
);
const handleDelete = useCallback((id: string) => {
console.info('DELETE', id);
}, []);
return (
<>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
gap: 2, // Espacement vertical entre les éléments
}}
>
{jobs.map((job) => (
<JobItem
key={job.id}
job={job}
onView={() => handleView(job.id)}
onEdit={() => handleEdit(job.id)}
onDelete={() => handleDelete(job.id)}
/>
))}
</Box>
{jobs.length > 8 && (
<Pagination
count={8}
sx={{
mt: { xs: 8, md: 8 },
[`& .${paginationClasses.ul}`]: { justifyContent: 'center' },
}}
/>
)}
</>
);
}
import type { IFreelancerItem } from 'src/types/freelancer';
import { z as zod } from 'zod';
import { useMemo, useEffect } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import Box from '@mui/material/Box';
import Chip from '@mui/material/Chip';
import Card from '@mui/material/Card';
import Stack from '@mui/material/Stack';
// import Paper from '@mui/material/Paper';
import Switch from '@mui/material/Switch';
import Divider from '@mui/material/Divider';
// import ButtonBase from '@mui/material/ButtonBase';
import CardHeader from '@mui/material/CardHeader';
import Typography from '@mui/material/Typography';
import LoadingButton from '@mui/lab/LoadingButton';
import InputAdornment from '@mui/material/InputAdornment';
import FormControlLabel from '@mui/material/FormControlLabel';
import { paths } from 'src/routes/paths';
import { useRouter } from 'src/routes/hooks';
import { _roles, FREELANCER_BADGE_OPTIONS, FREELANCER_SKILL_OPTIONS } from 'src/shared/_mock';
import { toast } from 'src/shared/components/snackbar';
import { Form, Field } from 'src/shared/components/hook-form';
// ----------------------------------------------------------------------
export type NewJobSchemaType = zod.infer<typeof NewJobSchema>;
export const NewJobSchema = zod.object({
title: zod.string().min(1, { message: 'Le titre est requis !' }),
content: zod.string().min(1, { message: 'Le contenu est requis !' }),
prestations: zod.string().min(1, { message: 'La prestations est requis !' }),
exigences: zod.string().min(1, { message: "L'exigences est requis !" }),
certifs: zod.string().array(),
// employmentTypes: zod.string().array().nonempty({ message: 'Choisissez au moins une option !' }),
// role: schemaHelper.objectOrNull<string | null>({
// message: { required_error: 'Le rôle est requis !' },
// }),
// skills: zod.string().array().nonempty({ message: 'Choisissez au moins une option !' }),
// workingSchedule: zod.string().array().nonempty({ message: 'Choisissez au moins une option !' }),
// locations: zod.string().array().nonempty({ message: 'Choisissez au moins une option !' }),
// expiredDate: schemaHelper.date({
// message: { required_error: "La date d'expiration est requise !" },
// }),
salary: zod.object({
price: zod.number().min(1, { message: 'Le montant est requis !' }),
// Not required
// type: zod.string(),
negotiable: zod.boolean(),
}),
// benefits: zod.string().array().nonempty({ message: 'Choisissez au moins une option !' }),
// Not required
// experience: zod.string(),
});
// ----------------------------------------------------------------------
type Props = {
currentJob?: IFreelancerItem;
};
export function JobNewEditForm({ currentJob }: Props) {
const router = useRouter();
const defaultValues = useMemo(
() => ({
title: currentJob?.title || '',
content: currentJob?.content || '',
prestations: currentJob?.prestations || '',
exigences: currentJob?.exigences || '',
certifs: currentJob?.certifs || [],
salary: currentJob?.salary || { price: 0, negotiable: false },
// benefits: currentJob?.benefits || [],
}),
[currentJob]
);
const methods = useForm<NewJobSchemaType>({
mode: 'all',
resolver: zodResolver(NewJobSchema),
defaultValues,
});
const {
reset,
// control,
handleSubmit,
formState: { isSubmitting },
} = methods;
useEffect(() => {
if (currentJob) {
reset(defaultValues);
}
}, [currentJob, defaultValues, reset]);
const onSubmit = handleSubmit(async (data) => {
try {
await new Promise((resolve) => setTimeout(resolve, 500));
reset();
toast.success(currentJob ? 'Mise à jour réussie !' : 'Création réussie !');
router.push(paths.dashboard.freelancers.jobs);
console.info('DATA', data);
} catch (error) {
console.error(error);
}
});
const renderDetails = (
<Card>
<CardHeader title="Détails" subheader="Titre, description, image..." sx={{ mb: 3 }} />
<Divider />
<Stack spacing={3} sx={{ p: 3 }}>
<Stack spacing={1.5}>
<Typography variant="subtitle2">Titre</Typography>
<Field.Text name="title" placeholder="Ex: Ingenieur logiciel..." />
</Stack>
<Stack spacing={1.5}>
<Typography variant="subtitle2">Contenu</Typography>
<Field.Editor
name="content"
placeholder="Création d'un site e-commerce responsive. Intégration catalogue produits, système de paiement sécurisé, gestion des stocks. Optimisation SEO."
sx={{ maxHeight: 480 }}
/>
</Stack>
</Stack>
</Card>
);
const renderProperties = (
<Card>
<CardHeader
title="Propriétés"
// subheader="Additional functions and attributes..."
sx={{ mb: 3 }}
/>
<Divider />
<Stack spacing={3} sx={{ p: 3 }}>
<Stack spacing={1.5}>
<Typography variant="subtitle2">Quelles sont les prestations attendues ?</Typography>
<Field.Editor
name="prestations"
placeholder="Développement front-end et back-end, design UX/UI, mise en place CMS, formation client."
sx={{ maxHeight: 480 }}
/>
</Stack>
<Stack spacing={1.5}>
<Typography variant="subtitle2">Quelles sont vos exigences ?</Typography>
<Field.Editor
name="exigences"
placeholder="Expérience en e-commerce, maîtrise HTML/CSS/JS/PHP, connaissance WooCommerce/Shopify."
sx={{ maxHeight: 480 }}
/>
</Stack>
<Stack spacing={1.5}>
<Typography variant="subtitle2">Compétences</Typography>
<Field.Autocomplete
name="skills"
placeholder="+ Compétences"
multiple
disableCloseOnSelect
options={FREELANCER_SKILL_OPTIONS.map((option) => option)}
getOptionLabel={(option) => option}
renderOption={(props, option) => (
<li {...props} key={option}>
{option}
</li>
)}
renderTags={(selected, getTagProps) =>
selected.map((option, index) => (
<Chip
{...getTagProps({ index })}
key={option}
label={option}
size="small"
color="info"
variant="soft"
/>
))
}
/>
</Stack>
<Stack spacing={1.5}>
<Typography variant="subtitle2">Certifs</Typography>
<Field.Autocomplete
name="certifs"
placeholder="+ Certifs"
multiple
disableCloseOnSelect
options={FREELANCER_BADGE_OPTIONS.map((option) => option)}
getOptionLabel={(option) => option}
renderOption={(props, option) => (
<li {...props} key={option}>
{option}
</li>
)}
renderTags={(selected, getTagProps) =>
selected.map((option, index) => (
<Chip
{...getTagProps({ index })}
key={option}
label={option}
size="small"
color="info"
variant="soft"
/>
))
}
/>
</Stack>
{/* <Stack spacing={1.5}>
<Typography variant="subtitle2">Horaires de travail</Typography>
<Field.Autocomplete
name="Horaires_de_travail"
placeholder="+ Horaires"
multiple
disableCloseOnSelect
options={JOB_WORKING_SCHEDULE_OPTIONS.map((option) => option)}
getOptionLabel={(option) => option}
renderOption={(props, option) => (
<li {...props} key={option}>
{option}
</li>
)}
renderTags={(selected, getTagProps) =>
selected.map((option, index) => (
<Chip
{...getTagProps({ index })}
key={option}
label={option}
size="small"
color="info"
variant="soft"
/>
))
}
/>
</Stack> */}
{/* <Stack spacing={1.5}>
<Typography variant="subtitle2">Lieux</Typography>
<Field.CountrySelect multiple name="locations" placeholder="+ Lieux" />
</Stack>
<Stack spacing={1.5}>
<Typography variant="subtitle2">Expiré</Typography>
<Field.DatePicker name="expiredDate" />
</Stack> */}
<Stack spacing={2}>
<Typography variant="subtitle2">Salaire</Typography>
{/* <Controller
name="salary.type"
control={control}
render={({ field }) => (
<Box gap={2} display="grid" gridTemplateColumns="repeat(2, 1fr)">
{[
{
label: 'Horaire',
icon: <Iconify icon="solar:clock-circle-bold" width={32} sx={{ mb: 2 }} />,
},
{
label: 'Personnalisé',
icon: <Iconify icon="solar:wad-of-money-bold" width={32} sx={{ mb: 2 }} />,
},
].map((item) => (
<Paper
component={ButtonBase}
variant="outlined"
key={item.label}
onClick={() => field.onChange(item.label)}
sx={{
p: 2.5,
borderRadius: 1,
typography: 'subtitle2',
flexDirection: 'column',
...(item.label === field.value && {
borderWidth: 2,
borderColor: 'text.primary',
}),
}}
>
{item.icon}
{item.label}
</Paper>
))}
</Box>
)}
/> */}
<Field.Text
name="salary.price"
placeholder="0.00"
type="number"
InputProps={{
startAdornment: (
<InputAdornment position="start">
<Box sx={{ typography: 'subtitle2', color: 'text.disabled' }}></Box>
</InputAdornment>
),
}}
/>
<Field.Switch name="salary.negotiable" label="Salaire est négociable" />
</Stack>
</Stack>
</Card>
);
const renderActions = (
<Box display="flex" alignItems="center" flexWrap="wrap">
<FormControlLabel
control={<Switch defaultChecked inputProps={{ id: 'publish-switch' }} />}
label="Publish"
sx={{ flexGrow: 1, pl: 3 }}
/>
<LoadingButton
type="submit"
variant="contained"
size="large"
loading={isSubmitting}
sx={{ ml: 2 }}
>
{!currentJob ? "Créer l'emploi" : 'Enregistrer les modifications'}
</LoadingButton>
</Box>
);
return (
<Form methods={methods} onSubmit={onSubmit}>
<Stack spacing={{ xs: 3, md: 5 }} sx={{ mx: 'auto', maxWidth: { xs: 720, xl: 880 } }}>
{renderDetails}
{renderProperties}
{renderActions}
</Stack>
</Form>
);
}
import type { IFreelancerItem } from 'src/types/freelancer';
import type { UseSetStateReturn } from 'src/hooks/use-set-state';
import parse from 'autosuggest-highlight/parse';
import match from 'autosuggest-highlight/match';
import Box from '@mui/material/Box';
import TextField from '@mui/material/TextField';
import Typography from '@mui/material/Typography';
import Autocomplete from '@mui/material/Autocomplete';
import InputAdornment from '@mui/material/InputAdornment';
import { paths } from 'src/routes/paths';
import { useRouter } from 'src/routes/hooks';
import { Iconify } from 'src/shared/components/iconify';
import { SearchNotFound } from 'src/shared/components/search-not-found';
// ----------------------------------------------------------------------
type Props = {
onSearch: (inputValue: string) => void;
search: UseSetStateReturn<{
query: string;
results: IFreelancerItem[];
}>;
};
export function JobSearch({ search, onSearch }: Props) {
const router = useRouter();
const handleClick = (id: string) => {
router.push(paths.dashboard.freelancers.details(id));
};
const handleKeyUp = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (search.state.query) {
if (event.key === 'Enter') {
const selectProduct = search.state.results.filter(
(job) => job.title === search.state.query
)[0];
handleClick(selectProduct.id);
}
}
};
return (
<Autocomplete
sx={{ width: { xs: 1, sm: 260 } }}
autoHighlight
popupIcon={null}
options={search.state.results}
onInputChange={(event, newValue) => onSearch(newValue)}
getOptionLabel={(option) => option.title}
noOptionsText={<SearchNotFound query={search.state.query} />}
isOptionEqualToValue={(option, value) => option.id === value.id}
renderInput={(params) => (
<TextField
{...params}
placeholder="Recherche..."
onKeyUp={handleKeyUp}
InputProps={{
...params.InputProps,
startAdornment: (
<InputAdornment position="start">
<Iconify icon="eva:search-fill" sx={{ ml: 1, color: 'text.disabled' }} />
</InputAdornment>
),
}}
/>
)}
renderOption={(props, job, { inputValue }) => {
const matches = match(job.title, inputValue);
const parts = parse(job.title, matches);
return (
<Box component="li" {...props} onClick={() => handleClick(job.id)} key={job.id}>
<div>
{parts.map((part, index) => (
<Typography
key={index}
component="span"
color={part.highlight ? 'primary' : 'textPrimary'}
sx={{
typography: 'body2',
fontWeight: part.highlight ? 'fontWeightSemiBold' : 'fontWeightMedium',
}}
>
{part.text}
</Typography>
))}
</div>
</Box>
);
}}
/>
);
}
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import MenuList from '@mui/material/MenuList';
import MenuItem from '@mui/material/MenuItem';
import { Iconify } from 'src/shared/components/iconify';
import { usePopover, CustomPopover } from 'src/shared/components/custom-popover';
// ----------------------------------------------------------------------
type Props = {
sort: string;
onSort: (newValue: string) => void;
sortOptions: {
value: string;
label: string;
}[];
};
export function JobSort({ sort, onSort, sortOptions }: Props) {
const popover = usePopover();
return (
<>
<Button
disableRipple
color="inherit"
onClick={popover.onOpen}
endIcon={
<Iconify
icon={popover.open ? 'eva:arrow-ios-upward-fill' : 'eva:arrow-ios-downward-fill'}
/>
}
sx={{ fontWeight: 'fontWeightSemiBold' }}
>
Trier par :
<Box
component="span"
sx={{ ml: 0.5, fontWeight: 'fontWeightBold', textTransform: 'capitalize' }}
>
{sort}
</Box>
</Button>
<CustomPopover open={popover.open} anchorEl={popover.anchorEl} onClose={popover.onClose}>
<MenuList>
{sortOptions.map((option) => (
<MenuItem
key={option.value}
selected={option.value === sort}
onClick={() => {
popover.onClose();
onSort(option.value);
}}
>
{option.label}
</MenuItem>
))}
</MenuList>
</CustomPopover>
</>
);
}
export * from './job-list-view';
export * from './job-edit-view';
export * from './job-create-view';
export * from './job-details-view';
'use client';
import { paths } from 'src/routes/paths';
import { DashboardContent } from 'src/shared/layouts/dashboard';
import { CustomBreadcrumbs } from 'src/shared/components/custom-breadcrumbs';
import { JobNewEditForm } from '../job-new-edit-form';
// ----------------------------------------------------------------------
export function JobCreateView() {
return (
<DashboardContent>
<CustomBreadcrumbs
heading="Create a new job"
links={[
{ name: 'Dashboard', href: paths.dashboard.root },
{ name: 'Profile', href: paths.dashboard.recruiter.root },
{ name: 'New job' },
]}
sx={{ mb: { xs: 3, md: 5 } }}
/>
<JobNewEditForm />
</DashboardContent>
);
}
'use client';
import type { IFreelancerItem } from 'src/types/freelancer';
import { useState, useCallback } from 'react';
import { paths } from 'src/routes/paths';
import Tab from '@mui/material/Tab';
import Tabs from '@mui/material/Tabs';
import { useTabs } from 'src/hooks/use-tabs';
import { DashboardContent } from 'src/shared/layouts/dashboard';
import { FREELANCER_DETAILS_TABS, FREELANCER_PUBLISH_OPTIONS } from 'src/shared/_mock';
import { Label } from 'src/shared/components/label';
import { JobDetailsContent } from '../job-details-content';
import { JobDetailsToolbar } from '../job-details-toolbar';
import { JobDetailsCandidates } from '../job-details-candidates';
// ----------------------------------------------------------------------
type Props = {
job?: IFreelancerItem;
};
export function JobDetailsView({ job }: Props) {
const tabs = useTabs('contenu');
const [publish, setPublish] = useState(job?.publish);
const handleChangePublish = useCallback((newValue: string) => {
setPublish(newValue);
}, []);
const renderTabs = (
<Tabs value={tabs.value} onChange={tabs.onChange} sx={{ mb: { xs: 3, md: 5 } }}>
{FREELANCER_DETAILS_TABS.map((tab) => (
<Tab
key={tab.value}
iconPosition="end"
value={tab.value}
label={tab.label}
icon={
tab.value === 'candidates' ? (
<Label variant="filled">{job?.candidates.length}</Label>
) : (
''
)
}
/>
))}
</Tabs>
);
return (
<DashboardContent>
<JobDetailsToolbar
backLink={paths.dashboard.recruiter.root}
editLink={paths.dashboard.job.edit(`${job?.id}`)}
liveLink="#"
publish={publish || ''}
onChangePublish={handleChangePublish}
publishOptions={FREELANCER_PUBLISH_OPTIONS}
/>
{renderTabs}
{tabs.value === 'contenu' && <JobDetailsContent job={job} />}
{tabs.value === 'candidats' && <JobDetailsCandidates candidates={job?.candidates ?? []} />}
</DashboardContent>
);
}
'use client';
import type { IFreelancerItem } from 'src/types/freelancer';
import { paths } from 'src/routes/paths';
import { DashboardContent } from 'src/shared/layouts/dashboard';
import { CustomBreadcrumbs } from 'src/shared/components/custom-breadcrumbs';
import { JobNewEditForm } from '../job-new-edit-form';
// ----------------------------------------------------------------------
type Props = {
job?: IFreelancerItem;
};
export function JobEditView({ job }: Props) {
return (
<DashboardContent>
<CustomBreadcrumbs
heading="Edit"
links={[
{ name: 'Dashboard', href: paths.dashboard.root },
{ name: 'Profile', href: paths.dashboard.recruiter.root },
{ name: job?.title },
]}
sx={{ mb: { xs: 3, md: 5 } }}
/>
<JobNewEditForm currentJob={job} />
</DashboardContent>
);
}
0% ou .
You are about to add 0 people to the discussion. Proceed with caution.
Terminez d'abord l'édition de ce message.
Veuillez vous inscrire ou vous pour commenter