NTP-59601 | Anomaly Tracker for Inhouse (#1162)

This commit is contained in:
Aishwarya Srivastava
2025-05-06 19:38:36 +05:30
committed by GitHub
23 changed files with 997 additions and 7 deletions

View File

@@ -0,0 +1,63 @@
import React, { useEffect } from 'react';
import Animated, {
SharedValue,
useAnimatedProps,
useSharedValue,
withTiming,
} from 'react-native-reanimated';
import Svg, { Circle, NumberArray } from 'react-native-svg';
import { COLORS } from '@rn-ui-lib/colors';
const AnimatedCircle = Animated.createAnimatedComponent(Circle);
interface AnimatedCircularLoaderIco {
radius: number;
circumference: number;
strokeWidth?: number;
strokeColor?: string;
centreX?: string | number;
centreY?: string | number;
origin?: NumberArray | SharedValue<NumberArray>;
progressPercent: number;
}
const AnimatedCircularLoaderIcon = (props: AnimatedCircularLoaderIco) => {
const {
radius,
circumference,
strokeWidth = 5,
strokeColor = '#FFEFDB',
centreX = '22',
centreY = '21',
origin = '22,21',
progressPercent,
} = props;
const progress = useSharedValue(0);
useEffect(() => {
const normalizedProgress = progressPercent / 100;
progress.value = withTiming(normalizedProgress, { duration: 400 });
}, []);
const circleAnimatedProps = useAnimatedProps(() => ({
strokeDashoffset: circumference * (1 - progress.value),
}));
return (
<Svg width="44px" height="42px" fill="none">
<Circle cx={centreX} cy={centreY} r={radius} stroke={strokeColor} strokeWidth={strokeWidth} />
<AnimatedCircle
cx={centreX}
cy={centreY}
r={radius}
stroke={COLORS.TEXT.YELLOW}
strokeWidth={strokeWidth}
strokeDasharray={circumference}
animatedProps={circleAnimatedProps}
rotation="-90"
origin={origin}
/>
</Svg>
);
};
export default AnimatedCircularLoaderIcon;

View File

@@ -0,0 +1,33 @@
import * as React from 'react';
import Svg, { Path, Rect } from 'react-native-svg';
const AnomalyTrackerIcon = () => (
<Svg width="18" height="19" viewBox="0 0 18 19" fill="none">
<Path
d="M13.8232 14.8232L17.332 18.332"
stroke="#C8A311"
stroke-width="1.6"
stroke-miterlimit="10"
/>
<Path
d="M1.21973 6.49058C2.36282 3.66302 5.13905 1.66602 8.37497 1.66602C12.6381 1.66602 16.0855 5.13093 16.0855 9.37654C16.0855 13.6222 12.6469 17.0783 8.37497 17.0783C5.35812 17.0783 2.73864 15.3388 1.47313 12.8096"
stroke="#C8A311"
stroke-width="1.6"
stroke-miterlimit="10"
/>
<Path
d="M0.666016 9.46345H3.56952C4.13093 9.46345 4.64847 9.13012 4.9204 8.6038L5.70988 7.11257C5.99935 6.55994 6.75374 6.67398 6.89409 7.29679L8.01689 12.1915C8.15724 12.8143 8.93795 12.9284 9.20988 12.3494L10.5081 9.63889"
stroke="#C8A311"
stroke-width="1.6"
stroke-miterlimit="10"
/>
<Path
d="M10.9832 9.7536C11.5797 9.7536 12.0622 9.27114 12.0622 8.67465C12.0622 8.07816 11.5797 7.5957 10.9832 7.5957C10.3868 7.5957 9.9043 8.07816 9.9043 8.67465C9.9043 9.27114 10.3868 9.7536 10.9832 9.7536Z"
stroke="#C8A311"
stroke-width="1.6"
stroke-miterlimit="10"
/>
</Svg>
);
export default AnomalyTrackerIcon;

View File

@@ -0,0 +1,112 @@
import * as React from 'react';
import Svg, { Path } from 'react-native-svg';
const NoAnomaliesFoundIcon = () => (
<Svg width="156" height="182" viewBox="0 0 156 182" fill="none">
<Path
d="M102.453 56.7171L28.559 59.9935C28.0106 60.0216 27.5747 60.4856 27.6029 61.0481L32.0323 161.111C32.0604 161.659 32.5244 162.095 33.0869 162.067L106.981 158.791C107.529 158.763 107.965 158.299 107.937 157.736L103.508 57.6733C103.48 57.1249 103.016 56.689 102.453 56.7171Z"
fill="#DBDBDB"
stroke="black"
stroke-width="0.321519"
stroke-linecap="round"
stroke-linejoin="round"
/>
<Path
d="M113.942 51.2875H39.9773C39.4148 51.2875 38.9648 51.7375 38.9648 52.2999V152.461C38.9648 153.024 39.4148 153.474 39.9773 153.474H113.956C114.518 153.474 114.968 153.024 114.968 152.461V52.2859C114.968 51.7234 114.518 51.2734 113.956 51.2734L113.942 51.2875Z"
fill="white"
stroke="black"
stroke-width="0.321519"
stroke-linecap="round"
stroke-linejoin="round"
/>
<Path
d="M49.6235 72.9434H87.7447V79.3695H49.6235V72.9434Z"
fill="#DBDBDB"
stroke="black"
stroke-width="0.321519"
stroke-linecap="round"
stroke-linejoin="round"
/>
<Path
d="M49.6094 88.6377H106.32V95.0639H49.6094V88.6377Z"
fill="#DBDBDB"
stroke="black"
stroke-width="0.321519"
stroke-linecap="round"
stroke-linejoin="round"
/>
<Path
d="M121.774 135.082L117.133 138.612L124.755 148.652L129.395 145.122L121.774 135.082Z"
fill="#545454"
stroke="black"
stroke-width="0.321519"
stroke-linecap="round"
stroke-linejoin="round"
/>
<Path
d="M122.617 134.548L117.977 138.077L125.598 148.117L130.239 144.588L122.617 134.548Z"
fill="white"
stroke="black"
stroke-width="0.321519"
stroke-linecap="round"
stroke-linejoin="round"
/>
<Path
opacity="0.3"
d="M127.553 141.41L123.264 144.672L125.542 147.682L129.831 144.419L127.553 141.41Z"
fill="#12173D"
/>
<Path
opacity="0.3"
d="M126.513 97.6774C116.262 84.1782 97.0252 81.5346 83.526 91.7856C70.0268 102.037 67.3832 121.273 77.6342 134.772C87.8851 148.271 107.121 150.915 120.621 140.664C134.12 130.413 136.763 111.163 126.513 97.6774ZM117.246 136.22C106.207 144.601 90.4725 142.45 82.0777 131.411C73.6969 120.373 75.8484 104.624 86.9008 96.2431C97.9392 87.8624 113.688 90.0138 122.069 101.066C130.45 112.105 128.284 127.854 117.246 136.234V136.22Z"
fill="#12173D"
/>
<Path
d="M119.594 134.237C108.556 142.618 92.8206 140.466 84.4258 129.428C76.045 118.39 78.1965 102.641 89.2489 94.2598C100.287 85.879 116.036 88.0305 124.417 99.0829C132.798 110.121 130.632 125.87 119.594 134.251V134.237Z"
fill="white"
fill-opacity="0.6"
/>
<Path
d="M128.861 95.695C118.61 82.1958 99.3734 79.5522 85.8742 89.8031C72.375 100.054 69.7314 119.29 79.9823 132.79C90.2333 146.289 109.47 148.932 122.969 138.681C136.468 128.431 139.112 109.194 128.861 95.695ZM119.594 134.238C108.556 142.619 92.8206 140.467 84.4258 129.429C76.0451 118.39 78.1965 102.641 89.249 94.2607C100.287 85.8799 116.036 88.0314 124.417 99.0838C132.798 110.122 130.632 125.871 119.594 134.252V134.238Z"
fill="#E6F1FF"
stroke="black"
stroke-width="0.321519"
stroke-linecap="round"
stroke-linejoin="round"
/>
<Path
d="M122.84 151.634C121.912 150.382 122.179 148.625 123.417 147.697L128.577 143.844C129.829 142.916 131.586 143.169 132.514 144.42L153.565 172.783C154.493 174.034 154.226 175.792 152.988 176.72L147.828 180.573C146.576 181.501 144.818 181.248 143.89 179.996L122.84 151.634Z"
fill="#545454"
stroke="black"
stroke-width="0.321519"
stroke-linecap="round"
stroke-linejoin="round"
/>
<Path
d="M123.559 151.084C122.631 149.833 122.898 148.075 124.136 147.147L129.296 143.294C130.548 142.366 132.306 142.619 133.234 143.87L154.284 172.233C155.212 173.484 154.945 175.242 153.707 176.17L148.547 180.023C147.295 180.951 145.538 180.698 144.61 179.446L123.559 151.084Z"
fill="white"
stroke="black"
stroke-width="0.321519"
stroke-linecap="round"
stroke-linejoin="round"
/>
<Path
d="M19.7173 176.142H9.14314"
stroke="#1C1C1C"
stroke-width="0.285714"
stroke-linejoin="round"
/>
<Path
d="M58.4912 176.142H26.7688"
stroke="#1C1C1C"
stroke-width="0.285714"
stroke-linejoin="round"
/>
<Path
d="M134.857 63.5703H153.46"
stroke="#1C1C1C"
stroke-width="0.228571"
stroke-linejoin="round"
/>
</Svg>
);
export default NoAnomaliesFoundIcon;

View File

@@ -0,0 +1,18 @@
import * as React from 'react';
import Svg, { G, Mask, Path, Rect } from 'react-native-svg';
const UpdateIcon = () => (
<Svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<Mask id="mask0_6963_151737" maskUnits="userSpaceOnUse" x="0" y="0" width="16" height="16">
<Rect width="16" height="16" fill="#D9D9D9" />
</Mask>
<G mask="url(#mask0_6963_151737)">
<Path
d="M12.8667 5.94967L10.0333 3.14967L10.9667 2.21634C11.2222 1.96079 11.5362 1.83301 11.9087 1.83301C12.2807 1.83301 12.5944 1.96079 12.85 2.21634L13.7833 3.14967C14.0389 3.40523 14.1722 3.71367 14.1833 4.07501C14.1944 4.4359 14.0722 4.74412 13.8167 4.99967L12.8667 5.94967ZM2.66667 13.9997C2.47778 13.9997 2.31956 13.9357 2.192 13.8077C2.064 13.6801 2 13.5219 2 13.333V11.4497C2 11.3608 2.01667 11.2748 2.05 11.1917C2.08333 11.1081 2.13333 11.033 2.2 10.9663L9.06667 4.09967L11.9 6.93301L5.03333 13.7997C4.96667 13.8663 4.89178 13.9163 4.80867 13.9497C4.72511 13.983 4.63889 13.9997 4.55 13.9997H2.66667Z"
fill="#0276FE"
/>
</G>
</Svg>
);
export default UpdateIcon;

View File

@@ -119,10 +119,12 @@ export enum ApiKeys {
GET_FEEDBACK_ADDRESSES = 'GET_FEEDBACK_ADDRESSES',
GET_TRAINING_MATERIAL_LIST = 'GET_TRAINING_MATERIAL_LIST',
GET_TRAINING_MATERIAL_DETAILS = 'GET_TRAINING_MATERIAL_DETAILS',
SELF_CALL_ACK= '/api/v1/self-call',
GET_CASES_GEOLOCATION_DISTANCE_FROM_AGENT_LOCATION = "GET_CASES_GEOLOCATION_DISTANCE_FROM_AGENT_LOCATION",
SELF_CALL_ACK = '/api/v1/self-call',
GET_CASES_GEOLOCATION_DISTANCE_FROM_AGENT_LOCATION = 'GET_CASES_GEOLOCATION_DISTANCE_FROM_AGENT_LOCATION',
GET_PRE_SIGNED_URL_FOR_FEEDBACK_IMAGE = 'GET_PRE_SIGNED_URL_FOR_FEEDBACK_IMAGE',
FEEDBACK_ORIGINAL_IMAGE_ACK = 'FEEDBACK_ORIGINAL_IMAGE_ACK',
GET_ANOMALY_DETAILS = 'GET_ANOMALY_DETAILS',
GET_ANOMALY_ACTIVITY_LOG = 'GET_ANOMALY_ACTIVITY_LOG',
}
export const API_URLS: Record<ApiKeys, string> = {} as Record<ApiKeys, string>;
@@ -231,9 +233,12 @@ API_URLS[ApiKeys.GET_TRAINING_MATERIAL_DETAILS] = '/training-page/{docRefId}';
API_URLS[ApiKeys.SELF_CALL_ACK] = '/sync-data/self-call-metadata';
API_URLS[ApiKeys.GET_TOP_ADDRESSES] = '/collection-cases/unified-locations';
API_URLS[ApiKeys.GET_FEEDBACK_ADDRESSES] = '/collection-cases/unified-locations/lite';
API_URLS[ApiKeys.GET_CASES_GEOLOCATION_DISTANCE_FROM_AGENT_LOCATION] = '/geolocation-distance/single-source'
API_URLS[ApiKeys.GET_CASES_GEOLOCATION_DISTANCE_FROM_AGENT_LOCATION] =
'/geolocation-distance/single-source';
API_URLS[ApiKeys.GET_PRE_SIGNED_URL_FOR_FEEDBACK_IMAGE] = '/file-upload/presigned-url';
API_URLS[ApiKeys.FEEDBACK_ORIGINAL_IMAGE_ACK] = '/file-upload/acknowledge';
API_URLS[ApiKeys.GET_ANOMALY_DETAILS] = '/anomaly-tracker';
API_URLS[ApiKeys.GET_ANOMALY_ACTIVITY_LOG] = '/anomaly-tracker/activity-logs/{anomalyReferenceId}';
export const API_STATUS_CODE = {
OK: 200,
@@ -364,7 +369,7 @@ axiosInstance.interceptors.response.use(
const url = response?.config?.url;
const apiKey = getKeyByValue(url, API_URLS);
sendApiToClickstreamEvent(response, milliseconds, false);
if(apiKey) {
if (apiKey) {
logger({
msg: 'api response error',
extras: {
@@ -374,7 +379,7 @@ axiosInstance.interceptors.response.use(
endpoint: API_URLS[apiKey as keyof typeof API_URLS],
method: response?.config?.method || '',
},
type: 'error'
type: 'error',
});
}
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_API_FAILED, {

View File

@@ -0,0 +1,49 @@
import { createSlice } from '@reduxjs/toolkit';
const initialState = {
openAnomaliesList: {},
closedAnomaliesList: {},
anomalyDetailsLoading: false,
anomalyDetails: {
data: {},
loading: false,
},
actionLoading: false,
acitvityLogs: {
data: [],
loading: false,
},
};
const anomalyTrackerSlice = createSlice({
name: 'anomalyTracker',
initialState,
reducers: {
setOpenAnomalyList: (state, action) => {
state.openAnomaliesList = action.payload;
},
setClosedAnomalyList: (state, action) => {
state.closedAnomaliesList = action.payload;
},
setAnomalyDetailsLoading: (state, action) => {
state.anomalyDetailsLoading = action.payload;
},
setActivityLogs: (state, action) => {
state.acitvityLogs.data = action.payload;
},
setActivityLogsLoading: (state, action) => {
state.acitvityLogs.loading = action.payload;
},
},
});
export const {
setOpenAnomalyList,
setClosedAnomalyList,
setAnomalyDetailsLoading,
setRcaReasons,
setActivityLogs,
setActivityLogsLoading,
} = anomalyTrackerSlice.actions;
export default anomalyTrackerSlice.reducer;

View File

@@ -0,0 +1,105 @@
import React from 'react';
import { View, StyleSheet, ActivityIndicator, FlatList } from 'react-native';
import AnomalyDetailsItem from './AnomalyDetailsItem';
import { useAppSelector } from '@hooks';
import { AnomalyType } from './constants';
import { GenericStyles } from '@rn-ui-lib/styles';
import { COLORS } from '@rn-ui-lib/colors';
import Text from '@rn-ui-lib/components/Text';
import NoAnomaliesFoundIcon from '@assets/icons/NoAnomaliesFoundIcon';
import Heading from '@rn-ui-lib/components/Heading';
interface IAnomaliesDetailsListProps {
anomalyType: string;
}
const AnomaliesDetailsList = ({ anomalyType }: IAnomaliesDetailsListProps) => {
const openAnomaliesData = useAppSelector(
(state) => state?.anomalyTracker?.openAnomaliesList?.data || []
);
const closedAnomaliesData = useAppSelector(
(state) => state?.anomalyTracker?.closedAnomaliesList?.data || []
);
const isLoading = useAppSelector((state) => state?.anomalyTracker?.anomalyDetailsLoading);
if (isLoading) {
return (
<View style={[GenericStyles.fill, GenericStyles.centerAlignedRow]}>
<ActivityIndicator size="large" color={COLORS.BASE.BLUE} />
</View>
);
}
if (anomalyType === AnomalyType.OPEN) {
return (
<>
{openAnomaliesData?.length > 0 ? (
<FlatList
data={openAnomaliesData}
keyExtractor={(item) => item?.anomalyId}
renderItem={({ item }) => <AnomalyDetailsItem item={item} anomalyType={anomalyType} />}
contentContainerStyle={[GenericStyles.pb16]}
/>
) : (
<View style={[GenericStyles.w100, styles.centerAbsolute]}>
<NoAnomaliesFoundIcon />
<View style={[GenericStyles.mt12, styles.text]}>
<Heading
dark
bold
type="h3"
style={[GenericStyles.mt16, GenericStyles.centerAlignedText]}
>
No open issues
</Heading>
<Text small dark style={[GenericStyles.centerAlignedText, GenericStyles.fontSize12]}>
You have closed all the issues
</Text>
</View>
</View>
)}
</>
);
}
return (
<>
{closedAnomaliesData?.length > 0 ? (
<FlatList
data={closedAnomaliesData}
keyExtractor={(item) => item?.anomalyId}
renderItem={({ item }) => <AnomalyDetailsItem item={item} anomalyType={anomalyType} />}
contentContainerStyle={[GenericStyles.pb16]}
/>
) : (
<View style={[GenericStyles.w100, styles.centerAbsolute]}>
<NoAnomaliesFoundIcon />
<View style={[GenericStyles.mt12, styles.text]}>
<Heading
dark
bold
type="h3"
style={[GenericStyles.mt16, GenericStyles.centerAlignedText]}
>
No closed issues
</Heading>
<Text small dark style={[GenericStyles.centerAlignedText, GenericStyles.fontSize12]}>
Closed issues are shown here
</Text>
</View>
</View>
)}
</>
);
};
const styles = StyleSheet.create({
centerAbsolute: {
marginTop: 160,
justifyContent: 'center',
alignItems: 'center',
},
text: {
width: '100%',
},
});
export default AnomaliesDetailsList;

View File

@@ -0,0 +1,30 @@
import React from 'react';
import { View, StyleSheet } from 'react-native';
import SingleAnomalyDetails from './SingleAnomalyDetails';
import RcaEtaContainer from './RcaEtaContainer';
import { AnomalyItem } from './interfaces';
interface IAnomalyCardBodyProps {
item: AnomalyItem;
anomalyType: string;
}
const AnomalyCardBody = (props: IAnomalyCardBodyProps) => {
const { item, anomalyType } = props;
return (
<View style={styles.container}>
<SingleAnomalyDetails item={item} anomalyType={anomalyType} />
<RcaEtaContainer item={item} anomalyType={anomalyType} />
</View>
);
};
const styles = StyleSheet.create({
container: {
borderBottomRightRadius: 6,
borderBottomLeftRadius: 6,
paddingHorizontal: 16,
paddingTop: 20,
paddingBottom: 16,
},
});
export default AnomalyCardBody;

View File

@@ -0,0 +1,60 @@
import { COLORS } from '@rn-ui-lib/colors';
import Text from '@rn-ui-lib/components/Text';
import React from 'react';
import { View, StyleSheet } from 'react-native';
import { AnomalyCardHeaderColor, AnomalyPriorityColor } from './constants';
import { AnomalyCardHeaderProps } from './interfaces';
const AnomalyCardHeader = (props: AnomalyCardHeaderProps) => {
const { item } = props;
const headerColor =
AnomalyCardHeaderColor[item?.priority as keyof typeof AnomalyCardHeaderColor] ||
COLORS.BACKGROUND.YELLOW_LIGHT;
const labelColor =
AnomalyPriorityColor[item?.priority as keyof typeof AnomalyPriorityColor] ||
COLORS.TEXT.YELLOW_LIGHT;
return (
<View style={[styles.container, { backgroundColor: headerColor }]}>
<Text style={styles.text} dark>
{item?.subtype}
</Text>
<View
style={[
styles.priorityLabel,
{
backgroundColor: labelColor,
},
]}
>
<Text style={styles.priorityText}>{item?.priority}</Text>
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
borderTopLeftRadius: 8,
borderTopRightRadius: 8,
paddingHorizontal: 16,
paddingVertical: 12,
alignItems: 'flex-start',
gap: 10,
},
priorityLabel: {
borderRadius: 4,
paddingHorizontal: 8,
paddingVertical: 1,
},
text: {
fontSize: 14,
fontWeight: '700',
color: COLORS.TEXT.DARK,
flex: 1,
wordBreak: 'break-word',
},
priorityText: { fontSize: 12, color: COLORS.TEXT.WHITE },
});
export default AnomalyCardHeader;

View File

@@ -0,0 +1,31 @@
import { COLORS } from '@rn-ui-lib/colors';
import { getShadowStyle } from '@rn-ui-lib/styles';
import React from 'react';
import { View, StyleSheet } from 'react-native';
import AnomalyCardBody from './AnomalyCardBody';
import AnomalyCardHeader from './AnomalyCardHeader';
import { AnomalyItem } from './interfaces';
interface IAnomalyDetailsItemProps {
anomalyType: string;
item: AnomalyItem;
}
const AnomalyDetailsItem = (props: IAnomalyDetailsItemProps) => {
const { anomalyType, item } = props;
return (
<View style={styles.container}>
<AnomalyCardHeader item={item} />
<AnomalyCardBody item={item} anomalyType={anomalyType} />
</View>
);
};
const styles = StyleSheet.create({
container: {
backgroundColor: COLORS.BACKGROUND.PRIMARY,
borderRadius: 8,
marginHorizontal: 16,
marginTop: 16,
...getShadowStyle(2),
},
});
export default AnomalyDetailsItem;

View File

@@ -0,0 +1,65 @@
import React from 'react';
import { Pressable, View, StyleSheet } from 'react-native';
import Text from '@rn-ui-lib/components/Text';
import AnomalyIcon from '@assets/icons/AnomalyIcon';
import { navigateToScreen } from '@components/utlis/navigationUtlis';
import { PageRouteEnum } from '@screens/auth/ProtectedRouter';
import { CaseDetailStackEnum } from '@screens/caseDetails/CaseDetailStack';
import { GenericStyles, getShadowStyle } from '@rn-ui-lib/styles';
import { COLORS } from '@rn-ui-lib/colors';
import Chevron from '@rn-ui-lib/icons/Chevron';
import { useAppSelector } from '@hooks';
const AnomalyOverviewCard = () => {
const totalOpenAnomalies =
useAppSelector((state) => state?.anomalyTracker?.openAnomaliesList?.pages?.totalElements);
const onClick = () => {
navigateToScreen(PageRouteEnum.CASE_DETAIL_STACK, {
screen: CaseDetailStackEnum.ANOMALY_TRACKER,
});
};
return (
<View style={styles.container}>
<View style={styles.textContainer}>
<AnomalyIcon />
<Text style={styles.text}>{totalOpenAnomalies} Open issues</Text>
</View>
<Pressable onPress={onClick} style={styles.textContainer}>
<Text style={styles.buttonText}>View</Text>
<View style={styles.rightIcon}>
<Chevron fillColor={COLORS.TEXT.BLUE} />
</View>
</Pressable>
</View>
);
};
const styles = StyleSheet.create({
container: {
display: 'flex',
flexDirection: 'row',
borderRadius: 4,
marginHorizontal: 16,
...getShadowStyle(2),
backgroundColor: COLORS.BACKGROUND.ORANGE,
borderWidth: 1,
borderColor: COLORS.BORDER.ORANGE,
marginBottom: 16,
padding: 12,
justifyContent: 'space-between',
},
textContainer: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
gap: 6,
},
rightIcon: {
marginTop: 3,
},
text: { fontSize: 13, fontWeight: '600' },
buttonText: { color: COLORS.TEXT.BLUE, fontWeight: '600', fontSize: 13 },
});
export default AnomalyOverviewCard;

View File

@@ -0,0 +1,43 @@
import React, { useEffect, useState } from 'react';
import { anomalyPageTitle, AnomalyType, TABS } from './constants';
import { GenericStyles, getShadowStyle } from '@rn-ui-lib/styles';
import NavigationHeader from '@rn-ui-lib/components/NavigationHeader';
import CustomTabs from '@rn-ui-lib/components/customTabs/CustomTabs';
import { goBack } from '@components/utlis/navigationUtlis';
import AnomaliesDetailsList from './AnomaliesDetailsList';
import { useAppDispatch } from '@hooks';
import { getAnomalyDetails } from './utils';
interface IAnomalyTracker {}
const AnomalyTracker: React.FC<IAnomalyTracker> = (props: IAnomalyTracker) => {
const [currentTab, setCurrentTab] = useState<string>(AnomalyType.OPEN);
const dispatch = useAppDispatch();
useEffect(() => {
dispatch(getAnomalyDetails(AnomalyType.OPEN, {}, true));
});
const handleTabChange = (tab: string) => {
if (tab === currentTab) return;
if (tab === AnomalyType.OPEN) {
dispatch(getAnomalyDetails(AnomalyType.OPEN, {}, true));
} else {
dispatch(getAnomalyDetails(AnomalyType.RESOLVED, {}, true));
}
setCurrentTab(tab);
};
return (
<>
<NavigationHeader title={anomalyPageTitle} onBack={goBack} />
<CustomTabs
tabs={TABS}
currentTab={currentTab}
onTabChange={handleTabChange}
containerStyle={[getShadowStyle(2), GenericStyles.pt12]}
/>
<AnomaliesDetailsList anomalyType={currentTab} />
</>
);
};
export default AnomalyTracker;

View File

@@ -0,0 +1,40 @@
import React from 'react';
import { View, StyleSheet } from 'react-native';
import Text from '@rn-ui-lib/components/Text';
import { COLORS } from '@rn-ui-lib/colors';
import AnimatedCircularLoaderIcon from '@assets/icons/AnimatedCircularLoaderIcon';
const DaysTillEscalationComponent = (props: {
dayTillEscalation: number;
progressPercent: number;
}) => {
const { dayTillEscalation, progressPercent } = props;
const radius = 18;
const circumference = 2 * Math.PI * radius;
return (
<>
<AnimatedCircularLoaderIcon
radius={radius}
circumference={circumference}
progressPercent={dayTillEscalation > 0 ? progressPercent : 0}
/>
<View
style={{
position: 'absolute',
transform: [{ translateX: 82 }],
}}
>
{dayTillEscalation > 0 ? (
<Text style={styles.textContainer}>{dayTillEscalation}</Text>
) : (
<Text style={styles.textContainer}>0</Text>
)}
</View>
</>
);
};
const styles = StyleSheet.create({
textContainer: { fontSize: 14, color: COLORS.TEXT.YELLOW, fontWeight: '700' },
});
export default DaysTillEscalationComponent;

View File

@@ -0,0 +1,47 @@
import { COLORS } from '@rn-ui-lib/colors';
import Text from '@rn-ui-lib/components/Text';
import React from 'react';
import { View, StyleSheet, Pressable } from 'react-native';
import Chevron from '@rn-ui-lib/icons/Chevron';
import { AnomalyType } from './constants';
interface IRcaEtaActionRowProps {
anomalyType: string;
label: string;
onClick: () => void;
status: string;
statusColor: { color: string };
buttonLabel: string;
}
const RcaEtaActionRow = (props: IRcaEtaActionRowProps) => {
const { anomalyType, label, onClick, status, statusColor, buttonLabel } = props;
return (
<View style={styles.row}>
<View style={styles.label}>
<Text>{label} </Text>
{anomalyType === AnomalyType.OPEN ? <Text style={statusColor}>{status}</Text> : null}
</View>
<Pressable onPress={onClick} style={styles.label}>
<Text style={styles.buttonLabel}>{buttonLabel}</Text>
<View style={styles.rightIcon}>
<Chevron fillColor={COLORS.TEXT.BLUE} />
</View>
</Pressable>
</View>
);
};
const styles = StyleSheet.create({
row: {
padding: 8,
justifyContent: 'space-between',
display: 'flex',
flexDirection: 'row',
},
label: { display: 'flex', flexDirection: 'row', gap: 4 },
rightIcon: {
marginTop: 9,
},
buttonLabel: { color: COLORS.TEXT.BLUE, fontWeight: '600', fontSize: 13 },
});
export default RcaEtaActionRow;

View File

@@ -0,0 +1,94 @@
import { COLORS } from '@rn-ui-lib/colors';
import React, { useEffect, useState } from 'react';
import { View, StyleSheet } from 'react-native';
import BottomSheetWrapper from '@common/BottomSheetWrapper';
import { AnomalyType } from './constants';
import { useAppDispatch } from '@hooks';
import { getActivityLogs } from './utils';
import dayjs from 'dayjs';
import { BUSINESS_DATE_FORMAT } from '@rn-ui-lib/utils/dates';
import RcaEtaActionRow from './RcaEtaActionRow';
import BottomsheetHeader from './BottomsheetHeader';
import ViewRcaEtaDetails from './ViewRcaEtaDetails';
import { AnomalyItem } from './interfaces';
interface IRcaEtaContainerProps {
item: AnomalyItem;
anomalyType: string;
}
const RcaEtaContainer = (props: IRcaEtaContainerProps) => {
const { item, anomalyType } = props;
const [showETABottomSheet, setShowETABottomSheet] = useState(false);
const [showRCABottomSheet, setShowRCABottomSheet] = useState(false);
const [selectedAnomalyId, setSelectedAnomalyId] = useState('');
const dispatch = useAppDispatch();
const onETAClick = () => {
if (!(item?.estimatedTime || anomalyType === AnomalyType.RESOLVED)) return; //TODO - Redirect to fill ETA form
setSelectedAnomalyId(item?.anomalyReferenceId as string);
setShowETABottomSheet(true);
};
const onRCAClick = () => {
if (!(item?.rca || anomalyType === AnomalyType.RESOLVED)) return; //TODO - Redirect to fill ETA form
setSelectedAnomalyId(item?.anomalyReferenceId as string);
setShowRCABottomSheet(true);
};
const etaStatus = item?.estimatedTime
? dayjs(item?.estimatedTime?.at)?.format(BUSINESS_DATE_FORMAT)
: 'Unfilled';
const handleClose = () => {
setShowRCABottomSheet(false);
setShowETABottomSheet(false);
setSelectedAnomalyId('');
};
useEffect(() => {
if (selectedAnomalyId) dispatch(getActivityLogs(selectedAnomalyId));
}, [selectedAnomalyId]);
return (
<View style={styles.container}>
<RcaEtaActionRow
anomalyType={anomalyType}
label={'RCA'}
onClick={onRCAClick}
status={item?.rca ? 'Filled' : 'Unfilled'}
statusColor={{ color: item?.rca ? COLORS.TEXT.GREEN : COLORS.TEXT.RED }}
buttonLabel={item?.rca || anomalyType === AnomalyType.RESOLVED ? 'View' : 'Fill'}
/>
<View style={styles.line} />
<RcaEtaActionRow
anomalyType={anomalyType}
label={'ETA'}
onClick={onETAClick}
status={etaStatus}
statusColor={{ color: item?.estimatedTime ? COLORS.TEXT.GREEN : COLORS.TEXT.RED }}
buttonLabel={item?.estimatedTime || anomalyType === AnomalyType.RESOLVED ? 'View' : 'Fill'}
/>
<BottomSheetWrapper
visible={showETABottomSheet || showRCABottomSheet}
HeaderNode={() => (
<BottomsheetHeader title={showRCABottomSheet ? 'RCA' : 'ETA'} handleClose={handleClose} />
)}
allowBackdropClose={true}
setVisible={handleClose}
heightPercentage={showETABottomSheet ? 26 : 30}
>
<ViewRcaEtaDetails showETABottomSheet={showETABottomSheet} />
</BottomSheetWrapper>
</View>
);
};
const styles = StyleSheet.create({
container: {
borderRadius: 8,
backgroundColor: COLORS.BACKGROUND.LIGHT_GREY,
},
line: {
height: 2,
backgroundColor: COLORS.BORDER.PRIMARY,
},
});
export default RcaEtaContainer;

View File

@@ -0,0 +1,68 @@
import { COLORS } from '@rn-ui-lib/colors';
import Text from '@rn-ui-lib/components/Text';
import React from 'react';
import { View, StyleSheet } from 'react-native';
import { sanitizeString } from '@components/utlis/commonFunctions';
import { BUSINESS_DATE_FORMAT, dateFormat } from '@rn-ui-lib/utils/dates';
import { AnomalyType } from './constants';
import DaysTillEscalationComponent from './DaysTillEscalationComponent';
import { GenericStyles } from '@rn-ui-lib/styles';
import { AnomalyItem } from './interfaces';
interface ISingleAnomalyDetailsProps {
item: AnomalyItem;
anomalyType: string;
}
const SingleAnomalyDetails = (props: ISingleAnomalyDetailsProps) => {
const { item, anomalyType } = props;
return (
<View style={styles.container}>
<View>
<Text style={styles.textLabel}>Created on</Text>
<Text style={styles.date}>
{sanitizeString(dateFormat(new Date(item?.createdAt as string), BUSINESS_DATE_FORMAT))}
</Text>
</View>
{anomalyType === AnomalyType.OPEN ? (
<View style={styles.daysTillEscalationText}>
<Text style={[GenericStyles.mr4, styles.textLabel, styles.textAlign, styles.w60]}>Days till Escalation</Text>
<DaysTillEscalationComponent
dayTillEscalation={item?.daysRemaining as number}
progressPercent={item?.escalationPercentage as number}
/>
</View>
) : (
<View>
<Text style={[styles.textLabel, styles.textAlign]}>Resolved on</Text>
<Text style={[styles.date, styles.textAlign]}>
{sanitizeString(dateFormat(new Date(item?.resolution?.at), BUSINESS_DATE_FORMAT))}
</Text>
</View>
)}
</View>
);
};
const styles = StyleSheet.create({
container: {
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
paddingBottom: 16,
alignItems: 'center',
},
priorityLabel: {
borderRadius: 4,
backgroundColor: COLORS.TEXT.RED,
paddingHorizontal: 6,
paddingVertical: 1,
},
textLabel: { fontSize: 12, color: COLORS.TEXT.LIGHT },
date: { fontSize: 14, color: COLORS.TEXT.BLACK, fontWeight: '600' },
daysTillEscalationText: { display: 'flex', flexDirection: 'row', alignItems: 'center' },
textAlign: { textAlign: 'right' },
w60: { width: 60 },
});
export default SingleAnomalyDetails;

View File

@@ -0,0 +1,31 @@
import { COLORS } from '@rn-ui-lib/colors';
export enum AnomalyType {
OPEN = 'OPEN',
RESOLVED = 'RESOLVED',
}
export const TABS = [
{
key: AnomalyType.OPEN,
label: 'Open',
},
{
key: AnomalyType.RESOLVED,
label: 'Closed',
},
];
export const anomalyPageTitle = 'Your issues';
export const AnomalyPriorityColor = {
P0: COLORS.TEXT.RED,
P1: COLORS.TEXT.YELLOW,
P2: COLORS.TEXT.YELLOW_LIGHT,
};
export const AnomalyCardHeaderColor = {
P0: COLORS.BACKGROUND.RED,
P1: COLORS.BACKGROUND.ORANGE,
P2: COLORS.BACKGROUND.YELLOW_LIGHT,
};

View File

@@ -0,0 +1,21 @@
export interface AnomalyItem {
escalationPercentage?: number;
daysRemaining?: number;
anomalyId?: string;
createdAt?: string;
type?: string;
subtype?: string;
priority?: string;
anomalyDetectedOn?: string;
raisedFor?: {};
anomalyReferenceId?: string;
resolution?: {};
estimatedTime?: {};
rca?: {};
resolutionTime?: string;
resolutionType?: string;
}
export interface AnomalyCardHeaderProps {
item: AnomalyItem;
}

View File

@@ -0,0 +1,53 @@
import axiosInstance, { API_STATUS_CODE, ApiKeys, getApiUrl } from '@components/utlis/apiHelper';
import { AnomalyType } from './constants';
import { logError } from '@components/utlis/errorUtils';
import { AppDispatch } from '@store';
import {
setActivityLogs,
setActivityLogsLoading,
setAnomalyDetailsLoading,
setClosedAnomalyList,
setOpenAnomalyList,
} from '@reducers/anomalyTrackerSlice';
export const getAnomalyDetails =
(anomalyType: string, payload: {}, isLoading: boolean) => (dispatch: AppDispatch) => {
const url = getApiUrl(ApiKeys.GET_ANOMALY_DETAILS);
if (isLoading) dispatch(setAnomalyDetailsLoading(true));
axiosInstance
.get(url, {
params: { status: anomalyType },
})
.then((res) => {
if (res?.status === API_STATUS_CODE.OK) {
if (anomalyType === AnomalyType.OPEN) {
dispatch(setOpenAnomalyList(res?.data));
return;
}
dispatch(setClosedAnomalyList(res?.data));
}
})
.catch((err) => {
logError(err);
})
.finally(() => {
dispatch(setAnomalyDetailsLoading(false));
});
};
export const getActivityLogs = (anomalyReferenceId: string) => (dispatch: AppDispatch) => {
const url = getApiUrl(ApiKeys.GET_ANOMALY_ACTIVITY_LOG, { anomalyReferenceId });
dispatch(setActivityLogsLoading(true));
axiosInstance
.get(url)
.then((response) => {
if (response.status === API_STATUS_CODE.OK) {
dispatch(setActivityLogs(response?.data));
}
})
.catch((err) => {
logError(err);
})
.finally(() => dispatch(setActivityLogsLoading(false)));
};

View File

@@ -3,6 +3,7 @@ import React from 'react';
import InternalAgentPerformanceCard from './InternalAgentPerformanceCard';
import PerformanceMeter from './PerformanceMeter';
import PerformanceOverview from './PerformanceOverview';
import AnomalyOverviewCard from './AnomalyTracker/AnomalyOverviewCard';
const InternalAgentDashboard = () => {
const performanceData = useAppSelector((state) => state.agentPerformance.performanceData);
@@ -14,9 +15,16 @@ const InternalAgentDashboard = () => {
atleastOneEmiCollected: cases?.atleastOneEmiCollected,
totalEmi: cases?.totalEmi,
};
const totalOpenAnomalies =
useAppSelector((state) => state?.anomalyTracker?.openAnomaliesList?.pages?.totalElements) || 0;
const totalClosedAnomalies =
useAppSelector((state) => state?.anomalyTracker?.closedAnomaliesList?.pages?.totalElements) ||
0;
const isOpenOrClosedAnomaliesPresent = totalOpenAnomalies + totalClosedAnomalies > 0;
return (
<>
{isOpenOrClosedAnomaliesPresent ? <AnomalyOverviewCard /> : null}
<PerformanceOverview performanceOverviewData={internalAgentPerformanceOverview} />
<PerformanceMeter />
<InternalAgentPerformanceCard />

View File

@@ -16,6 +16,8 @@ import { addClickstreamEvent } from '../../services/clickstreamEventService';
import DashboardHeader from './DashboardHeader';
import ExternalAgentDashboard from './ExternalAgentDashboard';
import InternalAgentDashboard from './InternalAgentDashboard';
import { getAnomalyDetails } from './AnomalyTracker/utils';
import { AnomalyType } from './AnomalyTracker/constants';
const Dashboard = () => {
const [refreshing, setRefreshing] = React.useState(false);
@@ -27,7 +29,14 @@ const Dashboard = () => {
const fetchAgentPerformanceMetrics = () => {
setIsLoading(true);
dispatch(getPerformanceMetrics(Object.keys(caseDetailsIds ?? {}), isExternalAgent, setIsLoading));
dispatch(
getPerformanceMetrics(Object.keys(caseDetailsIds ?? {}), isExternalAgent, setIsLoading)
);
// TODO: Uncomment when api is added by backend
// if (!isExternalAgent) {
// dispatch(getAnomalyDetails(AnomalyType.OPEN, {}, false));
// dispatch(getAnomalyDetails(AnomalyType.RESOLVED, {}, false));
// }
};
useEffect(() => {

View File

@@ -31,6 +31,7 @@ import CallCustomer from './CallCustomer';
import TopAddresses from '@screens/addresses/topAddresses/TopAddresses';
import OtherAddresses from '@screens/addresses/otherAddresses/OtherAddresses';
import Escalations from '@screens/escalations/Escalations';
import AnomalyTracker from '@screens/Dashboard/AnomalyTracker/AnomlayTracker';
const Stack = createNativeStackNavigator();
@@ -59,6 +60,7 @@ export enum CaseDetailStackEnum {
TOP_ADDRESSES = 'TopAddresses',
OTHER_ADDRESSES = 'OtherAddresses',
ESCALATIONS = 'Escalations',
ANOMALY_TRACKER = "ANOMALY_TRACKER",
}
const CaseDetailStack = () => {
@@ -124,6 +126,7 @@ const CaseDetailStack = () => {
))}
<Stack.Screen name={CaseDetailStackEnum.TOP_ADDRESSES} component={TopAddresses} />
<Stack.Screen name={CaseDetailStackEnum.OTHER_ADDRESSES} component={OtherAddresses} />
<Stack.Screen name={CaseDetailStackEnum.ANOMALY_TRACKER} component={AnomalyTracker} />
</Stack.Navigator>
);
};

View File

@@ -40,6 +40,7 @@ import skipTracingAddressesSlice from '@reducers/skipTracingAddressesSlice';
import trainingMaterialSlice from '@reducers/trainingMaterialSlice';
import feedbackFormSlice from '@reducers/feedbackFormSlice';
import topAddressesSlice from '@reducers/topAddressesSlice';
import anomalyTrackerSlice from '@reducers/anomalyTrackerSlice';
const rootReducer = combineReducers({
case: caseReducer,
@@ -79,7 +80,8 @@ const rootReducer = combineReducers({
postOperationalHourRestrictionsSlice: postOperationalHourRestrictionsSlice,
trainingMaterial: trainingMaterialSlice,
feedbackForm: feedbackFormSlice,
topAddresses: topAddressesSlice
topAddresses: topAddressesSlice,
anomalyTracker: anomalyTrackerSlice,
});
const persistConfig = {