Merge branch 'master' of github.com:navi-medici/address-verification-app into WhatsappFeedbackEnhancment

This commit is contained in:
Ashish Deo
2023-10-31 19:47:43 +05:30
29 changed files with 727 additions and 196 deletions

View File

@@ -131,8 +131,8 @@ def reactNativeArchitectures() {
return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
}
def VERSION_CODE = 96
def VERSION_NAME = "2.5.1"
def VERSION_CODE = 97
def VERSION_NAME = "2.5.2"
android {
ndkVersion rootProject.ext.ndkVersion

View File

@@ -1,6 +1,6 @@
{
"name": "AV_APP",
"version": "2.5.1",
"version": "2.5.2",
"private": true,
"scripts": {
"android:dev": "yarn move:dev && react-native run-android",

View File

@@ -20,6 +20,7 @@ import { AppDispatch } from '../store/store';
export const getPerformanceMetrics =
(
caseDetailsIds: Array<string>,
isExternalAgent: boolean,
setIsLoading: (isLoading: boolean) => void,
callbackFn?: () => void
) =>
@@ -37,11 +38,35 @@ export const getPerformanceMetrics =
atLeastOneEmiCollectedCases,
allCases,
} = response?.data?.casesSplit || {};
const { unVisitedCount, nonContactableCount, nonPtpCount, brokenPtpCount } =
getFiltersCount(caseDetailsIds, response?.data?.casesSplit);
if (isExternalAgent) {
dispatch(
setPerformanceData({
cases: {
visitedCases,
unvisitedCases: unVisitedCount,
contactableCases,
nonContactableCases: nonContactableCount,
totalPtp: ptpCases,
nonPtp: nonPtpCount,
convertedPtp: convertedPtpCases,
brokenPtp: brokenPtpCount,
totalEmi: allCases ?? 0,
atleastOneEmiCollected: atLeastOneEmiCollectedCases,
},
lastUpdatedAt: response?.data?.lastUpdatedAt,
totalCashCollected: response?.data?.totalCashCollected ?? 0,
feedbackDetails: response?.data?.feedbackSplitView,
})
);
return;
}
const { totalLevel, currentLevel } = response?.data?.performanceLevel || {};
const { tillDate, progression } = response?.data?.cashCollectedComparison || {};
const graphData = getGraphFormattedData(progression);
const { unVisitedCount, nonContactableCount, nonPtpCount, brokenPtpCount } =
getFiltersCount(caseDetailsIds, response?.data?.casesSplit);
const totalLevelInNumber = Number(totalLevel);
const currentLevelInNumber = Number(currentLevel);

View File

@@ -0,0 +1,34 @@
import { COLORS } from '@rn-ui-lib/colors';
import React from 'react';
import Svg, { Rect, G, Path, Mask } from 'react-native-svg';
interface SuspiciousFeedbackIconProps {
size?: number;
fillColor?: string;
}
const SuspiciousFeedbackIcon = ({
size = 20,
fillColor = COLORS.BACKGROUND.RED,
}: SuspiciousFeedbackIconProps) => {
return (
<Svg width={size} height={size} viewBox={`0 0 ${size} ${size}`} fill="none">
<Rect width={size} height={size} rx={4} fill={fillColor} />
<Mask id="mask0_7693_106251" maskUnits="userSpaceOnUse" x={2} y={2} width={16} height={16}>
<Rect x={2} y={2} width={16} height={16} fill="#D9D9D9" />
</Mask>
<G mask="url(#mask0_7693_106251)">
<Path
d="M5.99992 14.4967L4.46659 16.0301C4.25547 16.2412 4.01381 16.2884 3.74159 16.1717C3.46936 16.0551 3.33325 15.8467 3.33325 15.5467V5.16341C3.33325 4.79674 3.46381 4.48286 3.72492 4.22174C3.98603 3.96063 4.29992 3.83008 4.66659 3.83008H15.3333C15.6999 3.83008 16.0138 3.96063 16.2749 4.22174C16.536 4.48286 16.6666 4.79674 16.6666 5.16341V13.1634C16.6666 13.5301 16.536 13.844 16.2749 14.1051C16.0138 14.3662 15.6999 14.4967 15.3333 14.4967H5.99992Z"
fill="#E92C2C"
/>
<Path
d="M10.2241 5.50195C9.87298 5.50195 9.59034 5.79043 9.59753 6.14151L9.67453 9.90445C9.68065 10.2036 9.92491 10.4429 10.2241 10.4429C10.5234 10.4429 10.7676 10.2036 10.7737 9.90445L10.8507 6.14151C10.8579 5.79043 10.5753 5.50195 10.2241 5.50195ZM10.2222 12.502C10.4374 12.502 10.6214 12.4365 10.7744 12.3055C10.9273 12.1723 11.0025 12.012 10.9999 11.8246C11.0025 11.6395 10.9273 11.4815 10.7744 11.3505C10.6214 11.2173 10.4374 11.1507 10.2222 11.1507C10.0122 11.1507 9.83072 11.2173 9.67777 11.3505C9.52481 11.4815 9.44704 11.6395 9.44444 11.8246C9.44704 11.9488 9.48463 12.0628 9.55722 12.1667C9.62721 12.2683 9.72054 12.3496 9.83721 12.4105C9.95387 12.4715 10.0822 12.502 10.2222 12.502Z"
fill="white"
/>
</G>
</Svg>
);
};
export default SuspiciousFeedbackIcon;

View File

@@ -612,7 +612,7 @@ export const CLICKSTREAM_EVENT_NAMES = {
},
FA_PERFORMANCE_DASHBOARD_UNVISITED_CASES_LOAD: {
name: 'FA_PERFORMANCE_DASHBOARD_UNVISITED_CASES_LOAD',
description: 'Performance Dashboard Button Clicked',
description: 'Performance Dashboard Unvisited Cases Load',
},
FA_PERFORMANCE_DASHBOARD_NON_CONTACTABLE_CASES_CLICKED: {
name: 'FA_PERFORMANCE_DASHBOARD_NON_CONTACTABLE_CASES_CLICKED',

View File

@@ -38,6 +38,14 @@ const initialState: AgentPerformanceInterface = {
},
cashCollectedGraphData: [],
lastUpdatedAt: 0,
feedbackDetails: {
totalFeedbackCount: 0,
todaysFeedbackCount: 0,
todaysGenuineFeedbackCount: 0,
todaysSuspiciousFeedbackCount: 0,
totalSuspiciousFeedbackCount: 0,
totalGenuineFeedbackCount: 0,
},
},
cashCollectedData: {
data: [],

View File

@@ -5,6 +5,7 @@ import { SCREEN_ANIMATION_DURATION } from '../../common/Constants';
import { PageRouteEnum } from '../auth/ProtectedRouter';
import CashCollected from '../cashCollected';
import FilteredCases from '../filteredCases';
import { Animation } from './interface';
const Stack = createNativeStackNavigator();
@@ -27,8 +28,24 @@ function DashBoardScreens() {
animationDuration: SCREEN_ANIMATION_DURATION,
}}
/>
<Stack.Screen name={PageRouteEnum.CASH_COLLECTED} component={CashCollected} />
<Stack.Screen name={PageRouteEnum.FILTERED_CASES} component={FilteredCases} />
<Stack.Screen
name={PageRouteEnum.CASH_COLLECTED}
component={CashCollected}
options={{
header: () => null,
animationDuration: SCREEN_ANIMATION_DURATION,
animation: Animation.SLIDE_FROM_RIGHT,
}}
/>
<Stack.Screen
name={PageRouteEnum.FILTERED_CASES}
component={FilteredCases}
options={{
header: () => null,
animationDuration: SCREEN_ANIMATION_DURATION,
animation: Animation.SLIDE_FROM_RIGHT,
}}
/>
</Stack.Navigator>
);
}

