| @@ -1,4 +1,4 @@ | |||
| import React, { useState } from "react"; | |||
| import React, { useEffect, useState } from "react"; | |||
| import PropType from "prop-types"; | |||
| import Box from "@mui/material/Box"; | |||
| import Drawer from "@mui/material/Drawer"; | |||
| @@ -8,44 +8,59 @@ import Checkbox from "@mui/material/Checkbox"; | |||
| import Slider from "@mui/material/Slider"; | |||
| import filterIcon from "../../assets/images/filter_vector.png"; | |||
| import x from "../../assets/images/x.png"; | |||
| import { filterCandidates } from "../../store/actions/candidates/candidatesActions"; | |||
| import { | |||
| filterCandidates, | |||
| fetchAdsCandidates | |||
| } from "../../store/actions/candidates/candidatesActions"; | |||
| import { useDispatch } from "react-redux"; | |||
| import { formatDateInput } from "../../util/helpers/dateHelpers"; | |||
| import { changeIsCheckedValue,resetIsCheckedValue } from "../../store/actions/technologies/technologiesActions"; | |||
| const CandidateFilters = ({ open, handleClose,pageSize,currentPage }) => { | |||
| const CandidateFilters = ({ | |||
| open, | |||
| handleClose, | |||
| pageSize, | |||
| currentPage, | |||
| isTableView, | |||
| technologies, | |||
| }) => { | |||
| const dispatch = useDispatch(); | |||
| const [sliderValue, setSliderValue] = useState([0, 0]); | |||
| const [startingDate,setStartingDate] = useState('') | |||
| const [endingDate,setEndingDate] = useState('') | |||
| const [technologies, setTechnologies] = useState([ | |||
| { name: "react", isChecked: false }, | |||
| { name: "angular", isChecked: false }, | |||
| { name: "sql", isChecked: false }, | |||
| { name: "nodejs", isChecked: false }, | |||
| { name: ".net", isChecked: false }, | |||
| { name: "git", isChecked: false }, | |||
| { name: "html/css", isChecked: false }, | |||
| { name: "vanillaJS", isChecked: false }, | |||
| ]); | |||
| const [startingDate, setStartingDate] = useState(""); | |||
| const [endingDate, setEndingDate] = useState(""); | |||
| const [typesOfEmployments, setTypesOfEmployments] = useState([ | |||
| { name: "Posao", isChecked: true }, | |||
| { name: "Posao", isChecked: false }, | |||
| { name: "Intership", isChecked: false }, | |||
| ]); | |||
| const [isInitial, setIsInitial] = useState(true) | |||
| useEffect(() => { | |||
| console.log(isTableView); | |||
| }, [isTableView]); | |||
| const handleSliderChange = (_, newValue) => { | |||
| setSliderValue(newValue); | |||
| }; | |||
| const updateState = (name, value) => { | |||
| const newState = technologies.map((obj) => { | |||
| if (obj.name === name) { | |||
| return { ...obj, isChecked: value }; | |||
| } | |||
| return obj; | |||
| }); | |||
| setTechnologies(newState); | |||
| const resetFilters = () => { | |||
| setTypesOfEmployments([ | |||
| { name: "Posao", isChecked: false }, | |||
| { name: "Intership", isChecked: false }, | |||
| ]); | |||
| setSliderValue([0,0]) | |||
| setEndingDate("") | |||
| setStartingDate("") | |||
| }; | |||
| useEffect(() => { | |||
| if(isInitial) { | |||
| setIsInitial(false) | |||
| return; | |||
| } | |||
| dispatch(resetIsCheckedValue()) | |||
| resetFilters(); | |||
| }, [isTableView]) | |||
| const updateTypeState = (name, value) => { | |||
| const newState = typesOfEmployments.map((obj) => { | |||
| if (obj.name === name) { | |||
| @@ -57,63 +72,97 @@ const CandidateFilters = ({ open, handleClose,pageSize,currentPage }) => { | |||
| setTypesOfEmployments(newState); | |||
| }; | |||
| const handleTechologiesChange = (event) => { | |||
| updateState(event.target.name, event.target.checked); | |||
| const handleTechologiesChange = (e) => { | |||
| const { name } = e.target; | |||
| dispatch(changeIsCheckedValue(name)); | |||
| }; | |||
| const getSelectedEmploymentType = () => { | |||
| return typesOfEmployments.filter((e) => e.isChecked === true); | |||
| }; | |||
| const getSelectedTechnologies = () => { | |||
| let k = technologies.filter(t => t.isChecked === true) | |||
| let n = [] | |||
| k.forEach(technology => { | |||
| n.push(technology.name) | |||
| }); | |||
| return n; | |||
| } | |||
| const handleChangeTypeOfEmployment = (name) => { | |||
| updateTypeState(name, true); | |||
| }; | |||
| const handleApiResponseSuccess = () => { | |||
| handleClose() | |||
| handleClose(); | |||
| }; | |||
| const isThereSelectedFilter = () => { | |||
| let tech = technologies.filter(t => t.isChecked === true); | |||
| if(tech.length > 0){ | |||
| return true; | |||
| } | |||
| let k = typesOfEmployments.filter(te => te.isChecked === true) | |||
| if(k.length > 0){ | |||
| return true | |||
| } | |||
| if(sliderValue[0] !== 0 || sliderValue[1] !== 0) { | |||
| return true | |||
| } | |||
| if(startingDate !== "" || endingDate !== ""){ | |||
| return true; | |||
| } | |||
| return false | |||
| } | |||
| const fiterItems = () => { | |||
| const tech = technologies | |||
| .filter((tech) => tech.isChecked === true) | |||
| .map((tech) => tech.name); | |||
| const selectedEmploymentType = getSelectedEmploymentType(); | |||
| isTableView ? | |||
| dispatch( | |||
| filterCandidates({ | |||
| pageSize, | |||
| currentPage, | |||
| minExperience: sliderValue[0], | |||
| maxExperience: sliderValue[1], | |||
| employmentType: getSelectedEmploymentType()[0].name, | |||
| minDateOfApplication:startingDate, | |||
| maxDateOfApplication:endingDate, | |||
| technologies:getSelectedTechnologies(), | |||
| handleApiResponseSuccess | |||
| employmentType:selectedEmploymentType.length === 0 ? "" : selectedEmploymentType[0].name, | |||
| minDateOfApplication: startingDate, | |||
| maxDateOfApplication: endingDate, | |||
| technologies: tech, | |||
| handleApiResponseSuccess, | |||
| }) | |||
| ) : | |||
| dispatch( | |||
| fetchAdsCandidates({ | |||
| pageSize, | |||
| currentPage, | |||
| minExperience: sliderValue[0], | |||
| maxExperience: sliderValue[1], | |||
| employmentType:selectedEmploymentType.length === 0 ? "" : selectedEmploymentType[0].name, | |||
| minDateOfApplication: startingDate, | |||
| maxDateOfApplication: endingDate, | |||
| technologies: tech, | |||
| handleApiResponseSuccess, | |||
| }) | |||
| ); | |||
| }; | |||
| const handleChangeStartingDate = (event) => { | |||
| console.log(event) | |||
| setStartingDate(event.target.value) | |||
| } | |||
| console.log(event); | |||
| setStartingDate(event.target.value); | |||
| }; | |||
| const handleChangeEndingDate = (event) => { | |||
| console.log(event) | |||
| setEndingDate(event.target.value) | |||
| } | |||
| console.log(event); | |||
| setEndingDate(event.target.value); | |||
| }; | |||
| const getDayBeforeToday = (date = new Date()) => { | |||
| const previous = new Date(date.getTime()); | |||
| previous.setDate(date.getDate() - 1); | |||
| return previous; | |||
| } | |||
| }; | |||
| const list = () => ( | |||
| <Box | |||
| @@ -200,16 +249,28 @@ const CandidateFilters = ({ open, handleClose,pageSize,currentPage }) => { | |||
| <p>Datum prijave</p> | |||
| <div className="filter-date-container"> | |||
| <p>Od</p> | |||
| <input type="date" id="start" max={formatDateInput(getDayBeforeToday())} value={startingDate} onChange={handleChangeStartingDate}/> | |||
| <input | |||
| type="date" | |||
| id="start" | |||
| max={formatDateInput(getDayBeforeToday())} | |||
| value={startingDate} | |||
| onChange={handleChangeStartingDate} | |||
| /> | |||
| </div> | |||
| <div className="filter-date-container"> | |||
| <p>Do</p> | |||
| <input type="date" id="start" max={ formatDateInput(new Date())} value={endingDate} onChange={handleChangeEndingDate}/> | |||
| <input | |||
| type="date" | |||
| id="start" | |||
| max={formatDateInput(new Date())} | |||
| value={endingDate} | |||
| onChange={handleChangeEndingDate} | |||
| /> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| <div className="ad-filters-search" style={{ marginTop: "45px" }}> | |||
| <button className="c-btn c-btn--primary" onClick={fiterItems}> | |||
| <button className="c-btn c-btn--primary" onClick={fiterItems} disabled={!isThereSelectedFilter()}> | |||
| Pretrazi | |||
| </button> | |||
| </div> | |||
| @@ -230,8 +291,10 @@ CandidateFilters.propTypes = { | |||
| open: PropType.any, | |||
| handleClose: PropType.func, | |||
| candidates: PropType.array, | |||
| currentPage:PropType.number, | |||
| pageSize:PropType.number | |||
| currentPage: PropType.number, | |||
| pageSize: PropType.number, | |||
| isTableView: PropType.bool, | |||
| technologies: PropType.array, | |||
| }; | |||
| export default CandidateFilters; | |||
| @@ -13,4 +13,4 @@ export const LEFT_ARROW_KEYCODE = 37; | |||
| export const BACKSPACE_KEYCODE = 8; | |||
| export const TAB_KEYCODE = 9; | |||
| export const PAGE_SIZE_CANDIDATES = 6; | |||
| export const PAGE_SIZE_CANDIDATES = 2; | |||
| @@ -12,15 +12,17 @@ import { MentionsInput, Mention } from "react-mentions"; | |||
| import { fetchCandidate } from "../../store/actions/candidate/candidateActions"; | |||
| import { useMediaQuery } from "@mui/material"; | |||
| import { useTheme } from "@mui/system"; | |||
| import CandidateAd from '../../components/Candidates/CandidateAd'; | |||
| import CandidateAd from "../../components/Candidates/CandidateAd"; | |||
| import Slider from "react-slick"; | |||
| import arrow_left from "../../assets/images/arrow_left.png"; | |||
| import arrow_right from "../../assets/images/arrow_right.png"; | |||
| import { ADS_PAGE } from "../../constants/pages"; | |||
| import { ADS_PAGE, CANDIDATES_PAGE } from "../../constants/pages"; | |||
| import PropTypes from "prop-types"; | |||
| import { deleteCandidate } from "../../request/candidatesRequest"; | |||
| import { deleteCandidate } from "../../store/actions/candidate/candidateActions"; | |||
| import ConfirmDialog from "../../components/MUI/ConfirmDialog"; | |||
| import deleteIcon from "../../assets/images/delete.png"; | |||
| const CandidateDetailsPage = ({history}) => { | |||
| const CandidateDetailsPage = ({ history }) => { | |||
| const dispatch = useDispatch(); | |||
| const { users } = useSelector((s) => s.users); | |||
| const { user } = useSelector((s) => s.user); | |||
| @@ -31,6 +33,7 @@ const CandidateDetailsPage = ({history}) => { | |||
| const theme = useTheme(); | |||
| const matches = useMediaQuery(theme.breakpoints.down("680")); | |||
| const adsSliderRef = useRef(); | |||
| const [showDelete, setDelete] = useState(false); | |||
| useEffect(() => { | |||
| dispatch(fetchCandidate({ id })); | |||
| @@ -143,10 +146,6 @@ const CandidateDetailsPage = ({history}) => { | |||
| adsSliderRef.current.slickNext(); | |||
| }; | |||
| const deleteCurrentCandidate = () => { | |||
| dispatch(deleteCandidate({id})) | |||
| } | |||
| const navigateToDetailsPage = (id) => { | |||
| history.push({ | |||
| pathname: ADS_PAGE + "/" + id, | |||
| @@ -154,13 +153,42 @@ const CandidateDetailsPage = ({history}) => { | |||
| from: history.location.pathname, | |||
| }, | |||
| }); | |||
| } | |||
| }; | |||
| const handleApiResponseSuccessDelete = () => { | |||
| history.push({ | |||
| pathname: CANDIDATES_PAGE, | |||
| state: { | |||
| from: history.location.pathname, | |||
| }, | |||
| }); | |||
| }; | |||
| const deleteCandidateHandler = () => { | |||
| dispatch( | |||
| deleteCandidate({ | |||
| id, | |||
| handleApiResponseSuccess: handleApiResponseSuccessDelete, | |||
| }) | |||
| ); | |||
| }; | |||
| return (candidate && Object.keys(candidate).length === 0) || | |||
| user === undefined ? ( | |||
| <p>Loading...</p> | |||
| ) : ( | |||
| <div className="main-candidate-container pl-144 pt-36px"> | |||
| <ConfirmDialog | |||
| open={showDelete} | |||
| title={"Brisanje kandidata"} | |||
| subtitle={candidate.firstName + " " + candidate.lastName} | |||
| imgSrc={deleteIcon} | |||
| content="Da li ste sigurni da zelite da obrisete kandidata?" | |||
| onClose={() => { | |||
| setDelete(false); | |||
| }} | |||
| onConfirm={deleteCandidateHandler} | |||
| /> | |||
| <div className="l-t-rectangle"></div> | |||
| <div className="r-b-rectangle"></div> | |||
| <div className="top-candidate-container"> | |||
| @@ -172,7 +200,10 @@ const CandidateDetailsPage = ({history}) => { | |||
| </p> | |||
| </div> | |||
| <div className="candidate-option-container"> | |||
| <IconButton className="c-btn c-btn--primary-outlined candidate-btn" onClick={deleteCurrentCandidate}> | |||
| <IconButton | |||
| className="c-btn c-btn--primary-outlined candidate-btn" | |||
| onClick={() => setDelete(true)} | |||
| > | |||
| Obrisi | |||
| <img src={deleteImage} alt="delete" className="candidates-image" /> | |||
| </IconButton> | |||
| @@ -0,0 +1,136 @@ | |||
| import React, { useEffect } from "react"; | |||
| import arrow_left from "../../assets/images/arrow_left.png"; | |||
| import arrow_right from "../../assets/images/arrow_right.png"; | |||
| import adImage from "../../assets/images/.net_icon.png"; | |||
| import Slider from "react-slick"; | |||
| import useDynamicRefs from "use-dynamic-refs"; | |||
| import { fetchAdsCandidates } from "../../store/actions/candidates/candidatesActions"; | |||
| import { useDispatch, useSelector } from "react-redux"; | |||
| import CandidateCard from "../../components/Candidates/CandidateCard"; | |||
| const AdsCandidatesPage = () => { | |||
| const dispatch = useDispatch(); | |||
| const { adsCandidates } = useSelector((s) => s.candidates); | |||
| const [getRef, setRef] = useDynamicRefs(); | |||
| useEffect(() => { | |||
| dispatch( | |||
| fetchAdsCandidates({ | |||
| currentPage: 0, | |||
| pageSize: 0, | |||
| minExperience: 0, | |||
| maxExperience: 0, | |||
| employmentType: "", | |||
| minDateOfApplication: "", | |||
| maxDateOfApplication: "", | |||
| technologies: [], | |||
| }) | |||
| ); | |||
| },[dispatch]); | |||
| var settings = { | |||
| dots: false, | |||
| infinite: false, | |||
| speed: 400, | |||
| slidesToShow: 4, | |||
| slidesToScroll: 4, | |||
| initialSlide: 0, | |||
| arrows: true, | |||
| variableWidth: true, | |||
| responsive: [ | |||
| { | |||
| breakpoint: 1024, | |||
| settings: { | |||
| slidesToShow: 3, | |||
| slidesToScroll: 3, | |||
| infinite: true, | |||
| dots: false, | |||
| }, | |||
| }, | |||
| { | |||
| breakpoint: 900, | |||
| settings: { | |||
| slidesToShow: 2, | |||
| slidesToScroll: 2, | |||
| initialSlide: 0, | |||
| }, | |||
| }, | |||
| { | |||
| breakpoint: 480, | |||
| settings: { | |||
| slidesToShow: 1, | |||
| slidesToScroll: 1, | |||
| }, | |||
| }, | |||
| ], | |||
| }; | |||
| const getDummyCandidates = (len) => { | |||
| const candidates = []; | |||
| for (let i = 0; i < 4 - len + 1; i++) { | |||
| candidates.push( | |||
| <CandidateCard key={i} className="hiddenAd" history={history} /> | |||
| ); | |||
| } | |||
| return candidates; | |||
| }; | |||
| const activeAdsArrowLeftHandler = (index) => { | |||
| getRef(index.toString()).current.slickPrev(); | |||
| }; | |||
| const activeAdsArrowRightHandler = (index) => { | |||
| getRef(index.toString()).current.slickNext(); | |||
| }; | |||
| return ( | |||
| <div className="ads-candidates-container top-cnd"> | |||
| {adsCandidates.map((adCandidates, index) => ( | |||
| <div className="ads-candidates" key={index}> | |||
| <div className="ads-candidates-top-container"> | |||
| <img src={adImage} style={{ width: "49px", height: "39px" }} /> | |||
| <p className="ads-candidates-title">{adCandidates.title}</p> | |||
| <p className="ads-candidates-numberOfApplicants"> | |||
| | {adCandidates.nubmerOfApplicants} prijavljenih | |||
| </p> | |||
| </div> | |||
| <div | |||
| className="ads-candidates-slider" | |||
| style={{ display: "flex", marginTop: "31px" }} | |||
| > | |||
| {adCandidates.applicants.length > 3 && ( | |||
| <div className="active-ads-ads-arrows"> | |||
| <button onClick={() => activeAdsArrowLeftHandler(index)}> | |||
| <img src={arrow_left} alt="arrow-left" /> | |||
| </button> | |||
| <button onClick={() => activeAdsArrowRightHandler(index)}> | |||
| <img src={arrow_right} alt="arrow-right" /> | |||
| </button> | |||
| </div> | |||
| )} | |||
| <Slider | |||
| {...settings} | |||
| ref={setRef(index.toString())} | |||
| style={{ width: "100%" }} | |||
| > | |||
| {adCandidates.applicants.map((candidate, index) => ( | |||
| <CandidateCard | |||
| key={index} | |||
| candidate={candidate} | |||
| history={history} | |||
| className={index === 0 ? "" : "left-move-candidateAd"} | |||
| /> | |||
| ))} | |||
| {adCandidates.applicants.length <= 4 && | |||
| getDummyCandidates(adCandidates.applicants.length)} | |||
| </Slider> | |||
| </div> | |||
| </div> | |||
| ))} | |||
| </div> | |||
| ); | |||
| }; | |||
| export default AdsCandidatesPage; | |||
| @@ -1,64 +1,35 @@ | |||
| import IconButton from "../../components/IconButton/IconButton"; | |||
| import React, { useEffect, useState } from "react"; | |||
| import { useDispatch, useSelector } from "react-redux"; | |||
| import { | |||
| fetchAdsCandidates, | |||
| filterCandidates, | |||
| } from "../../store/actions/candidates/candidatesActions"; | |||
| import tableImage from "../../assets/images/table.png"; | |||
| import adImage from "../../assets/images/.net_icon.png"; | |||
| import filterImage from "../../assets/images/filters.png"; | |||
| import { formatDate } from "../../util/helpers/dateHelpers"; | |||
| import planeImage from "../../assets/images/planeVector.png"; | |||
| import arrow_left from "../../assets/images/arrow_left.png"; | |||
| import arrow_right from "../../assets/images/arrow_right.png"; | |||
| import { useMediaQuery } from "@mui/material"; | |||
| import { useTheme } from "@mui/system"; | |||
| import { CANDIDATES_PAGE } from "../../constants/pages"; | |||
| import PropTypes from "prop-types"; | |||
| import Slider from "react-slick"; | |||
| import CandidateCard from "../../components/Candidates/CandidateCard"; | |||
| import useDynamicRefs from "use-dynamic-refs"; | |||
| import CandidateFilters from "../../components/Candidates/CandidateFilters"; | |||
| import Pagination from "@mui/material/Pagination"; | |||
| import { PAGE_SIZE_CANDIDATES } from "../../constants/keyCodeConstants"; | |||
| import AdsCandidatesPage from "./AdsCandidatesPage"; | |||
| import TableViewPage from "./TableViewPage"; | |||
| import { setTechnologiesReq } from "../../store/actions/technologies/technologiesActions"; | |||
| import { selectTechnologies } from "../../store/selectors/technologiesSelectors"; | |||
| import { useDispatch, useSelector } from "react-redux"; | |||
| import InviteDialog from "../../components/MUI/InviteDialog"; | |||
| import addUser from "../../assets/images/addUser.png"; | |||
| import x from "../../assets/images/x.png"; | |||
| const CandidatesPage = ({ history }) => { | |||
| const dispatch = useDispatch(); | |||
| const { candidates } = useSelector((s) => s.candidates); | |||
| const { pagination } = useSelector((s) => s.candidates); // pagination is total number of candidates on backend | |||
| const { adsCandidates } = useSelector((s) => s.candidates); | |||
| const theme = useTheme(); | |||
| const matches = useMediaQuery(theme.breakpoints.down("sm")); | |||
| const [isTableView, setIsTableView] = useState(true); | |||
| const [getRef, setRef] = useDynamicRefs(); | |||
| const [toggleFiltersDrawer, setToggleFiltersDrawer] = useState(false); | |||
| const [page, setPage] = React.useState(1); | |||
| const technologies = useSelector(selectTechnologies); | |||
| const [showInvite, setShowInvite] = useState(false); | |||
| useEffect(() => { | |||
| dispatch( | |||
| filterCandidates({ | |||
| currentPage: page, | |||
| pageSize: PAGE_SIZE_CANDIDATES, | |||
| minExperience: 0, | |||
| maxExperience: 0, | |||
| employmentType: "", | |||
| minDateOfApplication: "", | |||
| maxDateOfApplication: "", | |||
| technologies: [], | |||
| }) | |||
| ); | |||
| dispatch(fetchAdsCandidates()); | |||
| }, [dispatch]); | |||
| const navigate = (applicantId) => { | |||
| history.push({ | |||
| pathname: CANDIDATES_PAGE + "/" + applicantId, | |||
| state: { | |||
| from: history.location.pathname, | |||
| }, | |||
| }); | |||
| }; | |||
| dispatch(setTechnologiesReq()); | |||
| }, []); | |||
| const changeView = () => { | |||
| setIsTableView(!isTableView); | |||
| @@ -68,86 +39,54 @@ const CandidatesPage = ({ history }) => { | |||
| setToggleFiltersDrawer((oldState) => !oldState); | |||
| }; | |||
| var settings = { | |||
| dots: false, | |||
| infinite: false, | |||
| speed: 400, | |||
| slidesToShow: 4, | |||
| slidesToScroll: 4, | |||
| initialSlide: 0, | |||
| arrows: true, | |||
| variableWidth: true, | |||
| responsive: [ | |||
| { | |||
| breakpoint: 1024, | |||
| settings: { | |||
| slidesToShow: 3, | |||
| slidesToScroll: 3, | |||
| infinite: true, | |||
| dots: false, | |||
| }, | |||
| }, | |||
| { | |||
| breakpoint: 900, | |||
| settings: { | |||
| slidesToShow: 2, | |||
| slidesToScroll: 2, | |||
| initialSlide: 0, | |||
| }, | |||
| }, | |||
| { | |||
| breakpoint: 480, | |||
| settings: { | |||
| slidesToShow: 1, | |||
| slidesToScroll: 1, | |||
| }, | |||
| }, | |||
| ], | |||
| }; | |||
| const getDummyCandidates = (len) => { | |||
| const candidates = []; | |||
| for (let i = 0; i < 4 - len + 1; i++) { | |||
| candidates.push( | |||
| <CandidateCard key={i} className="hiddenAd" history={history} /> | |||
| ); | |||
| } | |||
| return candidates; | |||
| }; | |||
| const activeAdsArrowLeftHandler = (index) => { | |||
| getRef(index.toString()).current.slickPrev(); | |||
| }; | |||
| const activeAdsArrowRightHandler = (index) => { | |||
| getRef(index.toString()).current.slickNext(); | |||
| }; | |||
| const handleChange = (event, value) => { | |||
| dispatch( | |||
| filterCandidates({ | |||
| currentPage: value, | |||
| pageSize: PAGE_SIZE_CANDIDATES, | |||
| minExperience: 0, | |||
| maxExperience: 5, | |||
| employmentType: "Intership", | |||
| minDateOfApplication: "10/05/2020", | |||
| maxDateOfApplication: "10/10/2022", | |||
| technologies: [], | |||
| }) | |||
| ); | |||
| setPage(value); | |||
| const inviteCandidate = () => { | |||
| setShowInvite(true) | |||
| }; | |||
| return ( | |||
| <div className="main-candidates-container pl-144 pt-36px"> | |||
| <InviteDialog | |||
| open={showInvite} | |||
| onClose={() => { | |||
| setShowInvite(false); | |||
| }} | |||
| title={ | |||
| <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={addUser} | |||
| /> | |||
| <h5>Invite korisnika</h5> | |||
| <div className="vr"></div> | |||
| <p className="dialog-subtitle">Registration link</p> | |||
| </div> | |||
| <IconButton onClick={() => setShowInvite(false)}> | |||
| <img | |||
| style={{ | |||
| position: "relative", | |||
| top: -0.25, | |||
| }} | |||
| src={x} | |||
| /> | |||
| </IconButton> | |||
| </div> | |||
| } | |||
| /> | |||
| <CandidateFilters | |||
| open={toggleFiltersDrawer} | |||
| handleClose={handleToggleFiltersDrawer} | |||
| pageSize={PAGE_SIZE_CANDIDATES} | |||
| currentPage={page} | |||
| isTableView={isTableView} | |||
| technologies={technologies} | |||
| /> | |||
| <div className="l-t-rectangle"></div> | |||
| <div className="r-b-rectangle"></div> | |||
| @@ -222,108 +161,17 @@ const CandidatesPage = ({ history }) => { | |||
| </div> | |||
| </div> | |||
| {isTableView ? ( | |||
| <div className="candidates-table"> | |||
| <table | |||
| className="usersTable" | |||
| style={{ width: "1117px", height: "100px" }} | |||
| > | |||
| <thead> | |||
| <tr className="headingRow"> | |||
| <th>Ime i prezime</th> | |||
| <th>Iskustvo</th> | |||
| <th>Datum prijave</th> | |||
| <th>Pozicija</th> | |||
| <th>CV link</th> | |||
| </tr> | |||
| </thead> | |||
| <tbody> | |||
| {candidates.map((candidate, index) => ( | |||
| <tr | |||
| key={index} | |||
| className="secondaryRow cadidate-row" | |||
| style={{ | |||
| width: "800px", | |||
| height: "40px", | |||
| borderRadius: "12px", | |||
| cursor: "pointer", | |||
| }} | |||
| onClick={() => navigate(candidate.applicantId)} | |||
| > | |||
| <td> | |||
| {candidate.firstName} {candidate.lastName} | |||
| </td> | |||
| <td>{candidate.experience}</td> | |||
| <td>{formatDate(candidate.dateOfApplication)}</td> | |||
| <td>{candidate.position}</td> | |||
| <td> | |||
| <a href={candidate.CV} className="cvLink"> | |||
| {candidate.firstName} | |||
| {candidate.lastName}.pdf | |||
| </a> | |||
| </td> | |||
| </tr> | |||
| ))} | |||
| </tbody> | |||
| </table> | |||
| <Pagination | |||
| count={ | |||
| parseInt(pagination) <= PAGE_SIZE_CANDIDATES | |||
| ? 1 | |||
| : Math.ceil(parseInt(pagination) / PAGE_SIZE_CANDIDATES) | |||
| } | |||
| color="primary" | |||
| style={{ alignSelf: "center", marginTop: "20px" }} | |||
| onChange={handleChange} | |||
| shape="rounded" | |||
| /> | |||
| </div> | |||
| <TableViewPage history={history} page={page} setPage={setPage} /> | |||
| ) : ( | |||
| <div className="ads-candidates-container top-cnd"> | |||
| {adsCandidates.map((adCandidates, index) => ( | |||
| <div className="ads-candidates" key={index}> | |||
| <div className="ads-candidates-top-container"> | |||
| <img src={adImage} style={{ width: "49px", height: "39px" }} /> | |||
| <p className="ads-candidates-title">{adCandidates.title}</p> | |||
| <p className="ads-candidates-numberOfApplicants"> | |||
| | {adCandidates.nubmerOfApplicants} prijavljenih | |||
| </p> | |||
| </div> | |||
| <div | |||
| className="ads-candidates-slider" | |||
| style={{ display: "flex", marginTop: "31px" }} | |||
| > | |||
| {adCandidates.applicants.length > 3 && ( | |||
| <div className="active-ads-ads-arrows"> | |||
| <button onClick={() => activeAdsArrowLeftHandler(index)}> | |||
| <img src={arrow_left} alt="arrow-left" /> | |||
| </button> | |||
| <button onClick={() => activeAdsArrowRightHandler(index)}> | |||
| <img src={arrow_right} alt="arrow-right" /> | |||
| </button> | |||
| </div> | |||
| )} | |||
| <Slider | |||
| {...settings} | |||
| ref={setRef(index.toString())} | |||
| style={{ width: "100%" }} | |||
| > | |||
| {adCandidates.applicants.map((candidate, index) => ( | |||
| <CandidateCard | |||
| key={index} | |||
| candidate={candidate} | |||
| history={history} | |||
| className={index === 0 ? "" : "left-move-candidateAd"} | |||
| /> | |||
| ))} | |||
| {adCandidates.applicants.length <= 4 && | |||
| getDummyCandidates(adCandidates.applicants.length)} | |||
| </Slider> | |||
| </div> | |||
| </div> | |||
| ))} | |||
| </div> | |||
| <AdsCandidatesPage/> | |||
| )} | |||
| <IconButton className={'c-btn candidate-btn invite-btn invite-btn-color ' + (isTableView === false ? 'proba' : '')}> | |||
| <IconButton | |||
| className={ | |||
| "c-btn candidate-btn invite-btn invite-btn-color " + | |||
| (isTableView === false ? "proba" : "") | |||
| } | |||
| onClick={inviteCandidate} | |||
| > | |||
| <img src={planeImage} alt="table" className="candidates-image" /> | |||
| Invite | |||
| </IconButton> | |||
| @@ -0,0 +1,126 @@ | |||
| import React, { useEffect } from "react"; | |||
| import Pagination from "@mui/material/Pagination"; | |||
| import { formatDate } from "../../util/helpers/dateHelpers"; | |||
| import { useDispatch, useSelector } from "react-redux"; | |||
| import PropTypes from "prop-types"; | |||
| import { CANDIDATES_PAGE } from "../../constants/pages"; | |||
| import { filterCandidates } from "../../store/actions/candidates/candidatesActions"; | |||
| import { PAGE_SIZE_CANDIDATES } from "../../constants/keyCodeConstants"; | |||
| const TableViewPage = ({ history, setPage, page }) => { | |||
| const dispatch = useDispatch(); | |||
| const { candidates } = useSelector((s) => s.candidates); | |||
| const { pagination } = useSelector((s) => s.candidates); // pagination is total number of candidates on backend | |||
| const navigate = (applicantId) => { | |||
| history.push({ | |||
| pathname: CANDIDATES_PAGE + "/" + applicantId, | |||
| state: { | |||
| from: history.location.pathname, | |||
| }, | |||
| }); | |||
| }; | |||
| useEffect(() => { | |||
| dispatch( | |||
| filterCandidates({ | |||
| currentPage: page, | |||
| pageSize: PAGE_SIZE_CANDIDATES, | |||
| minExperience: 0, | |||
| maxExperience: 0, | |||
| employmentType: "", | |||
| minDateOfApplication: "", | |||
| maxDateOfApplication: "", | |||
| technologies: [], | |||
| }) | |||
| ); | |||
| }, [dispatch]); | |||
| const handleChange = (_, value) => { | |||
| dispatch( | |||
| filterCandidates({ | |||
| currentPage: value, | |||
| pageSize: PAGE_SIZE_CANDIDATES, | |||
| minExperience: 0, | |||
| maxExperience: 0, | |||
| employmentType: "", | |||
| minDateOfApplication: "", | |||
| maxDateOfApplication: "", | |||
| technologies: [], | |||
| }) | |||
| ); | |||
| setPage(value); | |||
| }; | |||
| return ( | |||
| <div className="candidates-table"> | |||
| <table | |||
| className="usersTable" | |||
| style={{ width: "1117px", height: "100px" }} | |||
| > | |||
| <thead> | |||
| <tr className="headingRow"> | |||
| <th>Ime i prezime</th> | |||
| <th>Iskustvo</th> | |||
| <th>Datum prijave</th> | |||
| <th>Pozicija</th> | |||
| <th>CV link</th> | |||
| </tr> | |||
| </thead> | |||
| <tbody> | |||
| {candidates.map((candidate, index) => ( | |||
| <tr | |||
| key={index} | |||
| className="secondaryRow cadidate-row" | |||
| style={{ | |||
| width: "800px", | |||
| height: "40px", | |||
| borderRadius: "12px", | |||
| cursor: "pointer", | |||
| }} | |||
| onClick={() => navigate(candidate.applicantId)} | |||
| > | |||
| <td> | |||
| {candidate.firstName} {candidate.lastName} | |||
| </td> | |||
| <td>{candidate.experience}</td> | |||
| <td>{formatDate(candidate.dateOfApplication)}</td> | |||
| <td>{candidate.position}</td> | |||
| <td> | |||
| <a href={candidate.CV} className="cvLink"> | |||
| {candidate.firstName} | |||
| {candidate.lastName}.pdf | |||
| </a> | |||
| </td> | |||
| </tr> | |||
| ))} | |||
| </tbody> | |||
| </table> | |||
| <Pagination | |||
| count={ | |||
| parseInt(pagination) <= PAGE_SIZE_CANDIDATES | |||
| ? 1 | |||
| : Math.ceil(parseInt(pagination) / PAGE_SIZE_CANDIDATES) | |||
| } | |||
| color="primary" | |||
| style={{ alignSelf: "center", marginTop: "20px" }} | |||
| onChange={handleChange} | |||
| shape="rounded" | |||
| /> | |||
| </div> | |||
| ); | |||
| }; | |||
| TableViewPage.propTypes = { | |||
| history: PropTypes.shape({ | |||
| replace: PropTypes.func, | |||
| push: PropTypes.func, | |||
| location: PropTypes.shape({ | |||
| pathname: PropTypes.string, | |||
| }), | |||
| }), | |||
| setPage: PropTypes.func, | |||
| page: PropTypes.number, | |||
| }; | |||
| export default TableViewPage; | |||
| @@ -17,7 +17,7 @@ export default { | |||
| }, | |||
| candidates: { | |||
| filteredCandidates:base + "/applicants", | |||
| allAdsCandidates: base + "/applicants/adsApplicants" | |||
| allFilteredAdsCandidates: base + "/applicants/adsApplicants" | |||
| }, | |||
| ads: { | |||
| allAds: base + "/ads", | |||
| @@ -30,7 +30,30 @@ export const getCandidate = (id) => | |||
| getRequest(apiEndpoints.candidates.filteredCandidates + "/" + id); | |||
| export const createComment = (data) => | |||
| postRequest(apiEndpoints.comments.addComment, data); | |||
| export const getAllAdsCandidates = () => | |||
| getRequest(apiEndpoints.candidates.allAdsCandidates); | |||
| export const getFilteredAdsCandidates = (payload) => { | |||
| let technologiesQuery = ""; | |||
| for (let i = 0; i < payload.technologies.length; i++) { | |||
| technologiesQuery += `technologies=${payload.technologies[i]}&`; | |||
| } | |||
| return getRequest( | |||
| apiEndpoints.candidates.allFilteredAdsCandidates + | |||
| "?currentPage=" + | |||
| payload.currentPage + | |||
| "&pageSize=" + | |||
| payload.pageSize + | |||
| "&minExperience=" + | |||
| payload.minExperience + | |||
| "&maxExperience=" + | |||
| payload.maxExperience + | |||
| "&employmentType=" + | |||
| payload.employmentType + | |||
| "&minDateOfApplication=" + | |||
| payload.minDateOfApplication + | |||
| "&maxDateOfApplication=" + | |||
| payload.maxDateOfApplication + | |||
| "&" + | |||
| technologiesQuery | |||
| ); | |||
| } | |||
| export const deleteCandidate = (id) => | |||
| deleteRequest(apiEndpoints.candidates.allCandidates + "?id=" + id); | |||
| deleteRequest(apiEndpoints.candidates.filteredCandidates + "?id=" + id); | |||
| @@ -22,8 +22,9 @@ import { | |||
| payload, | |||
| }); | |||
| export const fetchAdsCandidates = () => ({ | |||
| export const fetchAdsCandidates = (payload) => ({ | |||
| type: ADS_CANDIDATES_FETCH, | |||
| payload | |||
| }); | |||
| export const fetchAdsCandidatesError = (payload) => ({ | |||
| @@ -34,4 +35,4 @@ import { | |||
| export const fetchAdsCandidatesSuccess = (payload) => ({ | |||
| type: ADS_CANDIDATES_SUCCESS, | |||
| payload, | |||
| }); | |||
| }); | |||
| @@ -3,3 +3,4 @@ export const FETCH_TECHNOLOGIES_ERR = "FETCH_TECHNOLOGIES_ERR"; | |||
| export const FETCH_TECHNOLOGIES_SUCCESS = "FETCH_TECHNOLOGIES_SUCCESS"; | |||
| export const CHANGE_ISCHECKED_VALUE = "CHANGE_ISCHECKED_VALUE"; | |||
| export const RESET_IS_CHECKED_VALUE = "RESET_IS_CHECKED_VALUE" | |||
| @@ -2,7 +2,8 @@ import { | |||
| FETCH_TECHNOLOGIES_ERR, | |||
| FETCH_TECHNOLOGIES_SUCCESS, | |||
| FETCH_TECHNOLOGIES_REQ, | |||
| CHANGE_ISCHECKED_VALUE | |||
| CHANGE_ISCHECKED_VALUE, | |||
| RESET_IS_CHECKED_VALUE | |||
| } from "./technologiesActionConstants"; | |||
| export const setTechnologiesReq = () => ({ | |||
| @@ -22,4 +23,8 @@ import { | |||
| export const changeIsCheckedValue = (payload) => ({ | |||
| type: CHANGE_ISCHECKED_VALUE, | |||
| payload, | |||
| }); | |||
| }); | |||
| export const resetIsCheckedValue = () => ({ | |||
| type:RESET_IS_CHECKED_VALUE | |||
| }) | |||
| @@ -10,7 +10,7 @@ const initialState = { | |||
| candidates: [], | |||
| adsCandidates: [], | |||
| errorMessage: "", | |||
| pagination: 0, | |||
| pagination: 0 | |||
| }; | |||
| export default createReducer( | |||
| @@ -50,4 +50,4 @@ function setAdsCandidatesError(state, action) { | |||
| ...state, | |||
| errorMessage: action.payload, | |||
| }; | |||
| } | |||
| } | |||
| @@ -3,6 +3,7 @@ import { | |||
| FETCH_TECHNOLOGIES_SUCCESS, | |||
| FETCH_TECHNOLOGIES_ERR, | |||
| CHANGE_ISCHECKED_VALUE, | |||
| RESET_IS_CHECKED_VALUE | |||
| } from "../../actions/technologies/technologiesActionConstants"; | |||
| const initialState = { | |||
| @@ -15,6 +16,7 @@ export default createReducer( | |||
| [FETCH_TECHNOLOGIES_SUCCESS]: setStateTechnologies, | |||
| [FETCH_TECHNOLOGIES_ERR]: setStateErrorMessage, | |||
| [CHANGE_ISCHECKED_VALUE]: setIsCheckedTechnology, | |||
| [RESET_IS_CHECKED_VALUE]: resetIsCheckedTechnologies | |||
| }, | |||
| initialState | |||
| ); | |||
| @@ -43,3 +45,7 @@ function setIsCheckedTechnology(state, action) { | |||
| ), | |||
| }; | |||
| } | |||
| function resetIsCheckedTechnologies(state){ | |||
| return {...state,technologies:state.technologies.map(t => ({...t,isChecked:false}))} | |||
| } | |||
| @@ -4,20 +4,20 @@ import { addHeaderToken } from "../../request"; | |||
| import { | |||
| createComment, | |||
| getCandidate, | |||
| getAllAdsCandidates, | |||
| getFilteredAdsCandidates, | |||
| deleteCandidate, | |||
| getFilteredCandidates | |||
| } from "../../request/candidatesRequest"; | |||
| import { authScopeStringGetHelper } from "../../util/helpers/authScopeHelpers"; | |||
| import { | |||
| ADS_CANDIDATES_FETCH, | |||
| FILTER_CANDIDATES | |||
| FILTER_CANDIDATES, | |||
| } from "../actions/candidates/candidatesActionConstants"; | |||
| import { | |||
| fetchAdsCandidatesError, | |||
| fetchAdsCandidatesSuccess, | |||
| filterCandidatesSuccess, | |||
| filterCandidatesError, | |||
| filterCandidatesError | |||
| } from "../actions/candidates/candidatesActions"; | |||
| import { rejectErrorCodeHelper } from "../../util/helpers/rejectErrorCodeHelper"; | |||
| import { | |||
| @@ -33,8 +33,6 @@ import { | |||
| CANDIDATE_COMMENTS_FETCH, | |||
| DELETE_CANDIDATE, | |||
| } from "../actions/candidate/candidateActionConstants"; | |||
| import { CANDIDATES_PAGE } from "../../constants/pages"; | |||
| import history from "../utils/history"; | |||
| export function* filterCandidates({ payload }) { | |||
| @@ -77,12 +75,15 @@ export function* addComment(data) { | |||
| } | |||
| } | |||
| export function* getAdsCandidates() { | |||
| export function* getAdsCandidates({payload}) { | |||
| try { | |||
| const JwtToken = yield call(authScopeStringGetHelper, JWT_TOKEN); | |||
| yield call(addHeaderToken, JwtToken); | |||
| const { data } = yield call(getAllAdsCandidates); | |||
| const { data } = yield call(getFilteredAdsCandidates,payload); | |||
| yield put(fetchAdsCandidatesSuccess(data)); | |||
| if(payload.handleApiResponseSuccess){ | |||
| yield call(payload.handleApiResponseSuccess) | |||
| } | |||
| } catch (error) { | |||
| const errorMessage = yield call(rejectErrorCodeHelper, error); | |||
| yield put(fetchAdsCandidatesError(errorMessage)); | |||
| @@ -90,9 +91,12 @@ export function* getAdsCandidates() { | |||
| } | |||
| export function* deleteSingleCandidate({ payload }) { | |||
| console.log(payload) | |||
| try { | |||
| yield call(deleteCandidate, payload.id); | |||
| yield call(history.replace, CANDIDATES_PAGE); | |||
| if(payload.handleApiResponseSuccess){ | |||
| yield call(payload.handleApiResponseSuccess) | |||
| } | |||
| yield put(deleteCandidateSuccess()); | |||
| } catch (error) { | |||
| const errorMessage = yield call(rejectErrorCodeHelper, error); | |||