Просмотр исходного кода

Added ability to move applicant from one selection to other

pull/41/head
Safet Purkovic 3 лет назад
Родитель
Сommit
327544aec7

+ 73
- 230
src/assets/styles/components/_selectionProcessPage.scss Просмотреть файл

h1,
h3 {
margin: 0;
padding: 0;
}

.ads {
.selections {
margin-top: 36px; margin-top: 36px;
padding-left: 3rem;
padding-left: 72px;
} }


.active-ads-header {
.level-header {
padding-left: 81px; padding-left: 81px;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
} }


.activee{
/* Blue 4 */

background : #E8F7FF;
border : 1px solid #226CB0;
}

.active-ads
{
.selection-levels {
overflow-x: scroll; overflow-x: scroll;
padding-bottom: 100px; padding-bottom: 100px;
} }


.active-ads-subheader {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 600;
font-size: 24px;
line-height: 36px;
padding-left: 0.3rem;
/* identical to box height, or 100% */
color:#226CB0;
letter-spacing: 0.02em;
}
.active-ads-subheader-spliter {
.level-header-subheader {
font-family: 'Source Sans Pro'; font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 600;
font-size: 24px;
line-height: 36px;
padding-left: 0.3rem;
/* identical to box height, or 100% */
color:#272727;
letter-spacing: 0.02em;
font-style: normal;
font-weight: 600;
font-size: 24px;
line-height: 36px;
padding-left: 0.3rem;
color: #226CB0;
letter-spacing: 0.02em;
} }


.filter-vector {
margin-left: 0.5rem !important;
.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;
} }


.active-ads-ads {
.selection-levels-processes {
display: flex; display: flex;
margin-top: 39px; margin-top: 39px;
position: relative; position: relative;
} }


.active-ads-ads-ad {
.selection-levels-processes-process {
padding-left: 81px; padding-left: 81px;
display: flex; display: flex;
} }



.selection-card { .selection-card {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: start; justify-content: start;
align-items: left; align-items: left;
// width: 550px;
height: fit-content; height: fit-content;
padding: 36px; padding: 36px;
background: #F4F4F4; background: #F4F4F4;
margin-right: 36px; margin-right: 36px;
} }


.bg-danger{
.bg-danger {
background-color: #272727; background-color: #272727;
} }


.grey {
color: #e4e4e4
}

.selection-item { .selection-item {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
vertical-align: top; vertical-align: top;
align-items: left; align-items: left;
width: 400px; width: 400px;
// height: 400px;
padding: 18px 36px; padding: 18px 36px;
background: #FFFFFF; background: #FFFFFF;
border: 1px solid #e4e4e4; border: 1px solid #e4e4e4;
flex-grow: 0; flex-grow: 0;
} }


.ad-card-logo img {
width: 61px;
height: 49px;
flex: none;
order: 2;
flex-grow: 0;
}
.selection-item-name, .selection-item-date{
margin: auto 0 !important;
.selection-item-name,
.selection-item-date {
margin: auto 0 !important;
} }

.selection-item-name p { .selection-item-name p {
height: 20px; 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;
}

.ad-card-buttons {
display: flex;
flex-direction: row;
align-items: flex-start;
justify-content: center;
padding: 0px;
gap: 18px;
width: 281px;
height: 38px;
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 600;
font-size: 16px;
line-height: 20px;
text-align: right;
color: #226CB0;
flex: none; flex: none;
order: 0;
order: 2;
flex-grow: 0; flex-grow: 0;
} }


flex-grow: 0; flex-grow: 0;
} }


.add-ad {
margin-top: 49px;
display: flex;
justify-content: flex-end;
align-items: center;
padding-right: 5rem !important;
padding-bottom: 49px;
}

.add-ad-btn {
.sel-item {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: center;
align-items: center; align-items: center;
padding: 18px 72px;
gap: 10px;
width: 201px;
height: 51px;
background: #226cb0;
border-radius: 9px;
}

.ad-filters-header-container {
display: flex;
justify-content: space-between;
}

.ad-filters-header {
display: flex;
align-items: center;
}

.ad-filters-header-close {
cursor: pointer;
}

.ad-filters-header > * {
margin-right: 0.25rem;
}

.ad-filters-header img {
width: 18px;
height: 15.75px;
}

.ad-filters-header sub {
color: #226cb0;
padding: 18px 36px;
gap: 18px;
width: 458px;
background: #FFFFFF;
border: 1px solid #E4E4E4;
border-radius: 18px;
} }