View File

@@ -0,0 +1,34 @@
import { useAppSelector } from '@hooks';
import React from 'react';
import ExternalAgentPerformanceCard from './ExternalAgentPerformanceCard';
import PerformanceOverview from './PerformanceOverview';
const ExternalAgentDashboard = () => {
const performanceData = useAppSelector((state) => state.agentPerformance.performanceData);
const { totalCashCollected = 0, cases, feedbackDetails } = performanceData || {};
const { totalFeedbackCount = 0, totalSuspiciousFeedbackCount = 0 } = feedbackDetails || {};
let suspiciousFeedback = 0;
if (totalFeedbackCount && totalSuspiciousFeedbackCount) {
suspiciousFeedback = (totalSuspiciousFeedbackCount * 100) / totalFeedbackCount;
}
const externalAgentPerformanceOverview = {
totalCashCollected,
atleastOneEmiCollected: cases?.atleastOneEmiCollected,
totalEmi: cases?.totalEmi,
suspiciousFeedback,
};
return (
<>
<PerformanceOverview
isExternalAgent
performanceOverviewData={externalAgentPerformanceOverview}
/>
<ExternalAgentPerformanceCard />
</>
);
};
export default ExternalAgentDashboard;

View File

@@ -0,0 +1,111 @@
import React from 'react';
import { StyleSheet, View } from 'react-native';
import { GenericStyles, getShadowStyle } from '../../../RN-UI-LIB/src/styles';
import { COLORS } from '../../../RN-UI-LIB/src/styles/colors';
import { navigateToScreen } from '../../components/utlis/navigationUtlis';
import { useAppDispatch, useAppSelector } from '../../hooks';
import { setFilteredListToast } from '../../reducer/allCasesSlice';
import { setSelectedAgentDashboardFilter } from '../../reducer/filtersSlice';
import { addClickstreamEvent } from '../../services/clickstreamEventService';
import { PageRouteEnum } from '../auth/ProtectedRouter';
import PerformanceCardV1 from './PerformanceCardV1';
import PerformanceCardV2 from './PerformanceCardV2';
import {
CurrentAllocationStatsFilterMap,
CurrentAllocationStatsMap,
PerformanceDetailsType,
} from './interface';
import {
getClickedEventName,
getExternalAgentPerformanceDetails,
getPerformanceDetailFilter,
} from './utils';
import { COMMON_FILTER } from './constants';
const ExternalAgentPerformanceCard = () => {
const { performanceData, filters } = useAppSelector((state) => ({
performanceData: state?.agentPerformance?.performanceData,
filters: state?.filters?.filters,
}));
const { cases, feedbackDetails } = performanceData || {};
const performanceDetails = getExternalAgentPerformanceDetails(cases, feedbackDetails);
const dispatch = useAppDispatch();
const handleActionItemClick = (item: PerformanceDetailsType) => {
if (item?.redirectionType) {
addClickstreamEvent(getClickedEventName(item.redirectionType), {});
dispatch(
setSelectedAgentDashboardFilter(
getPerformanceDetailFilter(
item.redirectionType,
Object.keys(filters?.[COMMON_FILTER]?.filters ?? {})?.includes(
CurrentAllocationStatsFilterMap[item.redirectionType]
)
)
)
);
dispatch(
setFilteredListToast({
showToast: true,
caseType: CurrentAllocationStatsMap[item.redirectionType],
})
);
navigateToScreen(PageRouteEnum.FILTERED_CASES);
}
};
return (
<View style={styles.container}>
{performanceDetails?.map((item) =>
item?.v1 ? (
<PerformanceCardV1 item={item} handleActionItemClick={handleActionItemClick} />
) : (
<PerformanceCardV2 item={item} handleActionItemClick={handleActionItemClick} />
)
)}
</View>
);
};
const styles = StyleSheet.create({
container: {
flexWrap: 'wrap',
justifyContent: 'space-between',
...GenericStyles.row,
...GenericStyles.plr16,
...GenericStyles.mb8,
...GenericStyles.mt16,
},
title: {
fontSize: 13,
marginLeft: 4,
marginBottom: 4,
},
totalCount: {
color: COLORS.TEXT.DARK,
fontSize: 36,
fontWeight: '600',
paddingTop: 16,
},
rightIcon: {
marginTop: 2,
},
subTitle: {
color: COLORS.BASE.BLUE,
},
fw700: {
fontWeight: '700',
},
pressableCard: {
backgroundColor: COLORS.BACKGROUND.PRIMARY,
width: '47%',
...GenericStyles.mb16,
...GenericStyles.pt12,
...GenericStyles.ph12,
...getShadowStyle(2),
borderRadius: 6,
position: 'relative',
},
});
export default ExternalAgentPerformanceCard;

