Selaa lähdekoodia

implemented candidate rejection with comments

pull/132/head
meris.ahmatovic 3 vuotta sitten
vanhempi
commit
4ed2fc261a

BIN
src/assets/images/unsucc.png Näytä tiedosto


+ 3
- 4
src/assets/styles/components/_icon-button.scss Näytä tiedosto

@@ -10,11 +10,10 @@
height: 34px !important;
min-width: none !important;
}
.td-btn.active{
background-color: rgba(100,255,100,0.75) !important;
}
.td-btn.inactive{
background-color: rgba(255,100,100,0.75) !important;
background: #FFEAEE !important;
color: #D72228 !important;
border-color: #D72228 !important;
}

.userPageBtn.activeEnable{

+ 4
- 0
src/assets/styles/components/_modal.scss Näytä tiedosto

@@ -194,4 +194,8 @@ $header-height-mobile: pxToRemMd(74px);
font-size: 16px;
color: $mainBlack;
text-align: center;

&.comment{
padding: 20px 0px 15px 0px;
}
}

+ 11
- 3
src/assets/styles/components/_selectionProcessPage.scss Näytä tiedosto

@@ -122,10 +122,13 @@ h3 {
background-color: $mainBlueLight !important;
transition: .25s;
.status button{
background-color: $mainBlueLight !important;
transition: .25s;
}
}
&.unsucc{
background: #FFEAEE !important;
color: #D72228 !important;
}
background-color: $mainBlueLight !important;
}}

}

@@ -511,6 +514,11 @@ h3 {
font-weight: 500 !important;
text-transform: capitalize !important;
}
.unsucc{
background: #FFEAEE !important;
color: #D72228 !important;
}