.ad-filters-sub-title {
font-family: "Source Sans Pro";
.sel-item .status {
font-family: 'Source Sans Pro';
font-style: normal; font-style: normal;
font-weight: 600;
font-weight: 400;
font-size: 16px; font-size: 16px;
line-height: 20px; line-height: 20px;
color: #272727; color: #272727;
flex: none;
order: 0;
flex-grow: 0;
} }


.ad-filters-experience {
margin-top: 18px;
box-sizing: border-box;
}

.ad-filters-experience-slider {
margin-top: 5px;
}

.ad-filters-technologies {
margin-top: 18px;
}

.ad-filters-employment-type {
display: flex;
}

.ad-filters-employment-type > button {
margin-right: 0.5rem;
margin-top: 18px;
}

.ad-filters-search {
margin-top: 18px;
padding-bottom: 18px;
}

.ad-filters-search > * {
width: 100%;
}

.sel-item{
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 18px 36px;
gap: 18px;

width: 458px;

/* White */

background: #FFFFFF;
/* Gray E4 */

border: 1px solid #E4E4E4;
border-radius: 18px;
}

.sel-item .p{
/* Paragraph */

font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 400;
font-size: 16px;
line-height: 20px;

/* Main Black */
color: #272727;


/* Inside auto layout */

flex: none;
order: 0;
flex-grow: 0;
}

.sel-item .date{
/* 22.07. | 14:10h */

/* Paragraph */

font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 400;
font-size: 16px;
line-height: 20px;

/* Main Black */

color: #272727;
.sel-item .date {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 400;
font-size: 16px;
line-height: 20px;
color: #272727;
} }


.sel-item .rig{
height: 20px;

/* Bold Paragraph */

font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 600;
font-size: 16px;
line-height: 20px;
text-align: right;

/* Main Blue */

color: #226CB0;


/* Inside auto layout */

flex: none;
order: 1;
flex-grow: 0;
.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 .p button {
.sel-item .status button {
box-sizing: border-box; box-sizing: border-box;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
min-width: 76px; min-width: 76px;
height: 38px; height: 38px;
border: 1px solid #e4e4e4; border: 1px solid #e4e4e4;
background: white;
border-radius: 9px; border-radius: 9px;
flex: none; flex: none;
order: 0; order: 0;

+ 47
- 25
src/components/Selection/Selection.js Просмотреть файл

import React from "react"; import React from "react";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import { selectDoneProcessError } from "../../store/selectors/processesSelectors";
import { setDoneProcessReq } from "../../store/actions/processes/processesAction";
import { useDispatch, useSelector } from "react-redux";
import { formatDateSrb, formatTimeSrb } from "../../util/helpers/dateHelpers";



const dragStart = (e, applicant) => {
// e.dataTransfer.setData("applicant", applicant.id);
e.dataTransfer.setData("text/plain",JSON.stringify(applicant));
}


const dragOver = (e) => {
e.preventDefault();
}


const dropItem = (e,selId) =>{
var data = e.dataTransfer.getData("text/plain");
const applicant = JSON.parse(data);
if(applicant.currentSelection !== selId){
// SEND REQUEST TO BACKEND TO STORE NEW SELECTION
console.log('jup')
}
}


const Selection = (props) => { const Selection = (props) => {
console.log(props.selection);
const applicants = props.selection.applicants;
const renderList = applicants.map((item, index) => {
const applicants = props.selection.selectionProcesses;
const errorMessage = useSelector(selectDoneProcessError);
const dispatch = useDispatch();

const dragStart = (e, applicant) => {
e.dataTransfer.setData("text/plain",JSON.stringify(applicant));
}
const dragOver = (e) => {
e.preventDefault();
}
const dropItem = (e,selId) =>{
var data = e.dataTransfer.getData("text/plain");
const selectionProcess = JSON.parse(data);
console.log(selectionProcess)
if(selectionProcess.selectionLevelId !== selId){
dispatch(setDoneProcessReq({id: selectionProcess.id}));
}
if(errorMessage)
{
console.log(errorMessage)
}
}

const renderList = applicants?.map((item, index) => {
return <div draggable key={index} className="sel-item" onDragStart={e => dragStart(e,item)}> return <div draggable key={index} className="sel-item" onDragStart={e => dragStart(e,item)}>
<div className="p">
<div className="status">
<button>{item.status}</button> <button>{item.status}</button>
</div> </div>
<div className="date"> <div className="date">
<p>{item.date}</p>
<p>{formatDateSrb(item.date)} <span className="grey">|</span> {formatTimeSrb(item.date)}</p>
</div> </div>
<div className="rig">
<p>{item.name}</p>
<div className="full-name">
<p>{item.applicant.firstName + " " + item.applicant.lastName}</p>
</div> </div>
</div> </div>
} }
<h3>{props.selection.name}</h3> <h3>{props.selection.name}</h3>
</div> </div>


{renderList}
{applicants.length > 0 && renderList}
{applicants.length === 0 && <div className="sel-item">
<div className="date">
<p>Nema kandidata u selekciji</p>
</div>
</div>}
</div> </div>
); );
}; };
selection: PropTypes.shape({ selection: PropTypes.shape({
id: PropTypes.number, id: PropTypes.number,
name : PropTypes.string, name : PropTypes.string,
applicants: PropTypes.arrayOf(PropTypes.shape({
selectionProcesses: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.number, id: PropTypes.number,
name: PropTypes.string, name: PropTypes.string,
date: PropTypes.string, date: PropTypes.string,
status: PropTypes.string, status: PropTypes.string,
currentSelection: PropTypes.number, currentSelection: PropTypes.number,
map: PropTypes.func
map: PropTypes.func,
applicant: PropTypes.shape({
firstName: PropTypes.string,
lastName: PropTypes.string
})
})) }))
}), }),
}; };