View File

@@ -0,0 +1,26 @@
import { useAppSelector } from '@hooks';
import React from 'react';
import InternalAgentPerformanceCard from './InternalAgentPerformanceCard';
import PerformanceMeter from './PerformanceMeter';
import PerformanceOverview from './PerformanceOverview';
const InternalAgentDashboard = () => {
const performanceData = useAppSelector((state) => state.agentPerformance.performanceData);
const { totalCashCollected = 0, cases } = performanceData || {};
const internalAgentPerformanceOverview = {
totalCashCollected,
atleastOneEmiCollected: cases?.atleastOneEmiCollected,
totalEmi: cases?.totalEmi,
};
return (
<>
<PerformanceOverview performanceOverviewData={internalAgentPerformanceOverview} />
<PerformanceMeter />
<InternalAgentPerformanceCard />
</>
);
};
export default InternalAgentDashboard;

View File

@@ -0,0 +1,68 @@
import React from 'react';
import { StyleSheet, View } from 'react-native';
import { GenericStyles } from '../../../RN-UI-LIB/src/styles';
import { navigateToScreen } from '../../components/utlis/navigationUtlis';
import { useAppDispatch, useAppSelector } from '../../hooks';
import { setFilteredListToast } from '../../reducer/allCasesSlice';
import { setSelectedAgentDashboardFilter } from '../../reducer/filtersSlice';
import { addClickstreamEvent } from '../../services/clickstreamEventService';
import { PageRouteEnum } from '../auth/ProtectedRouter';
import PerformanceCardV1 from './PerformanceCardV1';
import {
CurrentAllocationStatsFilterMap,
CurrentAllocationStatsMap,
PerformanceDetailsType,
} from './interface';
import { getClickedEventName, getPerformanceDetailFilter, getPerformanceDetails } from './utils';
const InternalAgentPerformanceCard = () => {
const { performanceData, filters } = useAppSelector((state) => ({
performanceData: state.agentPerformance.performanceData,
filters: state.filters.filters,
}));
const { cases } = performanceData || {};
const performanceDetails = getPerformanceDetails(cases);
const dispatch = useAppDispatch();
const handleActionItemClick = (item: PerformanceDetailsType) => {
dispatch(
setSelectedAgentDashboardFilter(
getPerformanceDetailFilter(
item.redirectionType,
Object.keys(filters?.['COMMON']?.filters ?? {})?.includes(
CurrentAllocationStatsFilterMap[item.redirectionType]
)
)
)
);
dispatch(
setFilteredListToast({
showToast: true,
caseType: CurrentAllocationStatsMap[item.redirectionType],
})
);
navigateToScreen(PageRouteEnum.FILTERED_CASES);
addClickstreamEvent(getClickedEventName(item.redirectionType), {});
};
return (
<View style={styles.container}>
{performanceDetails?.map((item) => (
<PerformanceCardV1 item={item} handleActionItemClick={handleActionItemClick} />
))}
</View>
);
};
const styles = StyleSheet.create({
container: {
flexWrap: 'wrap',
justifyContent: 'space-between',
...GenericStyles.row,
...GenericStyles.plr16,
...GenericStyles.mb8,
...GenericStyles.mt16,
},
});
export default InternalAgentPerformanceCard;

View File

