Skip to content
This repository was archived by the owner on Mar 13, 2025. It is now read-only.
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 14bb09a

Browse files
committedFeb 20, 2021
Merge branch 'dev' into feature/member-management
# Conflicts: # src/components/FormField/index.jsx # src/components/TCForm/styles.module.scss
2 parents 241083d + ee297cb commit 14bb09a

File tree

18 files changed

+316
-296
lines changed

18 files changed

+316
-296
lines changed
 

‎src/components/DateInput/index.jsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,19 @@
66
import React from "react";
77
import PT from "prop-types";
88
import DatePicker from "react-datepicker";
9+
import cn from "classnames";
910
import "./styles.module.scss";
1011

1112
const DateInput = (props) => {
1213
return (
13-
<div styleName="datepicker-wrapper">
14+
<div styleName={cn("datepicker-wrapper", props.className)}>
1415
<DatePicker
1516
dateFormat="MM/dd/yyyy"
1617
placeholderText={props.placeholder}
1718
selected={props.value}
1819
onChange={props.onChange}
1920
onBlur={props.onBlur}
21+
onCalendarClose={props.onBlur}
2022
onFocus={props.onFocus}
2123
/>
2224
</div>
@@ -29,6 +31,7 @@ DateInput.propTypes = {
2931
placeholder: PT.string,
3032
onBlur: PT.func,
3133
onFocus: PT.func,
34+
className: PT.string
3235
};
3336

3437
export default DateInput;

‎src/components/DateInput/styles.module.scss

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@
44
& > div {
55
width: 100%;
66
}
7+
&.error {
8+
input{
9+
border-color: #fe665d;
10+
}
11+
12+
}
713
}
814

915
.datepicker-wrapper > div:nth-child(2) > div:nth-child(2) {

‎src/components/FormField/index.jsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@ import ReactSelect from "../../components/ReactSelect";
1313
import DateInput from "../../components/DateInput";
1414
import "./styles.module.scss";
1515

16-
const FormField = ({ field, isGroupField }) => {
16+
const FormField = ({ field }) => {
1717
return (
1818
<Field name={field.name}>
1919
{({ input, meta }) => (
20-
<div styleName={isGroupField ? "field-group-field" : ""}>
21-
{!field.readonly && (
20+
<div>
21+
{ !field.readonly && (
2222
<label
2323
styleName={
2424
(input.value != "undefined" &&
@@ -56,6 +56,7 @@ const FormField = ({ field, isGroupField }) => {
5656
onBlur={input.onBlur}
5757
onFocus={input.onFocus}
5858
className={meta.error && meta.touched ? "error" : ""}
59+
step={field.step}
5960
/>
6061
)}
6162
{field.type === FORM_FIELD_TYPE.TEXTAREA && (
@@ -75,6 +76,7 @@ const FormField = ({ field, isGroupField }) => {
7576
onChange={input.onChange}
7677
onBlur={input.onBlur}
7778
onFocus={input.onFocus}
79+
className={meta.error && meta.touched ? "error" : ""}
7880
/>
7981
)}
8082
{field.type === FORM_FIELD_TYPE.SELECT && (
@@ -87,7 +89,7 @@ const FormField = ({ field, isGroupField }) => {
8789
onFocus={input.onFocus}
8890
/>
8991
)}
90-
{field.isRequired && meta.error && meta.touched && (
92+
{(field.isRequired || field.customValidator) && meta.error && meta.touched && (
9193
<div styleName="field-error">{meta.error}</div>
9294
)}
9395
</div>

‎src/components/FormField/styles.module.scss

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
.job-field-label {
55
color: #aaaaaa;
6-
font-family: Roboto;
6+
@include font-roboto;
77
font-size: 12px;
88
line-height: 24px;
99
text-align: left;
@@ -37,19 +37,14 @@ input:read-only {
3737
margin-top: 24px;
3838
}
3939

40-
.field-group-field {
41-
width: 45%;
42-
}
43-
4440
.field-error {
45-
font-family: "Roboto", Helvetica, Arial, sans-serif;
46-
font-weight: 400;
41+
@include font-roboto;
4742
width: 100%;
48-
height: 40px;
49-
line-height: 40px;
50-
padding: 0 10px;
43+
min-height: 40px;
44+
line-height: 20px;
45+
padding: 9px 10px;
5146
margin: 10px 0 5px;
52-
font-size: 15px;
47+
font-size: 14px;
5348
color: #ff5b52;
5449
border: 1px solid #ffd5d1;
5550
cursor: auto;

‎src/components/SkillsList/styles.module.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
.skills-list {
1111
font-size: 14px;
1212
line-height: 22px;
13+
outline: none;
1314
}
1415

1516
.popover {

‎src/components/TCForm/index.jsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,14 +56,13 @@ const TCForm = ({
5656
<>
5757
{row.type === FORM_ROW_TYPE.GROUP && (
5858
<div styleName="field-group">
59-
{row.fields.map((field) => {
60-
return (
59+
{row.fields.map((field) => (
60+
<div styleName="field-group-field" key={field.name}>
6161
<FormField
6262
field={fields[field]}
63-
isGroupField={true}
6463
/>
65-
);
66-
})}
64+
</div>
65+
))}
6766
</div>
6867
)}
6968
{row.type === FORM_ROW_TYPE.SINGLE &&
@@ -106,6 +105,7 @@ TCForm.propTypes = {
106105
label: PT.string,
107106
type: PT.oneOf(Object.values(FORM_FIELD_TYPE)).isRequired,
108107
isRequired: PT.bool,
108+
customValidator: PT.func,
109109
validationMessage: PT.string,
110110
name: PT.string.isRequired,
111111
component: PT.element,
@@ -119,6 +119,7 @@ TCForm.propTypes = {
119119
maxLength: PT.number,
120120
styleName: PT.string,
121121
readonly: PT.string,
122+
step: PT.number,
122123
})
123124
).isRequired,
124125
onSubmit: PT.func,

‎src/components/TCForm/styles.module.scss

Lines changed: 62 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,48 +3,64 @@
33

44
.tc-form {
55
width: 100%;
6+
}
7+
8+
.job-form-fields-container {
9+
padding: 40px 0px 20px;
610

7-
.job-form-fields-container {
8-
padding: 40px 0px 20px;
9-
10-
.job-form-fields-wrapper {
11-
width: 100%;
12-
max-width: 640px;
13-
margin: 0 auto;
14-
text-align: left;
15-
}
16-
17-
input:not([type="checkbox"]),
18-
textarea {
19-
margin-bottom: 0px;
20-
}
21-
22-
.field-group {
23-
display: flex;
24-
flex-wrap: wrap;
25-
justify-content: space-between;
26-
}
11+
input:not([type="checkbox"]),
12+
textarea {
13+
margin-bottom: 0px;
2714
}
15+
}
2816

29-
.form-actions {
30-
border-top: 1px solid #e9e9e9;
31-
display: flex;
32-
justify-content: flex-start;
33-
flex-direction: row;
34-
align-items: center;
35-
width: 100%;
36-
padding: 16px 0 16px;
37-
:first-child {
38-
margin-left: auto;
39-
}
40-
button {
41-
height: 40px;
42-
border-radius: 20px;
43-
line-height: 40px;
44-
}
45-
a {
46-
margin-left: 15px;
47-
}
17+
.job-form-fields-wrapper{
18+
width: 100%;
19+
max-width: 640px;
20+
margin: 0 auto;
21+
text-align: left;
22+
}
23+
24+
.field-group {
25+
display: flex;
26+
flex-wrap: wrap;
27+
justify-content: space-between;
28+
}
29+
30+
.field-group-field {
31+
padding: 0 10px;
32+
width: 50%;
33+
}
34+
35+
.field-group-field:first-child {
36+
padding-left: 0;
37+
}
38+
39+
.field-group-field:last-child {
40+
padding-right: 0;
41+
}
42+
43+
.form-actions {
44+
border-top: 1px solid #e9e9e9;
45+
display: flex;
46+
justify-content: flex-start;
47+
flex-direction: row;
48+
align-items: center;
49+
width: 100%;
50+
padding: 16px 0 16px;
51+
52+
:first-child {
53+
margin-left: auto;
54+
}
55+
56+
button {
57+
height: 40px;
58+
border-radius: 20px;
59+
line-height: 40px;
60+
}
61+
62+
a {
63+
margin-left: 15px;
4864
}
4965
}
5066

@@ -57,3 +73,10 @@
5773
.datepicker-wrapper > div:nth-child(2) > div:nth-child(2) {
5874
z-index: 100;
5975
}
76+
77+
@media (max-width: 650px) {
78+
.field-group-field {
79+
padding: 0;
80+
width: 100%;
81+
}
82+
}

‎src/components/TCForm/utils.js

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,27 @@
44
import _ from "lodash";
55
import { FORM_FIELD_TYPE } from "../../constants";
66

7+
/**
8+
* Returns the option from list of option by value
9+
*
10+
* @param {any} value value of option
11+
* @param {[{ label: string, value: any }]} selectOptions list of option
12+
*
13+
* @returns {{ label: string, value: any }} select option
14+
*/
15+
const getSelectOptionByValue = (value, selectOptions) => {
16+
const option = _.find(selectOptions, { value });
17+
18+
if (!option) {
19+
return {
20+
label: `Unsuppored value: ${value}`,
21+
value,
22+
};
23+
}
24+
25+
return option;
26+
};
27+
728
/**
829
* Extract value from field by type
930
* @param {any} value value
@@ -18,12 +39,16 @@ const extractValue = (value, field) => {
1839
switch (field.type) {
1940
case FORM_FIELD_TYPE.SELECT: {
2041
return field.isMulti
21-
? value.map((x) => field.selectOptions.find((y) => y.value === x))
22-
: field.selectOptions.find((y) => y.value === value);
42+
? value.map((valueItem) =>
43+
getSelectOptionByValue(valueItem, field.selectOptions)
44+
)
45+
: getSelectOptionByValue(value, field.selectOptions);
2346
}
47+
2448
case FORM_FIELD_TYPE.DATE: {
2549
return new Date(value);
2650
}
51+
2752
default: {
2853
return value;
2954
}
@@ -71,10 +96,15 @@ export const getValidator = (fields) => {
7196
return (values) => {
7297
const errors = {};
7398
fields
74-
.filter((f) => f.isRequired)
99+
.filter((f) => f.isRequired || f.customValidator)
75100
.forEach((f) => {
76-
if (!values[f.name]) {
101+
if (f.isRequired && !values[f.name]) {
77102
errors[f.name] = f.validationMessage;
103+
} else if (f.customValidator) {
104+
const error = f.customValidator(f, fields, values);
105+
if (error) {
106+
errors[f.name] = error;
107+
}
78108
}
79109
});
80110
return errors;

‎src/components/TextInput/index.jsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,13 @@
55
*/
66
import React from "react";
77
import PT from "prop-types";
8+
import cn from "classnames";
89
import "./styles.module.scss";
910

1011
function TextInput(props) {
1112
return (
1213
<input
13-
styleName={`TextInput ${props.className} ${
14-
props.readonly ? "readonly" : ""
15-
}`}
14+
styleName={cn("TextInput", props.className, {"readonly": props.readonly})}
1615
maxLength={props.maxLength}
1716
min={props.minValue}
1817
onChange={(event) => {
@@ -38,6 +37,7 @@ function TextInput(props) {
3837
readOnly={props.readonly ?? false}
3938
onBlur={props.onBlur}
4039
onFocus={props.onFocus}
40+
step={props.step}
4141
/>
4242
);
4343
}
@@ -47,6 +47,7 @@ TextInput.defaultProps = {
4747
maxLength: Number.MAX_VALUE,
4848
placeholder: "",
4949
minValue: 0,
50+
step: null,
5051
};
5152

5253
TextInput.propTypes = {

‎src/routes/JobDetails/index.jsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,19 @@ const JobDetails = ({ teamId, jobId }) => {
3131
if (!!skills && !!job) {
3232
setSkillSet(
3333
job.skills
34-
?.map((val) => {
35-
const skill = skills.find((sk) => sk.id === val);
34+
// map skill ids to names
35+
?.map((skillId) => {
36+
const skill = _.find(skills, { id: skillId });
37+
38+
if (!skill) {
39+
console.warn(`Couldn't find name for skill id "${skillId}" of the job "${job.id}".`)
40+
return null
41+
}
42+
3643
return skill.name;
3744
})
45+
// remove `null` values for skills when we couldn't find the name
46+
.filter((skillName) => !!skillName)
3847
.join(", ")
3948
);
4049
}
@@ -64,13 +73,13 @@ const JobDetails = ({ teamId, jobId }) => {
6473
<DataItem title="Start Date" icon={<IconDescription />}>
6574
{formatDate(job.startDate)}
6675
</DataItem>
67-
<DataItem title="Duration" icon={<IconDescription />}>
76+
<DataItem title="Duration (weekly)" icon={<IconDescription />}>
6877
{job.duration || "TBD"}
6978
</DataItem>
7079
<DataItem title="Resource Type" icon={<IconDescription />}>
7180
{job.resourceType}
7281
</DataItem>
73-
<DataItem title="Rate Type" icon={<IconDescription />}>
82+
<DataItem title="Resource Rate Frequency" icon={<IconDescription />}>
7483
{job.rateType}
7584
</DataItem>
7685
<DataItem title="Workload" icon={<IconDescription />}>

‎src/routes/JobForm/index.jsx

Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -59,22 +59,22 @@ const JobForm = ({ teamId, jobId }) => {
5959
}
6060
};
6161

62-
const getRequestData = (values) => {
63-
return {
64-
projectId: values.projectId,
65-
externalId: values.externalId,
66-
description: values.description,
67-
title: values.title,
68-
startDate: values.startDate,
69-
duration: values.duration,
70-
numPositions: values.numPositions,
71-
resourceType: values.resourceType,
72-
rateType: values.rateType,
73-
workload: values.workload,
74-
skills: values.skills,
75-
status: values.status,
76-
};
77-
};
62+
// as we are using `PUT` method (not `PATCH`) we have send ALL the fields
63+
// fields which we don't send would become `null` otherwise
64+
const getRequestData = (values) => _.pick(values, [
65+
'projectId',
66+
'externalId',
67+
'description',
68+
'title',
69+
'startDate',
70+
'duration',
71+
'numPositions',
72+
'resourceType',
73+
'rateType',
74+
'workload',
75+
'skills',
76+
'status',
77+
]);
7878

7979
useEffect(() => {
8080
if (skills && job && !options) {
@@ -104,11 +104,7 @@ const JobForm = ({ teamId, jobId }) => {
104104
/>
105105
<div styleName="job-modification-details">
106106
<TCForm
107-
configuration={
108-
isEdit
109-
? getEditJobConfig(options, onSubmit)
110-
: getCreateJobConfig(options, onSubmit)
111-
}
107+
configuration={getEditJobConfig(options, onSubmit)}
112108
initialValue={job}
113109
submitButton={{ text: isEdit ? "Save" : "Create" }}
114110
backButton={{

‎src/routes/JobForm/utils.js

Lines changed: 5 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,6 @@ import {
99
FORM_FIELD_TYPE,
1010
} from "../../constants";
1111

12-
const CREATE_JOB_ROWS = [
13-
{ type: FORM_ROW_TYPE.SINGLE, fields: ["title"] },
14-
{ type: FORM_ROW_TYPE.SINGLE, fields: ["description"] },
15-
{ type: FORM_ROW_TYPE.SINGLE, fields: ["numPositions"] },
16-
{ type: FORM_ROW_TYPE.SINGLE, fields: ["skills"] },
17-
{ type: FORM_ROW_TYPE.GROUP, fields: ["startDate", "duration"] },
18-
{ type: FORM_ROW_TYPE.SINGLE, fields: ["resourceType"] },
19-
{ type: FORM_ROW_TYPE.SINGLE, fields: ["rateType"] },
20-
{ type: FORM_ROW_TYPE.SINGLE, fields: ["workload"] },
21-
{ type: FORM_ROW_TYPE.SINGLE, fields: ["status"] },
22-
];
23-
2412
const EDIT_JOB_ROWS = [
2513
{ type: FORM_ROW_TYPE.SINGLE, fields: ["title"] },
2614
{ type: FORM_ROW_TYPE.SINGLE, fields: ["description"] },
@@ -64,6 +52,7 @@ export const getEditJobConfig = (skillOptions, onSubmit) => {
6452
name: "numPositions",
6553
minValue: 1,
6654
placeholder: "Number Of Opening",
55+
step: 1,
6756
},
6857
{
6958
label: "Job Skills",
@@ -79,11 +68,12 @@ export const getEditJobConfig = (skillOptions, onSubmit) => {
7968
placeholder: "Start Date",
8069
},
8170
{
82-
label: "Duration",
71+
label: "Duration (weekly)",
8372
type: FORM_FIELD_TYPE.NUMBER,
8473
name: "duration",
8574
minValue: 1,
8675
placeholder: "Duration",
76+
step: 1,
8777
},
8878
{
8979
label: "Resource Type",
@@ -93,7 +83,7 @@ export const getEditJobConfig = (skillOptions, onSubmit) => {
9383
placeholder: "Resource Type",
9484
},
9585
{
96-
label: "Rate Type",
86+
label: "Resource Rate Frequency",
9787
type: FORM_FIELD_TYPE.SELECT,
9888
name: "rateType",
9989
selectOptions: RATE_TYPE_OPTIONS,
@@ -116,89 +106,4 @@ export const getEditJobConfig = (skillOptions, onSubmit) => {
116106
onSubmit: onSubmit,
117107
rows: EDIT_JOB_ROWS,
118108
};
119-
};
120-
121-
/**
122-
* return create job configuration
123-
* @param {any} skillOptions skill options
124-
* @param {func} onSubmit submit callback
125-
*/
126-
export const getCreateJobConfig = (skillOptions, onSubmit) => {
127-
return {
128-
fields: [
129-
{
130-
label: "Job Name",
131-
type: FORM_FIELD_TYPE.TEXT,
132-
isRequired: true,
133-
validationMessage: "Please, enter Job Name",
134-
name: "title",
135-
maxLength: 128,
136-
placeholder: "Job Name",
137-
},
138-
{
139-
label: "Job Description",
140-
type: FORM_FIELD_TYPE.TEXTAREA,
141-
name: "description",
142-
placeholder: "Job Description",
143-
},
144-
{
145-
label: "Number Of Opening",
146-
type: FORM_FIELD_TYPE.NUMBER,
147-
isRequired: true,
148-
validationMessage: "Please, enter Job Name",
149-
name: "numPositions",
150-
minValue: 1,
151-
placeholder: "Number Of Opening",
152-
},
153-
{
154-
label: "Job Skills",
155-
type: FORM_FIELD_TYPE.SELECT,
156-
isMulti: true,
157-
name: "skills",
158-
selectOptions: skillOptions,
159-
},
160-
{
161-
label: "Start Date",
162-
type: FORM_FIELD_TYPE.DATE,
163-
name: "startDate",
164-
placeholder: "Start Date",
165-
},
166-
{
167-
label: "Duration",
168-
type: FORM_FIELD_TYPE.NUMBER,
169-
name: "duration",
170-
minValue: 1,
171-
placeholder: "Duration",
172-
},
173-
{
174-
label: "Resource Type",
175-
type: FORM_FIELD_TYPE.TEXT,
176-
name: "resourceType",
177-
maxLength: 255,
178-
placeholder: "Resource Type",
179-
},
180-
{
181-
label: "Rate Type",
182-
type: FORM_FIELD_TYPE.SELECT,
183-
name: "rateType",
184-
selectOptions: RATE_TYPE_OPTIONS,
185-
},
186-
{
187-
label: "Workload",
188-
type: FORM_FIELD_TYPE.SELECT,
189-
name: "workload",
190-
selectOptions: WORKLOAD_OPTIONS,
191-
},
192-
{
193-
label: "Status",
194-
type: FORM_FIELD_TYPE.SELECT,
195-
isRequired: true,
196-
validationMessage: "Please, select Status",
197-
name: "status",
198-
selectOptions: STATUS_OPTIONS,
199-
},
200-
],
201-
onSubmit: onSubmit,
202-
rows: CREATE_JOB_ROWS,
203-
};
204-
};
109+
};

‎src/routes/PositionDetails/index.jsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
*
44
* Page for the list of candidates for position.
55
*/
6-
import React, { useCallback, useState } from "react";
6+
import React, { useCallback, useEffect, useState } from "react";
77
import PT from "prop-types";
88
import Page from "components/Page";
99
import LoadingIndicator from "components/LoadingIndicator";
@@ -16,7 +16,8 @@ import { useTeamPositionsState } from "./hooks/useTeamPositionsState";
1616
import "./styles.module.scss";
1717

1818
const PositionDetails = ({ teamId, positionId }) => {
19-
const [candidateStatus, setCandidateStatus] = useState(CANDIDATE_STATUS.OPEN);
19+
// be dafault show "Interested" tab
20+
const [candidateStatus, setCandidateStatus] = useState(CANDIDATE_STATUS.SHORTLIST);
2021
const {
2122
state: { position, error },
2223
updateCandidate,
@@ -29,6 +30,13 @@ const PositionDetails = ({ teamId, positionId }) => {
2930
[setCandidateStatus]
3031
);
3132

33+
// if there are some candidates to review, then show "To Review" tab by default
34+
useEffect(() => {
35+
if (position && _.filter(position.candidates, { status: CANDIDATE_STATUS.OPEN }).length > 0) {
36+
setCandidateStatus(CANDIDATE_STATUS.OPEN)
37+
}
38+
}, [position])
39+
3240
return (
3341
<Page title="Job Details">
3442
{!position ? (

‎src/routes/ResourceBookingDetails/ResourceDetails/index.jsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import IconComputer from "../../../assets/images/icon-computer.svg";
1212
import IconBag from "../../../assets/images/icon-bag.svg";
1313
import "./styles.module.scss";
1414

15-
const ResourceDetails = ({ resource }) => {
15+
const ResourceDetails = ({ resource, jobTitle }) => {
1616
return (
1717
<div styleName="resource-details">
1818
<div styleName="table">
@@ -21,7 +21,7 @@ const ResourceDetails = ({ resource }) => {
2121
<DataItem
2222
icon={<IconComputer />}
2323
title="Job Title"
24-
children={resource.title}
24+
children={jobTitle}
2525
/>
2626
</div>
2727
<div styleName="table-cell">
@@ -68,8 +68,8 @@ const ResourceDetails = ({ resource }) => {
6868
};
6969

7070
ResourceDetails.propTypes = {
71+
jobTitle: PT.string,
7172
resource: PT.shape({
72-
title: PT.string,
7373
startDate: PT.string,
7474
endDate: PT.string,
7575
id: PT.string,

‎src/routes/ResourceBookingDetails/ResourceSummary/index.jsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@ import PT from "prop-types";
88
import User from "components/User";
99
import "./styles.module.scss";
1010

11-
const ResourceSummary = ({ candidate }) => {
11+
const ResourceSummary = ({ member }) => {
1212
return (
1313
<div styleName="resource-summary">
1414
<div styleName="resource-summary-wrapper">
1515
<User
1616
user={{
17-
...candidate,
18-
photoUrl: candidate.photo_url,
17+
...member,
18+
photoUrl: member.photo_url,
1919
}}
2020
/>
2121
</div>
@@ -24,7 +24,7 @@ const ResourceSummary = ({ candidate }) => {
2424
};
2525

2626
ResourceSummary.propTypes = {
27-
candidate: PT.shape({
27+
member: PT.shape({
2828
firstName: PT.string,
2929
handle: PT.string,
3030
id: PT.string,

‎src/routes/ResourceBookingDetails/index.jsx

Lines changed: 41 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@
44
* Page for resource booking details.
55
* It gets `teamId` and `resourceBookingId` from the router.
66
*/
7-
import React, { useState, useEffect } from "react";
7+
import React, { useMemo, useState, useEffect } from "react";
88
import PT from "prop-types";
9+
import _ from "lodash";
910
import Page from "../../components/Page";
1011
import PageHeader from "../../components/PageHeader";
1112
import { useData } from "hooks/useData";
1213
import { getReourceBookingById } from "services/resourceBookings";
13-
import { getPositionDetails } from "services/teams";
14+
import { getTeamById } from "services/teams";
1415
import LoadingIndicator from "../../components/LoadingIndicator";
1516
import withAuthentication from "../../hoc/withAuthentication";
1617
import Button from "../../components/Button";
@@ -19,53 +20,54 @@ import ResourceDetails from "./ResourceDetails";
1920
import "./styles.module.scss";
2021

2122
const ResourceBookingDetails = ({ teamId, resourceBookingId }) => {
22-
const [jobId, setJobId] = useState(null);
23-
const [title, setTitle] = useState(null);
24-
const [candidate, setCandidate] = useState(null);
25-
const [rb, loadingError] = useData(getReourceBookingById, resourceBookingId);
23+
const [resource, loadingError] = useData(getReourceBookingById, resourceBookingId);
24+
const [team, loadingTeamError] = useData(getTeamById, teamId);
25+
const [jobTitle, setJobTitle] = useState("")
26+
const [member, setMember] = useState("")
2627

2728
useEffect(() => {
28-
if (!!rb) {
29-
setJobId(rb.jobId);
30-
}
31-
}, [rb]);
29+
if (team) {
30+
const resourceWithMemberDetails = _.find(
31+
team.resources,
32+
{ id: resourceBookingId }
33+
);
3234

33-
useEffect(() => {
34-
if (jobId) {
35-
getPositionDetails(teamId, jobId).then((response) => {
36-
const data = response.data.candidates?.find(
37-
(x) => x.userId === rb.userId
38-
);
39-
setCandidate(data);
40-
setTitle(response.data.title);
41-
});
35+
// resource inside Team object has all the member details we need
36+
setMember(resourceWithMemberDetails);
37+
38+
if (resourceWithMemberDetails.jobId) {
39+
const job = _.find(team.jobs, { id: resourceWithMemberDetails.jobId });
40+
setJobTitle(_.get(job, "title", `<Not Found> ${resourceWithMemberDetails.jobId}`));
41+
} else {
42+
setJobTitle("<Not Assigned>")
43+
}
4244
}
43-
}, [jobId]);
45+
}, [team, resourceBookingId]);
4446

4547
return (
4648
<Page title="Member Details">
47-
{!candidate ? (
48-
<LoadingIndicator error={loadingError} />
49+
{!(member && resource) ? (
50+
<LoadingIndicator error={loadingError || loadingTeamError} />
4951
) : (
50-
<>
51-
<PageHeader
52-
title="Member Details"
53-
backTo={`/taas/myteams/${teamId}`}
54-
/>
55-
<div styleName="content-wrapper">
56-
<ResourceSummary candidate={candidate} />
57-
<ResourceDetails resource={{ ...rb, title: title }} />
58-
<div styleName="actions">
59-
<Button
60-
size="medium"
61-
routeTo={`/taas/myteams/${teamId}/rb/${rb.id}/edit`}
62-
>
63-
Edit Member Details
52+
<>
53+
<PageHeader
54+
title="Member Details"
55+
backTo={`/taas/myteams/${teamId}`}
56+
/>
57+
<div styleName="content-wrapper">
58+
<ResourceSummary member={member} />
59+
<ResourceDetails resource={resource} jobTitle={jobTitle} />
60+
<div styleName="actions">
61+
<Button
62+
size="medium"
63+
routeTo={`/taas/myteams/${teamId}/rb/${resource.id}/edit`}
64+
>
65+
Edit Member Details
6466
</Button>
67+
</div>
6568
</div>
66-
</div>
67-
</>
68-
)}
69+
</>
70+
)}
6971
</Page>
7072
);
7173
};

‎src/routes/ResourceBookingForm/index.jsx

Lines changed: 56 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
* Page for edit resource booking details.
55
* It gets `teamId` and `resourceBookingId` from the router.
66
*/
7-
import React, { useState, useEffect } from "react";
7+
import React, { useState, useMemo } from "react";
88
import PT from "prop-types";
9+
import _ from "lodash";
910
import { toastr } from "react-redux-toastr";
1011
import Page from "../../components/Page";
1112
import PageHeader from "../../components/PageHeader";
@@ -14,7 +15,7 @@ import {
1415
getReourceBookingById,
1516
updateReourceBooking,
1617
} from "services/resourceBookings";
17-
import { getPositionDetails } from "services/teams";
18+
import { getTeamById } from "services/teams";
1819
import LoadingIndicator from "../../components/LoadingIndicator";
1920
import withAuthentication from "../../hoc/withAuthentication";
2021
import TCForm from "../../components/TCForm";
@@ -23,31 +24,33 @@ import "./styles.module.scss";
2324

2425
const ResourceBookingDetails = ({ teamId, resourceBookingId }) => {
2526
const [submitting, setSubmitting] = useState(false);
26-
const [jobId, setJobId] = useState(null);
27-
const [formData, setFormData] = useState(null);
27+
const [team, loadingTeamError] = useData(getTeamById, teamId);
2828
const [rb, loadingError] = useData(getReourceBookingById, resourceBookingId);
2929

30-
useEffect(() => {
31-
if (!!rb) {
32-
setJobId(rb.jobId);
33-
}
34-
}, [rb]);
30+
const formData = useMemo(() => {
31+
if (team && rb) {
32+
const resource = _.find(
33+
team.resources,
34+
{ id: resourceBookingId }
35+
);
36+
37+
const data = {
38+
...rb,
39+
// resource inside Team object has all the member details we need
40+
// so we can get user handle from there
41+
handle: resource.handle,
42+
};
3543

36-
useEffect(() => {
37-
if (jobId) {
38-
getPositionDetails(teamId, jobId).then((response) => {
39-
const candidate = response.data.candidates?.find(
40-
(x) => x.userId === rb.userId
41-
);
42-
const data = {
43-
title: response.data.title,
44-
...candidate,
45-
...rb,
46-
};
47-
setFormData(data);
48-
});
44+
if (resource.jobId) {
45+
const job = _.find(team.jobs, { id: resource.jobId });
46+
data.jobTitle = _.get(job, "title", `<Not Found> ${resource.jobId}`);
47+
} else {
48+
data.jobTitle = "<Not Assigned>";
49+
}
50+
51+
return data;
4952
}
50-
}, [jobId, rb, teamId]);
53+
}, [rb, team, resourceBookingId]);
5154

5255
const onSubmit = async (values) => {
5356
const data = getRequestData(values);
@@ -64,41 +67,42 @@ const ResourceBookingDetails = ({ teamId, resourceBookingId }) => {
6467
);
6568
};
6669

67-
const getRequestData = (values) => {
68-
return {
69-
projectId: values.projectId,
70-
startDate: values.startDate,
71-
endDate: values.endDate,
72-
memberRate: values.memberRate,
73-
customerRate: values.customerRate,
74-
status: values.status,
75-
userId: values.userId,
76-
rateType: values.rateType,
77-
};
78-
};
70+
// as we are using `PUT` method (not `PATCH`) we have send ALL the fields
71+
// fields which we don't send would become `null` otherwise
72+
const getRequestData = (values) => _.pick(values, [
73+
'projectId',
74+
'userId',
75+
'jobId',
76+
'status',
77+
'startDate',
78+
'endDate',
79+
'memberRate',
80+
'customerRate',
81+
'rateType',
82+
]);
7983

8084
return (
8185
<Page title="Edit Member Details">
8286
{!formData ? (
83-
<LoadingIndicator error={loadingError} />
87+
<LoadingIndicator error={loadingError || loadingTeamError} />
8488
) : (
85-
<>
86-
<PageHeader
87-
title="Edit Member Details"
88-
backTo={`/taas/myteams/${teamId}/rb/${rb.id}`}
89-
/>
90-
<div styleName="rb-modification-details">
91-
<TCForm
92-
configuration={getEditResourceBookingConfig(onSubmit)}
93-
initialValue={formData}
94-
submitButton={{ text: "Save" }}
95-
backButton={{ text: "Cancel", backTo: `/taas/myteams/${teamId}` }}
96-
submitting={submitting}
97-
setSubmitting={setSubmitting}
89+
<>
90+
<PageHeader
91+
title="Edit Member Details"
92+
backTo={`/taas/myteams/${teamId}/rb/${resourceBookingId}`}
9893
/>
99-
</div>
100-
</>
101-
)}
94+
<div styleName="rb-modification-details">
95+
<TCForm
96+
configuration={getEditResourceBookingConfig(onSubmit)}
97+
initialValue={formData}
98+
submitButton={{ text: "Save" }}
99+
backButton={{ text: "Cancel", backTo: `/taas/myteams/${teamId}` }}
100+
submitting={submitting}
101+
setSubmitting={setSubmitting}
102+
/>
103+
</div>
104+
</>
105+
)}
102106
</Page>
103107
);
104108
};

‎src/routes/ResourceBookingForm/utils.js

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,17 @@
33
*
44
* utility class
55
*/
6+
import moment from "moment";
7+
import _ from "lodash";
68
import {
79
STATUS_OPTIONS,
810
FORM_ROW_TYPE,
911
FORM_FIELD_TYPE,
1012
} from "../../constants";
1113

12-
const EDIT_ResourceBooking_ROWS = [
13-
{ type: FORM_ROW_TYPE.SINGLE, fields: ["title"] },
14+
const EDIT_RESOURCE_BOOKING_ROWS = [
1415
{ type: FORM_ROW_TYPE.SINGLE, fields: ["handle"] },
16+
{ type: FORM_ROW_TYPE.SINGLE, fields: ["jobTitle"] },
1517
{ type: FORM_ROW_TYPE.GROUP, fields: ["startDate", "endDate"] },
1618
{ type: FORM_ROW_TYPE.GROUP, fields: ["customerRate", "memberRate"] },
1719
{ type: FORM_ROW_TYPE.SINGLE, fields: ["status"] },
@@ -24,33 +26,65 @@ const EDIT_ResourceBooking_ROWS = [
2426
export const getEditResourceBookingConfig = (onSubmit) => {
2527
return {
2628
fields: [
27-
{ readonly: true, type: FORM_FIELD_TYPE.TEXT, name: "title" },
2829
{ readonly: true, type: FORM_FIELD_TYPE.TEXT, name: "handle" },
30+
{ readonly: true, type: FORM_FIELD_TYPE.TEXT, name: "jobTitle" },
2931
{
3032
label: "Client Rate",
3133
type: FORM_FIELD_TYPE.NUMBER,
3234
name: "customerRate",
3335
minValue: 0,
3436
placeholder: "Client Rate",
37+
step: 0.01,
3538
},
3639
{
3740
label: "Member Rate",
3841
type: FORM_FIELD_TYPE.NUMBER,
3942
name: "memberRate",
4043
minValue: 0,
4144
placeholder: "Member Rate",
45+
step: 0.01,
4246
},
4347
{
4448
label: "Start Date",
4549
type: FORM_FIELD_TYPE.DATE,
4650
name: "startDate",
4751
placeholder: "Start Date",
52+
customValidator: (field, fields, values) => {
53+
const endDateField = _.find(fields, { name: "endDate" });
54+
const startDate = values[field.name];
55+
const endDate = values["endDate"];
56+
if (
57+
startDate &&
58+
endDate &&
59+
moment(endDate)
60+
.startOf("day")
61+
.isBefore(moment(startDate).startOf("day"))
62+
) {
63+
return "Start Date should not be after End Date";
64+
}
65+
return null;
66+
},
4867
},
4968
{
5069
label: "End Date",
5170
type: FORM_FIELD_TYPE.DATE,
5271
name: "endDate",
5372
placeholder: "End Date",
73+
customValidator: (field, fields, values) => {
74+
const startDateField = _.find(fields, { name: "startDate" });
75+
const endDate = values[field.name];
76+
const startDate = values["startDate"];
77+
if (
78+
startDate &&
79+
endDate &&
80+
moment(endDate)
81+
.startOf("day")
82+
.isBefore(moment(startDate).startOf("day"))
83+
) {
84+
return "End Date should not be before Start Date";
85+
}
86+
return null;
87+
},
5488
},
5589
{
5690
label: "Status",
@@ -62,6 +96,6 @@ export const getEditResourceBookingConfig = (onSubmit) => {
6296
},
6397
],
6498
onSubmit: onSubmit,
65-
rows: EDIT_ResourceBooking_ROWS,
99+
rows: EDIT_RESOURCE_BOOKING_ROWS,
66100
};
67101
};

0 commit comments

Comments
 (0)
This repository has been archived.