+ 29
- 101
src/pages/SelectionProcessPage/SelectionProcessPage.js Просмотреть файл

import React, { useState } from "react";
import React, { useState, useEffect } from "react";
import { useSelector} from 'react-redux';
import Selection from "../../components/Selection/Selection"; import Selection from "../../components/Selection/Selection";
import IconButton from "../../components/IconButton/IconButton"; import IconButton from "../../components/IconButton/IconButton";
import filterVector from "../../assets/images/filter_vector.png"; import filterVector from "../../assets/images/filter_vector.png";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import AddAdModal from "../../components/Ads/AddAdModal"; import AddAdModal from "../../components/Ads/AddAdModal";
import { useDispatch } from "react-redux";
import AdFilters from "../../components/Ads/AdFilters"; import AdFilters from "../../components/Ads/AdFilters";
import { setProcessesReq } from "../../store/actions/processes/processesAction";
import { selectProcesses, selectDoneProcess } from "../../store/selectors/processesSelectors";


const SelectionProcessPage = () => { const SelectionProcessPage = () => {
const [toggleFiltersDrawer, setToggleFiltersDrawer] = useState(false); const [toggleFiltersDrawer, setToggleFiltersDrawer] = useState(false);
const [toggleModal, setToggleModal] = useState(false); const [toggleModal, setToggleModal] = useState(false);
// const errorMessage = useSelector(selectProcessesError);
const processes = useSelector(selectProcesses);
// const doneErrorMessage = useSelector(selectDoneProcessError);
const doneProcess = useSelector(selectDoneProcess);
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useDispatch();

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

useEffect(() => {
dispatch(setProcessesReq());
},[doneProcess])

// console.log(errorMessage);
// console.log(doneErrorMessage);


const handleToggleFiltersDrawer = () => { const handleToggleFiltersDrawer = () => {
setToggleFiltersDrawer((oldState) => !oldState); setToggleFiltersDrawer((oldState) => !oldState);
setToggleModal((oldState) => !oldState); setToggleModal((oldState) => !oldState);
}; };


const selections = [
{
id: 1,
name: "HR interview",
applicants: [
{
id: 1,
name: "Stefan Petrovic",
status: "Zakazan",
date: "01.01.2022 11:00",
currentSelection: 1
},
{
id: 2,
name: "Stefan Petrovic",
status: "Otkazan",
date: "01.01.2022 11:00",
currentSelection: 1
},
{
id: 3,
name: "Stefan Petrovic",
status: "Ceka na zakazivanje",
currentSelection: 1
}]
},
{
id: 2,
name: "Screening test",
applicants: [
{
id: 1,
name: "Stefan Petrovic",
status: "Zakazan",
date: "01.01.2022 11:00",
currentSelection: 2
},
{
id: 2,
name: "Stefan Petrovic",
status: "Otkazan",
date: "01.01.2022 11:00",
currentSelection: 2
}]
},
{
id: 3,
name: "Technical interview",
applicants: [
{
id: 1,
name: "Stefan Petrovic",
status: "Zakazan",
date: "01.01.2022 11:00",
currentSelection: 3
},
{
id: 2,
name: "Stefan Petrovic",
status: "Otkazan",
date: "01.01.2022 11:00",
currentSelection: 3
},
{
id: 3,
name: "Stefan Petrovic",
status: "Ceka na zakazivanje",
currentSelection: 3
}]
},
{
id: 4,
name: "Final decision",
applicants: [
{
id: 1,
name: "Stefan Petrovic",
status: "Zakazan",
date: "01.01.2022 11:00",
currentSelection: 4
},
{
id: 2,
name: "Stefan Petrovic",
status: "Otkazan",
date: "01.01.2022 11:00",
currentSelection: 4
}]
}
]


const renderList = selections.map((item, index) => {
const renderList = processes.map((item, index) => {
return <Selection selection={item} key={index}/> return <Selection selection={item} key={index}/>
} }
); );
handleClose={handleToggleFiltersDrawer} handleClose={handleToggleFiltersDrawer}
/> />
<AddAdModal open={toggleModal} handleClose={handleToggleModal} /> <AddAdModal open={toggleModal} handleClose={handleToggleModal} />
<div className="ads">
<div className="active-ads">
<div className="active-ads-header">
<div className="selections">
<div className="selection-levels">
<div className="level-header">
<h1>{t("selection.title")} <h1>{t("selection.title")}
<span className="active-ads-subheader-spliter">
<span className="level-header-spliter">
| |
</span> </span>
<span className="active-ads-subheader">
<span className="level-header-subheader">
Svi kandidati Svi kandidati
</span> </span>
</h1> </h1>
<img src={filterVector} alt="filter" className="filter-vector" /> <img src={filterVector} alt="filter" className="filter-vector" />
</IconButton> </IconButton>
</div> </div>
<div className="active-ads-ads">
<div className="active-ads-ads-ad">
<div className="selection-levels-processes">
<div className="selection-levels-processes-process">
{renderList} {renderList}
</div> </div>
</div> </div>

+ 6
- 1
src/request/apiEndpoints.js Просмотреть файл

}, },
comments:{ comments:{
addComment:base + '/comments' addComment:base + '/comments'
}
},
processes: {
allLevels: base + "/selectionlevels",
doneProcess: base + "/selectionprocesses",
// allProcesses: base + "/selectionprocesses",
},
}; };