@@ -1,123 +0,0 @@
import React from 'react';
import { StyleSheet, TouchableOpacity, View } from 'react-native';
import Heading from '../../../RN-UI-LIB/src/components/Heading';
import Text from '../../../RN-UI-LIB/src/components/Text';
import Chevron from '../../../RN-UI-LIB/src/Icons/Chevron';
import { GenericStyles, getShadowStyle } from '../../../RN-UI-LIB/src/styles';
import { COLORS } from '../../../RN-UI-LIB/src/styles/colors';
import { navigateToScreen } from '../../components/utlis/navigationUtlis';
import { useAppDispatch, useAppSelector } from '../../hooks';
import { setFilteredListToast } from '../../reducer/allCasesSlice';
import { setSelectedAgentDashboardFilter } from '../../reducer/filtersSlice';
import { addClickstreamEvent } from '../../services/clickstreamEventService';
import { PageRouteEnum } from '../auth/ProtectedRouter';
import {
CurrentAllocationStatsFilterMap,
CurrentAllocationStatsMap,
PerformanceDetailsType,
} from './interface';
import { getClickedEventName, getPerformanceDetailFilter, getPerformanceDetails } from './utils';
const PerformanceCard = () => {
const { performanceData, filters } = useAppSelector((state) => ({
performanceData: state.agentPerformance.performanceData,
filters: state.filters.filters,
}));
const { cases } = performanceData || {};
const performanceDetails = getPerformanceDetails(cases);
const dispatch = useAppDispatch();
const handleActionItemClick = (item: PerformanceDetailsType) => {
dispatch(
setSelectedAgentDashboardFilter(
getPerformanceDetailFilter(
item.redirectionType,
Object.keys(filters?.['COMMON']?.filters ?? {})?.includes(
CurrentAllocationStatsFilterMap[item.redirectionType]
)
)
)
);
dispatch(
setFilteredListToast({
showToast: true,
caseType: CurrentAllocationStatsMap[item.redirectionType],
})
);
navigateToScreen(PageRouteEnum.FILTERED_CASES);
addClickstreamEvent(getClickedEventName(item.redirectionType), {});
};
return (
<View style={styles.container}>
{performanceDetails.map((item) => (
<View key={item.convertedText} style={styles.pressableCard}>
<Heading style={styles.totalCount}>{item.totalConverted}</Heading>
<Text style={styles.title}>{item.convertedText}</Text>
<TouchableOpacity onPress={() => handleActionItemClick(item)} activeOpacity={0.8}>
<View style={[GenericStyles.borderTop, GenericStyles.w100]} />
<View
style={[
GenericStyles.row,
GenericStyles.alignCenter,
GenericStyles.spaceBetween,
GenericStyles.pv12,
]}
>
<View style={GenericStyles.row}>
<Text style={[styles.subTitle, styles.fw700]}>{item.totalNotConverted} </Text>
<Text style={styles.subTitle}>{item.notConvertedText}</Text>
</View>
<View style={styles.rightIcon}>
<Chevron fillColor={COLORS.TEXT.BLUE} />
</View>
</View>
</TouchableOpacity>
</View>
))}
</View>
);
};
const styles = StyleSheet.create({
container: {
flexWrap: 'wrap',
justifyContent: 'space-between',
...GenericStyles.row,
...GenericStyles.plr16,
...GenericStyles.mb8,
...GenericStyles.mt16,
},
title: {
fontSize: 13,
marginTop: 8,
marginBottom: 8,
},
totalCount: {
color: COLORS.TEXT.DARK,
fontSize: 36,
fontWeight: '600',
paddingTop: 16,
},
rightIcon: {
marginTop: 2,
},
subTitle: {
color: COLORS.BASE.BLUE,
},
fw700: {
fontWeight: '700',
},
pressableCard: {
backgroundColor: COLORS.BACKGROUND.PRIMARY,
width: '47%',
...GenericStyles.mb16,
...GenericStyles.pt12,
...GenericStyles.ph12,
...getShadowStyle(2),
borderRadius: 6,
position: 'relative',
},
});
export default PerformanceCard;

View File

@@ -0,0 +1,80 @@
import React from 'react';
import { COLORS } from '@rn-ui-lib/colors';
import Heading from '@rn-ui-lib/components/Heading';
import Text from '@rn-ui-lib/components/Text';
import Chevron from '@rn-ui-lib/icons/Chevron';
import { GenericStyles, getShadowStyle } from '@rn-ui-lib/styles';
import { StyleSheet, TouchableOpacity, View } from 'react-native';
import { PerformanceCardProps } from './interface';
const PerformanceCardV1 = (props: PerformanceCardProps) => {
const { item, handleActionItemClick } = props;
return (
<View key={item.convertedText} style={cardStyles.pressableCard}>
<Heading style={cardStyles.totalCount}>{item.totalConverted}</Heading>
<Text style={cardStyles.title}>{item.convertedText}</Text>
<TouchableOpacity onPress={() => handleActionItemClick(item)} activeOpacity={0.8}>
<View style={[GenericStyles.borderTop, GenericStyles.w100]} />
<View
style={[
GenericStyles.row,
GenericStyles.alignCenter,
GenericStyles.spaceBetween,
GenericStyles.pv12,
]}
>
<View style={GenericStyles.row}>
<Text style={[cardStyles.subTitle, cardStyles.fw700]}>{item.totalNotConverted} </Text>
<Text style={cardStyles.subTitle}>{item.notConvertedText}</Text>
</View>
<View style={cardStyles.rightIcon}>
<Chevron fillColor={COLORS.TEXT.BLUE} />
</View>
</View>
</TouchableOpacity>
</View>
);
};
export const cardStyles = StyleSheet.create({
container: {
flexWrap: 'wrap',
justifyContent: 'space-between',
...GenericStyles.row,
...GenericStyles.plr16,
...GenericStyles.mb8,
...GenericStyles.mt16,
},
title: {
fontSize: 13,
marginTop: 8,
marginBottom: 8,
},
totalCount: {
color: COLORS.TEXT.DARK,
fontSize: 36,
fontWeight: '600',
paddingTop: 16,
},
rightIcon: {
marginTop: 2,
},
subTitle: {
color: COLORS.BASE.BLUE,
},
fw700: {
fontWeight: '700',
},
pressableCard: {
backgroundColor: COLORS.BACKGROUND.PRIMARY,
width: '47%',
...GenericStyles.mb16,
...GenericStyles.pt12,
...GenericStyles.ph12,
...getShadowStyle(2),
borderRadius: 6,
position: 'relative',
},
});
export default PerformanceCardV1;

View File

