From 7f6984f1cc89c10506d354135af26ea112757e89 Mon Sep 17 00:00:00 2001 From: yoution Date: Mon, 31 May 2021 12:00:19 +0800 Subject: [PATCH] TC Billing Account Enhancemen --- src/api/billingAccounts.js | 11 +++++++++++ src/assets/icons/v.2.5/icon-warning.svg | 13 +++++++++++++ src/config/constants.js | 7 +++++++ src/helpers/projectHelper.js | 10 ++++++++-- src/projects/actions/project.js | 11 +++++++++++ src/projects/actions/projectDashboard.js | 5 ++++- .../detail/components/BillingAccountField/index.js | 3 +++ .../EditProjectDefaultsForm.jsx | 10 ++++++---- .../CreateMilestoneForm/CreateMilestoneForm.jsx | 4 ++-- .../detail/containers/ProjectDefaultsContainer.jsx | 4 +++- .../detail/containers/ProjectInfoContainer.js | 8 +++++--- src/projects/reducers/project.js | 12 ++++++++++++ src/reducers/alerts.js | 7 +++++++ 13 files changed, 92 insertions(+), 13 deletions(-) create mode 100644 src/assets/icons/v.2.5/icon-warning.svg diff --git a/src/api/billingAccounts.js b/src/api/billingAccounts.js index 5b986857e..639cbe944 100644 --- a/src/api/billingAccounts.js +++ b/src/api/billingAccounts.js @@ -12,3 +12,14 @@ export function fetchBillingAccounts(projectId) { return axios.get(`${TC_API_URL}/v5/projects/${projectId}/billingAccounts`) } + +/** + * Get billing account based on project id + * + * @param {String} projectId Id of the project + * + * @returns {Promise} Billing account data + */ +export function fetchBillingAccount(projectId) { + return axios.get(`${TC_API_URL}/v5/projects/${projectId}/billingAccount`) +} diff --git a/src/assets/icons/v.2.5/icon-warning.svg b/src/assets/icons/v.2.5/icon-warning.svg new file mode 100644 index 000000000..f3e59dfbe --- /dev/null +++ b/src/assets/icons/v.2.5/icon-warning.svg @@ -0,0 +1,13 @@ + + + + Created with Sketch. + + + + + + + + + diff --git a/src/config/constants.js b/src/config/constants.js index a1a0179c9..d88942685 100644 --- a/src/config/constants.js +++ b/src/config/constants.js @@ -120,6 +120,13 @@ export const CLEAR_PROJECT_SUGGESTIONS_SEARCH = 'CLEAR_PROJECT_SUGGESTIONS_SEA export const PROJECT_SUGGESTIONS_SEARCH_FAILURE = 'PROJECT_SUGGESTIONS_SEARCH_FAILURE' export const PROJECT_SUGGESTIONS_SEARCH_SUCCESS = 'PROJECT_SUGGESTIONS_SEARCH_SUCCESS' + +// project billingAccount +export const LOAD_PROJECT_BILLING_ACCOUNT = 'LOAD_PROJECT_BILLING_ACCOUNT' +export const LOAD_PROJECT_BILLING_ACCOUNT_PENDING = 'LOAD_PROJECT_BILLING_ACCOUNT_PENDING' +export const LOAD_PROJECT_BILLING_ACCOUNT_FAILURE = 'LOAD_PROJECT_BILLING_ACCOUNT_FAILURE' +export const LOAD_PROJECT_BILLING_ACCOUNT_SUCCESS = 'LOAD_PROJECT_BILLING_ACCOUNT_SUCCESS' + // Project Dashboard export const LOAD_PROJECT_DASHBOARD = 'LOAD_PROJECT_DASHBOARD' export const LOAD_PROJECT_DASHBOARD_PENDING = 'LOAD_PROJECT_DASHBOARD_PENDING' diff --git a/src/helpers/projectHelper.js b/src/helpers/projectHelper.js index bab78e28d..047a18a1d 100644 --- a/src/helpers/projectHelper.js +++ b/src/helpers/projectHelper.js @@ -20,6 +20,7 @@ import ReportsIcon from '../assets/icons/v.2.5/icon-reports.svg' import AssetsLibraryIcon from '../assets/icons/v.2.5/icon-assets-library.svg' import FAQIcon from '../assets/icons/faq.svg' import AccountSecurityIcon from 'assets/icons/v.2.5/icon-account-security.svg' +import WarningIcon from '../assets/icons/v.2.5/icon-warning.svg' import InvisibleIcon from '../assets/icons/invisible.svg' import { formatNumberWithCommas } from './format' @@ -259,8 +260,9 @@ export function getNewProjectLink(orgConfigs) { * Get the list of navigation links for project details view * @param {Object} project - The project object * @param {string} projectId - The project id + * @param {boolean} isBillingAccountExpired - is billingAccount expired */ -export function getProjectNavLinks(project, projectId, renderFAQs) { +export function getProjectNavLinks(project, projectId, renderFAQs, isBillingAccountExpired) { let messagesTab = null // `Discussions` items can be added as soon as project is loaded // if discussions are not hidden for it @@ -289,7 +291,11 @@ export function getProjectNavLinks(project, projectId, renderFAQs) { } if (hasPermission(PERMISSIONS.VIEW_PROJECT_SETTINGS)) { - navLinks.push({ label: 'Project Settings', to: `/projects/${projectId}/settings`, Icon: AccountSecurityIcon, iconClassName: 'stroke' }) + if (!isBillingAccountExpired) { + navLinks.push({ label: 'Project Settings', to: `/projects/${projectId}/settings`, Icon: AccountSecurityIcon, iconClassName: 'stroke' }) + }else { + navLinks.push({ label: Project Settings , to: `/projects/${projectId}/settings`, Icon: AccountSecurityIcon, iconClassName: 'stroke' }) + } } return navLinks diff --git a/src/projects/actions/project.js b/src/projects/actions/project.js index a4b64b3ad..848e2540d 100644 --- a/src/projects/actions/project.js +++ b/src/projects/actions/project.js @@ -13,6 +13,7 @@ import { getProjectById, createScopeChangeRequest as createScopeChangeRequestAPI, updateScopeChangeRequest as updateScopeChangeRequestAPI, } from '../../api/projects' +import {fetchBillingAccount} from '../../api/billingAccounts' import { getProjectInviteById, getProjectMemberInvites, @@ -26,6 +27,7 @@ import { } from '../../api/projectMembers' // import { loadProductTimelineWithMilestones } from './productsTimelines' import { + LOAD_PROJECT_BILLING_ACCOUNT, LOAD_PROJECT, LOAD_PROJECT_MEMBER_INVITE, CREATE_PROJECT, @@ -661,6 +663,15 @@ export function loadProjectMembers(projectId) { } } +export function loadProjectBillingAccount(projectId) { + return (dispatch) => { + return dispatch({ + type: LOAD_PROJECT_BILLING_ACCOUNT, + payload: fetchBillingAccount(projectId) + }) + } +} + export function loadProjectMemberInvites(projectId) { return (dispatch) => { return dispatch({ diff --git a/src/projects/actions/projectDashboard.js b/src/projects/actions/projectDashboard.js index efb9127e2..ef083cea9 100644 --- a/src/projects/actions/projectDashboard.js +++ b/src/projects/actions/projectDashboard.js @@ -1,5 +1,5 @@ import _ from 'lodash' -import { loadProject, loadProjectInvite, loadProjectMembers, loadProjectMemberInvites } from './project' +import { loadProject, loadProjectBillingAccount, loadProjectInvite, loadProjectMembers, loadProjectMemberInvites } from './project' import { loadProjectPlan } from './projectPlan' import { loadProjectsMetadata } from '../../actions/templates' import { LOAD_PROJECT_DASHBOARD, LOAD_ADDITIONAL_PROJECT_DATA } from '../../config/constants' @@ -27,10 +27,13 @@ const getDashboardData = (dispatch, getState, projectId, isOnlyLoadProjectInfo) //dispatch(loadMembers(userIds)), dispatch(loadProjectMembers(projectId)), dispatch(loadProjectMemberInvites(projectId)) + ] if (isOnlyLoadProjectInfo) { promises = [] } + // load project billingAccounts + promises.push(dispatch(loadProjectBillingAccount(projectId))) // for new projects load phases, products, project template and product templates if (['v3', 'v4'].indexOf(project.version) !== -1) { diff --git a/src/projects/detail/components/BillingAccountField/index.js b/src/projects/detail/components/BillingAccountField/index.js index f466e0e3c..76f3045ca 100644 --- a/src/projects/detail/components/BillingAccountField/index.js +++ b/src/projects/detail/components/BillingAccountField/index.js @@ -77,6 +77,8 @@ class BillingAccountField extends React.Component { } render() { + + const {isExpired} = this.props const placeholder = this.state.billingAccounts.length > 0 ? 'Select billing account' : 'No Billing Account Available' @@ -92,6 +94,7 @@ class BillingAccountField extends React.Component { isDisabled={this.state.billingAccounts.length === 0} showDropdownIndicator /> + {isExpired && Expired} {this.state.selectedBillingAccount && (
!_.isEqual(this.props.project[key], this.state.project[key])) @@ -61,7 +61,7 @@ class EditProjectDefaultsForm extends React.Component { return acc }, {}) updateProject(id, updateProjectObj) - .then(() => this.setState({enableButton: false})) + .then(() => this.setState({enableButton: false})).then(() => loadProjectBillingAccount(id)) .catch(console.error) } @@ -86,6 +86,7 @@ class EditProjectDefaultsForm extends React.Component {
@@ -105,7 +106,8 @@ class EditProjectDefaultsForm extends React.Component { } const mapDispatchToProps = { - updateProject + updateProject, + loadProjectBillingAccount } export default protectComponent( diff --git a/src/projects/detail/components/timeline/CreateMilestoneForm/CreateMilestoneForm.jsx b/src/projects/detail/components/timeline/CreateMilestoneForm/CreateMilestoneForm.jsx index 7a609a8a0..c5e59db9e 100644 --- a/src/projects/detail/components/timeline/CreateMilestoneForm/CreateMilestoneForm.jsx +++ b/src/projects/detail/components/timeline/CreateMilestoneForm/CreateMilestoneForm.jsx @@ -34,13 +34,13 @@ class CreateMilestoneForm extends React.Component { cancelEdit() { const { previousMilestone } = this.props const startDate = previousMilestone ? moment.utc(previousMilestone.completionDate || previousMilestone.endDate) : moment.utc() - this.setState({ + this.state = { isEditing: false, type: '', name: '', startDate: startDate.format('YYYY-MM-DD'), endDate: startDate.add(3, 'days').format('YYYY-MM-DD') - }) + } } onAddClick() { diff --git a/src/projects/detail/containers/ProjectDefaultsContainer.jsx b/src/projects/detail/containers/ProjectDefaultsContainer.jsx index 450a22198..a23599514 100644 --- a/src/projects/detail/containers/ProjectDefaultsContainer.jsx +++ b/src/projects/detail/containers/ProjectDefaultsContainer.jsx @@ -18,6 +18,7 @@ class ProjectDefaultsContainer extends Component { isProcessing, feeds, isFeedsLoading, + isBillingAccountExpired, productsTimelines, phasesTopics, location, @@ -54,7 +55,7 @@ class ProjectDefaultsContainer extends Component { - + ) @@ -68,6 +69,7 @@ const mapStateToProps = ({ loadUser, projectState, projectTopics, topics }) => { return { user: loadUser.user, + isBillingAccountExpired: projectState.isBillingAccountExpired, isProcessing: projectState.processing, phases: projectState.phases, phasesNonDirty: projectState.phasesNonDirty, diff --git a/src/projects/detail/containers/ProjectInfoContainer.js b/src/projects/detail/containers/ProjectInfoContainer.js index 1b81baefb..b9f89199a 100644 --- a/src/projects/detail/containers/ProjectInfoContainer.js +++ b/src/projects/detail/containers/ProjectInfoContainer.js @@ -83,6 +83,7 @@ class ProjectInfoContainer extends React.Component { !_.isEqual(nextProps.feeds, this.props.feeds) || !_.isEqual(nextProps.phases, this.props.phases) || !_.isEqual(nextProps.productsTimelines, this.props.productsTimelines) || + !_.isEqual(nextProps.isBillingAccountExpired, this.props.isBillingAccountExpired) || !_.isEqual(nextProps.phasesTopics, this.props.phasesTopics) || !_.isEqual(nextProps.isFeedsLoading, this.props.isFeedsLoading) || !_.isEqual(nextProps.isProjectProcessing, this.props.isProjectProcessing) || @@ -426,7 +427,7 @@ class ProjectInfoContainer extends React.Component { render() { const { showDeleteConfirm } = this.state const { project, phases, hideInfo, hideMembers, - productsTimelines, isProjectProcessing, notifications, projectTemplates } = this.props + productsTimelines, isProjectProcessing, notifications, projectTemplates, isBillingAccountExpired } = this.props const projectTemplateId = project.templateId const projectTemplateKey = _.get(project, 'details.products[0]') @@ -457,7 +458,7 @@ class ProjectInfoContainer extends React.Component { const notReadAssetsNotifications = filterFileAndLinkChangedNotifications(projectNotReadNotifications) const renderFAQs = containsFAQ(projectTemplate) - const navLinks = getProjectNavLinks(project, project.id, renderFAQs).map((navLink) => { + const navLinks = getProjectNavLinks(project, project.id, renderFAQs, isBillingAccountExpired).map((navLink) => { if (navLink.label === 'Messages') { navLink.count = notReadMessageNotifications.length navLink.toolTipText = 'New messages' @@ -593,10 +594,11 @@ ProjectInfoContainer.PropTypes = { canAccessPrivatePosts: PropTypes.bool.isRequired, } -const mapStateToProps = ({ templates, notifications }) => { +const mapStateToProps = ({ templates, projectState, notifications }) => { const canAccessPrivatePosts = hasPermission(PERMISSIONS.ACCESS_PRIVATE_POST) return ({ projectTemplates : templates.projectTemplates, + isBillingAccountExpired: projectState.isBillingAccountExpired, canAccessPrivatePosts, notifications: notifications.notifications, }) diff --git a/src/projects/reducers/project.js b/src/projects/reducers/project.js index 5ceb3a85d..a877ce308 100644 --- a/src/projects/reducers/project.js +++ b/src/projects/reducers/project.js @@ -1,6 +1,7 @@ import { CREATE_PROJECT_PHASE_TIMELINE_MILESTONES_PENDING, CREATE_PROJECT_PHASE_TIMELINE_MILESTONES_SUCCESS, CREATE_PROJECT_PHASE_TIMELINE_MILESTONES_FAILURE, LOAD_PROJECT_PENDING, LOAD_PROJECT_SUCCESS, LOAD_PROJECT_MEMBER_INVITE_PENDING, LOAD_PROJECT_MEMBER_INVITE_SUCCESS, LOAD_PROJECT_FAILURE, + LOAD_PROJECT_BILLING_ACCOUNT_SUCCESS, LOAD_PROJECT_BILLING_ACCOUNT_FAILURE, CREATE_PROJECT_PENDING, CREATE_PROJECT_SUCCESS, CREATE_PROJECT_FAILURE, CLEAR_LOADED_PROJECT, UPDATE_PROJECT_PENDING, UPDATE_PROJECT_SUCCESS, UPDATE_PROJECT_FAILURE, DELETE_PROJECT_PENDING, DELETE_PROJECT_SUCCESS, DELETE_PROJECT_FAILURE, @@ -36,6 +37,7 @@ export function getEmptyProjectObject() { const initialState = { isLoading: true, + isBillingAccountExpired: false, processing: false, processingMembers: false, updatingMemberIds: [], @@ -151,6 +153,16 @@ export const projectState = function (state=initialState, action) { return Object.assign({}, state, { isCreatingPhase: true }) + case LOAD_PROJECT_BILLING_ACCOUNT_SUCCESS: + return { + ...state, + isBillingAccountExpired: !action.payload.data.active, + } + case LOAD_PROJECT_BILLING_ACCOUNT_FAILURE: + return { + ...state, + isBillingAccountExpired: false, + } case CREATE_PROJECT_PHASE_TIMELINE_MILESTONES_SUCCESS: case CREATE_PROJECT_PHASE_SUCCESS: { const { phase, product } = action.payload diff --git a/src/reducers/alerts.js b/src/reducers/alerts.js index 9ed828d00..dbbca6a6f 100644 --- a/src/reducers/alerts.js +++ b/src/reducers/alerts.js @@ -2,6 +2,8 @@ import _ from 'lodash' import Alert from 'react-s-alert' /* eslint-disable no-unused-vars */ import { + // billing account + LOAD_PROJECT_BILLING_ACCOUNT_SUCCESS, // bulk phase and milestones CREATE_PROJECT_PHASE_TIMELINE_MILESTONES_SUCCESS, // Project @@ -168,6 +170,11 @@ export default function(state = {}, action) { case CANCEL_SCOPE_CHANGE_FAILURE: Alert.error('Unable to Cancel the Scope Change') return state + case LOAD_PROJECT_BILLING_ACCOUNT_SUCCESS: + if (!action.payload.data.active) { + Alert.error('The billing account of this project is expired, please update it.') + } + return state case ACTIVATE_SCOPE_CHANGE_SUCCESS: Alert.success('Activated the Scope Change successfully')