diff --git a/src/assets/images/incentive-note.svg b/src/assets/images/incentive-note.svg new file mode 100644 index 00000000..812cf7d7 --- /dev/null +++ b/src/assets/images/incentive-note.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/styles/variables.scss b/src/assets/styles/variables.scss index 68c54555..953c6bb0 100644 --- a/src/assets/styles/variables.scss +++ b/src/assets/styles/variables.scss @@ -47,6 +47,7 @@ --border: #e8e8e8; --border-warm: #e9e7e6; --border-cold: #e6e7e9; + --border-cold-light: #e6efe7; --border-greyscale: #e7e7e7; --white-smoke: #f3f3f3; @@ -107,6 +108,8 @@ --pop-up-highlight: #fee5c4; --geolocation-stepper-ellipse-color: #67acfe; + --incentive-green: #2b5a2a; + // black base order --black-base: #1a1a1a; @@ -159,6 +162,7 @@ --z-index-ameyo-disconnect-btn: 99; --z-index-leaderboard-overlay: 99; --z-index-leaderboard: 100; + --z-index-leaderboard-tooltip: 1200; --z-index-default-layout-open-cosmos-alert: 100; --z-index-ameyo-reallocation-loader: 100; --z-index-autocomplete-dropdown: 100; diff --git a/src/components/leaderboard/CashEarnedChartHC.tsx b/src/components/leaderboard/CashEarnedChartHC.tsx index 60138390..0bda735c 100644 --- a/src/components/leaderboard/CashEarnedChartHC.tsx +++ b/src/components/leaderboard/CashEarnedChartHC.tsx @@ -1,11 +1,14 @@ import React, { useEffect, useMemo, useState } from 'react'; import Highcharts from 'highcharts'; import HighchartsReact from 'highcharts-react-official'; -import { ILeaderboard } from 'src/reducers/leaderboardSlice'; +import { ILeaderboard, IncentiveData } from 'src/reducers/leaderboardSlice'; import styles from './Leaderboard.module.scss'; -import { dateFormat, monthNames } from 'src/utils/DateHelper'; -import { shortNumberNotationStartingWithLakhs } from 'src/utils/commonUtils'; -import { RightArrow } from 'src/assets/icons/RightArrowThin'; +import { dateFormat } from 'src/utils/DateHelper'; +import { + getNumberFormat, + shortNumberNotationStartingWithLakhs, + shortNumberNotationWithoutTrailingZeroesAndRounding +} from 'src/utils/commonUtils'; import ToggleTabs from '../toggleTabs/ToggleTabs'; import { ITabsOption, SelectPickerOptionProps } from '../interfaces'; import cx from 'classnames'; @@ -18,6 +21,16 @@ import { addClickstreamEvent } from '../../service/clickStreamEventService'; import { CLICKSTREAM_EVENT_NAMES } from '../../service/clickStream.constant'; import { Typography } from '@navi/web-ui/lib/primitives'; import RadioGroup from '@navi/web-ui/lib/components/RadioGroup'; +import InfoIconOutlined from '@cp/assets/images/icons/InfoIconOutlined'; +import { + Directions, + Tooltip as TooltipV2, + TooltipContent, + TooltipTrigger +} from '@cp/components/TooltipV2/TooltipV2'; +import { INCENTIVE_BUCKET_GROUP } from '@cp/components/leaderboard/leaderboardConstant'; +import Loader from '@cp/components/Loader/Loader'; + interface CashEarnedChartHCProps extends ILeaderboard { titleContainerClass?: string; pieChartContainerClass?: string; @@ -102,22 +115,29 @@ const CashEarnedChartHC: React.FC = props => { pieChartContainerClass, animation = true, showDropdown = false, - renderedOn + renderedOn, + incentiveData, + prevMonthIncentiveData, + monthlyIncentiveData } = props; const dispatch = useDispatch(); const MONTH_OPTIONS = getMonthOptions(); const referenceId = useSelector((state: RootState) => state.common.userData.referenceId); + const leaderboardLoading = useSelector( + (state: RootState) => state.leaderboard.leaderboardLoading + ); const [view, setView] = React.useState<'curr' | 'prev'>('curr'); const [cashCollectedDropdownSelectedOption, setCashCollectedDropdownSelectedOption] = useState( MONTH_OPTIONS[MONTH_OPTIONS.length - 1] ); const handleViewChange = () => setView(prev => (prev == 'curr' ? 'prev' : 'curr')); const buttonPosition = view == 'curr' ? { left: 16 } : { right: 16 }; - const DEFAULT_OPTION = 'Total'; - const [selectedRadioOption, setSelectedRadioOption] = useState(DEFAULT_OPTION); + const DEFAULT_OPTION = INCENTIVE_BUCKET_GROUP.TOTAL; + const [selectedRadioOption, setSelectedRadioOption] = + useState(DEFAULT_OPTION); const handleRadioChange = (event: React.SyntheticEvent) => { const value = (event?.target as HTMLInputElement)?.value; - setSelectedRadioOption(value); + setSelectedRadioOption(value as INCENTIVE_BUCKET_GROUP); if (referenceId) { dispatch( getLeaderboardDetails( @@ -235,19 +255,19 @@ const CashEarnedChartHC: React.FC = props => { const DEFAULT_OPTIONS = [ { - id: 'Total', - value: 'Total', - label: 'Total' + id: INCENTIVE_BUCKET_GROUP.TOTAL, + value: INCENTIVE_BUCKET_GROUP.TOTAL, + label: INCENTIVE_BUCKET_GROUP.TOTAL }, { - id: '1-30', - value: '1-30', - label: '1-30' + id: INCENTIVE_BUCKET_GROUP.DPD_1_TO_30, + value: INCENTIVE_BUCKET_GROUP.DPD_1_TO_30, + label: INCENTIVE_BUCKET_GROUP.DPD_1_TO_30 }, { - id: '30+', - value: '30+', - label: '30+' + id: INCENTIVE_BUCKET_GROUP.DPD_30_PLUS, + value: INCENTIVE_BUCKET_GROUP.DPD_30_PLUS, + label: INCENTIVE_BUCKET_GROUP.DPD_30_PLUS } ]; @@ -263,6 +283,41 @@ const CashEarnedChartHC: React.FC = props => { } return noData; }, [data]); + + const [selectedIncentiveData, setIncentiveData] = useState(); + const totalCashCollectedForSelectedRadioOption = useMemo( + () => + selectedRadioOption === INCENTIVE_BUCKET_GROUP.DPD_1_TO_30 + ? selectedIncentiveData?.total1To30DaysCashCollected + : selectedRadioOption === INCENTIVE_BUCKET_GROUP.DPD_30_PLUS + ? selectedIncentiveData?.total30PlusDaysCashCollected + : selectedIncentiveData?.thresholdForIncentive, + [selectedRadioOption, selectedIncentiveData] + ); + const totalIncentiveEarnedForSelectedRadioOption = useMemo( + () => + selectedRadioOption === INCENTIVE_BUCKET_GROUP.DPD_1_TO_30 + ? selectedIncentiveData?.total1To30DaysIncentiveEarned + : selectedRadioOption === INCENTIVE_BUCKET_GROUP.DPD_30_PLUS + ? selectedIncentiveData?.total30PlusDaysIncentiveEarned + : selectedIncentiveData?.incentiveEarned, + [selectedRadioOption, selectedIncentiveData] + ); + + useEffect(() => { + if (showDropdown) { + setIncentiveData(monthlyIncentiveData); + } + }, [monthlyIncentiveData]); + + useEffect(() => { + if (view === 'curr') { + setIncentiveData(incentiveData); + } else { + setIncentiveData(prevMonthIncentiveData); + } + }, [view]); + return ( <>
@@ -299,15 +354,67 @@ const CashEarnedChartHC: React.FC = props => { {getFirstDate()} - {getLastDate(view)} - ) : null} -
- -
- {showDropdown ? ( - <> + ) : ( Cash Collection Breakup - - ) : null} + )} +
+
+ + Incentive Earned + + + + + + + {selectedRadioOption === INCENTIVE_BUCKET_GROUP.TOTAL ? ( +
+ + Total Cash Collected (a) + + + {getNumberFormat(selectedIncentiveData?.totalCashCollected, 2) || `₹0`} + +
+ ) : ( + <> + )} +
+ + {selectedRadioOption !== INCENTIVE_BUCKET_GROUP.TOTAL + ? `Total ${selectedRadioOption} cash collected` + : `Threshold for incentives (b)`} + + + {getNumberFormat(totalCashCollectedForSelectedRadioOption, 2) || `₹0`} + +
+ {selectedRadioOption === INCENTIVE_BUCKET_GROUP.TOTAL ? ( +
+ + Amount eligible for incentives (a-b) + + + {getNumberFormat(selectedIncentiveData?.amountEligibleForIncentive, 2) || `₹0`} + +
+ ) : ( + <> + )} +
+ + {selectedRadioOption !== INCENTIVE_BUCKET_GROUP.TOTAL + ? `Incentive earned on ${selectedRadioOption} cases` + : `Incentives earned (${selectedIncentiveData?.incentivePercentage}% of eligible + amount)`} + + + {getNumberFormat(totalIncentiveEarnedForSelectedRadioOption, 2) || `₹0`} + +
+
+
+
= props => { })} >
- - {noDataInPieChart && ( - - No cash collected for 30+ cases - - )} +
+
+ {shortNumberNotationWithoutTrailingZeroesAndRounding( + totalIncentiveEarnedForSelectedRadioOption, + 2, + '', + 1e5, + true + ) || `₹0`} +
+
+
+ + {noDataInPieChart && ( + + No cash collected for 30+ cases + + )} +
= props => { options={DEFAULT_OPTIONS} labelClassName="" /> + + *Subject to month end adjustments +
+ ); }; diff --git a/src/components/leaderboard/Leaderboard.module.scss b/src/components/leaderboard/Leaderboard.module.scss index 547f4e3c..5c590fe7 100644 --- a/src/components/leaderboard/Leaderboard.module.scss +++ b/src/components/leaderboard/Leaderboard.module.scss @@ -15,7 +15,8 @@ align-items: center; z-index: var(--z-index-leaderboard); box-sizing: border-box; - max-width: 700px; + min-width: 695px; + max-width: 800px; .header { height: 44px; @@ -24,8 +25,15 @@ display: flex; flex-direction: row; justify-content: space-between; + justify-self: center; align-items: center; cursor: pointer; + width: 100%; + .separator { + height: 24px; + width: 1px; + border-right: 1px solid var(--navigation-blue-c2); + } } &.leaderBoardActive { @@ -482,7 +490,28 @@ .pieChartContainer { width: 100%; display: flex; - justify-content: center; + justify-content: space-between; + align-items: center; + .incentiveSectionContainer { + border-right: 1px solid var(--light-grayscale-border); + height: 132px; + display: flex; + justify-content: center; + align-items: center; + .svgContainer { + height: 60px; + width: 150px; + display: flex; + justify-content: center; + align-items: center; + background: url('/src/assets/images/incentive-note.svg'); + color: var(--incentive-green); + font-size: 20px; + font-weight: 700; + line-height: 28px; + letter-spacing: -0.4px; + } + } } .pieChartDashboardWidth { @@ -613,8 +642,17 @@ flex-direction: row-reverse; justify-content: space-between; align-items: center; - padding: 0 3px 0 24px; + padding: 0 10px 0 10px; background: var(--bg-primary); + .incentiveHeader { + width: 30%; + text-align: left; + display: flex; + align-items: center; + gap: 4px; + .incentiveText { + } + } } .radioContainer { @@ -690,10 +728,32 @@ } } +.incentiveDisclaimerText { + color: var(--grayscale-2); + position: absolute; + left: 10px; + font-size: 10px; +} + .noDataText { color: var(--grayscale-3); padding-left: 20px; position: absolute; top: 180px; - left: auto; + left: 51.5%; +} + +.tooltipBorder { + border-top: 1px dashed var(--grayscale-2); + border-bottom: 1px dashed var(--grayscale-2); +} + +.loadingState { + width: 100%; + height: 120%; + position: absolute; + top: 0; + right: 0; + background-color: rgba(255, 255, 255, 0.6); + animation: fadeIn 120ms; } diff --git a/src/components/leaderboard/Leaderboard.tsx b/src/components/leaderboard/Leaderboard.tsx index 8d8aebbd..20c54238 100644 --- a/src/components/leaderboard/Leaderboard.tsx +++ b/src/components/leaderboard/Leaderboard.tsx @@ -128,12 +128,15 @@ const Leaderboard: React.FC = props => { })} > -
-
+ + + {name ? `${name}` : ''} + + +
+
+
- - {name ? `${name}` : ''} - {`Last ${leaderboard?.noOfDaysCashCollected} days cash collected`} @@ -160,6 +163,7 @@ const Leaderboard: React.FC = props => {
+
@@ -195,7 +199,7 @@ const Leaderboard: React.FC = props => { className={styles.content} style={{ justifyContent: 'right', flexDirection: 'column', padding: 0 }} > - + {performanceData ? ( diff --git a/src/components/leaderboard/leaderboardConstant.ts b/src/components/leaderboard/leaderboardConstant.ts index 62b46aec..7cc83253 100644 --- a/src/components/leaderboard/leaderboardConstant.ts +++ b/src/components/leaderboard/leaderboardConstant.ts @@ -2,3 +2,15 @@ export enum PERFORMANCE_CHART_TYPE { LAST_30_DAYS = 'LAST_30_DAYS', LAST_7_DAYS = 'LAST_7_DAYS' } + +export enum INCENTIVE_BUCKET_GROUP { + TOTAL = 'Total', + DPD_1_TO_30 = '1-30', + DPD_30_PLUS = '30+' +} + +export const INCENTIVE_MONTH_KEYS = { + incentiveData: 'incentiveData', + prevMonthIncentiveData: 'prevMonthIncentiveData', + monthlyIncentiveData: 'monthlyIncentiveData' +}; diff --git a/src/pages/Dashboard/Dashboard.tsx b/src/pages/Dashboard/Dashboard.tsx index 314841cb..17a93050 100644 --- a/src/pages/Dashboard/Dashboard.tsx +++ b/src/pages/Dashboard/Dashboard.tsx @@ -393,7 +393,7 @@ const DashBoard = () => { ) : null} - + void) { + dispatch(showLoader(true)); await axiosInstance .get(encodeURI(url), { headers: { donotHandleError: true }, @@ -366,6 +368,7 @@ export const getLeaderboardDetails = ( dispatch(setLeaderboard(response?.data)); }) .catch(err => { + dispatch(showLoader(false)); logError(err, 'Getting error from getLeaderboardDetails :: ' + url); }); }; diff --git a/src/reducers/leaderboardSlice.ts b/src/reducers/leaderboardSlice.ts index b31258b3..b9e43690 100644 --- a/src/reducers/leaderboardSlice.ts +++ b/src/reducers/leaderboardSlice.ts @@ -23,6 +23,10 @@ export interface ILeaderboard { totalCurrentMonthCashCollected: any; totalPrevMonthCashCollected: any; isExpanded: boolean; + incentiveData?: IncentiveData; + prevMonthIncentiveData?: IncentiveData; + monthlyIncentiveData?: IncentiveData; + leaderboardLoading: boolean; } export interface IPromiseCount { @@ -65,6 +69,18 @@ export interface IDataSet { lowerLevelCashCollectedInLast30Days: string | null; } +export interface IncentiveData { + totalCashCollected?: number; + thresholdForIncentive?: number; + amountEligibleForIncentive?: number; + incentivePercentage?: number; + incentiveEarned?: number; + total1To30DaysCashCollected?: number; + total30PlusDaysCashCollected?: number; + total1To30DaysIncentiveEarned?: number; + total30PlusDaysIncentiveEarned?: number; +} + const initialState = { agentLevelRanking: 0, maxLevel: 0, @@ -82,7 +98,8 @@ const initialState = { levelLowerLimit: 0 } ], - isExpanded: false + isExpanded: false, + leaderboardLoading: false } as unknown as ILeaderboard; const COLOR_MAPPING = { @@ -111,7 +128,10 @@ const notificationSlice = createSlice({ totalPrevMonthCashCollected, todaysCashCollected, noOfDaysCashCollected, - dateWhenRankingVisible + dateWhenRankingVisible, + incentiveData, + prevMonthIncentiveData, + monthlyIncentiveData } = action.payload; // todo make it action.payload state.agentLevelRanking = agentLevelRanking; @@ -140,6 +160,10 @@ const notificationSlice = createSlice({ state.totalMonthlyCashCollected = monthlyCashCollected?.totalMonthlyCashCollected; state.totalCurrentMonthCashCollected = totalCurrentMonthCashCollected; state.totalPrevMonthCashCollected = totalPrevMonthCashCollected; + state.incentiveData = incentiveData; + state.prevMonthIncentiveData = prevMonthIncentiveData; + state.monthlyIncentiveData = monthlyIncentiveData; + state.leaderboardLoading = false; }, setMonthlyPerformance(state, action) { // state.performance = payload; @@ -191,6 +215,9 @@ const notificationSlice = createSlice({ }, setIsExpanded(state, action) { state.isExpanded = action.payload; + }, + showLoader(state, action) { + state.leaderboardLoading = action.payload; } } }); @@ -202,7 +229,8 @@ export const { setPercentCompletedCases, setPromiseCount, setCallbridgeData, - setIsExpanded + setIsExpanded, + showLoader } = notificationSlice.actions; export default notificationSlice.reducer; diff --git a/src/utils/commonUtils.ts b/src/utils/commonUtils.ts index 28ea7742..d5f4af94 100644 --- a/src/utils/commonUtils.ts +++ b/src/utils/commonUtils.ts @@ -17,6 +17,7 @@ import { getMaxInputDate } from '../pages/LiveLocationTracker/utils'; import { DateFormat } from './DateHelper'; export const casesPath = `/cases/details/lan/{accountNumber}/customer/{customerReferenceId}/tab/overview`; + export const isObjectEmpty = (object: any) => { for (const prop in object) { // eslint-disable-next-line no-prototype-builtins @@ -30,7 +31,8 @@ export enum DEVICES { IOS = 'IOS' } -export const getNumberFormat = (value: number | bigint, fractionalDigits: number) => { +export const getNumberFormat = (value: number | bigint | undefined, fractionalDigits: number) => { + if (!value) return; return new Intl.NumberFormat('en-IN', { style: 'currency', maximumFractionDigits: fractionalDigits, @@ -126,18 +128,20 @@ export const toFixedWithoughtRoundOff = (value = 0, decimals = 2): string => { export const shortNumberNotationWithoutTrailingZeroesAndRounding = ( value = 0, decimals = 2, - separator = '' + separator = '', + minThreshold = 1e5, + useNumberFormat = false ) => { if (typeof value !== 'number') return ''; const parsedValue = Number(value); - if (parsedValue >= 1e7) { + if (parsedValue >= 1e7 && minThreshold <= 1e7) { return '₹' + toFixedWithoughtRoundOff(parsedValue / 1e7, decimals) + `${separator}Cr`; - } else if (parsedValue >= 1e5) { + } else if (parsedValue >= 1e5 && minThreshold <= 1e5) { return '₹' + toFixedWithoughtRoundOff(parsedValue / 1e5, decimals) + `${separator}L`; - } else if (parsedValue >= 1e3) { + } else if (parsedValue >= 1e3 && minThreshold <= 1e3) { return '₹' + toFixedWithoughtRoundOff(parsedValue / 1e3, decimals) + `${separator}K`; } - return '₹' + parsedValue; + return useNumberFormat ? getNumberFormat(parsedValue, decimals) : '₹' + parsedValue; }; export const shortNumberNotationStartingWithLakhs = (