@@ -0,0 +1,103 @@
import { COLORS } from '@rn-ui-lib/colors';
import Heading from '@rn-ui-lib/components/Heading';
import Text from '@rn-ui-lib/components/Text';
import Chevron from '@rn-ui-lib/icons/Chevron';
import { GenericStyles } from '@rn-ui-lib/styles';
import React from 'react';
import { StyleSheet, TouchableOpacity, View } from 'react-native';
import { PerformanceCardProps } from './interface';
import { cardStyles } from './PerformanceCardV1';
const PerformanceCardV2 = (props: PerformanceCardProps) => {
const { item, handleActionItemClick } = props;
return (
<View key={item.convertedText} style={cardStyles.pressableCard}>
<View style={[GenericStyles.row, GenericStyles.alignItemsFlexEnd, GenericStyles.pb4]}>
<Heading style={cardStyles.totalCount}>{item.totalConverted}</Heading>
<Text style={styles.title}>{item.convertedText}</Text>
</View>
<View
style={[GenericStyles.borderTop, GenericStyles.w100, { borderColor: COLORS.BORDER.GREY }]}
/>
<View style={[GenericStyles.row, GenericStyles.alignCenter, GenericStyles.pv6]}>
<View
style={[GenericStyles.br8, styles.totalNotConvertedContainer, styles.greenBackground]}
/>
<Text style={[cardStyles.fw700, GenericStyles.fontSize14, styles.darkText]}>
{item.otherDetailsCount}{' '}
</Text>
<Text style={styles.notConvertedText}>{item.otherDetailsText}</Text>
</View>
{!item?.redirectionType && (
<>
<View
style={[
GenericStyles.borderTop,
GenericStyles.w100,
{ borderColor: COLORS.BORDER.GREY },
]}
/>
<View style={[GenericStyles.row, GenericStyles.alignCenter, GenericStyles.pv6]}>
<View style={[GenericStyles.br8, styles.totalNotConvertedContainer]} />
<Text style={[cardStyles.fw700, GenericStyles.fontSize14, styles.darkText]}>
{item.totalNotConverted}{' '}
</Text>
<Text style={styles.notConvertedText}>{item.notConvertedText}</Text>
</View>
</>
)}
{item?.redirectionType && (
<TouchableOpacity
onPress={() => handleActionItemClick(item)}
activeOpacity={0.8}
hitSlop={{ left: 12, right: 12 }}
>
<View style={[GenericStyles.borderTop, GenericStyles.w100]} />
<View
style={[
GenericStyles.row,
GenericStyles.alignCenter,
GenericStyles.spaceBetween,
GenericStyles.pv12,
]}
>
<View style={[GenericStyles.row, GenericStyles.alignCenter]}>
<View style={[GenericStyles.br8, styles.totalNotConvertedContainer]} />
<Text style={[cardStyles.subTitle, cardStyles.fw700]}>{item.totalNotConverted} </Text>
<Text style={cardStyles.subTitle}>{item.notConvertedText}</Text>
</View>
<View style={cardStyles.rightIcon}>
<Chevron fillColor={COLORS.TEXT.BLUE} />
</View>
</View>
</TouchableOpacity>
)}
</View>
);
};
const styles = StyleSheet.create({
title: {
fontSize: 13,
marginLeft: 4,
marginBottom: 4,
},
totalNotConvertedContainer: {
width: 2,
height: 16,
backgroundColor: COLORS.BORDER.DARK_RED,
marginRight: 6,
},
notConvertedText: {
fontSize: 12,
color: COLORS.TEXT.BLACK,
},
greenBackground: {
backgroundColor: COLORS.BORDER.DARK_GREEN,
},
darkText: {
color: COLORS.TEXT.DARK,
},
});
export default PerformanceCardV2;

View File

