| @@ -14,7 +14,9 @@ import { | |||
| USERS_PAGE, | |||
| CANDIDATES_PAGE, | |||
| USER_DETAILS_PAGE, | |||
| CANDIDATES_DETAILS_PAGE | |||
| CANDIDATES_DETAILS_PAGE, | |||
| SELECTION_PROCESS_PAGE, | |||
| SELECTION_PROCESS_OF_APPLICANT_PAGE | |||
| } from "./constants/pages"; | |||
| // import LoginPage from './pages/LoginPage/LoginPage'; | |||
| @@ -34,6 +36,8 @@ import CandidatesPage from './pages/CandidatesPage/CandidatesPage' | |||
| import AdDetailsPage from "./pages/AdsPage/AdDetailsPage"; | |||
| import UserDetails from "./pages/UsersPage/UserDetails"; | |||
| import CandidateDetailsPage from "./pages/CandidateDetailsPage/CandidateDetailsPage"; | |||
| import SelectionProcessPage from "./pages/SelectionProcessPage/SelectionProcessPage"; | |||
| import SelectionProcessOfApplicantPage from "./pages/SelectionProcessPage/SelectionProcessOfApplicantPage"; | |||
| const AppRoutes = () => ( | |||
| <Switch> | |||
| @@ -54,6 +58,8 @@ const AppRoutes = () => ( | |||
| <PrivateRoute exact path={USERS_PAGE} component={UsersPage} /> | |||
| <PrivateRoute exact path={CANDIDATES_PAGE} component={CandidatesPage} /> | |||
| <PrivateRoute exact path={CANDIDATES_DETAILS_PAGE} component={CandidateDetailsPage} /> | |||
| <PrivateRoute exact path={SELECTION_PROCESS_PAGE} component={SelectionProcessPage} /> | |||
| <PrivateRoute exact path={SELECTION_PROCESS_OF_APPLICANT_PAGE} component={SelectionProcessOfApplicantPage} /> | |||
| <Redirect from="*" to={NOT_FOUND_PAGE} /> | |||
| </Switch> | |||
| ); | |||
| @@ -0,0 +1,366 @@ | |||
| .selections { | |||
| margin-top: 36px; | |||
| padding-left: 72px; | |||
| @include media-below($bp-xl) { | |||
| padding-left: 36px !important; | |||
| } | |||
| } | |||
| .level-header { | |||
| padding-left: 81px; | |||
| display: flex; | |||
| justify-content: space-between; | |||
| align-items: center; | |||
| @include media-below($bp-xl) { | |||
| padding: 0 0.75rem !important; | |||
| } | |||
| } | |||
| .selection-levels { | |||
| overflow-x: scroll; | |||
| padding-bottom: 100px; | |||
| } | |||
| .level-header-subheader { | |||
| font-family: 'Source Sans Pro'; | |||
| font-style: normal; | |||
| font-weight: 600; | |||
| font-size: 24px; | |||
| line-height: 36px; | |||
| padding-left: 0.3rem; | |||
| color: #226CB0; | |||
| letter-spacing: 0.02em; | |||
| } | |||
| .level-header-spliter { | |||
| font-family: 'Source Sans Pro'; | |||
| font-style: normal; | |||
| font-weight: 600; | |||
| font-size: 24px; | |||
| line-height: 36px; | |||
| padding-left: 0.3rem; | |||
| color: #272727; | |||
| letter-spacing: 0.02em; | |||
| } | |||
| .selection-levels-processes { | |||
| display: flex; | |||
| margin-top: 39px; | |||
| position: relative; | |||
| } | |||
| .selection-levels-processes-process { | |||
| padding-left: 81px; | |||
| display: flex; | |||
| @include media-below($bp-xl) { | |||
| padding-left: 0; | |||
| } | |||
| } | |||
| .selection-card { | |||
| display: flex; | |||
| flex-direction: column; | |||
| justify-content: start; | |||
| align-items: left; | |||
| height: fit-content; | |||
| padding: 36px; | |||
| background: #F4F4F4; | |||
| border: 1px solid #e4e4e4; | |||
| border-radius: 18px; | |||
| gap: 18px; | |||
| margin-right: 36px; | |||
| @include media-below($bp-xl) { | |||
| margin-right: 20px !important; | |||
| padding: 36px !important; | |||
| } | |||
| } | |||
| .sel-item:hover { | |||
| scale: 1.05; | |||
| border-color: $mainBlue !important; | |||
| background-color: $mainBlueLight !important; | |||
| } | |||
| .bg-danger { | |||
| background-color: #272727; | |||
| } | |||
| .grey { | |||
| color: #e4e4e4 | |||
| } | |||
| .selection-item { | |||
| display: flex; | |||
| flex-direction: row; | |||
| justify-content: left; | |||
| vertical-align: top; | |||
| align-items: left; | |||
| width: 400px; | |||
| padding: 18px 36px; | |||
| background: #FFFFFF; | |||
| border: 1px solid #e4e4e4; | |||
| border-radius: 18px; | |||
| gap: 18px; | |||
| margin-right: 36px; | |||
| } | |||
| .selection-item-date p { | |||
| text-align: right; | |||
| font-family: "Source Sans Pro"; | |||
| font-style: normal; | |||
| font-weight: 400; | |||
| font-size: 16px; | |||
| line-height: 15px; | |||
| color: #272727; | |||
| flex: none; | |||
| order: 4; | |||
| flex-grow: 0; | |||
| } | |||
| .selection-card-title h3 { | |||
| font-family: "Source Sans Pro"; | |||
| font-style: normal; | |||
| font-weight: 600; | |||
| font-size: 32px; | |||
| line-height: 32px; | |||
| letter-spacing: 0.02em; | |||
| color: #272727; | |||
| flex: none; | |||
| order: 0; | |||
| flex-grow: 0; | |||
| } | |||
| .selection-item-name, | |||
| .selection-item-date { | |||
| margin: auto 0 !important; | |||
| } | |||
| .selection-item-name p { | |||
| height: 20px; | |||
| font-family: 'Source Sans Pro'; | |||
| font-style: normal; | |||
| font-weight: 600; | |||
| font-size: 16px; | |||
| line-height: 20px; | |||
| text-align: right; | |||
| color: #226CB0; | |||
| flex: none; | |||
| order: 2; | |||
| flex-grow: 0; | |||
| } | |||
| .selection-item-buttons button { | |||
| box-sizing: border-box; | |||
| display: flex; | |||
| flex-direction: row; | |||
| justify-content: center; | |||
| font-size: 16px; | |||
| align-items: center; | |||
| padding: 9px; | |||
| gap: 10px; | |||
| min-width: 76px; | |||
| height: 38px; | |||
| border: 1px solid #e4e4e4; | |||
| border-radius: 9px; | |||
| flex: none; | |||
| order: 0; | |||
| flex-grow: 0; | |||
| } | |||
| .sel-item { | |||
| display: flex; | |||
| flex-direction: row; | |||
| align-items: center; | |||
| padding: 18px 36px; | |||
| gap: 18px; | |||
| cursor: pointer; | |||
| width: 458px; | |||
| background: #FFFFFF; | |||
| border: 1px solid #E4E4E4; | |||
| border-radius: 18px; | |||
| transition: 0.3s; | |||
| } | |||
| .sel-item .status { | |||
| font-family: 'Source Sans Pro'; | |||
| font-style: normal; | |||
| font-weight: 400; | |||
| font-size: 16px; | |||
| line-height: 20px; | |||
| color: #272727; | |||
| flex: none; | |||
| order: 0; | |||
| flex-grow: 0; | |||
| } | |||
| .sel-item .date { | |||
| font-family: 'Source Sans Pro'; | |||
| font-style: normal; | |||
| font-weight: 400; | |||
| font-size: 16px; | |||
| line-height: 20px; | |||
| color: #272727; | |||
| } | |||
| .sel-item .full-name { | |||
| height: 20px; | |||
| font-family: 'Source Sans Pro'; | |||
| font-style: normal; | |||
| font-weight: 600; | |||
| font-size: 16px; | |||
| text-align: right; | |||
| color: #226CB0; | |||
| flex: 3 0 auto; | |||
| order: 1; | |||
| } | |||
| .sel-item .status button { | |||
| box-sizing: border-box; | |||
| display: flex; | |||
| flex-direction: row; | |||
| justify-content: center; | |||
| font-size: 16px; | |||
| align-items: center; | |||
| padding: 9px; | |||
| gap: 10px; | |||
| min-width: 76px; | |||
| height: 38px; | |||
| border: 1px solid #e4e4e4; | |||
| background: white; | |||
| border-radius: 9px; | |||
| flex: none; | |||
| order: 0; | |||
| flex-grow: 0; | |||
| } | |||
| .active-process { | |||
| scale: 1.05; | |||
| border-color: $mainBlue !important; | |||
| background-color: $mainBlueLight !important; | |||
| } | |||
| .active-process-card { | |||
| display: flex; | |||
| flex-direction: column; | |||
| justify-content: start; | |||
| align-items: flex-start; | |||
| padding: 18px; | |||
| background: #ffffff; | |||
| border: 1px solid #e4e4e4; | |||
| border-radius: 18px; | |||
| gap: 18px; | |||
| margin-right: 36px; | |||
| @include media-below($bp-xl) { | |||
| margin-right: 20px !important; | |||
| padding: 36px !important; | |||
| } | |||
| } | |||
| .active-process-card-header { | |||
| display: flex; | |||
| flex-direction: row; | |||
| justify-content: start; | |||
| align-items: flex-start; | |||
| padding: 0px; | |||
| gap: 18px; | |||
| } | |||
| .active-process-card-body { | |||
| display: flex; | |||
| flex-direction: column; | |||
| justify-content: center; | |||
| width: 100%; | |||
| align-items: center; | |||
| padding: 34px; | |||
| gap: 18px; | |||
| @include media-below($bp-xl) { | |||
| margin-right: 20px !important; | |||
| padding: 36px !important; | |||
| } | |||
| } | |||
| .active-process-card-date p { | |||
| font-family: 'Source Sans Pro'; | |||
| font-style: normal; | |||
| font-weight: 400; | |||
| font-size: 16px; | |||
| line-height: 20px; | |||
| color: #272727; | |||
| flex: none; | |||
| order: 0; | |||
| flex-grow: 0; | |||
| z-index: 0; | |||
| } | |||
| .active-process-card-number p { | |||
| font-family: 'Source Sans Pro'; | |||
| font-style: normal; | |||
| font-weight: 600; | |||
| font-size: 16px; | |||
| line-height: 20px; | |||
| text-align: left; | |||
| background: conic-gradient(from 73.66deg at 50% 50%, #226CB0 0deg, #BA6FB9 106.88deg, #5E9FDB 228.75deg, #226CB0 360deg); | |||
| -webkit-background-clip: text; | |||
| -webkit-text-fill-color: transparent; | |||
| background-clip: text; | |||
| text-fill-color: transparent; | |||
| flex: none; | |||
| order: 6; | |||
| flex-grow: 0; | |||
| z-index: 6; | |||
| } | |||
| .active-process-card-buttons { | |||
| overflow: hidden; | |||
| display: flex; | |||
| flex-direction: row; | |||
| align-items: flex-start; | |||
| justify-content: center; | |||
| padding: 0px; | |||
| gap: 18px; | |||
| flex: none; | |||
| order: 4; | |||
| flex-grow: 0; | |||
| @include media-below($bp-xl) { | |||
| gap: 9px !important; | |||
| } | |||
| } | |||
| .active-process-card-buttons button { | |||
| box-sizing: border-box; | |||
| display: flex; | |||
| flex-direction: row; | |||
| justify-content: center; | |||
| align-items: center; | |||
| padding: 9px; | |||
| gap: 10px; | |||
| height: 38px; | |||
| background: transparent; | |||
| border: 1px solid #e4e4e4; | |||
| border-radius: 9px; | |||
| flex: none; | |||
| order: 0; | |||
| flex-grow: 0; | |||
| } | |||
| .active-process-card-link{ | |||
| font-family: 'Source Sans Pro'; | |||
| font-style: normal; | |||
| font-weight: 400; | |||
| font-size: 12px; | |||
| text-decoration-line: underline; | |||
| color: #226CB0; | |||
| flex: none; | |||
| order: 4; | |||
| flex-grow: 0; | |||
| z-index: 4; | |||
| } | |||
| @@ -0,0 +1,68 @@ | |||
| import React from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { formatDate } from "../../util/helpers/dateHelpers"; | |||
| import { Link } from "react-router-dom"; | |||
| const ApplicantSelection = ({ | |||
| levelNumber, | |||
| levelName, | |||
| schedguler, | |||
| link, | |||
| date, | |||
| status, | |||
| id, | |||
| onShowAdDetails, | |||
| className, | |||
| }) => { | |||
| return ( | |||
| <div className={`active-process-card ${className}`} onClick={onShowAdDetails}> | |||
| <div className="active-process-card-header"> | |||
| <div className="active-process-card-number"> | |||
| <p> | |||
| {levelNumber} | |||
| </p> | |||
| </div> | |||
| </div> | |||
| <div className="active-process-card-body"> | |||
| <div className="active-process-card-date"> | |||
| <p> | |||
| {date && date !== "" && formatDate(new Date(date))} | |||
| </p> | |||
| </div> | |||
| <div className="ad-card-title"> | |||
| <h3>{levelName}</h3> | |||
| </div> | |||
| <div className="active-process-card-date"> | |||
| <p> | |||
| {schedguler} | |||
| </p> | |||
| </div> | |||
| <div className="active-process-card-buttons"> | |||
| <button>{status}</button> | |||
| </div> | |||
| <div className="active-process-card-link"> | |||
| {(status === "Odrađen") && <Link className="ad-details-buttons-link" to={"/candidates/" + id}>Detaljni izveštaja</Link>} | |||
| {(link && status !== "Odrađen") && <a href={link} className="ad-details-buttons-link">Link do Google Meet-a</a>} | |||
| </div> | |||
| </div> | |||
| </div> | |||
| ); | |||
| }; | |||
| ApplicantSelection.propTypes = { | |||
| id: PropTypes.string, | |||
| levelNumber: PropTypes.number, | |||
| levelName: PropTypes.string, | |||
| schedguler: PropTypes.string, | |||
| link: PropTypes.string, | |||
| date: PropTypes.any, | |||
| status: PropTypes.string, | |||
| onShowAdDetails: PropTypes.func, | |||
| className: PropTypes.any, | |||
| }; | |||
| export default ApplicantSelection; | |||
| @@ -0,0 +1,99 @@ | |||
| import React from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { selectDoneProcessError } from "../../store/selectors/processSelectors"; | |||
| import { setDoneProcessReq } from "../../store/actions/processes/processAction"; | |||
| import { useDispatch, useSelector } from "react-redux"; | |||
| import { formatDateSrb, formatTimeSrb } from "../../util/helpers/dateHelpers"; | |||
| import { SELECTION_PROCESS_OF_APPLICANT_PAGE } from "../../constants/pages"; | |||
| const dragStart = (e, applicant) => { | |||
| e.dataTransfer.setData("text/plain", JSON.stringify(applicant)); | |||
| } | |||
| const dragOver = (e) => { | |||
| e.preventDefault(); | |||
| } | |||
| const Selection = (props) => { | |||
| const applicants = props.selection.selectionProcesses; | |||
| const errorMessage = useSelector(selectDoneProcessError); | |||
| const dispatch = useDispatch(); | |||
| const dropItem = (e, selId) => { | |||
| var data = e.dataTransfer.getData("text/plain"); | |||
| const selectionProcess = JSON.parse(data); | |||
| if (selectionProcess.selectionLevelId !== selId) { | |||
| dispatch(setDoneProcessReq({ id: selectionProcess.id })); | |||
| } | |||
| if (errorMessage) { | |||
| console.log(errorMessage) | |||
| } | |||
| } | |||
| const handleOpenDetails = (id) => { | |||
| props.history.push(SELECTION_PROCESS_OF_APPLICANT_PAGE.replace(":id", id)) | |||
| } | |||
| const renderList = applicants?.map((item, index) => { | |||
| return <div draggable key={index} className="sel-item" onDragStart={e => dragStart(e, item)} | |||
| onClick={() => handleOpenDetails(item.applicant.applicantId)}> | |||
| <div className="status"> | |||
| <button>{item.status}</button> | |||
| </div> | |||
| <div className="date"> | |||
| <p>{formatDateSrb(item.date)} <span className="grey">|</span> {formatTimeSrb(item.date)}</p> | |||
| </div> | |||
| <div className="full-name"> | |||
| <p>{item.applicant.firstName + " " + item.applicant.lastName}</p> | |||
| </div> | |||
| </div> | |||
| } | |||
| ); | |||
| return ( | |||
| <div dropppable="true" id={props.selection.id} className="selection-card" | |||
| onDragOver={e => dragOver(e)} | |||
| onDrop={e => dropItem(e, props.selection.id)} | |||
| > | |||
| <div className="selection-card-title"> | |||
| <h3>{props.selection.name}</h3> | |||
| </div> | |||
| {applicants.length > 0 && renderList} | |||
| {applicants.length === 0 && <div className="sel-item"> | |||
| <div className="date"> | |||
| <p>Nema kandidata u selekciji</p> | |||
| </div> | |||
| </div>} | |||
| </div> | |||
| ); | |||
| }; | |||
| Selection.propTypes = { | |||
| history: PropTypes.shape({ | |||
| replace: PropTypes.func, | |||
| push: PropTypes.func, | |||
| location: PropTypes.shape({ | |||
| pathname: PropTypes.string, | |||
| }), | |||
| }), | |||
| selection: PropTypes.shape({ | |||
| id: PropTypes.number, | |||
| name: PropTypes.string, | |||
| selectionProcesses: PropTypes.arrayOf(PropTypes.shape({ | |||
| id: PropTypes.number, | |||
| name: PropTypes.string, | |||
| date: PropTypes.string, | |||
| status: PropTypes.string, | |||
| currentSelection: PropTypes.number, | |||
| map: PropTypes.func, | |||
| applicant: PropTypes.shape({ | |||
| firstName: PropTypes.string, | |||
| lastName: PropTypes.string | |||
| }) | |||
| })) | |||
| }), | |||
| }; | |||
| export default Selection; | |||
| @@ -10,4 +10,6 @@ export const USER_DETAILS_PAGE = '/users/:id'; | |||
| export const CANDIDATES_PAGE = '/candidates'; | |||
| export const CANDIDATES_DETAILS_PAGE = '/candidates/:id'; | |||
| export const FORGOT_PASSWORD_CONFIRMATION_PAGE = '/forgot-password-confirmation'; | |||
| export const RESET_PASSWORD_PAGE = '/reset-password'; | |||
| export const RESET_PASSWORD_PAGE = '/reset-password'; | |||
| export const SELECTION_PROCESS_PAGE = '/selectionFlow'; | |||
| export const SELECTION_PROCESS_OF_APPLICANT_PAGE = '/selectionflow/:id'; | |||
| @@ -107,26 +107,30 @@ export default { | |||
| // ClientIpAddressIsNullOrEmpty:"Client Ip address is null or empty", | |||
| // UsernameDoesNotExist: "Username does not exist" | |||
| // }, | |||
| nav: { | |||
| ads: "Oglasi", | |||
| selectionFlow: "Tok Selekcije", | |||
| candidates: "Kandidati", | |||
| planer: "Planer", | |||
| patterns: "Šabloni", | |||
| stats: "Statistika", | |||
| users: "Korisnici", | |||
| signOut: "Izloguj se", | |||
| ads: 'Oglasi', | |||
| selectionFlow: 'Tok Selekcije', | |||
| candidates: 'Kandidati', | |||
| planer: 'Planer', | |||
| patterns: 'Šabloni', | |||
| stats: 'Statistika', | |||
| users: 'Korisnici', | |||
| signOut: 'Izloguj se' | |||
| }, | |||
| ads: { | |||
| activeAds: "Aktivni Oglasi", | |||
| archiveAds: "Arhiva", | |||
| adDetailsDescription: | |||
| "Tim Diligent neprestano raste! Tražimo timskog igrača koji će raditi s iskusnim inženjerima. Ako je tehnologija vaša strast i spremni ste svaki dan pomicati granice svog znanja, onda je Diligent pravo mesto za vas. Ukoliko niste iz Niša, nudimo full-time remote poziciju.", | |||
| adDetailsExperience: "godina iskustva", | |||
| adDetailsExpiredAt: "Rok prijave do", | |||
| adDetailsKeyResponsibilities: "Ključne Odgovornosti", | |||
| adDetailsRequirements: "Zahtevi", | |||
| adDetailsOffer: "Šta Nudimo", | |||
| archiveAdsCandidates: "Prijavljeni kandidati", | |||
| }, | |||
| adDetailsExperience: "godina iskustva", | |||
| adDetailsExpiredAt: "Rok prijave do", | |||
| adDetailsKeyResponsibilities: "Ključne Odgovornosti", | |||
| adDetailsRequirements: "Zahtevi", | |||
| adDetailsOffer: "Šta Nudimo", | |||
| archiveAdsCandidates: "Prijavljeni kandidati", | |||
| }, | |||
| selection: { | |||
| title: "Tok Selekcije" | |||
| } | |||
| }; | |||
| @@ -17,6 +17,7 @@ | |||
| @import './assets/styles/components/user-profile'; | |||
| @import './assets/styles/components/auth'; | |||
| @import './assets/styles/components/login'; | |||
| @import './assets/styles/components/selectionProcessPage'; | |||
| @import './assets/styles/components/login-card'; | |||
| @import './assets/styles/components/forgot-password'; | |||
| @import './assets/styles/components/input'; | |||
| @@ -43,7 +43,7 @@ const CandidateDetailsPage = () => { | |||
| }; | |||
| const getArray = () => { | |||
| let newArray = users.map(function (value) { | |||
| let newArray = users?.map(function (value) { | |||
| return { id: value.id, display: value.firstName + value.lastName }; | |||
| }); | |||
| @@ -51,7 +51,6 @@ const CandidateDetailsPage = () => { | |||
| }; | |||
| const tranformDisplay = (id, display) => { | |||
| console.log(id); | |||
| return "@" + display + " "; | |||
| }; | |||
| @@ -110,7 +109,7 @@ const CandidateDetailsPage = () => { | |||
| : "Experience:" + candidate.experience} | |||
| </p> | |||
| <div className="technologies-candidate-container"> | |||
| {candidate.technologyApplicants.map((obj, index) => ( | |||
| {candidate?.technologyApplicants?.map((obj, index) => ( | |||
| <div className="technology-candidate-card" key={index}> | |||
| {obj.technology.name} | |||
| </div> | |||
| @@ -167,7 +166,7 @@ const CandidateDetailsPage = () => { | |||
| }} | |||
| ref={messageContainer} | |||
| > | |||
| {candidate.comments.map((comment, index) => ( | |||
| {candidate?.comments?.map((comment, index) => ( | |||
| <div className="comment-sub-container" key={index}> | |||
| <div className="comment-sender"> | |||
| <p> | |||
| @@ -236,7 +235,7 @@ const CandidateDetailsPage = () => { | |||
| <div className="applicant-ads-container"> | |||
| <p>Sve prijave</p> | |||
| <div className="applicant-ads-sub-container"> | |||
| {candidate.ads.map((add, index) => ( | |||
| {candidate?.ads?.map((add, index) => ( | |||
| <div key={index} className="applicant-add"> | |||
| <p className="applicant-add-date"> | |||
| {formatDate(add.createdAt)} | |||
| @@ -0,0 +1,194 @@ | |||
| import React, { useState, useEffect, useRef } from "react"; | |||
| import arrow_left from "../../assets/images/arrow_left.png"; | |||
| import arrow_right from "../../assets/images/arrow_right.png"; | |||
| import { useParams } from "react-router-dom"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { Link } from "react-router-dom"; | |||
| import AddAdModal from "../../components/Ads/AddAdModal"; | |||
| import AdFilters from "../../components/Ads/AdFilters"; | |||
| import { useDispatch } from "react-redux"; | |||
| import { setApplicantProcessesReq } from "../../store/actions/processes/applicantProcessesAction"; | |||
| import { useSelector } from "react-redux"; | |||
| import { selectApplicantProcesses } from "../../store/selectors/applicantProcessesSelectors"; | |||
| import FilterButton from "../../components/Button/FilterButton"; | |||
| import Slider from "react-slick"; | |||
| import "slick-carousel/slick/slick.css"; | |||
| 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" | |||
| const SelectionProcessOfApplicantPage = () => { | |||
| const theme = useTheme(); | |||
| const matches = useMediaQuery(theme.breakpoints.down("sm")); | |||
| const [toggleFiltersDrawer, setToggleFiltersDrawer] = useState(false); | |||
| const [toggleModal, setToggleModal] = useState(false); | |||
| const processes = useSelector(selectApplicantProcesses); | |||
| const levels = useSelector(selectProcesses) | |||
| const { id } = useParams(); | |||
| const activeAdsSliderRef = useRef(); | |||
| const { t } = useTranslation(); | |||
| const dispatch = useDispatch(); | |||
| useEffect(() => { | |||
| dispatch(setApplicantProcessesReq(id)); | |||
| dispatch(setProcessesReq()); | |||
| }, []); | |||
| const handleToggleFiltersDrawer = () => { | |||
| setToggleFiltersDrawer((oldState) => !oldState); | |||
| }; | |||
| const handleToggleModal = () => { | |||
| setToggleModal((oldState) => !oldState); | |||
| }; | |||
| var settings = { | |||
| dots: false, | |||
| infinite: false, | |||
| speed: 500, | |||
| initialSlide: 0, | |||
| responsive: [ | |||
| { | |||
| breakpoint: 1024, | |||
| settings: { | |||
| slidesToShow: 4, | |||
| slidesToScroll: 4, | |||
| infinite: true, | |||
| dots: false, | |||
| }, | |||
| }, | |||
| { | |||
| breakpoint: 900, | |||
| settings: { | |||
| slidesToShow: 3, | |||
| slidesToScroll: 3, | |||
| initialSlide: 0, | |||
| }, | |||
| }, | |||
| { | |||
| breakpoint: 480, | |||
| settings: { | |||
| slidesToShow: 1, | |||
| slidesToScroll: 1, | |||
| }, | |||
| }, | |||
| ], | |||
| }; | |||
| const activeAdsArrowLeftHandler = () => { | |||
| activeAdsSliderRef.current.slickPrev(); | |||
| }; | |||
| const activeAdsArrowRightHandler = () => { | |||
| 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 key={i} className="hiddenAd" | |||
| date={""} />); | |||
| return applicantSelections; | |||
| }; | |||
| return ( | |||
| <> | |||
| <div className="l-t-rectangle"></div> | |||
| <div className="r-b-rectangle"></div> | |||
| {/* <AdFilters /> */} | |||
| <AdFilters | |||
| open={toggleFiltersDrawer} | |||
| handleClose={handleToggleFiltersDrawer} | |||
| /> | |||
| <AddAdModal open={toggleModal} handleClose={handleToggleModal} /> | |||
| <div className="ads"> | |||
| {processes && processes.length > 0 && ( | |||
| <div className="active-ads"> | |||
| <div className="active-ads-header"> | |||
| <h1>{t("selection.title")} | |||
| <span className="level-header-spliter"> | |||
| | | |||
| </span> | |||
| <span className="level-header-subheader"> | |||
| {id} | |||
| </span> | |||
| </h1> | |||
| <FilterButton onShowFilters={handleToggleFiltersDrawer} /> | |||
| </div> | |||
| <div className="active-ads-ads"> | |||
| <div className="active-ads-ads-a"> | |||
| {!matches && ( | |||
| <div className="active-ads-ads-arrows"> | |||
| <button onClick={activeAdsArrowLeftHandler}> | |||
| <img src={arrow_left} alt="arrow-left" /> | |||
| </button> | |||
| <button onClick={activeAdsArrowRightHandler}> | |||
| <img src={arrow_right} alt="arrow-right" /> | |||
| </button> | |||
| </div> | |||
| )} | |||
| </div> | |||
| <div className="active-ads-ads-ad"> | |||
| <Slider | |||
| {...settings} | |||
| slidesToShow={4} | |||
| slidesToScroll={4} | |||
| style={{ width: "100%" }} | |||
| ref={activeAdsSliderRef} | |||
| > | |||
| {processes.map((process, index) => | |||
| { | |||
| return <ApplicantSelection | |||
| levelNumber={index + 1} | |||
| levelName={process.selectionLevel.name} | |||
| schedguler={"SAfet Purkovic"} | |||
| 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> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| )} | |||
| {matches && ( | |||
| <div className="active-ads-ads-arrows"> | |||
| <button onClick={activeAdsArrowLeftHandler}> | |||
| <img src={arrow_left} alt="arrow-left" /> | |||
| </button> | |||
| <button onClick={activeAdsArrowRightHandler}> | |||
| <img src={arrow_right} alt="arrow-right" /> | |||
| </button> | |||
| </div> | |||
| )} | |||
| </div> | |||
| <div className="add-ad"> | |||
| <Link className="ad-details-buttons-link" to="/selectionflow"> | |||
| Nazad na sve kandidate | |||
| </Link> | |||
| </div> | |||
| </> | |||
| ); | |||
| }; | |||
| export default SelectionProcessOfApplicantPage; | |||
| @@ -0,0 +1,101 @@ | |||
| import React, { useState, useEffect } from "react"; | |||
| import { useSelector} from 'react-redux'; | |||
| import Selection from "../../components/Selection/Selection"; | |||
| import IconButton from "../../components/IconButton/IconButton"; | |||
| import filterVector from "../../assets/images/filter_vector.png"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import AddAdModal from "../../components/Ads/AddAdModal"; | |||
| import { useDispatch } from "react-redux"; | |||
| import AdFilters from "../../components/Ads/AdFilters"; | |||
| import { setDoneProcess } from "../../store/actions/processes/processAction"; | |||
| import { setProcessesReq } from "../../store/actions/processes/processesAction"; | |||
| import { selectDoneProcess } from "../../store/selectors/processSelectors"; | |||
| import { selectProcesses } from "../../store/selectors/processesSelectors" | |||
| import PropTypes from "prop-types"; | |||
| const SelectionProcessPage = ({history}) => { | |||
| const [toggleFiltersDrawer, setToggleFiltersDrawer] = useState(false); | |||
| const [toggleModal, setToggleModal] = useState(false); | |||
| // const errorMessage = useSelector(selectProcessesError); | |||
| const process = useSelector(selectDoneProcess); | |||
| const processes = useSelector(selectProcesses); | |||
| // const doneErrorMessage = useSelector(selectDoneProcessError); | |||
| const { t } = useTranslation(); | |||
| const dispatch = useDispatch(); | |||
| useEffect(() => { | |||
| dispatch(setProcessesReq()); | |||
| }, []); | |||
| useEffect(() => { | |||
| dispatch(setProcessesReq()); | |||
| dispatch(setDoneProcess(false)); | |||
| },[process.process.doneProcess]) | |||
| const handleToggleFiltersDrawer = () => { | |||
| setToggleFiltersDrawer((oldState) => !oldState); | |||
| }; | |||
| const handleToggleModal = () => { | |||
| setToggleModal((oldState) => !oldState); | |||
| }; | |||
| const renderList = processes.map((item, index) => { | |||
| return <Selection selection={item} key={index} history={history}/> | |||
| } | |||
| ); | |||
| return ( | |||
| <> | |||
| <div className="l-t-rectangle"></div> | |||
| <div className="r-b-rectangle"></div> | |||
| <AdFilters /> | |||
| <AdFilters | |||
| open={toggleFiltersDrawer} | |||
| handleClose={handleToggleFiltersDrawer} | |||
| /> | |||
| <AddAdModal open={toggleModal} handleClose={handleToggleModal} /> | |||
| <div className="selections"> | |||
| <div className="selection-levels"> | |||
| <div className="level-header"> | |||
| <h1>{t("selection.title")} | |||
| <span className="level-header-spliter"> | |||
| | | |||
| </span> | |||
| <span className="level-header-subheader"> | |||
| Svi kandidati | |||
| </span> | |||
| </h1> | |||
| <IconButton | |||
| sx={{ marginLeft: "15px" }} | |||
| className="c-btn c-btn--primary-outlined" | |||
| onClick={handleToggleFiltersDrawer} | |||
| > | |||
| Filteri{" "} | |||
| <img src={filterVector} alt="filter" className="filter-vector" /> | |||
| </IconButton> | |||
| </div> | |||
| <div className="selection-levels-processes"> | |||
| <div className="selection-levels-processes-process"> | |||
| {renderList} | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </> | |||
| ); | |||
| }; | |||
| SelectionProcessPage.propTypes = { | |||
| history: PropTypes.shape({ | |||
| replace: PropTypes.func, | |||
| push: PropTypes.func, | |||
| location: PropTypes.shape({ | |||
| pathname: PropTypes.string, | |||
| }), | |||
| }), | |||
| }; | |||
| export default SelectionProcessPage; | |||
| @@ -24,5 +24,11 @@ export default { | |||
| }, | |||
| comments:{ | |||
| addComment:base + '/comments' | |||
| } | |||
| }, | |||
| processes: { | |||
| allLevels: base + "/selectionlevels", | |||
| doneProcess: base + "/selectionprocesses", | |||
| getApplicantProcesses: base + "/applicants/processes", | |||
| // allProcesses: base + "/selectionprocesses", | |||
| }, | |||
| }; | |||
| @@ -0,0 +1,6 @@ | |||
| import { getRequest } from "."; | |||
| import apiEndpoints from "./apiEndpoints"; | |||
| export const getAllLevels = () => getRequest(apiEndpoints.processes.allLevels); | |||
| export const doneProcess = (id) => getRequest(`${apiEndpoints.processes.doneProcess}/${id}`); | |||
| export const getProcessesOfApplicant = (id) => getRequest(`${apiEndpoints.processes.getApplicantProcesses}/${id}`); | |||
| @@ -0,0 +1,20 @@ | |||
| import { | |||
| FETCH_APPLICANT_PROCESSES_REQ, | |||
| FETCH_APPLICANT_PROCESSES_ERR, | |||
| FETCH_APPLICANT_PROCESSES_SUCCESS, | |||
| } from "./processesActionConstants"; | |||
| export const setApplicantProcessesReq = (payload) => ({ | |||
| type: FETCH_APPLICANT_PROCESSES_REQ, | |||
| payload | |||
| }); | |||
| export const setApplicantProcessesError = (payload) => ({ | |||
| type: FETCH_APPLICANT_PROCESSES_ERR, | |||
| payload, | |||
| }); | |||
| export const setApplicantProcesses = (payload) => ({ | |||
| type: FETCH_APPLICANT_PROCESSES_SUCCESS, | |||
| payload, | |||
| }); | |||
| @@ -0,0 +1,21 @@ | |||
| import { | |||
| PUT_PROCESS_ERR, | |||
| PUT_PROCESS_REQ, | |||
| PUT_PROCESS_SUCCESS | |||
| } from "./processesActionConstants"; | |||
| export const setDoneProcessReq = (payload) => ({ | |||
| type: PUT_PROCESS_REQ, | |||
| payload | |||
| }); | |||
| export const setDoneProcessError = (payload) => ({ | |||
| type: PUT_PROCESS_ERR, | |||
| payload, | |||
| }); | |||
| export const setDoneProcess = (payload) => ({ | |||
| type: PUT_PROCESS_SUCCESS, | |||
| payload | |||
| }); | |||
| @@ -0,0 +1,19 @@ | |||
| import { | |||
| FETCH_PROCESSES_REQ, | |||
| FETCH_PROCESSES_ERR, | |||
| FETCH_PROCESSES_SUCCESS, | |||
| } from "./processesActionConstants"; | |||
| export const setProcessesReq = () => ({ | |||
| type: FETCH_PROCESSES_REQ, | |||
| }); | |||
| export const setProcessesError = (payload) => ({ | |||
| type: FETCH_PROCESSES_ERR, | |||
| payload, | |||
| }); | |||
| export const setProcesses = (payload) => ({ | |||
| type: FETCH_PROCESSES_SUCCESS, | |||
| payload, | |||
| }); | |||
| @@ -0,0 +1,9 @@ | |||
| export const FETCH_PROCESSES_REQ = 'FETCH_PROCESSES_REQ'; | |||
| export const FETCH_PROCESSES_ERR = 'FETCH_PROCESSES_ERR'; | |||
| export const FETCH_PROCESSES_SUCCESS = 'FETCH_PROCESSES_SUCCESS'; | |||
| export const PUT_PROCESS_REQ = 'PUT_PROCESS_REQ'; | |||
| export const PUT_PROCESS_ERR = 'PUT_PROCESS_ERR'; | |||
| export const PUT_PROCESS_SUCCESS = 'PUT_PROCESS_SUCCESS'; | |||
| export const FETCH_APPLICANT_PROCESSES_REQ = 'FETCH_APPLICANT_PROCESSES_REQ'; | |||
| export const FETCH_APPLICANT_PROCESSES_ERR = 'FETCH_APPLICANT_PROCESSES_ERR'; | |||
| export const FETCH_APPLICANT_PROCESSES_SUCCESS = 'FETCH_APPLICANT_PROCESSES_SUCCESS'; | |||
| @@ -4,11 +4,14 @@ import loadingReducer from './loading/loadingReducer'; | |||
| import userReducer from './user/userReducer'; | |||
| import randomDataReducer from './randomData/randomDataReducer'; | |||
| import usersReducer from './user/usersReducer'; | |||
| import candidatesReducer from './candidates/candidatesReducer'; | |||
| import candidateReducer from './candidate/candidateReducer'; | |||
| import adsReducer from "./ad/adsReducer"; | |||
| import adReducer from "./ad/adReducer"; | |||
| import archiveAdsReducer from "./ad/archiveAdsReducer"; | |||
| import candidatesReducer from "./candidates/candidatesReducer"; | |||
| import processesReducer from './processes/processesReducer'; | |||
| import processReducer from "./processes/processReducer"; | |||
| import applicantProcessesReducer from "./processes/applicantProcessesReducer"; | |||
| export default combineReducers({ | |||
| login: loginReducer, | |||
| @@ -16,9 +19,12 @@ export default combineReducers({ | |||
| loading: loadingReducer, | |||
| randomData: randomDataReducer, | |||
| users: usersReducer, | |||
| candidates:candidatesReducer, | |||
| candidate:candidateReducer, | |||
| ads: adsReducer, | |||
| ad: adReducer, | |||
| archiveAds: archiveAdsReducer, | |||
| candidates: candidatesReducer, | |||
| processes: processesReducer, | |||
| process: processReducer, | |||
| applicantProcesses: applicantProcessesReducer | |||
| }); | |||
| @@ -0,0 +1,32 @@ | |||
| import { | |||
| FETCH_APPLICANT_PROCESSES_ERR, | |||
| FETCH_APPLICANT_PROCESSES_SUCCESS | |||
| } from "../../actions/processes/processesActionConstants"; | |||
| import createReducer from "../../utils/createReducer"; | |||
| const initialState = { | |||
| applicantProcesses: [], | |||
| errorMessage: "" | |||
| }; | |||
| export default createReducer( | |||
| { | |||
| [FETCH_APPLICANT_PROCESSES_SUCCESS]: setStateApplicantProcesses, | |||
| [FETCH_APPLICANT_PROCESSES_ERR]: setApplicantProcessesErrorMessage, | |||
| }, | |||
| initialState | |||
| ); | |||
| function setStateApplicantProcesses(state, action) { | |||
| return { | |||
| ...state, | |||
| applicantProcesses: action.payload, | |||
| }; | |||
| } | |||
| function setApplicantProcessesErrorMessage(state, action) { | |||
| return { | |||
| ...state, | |||
| errorMessage: action.payload, | |||
| }; | |||
| } | |||
| @@ -0,0 +1,33 @@ | |||
| import { | |||
| PUT_PROCESS_ERR, | |||
| PUT_PROCESS_SUCCESS | |||
| } from "../../actions/processes/processesActionConstants"; | |||
| import createReducer from "../../utils/createReducer"; | |||
| const initialState = { | |||
| doneProcess: false, | |||
| errorMessage: "", | |||
| }; | |||
| export default createReducer( | |||
| { | |||
| [PUT_PROCESS_SUCCESS]: setStateDoneProcess, | |||
| [PUT_PROCESS_ERR]: setDoneProcessErrorMessage, | |||
| }, | |||
| initialState | |||
| ); | |||
| function setStateDoneProcess(state, action) { | |||
| return { | |||
| ...state, | |||
| doneProcess: action.payload, | |||
| }; | |||
| } | |||
| function setDoneProcessErrorMessage(state, action) { | |||
| return { | |||
| ...state, | |||
| errorMessage: action.payload, | |||
| }; | |||
| } | |||
| @@ -0,0 +1,32 @@ | |||
| import { | |||
| FETCH_PROCESSES_ERR, | |||
| FETCH_PROCESSES_SUCCESS | |||
| } from "../../actions/processes/processesActionConstants"; | |||
| import createReducer from "../../utils/createReducer"; | |||
| const initialState = { | |||
| processes: [], | |||
| errorMessage: "", | |||
| }; | |||
| export default createReducer( | |||
| { | |||
| [FETCH_PROCESSES_SUCCESS]: setStateProcesses, | |||
| [FETCH_PROCESSES_ERR]: setProcessesErrorMessage, | |||
| }, | |||
| initialState | |||
| ); | |||
| function setStateProcesses(state, action) { | |||
| return { | |||
| ...state, | |||
| processes: action.payload, | |||
| }; | |||
| } | |||
| function setProcessesErrorMessage(state, action) { | |||
| return { | |||
| ...state, | |||
| errorMessage: action.payload, | |||
| }; | |||
| } | |||
| @@ -3,12 +3,14 @@ import adsSaga from "./adsSaga"; | |||
| import candidatesSaga from './candidatesSaga'; | |||
| import loginSaga from "./loginSaga"; | |||
| import usersSaga from "./usersSaga"; | |||
| import processesSaga from "./processSaga"; | |||
| export default function* rootSaga() { | |||
| yield all([ | |||
| loginSaga(), | |||
| usersSaga(), | |||
| adsSaga(), | |||
| candidatesSaga() | |||
| candidatesSaga(), | |||
| processesSaga(), | |||
| ]); | |||
| } | |||
| @@ -76,7 +76,6 @@ function* fetchUser({ payload }) { | |||
| function* forgetPassword({ payload }) { | |||
| try { | |||
| // console.log(payload) | |||
| const { data } = yield call(forgetPasswordEmailSend, payload); | |||
| yield put(forgetPasswordSuccess(data)); | |||
| if (payload.handleApiResponseSuccess) { | |||
| @@ -98,9 +97,7 @@ function* forgetPassword({ payload }) { | |||
| function* resetPassword({ payload }) { | |||
| try { | |||
| // console.log(payload) | |||
| const { data } = yield call(sendResetPassword, payload); | |||
| // console.log(data); | |||
| yield put(forgetPasswordSuccess(data)); | |||
| if (payload.handleApiResponseSuccess) { | |||
| yield call(payload.handleApiResponseSuccess); | |||
| @@ -0,0 +1,41 @@ | |||
| import { all, call, put, takeLatest } from "redux-saga/effects"; | |||
| import { getAllLevels, doneProcess, getProcessesOfApplicant } from "../../request/processesReguest"; | |||
| import { setProcesses, setProcessesError } from "../actions/processes/processesAction"; | |||
| import { setDoneProcess, setDoneProcessError } from "../actions/processes/processAction"; | |||
| import { setApplicantProcesses, setApplicantProcessesError } from "../actions/processes/applicantProcessesAction"; | |||
| import { FETCH_PROCESSES_REQ, PUT_PROCESS_REQ, FETCH_APPLICANT_PROCESSES_REQ } from "../actions/processes/processesActionConstants"; | |||
| export function* getProcesses() { | |||
| try { | |||
| const result = yield call(getAllLevels); | |||
| yield put(setProcesses(result.data)); | |||
| } catch (error) { | |||
| yield put(setProcessesError(error)); | |||
| } | |||
| } | |||
| export function* finishProcess(payload) { | |||
| try { | |||
| const id = payload.payload.id; | |||
| const result = yield call(doneProcess,id); | |||
| yield put(setDoneProcess(result.data)); | |||
| } catch (error) { | |||
| yield put(setDoneProcessError(error)); | |||
| } | |||
| } | |||
| export function* getApplicantProcesses(payload) { | |||
| try { | |||
| const id = payload.payload; | |||
| const result = yield call(getProcessesOfApplicant,id); | |||
| yield put(setApplicantProcesses(result.data.selectionProcesses)); | |||
| } catch (error) { | |||
| yield put(setApplicantProcessesError(error)); | |||
| } | |||
| } | |||
| export default function* processesSaga() { | |||
| yield all([takeLatest(FETCH_PROCESSES_REQ, getProcesses)]); | |||
| yield all([takeLatest(PUT_PROCESS_REQ, finishProcess)]); | |||
| yield all([takeLatest(FETCH_APPLICANT_PROCESSES_REQ, getApplicantProcesses)]); | |||
| } | |||
| @@ -0,0 +1,10 @@ | |||
| import { createSelector } from "@reduxjs/toolkit"; | |||
| export const applicantProcessesSelector = (state) => state.applicantProcesses; | |||
| export const selectApplicantProcesses = createSelector(applicantProcessesSelector, (state) => state.applicantProcesses); | |||
| export const selectApplicantProcessesError = createSelector( | |||
| applicantProcessesSelector, | |||
| (state) => state.errorMessage | |||
| ); | |||
| @@ -0,0 +1,10 @@ | |||
| import { createSelector } from "@reduxjs/toolkit"; | |||
| export const doneProcessSelector = (state) => state; | |||
| export const selectDoneProcess = createSelector(doneProcessSelector, (state) => state); | |||
| export const selectDoneProcessError = createSelector( | |||
| doneProcessSelector, | |||
| (state) => state.errorMessage | |||
| ); | |||
| @@ -0,0 +1,10 @@ | |||
| import { createSelector } from "@reduxjs/toolkit"; | |||
| export const processesSelector = (state) => state.processes; | |||
| export const selectProcesses = createSelector(processesSelector, (state) => state.processes); | |||
| export const selectProcessesError = createSelector( | |||
| processesSelector, | |||
| (state) => state.errorMessage | |||
| ); | |||
| @@ -38,3 +38,13 @@ export function formatDateRange(dates) { | |||
| const end = formatDate(dates.end); | |||
| return i18next.t('common.date.range', { start, end }); | |||
| } | |||
| export function formatDateSrb(date) { | |||
| const dt = new Date(date); | |||
| return format(dt, 'dd.MM.'); | |||
| } | |||
| export function formatTimeSrb(date) { | |||
| const dt = new Date(date); | |||
| return format(dt, 'HH.mm.'); | |||
| } | |||