diff --git a/src/components/ActionsMenu/index.jsx b/src/components/ActionsMenu/index.jsx index b888ade0..7bc36da3 100644 --- a/src/components/ActionsMenu/index.jsx +++ b/src/components/ActionsMenu/index.jsx @@ -100,7 +100,11 @@ const ActionsMenu = ({ options = [] }) => { onClick={closeOnAction(option.action)} role="button" tabIndex={0} - styleName="option" + styleName={ + "option" + + (option.style ? " " + option.style : "") + + (option.disabled ? " disabled" : "") + } > {option.label} diff --git a/src/components/ActionsMenu/styles.module.scss b/src/components/ActionsMenu/styles.module.scss index c9556de7..18d15c78 100644 --- a/src/components/ActionsMenu/styles.module.scss +++ b/src/components/ActionsMenu/styles.module.scss @@ -39,10 +39,23 @@ } .option { - color: #0d61bf; + color: #219174; cursor: pointer; - font-size: 14px; - line-height: 20px; + font-size: 12px; + font-weight: bold; + letter-spacing: 0.8px; + line-height: 30px; + text-transform: uppercase; outline: none; padding: 5px 0; } + +.danger { + color: #EF476F; +} + +.disabled { + color: gray; + opacity: 0.6; + pointer-events: none; +} diff --git a/src/constants/index.js b/src/constants/index.js index 9155d978..7cd2c580 100644 --- a/src/constants/index.js +++ b/src/constants/index.js @@ -97,8 +97,10 @@ export const RATE_TYPE = { export const CANDIDATE_STATUS = { OPEN: "open", SELECTED: "selected", - SHORTLIST: "shortlist", - REJECTED: "rejected", + PLACED: "placed", + CLIENT_REJECTED_SCREENING: "client rejected - screening", + CLIENT_REJECTED_INTERVIEW: "client rejected - interview", + REJECTED_OTHER: "rejected - other", INTERVIEW: "interview", TOPCODER_REJECTED: "topcoder-rejected", }; @@ -126,13 +128,18 @@ export const CANDIDATE_STATUS_FILTERS = [ key: CANDIDATE_STATUS_FILTER_KEY.INTERESTED, buttonText: "Interviews", title: "Interviews", - statuses: [CANDIDATE_STATUS.SHORTLIST, CANDIDATE_STATUS.INTERVIEW], + statuses: [CANDIDATE_STATUS.SELECTED, CANDIDATE_STATUS.INTERVIEW], }, { key: CANDIDATE_STATUS_FILTER_KEY.NOT_INTERESTED, buttonText: "Declined", title: "Declined", - statuses: [CANDIDATE_STATUS.REJECTED, CANDIDATE_STATUS.TOPCODER_REJECTED], + statuses: [ + CANDIDATE_STATUS.CLIENT_REJECTED_SCREENING, + CANDIDATE_STATUS.CLIENT_REJECTED_INTERVIEW, + CANDIDATE_STATUS.REJECTED_OTHER, + CANDIDATE_STATUS.TOPCODER_REJECTED, + ], }, ]; @@ -297,7 +304,7 @@ export const JOB_STATUS_OPTIONS = [ * resource booking status options */ export const RESOURCE_BOOKING_STATUS_OPTIONS = [ - { value: "assigned", label: "assigned" }, + { value: "placed", label: "placed" }, { value: "closed", label: "closed" }, { value: "cancelled", label: "cancelled" }, ]; diff --git a/src/routes/PositionDetails/components/InterviewDetailsPopup/index.jsx b/src/routes/PositionDetails/components/InterviewDetailsPopup/index.jsx index 363bf95c..e7a6d363 100644 --- a/src/routes/PositionDetails/components/InterviewDetailsPopup/index.jsx +++ b/src/routes/PositionDetails/components/InterviewDetailsPopup/index.jsx @@ -4,11 +4,13 @@ * Popup that allows user to schedule an interview * Calls addInterview action */ -import React, { useCallback } from "react"; +import React, { useCallback, useEffect, useState } from "react"; +import { getAuthUserProfile } from "@topcoder/micro-frontends-navbar-app"; import { Form } from "react-final-form"; import arrayMutators from "final-form-arrays"; import { FieldArray } from "react-final-form-arrays"; -import { useDispatch } from "react-redux"; +import { toastr } from "react-redux-toastr"; +import { useDispatch, useSelector } from "react-redux"; import { addInterview } from "../../actions"; import User from "components/User"; import BaseModal from "components/BaseModal"; @@ -39,10 +41,20 @@ const validator = (values) => { }; /********************* */ - +// TODO: preserve form input in case of error function InterviewDetailsPopup({ open, onClose, candidate, openNext }) { + const [isLoading, setIsLoading] = useState(true); + const [myEmail, setMyEmail] = useState(""); + const { loading } = useSelector((state) => state.positionDetails); const dispatch = useDispatch(); + useEffect(() => { + getAuthUserProfile().then((res) => { + setMyEmail(res.email || ""); + setIsLoading(false); + }); + }, []); + const onSubmitCallback = useCallback( async (formData) => { const attendeesList = @@ -54,15 +66,21 @@ function InterviewDetailsPopup({ open, onClose, candidate, openNext }) { attendeesList, }; - await dispatch(addInterview(candidate.id, interviewData)); + try { + await dispatch(addInterview(candidate.id, interviewData)); + } catch (err) { + toastr.error("Interview Creation Failed", err.message); + throw err; + } }, [dispatch, candidate] ); - return ( + return isLoading ? null : (
Begin scheduling @@ -154,6 +172,7 @@ function InterviewDetailsPopup({ open, onClose, candidate, openNext }) { label: "Email Address", maxLength: 320, customValidator: true, + disabled: index === 0, }} /> diff --git a/src/routes/PositionDetails/components/PositionCandidates/index.jsx b/src/routes/PositionDetails/components/PositionCandidates/index.jsx index ede24193..61cd6f98 100644 --- a/src/routes/PositionDetails/components/PositionCandidates/index.jsx +++ b/src/routes/PositionDetails/components/PositionCandidates/index.jsx @@ -140,10 +140,10 @@ const PositionCandidates = ({ position, statusFilterKey, updateCandidate }) => { [setPage] ); - const markCandidateShortlisted = useCallback( - (candidateId) => { - return updateCandidate(candidateId, { - status: CANDIDATE_STATUS.SHORTLIST, + const markCandidateSelected = useCallback( + (candidate) => { + return updateCandidate(candidate.id, { + status: CANDIDATE_STATUS.SELECTED, }) .then(() => { toastr.success("Candidate is marked as interested."); @@ -161,9 +161,13 @@ const PositionCandidates = ({ position, statusFilterKey, updateCandidate }) => { ); const markCandidateRejected = useCallback( - (candidateId) => { - return updateCandidate(candidateId, { - status: CANDIDATE_STATUS.REJECTED, + (candidate) => { + const hasInterviews = + candidate.interviews && candidate.interviews.length > 0; + return updateCandidate(candidate.id, { + status: hasInterviews + ? CANDIDATE_STATUS.CLIENT_REJECTED_INTERVIEW + : CANDIDATE_STATUS.CLIENT_REJECTED_SCREENING, }) .then(() => { toastr.success("Candidate is marked as not interested."); @@ -259,6 +263,7 @@ const PositionCandidates = ({ position, statusFilterKey, updateCandidate }) => { action: () => { openSelectCandidatePopup(candidate, true); }, + style: "danger", }, ]} /> @@ -267,20 +272,41 @@ const PositionCandidates = ({ position, statusFilterKey, updateCandidate }) => { {statusFilterKey === CANDIDATE_STATUS_FILTER_KEY.INTERESTED && hasPermission(PERMISSIONS.UPDATE_JOB_CANDIDATE) && (
- - {candidate.interviews && - candidate.interviews.length > 0 && ( - - )} + { + openInterviewDetailsPopup(candidate); + }, + }, + { + label: "View Previous Interviews", + action: () => { + openPrevInterviewsPopup(candidate); + }, + disabled: + !!candidate.interviews !== true || + candidate.interviews.length === 0, + }, + { + separator: true, + }, + { + label: "Select Candidate", + action: () => { + openSelectCandidatePopup(candidate); + }, + }, + { + label: "Decline Candidate", + action: () => { + openSelectCandidatePopup(candidate, true); + }, + style: "danger", + }, + ]} + />
)} @@ -329,7 +355,7 @@ const PositionCandidates = ({ position, statusFilterKey, updateCandidate }) => { candidate={selectedCandidate} open={selectCandidateOpen} isReject={isReject} - shortList={markCandidateShortlisted} + select={markCandidateSelected} reject={markCandidateRejected} closeModal={() => setSelectCandidateOpen(false)} /> diff --git a/src/routes/PositionDetails/components/SelectCandidatePopup/index.jsx b/src/routes/PositionDetails/components/SelectCandidatePopup/index.jsx index 3ccae975..c2bf6c14 100644 --- a/src/routes/PositionDetails/components/SelectCandidatePopup/index.jsx +++ b/src/routes/PositionDetails/components/SelectCandidatePopup/index.jsx @@ -17,19 +17,19 @@ const SelectCandidatePopup = ({ open, closeModal, reject, - shortList, + select, }) => { const [isLoading, setIsLoading] = useState(false); const confirmSelection = useCallback(async () => { setIsLoading(true); if (isReject) { - await reject(candidate.id); + await reject(candidate); } else { - await shortList(candidate.id); + await select(candidate); } setIsLoading(false); - }, [isReject, candidate, reject, shortList]); + }, [isReject, candidate, reject, select]); return ( {isLoading ? ( + ) : isReject ? ( +

Are you sure you want to decline the selected candidate?

) : ( -

- {isReject - ? "Are you sure you want to decline the selected candidate?" - : "Please confirm your selection of the above candidate"} -

+ <> +

+ You have selected this applicant - you want this member on your + team! What happens next: +

+
    +
  1. + Upon confirmation, Topcoder will confirm the arrangement with + the selected member +
  2. +
  3. + A Topcoder Rep will contact you with details on the work + arrangement +
  4. +
  5. + When both sides accept, we will finalize the agreement and + begin onboarding +
  6. +
+ )} )} diff --git a/src/routes/PositionDetails/components/SelectCandidatePopup/styles.module.scss b/src/routes/PositionDetails/components/SelectCandidatePopup/styles.module.scss index af224c4b..d05d1184 100644 --- a/src/routes/PositionDetails/components/SelectCandidatePopup/styles.module.scss +++ b/src/routes/PositionDetails/components/SelectCandidatePopup/styles.module.scss @@ -1,3 +1,11 @@ +ol { + list-style-type: decimal; + padding: 25px; + li { + margin-top: 5px; + } +} + .user { font-size: 14px; color: #0D61BF; diff --git a/src/routes/PositionDetails/reducers/index.js b/src/routes/PositionDetails/reducers/index.js index 16289269..8a46a7ac 100644 --- a/src/routes/PositionDetails/reducers/index.js +++ b/src/routes/PositionDetails/reducers/index.js @@ -59,9 +59,10 @@ const patchInterviewInState = (state, candidateId, interviewData) => { return state; } + const hasInterviews = !!state.position.candidates[candidateIndex].interviews; const updatedCandidate = update(state.position.candidates[candidateIndex], { status: { $set: "interview" }, - interviews: { $push: [interviewData] }, + interviews: { [hasInterviews ? "$push" : "$set"]: [interviewData] }, }); return update(state, { @@ -121,11 +122,10 @@ const reducer = (state = initialState, action) => { }); case ACTION_TYPE.ADD_INTERVIEW_PENDING: - return { - ...state, - loading: true, - error: undefined, - }; + return update(state, { + loading: { $set: true }, + error: { $set: undefined }, + }); case ACTION_TYPE.ADD_INTERVIEW_SUCCESS: return patchInterviewInState(state, action.meta.candidateId, { @@ -133,11 +133,10 @@ const reducer = (state = initialState, action) => { }); case ACTION_TYPE.ADD_INTERVIEW_ERROR: - return { - ...state, - loading: false, - error: action.payload, - }; + return update(state, { + loading: { $set: false }, + error: { $set: action.payload }, + }); default: return state; }