@@ -28,6 +28,7 @@ const PerformanceItem = (props: PerformanceItemProps) => {
GenericStyles.spaceBetween,
styles.performanceContainer,
]}
hitSlop={{ left: 16, right: 16 }}
onPress={handlePress}
>
<View style={[GenericStyles.row, GenericStyles.alignCenter]}>
@@ -41,7 +42,7 @@ const PerformanceItem = (props: PerformanceItemProps) => {
const styles = StyleSheet.create({
performanceContainer: {
padding: 14,
paddingVertical: 14,
},
});

View File

@@ -38,8 +38,8 @@ const PerformanceLevelGraph = () => {
const isBiggerBox = (box: Box) => box.id === performanceLevel?.currentLevel;
const selfLevelPosition =
((cashCollectedTillDate.self - cashCollectedTillDate.belowLevel) * 100) /
(cashCollectedTillDate.aboveLevel - cashCollectedTillDate.belowLevel + 1); // if denominator difference is 0 we are adding it to 1, to avoid calculation error, this should never happen i
((cashCollectedTillDate?.self - cashCollectedTillDate?.belowLevel) * 100) /
(cashCollectedTillDate?.aboveLevel - cashCollectedTillDate?.belowLevel + 1); // if denominator difference is 0 we are adding it to 1, to avoid calculation error, this should never happen i
const getBoxStyles = (box: Box) => {
return {

View File

@@ -9,12 +9,13 @@ import EmiCollectedIcon from '../../assets/icons/EmiCollectedIcon';
import PerformanceItem from './PerformanceItem';
import { PerformanceDetails } from './constants';
import { PageRouteEnum } from '../auth/ProtectedRouter';
import { useAppSelector } from '../../hooks';
import { formatAmount } from '../../../RN-UI-LIB/src/utlis/amount';
import SuspiciousFeedbackIcon from '@assets/icons/SuspiciousFeedbackIcon';
import { PerformanceOverviewProps } from './interface';
import { sanatizeSuspisiousFeedback } from './utils';
const PerformanceOverview = () => {
const performanceData = useAppSelector((state) => state.agentPerformance.performanceData);
const { totalCashCollected = 0, cases } = performanceData || {};
const PerformanceOverview = (props: PerformanceOverviewProps) => {
const { isExternalAgent, performanceOverviewData } = props;
return (
<View style={styles.container}>
@@ -28,7 +29,7 @@ const PerformanceOverview = () => {
numberOfLines={1}
style={[styles.fw700, styles.leftContent, GenericStyles.fontSize16, styles.width150]}
>
{formatAmount(Number(totalCashCollected.toFixed(2)), false)}
{formatAmount(Number(performanceOverviewData?.totalCashCollected?.toFixed(2)), false)}
</Text>
<View style={styles.rightIcon}>
<Chevron fillColor={COLORS.TEXT.BLUE} />
@@ -43,12 +44,30 @@ const PerformanceOverview = () => {
rightSideView={
<View style={[GenericStyles.row, GenericStyles.alignCenter]}>
<Text style={[GenericStyles.fontSize16, styles.fw700, styles.leftContent]}>
{cases?.atleastOneEmiCollected}{' '}
{performanceOverviewData?.atleastOneEmiCollected}{' '}
</Text>
<Text style={[styles.rightContent, GenericStyles.fontSize14]}>
/ {performanceOverviewData?.totalEmi}
</Text>
<Text style={[styles.rightContent, GenericStyles.fontSize14]}>/ {cases?.totalEmi}</Text>
</View>
}
/>
{isExternalAgent && (
<>
<View style={[GenericStyles.borderTop, GenericStyles.w100]} />
<PerformanceItem
title={PerformanceDetails.suspiciousFeedback}
icon={<SuspiciousFeedbackIcon />}
rightSideView={
<View style={[GenericStyles.row, GenericStyles.alignCenter]}>
<Text style={[GenericStyles.fontSize16, styles.fw700, styles.leftContent]}>
{sanatizeSuspisiousFeedback(performanceOverviewData?.suspiciousFeedback ?? 0)}%
</Text>
</View>
}
/>
</>
)}
</View>
);
};
@@ -59,6 +78,7 @@ const styles = StyleSheet.create({
...GenericStyles.br6,
...GenericStyles.mr16,
...GenericStyles.ml16,
paddingHorizontal: 16,
...getShadowStyle(2),
},
leftContent: {

View File

@@ -4,6 +4,7 @@ export const PerformanceDetails = {
totalCashCollected: 'Total cash collected',
caseWithAtleast1Emi: 'Cases with atleast 1 EMI collected',
performanceLevel: 'Performance level',
suspiciousFeedback: 'Suspicious feedback %',
};
export const PerformanceCardDetails = {
@@ -15,6 +16,11 @@ export const PerformanceCardDetails = {
nonPtp: 'non PTPs',
ptpConverted: 'PTPs converted',
brokenPtp: 'broken PTPs',
suspicious: 'suspicious',
genuine: 'genuine',
feedbacks: 'feedbacks today',
ptps: 'PTPs',
paid: 'paid',
};
export const PerfomanceHeaderTitle = 'Performance summary';
@@ -70,3 +76,5 @@ export const BIGGER_BOX_SIZE_MULTIPLIER = 1.25;
export const xAxisLabels = ['6th', '13th', '20th', '27th'];
export const yAxisLabels = [4, 3, 2, 1];
export const COMMON_FILTER = 'COMMON';

View File

@@ -16,21 +16,21 @@ import { useAppDispatch, useAppSelector } from '../../hooks';
import { resetTodoList } from '../../reducer/allCasesSlice';
import { addClickstreamEvent } from '../../services/clickstreamEventService';
import DashboardHeader from './DashboardHeader';
import PerformanceCard from './PerformanceCard';
import PerformanceMeter from './PerformanceMeter';
import PerformanceOverview from './PerformanceOverview';
import ExternalAgentDashboard from './ExternalAgentDashboard';
import InternalAgentDashboard from './InternalAgentDashboard';
const Dashboard = () => {
const [refreshing, setRefreshing] = React.useState(false);
const [isLoading, setIsLoading] = useState(false);
const caseDetailsIds = useAppSelector((state) => Object.keys(state.allCases.caseDetails));
const isExternalAgent = useAppSelector((state) => state.user?.isExternalAgent);
const dispatch = useAppDispatch();
const isFocused = useIsFocused();
const fetchAgentPerformanceMetrics = (callbackFn?: () => void) => {
setIsLoading(true);
dispatch(getPerformanceMetrics(caseDetailsIds, setIsLoading, callbackFn));
dispatch(getPerformanceMetrics(caseDetailsIds, isExternalAgent, setIsLoading, callbackFn));
};
useEffect(() => {
@@ -68,9 +68,7 @@ const Dashboard = () => {
style={styles.scrollViewContainer}
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}
>
<PerformanceOverview />
<PerformanceMeter />
<PerformanceCard />
{isExternalAgent ? <ExternalAgentDashboard /> : <InternalAgentDashboard />}
</ScrollView>
</View>
</SafeAreaView>

View File

@@ -43,6 +43,15 @@ export interface CashCollectedProgressItem {
belowLevel: number;
}
export interface FeedbackDetals {
totalFeedbackCount: number;
todaysFeedbackCount: number;
todaysGenuineFeedbackCount: number;
todaysSuspiciousFeedbackCount: number;
totalSuspiciousFeedbackCount: number;
totalGenuineFeedbackCount: number;
}
export interface PerformanceDataType {
cases: CasesType;
performanceLevel: PerformanceLevelType;
@@ -51,6 +60,7 @@ export interface PerformanceDataType {
lastUpdatedAt: number;
cashCollectedTillDate: CashCollectedProgressItem;
cashCollectedGraphData: CashCollectedGraphData;
feedbackDetails: FeedbackDetals;
}
export interface CashCollectedData {
@@ -122,3 +132,35 @@ export interface CashCollectedSplitPayload {
pageNo: number;
pageSize: number;
}
interface PerformanceOverviewData {
totalCashCollected: number;
atleastOneEmiCollected: number;
totalEmi: number;
suspiciousFeedback?: number;
}
export interface PerformanceOverviewProps {
isExternalAgent?: boolean;
performanceOverviewData: PerformanceOverviewData;
}
interface PerformanceCardItem {
totalConverted: number;
convertedText: string;
totalNotConverted: number;
notConvertedText: string;
redirectionType: CurrentAllocationStats;
otherDetailsCount?: number;
otherDetailsText?: string;
v1?: boolean;
}
export interface PerformanceCardProps {
item: PerformanceCardItem;
handleActionItemClick: (item: PerformanceDetailsType) => void;
}
export enum Animation {
SLIDE_FROM_RIGHT = 'slide_from_right',
}

View File

@@ -84,6 +84,63 @@ export const getPerformanceDetails = (cases: CasesType) => {
];
};
export const getExternalAgentPerformanceDetails = (cases: CasesType, feedbackDetails: any) => {
const {
visitedCases,
unvisitedCases,
contactableCases,
nonContactableCases,
totalPtp,
convertedPtp,
brokenPtp,
} = cases || {};
const {
todaysFeedbackCount = 0,
todaysGenuineFeedbackCount = 0,
todaysSuspiciousFeedbackCount = 0,
} = feedbackDetails || {};
return [
{
totalConverted: visitedCases,
convertedText: PerformanceCardDetails.visitedCases,
totalNotConverted: unvisitedCases,
notConvertedText: PerformanceCardDetails.unVisitedCases,
otherDetailsCount: 0,
otherDetailsText: '',
redirectionType: CurrentAllocationStats.UNVISITED,
v1: true,
},
{
totalConverted: contactableCases,
convertedText: PerformanceCardDetails.contactableCases,
totalNotConverted: nonContactableCases,
notConvertedText: PerformanceCardDetails.nonContactableCases,
otherDetailsCount: 0,
otherDetailsText: '',
redirectionType: CurrentAllocationStats.NON_CONTACTABLE,
v1: true,
},
{
totalConverted: totalPtp,
convertedText: PerformanceCardDetails.ptps,
otherDetailsCount: convertedPtp,
otherDetailsText: PerformanceCardDetails.paid,
totalNotConverted: brokenPtp,
notConvertedText: PerformanceCardDetails.brokenPtp,
redirectionType: CurrentAllocationStats.BROKEN_PTP,
},
{
totalConverted: todaysFeedbackCount,
convertedText: PerformanceCardDetails.feedbacks,
otherDetailsCount: todaysGenuineFeedbackCount,
otherDetailsText: PerformanceCardDetails.genuine,
totalNotConverted: todaysSuspiciousFeedbackCount,
notConvertedText: PerformanceCardDetails.suspicious,
},
];
};
export const getFiltersCount = (caseDetailsIds: Array<string>, cases: CasesType) => {
const { unvisitedCaseIds, nonContactableCaseIds, nonPtpCaseIds, brokenPtpCaseIds } = cases || {};
@@ -294,17 +351,23 @@ export const formatNumberWithSuffix = (number: number): string => {
const formatted = (number / 1000).toFixed(1);
return formatted.endsWith('.0') ? formatted.slice(0, -2) + 'k' : formatted + 'k';
} else {
return number.toString();
return number?.toString();
}
};
export const isAgentDashboardVisible = () => {
const user = useAppSelector((state) => state.user);
const isExternalAgent = user?.isExternalAgent;
const roles = user?.agentRoles;
const isFieldAgent = roles?.includes(IUserRole.ROLE_FIELD_AGENT);
return !isExternalAgent && isFieldAgent && roles?.length === 1;
return isFieldAgent && roles?.length === 1;
};
export const sanatizeSuspisiousFeedback = (feedbackPercent: number) => {
if (feedbackPercent) {
return feedbackPercent?.toFixed(1);
}
return feedbackPercent;
};

View File

@@ -101,9 +101,6 @@ const CasesList: React.FC<ICasesList> = ({
showAgentSelectionBottomSheet: state.reportees.showAgentSelectionBottomSheet,
}));
const { showToast = false, caseType = '' } =
useAppSelector((state: RootState) => state.allCases?.filteredListToast) || {};
const [showFilterModal, setShowFilterModal] = React.useState<boolean>(false);
const flashListRef = useRef<GenericType>(null);
const scrollAnimation = useRef(new Animated.Value(0)).current;
@@ -111,6 +108,7 @@ const CasesList: React.FC<ICasesList> = ({
const dispatch = useAppDispatch();
const firePageLoadEvent = () => {
if (isAgentDashboard) return;
if (getCurrentScreen()?.name === 'Cases') {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.AV_CASE_LIST_LOAD);
} else {
@@ -244,23 +242,6 @@ const CasesList: React.FC<ICasesList> = ({
intermediateTodoListMap,
]);
useEffect(() => {
if (showToast) {
const filteredCasesCount = filteredCasesList.length;
toast({
type: 'info',
text1: `${filteredCasesCount} ${caseType} case(s) have been filtered`,
visibilityTime: 3000,
});
dispatch(
setFilteredListToast({
showToast: false,
caseType,
})
);
}
}, [filteredCasesList]);
const handleSearchChange = useCallback(
debounce((query: string) => {
setSearchQuery(query);

View File

@@ -11,6 +11,8 @@ import QuickFilters from '../../components/screens/allCases/allCasesFilters/Quic
import { useAppSelector } from '../../hooks';
import { VisitPlanStatus } from '../../reducer/userSlice';
import { FeedbackStatus } from '../caseDetails/interface';
import { RootState } from '@store';
import { FiltePlaceholderText } from './interface';
interface IFilters {
searchQuery: string;
@@ -39,6 +41,10 @@ const Filters: React.FC<IFilters> = ({
filterCount: isVisitPlan ? state.filters.filterCountVisitPlan : state.filters.filterCount,
})
);
const { caseType = '' } =
useAppSelector((state: RootState) => state.allCases?.filteredListToast) || {};
const getBarWidth = () => {
if (!totalPinnedCount) {
return 0;
@@ -46,6 +52,12 @@ const Filters: React.FC<IFilters> = ({
return attemptedCount / totalPinnedCount;
};
const getTextInputPlaceholder = () => {
if (isVisitPlan) return FiltePlaceholderText.VISIT_PLAN;
if (caseType && isAgentDashboard) return `${caseType} ${FiltePlaceholderText.CASES}`;
return FiltePlaceholderText.MY_CASES;
};
return (
<View>
<View style={[GenericStyles.mh16, GenericStyles.pb16, GenericStyles.centerAlignedRow]}>
@@ -54,7 +66,7 @@ const Filters: React.FC<IFilters> = ({
style={!isAgentDashboard ? styles.textInput : styles.flexBasis95}
LeftComponent={<SearchIcon />}
onChangeText={handleSearchChange}
placeholder={`Search in ${isVisitPlan ? 'visit plan' : 'my cases'}`}
placeholder={`Search in ${getTextInputPlaceholder()}`}
defaultValue={searchQuery}
testID="test_search"
showClearIcon

View File

@@ -338,3 +338,9 @@ export interface IReportee {
agencyCode: string;
agencyName: string;
}
export enum FiltePlaceholderText {
VISIT_PLAN = 'visit plan',
CASES = 'cases',
MY_CASES = 'my cases',
}

View File

@@ -82,10 +82,6 @@ const AuthRouter = () => {
NetworkStatusService.listenForOnline(dispatch);
useEffect(() => {
Linking.addEventListener('url', (event) => handleUrl(event.url));
}, []);
useEffect(() => {
if (!isLoggedIn) {
CosmosForegroundService.isRunning().then((isRunning) => {

View File

@@ -43,13 +43,15 @@ const CashCollectedItem = (props: CashCollectedItemProps) => {
>
<View style={[GenericStyles.row, GenericStyles.alignCenter, styles.flexBasis96]}>
<CaseItemAvatar caseDetailObj={caseItemAvatarCaseDetailObj} shouldBatchAvatar={false} />
<View style={GenericStyles.pl12}>
<Text style={styles.textHeading}>{sanitizeString(customerName)}</Text>
<Text style={styles.subText}>
Collected : <Text>{formatAmount(Number(amountCollected?.toFixed(2)), false)}</Text>
<View style={[GenericStyles.pl12, GenericStyles.fill]}>
<Text style={styles.textHeading} numberOfLines={1}>
{sanitizeString(customerName)}
</Text>
<Text style={styles.subText}>
Current outstanding :{' '}
Collected: <Text>{formatAmount(Number(amountCollected?.toFixed(2)), false)}</Text>
</Text>
<Text style={styles.subText}>
Current outstanding:{' '}
<Text style={totalOverdueAmount > 0 ? styles.red : styles.overdueAmountColor}>
{formatAmount(totalOverdueAmount, false)}
</Text>

View File

@@ -100,6 +100,30 @@ const CashCollected = () => {
</View>
);
const listFooterComponent = () => {
if (data?.length) {
if (data.length < paginationDetails.totalElements) {
return (
<View
style={[GenericStyles.centerAlignedRow, GenericStyles.mb12, styles.loadingContainer]}
>
{isLoading ? <ActivityIndicator color={COLORS.BASE.BLUE} /> : null}
</View>
);
}
if (data.length === paginationDetails.totalElements) {
return (
<View style={[GenericStyles.centerAlignedRow, { marginTop: -6, marginBottom: 36 }]}>
<Text style={{ color: COLORS.TEXT.LIGHT }}>You have reached the end of the list</Text>
</View>
);
}
}
return null;
};
return (
<Layout>
<SafeAreaView style={[GenericStyles.fill, styles.container]}>
@@ -117,19 +141,7 @@ const CashCollected = () => {
)}
onEndReached={fetchMoreData}
onEndReachedThreshold={0.5}
ListFooterComponent={
data?.length && data.length < paginationDetails.totalElements ? (
<View
style={[
GenericStyles.centerAlignedRow,
GenericStyles.mb12,
styles.loadingContainer,
]}
>
{isLoading ? <ActivityIndicator color={COLORS.BASE.BLUE} /> : null}
</View>
) : null
}
ListFooterComponent={listFooterComponent()}
/>
) : (
<View style={styles.centerAbsolute}>

View File

@@ -1,9 +1,16 @@
import React from 'react';
import { getPageLoadEventName } from '@screens/Dashboard/utils';
import { addClickstreamEvent } from '@services/clickstreamEventService';
import React, { useEffect } from 'react';
import { useAppSelector } from '../../hooks';
import CasesList from '../allCases/CasesList';
const FilteredCases = () => {
const { pendingList, pinnedList, completedList } = useAppSelector((state) => state.allCases);
const { caseType } = useAppSelector((state) => state.allCases?.filteredListToast) || {};
useEffect(() => {
if (caseType) addClickstreamEvent(getPageLoadEventName(caseType));
}, []);
return (
<CasesList casesList={[...pendingList, ...pinnedList, ...completedList]} isAgentDashboard />