diff --git a/packages/plugin-cards-ui/src/deals/components/CalendarColumn.tsx b/packages/plugin-cards-ui/src/deals/components/CalendarColumn.tsx index 3fa9d9e280..6a005cd162 100644 --- a/packages/plugin-cards-ui/src/deals/components/CalendarColumn.tsx +++ b/packages/plugin-cards-ui/src/deals/components/CalendarColumn.tsx @@ -13,6 +13,7 @@ import styled from 'styled-components'; import options from '@erxes/ui-cards/src/deals/options'; import { IDeal, IDealTotalAmount } from '@erxes/ui-cards/src/deals/types'; import Deal from '@erxes/ui-cards/src/deals/components/DealItem'; +import styledTS from 'styled-components-ts'; type Props = { deals: IDeal[]; @@ -22,16 +23,31 @@ type Props = { onLoadMore: (skip: number) => void; }; -const Amount = styled.ul` +const Amount = styledTS<{ showAll: boolean }>(styled.ul)` list-style: none; overflow: hidden; margin: 0 0 5px; padding: 0 16px; + ${props => + props.showAll === false + ? ` + height: 20px; + overflow: hidden; + transition: all 300ms ease-out; + ` + : ` + height: unset; + `} + li { padding-right: 5px; font-size: 12px; + > div { + float: right; + } + span { font-weight: bold; font-size: 10px; @@ -45,9 +61,25 @@ const Amount = styled.ul` content: ''; } } + + div { + display: inline; + } `; class DealColumn extends React.Component { + componentDidMount() { + window.addEventListener('storageChange', this.handleStorageChange); + } + + componentWillUnmount() { + window.removeEventListener('storageChange', this.handleStorageChange); + } + + handleStorageChange = () => { + this.forceUpdate(); + }; + onLoadMore = () => { const { deals, onLoadMore } = this.props; onLoadMore(deals.length); @@ -69,7 +101,7 @@ class DealColumn extends React.Component { renderAmount(currencies: [{ name: string; amount: number }]) { return currencies.map((total, index) => ( -
+
{total.amount.toLocaleString()}{' '} {total.name} @@ -80,21 +112,104 @@ class DealColumn extends React.Component { } renderTotalAmount() { - const { dealTotalAmounts } = this.props; + const { dealTotalAmounts, deals } = this.props; const totalForType = dealTotalAmounts || []; - return ( - - {totalForType.map(type => ( -
  • - {type.name}: - {this.renderAmount(type.currencies)} + const forecastArray = []; + const totalAmountArray = []; + + dealTotalAmounts.map(total => + total.currencies.map(currency => totalAmountArray.push(currency)) + ); + + this.props.deals.map(deal => { + const probability = + deal.stage.probability === 'Won' + ? '100%' + : deal.stage.probability === 'Lost' + ? '0%' + : deal.stage.probability; + + Object.keys(deal.amount).map(key => + forecastArray.push({ + name: key, + amount: deal.amount[key] as number, + probability: parseInt(probability, 10) + }) + ); + }); + + const detail = () => { + if (!deals || deals.length === 0) { + return null; + } + + return ( + <> +
  • + Total ({deals.length}): + {this.renderPercentedAmount(totalAmountArray)}
  • - ))} +
  • + Forecasted: + {this.renderPercentedAmount(forecastArray)} +
  • + + ); + }; + + return ( + + {detail()} + {totalForType.map(type => { + if (type.name === 'In progress') { + return null; + } + + const percent = type.name === 'Won' ? '100%' : '0%'; + + return ( +
  • + + {type.name} ({percent}):{' '} + + {this.renderAmount(type.currencies)} +
  • + ); + })}
    ); } + renderPercentedAmount(currencies) { + const sumByName = {}; + + currencies.forEach(item => { + const { name, amount, probability = 100 } = item; + if (sumByName[name] === undefined) { + sumByName[name] = (amount * probability) / 100; + } else { + sumByName[name] += (amount * probability) / 100; + } + }); + + return Object.keys(sumByName).map((key, index) => ( +
    + {sumByName[key].toLocaleString(undefined, { + maximumFractionDigits: 0 + })}{' '} + + {key} + {index < Object.keys(sumByName).length - 1 && ','}  + +
    + )); + } + renderFooter() { const { deals, totalCount } = this.props; diff --git a/packages/ui-cards/src/boards/components/MainActionBar.tsx b/packages/ui-cards/src/boards/components/MainActionBar.tsx index 0cc3c2fcf9..9bfe379868 100644 --- a/packages/ui-cards/src/boards/components/MainActionBar.tsx +++ b/packages/ui-cards/src/boards/components/MainActionBar.tsx @@ -53,13 +53,23 @@ type Props = { viewType: string; }; -class MainActionBar extends React.Component { +type State = { + showDetail: boolean; +}; + +class MainActionBar extends React.Component { static defaultProps = { viewType: 'board', boardText: 'Board', pipelineText: 'Pipeline' }; + constructor(props: Props) { + super(props); + + this.state = { showDetail: false }; + } + renderBoards() { const { currentBoard, boards } = this.props; if ((currentBoard && boards.length === 1) || boards.length === 0) { @@ -358,6 +368,39 @@ class MainActionBar extends React.Component { ); }; + onDetailShowHandler = () => { + this.setState( + { + showDetail: !this.state.showDetail + }, + () => { + localStorage.setItem('showSalesDetail', `${this.state.showDetail}`); + const storageChangeEvent = new Event('storageChange'); + window.dispatchEvent(storageChangeEvent); + } + ); + }; + + renderSalesDetail = () => { + if ( + window.location.pathname.includes('deal/board') || + window.location.pathname.includes('deal/calendar') + ) { + return ( + + ); + } + + return null; + }; + render() { const { currentBoard, @@ -416,6 +459,7 @@ class MainActionBar extends React.Component { ) : null} {this.renderVisibility()} + {this.renderSalesDetail()} ); diff --git a/packages/ui-cards/src/boards/components/stage/Stage.tsx b/packages/ui-cards/src/boards/components/stage/Stage.tsx index 008bd0c45f..cefd54aab5 100644 --- a/packages/ui-cards/src/boards/components/stage/Stage.tsx +++ b/packages/ui-cards/src/boards/components/stage/Stage.tsx @@ -9,21 +9,22 @@ import { IndicatorItem, LoadingContent, StageFooter, + StageInfo, StageRoot, StageTitle } from '../../styles/stage'; +import { Dropdown, OverlayTrigger, Popover } from 'react-bootstrap'; +import { IItem, IOptions, IStage } from '../../types'; +import { renderAmount, renderPercentedAmount } from '../../utils'; + +import { AddForm } from '../../containers/portable'; +import { Draggable } from 'react-beautiful-dnd'; import EmptyState from '@erxes/ui/src/components/EmptyState'; import Icon from '@erxes/ui/src/components/Icon'; +import ItemList from '../stage/ItemList'; import ModalTrigger from '@erxes/ui/src/components/ModalTrigger'; -import { __ } from '@erxes/ui/src/utils/core'; import React from 'react'; -import { Draggable } from 'react-beautiful-dnd'; -import { AddForm } from '../../containers/portable'; -import { IItem, IOptions, IStage } from '../../types'; -import { renderAmount } from '../../utils'; -import ItemList from '../stage/ItemList'; -import { OverlayTrigger, Popover, Dropdown } from 'react-bootstrap'; -import { Row } from '@erxes/ui-settings/src/styles'; +import { __ } from '@erxes/ui/src/utils/core'; type Props = { loadingItems: () => boolean; @@ -84,8 +85,18 @@ export default class Stage extends React.Component { return clearInterval(handle); } }, 1000); + + window.addEventListener('storageChange', this.handleStorageChange); + } + + componentWillUnmount() { + window.removeEventListener('storageChange', this.handleStorageChange); } + handleStorageChange = () => { + this.forceUpdate(); + }; + shouldComponentUpdate(nextProps: Props, nextState: State) { const { stage, index, length, items, loadingItems } = this.props; const { showSortOptions } = this.state; @@ -340,6 +351,49 @@ export default class Stage extends React.Component { return ; } + const probability = + stage.probability === 'Won' + ? '100%' + : stage.probability === 'Lost' + ? '0%' + : stage.probability; + + const detail = () => { + if ( + window.location.pathname.includes('deal') && + Object.keys(stage.amount).length > 0 + ) { + const forecast = () => { + if (!probability) { + return null; + } + + return ( +
    + {__('Forecasted') + `(${probability}):`} + {renderPercentedAmount(stage.amount, parseInt(probability, 10))} +
    + ); + }; + + return ( + +
    + {__('Total') + ':'} + {renderAmount(stage.amount)} +
    + {forecast()} +
    + ); + } + + return null; + }; + return ( {(provided, snapshot) => ( @@ -353,10 +407,7 @@ export default class Stage extends React.Component {
    {this.renderCtrl()} - - {renderAmount(stage.amount)} - {renderAmount(stage.unUsedAmount, false)} - + {detail()} {this.renderIndicator()} diff --git a/packages/ui-cards/src/boards/graphql/queries.ts b/packages/ui-cards/src/boards/graphql/queries.ts index d8d7a58d73..98cae277b1 100644 --- a/packages/ui-cards/src/boards/graphql/queries.ts +++ b/packages/ui-cards/src/boards/graphql/queries.ts @@ -201,6 +201,7 @@ const stageCommon = ` code age defaultTick + probability `; const stages = ` diff --git a/packages/ui-cards/src/boards/styles/item.ts b/packages/ui-cards/src/boards/styles/item.ts index 88ad075e70..2df3c6cb28 100644 --- a/packages/ui-cards/src/boards/styles/item.ts +++ b/packages/ui-cards/src/boards/styles/item.ts @@ -7,6 +7,7 @@ import { FormContainer } from '../styles/common'; import { borderRadius } from './common'; import { rgba } from '@erxes/ui/src/styles/ecolor'; import styledTS from 'styled-components-ts'; +import { StageInfo } from './stage'; const buttonColor = '#0a1e3c'; @@ -43,6 +44,10 @@ export const PriceContainer = styled.div` ul { float: left; } + + ${StageInfo} { + margin-top: 10px; + } `; export const Left = styled.div` diff --git a/packages/ui-cards/src/boards/styles/stage.ts b/packages/ui-cards/src/boards/styles/stage.ts index f41d6a942f..771cb3a941 100644 --- a/packages/ui-cards/src/boards/styles/stage.ts +++ b/packages/ui-cards/src/boards/styles/stage.ts @@ -109,7 +109,7 @@ const Amount = styledTS<{ unUsed: boolean }>(styled.ul)` display: inline-block; li { - float: left; + float: right; ${props => props.unUsed && `text-decoration: line-through;`} padding-right: 5px; line-height: 22px; @@ -117,13 +117,6 @@ const Amount = styledTS<{ unUsed: boolean }>(styled.ul)` font-weight: bold; font-size: 10px; } - &:after { - content: '/'; - margin-left: 5px; - } - &:last-child:after { - content: ''; - } } `; @@ -197,6 +190,50 @@ export const StageTitle = styled.h4` position: relative; display: flex; justify-content: space-between; + + i { + cursor: pointer; + } +`; + +export const StageInfo = styledTS<{ showAll?: boolean }>(styled.div)` + margin-bottom: 10px; + ${props => + props.showAll === false + ? ` + height: 15px; + overflow: hidden; + transition: all 300ms ease-out; + ` + : ` + height: unset; + `} + div { + display: flex; + justify-content: space-between; + min-height: unset !important; + align-items: center; + } + + > div { + margin-bottom: 5px; + } + + span { + font-size: 11px; + font-weight: 600; + } + + ul { + margin: 0; + li { + font-size: 11px; + line-height: 12px; + span { + font-size: 9px; + } + } + } `; export const GroupTitle = styled.div` diff --git a/packages/ui-cards/src/boards/utils.tsx b/packages/ui-cards/src/boards/utils.tsx index d72883f987..33d9a14c15 100644 --- a/packages/ui-cards/src/boards/utils.tsx +++ b/packages/ui-cards/src/boards/utils.tsx @@ -154,6 +154,25 @@ export const renderAmount = (amount = {}, tick = true) => { ); }; +export const renderPercentedAmount = (amount = {}, percent, tick = true) => { + if (!Object.keys(amount).length) { + return <>; + } + + return ( + + + {Object.keys(amount).map(key => ( +
  • + {((amount[key] * percent) / 100).toLocaleString()}{' '} + {key} +
  • + ))} +
    +
    + ); +}; + export const invalidateCache = () => { localStorage.setItem('cacheInvalidated', 'true'); }; diff --git a/packages/ui-cards/src/deals/components/DealItem.tsx b/packages/ui-cards/src/deals/components/DealItem.tsx index 9a6ad69350..097fd2707c 100644 --- a/packages/ui-cards/src/deals/components/DealItem.tsx +++ b/packages/ui-cards/src/deals/components/DealItem.tsx @@ -1,20 +1,26 @@ +import { IOptions, IStage } from '../../boards/types'; import { PriceContainer, Right, Status } from '../../boards/styles/item'; -import { renderAmount, renderPriority } from '../../boards/utils'; +import { + renderAmount, + renderPercentedAmount, + renderPriority +} from '../../boards/utils'; import Assignees from '../../boards/components/Assignees'; import { Content } from '../../boards/styles/stage'; import Details from '../../boards/components/Details'; import DueDateLabel from '../../boards/components/DueDateLabel'; import EditForm from '../../boards/containers/editForm/EditForm'; +import { Flex } from '@erxes/ui/src/styles/main'; import { IDeal } from '../types'; -import { IOptions } from '../../boards/types'; +import ItemArchivedStatus from '../../boards/components/portable/ItemArchivedStatus'; import { ItemContainer } from '../../boards/styles/common'; import ItemFooter from '../../boards/components/portable/ItemFooter'; import Labels from '../../boards/components/label/Labels'; import React from 'react'; +import { StageInfo } from '../../boards/styles/stage'; import { __ } from '@erxes/ui/src/utils'; import { colors } from '@erxes/ui/src/styles'; -import ItemArchivedStatus from '../../boards/components/portable/ItemArchivedStatus'; type Props = { stageId?: string; @@ -103,9 +109,42 @@ class DealItem extends React.PureComponent { startDate, closeDate, isComplete, + stage = {} as IStage, customProperties } = item; + const probability = + stage.probability === 'Won' + ? '100%' + : stage.probability === 'Lost' + ? '0%' + : stage.probability; + + const forecast = () => { + if (!probability) { + return null; + } + + return ( + + Forecasted ({probability}):{' '} + {renderPercentedAmount(item.amount, parseInt(probability, 10))} + + ); + }; + + const total = () => { + if (Object.keys(item.amount).length === 0) { + return null; + } + + return ( + + Total: {renderAmount(item.amount)} + + ); + }; + return ( <>
    @@ -123,8 +162,10 @@ class DealItem extends React.PureComponent { /> - {renderAmount(item.unUsedAmount || {}, false)} - {renderAmount(item.amount)} + + {total()} + {forecast()} +