diff --git a/src/api/phaseMembers.js b/src/api/phaseMembers.js new file mode 100644 index 000000000..60782e834 --- /dev/null +++ b/src/api/phaseMembers.js @@ -0,0 +1,9 @@ +import { axiosInstance as axios } from './requestInterceptor' +import { PROJECTS_API_URL } from '../config/constants' + +export function updatePhaseMembers(projectId, phaseId, userIds) { + const url = `${PROJECTS_API_URL}/v5/projects/${projectId}/phases/${phaseId}/members` + const data = { userIds } + return axios.post(url, data) + .then(res => res.data) +} diff --git a/src/api/projects.js b/src/api/projects.js index 41b3366d2..5198f572b 100644 --- a/src/api/projects.js +++ b/src/api/projects.js @@ -34,6 +34,19 @@ export function getProjectSuggestions() { // TODO } +/** + * Get a challenge detail based on it's id + * @param {integer} challenId challenge id + * @return {object} challenge detail returned by api + */ +export function getChallengeById(challengeId) { + return axios.get(`${PROJECTS_API_URL}/v5/challenges/${challengeId}/`) + .then(resp => { + const res = resp.data + return res + }) +} + /** * Get a project based on it's id diff --git a/src/assets/icons/calendar.svg b/src/assets/icons/calendar.svg new file mode 100644 index 000000000..e0823911f --- /dev/null +++ b/src/assets/icons/calendar.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/icons/icon-calendar.svg b/src/assets/icons/icon-calendar.svg new file mode 100644 index 000000000..aadc80a0e --- /dev/null +++ b/src/assets/icons/icon-calendar.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/icon-calendar2.svg b/src/assets/icons/icon-calendar2.svg new file mode 100644 index 000000000..b2ebdd5ba --- /dev/null +++ b/src/assets/icons/icon-calendar2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/icon-copilot.svg b/src/assets/icons/icon-copilot.svg new file mode 100644 index 000000000..3ed5897d0 --- /dev/null +++ b/src/assets/icons/icon-copilot.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/icon-delete.svg b/src/assets/icons/icon-delete.svg new file mode 100644 index 000000000..870e9145e --- /dev/null +++ b/src/assets/icons/icon-delete.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/icon-disselect.svg b/src/assets/icons/icon-disselect.svg new file mode 100644 index 000000000..ba6201051 --- /dev/null +++ b/src/assets/icons/icon-disselect.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/assets/icons/icon-save2.svg b/src/assets/icons/icon-save2.svg new file mode 100644 index 000000000..264fda2ec --- /dev/null +++ b/src/assets/icons/icon-save2.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/icon-trash2.svg b/src/assets/icons/icon-trash2.svg new file mode 100644 index 000000000..b6088e145 --- /dev/null +++ b/src/assets/icons/icon-trash2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/config/constants.js b/src/config/constants.js index dcd197114..1203c00ab 100644 --- a/src/config/constants.js +++ b/src/config/constants.js @@ -12,6 +12,11 @@ export const LOAD_USER_CREDENTIAL_PENDING = 'LOAD_USER_CREDENTIAL_PENDING' export const LOAD_USER_CREDENTIAL_SUCCESS = 'LOAD_USER_CREDENTIAL_SUCCESS' export const LOAD_USER_CREDENTIAL_FAILURE = 'LOAD_USER_CREDENTIAL_FAILURE' +// Load_CHALLEGNES +export const LOAD_CHALLEGNES = 'LOAD_CHALLEGNES' +export const LOAD_CHALLEGNES_PENDING = 'LOAD_CHALLEGNES_PENDING' +export const LOAD_CHALLEGNES_SUCCESS = 'LOAD_CHALLEGNES_SUCCESS' +export const LOAD_CHALLEGNES_FAILURE = 'Load_CHALLEGNES_FAILURE' // Load organization configs export const LOAD_ORG_CONFIG_SUCCESS = 'LOAD_ORG_CONFIG_SUCCESS' @@ -753,6 +758,8 @@ export const PROJECT_ATTACHMENTS_FOLDER = process.env.PROJECT_ATTACHMENTS_FOLDER export const FILE_PICKER_ACCEPT = process.env.FILE_PICKER_ACCEPT || ['.bmp', '.gif', '.jpg', '.tex', '.xls', '.xlsx', '.doc', '.docx', '.zip', '.txt', '.pdf', '.png', '.ppt', '.pptx', '.rtf', '.csv'] export const SEGMENT_KEY = process.env.CONNECT_SEGMENT_KEY + +export const CHALLENGE_ID_MAPPING = 'challengeGuid' /* * URLs */ @@ -766,6 +773,7 @@ export const ACCOUNTS_APP_REGISTER_URL = process.env.ACCOUNTS_APP_REGISTER_URL | export const TC_API_URL = `https://api.${DOMAIN}` export const DIRECT_PROJECT_URL = `https://www.${DOMAIN}/direct/projectOverview?formData.projectId=` export const WORK_MANAGER_APP = `https://challenges.${DOMAIN}/projects` +export const CHALLENGE_DETAIL_APP = `https://www.${DOMAIN}/challenges` export const SALESFORCE_PROJECT_LEAD_LINK = process.env.SALESFORCE_PROJECT_LEAD_LINK export const SALESFORCE_BILLING_ACCOUNT_LINK = process.env.SALESFORCE_BILLING_ACCOUNT_LINK export const TC_NOTIFICATION_URL = process.env.TC_NOTIFICATION_URL || `${TC_API_URL}/v5/notifications` diff --git a/src/projects/actions/phaseMember.js b/src/projects/actions/phaseMember.js new file mode 100644 index 000000000..8cbb5ec42 --- /dev/null +++ b/src/projects/actions/phaseMember.js @@ -0,0 +1,10 @@ +import { updatePhaseMembers as updatePhaseMembersAPI } from '../../api/phaseMembers' + +export function updatePhaseMembers(projectId, phaseId, userIds) { + return (dispatch) => { + return dispatch({ + type: 'UPDATE_PROJECT_PHASE_MEMBERS', + payload: updatePhaseMembersAPI(projectId, phaseId, userIds) + }) + } +} diff --git a/src/projects/actions/project.js b/src/projects/actions/project.js index 51278d20c..23cd461ee 100644 --- a/src/projects/actions/project.js +++ b/src/projects/actions/project.js @@ -2,6 +2,7 @@ import _ from 'lodash' import moment from 'moment' import { flatten, unflatten } from 'flat' import { getProjectById, + getChallengeById, createProject as createProjectAPI, updateProject as updateProjectAPI, deleteProject as deleteProjectAPI, @@ -36,6 +37,7 @@ import { DELETE_PROJECT, PROJECT_DIRTY, PROJECT_DIRTY_UNDO, + LOAD_CHALLEGNES, LOAD_PROJECT_PHASES, UPDATE_PRODUCT, PROJECT_STATUS_DRAFT, @@ -161,6 +163,7 @@ export function loadProjectInvite(projectId) { }*/ } + /** * Get project phases together with products * @@ -187,12 +190,40 @@ function getProjectPhasesWithProducts(projectId) { 'spentBudget', 'startDate', 'status', + 'members', 'updatedAt', 'updatedBy', ].join(',') }) } +/** + * Get challenges by challenge ids + * + * @param {Number} milestoneId milestone id + * @param {Array} challengeIds challenge ids + * + * @returns {Promise<[]>} resolves to the list of challenge + */ +export function getChallengesByIds(milestoneId, challengeIds) { + + const requests = _.map(challengeIds, id => getChallengeById(id)) + const challengesAPIs = Promise.all(requests) + + return (dispatch) => { + return dispatch({ + type: LOAD_CHALLEGNES, + payload: challengesAPIs, + meta: { + milestoneId + } + }) + } + +} + + + /** * Load project phases with populated products * @@ -286,14 +317,12 @@ function createProductsTimelineAndMilestone(project) { * * @return {Promise} project */ -export function createProjectPhaseAndProduct(project, productTemplate, status = PHASE_STATUS_DRAFT, startDate, endDate, createTimeline = true, budget, details) { +export function createProjectPhaseAndProduct(project, productTemplate, status = PHASE_STATUS_DRAFT, startDate, endDate, createTimeline = true) { const param = { status, name: productTemplate.name, description: productTemplate.description, productTemplateId: productTemplate.id, - budget, - details, } if (startDate) { param['startDate'] = startDate.format('YYYY-MM-DD') @@ -358,12 +387,12 @@ function createPhaseAndMilestonesRequest(project, productTemplate, status = PHAS * @param {*} startDate * @param {*} endDate */ -export function createPhaseWithoutTimeline(project, productTemplate, status, startDate, endDate, budget, details) { +export function createPhaseWithoutTimeline(project, productTemplate, status, startDate, endDate) { return (dispatch) => { console.log(CREATE_PROJECT_PHASE) return dispatch({ type: CREATE_PROJECT_PHASE, - payload: createProjectPhaseAndProduct(project, productTemplate, status, startDate, endDate, false, budget, details) + payload: createProjectPhaseAndProduct(project, productTemplate, status, startDate, endDate, false) }) } } diff --git a/src/projects/detail/components/SimplePlan/CreateSimplePlan/CreateSimplePlan.jsx b/src/projects/detail/components/SimplePlan/CreateSimplePlan/CreateSimplePlan.jsx index b059b4503..e329aa0dc 100644 --- a/src/projects/detail/components/SimplePlan/CreateSimplePlan/CreateSimplePlan.jsx +++ b/src/projects/detail/components/SimplePlan/CreateSimplePlan/CreateSimplePlan.jsx @@ -3,7 +3,6 @@ */ import React from 'react' import PT from 'prop-types' -import _ from 'lodash' import GenericMenu from '../../../../../components/GenericMenu' // import ProjectDetailsWidget from '../ProjectDetailsWidget' import ManageMilestones from '../ManageMilestones' @@ -21,19 +20,6 @@ const createTabs = ({ onClick } ) => ([ class CreateSimplePlan extends React.Component { componentDidMount() { - const { project, milestones, loadMembers } = this.props - - let copilotIds = [] - milestones.forEach((milestone) => { - copilotIds = copilotIds.concat(_.get(milestone, 'details.copilots', [])) - }) - - const projectMemberIds = project.members.map(member => member.userId) - const missingMemberIds = _.difference(copilotIds, projectMemberIds) - if (missingMemberIds.length) { - loadMembers(missingMemberIds) - } - const contentInnerElement = document.querySelector('.twoColsLayout-contentInner') contentInnerElement.classList.add(styles['twoColsLayout-contentInner']) } @@ -51,8 +37,8 @@ class CreateSimplePlan extends React.Component { onChangeMilestones, onSaveMilestone, onRemoveMilestone, + onGetChallenges, isProjectLive, - members, isCustomer, } = this.props const onClickMilestonesTab = () => {} @@ -83,12 +69,12 @@ class CreateSimplePlan extends React.Component { @@ -103,7 +89,7 @@ CreateSimplePlan.propTypes = { onChangeMilestones: PT.func, onSaveMilestone: PT.func, onRemoveMilestone: PT.func, - members: PT.object, + onGetChallenges: PT.func, isCustomer: PT.bool, } diff --git a/src/projects/detail/components/SimplePlan/ManageMilestones/ManageMilestones.jsx b/src/projects/detail/components/SimplePlan/ManageMilestones/ManageMilestones.jsx index fbaea6e1e..e63f2aaad 100644 --- a/src/projects/detail/components/SimplePlan/ManageMilestones/ManageMilestones.jsx +++ b/src/projects/detail/components/SimplePlan/ManageMilestones/ManageMilestones.jsx @@ -3,10 +3,21 @@ */ import React from 'react' import PT from 'prop-types' +import moment from 'moment' import FormsyForm from 'appirio-tech-react-components/components/Formsy' import MilestoneRow from '../components/MilestoneRow' +import MilestoneChallengeHeader from '../components/MilestoneChallengeHeader' +import MilestoneChallengeRow from '../components/MilestoneChallengeRow' +import MilestoneChallengeFooter from '../components/MilestoneChallengeFooter' import MilestoneHeaderRow from '../components/MilestoneHeaderRow' +import MilestoneDeleteButton from '../components/MilestoneDeleteButton' +import MilestoneCopilots from '../components/MilestoneCopilots' +import MilestoneMoveDateButton from '../components/MilestoneMoveDateButton' + import * as milestoneHelper from '../components/helpers/milestone' +import IconUnselect from '../../../../../assets/icons/icon-disselect.svg' +import IconCopilot from '../../../../../assets/icons/icon-copilot.svg' +import { CHALLENGE_ID_MAPPING } from '../../../../../config/constants' // import IconGridView from '../../../../../assets/icons/ui-16px-2_grid-45-gray.svg' // import IconGnattView from '../../../../../assets/icons/icon-gnatt-gray.svg' @@ -18,11 +29,105 @@ class ManageMilestones extends React.Component { constructor(props) { super(props) + this.state = { + expandList: [] + } this.onSave = this.onSave.bind(this) this.onChange = this.onChange.bind(this) this.onAdd = this.onAdd.bind(this) this.onRemove = this.onRemove.bind(this) this.onDiscard = this.onDiscard.bind(this) + this.onExpandChallenges = this.onExpandChallenges.bind(this) + this.onUnselectAll = this.onUnselectAll.bind(this) + this.onDeleteAll = this.onDeleteAll.bind(this) + this.onAddCopilotAll = this.onAddCopilotAll.bind(this) + this.onMoveMilestoneDates = this.onMoveMilestoneDates.bind(this) + this.onLoadChallengesByPage = this.onLoadChallengesByPage.bind(this) + } + onMoveMilestoneDates(days) { + const { + milestones, + onSaveMilestone + } = this.props + + + if (days > 0) { + const seletedMilestones = _.filter(milestones, m => m.selected) + _.forEach(seletedMilestones, m => { + m.startDate = moment(m.startDate).add(days, 'days') + m.endDate = moment(m.endDate).add(days, 'days') + this.onChange(m) + onSaveMilestone(m.id) + }) + } + } + onAddCopilotAll(member, isAdd) { + const { milestones, onSaveMilestone } = this.props + const seletedMilestones = _.filter(milestones, m => m.selected) + _.forEach(seletedMilestones, m => { + if (!m.origin) { + m.origin = {...m} + } + if (isAdd) { + const isExist = _.find(m.members, o => o.userId === member.userId) + if (!isExist) { + // ignore + m.members = [...m.members, member] + this.onChange(m) + onSaveMilestone(m.id) + } + } else { + m.members = _.filter(m.members, o => o.userId !== member.userId) + if (m.members.length !== m.origin.members.length) { + this.onChange(m) + onSaveMilestone(m.id) + } + } + }) + } + + onDeleteAll() { + const { milestones, onRemoveMilestone } = this.props + const seletedMilestones = _.filter(milestones, m => m.selected) + _.forEach(seletedMilestones, m => { + onRemoveMilestone(m.id) + }) + } + + onUnselectAll() { + const { milestones, onChangeMilestones } = this.props + const milestonesUnselected = milestones.map(milestone => ({ + ...milestone, + selected: false + })) + onChangeMilestones(milestonesUnselected) + } + + onLoadChallengesByPage(index, milestone) { + const { onGetChallenges } = this.props + let challengeIds = _.map(milestone.products, `details.${CHALLENGE_ID_MAPPING}`).slice(6 * index, 7 * (index+1)) + challengeIds = _.filter(challengeIds) + if (!challengeIds.length) { + return + } + onGetChallenges(milestone.id, challengeIds) + } + onExpandChallenges(isExpand, milestone) { + let expandList = this.state.expandList + const { onGetChallenges } = this.props + + const challengeIds = _.filter(_.map(milestone.products, `details.${CHALLENGE_ID_MAPPING}`)).slice(0, 6) + + if (isExpand) { + if (challengeIds.length) { + onGetChallenges(milestone.id, challengeIds) + } + expandList.push(milestone.id) + this.setState({expandList}) + } else { + expandList = _.filter(expandList, e => milestone.id !== e) + this.setState({expandList}) + } } onChange(updatedMilestone) { @@ -63,15 +168,86 @@ class ManageMilestones extends React.Component { onRemoveMilestone(id) } + isExpandChallengeList(milestone) { + const isExpand = _.find(this.state.expandList, (i) => i === milestone.id) + if (isExpand) { + return true + } else { + return false + } + } + + renderChallengeTable(milestone) { + const { + isUpdatable + } = this.props + if (!this.isExpandChallengeList(milestone)) { + return + } + + let challengeIds = _.map(milestone.products, `details.${CHALLENGE_ID_MAPPING}`).slice(0, 6) + challengeIds = _.filter(challengeIds) + // no challenges + if (!challengeIds.length) { + return [ + , + + ] + } + + // loading challenges + if (milestone.isLoadingChallenges) { + return [ + , + , + + ] + } + + const rows = _.map(milestone.challenges, (c) => { + return + }) + return [ + , + ...rows, + + ] + } + getSelectCount() { + const { milestones } = this.props + const seletedMilestones = _.filter(milestones, m => m.selected) + return seletedMilestones.length + } + + renderAddCopilot() { + const { + projectMembers, + milestones, + } = this.props + + const seletedMilestones = _.filter(milestones, m => m.selected) + const copilots = _.intersectionBy(..._.map(seletedMilestones, 'members'), 'userId') + return ( + } + copilots={copilots} + projectMembers={projectMembers} + onAdd={(member) => this.onAddCopilotAll(member, true)} + onRemove={(member) => this.onAddCopilotAll(member, false)} + /> + ) + } + render() { const { milestones, projectMembers, onChangeMilestones, isUpdatable, - members, } = this.props + const canEdit = isUpdatable && this.getSelectCount() > 0 return (
@@ -82,6 +258,17 @@ class ManageMilestones extends React.Component {
*/} + {canEdit ?
+ {this.getSelectCount()} PROJECTS SELECTED +
: null } + {canEdit ?
: null} + { canEdit ?
+ +
: null } + { canEdit ?
+ {this.renderAddCopilot()} +
: null } + { canEdit ? : null} {isUpdatable && ( + +
+
+ ) + } +} + +ConfirmMoveMilestoneDate.propTypes = { + onClose: PT.func, +} + +export default ConfirmMoveMilestoneDate diff --git a/src/projects/detail/components/SimplePlan/components/ConfirmMoveMilestoneDate/ConfirmMoveMilestoneDate.scss b/src/projects/detail/components/SimplePlan/components/ConfirmMoveMilestoneDate/ConfirmMoveMilestoneDate.scss new file mode 100644 index 000000000..2b1b94aeb --- /dev/null +++ b/src/projects/detail/components/SimplePlan/components/ConfirmMoveMilestoneDate/ConfirmMoveMilestoneDate.scss @@ -0,0 +1,59 @@ +@import '~styles/variables'; + +.confirm-delete-milestone { + width: 204px; + padding: 15px 8px 12px 12px; + color: $tc-black; + background-color: $tc-white; + border: 1px solid $tc-gray-50; + border-radius: 3 * $corner-radius; + box-shadow: 0 3px 6px rbga(#000, .16); + + .icon { + width: 16px; + height: 16px; + margin-right: 8px; + color: red; + stroke: currentColor; + + * { + stroke: inherit; + } + } + .input-row { + display: flex; + align-items: center; + margin-bottom: 10px; + >input { + height: 26px; + width: 80px; + margin: 0 10px 0 0; + } + } + + + .title { + @include roboto-bold; + + display: flex; + margin-bottom: 8px; + font-size: $tc-body-xs; + line-height: 15px; + } + + .text { + @include roboto; + + margin-bottom: 12px; + font-size: $tc-body-sm; + line-height: 16px; + } + + .footer { + button + button { + margin-left: 10px; + } + } +} + + diff --git a/src/projects/detail/components/SimplePlan/components/ConfirmMoveMilestoneDate/index.js b/src/projects/detail/components/SimplePlan/components/ConfirmMoveMilestoneDate/index.js new file mode 100644 index 000000000..5f07433c0 --- /dev/null +++ b/src/projects/detail/components/SimplePlan/components/ConfirmMoveMilestoneDate/index.js @@ -0,0 +1,2 @@ +import ConfirmMoveMilestoneDate from './ConfirmMoveMilestoneDate' +export default ConfirmMoveMilestoneDate diff --git a/src/projects/detail/components/SimplePlan/components/MilestoneBudget/MilestoneBudget.jsx b/src/projects/detail/components/SimplePlan/components/MilestoneBudget/MilestoneBudget.jsx deleted file mode 100644 index 64bea9532..000000000 --- a/src/projects/detail/components/SimplePlan/components/MilestoneBudget/MilestoneBudget.jsx +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Milestone budget - */ -import React from 'react' -import PT from 'prop-types' - -import './MilestoneBudget.scss' - -function MilestoneBudget({ spent, budget }) { - return ( -
-
{budget > 0 ? `$${spent} of $${budget}` : `$${budget}`}
-
-
0 ? spent/budget : 0})`}}/> -
- ) -} - - -MilestoneBudget.propTypes = { - spent: PT.number, - budget: PT.number, -} - -export default MilestoneBudget diff --git a/src/projects/detail/components/SimplePlan/components/MilestoneBudget/MilestoneBudget.scss b/src/projects/detail/components/SimplePlan/components/MilestoneBudget/MilestoneBudget.scss deleted file mode 100644 index 308256d79..000000000 --- a/src/projects/detail/components/SimplePlan/components/MilestoneBudget/MilestoneBudget.scss +++ /dev/null @@ -1,35 +0,0 @@ -@import '~styles/variables'; - -.progress-bar { - position: relative; - padding-bottom: 6px; - - .text { - @include roboto; - font-size: 9px; - line-height: 12px; - color: $tc-gray-20; - } - - .background, - .progress { - position: absolute; - left: 0; - bottom: 0; - width: 100%; - border-width: 3px; - border-style: solid; - border-radius: 6px; - } - - .background { - background-color: $tc-gray-20; - border-color: $tc-gray-20; - } - - .progress { - background-color: $tc-green-110; - border-color: $tc-green-110; - transform-origin: left; - } -} diff --git a/src/projects/detail/components/SimplePlan/components/MilestoneBudget/index.js b/src/projects/detail/components/SimplePlan/components/MilestoneBudget/index.js deleted file mode 100644 index d76b13aa7..000000000 --- a/src/projects/detail/components/SimplePlan/components/MilestoneBudget/index.js +++ /dev/null @@ -1,2 +0,0 @@ -import MilestoneBudget from './MilestoneBudget' -export default MilestoneBudget diff --git a/src/projects/detail/components/SimplePlan/components/MilestoneChallengeFooter/MilestoneChallengeFooter.jsx b/src/projects/detail/components/SimplePlan/components/MilestoneChallengeFooter/MilestoneChallengeFooter.jsx new file mode 100644 index 000000000..08f263da1 --- /dev/null +++ b/src/projects/detail/components/SimplePlan/components/MilestoneChallengeFooter/MilestoneChallengeFooter.jsx @@ -0,0 +1,100 @@ +/** + * milestoneChallenge footer + */ +import React from 'react' +import _ from 'lodash' +import cn from 'classnames' +import PT from 'prop-types' +import { + WORK_MANAGER_APP, + CHALLENGE_ID_MAPPING +} from '../../../../../../../src/config/constants' +import './MilestoneChallengeFooter.scss' + + +class MilestoneChallengeFooter extends React.Component { + constructor(props) { + super(props) + this.state = { + curPage: 0 + } + this.loadChallengeBypage = this.loadChallengeBypage.bind(this) + } + + loadChallengeBypage(page) { + const { + milestone, + onLoadChallengesByPage + } = this.props + if (this.state.curPage === page) { + return + } + this.setState({curPage: page}) + onLoadChallengesByPage(page, milestone) + } + renderPagination() { + const { + milestone + } = this.props + let challengeIds = _.map(milestone.products, `details.${CHALLENGE_ID_MAPPING}`) + challengeIds = _.filter(challengeIds) + const length = Math.ceil(challengeIds.length/6) + + if (length === 0) { + return null + + } + + return ( +
+ { + _.map(_.range(length), (i, index) => { + return
{this.loadChallengeBypage(index)}}>{i+1}
+ }) + } +
+ ) + + } + + render() { + const { + isUpdatable, + milestone, + isLoading, + } = this.props + + if (isLoading) { + return null + } + + const url = `${WORK_MANAGER_APP}/${milestone.projectId}/challenges` + + return ( + + +
+ +
+ {this.renderPagination()} +
+
+ + + ) + + } +} + +MilestoneChallengeFooter.propTypes = { + onLoadChallengesByPage: PT.func, + milestone: PT.shape(), + isUpdatable: PT.bool, + isLoading: PT.bool +} + +export default MilestoneChallengeFooter diff --git a/src/projects/detail/components/SimplePlan/components/MilestoneChallengeFooter/MilestoneChallengeFooter.scss b/src/projects/detail/components/SimplePlan/components/MilestoneChallengeFooter/MilestoneChallengeFooter.scss new file mode 100644 index 000000000..5c2809a8c --- /dev/null +++ b/src/projects/detail/components/SimplePlan/components/MilestoneChallengeFooter/MilestoneChallengeFooter.scss @@ -0,0 +1,44 @@ +@import '~styles/variables'; + +.challenge-table-row-wrap { + @include roboto; + + font-size: $tc-label-sm; + color: $tc-gray-60; + line-height: 15px; +} + +.challenge-table-row { + display: flex; + align-items: center; + justify-content: space-between; + + .view-button { + margin-left: 30px; + background-color: #127D60; + padding: 4px 10px; + border-radius: 20px; + color: #fff; + } + .pagination { + display: flex; + margin-right: 20px; + div { + color: #127D60; + border: 1px solid #127D60; + margin: 0 3px; + width: 20px; + height: 20px; + border-radius: 50%; + text-align: center; + line-height: 18px; + cursor: pointer; + &.selected { + color: #000; + border-color: #000; + } + } + } +} + + diff --git a/src/projects/detail/components/SimplePlan/components/MilestoneChallengeFooter/index.js b/src/projects/detail/components/SimplePlan/components/MilestoneChallengeFooter/index.js new file mode 100644 index 000000000..3c78f59e3 --- /dev/null +++ b/src/projects/detail/components/SimplePlan/components/MilestoneChallengeFooter/index.js @@ -0,0 +1,2 @@ +import MilestoneChallengeFooter from './MilestoneChallengeFooter' +export default MilestoneChallengeFooter diff --git a/src/projects/detail/components/SimplePlan/components/MilestoneChallengeHeader/MilestoneChallengeHeader.jsx b/src/projects/detail/components/SimplePlan/components/MilestoneChallengeHeader/MilestoneChallengeHeader.jsx new file mode 100644 index 000000000..17a42de2c --- /dev/null +++ b/src/projects/detail/components/SimplePlan/components/MilestoneChallengeHeader/MilestoneChallengeHeader.jsx @@ -0,0 +1,29 @@ +/** + * milestoneChallenge header + */ +import React from 'react' +import PT from 'prop-types' +import './MilestoneChallengeHeader.scss' + +function MilestoneChallengeHeader({isUpdatable}) { + + return ( + + +
+
TITLE
+
STATUS
+
TYPE
+
START DATE
+
END DATE
+
+ + + ) +} + +MilestoneChallengeHeader.propTypes = { + isUpdatable: PT.bool +} + +export default MilestoneChallengeHeader diff --git a/src/projects/detail/components/SimplePlan/components/MilestoneChallengeHeader/MilestoneChallengeHeader.scss b/src/projects/detail/components/SimplePlan/components/MilestoneChallengeHeader/MilestoneChallengeHeader.scss new file mode 100644 index 000000000..b094a6941 --- /dev/null +++ b/src/projects/detail/components/SimplePlan/components/MilestoneChallengeHeader/MilestoneChallengeHeader.scss @@ -0,0 +1,48 @@ +@import '~styles/variables'; + +.challenge-table-row-wrap { + @include roboto-bold; + + font-size: $tc-label-sm; + color: $tc-gray-60; + line-height: 15px; +} + +.challenge-table-row { + display: flex; + align-items: center; + .title { + flex: 1; + margin-left: 30px; + margin-right: 10px; + } + .status { + width: 100px; + margin-right: 10px; + } + .type { + width: 100px; + margin-right: 10px; + div { + border-radius: 20px; + padding: 4px 0; + text-align: center; + color: #fff; + &.Design { background-color: $tc-light-blue-100; } + &.Development { background-color: $tc-green-100; } + &.DataScience { background-color: $tc-red-100; } + &.QualityAssurance { background-color: $tc-green-100; } + } + } + + .start-date { + width: 90px; + margin-right: 10px; + } + + .end-date { + width: 90px; + } + +} + diff --git a/src/projects/detail/components/SimplePlan/components/MilestoneChallengeHeader/index.js b/src/projects/detail/components/SimplePlan/components/MilestoneChallengeHeader/index.js new file mode 100644 index 000000000..f9ab97fd9 --- /dev/null +++ b/src/projects/detail/components/SimplePlan/components/MilestoneChallengeHeader/index.js @@ -0,0 +1,2 @@ +import MilestoneChallengeHeader from './MilestoneChallengeHeader' +export default MilestoneChallengeHeader diff --git a/src/projects/detail/components/SimplePlan/components/MilestoneChallengeRow/MilestoneChallengeRow.jsx b/src/projects/detail/components/SimplePlan/components/MilestoneChallengeRow/MilestoneChallengeRow.jsx new file mode 100644 index 000000000..17671da7b --- /dev/null +++ b/src/projects/detail/components/SimplePlan/components/MilestoneChallengeRow/MilestoneChallengeRow.jsx @@ -0,0 +1,88 @@ +/** + * milestoneChallenge row + */ +import React from 'react' +import PT from 'prop-types' +import moment from 'moment' +import { + CHALLENGE_DETAIL_APP +} from '../../../../../../../src/config/constants' + +import './MilestoneChallengeRow.scss' + +const STALLED_MSG = 'Stalled' +const DRAFT_MSG = 'Draft' + +function MilestoneChallengeRow({challenge, isEmpty, isLoading, isUpdatable}) { + + if (isEmpty) { + return ( + + +
+ no challenges found +
+ + + ) + } + + if (isLoading) { + return ( + + +
+ loading challenges... +
+ + + ) + } + const { + id, + name, + status, + track, + type, + startDate, + phases: allPhases, + endDate + } = challenge + + let statusPhase = allPhases + .filter(p => p.name !== 'Registration' && p.isOpen) + .sort((a, b) => moment(a.scheduledEndDate).diff(b.scheduledEndDate))[0] + + if (!statusPhase && type === 'First2Finish' && allPhases.length) { + statusPhase = _.clone(allPhases[0]) + statusPhase.name = 'Submission' + } + + let phaseMessage = STALLED_MSG + if (statusPhase) phaseMessage = statusPhase.name + else if (status === 'Draft') phaseMessage = DRAFT_MSG + + + return ( + + +
+ +
{phaseMessage}
+
{track}
+
{moment(startDate).format('MM-DD-YYYY')}
+
{moment(endDate).format('MM-DD-YYYY')}
+
+ + + ) +} + +MilestoneChallengeRow.propTypes = { + challenge: PT.shape(), + isUpdatable: PT.bool, + isEmpty: PT.bool, + isLoading: PT.bool +} + +export default MilestoneChallengeRow diff --git a/src/projects/detail/components/SimplePlan/components/MilestoneChallengeRow/MilestoneChallengeRow.scss b/src/projects/detail/components/SimplePlan/components/MilestoneChallengeRow/MilestoneChallengeRow.scss new file mode 100644 index 000000000..5e7462dfa --- /dev/null +++ b/src/projects/detail/components/SimplePlan/components/MilestoneChallengeRow/MilestoneChallengeRow.scss @@ -0,0 +1,70 @@ +@import '~styles/variables'; + +.challenge-table-row-wrap { + font-weight: 400; + font-size: 13px; + color: #6b6b6b; +} + +.challenge-empty-row { + text-align: center; +} + +.challenge-table-row { + display: flex; + align-items: center; + .title { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + margin-left: 30px; + margin-right: 10px; + color: #0A81FF + } + .status { + width: 100px; + margin-right: 10px; + div { + border-radius: 20px; + padding: 4px 0; + text-align: center; + color: #fff; + &.Submission { background-color: $tc-light-blue-100; } + &.IterativeReview { background-color: $tc-green-100; } + &.Registration { background-color: $tc-orange-100; } + &.Appeals { background-color: $tc-orange-100; } + &.AppealsResponse { background-color: $tc-orange-100; } + &.Post-Mortem { background-color: $tc-orange-100; } + &.Draft { background-color: $tc-gray-20; } + &.Stalled { background-color: $tc-gray-20; } + &.Completed { background-color: $tc-green-100; } + &.Review { background-color: $tc-orange-100; } + &.CheckpointReview { background-color: $tc-orange-100; } + } + + } + .type { + width: 100px; + margin-right: 10px; + div { + border-radius: 20px; + padding: 4px 0; + text-align: center; + color: #fff; + &.Design { background-color: #0076A5; } + &.Development { background-color: #328732; } + &.DataScience { background-color: #BA4C00; } + &.QualityAssurance { background-color: #8231A9 } + } + } + + .start-date { + width: 90px; + margin-right: 10px; + } + + .end-date { + width: 90px; + } + +} diff --git a/src/projects/detail/components/SimplePlan/components/MilestoneChallengeRow/index.js b/src/projects/detail/components/SimplePlan/components/MilestoneChallengeRow/index.js new file mode 100644 index 000000000..92968585d --- /dev/null +++ b/src/projects/detail/components/SimplePlan/components/MilestoneChallengeRow/index.js @@ -0,0 +1,2 @@ +import MilestoneChallengeRow from './MilestoneChallengeRow' +export default MilestoneChallengeRow diff --git a/src/projects/detail/components/SimplePlan/components/MilestoneCopilots/MilestoneCopilots.jsx b/src/projects/detail/components/SimplePlan/components/MilestoneCopilots/MilestoneCopilots.jsx index 30d39b0f4..f78e26b49 100644 --- a/src/projects/detail/components/SimplePlan/components/MilestoneCopilots/MilestoneCopilots.jsx +++ b/src/projects/detail/components/SimplePlan/components/MilestoneCopilots/MilestoneCopilots.jsx @@ -19,7 +19,8 @@ function MilestoneCopilots({ copilots, projectMembers, onAdd, - onRemove + onRemove, + customButton }) { const ScrollLock = React.createClass ({ componentDidMount() { @@ -40,9 +41,9 @@ function MilestoneCopilots({ return edit ? ( - + } {open && ( @@ -66,6 +67,7 @@ MilestoneCopilots.propTypes = { edit: PT.bool, copilots: PT.arrayOf(PT.shape()), projectMembers: PT.arrayOf(PT.shape()), + customButton: PT.element, onAdd: PT.func, onRemove: PT.func, } diff --git a/src/projects/detail/components/SimplePlan/components/MilestoneHeaderRow/MilestoneHeaderRow.jsx b/src/projects/detail/components/SimplePlan/components/MilestoneHeaderRow/MilestoneHeaderRow.jsx index 6822d19b1..b49df0870 100644 --- a/src/projects/detail/components/SimplePlan/components/MilestoneHeaderRow/MilestoneHeaderRow.jsx +++ b/src/projects/detail/components/SimplePlan/components/MilestoneHeaderRow/MilestoneHeaderRow.jsx @@ -31,6 +31,7 @@ function MilestoneHeaderRow ({ milestones, onChangeMilestones, isUpdatable }) { return ( + START DATE END DATE STATUS - BUDGET - {/* COPILOTS */} + COPILOTS {isUpdatable && (ACTION)} ) diff --git a/src/projects/detail/components/SimplePlan/components/MilestoneMoveDateButton/MilestoneMoveDateButton.jsx b/src/projects/detail/components/SimplePlan/components/MilestoneMoveDateButton/MilestoneMoveDateButton.jsx new file mode 100644 index 000000000..cc3d89c48 --- /dev/null +++ b/src/projects/detail/components/SimplePlan/components/MilestoneMoveDateButton/MilestoneMoveDateButton.jsx @@ -0,0 +1,87 @@ +import React from 'react' +import PT from 'prop-types' +import { Popper, Manager } from 'react-popper' +import ConfirmMoveMilestoneDate from '../ConfirmMoveMilestoneDate' +import IconCalendar from '../../../../../../assets/icons/icon-calendar2.svg' + +import './MilestoneMoveDateButton.scss' + +class MilestoneMoveDateButton extends React.Component { + constructor(props) { + super(props) + + this.state = { + open: false + } + + this.onClickOutside = this.onClickOutside.bind(this) + } + + componentDidUpdate() { + const { open } = this.state + + if (open) { + document.addEventListener('click', this.onClickOutside) + } else { + document.removeEventListener('click', this.onClickOutside) + } + } + + onClickOutside(event) { + if (this.confirmRef.contains(event.target)) { + return + } + + this.setState({ open: false }) + } + + render() { + const { onMove } = this.props + const { open } = this.state + + return ( + + { + event.stopPropagation() + + this.setState({ + open: !open + }) + }} + ref={ref => this.btnRef = ref} + > + + + {open && + + + {({ ref, style, placement, arrowProps }) => ( +
+
this.confirmRef = ref2}> + { + if (yes) { + onMove(value) + } + this.setState({ open: false }) + }} + /> +
+
+
+ )} + + + } + + ) + } +} + +MilestoneMoveDateButton.propTypes = { + onMove: PT.func, +} + +export default MilestoneMoveDateButton diff --git a/src/projects/detail/components/SimplePlan/components/MilestoneMoveDateButton/MilestoneMoveDateButton.scss b/src/projects/detail/components/SimplePlan/components/MilestoneMoveDateButton/MilestoneMoveDateButton.scss new file mode 100644 index 000000000..4674861de --- /dev/null +++ b/src/projects/detail/components/SimplePlan/components/MilestoneMoveDateButton/MilestoneMoveDateButton.scss @@ -0,0 +1,21 @@ +.icon-button{ + width: 14px; + height: 14px; + padding: 0; + border: 0; + cursor: pointer; + + svg { + width: 20px; + } +} + +.pane { + margin-top: 10px; + z-index: 1; +} + +.arrow { + width: 0; + height: 0; +} diff --git a/src/projects/detail/components/SimplePlan/components/MilestoneMoveDateButton/index.jsx b/src/projects/detail/components/SimplePlan/components/MilestoneMoveDateButton/index.jsx new file mode 100644 index 000000000..9ee70caf0 --- /dev/null +++ b/src/projects/detail/components/SimplePlan/components/MilestoneMoveDateButton/index.jsx @@ -0,0 +1,2 @@ +import MilestoneMoveDateButton from './MilestoneMoveDateButton' +export default MilestoneMoveDateButton diff --git a/src/projects/detail/components/SimplePlan/components/MilestoneRow/MilestoneRow.jsx b/src/projects/detail/components/SimplePlan/components/MilestoneRow/MilestoneRow.jsx index 4c7a5bd26..c8a2df999 100644 --- a/src/projects/detail/components/SimplePlan/components/MilestoneRow/MilestoneRow.jsx +++ b/src/projects/detail/components/SimplePlan/components/MilestoneRow/MilestoneRow.jsx @@ -5,28 +5,30 @@ import React from 'react' import PT from 'prop-types' import moment from 'moment' import FormsyForm from 'appirio-tech-react-components/components/Formsy' -import _ from 'lodash' import { components } from 'react-select' import { isValidStartEndDates } from '../../../../../../helpers/utils' import FormsySelect from '../../../../../../components/Select/FormsySelect' -// import MilestoneCopilots from '../MilestoneCopilots' +import MilestoneCopilots from '../MilestoneCopilots' import MilestoneStatus from '../MilestoneStatus' -import MilestoneBudget from '../MilestoneBudget' import MilestoneDeleteButton from '../MilestoneDeleteButton' import { PHASE_STATUS_OPTIONS } from '../../../../../../config/constants' -import IconCheck from '../../../../../../assets/icons/icon-check-thin.svg' -import IconXMark from '../../../../../../assets/icons/icon-x-mark-thin.svg' +import IconCheck from '../../../../../../assets/icons/icon-save2.svg' +import IconXMark from '../../../../../../assets/icons/icon-delete.svg' import IconPencil from '../../../../../../assets/icons/icon-ui-pencil.svg' import IconDots from '../../../../../../assets/icons/icon-dots.svg' import IconArrowDown from '../../../../../../assets/icons/arrow-6px-carret-down-normal.svg' +import IconExpand from '../../../../../../assets/icons/arrows-16px-1_minimal-right.svg' +import IconClose from '../../../../../../assets/icons/arrows-16px-1_minimal-down.svg' import styles from './MilestoneRow.scss' const TCFormFields = FormsyForm.Fields function MilestoneRow({ + isExpand, milestone, rowId, + onExpand, onChange, onSave, onRemove, @@ -35,26 +37,20 @@ function MilestoneRow({ allMilestones, isCreatingRow, isUpdatable, - members, + phaseMembers, }) { const phaseStatusOptions = PHASE_STATUS_OPTIONS const edit = milestone.edit - const copilotIds = _.get(milestone, 'details.copilots', []) - let copilots = copilotIds.map(userId => projectMembers.find(member => member.userId === userId)).filter(Boolean) - - if (copilots.length !== copilotIds.length) { - const missingCopilotIds = _.difference(copilotIds, projectMembers.map(member => member.userId)) - const missingCopilots = missingCopilotIds.map(userId => members[userId]) - copilots = copilots.concat(missingCopilots) - } + // hide email + const copilots = (phaseMembers || []).map(member => ({ ...member, email: undefined })) let milestoneRef let startDateRef let endDateRef - let budgetRef return edit ? ( + - - $ - { - if (!milestone.origin) { - milestone.origin = {...milestone} - } - onChange({...milestone, budget: value }) - }} - wrapperClass={styles.textInput} - innerRef={ref => budgetRef = ref} - /> - - {/* + copilot.userId).concat(member.userId) - onChange({...milestone, details: { ...details, copilots: copilotIdsUpdated } }) + const copilotsUpdated = copilots + .concat(member) + onChange({...milestone, members: copilotsUpdated }) }} onRemove={(member) => { if (!milestone.origin) { milestone.origin = {...milestone} } - const details = milestone.details - const copilotIdsUpdated = copilots.filter(copilot => copilot.userId !== member.userId).map(copilot => copilot.userId) - onChange({...milestone, details: { ...details, copilots: copilotIdsUpdated } }) + const copilotsUpdated = copilots + .filter(copilot => copilot.userId !== member.userId) + onChange({...milestone, members: copilotsUpdated }) }} /> - */} +