-
- {user.handle}
-
+ {/* if "handleLinkTo" is provided, use it as internal link, otherwise as external profile link */}
+ {handleLinkTo ? (
+
+
{user.handle}
+
+ ) : (
+
+ {user.handle}
+
+ )}
+
{!hideFullName && (
{formatFullName(user.firstName, user.lastName)}
)}
@@ -37,6 +46,7 @@ User.propTypes = {
handle: PT.string,
}),
hideFullName: PT.bool,
+ handleLinkTo: PT.string,
};
export default User;
diff --git a/src/constants/index.js b/src/constants/index.js
index e65c089b..c945f2d1 100644
--- a/src/constants/index.js
+++ b/src/constants/index.js
@@ -148,3 +148,64 @@ export const ACTION_TYPE = {
UPDATE_CANDIDATE_SUCCESS: "UPDATE_CANDIDATE_SUCCESS",
UPDATE_CANDIDATE_ERROR: "UPDATE_CANDIDATE_ERROR",
};
+
+/**
+ * All fonr field types
+ */
+export const FORM_FIELD_TYPE = {
+ TEXT: "text",
+ TEXTAREA: "textarea",
+ NUMBER: "number",
+ SELECT: "select",
+ MULTISELECT: "multiselect",
+ DATE: "date",
+ DATERANGE: "date_range",
+};
+
+/**
+ * All form action types
+ */
+export const FORM_ACTION_TYPE = {
+ SUBMIT: "submit",
+ BACK: "back",
+ CUSTOM: "custom",
+};
+
+/**
+ * All form row types
+ */
+export const FORM_ROW_TYPE = {
+ SINGLE: "single",
+ GROUP: "group",
+};
+
+/**
+ * Rate type options
+ */
+export const RATE_TYPE_OPTIONS = [
+ { value: null, label: "" },
+ { value: "hourly", label: "hourly" },
+ { value: "daily", label: "daily" },
+ { value: "weekly", label: "weekly" },
+ { value: "monthly", label: "monthly" },
+];
+
+/**
+ * workload options
+ */
+export const WORKLOAD_OPTIONS = [
+ { value: null, label: "" },
+ { value: "full-time", label: "full-time" },
+ { value: "fractional", label: "fractional" },
+];
+
+/**
+ * status options
+ */
+export const STATUS_OPTIONS = [
+ { value: "sourcing", label: "sourcing" },
+ { value: "in-review", label: "in-review" },
+ { value: "assigned", label: "assigned" },
+ { value: "closed", label: "closed" },
+ { value: "cancelled", label: "cancelled" },
+];
diff --git a/src/root.component.jsx b/src/root.component.jsx
index 834234fb..16526ba7 100644
--- a/src/root.component.jsx
+++ b/src/root.component.jsx
@@ -4,6 +4,10 @@ import { Router, Redirect } from "@reach/router";
import MyTeamsList from "./routes/MyTeamsList";
import MyTeamsDetails from "./routes/MyTeamsDetails";
import PositionDetails from "./routes/PositionDetails";
+import ResourceBookingDetails from "./routes/ResourceBookingDetails";
+import ResourceBookingForm from "./routes/ResourceBookingForm";
+import JobDetails from "./routes/JobDetails";
+import JobForm from "./routes/JobForm";
import ReduxToastr from "react-redux-toastr";
import store from "./store";
import "./styles/main.vendor.scss";
@@ -17,7 +21,12 @@ export default function Root() {
-
+
+
+
+
+
+
{/* Global config for Toastr popups */}
diff --git a/src/routes/JobDetails/index.jsx b/src/routes/JobDetails/index.jsx
new file mode 100644
index 00000000..293d9269
--- /dev/null
+++ b/src/routes/JobDetails/index.jsx
@@ -0,0 +1,105 @@
+/**
+ * JobDetails
+ *
+ * Page for job details.
+ * It gets `teamId` and `jobId` from the router.
+ */
+import React, { useEffect, useState } from "react";
+import PT from "prop-types";
+import Page from "../../components/Page";
+import PageHeader from "../../components/PageHeader";
+import { useData } from "hooks/useData";
+import { getJobById } from "services/jobs";
+import { getSkills } from "services/skills";
+import LoadingIndicator from "../../components/LoadingIndicator";
+import withAuthentication from "../../hoc/withAuthentication";
+import DataItem from "../../components/DataItem";
+import IconSkill from "../../assets/images/icon-skill.svg";
+import IconComputer from "../../assets/images/icon-computer.svg";
+import IconDescription from "../../assets/images/icon-description.svg";
+import IconOpenings from "../../assets/images/icon-openings.svg";
+import Button from "../../components/Button";
+import { formatDate } from "utils/format";
+import "./styles.module.scss";
+
+const JobDetails = ({ teamId, jobId }) => {
+ const [job, loadingError] = useData(getJobById, jobId);
+ const [skills] = useData(getSkills);
+ const [skillSet, setSkillSet] = useState(null);
+
+ useEffect(() => {
+ if (!!skills && !!job) {
+ setSkillSet(
+ job.skills
+ ?.map((val) => {
+ const skill = skills.find((sk) => sk.id === val);
+ return skill.name;
+ })
+ .join(", ")
+ );
+ }
+ }, [job, skills]);
+
+ return (
+
+ {!job || !skills ? (
+
+ ) : (
+ <>
+
+
+
+ }>
+ {job.title}
+
+ }>
+ {job.description}
+
+ }>
+ {job.numPositions}
+
+ }>
+ {skillSet}
+
+ }>
+ {formatDate(job.startDate)}
+
+ }>
+ {formatDate(job.endDate)}
+
+ }>
+ {job.resourceType}
+
+ }>
+ {job.rateType}
+
+ }>
+ {job.workload}
+
+ }>
+ {job.status}
+
+
+
+
+
+
+ >
+ )}
+
+ );
+};
+
+JobDetails.propTypes = {
+ teamId: PT.string,
+ jobId: PT.string,
+};
+
+export default withAuthentication(JobDetails);
diff --git a/src/routes/JobDetails/styles.module.scss b/src/routes/JobDetails/styles.module.scss
new file mode 100644
index 00000000..03e17f40
--- /dev/null
+++ b/src/routes/JobDetails/styles.module.scss
@@ -0,0 +1,36 @@
+@import "styles/include";
+
+.job-summary {
+ @include rounded-card;
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+ padding: 0 24px 20px;
+}
+
+.data-items {
+ padding-right: 15%;
+ > * {
+ margin-top: 20px;
+ margin-right: 48px;
+ }
+}
+
+.actions {
+ margin-top: 20px;
+ border-top: 1px solid #e9e9e9;
+ display: flex;
+ justify-content: flex-start;
+ flex-direction: row;
+ align-items: center;
+ width: 100%;
+ padding: 16px 0px 16px 0;
+ :first-child {
+ margin-left: auto;
+ }
+ button {
+ height: 40px;
+ border-radius: 20px;
+ min-width: 78px;
+ }
+}
diff --git a/src/routes/JobForm/index.jsx b/src/routes/JobForm/index.jsx
new file mode 100644
index 00000000..2ca9ea19
--- /dev/null
+++ b/src/routes/JobForm/index.jsx
@@ -0,0 +1,135 @@
+/* eslint-disable react-hooks/rules-of-hooks */
+/**
+ * JobForm
+ *
+ * Page for job create or edit.
+ * It gets `teamId` and `jobId` from the router.
+ */
+import React, { useState, useEffect } from "react";
+import PT from "prop-types";
+import { toastr } from "react-redux-toastr";
+import Page from "components/Page";
+import PageHeader from "components/PageHeader";
+import { useData } from "hooks/useData";
+import { getJobById, createJob, updateJob, getEmptyJob } from "services/jobs";
+import { getSkills } from "services/skills";
+import LoadingIndicator from "components/LoadingIndicator";
+import withAuthentication from "../../hoc/withAuthentication";
+import TCForm from "../../components/TCForm";
+import { getEditJobConfig, getCreateJobConfig } from "./utils";
+
+import "./styles.module.scss";
+
+const JobForm = ({ teamId, jobId }) => {
+ const isEdit = !!jobId;
+ const [options, setOptions] = useState(null);
+ const [submitting, setSubmitting] = useState(false);
+ const [skills, loadingError] = useData(getSkills);
+ const [job] = isEdit
+ ? useData(getJobById, jobId)
+ : useData(getEmptyJob, teamId);
+ const title = isEdit ? "Edit Job Details" : "Create Job";
+
+ const onSubmit = async (values) => {
+ const data = getRequestData(values);
+ if (isEdit) {
+ await updateJob(data, jobId).then(
+ () => {
+ toastr.success("Job updated successfully.");
+ setSubmitting(false);
+ window.history.pushState({}, null, `/taas/myteams/${teamId}`);
+ },
+ (err) => {
+ toastr.error(err.message);
+ setSubmitting(false);
+ }
+ );
+ } else {
+ await createJob(data).then(
+ () => {
+ toastr.success("Job created successfully.");
+ setSubmitting(false);
+ window.history.pushState({}, null, `/taas/myteams/${teamId}`);
+ },
+ (err) => {
+ toastr.error(err.message);
+ setSubmitting(false);
+ }
+ );
+ }
+ };
+
+ const getRequestData = (values) => {
+ return {
+ projectId: values.projectId,
+ externalId: values.externalId,
+ description: values.description,
+ title: values.title,
+ startDate: values.startDate,
+ endDate: values.endDate,
+ numPositions: values.numPositions,
+ resourceType: values.resourceType,
+ rateType: values.rateType,
+ workload: values.workload,
+ skills: values.skills,
+ status: values.status,
+ };
+ };
+
+ useEffect(() => {
+ if (skills && job && !options) {
+ const skillOptions = skills.map((item) => {
+ return {
+ value: item.id,
+ label: item.name,
+ };
+ });
+ setOptions(skillOptions);
+ }
+ }, [skills, job, options]);
+
+ return (
+
+ {!job || !skills || !options ? (
+
+ ) : (
+ <>
+
+
+
+
+ >
+ )}
+
+ );
+};
+
+JobForm.propTypes = {
+ teamId: PT.string,
+ jobId: PT.string,
+};
+
+export default withAuthentication(JobForm);
diff --git a/src/routes/JobForm/styles.module.scss b/src/routes/JobForm/styles.module.scss
new file mode 100644
index 00000000..d1b77b5c
--- /dev/null
+++ b/src/routes/JobForm/styles.module.scss
@@ -0,0 +1,10 @@
+@import "styles/include";
+@import "~react-datepicker/dist/react-datepicker.css";
+
+.job-modification-details {
+ @include rounded-card;
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+ padding: 0 24px 20px;
+}
diff --git a/src/routes/JobForm/utils.js b/src/routes/JobForm/utils.js
new file mode 100644
index 00000000..365c64e9
--- /dev/null
+++ b/src/routes/JobForm/utils.js
@@ -0,0 +1,202 @@
+/**
+ * JobForm utilities
+ */
+import {
+ RATE_TYPE_OPTIONS,
+ STATUS_OPTIONS,
+ WORKLOAD_OPTIONS,
+ FORM_ROW_TYPE,
+ FORM_FIELD_TYPE,
+} from "../../constants";
+
+const CREATE_JOB_ROWS = [
+ { type: FORM_ROW_TYPE.SINGLE, fields: ["title"] },
+ { type: FORM_ROW_TYPE.SINGLE, fields: ["description"] },
+ { type: FORM_ROW_TYPE.SINGLE, fields: ["numPositions"] },
+ { type: FORM_ROW_TYPE.SINGLE, fields: ["skills"] },
+ { type: FORM_ROW_TYPE.GROUP, fields: ["startDate", "endDate"] },
+ { type: FORM_ROW_TYPE.SINGLE, fields: ["resourceType"] },
+ { type: FORM_ROW_TYPE.SINGLE, fields: ["rateType"] },
+ { type: FORM_ROW_TYPE.SINGLE, fields: ["workload"] },
+ { type: FORM_ROW_TYPE.SINGLE, fields: ["status"] },
+];
+
+const EDIT_JOB_ROWS = [
+ { type: FORM_ROW_TYPE.SINGLE, fields: ["title"] },
+ { type: FORM_ROW_TYPE.SINGLE, fields: ["description"] },
+ { type: FORM_ROW_TYPE.SINGLE, fields: ["numPositions"] },
+ { type: FORM_ROW_TYPE.SINGLE, fields: ["skills"] },
+ { type: FORM_ROW_TYPE.GROUP, fields: ["startDate", "endDate"] },
+ { type: FORM_ROW_TYPE.SINGLE, fields: ["resourceType"] },
+ { type: FORM_ROW_TYPE.SINGLE, fields: ["rateType"] },
+ { type: FORM_ROW_TYPE.SINGLE, fields: ["workload"] },
+ { type: FORM_ROW_TYPE.SINGLE, fields: ["status"] },
+];
+
+/**
+ * return edit job configuration
+ * @param {any} skillOptions skill options
+ * @param {func} onSubmit submit callback
+ */
+export const getEditJobConfig = (skillOptions, onSubmit) => {
+ return {
+ fields: [
+ {
+ label: "Job Name",
+ type: FORM_FIELD_TYPE.TEXT,
+ isRequired: true,
+ validationMessage: "Please, enter Job Name",
+ name: "title",
+ maxLength: 128,
+ placeholder: "Job Name",
+ },
+ {
+ label: "Job Description",
+ type: FORM_FIELD_TYPE.TEXTAREA,
+ name: "description",
+ placeholder: "Job Description",
+ },
+ {
+ label: "Number Of Opening",
+ type: FORM_FIELD_TYPE.NUMBER,
+ isRequired: true,
+ validationMessage: "Please, enter Job Name",
+ name: "numPositions",
+ minValue: 1,
+ placeholder: "Number Of Opening",
+ },
+ {
+ label: "Job Skills",
+ type: FORM_FIELD_TYPE.SELECT,
+ isMulti: true,
+ name: "skills",
+ selectOptions: skillOptions,
+ },
+ {
+ label: "Start Date",
+ type: FORM_FIELD_TYPE.DATE,
+ name: "startDate",
+ placeholder: "Start Date",
+ },
+ {
+ label: "End Date",
+ type: FORM_FIELD_TYPE.DATE,
+ name: "endDate",
+ placeholder: "End Date",
+ },
+ {
+ label: "Resource Type",
+ type: FORM_FIELD_TYPE.TEXT,
+ name: "resourceType",
+ maxLength: 255,
+ placeholder: "Resource Type",
+ },
+ {
+ label: "Rate Type",
+ type: FORM_FIELD_TYPE.SELECT,
+ name: "rateType",
+ selectOptions: RATE_TYPE_OPTIONS,
+ },
+ {
+ label: "Workload",
+ type: FORM_FIELD_TYPE.SELECT,
+ name: "workload",
+ selectOptions: WORKLOAD_OPTIONS,
+ },
+ {
+ label: "Status",
+ type: FORM_FIELD_TYPE.SELECT,
+ isRequired: true,
+ validationMessage: "Please, select Status",
+ name: "status",
+ selectOptions: STATUS_OPTIONS,
+ },
+ ],
+ onSubmit: onSubmit,
+ rows: EDIT_JOB_ROWS,
+ };
+};
+
+/**
+ * return create job configuration
+ * @param {any} skillOptions skill options
+ * @param {func} onSubmit submit callback
+ */
+export const getCreateJobConfig = (skillOptions, onSubmit) => {
+ return {
+ fields: [
+ {
+ label: "Job Name",
+ type: FORM_FIELD_TYPE.TEXT,
+ isRequired: true,
+ validationMessage: "Please, enter Job Name",
+ name: "title",
+ maxLength: 128,
+ placeholder: "Job Name",
+ },
+ {
+ label: "Job Description",
+ type: FORM_FIELD_TYPE.TEXTAREA,
+ name: "description",
+ placeholder: "Job Description",
+ },
+ {
+ label: "Number Of Opening",
+ type: FORM_FIELD_TYPE.NUMBER,
+ isRequired: true,
+ validationMessage: "Please, enter Job Name",
+ name: "numPositions",
+ minValue: 1,
+ placeholder: "Number Of Opening",
+ },
+ {
+ label: "Job Skills",
+ type: FORM_FIELD_TYPE.SELECT,
+ isMulti: true,
+ name: "skills",
+ selectOptions: skillOptions,
+ },
+ {
+ label: "Start Date",
+ type: FORM_FIELD_TYPE.DATE,
+ name: "startDate",
+ placeholder: "Start Date",
+ },
+ {
+ label: "End Date",
+ type: FORM_FIELD_TYPE.DATE,
+ name: "endDate",
+ placeholder: "End Date",
+ },
+ {
+ label: "Resource Type",
+ type: FORM_FIELD_TYPE.TEXT,
+ name: "resourceType",
+ maxLength: 255,
+ placeholder: "Resource Type",
+ },
+ {
+ label: "Rate Type",
+ type: FORM_FIELD_TYPE.SELECT,
+ name: "rateType",
+ selectOptions: RATE_TYPE_OPTIONS,
+ },
+ {
+ label: "Workload",
+ type: FORM_FIELD_TYPE.SELECT,
+ name: "workload",
+ selectOptions: WORKLOAD_OPTIONS,
+ },
+ {
+ label: "Status",
+ type: FORM_FIELD_TYPE.SELECT,
+ isRequired: true,
+ validationMessage: "Please, select Status",
+ name: "status",
+ selectOptions: STATUS_OPTIONS,
+ },
+ ],
+ onSubmit: onSubmit,
+ rows: CREATE_JOB_ROWS,
+ };
+};
diff --git a/src/routes/MyTeamsDetails/components/TeamMembers/index.jsx b/src/routes/MyTeamsDetails/components/TeamMembers/index.jsx
index 945995fb..37cfde3f 100644
--- a/src/routes/MyTeamsDetails/components/TeamMembers/index.jsx
+++ b/src/routes/MyTeamsDetails/components/TeamMembers/index.jsx
@@ -116,6 +116,7 @@ const TeamMembers = ({ team }) => {
...member,
photoUrl: member.photo_url,
}}
+ handleLinkTo={`/taas/myteams/${team.id}/rb/${member.id}`}
/>