.active-process-card-buttons {
overflow: hidden;

+ 183
- 0
src/components/MUI/CommentProcessDialog.js Näytä tiedosto

@@ -0,0 +1,183 @@
import React, { useContext, useRef } from "react";
import PropTypes from "prop-types";
import x from "../../assets/images/x.png";
import { Editor } from "@tinymce/tinymce-react";
import {
Dialog,
DialogTitle,
DialogActions,
useMediaQuery,
useTheme,
DialogContent,
// TextField,
// TextField,
} from "@mui/material";
import IconButton from "../IconButton/IconButton";
// import { DateTimePicker } from "@mui/x-date-pickers/DateTimePicker";
import { useDispatch, useSelector } from "react-redux";
// import { format, isValid } from "date-fns";
// import { fetchInitProcess } from "../../store/actions/candidates/candidatesActions";
import { useEffect } from "react";
import { SelectionContext } from "../../context/SelectionContext";
import { setUpdateStatusReq } from "../../store/actions/processes/processAction";
// import { setUpdateInterviewerReq } from "../../store/actions/processes/processAction";
// import { setUpdateStatusReq } from "../../store/actions/processes/processAction";

const CommentProcessDialog = ({
title,
subtitle,
imgSrc,
onClose,
open,
maxWidth,
fullWidth,
responsive,
}) => {
const { activeProcessUnsuccess, setActiveProcessUnsuccess } = useContext(SelectionContext);

const theme = useTheme();
const fullScreen = useMediaQuery(theme.breakpoints.down("md"));

const editorContent = useRef();

const { isSuccess } = useSelector((s) => s.interviewerUpdate);

const dispatch = useDispatch();

useEffect(() => {
handleClose();
}, [dispatch, isSuccess]);

const handleClose = () => {
onClose();
};

const submitHandler = () => {
dispatch(
setUpdateStatusReq({
data: {
comment: editorContent.current.getContent(),
newStatus: "Neuspešno",
processId: activeProcessUnsuccess.id,
},
responseHandler: apiSuccess,
})
);
};

const apiSuccess = () => {
setActiveProcessUnsuccess(null);
};

return (
<Dialog
maxWidth={maxWidth}
keepMounted={false}
fullWidth={fullWidth}
fullScreen={responsive && fullScreen}
onClose={handleClose}
open={open}
style={{
padding: "0px",
}}
>
{/* {activeInterview?.id} */}
<div style={{ padding: "25px" }}>
<DialogTitle style={{ padding: 0 }}>
{fullScreen ? (
<>
<div className="flex-center" style={{ justifyContent: "start" }}>
<img
style={{
position: "relative",
top: -0.25,
paddingRight: "10px",
}}
src={imgSrc}
/>
<h5 style={{ textAlign: "start" }}>{title}</h5>
<div style={{ justifySelf: "stretch", flex: "1" }}></div>
<IconButton onClick={onClose}>
<img
style={{
position: "relative",
top: -0.25,
}}
src={x}
/>
</IconButton>
</div>
<p
className="dialog-subtitle"
style={{ paddingLeft: "23px", marginTop: "-10px" }}
>
| {subtitle}
</p>
</>
) : (
<div
className="flex-center"
style={{ justifyContent: "space-between" }}
>
<div className="flex-center" style={{ justifyContent: "start" }}>
<img
style={{
position: "relative",
top: -0.25,
paddingRight: "10px",
}}
src={imgSrc}
/>
<h5>{title}</h5>
<div className="vr"></div>
<p className="dialog-subtitle">{subtitle}</p>
</div>
</div>
)}
</DialogTitle>
<DialogContent>
<form className="modal-content comment">
<p style={{width:'100%', textAlign:'left', alignSelf:'flex-start', marginBottom:'10px'}}>Komentar (opciono):</p>
<Editor
onInit={(evt, editor) => (editorContent.current = editor)}
style={{ height: "20px" }}
/>
</form>
</DialogContent>
<DialogActions style={{ padding: 0, justifyContent: "space-between" }}>
{!fullScreen ? (
<IconButton
data-testid="editbtn"
className={`c-btn--primary-outlined interview-btn c-btn dialog-btn`}
onClick={onClose}
>
Otkaži
</IconButton>
) : (
""
)}
<IconButton
data-testid="editbtn"
className={`c-btn--primary-outlined sm-full interview-btn c-btn dialog-btn`}
onClick={submitHandler}
>
Potvrdi
</IconButton>
</DialogActions>
</div>
</Dialog>
);
};

CommentProcessDialog.propTypes = {
title: PropTypes.any,
subtitle: PropTypes.any,
imgSrc: PropTypes.any,
open: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
maxWidth: PropTypes.oneOf(["xs", "sm", "md", "lg", "xl"]),
fullWidth: PropTypes.bool,
responsive: PropTypes.bool,
};

export default CommentProcessDialog;

+ 3
- 3
src/components/MUI/InterviewDialog.js Näytä tiedosto

@@ -33,7 +33,7 @@ const InterviewDialog = ({
responsive,
}) => {
const [selected, setSelected] = useState("");
const [selectedInterviewer, setSelectedInterviewer] = useState("");
const [selectedInterviewer, setSelectedInterviewer] = useState(null);
const [value, setValue] = useState(null);

const theme = useTheme();
@@ -59,7 +59,7 @@ const InterviewDialog = ({

useEffect(() => {
setSelected("");
setSelectedInterviewer("");
setSelectedInterviewer(null);
setValue(null);
}, [onClose]);

@@ -181,7 +181,7 @@ const InterviewDialog = ({
<Select
labelId="demo-simple-select-label"
id="demo-simple-select"
value={selectedInterviewer}
value={selectedInterviewer ? selectedInterviewer : ''}
label="Ime intervjuera (opciono)"
onChange={(e) => {
setSelectedInterviewer(e.target.value);

+ 5
- 1
src/components/Selection/ApplicantSelection.js Näytä tiedosto

@@ -3,6 +3,7 @@ import PropTypes from "prop-types";
import { formatDate } from "../../util/helpers/dateHelpers";
import { Link } from "react-router-dom";
import success from "../../assets/images/svg/success.svg"
import unsucc from "../../assets/images/unsucc.png"

const ApplicantSelection = ({
levelNumber,
@@ -26,6 +27,9 @@ const ApplicantSelection = ({
{(status === "Odrađen") && <div className="active-process-card-logo">
<img src={success} alt="success-image" />
</div>}
{(status === "Neuspešno") && <div className="active-process-card-logo">
<img src={unsucc} alt="success-image" />
</div>}
</div>

<div className="active-process-card-body">
@@ -46,7 +50,7 @@ const ApplicantSelection = ({
</div>

<div className="active-process-card-buttons">
<button>{status}</button>
<button className={status === "Neuspešno" && 'unsucc' }>{status}</button>
</div>

<div className="active-process-card-link">

+ 1
- 1
src/components/Selection/Selection.js Näytä tiedosto

@@ -25,7 +25,7 @@ const Selection = (props) => {
var data = e.dataTransfer.getData("text/plain");
const selectionProcess = JSON.parse(data);

if (selectionProcess.selectionLevelId < selId) {
if (selectionProcess.selectionLevelId < selId && selectionProcess.status !== "Neuspešno") {
dispatch(
setDoneProcessReq({
id: selectionProcess.id,

+ 44
- 12
src/components/Selection/SelectionCard.js Näytä tiedosto

@@ -1,22 +1,34 @@
import React, { useContext, useState } from "react";
import React, { useContext, useEffect, useState } from "react";
import PropTypes from "prop-types";
import { formatDateSrb, formatTimeSrb } from "../../util/helpers/dateHelpers";
import { Button, FormControl, InputLabel, MenuItem, Select } from "@mui/material";
import {
Button,
FormControl,
InputLabel,
MenuItem,
Select,
} from "@mui/material";
import { SelectionContext } from "../../context/SelectionContext";
import { useDispatch } from "react-redux";
import { useDispatch, useSelector } from "react-redux";
import {
setDoneProcessReq,
setUpdateStatusReq,
} from "../../store/actions/processes/processAction";
// import Button from "../Button/Button";

const options = ["Zakazan", "Odrađen", "Čeka na zakazivanje"];
const options = ["Zakazan", "Odrađen", "Čeka na zakazivanje", "Neuspešno"];

const SelectionCard = (props) => {
const [showForm, setShowForm] = useState(false);
const [selected, setSelected] = useState(props.item.status);

const { setActiveProcess, setActiveInterview } = useContext(SelectionContext);
const { success } = useSelector((s) => s.statusUpdate);
const {
setActiveProcess,
setActiveInterview,
setActiveProcessUnsuccess,
// activeProcessUnsuccess,
} = useContext(SelectionContext);
const dispatch = useDispatch();

const statusChange = (e) => {
@@ -26,11 +38,22 @@ const SelectionCard = (props) => {
}
};

useEffect(() => {
setShowForm(false);
}, [success]);

const select = (e) => {
e.stopPropagation();
if (e.target.value === "Zakazan") {
// setovanje context state-a
setActiveProcess({ process: props.item, status: "Zakazan" });
}
// poseban blok u slucaju da treba prikazati odredjeni modal kada je izabrano 'NEUSPESNO'
else if (e.target.value === "Neuspešno") {
setActiveProcessUnsuccess(props.item);
} else if (e.target.value === "Odrađen") {
// ukoliko nije zadnji nivo selekcije kreirati proces za sledeci nivo
// u suprotnom samo promeniti status u odradjeno
if (props.item.selectionLevelId !== 4)
dispatch(
setDoneProcessReq({
@@ -45,8 +68,6 @@ const SelectionCard = (props) => {
dispatch(
setUpdateStatusReq({
data: {
// schedulerId: selected,
// appointment: value,
newStatus: "Odrađen",
processId: props.item.id,
},
@@ -66,7 +87,7 @@ const SelectionCard = (props) => {

const changeInterviewerHandler = (e) => {
e.stopPropagation();
setActiveInterview(props.item)
setActiveInterview(props.item);
};

return (
@@ -106,8 +127,17 @@ const SelectionCard = (props) => {
</FormControl>
</form>
) : (
<div className="status" onClick={(e) => statusChange(e)}>
<button>{props.item.status}</button>
<div
className="status"
onClick={(e) => {
if (props.item.status !== "Neuspešno") statusChange(e);
}}
>
<button
className={props.item.status === "Neuspešno" ? "unsucc" : ""}
>
{props.item.status}
</button>
</div>
)}
<div className="date-name">
@@ -128,11 +158,13 @@ const SelectionCard = (props) => {
</div>
</div>
</div>
{props.item.scheduler ? (
{props.item.scheduler &&
props.item.status !== "Neuspešno" &&
props.item.status !== "Odrađen" ? (
<div className="sel-item-scheduler">
<div className="change-interbtn">
<Button
className="interbtn"
className="interbtn"
onClick={(e) => changeInterviewerHandler(e)}
>
Promeni

+ 10
- 2
src/context/SelectionContext.js Näytä tiedosto

@@ -3,13 +3,21 @@ import PropTypes from "prop-types";

export const SelectionContext = createContext();

export const SelectionProvider = ({children}) => {
export const SelectionProvider = ({ children }) => {
const [activeProcess, setActiveProcess] = useState(null);
const [activeProcessUnsuccess, setActiveProcessUnsuccess] = useState(null);
const [activeInterview, setActiveInterview] = useState(null);

return (
<SelectionContext.Provider
value={{ activeProcess, setActiveProcess, activeInterview, setActiveInterview }}
value={{
activeProcess,
setActiveProcess,
activeInterview,
setActiveInterview,
setActiveProcessUnsuccess,
activeProcessUnsuccess,
}}
>
{children}
</SelectionContext.Provider>

+ 63
- 37
src/pages/SelectionProcessPage/SelectionProcessOfApplicantPage.js Näytä tiedosto

@@ -12,17 +12,18 @@ import "slick-carousel/slick/slick-theme.css";
import { useTheme } from "@emotion/react";
import { useMediaQuery } from "@mui/material";
import ApplicantSelection from "../../components/Selection/ApplicantSelection";
import { setProcessesReq } from "../../store/actions/processes/processesAction"
import { selectProcesses } from "../../store/selectors/processesSelectors"
import { selectApplicant } from "../../store/selectors/applicantWithProcessSelector"
import { setApplicantReq } from "../../store/actions/processes/applicantAction"
import { setProcessesReq } from "../../store/actions/processes/processesAction";
import { selectProcesses } from "../../store/selectors/processesSelectors";
import { selectApplicant } from "../../store/selectors/applicantWithProcessSelector";
import { setApplicantReq } from "../../store/actions/processes/applicantAction";
import parse from "html-react-parser";

const SelectionProcessOfApplicantPage = () => {
const theme = useTheme();
const matches = useMediaQuery(theme.breakpoints.down("sm"));
const applicant = useSelector(selectApplicant)
const applicant = useSelector(selectApplicant);
const processes = applicant?.selectionProcesses;
const levels = useSelector(selectProcesses)
const levels = useSelector(selectProcesses);
const { id } = useParams();
const activeAdsSliderRef = useRef();
const { t } = useTranslation();
@@ -33,7 +34,6 @@ const SelectionProcessOfApplicantPage = () => {
dispatch(setProcessesReq());
}, []);


var settings = {
dots: false,
infinite: false,
@@ -75,25 +75,31 @@ const SelectionProcessOfApplicantPage = () => {
activeAdsSliderRef.current.slickNext();
};


const concatLevels = () => {
const applicantSelections = [];

for (var i = processes.length; i < levels.length; i++) {
applicantSelections.push(<ApplicantSelection
levelNumber={levels[i].id}
levelName={levels[i].name}
schedguler={""}
date={""}
status={"Čeka na zakazivanje"}
id={id}
key={i} />);
applicantSelections.push(
<ApplicantSelection
levelNumber={levels[i].id}
levelName={levels[i].name}
schedguler={""}
date={""}
status={"Čeka na zakazivanje"}
id={id}
key={i}
/>
);
}
applicantSelections.push(<ApplicantSelection key={i} className="hiddenAd"
date={""} />);
applicantSelections.push(
<ApplicantSelection key={i} className="hiddenAd" date={""} />
);
return applicantSelections;
};

console.log(processes);
console.log(applicant?.selectionProcesses);

return (
<>
<div className="l-t-rectangle"></div>
@@ -103,15 +109,15 @@ const SelectionProcessOfApplicantPage = () => {
{processes && processes.length > 0 && (
<div className="active-ads">
<div className="active-ads-header">
<h1>{t("selection.title")}
<span className="level-header-spliter">
|
</span>
<h1>
{t("selection.title")}
<span className="level-header-spliter">|</span>
<span className="level-header-subheader">
{applicant.firstName + " " + applicant.lastName}
</span>
</h1>
</div>

<div className="active-ads-ads">
<div className="active-ads-ads-a">
{!matches && (
@@ -134,17 +140,25 @@ const SelectionProcessOfApplicantPage = () => {
ref={activeAdsSliderRef}
>
{processes.map((process, index) => {
return <ApplicantSelection
levelNumber={index + 1}
levelName={process.selectionLevel.name}
schedguler={`${process?.scheduler?.firstName} ${process?.scheduler?.lastName}`}
link={process.link}
date={new Date(process.date)}
status={process.status}
id={id}
key={index}
className={index === processes.length - 1 ? "active-process" : ""}
/>
return (
<ApplicantSelection
levelNumber={index + 1}
levelName={process.selectionLevel.name}
schedguler={
process?.scheduler
? `${process?.scheduler?.firstName} ${process?.scheduler?.lastName}`
: "Proces nema intervjuera."
}
link={process.link}
date={new Date(process.date)}
status={process.status}
id={id}
key={index}
className={
index === processes.length - 1 ? "active-process" : ""
}
/>
);
})}
{processes.length <= levels.length && concatLevels()}
</Slider>
@@ -164,10 +178,24 @@ const SelectionProcessOfApplicantPage = () => {
)}
<div className="active-process-tip">
<h3>
{t("selection.tipHeader")}
{applicant?.selectionProcesses?.some(
(n) => n.status === "Neuspešno"
)
? "Komentar:"
: t("selection.tipHeader")}
</h3>
<p>
{t("selection.tipBody")}
{/* {processes?.some(n => n.status === "Neuspešno") && parse(processes?.comment)} */}
{applicant?.selectionProcesses?.some(
(n) => n.status === "Neuspešno"
)
? parse(
applicant?.selectionProcesses.find(
(n) => n.status === "Neuspešno"
).comment
)
: t("selection.tipBody")}
{/* {t("selection.tipBody")} */}
</p>
</div>
</div>
@@ -180,6 +208,4 @@ const SelectionProcessOfApplicantPage = () => {
);
};



export default SelectionProcessOfApplicantPage;

+ 14
- 0
src/pages/SelectionProcessPage/SelectionProcessPage.js Näytä tiedosto

@@ -15,6 +15,7 @@ import PropTypes from "prop-types";
import SelectionFilter from "../../components/Selection/SelectionFilter";
import InterviewDialog from "../../components/MUI/InterviewDialog";
import plus from "../../assets/images/plus.png";
import forbiden from "../../assets/images/forbiden.png";
import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns";
import { LocalizationProvider } from "@mui/x-date-pickers";
import { fetchCandidateOptions } from "../../store/actions/candidates/candidatesActions";
@@ -22,6 +23,7 @@ import { SelectionContext } from "../../context/SelectionContext";
import { setUsersReq } from "../../store/actions/users/usersActions";
import StatusDialog from "../../components/MUI/StatusDialog";
import InterviewerDialog from "../../components/MUI/InterviewerDialog";
import CommentProcessDialog from "../../components/MUI/CommentProcessDialog";

const SelectionProcessPage = ({ history }) => {
const [toggleFiltersDrawer, setToggleFiltersDrawer] = useState(false);
@@ -43,6 +45,8 @@ const SelectionProcessPage = ({ history }) => {
setActiveProcess,
activeInterview,
setActiveInterview,
setActiveProcessUnsuccess,
activeProcessUnsuccess,
} = useContext(SelectionContext);

useEffect(() => {
@@ -52,6 +56,7 @@ const SelectionProcessPage = ({ history }) => {
const s = [
{ isChecked: false, name: "Zakazan" },
{ isChecked: false, name: "Odrađen" },
{ isChecked: false, name: "Neuspešno" },
{ isChecked: false, name: "Čeka na zakazivanje" },
{ isChecked: false, name: "Čeka se odgovor" },
];
@@ -106,6 +111,15 @@ const SelectionProcessPage = ({ history }) => {
setActiveProcess(null);
}}
/>
<CommentProcessDialog
open={activeProcessUnsuccess !== null}
title={"Komentar"}
subtitle={"Selekcija"}
imgSrc={forbiden}
onClose={() => {
setActiveProcessUnsuccess(null);
}}
/>
<InterviewerDialog
open={activeInterview !== null}
title={"Promena intervjuera"}

Loading…
Peruuta
Tallenna