+ 5
- 0
src/request/processesReguest.js Просмотреть файл

import { getRequest } from ".";
import apiEndpoints from "./apiEndpoints";

export const getAllLevels = () => getRequest(apiEndpoints.processes.allLevels);
export const doneProcess = (id) => getRequest(`${apiEndpoints.processes.doneProcess}/${id}`);

+ 37
- 0
src/store/actions/processes/processesAction.js Просмотреть файл

import {
FETCH_PROCESSES_REQ,
FETCH_PROCESSES_ERR,
FETCH_PROCESSES_SUCCESS,
PUT_PROCESS_ERR,
PUT_PROCESS_REQ,
PUT_PROCESS_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,
});

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
});

+ 6
- 0
src/store/actions/processes/processesActionConstants.js Просмотреть файл

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';

+ 5
- 0
src/store/reducers/index.js Просмотреть файл

import adsReducer from "./ad/adsReducer"; import adsReducer from "./ad/adsReducer";
import adReducer from "./ad/adReducer"; import adReducer from "./ad/adReducer";
import archiveAdsReducer from "./ad/archiveAdsReducer"; import archiveAdsReducer from "./ad/archiveAdsReducer";
import candidatesReducer from "./candidates/candidatesReducer";
import candidatesReducer from './candidates/candidatesReducer';
import processesReducer from './processes/processesReducer';


export default combineReducers({ export default combineReducers({
login: loginReducer, login: loginReducer,
ads: adsReducer, ads: adsReducer,
ad: adReducer, ad: adReducer,
archiveAds: archiveAdsReducer, archiveAds: archiveAdsReducer,
candidates: candidatesReducer,
processes: processesReducer
}); });

+ 51
- 0
src/store/reducers/processes/processesReducer.js Просмотреть файл

import {
FETCH_PROCESSES_ERR,
FETCH_PROCESSES_SUCCESS,
PUT_PROCESS_ERR,
PUT_PROCESS_SUCCESS
} from "../../actions/processes/processesActionConstants";
import createReducer from "../../utils/createReducer";

const initialState = {
processes: [],
doneProcess: false,
errorMessage: "",
};

export default createReducer(
{
[FETCH_PROCESSES_SUCCESS]: setStateProcesses,
[FETCH_PROCESSES_ERR]: setProcessesErrorMessage,
[PUT_PROCESS_SUCCESS]: setStateDoneProcess,
[PUT_PROCESS_ERR]: setDoneProcessErrorMessage,
},
initialState
);

function setStateProcesses(state, action) {
return {
...state,
processes: action.payload,
};
}

function setProcessesErrorMessage(state, action) {
return {
...state,
errorMessage: action.payload,
};
}

function setStateDoneProcess(state, action) {
return {
...state,
doneProcess: action.payload,
};
}

function setDoneProcessErrorMessage(state, action) {
return {
...state,
errorMessage: action.payload,
};
}

+ 3
- 1
src/store/saga/index.js Просмотреть файл

import candidatesSaga from './candidatesSaga'; import candidatesSaga from './candidatesSaga';
import loginSaga from "./loginSaga"; import loginSaga from "./loginSaga";
import usersSaga from "./usersSaga"; import usersSaga from "./usersSaga";
import processesSaga from "./processSaga";


export default function* rootSaga() { export default function* rootSaga() {
yield all([ yield all([
loginSaga(), loginSaga(),
usersSaga(), usersSaga(),
adsSaga(), adsSaga(),
candidatesSaga()
candidatesSaga(),
processesSaga(),
]); ]);
} }

+ 28
- 0
src/store/saga/processSaga.js Просмотреть файл

import { all, call, put, takeLatest } from "redux-saga/effects";
import { getAllLevels, doneProcess } from "../../request/processesReguest";
import { setProcesses, setProcessesError, setDoneProcess, setDoneProcessError } from "../actions/processes/processesAction";
import { FETCH_PROCESSES_REQ, PUT_PROCESS_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 done = yield call(doneProcess,id);
yield put(setDoneProcess(done));
} catch (error) {
yield put(setDoneProcessError(error));
}
}

export default function* processesSaga() {
yield all([takeLatest(FETCH_PROCESSES_REQ, getProcesses)]);
yield all([takeLatest(PUT_PROCESS_REQ, finishProcess)]);
}

+ 19
- 0
src/store/selectors/processesSelectors.js Просмотреть файл

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
);

export const doneProcessSelector = (state) => state.processes;

export const selectDoneProcess = createSelector(doneProcessSelector, (state) => state.processes);

export const selectDoneProcessError = createSelector(
doneProcessSelector,
(state) => state.errorMessage
);

+ 10
- 0
src/util/helpers/dateHelpers.js Просмотреть файл

const end = formatDate(dates.end); const end = formatDate(dates.end);
return i18next.t('common.date.range', { start, 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.');
}

Загрузка…
Отмена
Сохранить