NTP-68615 | Go live anomaly tracker (#1187)

This commit is contained in:
Aishwarya Srivastava
2025-06-02 17:53:35 +05:30
committed by GitHub
parent 9ef129f749
commit 8e80b34cea
26 changed files with 1029 additions and 193 deletions

View File

@@ -1,33 +1,38 @@
import { COLORS } from '@rn-ui-lib/colors';
import { IconProps } from '@rn-ui-lib/icons/types';
import * as React from 'react';
import Svg, { Path, Rect } from 'react-native-svg';
import Svg, { Path } 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>
);
const AnomalyTrackerIcon: React.FC<IconProps> = (props) => {
const { fillColor = COLORS.TEXT.YELLOW_LIGHT, width = 32, height = 36 } = props;
return (
<Svg width="18" height="19" viewBox="0 0 18 19" fill="none">
<Path
d="M13.8232 14.8232L17.332 18.332"
stroke={fillColor}
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={fillColor}
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={fillColor}
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={fillColor}
stroke-width="1.6"
stroke-miterlimit="10"
/>
</Svg>
);
};
export default AnomalyTrackerIcon;

View File

@@ -0,0 +1,43 @@
import React from 'react';
import Svg, { ClipPath, Defs, G, Path, Rect } from 'react-native-svg';
const EscalatedAnomalyIcon = () => {
return (
<Svg width="20" height="20" viewBox="0 0 20 20" fill="none">
<G clip-Path="url(#clip0_7461_158159)">
<Path
d="M11.2716 1.49986C11.1517 1.26585 10.9695 1.06947 10.7452 0.932346C10.5209 0.795216 10.2631 0.722656 10.0001 0.722656C9.73722 0.722656 9.47939 0.795216 9.25506 0.932346C9.03073 1.06947 8.84859 1.26585 8.72872 1.49986L0.87157 17.2141C0.761985 17.4316 0.70981 17.6734 0.720002 17.9169C0.730193 18.1601 0.802413 18.3967 0.929803 18.6043C1.05719 18.8119 1.23552 18.9834 1.44786 19.1026C1.66019 19.2219 1.89948 19.2849 2.143 19.2856H17.8572C18.1008 19.2849 18.3401 19.2219 18.5524 19.1026C18.7648 18.9834 18.9431 18.8119 19.0705 18.6043C19.1978 18.3967 19.2701 18.1601 19.2802 17.9169C19.2905 17.6734 19.2382 17.4316 19.1287 17.2141L11.2716 1.49986Z"
fill="#F98600"
/>
<Path
d="M10 7.14258V11.7854"
stroke="#FEE7CC"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<Path
d="M10.0002 15.7143C9.80297 15.7143 9.64307 15.5544 9.64307 15.3571C9.64307 15.1599 9.80297 15 10.0002 15"
stroke="#FEE7CC"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<Path
d="M10 15.7143C10.1972 15.7143 10.3571 15.5544 10.3571 15.3571C10.3571 15.1599 10.1972 15 10 15"
stroke="#FEE7CC"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</G>
<Defs>
<ClipPath id="clip0_7461_158159">
<Rect width="20" height="20" fill="white" />
</ClipPath>
</Defs>
</Svg>
);
};
export default EscalatedAnomalyIcon;

158
src/common/FormInput.tsx Normal file
View File

@@ -0,0 +1,158 @@
import { GenericStyles } from '@rn-ui-lib/styles';
import React from 'react';
import { Control, Controller, ControllerRenderProps } from 'react-hook-form';
import { View, StyleSheet } from 'react-native';
import Text from '@rn-ui-lib/components/Text';
import {
BUSINESS_DATE_FORMAT,
CUSTOM_ISO_DATE_FORMAT,
dateFormat,
ISO_DATE_FORMAT,
} from '@rn-ui-lib/utils/dates';
import dayjs from 'dayjs';
import WebBasedDatePicker from '@rn-ui-lib/components/WebBasedDatePicker';
import ErrorMessage from '@components/form/components/ErrorMessage';
import { AnswerType } from '@components/form/interface';
import RadioGroup from '@rn-ui-lib/components/radio_button/RadioGroup';
import { IOption, IEtaFormData, IRcaFormData } from '@screens/Dashboard/AnomalyTracker/interfaces';
import TextInput from '@rn-ui-lib/components/TextInput';
import { COLORS } from '@rn-ui-lib/colors';
import RNRadioButton from '@rn-ui-lib/components/radio_button/RadioButton';
interface IFormInputProps {
control: Control<IEtaFormData | IRcaFormData, any>;
question: {
text: string;
type: AnswerType;
};
maxDate?: string;
name: 'at' | 'comment' | 'reason';
isQuestionMandatory?: boolean;
placeholder?: string;
keyboardType?: 'default' | 'number-pad' | 'email-address' | 'phone-pad';
answerOptions?: IOption[];
rules?: {};
maxLength?: number;
}
const FormInput = (props: IFormInputProps) => {
const {
control,
maxDate,
question,
name,
isQuestionMandatory = false,
placeholder,
keyboardType = 'default',
answerOptions,
rules,
maxLength = 300,
} = props;
const todaysDate = dateFormat(new Date(), ISO_DATE_FORMAT);
const renderInput = (
field: ControllerRenderProps<IEtaFormData | IRcaFormData, 'at' | 'comment' | 'reason'>,
hasError: boolean
) => {
const { onChange, value, onBlur } = field;
switch (question.type) {
case AnswerType.date:
let dateString = value;
if (dateString) {
const parsedDate = dayjs(dateString, CUSTOM_ISO_DATE_FORMAT, true);
dateString = parsedDate.format(ISO_DATE_FORMAT);
}
return (
<WebBasedDatePicker
displayFormat={BUSINESS_DATE_FORMAT}
containerStyle={styles.inputContainerStyle}
type="date"
onChange={onChange}
outputFormat={ISO_DATE_FORMAT}
min={todaysDate}
max={maxDate}
/>
);
case AnswerType.option:
return (
<RadioGroup value={value} onValueChange={onChange} orientation="vertical">
{answerOptions?.map((option: IOption) => {
return (
<RNRadioButton
key={option.value}
id={option.value}
value={option.label}
containerStyle={GenericStyles.containerStyle}
textStyle={GenericStyles.mr16}
/>
);
})}
</RadioGroup>
);
case AnswerType.text:
return (
<TextInput
style={GenericStyles.fill}
value={value}
maxLength={maxLength}
onChangeText={onChange}
keyboardType={keyboardType}
placeholder={placeholder || 'Enter here'}
onBlur={onBlur}
error={hasError}
/>
);
default:
return (
<TextInput
style={GenericStyles.fill}
value={value}
maxLength={maxLength}
onChangeText={onChange}
keyboardType={keyboardType}
placeholder={placeholder || 'Enter here'}
onBlur={onBlur}
error={hasError}
/>
);
}
};
return (
<View style={[GenericStyles.ph16, GenericStyles.mt20]}>
<Text dark bold style={GenericStyles.mb8}>
{question?.text}
{isQuestionMandatory && <Text style={GenericStyles.redText}>*</Text>}
</Text>
<Controller
control={control}
rules={rules}
name={name}
render={({ field, fieldState }) => (
<>
{renderInput(field, !!fieldState?.error)}
{fieldState?.error?.message ? (
<ErrorMessage show={{ message: fieldState.error.message }} />
) : null}
</>
)}
/>
</View>
);
};
const styles = StyleSheet.create({
inputContainerStyle: {
backgroundColor: COLORS.BACKGROUND.PRIMARY,
borderRadius: 8,
borderWidth: 1,
padding: 0,
borderColor: COLORS.BORDER.PRIMARY,
color: COLORS.TEXT.BLACK,
},
});
export default FormInput;

View File

@@ -126,6 +126,8 @@ export enum ApiKeys {
FEEDBACK_ORIGINAL_IMAGE_ACK = 'FEEDBACK_ORIGINAL_IMAGE_ACK',
GET_ANOMALY_DETAILS = 'GET_ANOMALY_DETAILS',
GET_ANOMALY_ACTIVITY_LOG = 'GET_ANOMALY_ACTIVITY_LOG',
GET_ANOMALY_RCA_QUESTION = 'GET_ANOMALY_RCA_QUESTION',
UPDATE_ANOMALY_ACTION = 'UPDATE_ANOMALY_ACTION',
}
export const API_URLS: Record<ApiKeys, string> = {} as Record<ApiKeys, string>;
@@ -241,6 +243,9 @@ API_URLS[ApiKeys.GET_PRE_SIGNED_URL_FOR_FEEDBACK_IMAGE] = '/file-upload/presigne
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}';
API_URLS[ApiKeys.UPDATE_ANOMALY_ACTION] = '/anomaly-tracker/action/{anomalyReferenceId}';
API_URLS[ApiKeys.GET_ANOMALY_RCA_QUESTION] =
'/anomaly-tracker/question-tree/rca/{anomalyReferenceId}';
export const API_STATUS_CODE = {
OK: 200,

View File

@@ -1,8 +1,44 @@
import { createSlice } from '@reduxjs/toolkit';
import { ActivityLog, AnomalyItem, AnomalyResponse, IOption } from '@screens/Dashboard/AnomalyTracker/interfaces';
const initialState = {
openAnomaliesList: {},
closedAnomaliesList: {},
interface AnomalyTrackerState {
openAnomaliesList: AnomalyResponse;
closedAnomaliesList: AnomalyResponse;
anomalyDetailsLoading: boolean;
anomalyDetails: {
data: AnomalyItem;
loading: boolean;
};
actionLoading: boolean;
acitvityLogs: {
data: ActivityLog[];
loading: boolean;
};
rcaReasons: Array<{
options: IOption[] | undefined; label: string; value: string
}>;
updatingForm: boolean;
}
const initialState: AnomalyTrackerState = {
openAnomaliesList: {
data: [],
pages: {
pageNo: 0,
totalPages: 0,
pageSize: 0,
totalElements: 0
}
},
closedAnomaliesList: {
data: [],
pages: {
pageNo: 0,
totalPages: 0,
pageSize: 0,
totalElements: 0
}
},
anomalyDetailsLoading: false,
anomalyDetails: {
data: {},
@@ -13,6 +49,8 @@ const initialState = {
data: [],
loading: false,
},
rcaReasons: [],
updatingForm: false,
};
const anomalyTrackerSlice = createSlice({
@@ -34,6 +72,12 @@ const anomalyTrackerSlice = createSlice({
setActivityLogsLoading: (state, action) => {
state.acitvityLogs.loading = action.payload;
},
setRcaReasons: (state, action) => {
state.rcaReasons = action.payload;
},
setUpdatingForm: (state, action) => {
state.updatingForm = action.payload;
},
},
});
@@ -44,6 +88,7 @@ export const {
setRcaReasons,
setActivityLogs,
setActivityLogsLoading,
setUpdatingForm,
} = anomalyTrackerSlice.actions;
export default anomalyTrackerSlice.reducer;

View File

@@ -76,6 +76,7 @@ export interface IUserSlice extends IUser {
fieldAgentPerformanceDashboardEnabled: boolean;
isCallRecordingCosmosExotelEnabled: boolean;
isCosmosDiallerEnabled: boolean;
enableFieldAppAnomalyTracker: boolean;
};
employeeId: string;
is1To30FieldAgent: boolean;
@@ -111,6 +112,7 @@ const initialState: IUserSlice = {
fieldAgentPerformanceDashboardEnabled: false,
isCallRecordingCosmosExotelEnabled: false,
isCosmosDiallerEnabled: false,
enableFieldAppAnomalyTracker: false,
},
employeeId: '',
is1To30FieldAgent: false,

View File

@@ -35,7 +35,7 @@ const AnomaliesDetailsList = ({ anomalyType }: IAnomaliesDetailsListProps) => {
{openAnomaliesData?.length > 0 ? (
<FlatList
data={openAnomaliesData}
keyExtractor={(item) => item?.anomalyId}
keyExtractor={(item) => item?.anomalyId as string}
renderItem={({ item }) => <AnomalyDetailsItem item={item} anomalyType={anomalyType} />}
contentContainerStyle={[GenericStyles.pb16]}
/>
@@ -65,7 +65,7 @@ const AnomaliesDetailsList = ({ anomalyType }: IAnomaliesDetailsListProps) => {
{closedAnomaliesData?.length > 0 ? (
<FlatList
data={closedAnomaliesData}
keyExtractor={(item) => item?.anomalyId}
keyExtractor={(item) => item?.anomalyId as string}
renderItem={({ item }) => <AnomalyDetailsItem item={item} anomalyType={anomalyType} />}
contentContainerStyle={[GenericStyles.pb16]}
/>

View File

@@ -5,34 +5,47 @@ 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 { 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 totalOpenAnomalies = useAppSelector(
(state) => state?.anomalyTracker?.openAnomaliesList?.pages?.totalElements
);
const onClick = () => {
navigateToScreen(PageRouteEnum.CASE_DETAIL_STACK, {
screen: CaseDetailStackEnum.ANOMALY_TRACKER,
});
};
const areAllIssuesResolved = totalOpenAnomalies === 0;
return (
<View style={styles.container}>
<Pressable
onPress={onClick}
style={[
styles.container,
areAllIssuesResolved ? styles.closedAnomalyState : styles.openAnomalyState,
]}
>
<View style={styles.textContainer}>
<AnomalyIcon />
<Text style={styles.text}>{totalOpenAnomalies} Open issues</Text>
<AnomalyIcon
fillColor={areAllIssuesResolved ? COLORS.TEXT.GREEN : COLORS.TEXT.YELLOW_LIGHT}
/>
<Text style={styles.text}>
{areAllIssuesResolved ? 'All issues are closed' : `${totalOpenAnomalies} Open issues`}
</Text>
</View>
<Pressable onPress={onClick} style={styles.textContainer}>
<View style={styles.textContainer}>
<Text style={styles.buttonText}>View</Text>
<View style={styles.rightIcon}>
<Chevron fillColor={COLORS.TEXT.BLUE} />
</View>
</Pressable>
</View>
</View>
</Pressable>
);
};
@@ -40,12 +53,10 @@ const styles = StyleSheet.create({
container: {
display: 'flex',
flexDirection: 'row',
borderRadius: 4,
borderRadius: 6,
marginHorizontal: 16,
...getShadowStyle(2),
backgroundColor: COLORS.BACKGROUND.ORANGE,
borderWidth: 1,
borderColor: COLORS.BORDER.ORANGE,
marginBottom: 16,
padding: 12,
justifyContent: 'space-between',
@@ -56,6 +67,14 @@ const styles = StyleSheet.create({
alignItems: 'center',
gap: 6,
},
openAnomalyState: {
backgroundColor: COLORS.BACKGROUND.ORANGE,
borderColor: COLORS.BORDER.ORANGE,
},
closedAnomalyState: {
backgroundColor: COLORS.BACKGROUND.GREEN,
borderColor: COLORS.BORDER.GREEN,
},
rightIcon: {
marginTop: 3,
},

View File

@@ -0,0 +1,103 @@
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,
setRcaReasons,
setUpdatingForm,
} from '@reducers/anomalyTrackerSlice';
import { IEtaFormPayload, IRcaFormPayload } from './interfaces';
import { toast } from '@rn-ui-lib/components/toast';
import { ToastMessages } from '@screens/allCases/constants';
export const getAnomalyDetails =
(anomalyType: string, 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) => {
toast({
text1: ToastMessages.GENERIC_ERROR_TOAST,
type: 'error',
});
})
.finally(() => dispatch(setActivityLogsLoading(false)));
};
export const getRcaReasons =
(
anomalyReferenceId: string,
setIsRcaReasonsLoading: React.Dispatch<React.SetStateAction<boolean>>
) =>
(dispatch: AppDispatch) => {
const url = getApiUrl(ApiKeys.GET_ANOMALY_RCA_QUESTION, { anomalyReferenceId });
axiosInstance
.get(url)
.then((response) => {
if (response?.status === API_STATUS_CODE.OK) {
dispatch(setRcaReasons(response?.data));
}
})
.catch((err) => {
logError(err);
})
.finally(() => setIsRcaReasonsLoading(false));
};
export const updateAnomaly =
(
anomalyReferenceId: string,
payload: IEtaFormPayload | IRcaFormPayload,
callbackFn: () => void
) =>
(dispatch: AppDispatch) => {
const url = getApiUrl(ApiKeys.UPDATE_ANOMALY_ACTION, { anomalyReferenceId });
dispatch(setUpdatingForm(true));
axiosInstance
.put(url, payload)
.then((response) => {
if (response.status === API_STATUS_CODE.OK) {
callbackFn?.();
}
})
.catch((err) => {
logError(err);
})
.finally(() => dispatch(setUpdatingForm(false)));
};

View File

@@ -6,24 +6,22 @@ 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';
import { getAnomalyDetails } from './AnomalyTrackerActions';
interface IAnomalyTracker {}
const AnomalyTracker: React.FC<IAnomalyTracker> = (props: IAnomalyTracker) => {
const AnomalyTracker = () => {
const [currentTab, setCurrentTab] = useState<string>(AnomalyType.OPEN);
const dispatch = useAppDispatch();
useEffect(() => {
dispatch(getAnomalyDetails(AnomalyType.OPEN, {}, true));
dispatch(getAnomalyDetails(AnomalyType.OPEN, true));
}, []);
const handleTabChange = (tab: string) => {
if (tab === currentTab) return;
if (tab === AnomalyType.OPEN) {
dispatch(getAnomalyDetails(AnomalyType.OPEN, {}, true));
dispatch(getAnomalyDetails(AnomalyType.OPEN, true));
} else {
dispatch(getAnomalyDetails(AnomalyType.RESOLVED, {}, true));
dispatch(getAnomalyDetails(AnomalyType.RESOLVED, true));
}
setCurrentTab(tab);
};
@@ -36,6 +34,7 @@ const AnomalyTracker: React.FC<IAnomalyTracker> = (props: IAnomalyTracker) => {
currentTab={currentTab}
onTabChange={handleTabChange}
containerStyle={[getShadowStyle(2), GenericStyles.pt12]}
tabContainerStyle={GenericStyles.ml16}
/>
<AnomaliesDetailsList anomalyType={currentTab} />
</>

View File

@@ -9,9 +9,11 @@ import UpdateIcon from '@assets/icons/UpdateIcon';
interface IBottomsheetHeaderProps {
title: string;
handleClose: () => void;
showUpdateButton?: boolean;
handleUpdateButtonClick: () => void;
}
const BottomsheetHeader = (props: IBottomsheetHeaderProps) => {
const { title, handleClose } = props;
const { title, handleClose, showUpdateButton = false, handleUpdateButtonClick } = props;
return (
<View
style={[
@@ -25,15 +27,17 @@ const BottomsheetHeader = (props: IBottomsheetHeaderProps) => {
<Heading dark type="h3">
View {title}
</Heading>
<Pressable
onPress={() => {}} //TODO - Redirect toRCA ETA form
style={[styles.title, styles.gap4]}
>
<UpdateIcon />
<Heading dark type="h5" style={styles.buttonLabel}>
Update
</Heading>
</Pressable>
{showUpdateButton ? (
<Pressable
onPress={handleUpdateButtonClick}
style={[styles.title, styles.gap4]}
>
<UpdateIcon />
<Heading dark type="h5" style={styles.buttonLabel}>
Update
</Heading>
</Pressable>
) : null}
</View>
<Pressable onPress={handleClose}>
<CloseIcon color={COLORS.TEXT.LIGHT} />

View File

@@ -3,6 +3,7 @@ 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';
import EscalatedAnomalyIcon from '@assets/icons/EscalatedAnomalyIcon';
const DaysTillEscalationComponent = (props: {
dayTillEscalation: number;
@@ -19,16 +20,13 @@ const DaysTillEscalationComponent = (props: {
circumference={circumference}
progressPercent={dayTillEscalation > 0 ? progressPercent : 0}
/>
<View
style={{
position: 'absolute',
transform: [{ translateX: 82 }],
}}
>
<View style={styles.escalationTextContainer}>
{dayTillEscalation > 0 ? (
<Text style={styles.textContainer}>{dayTillEscalation}</Text>
) : (
<Text style={styles.textContainer}>0</Text>
<View style={styles.iconContainer}>
<EscalatedAnomalyIcon />
</View>
)}
</View>
</>
@@ -36,5 +34,12 @@ const DaysTillEscalationComponent = (props: {
};
const styles = StyleSheet.create({
textContainer: { fontSize: 14, color: COLORS.TEXT.YELLOW, fontWeight: '700' },
escalationTextContainer: {
position: 'absolute',
transform: [{ translateX: 82 }],
},
iconContainer: {
transform: [{ translateY: 6 }, { translateX: 4 }],
},
});
export default DaysTillEscalationComponent;

View File

@@ -0,0 +1,151 @@
import React from 'react';
import { View, StyleSheet, SafeAreaView, ScrollView, ActivityIndicator } from 'react-native';
import NavigationHeader, { Icon } from '@rn-ui-lib/components/NavigationHeader';
import { GenericStyles } from '@rn-ui-lib/styles';
import { goBack, navigateToScreen } from '@components/utlis/navigationUtlis';
import Button from '@rn-ui-lib/components/Button';
import { useForm } from 'react-hook-form';
import FormInput from '@common/FormInput';
import { AnswerType } from '@components/form/interface';
import { useAppDispatch, useAppSelector } from '@hooks';
import { getAnomalyDetails, updateAnomaly } from '../AnomalyTrackerActions';
import { CaseDetailStackEnum } from '@screens/caseDetails/CaseDetailStack';
import { PageRouteEnum } from '@screens/auth/ProtectedRouter';
import { toast } from '@rn-ui-lib/components/toast';
import { dateFormat, ISO_DATE_FORMAT } from '@rn-ui-lib/utils/dates';
import { COLORS } from '@rn-ui-lib/colors';
import { AnomalyType, VALIDATION_ERROR_MESSAGES } from '../constants';
import { IEtaFormData } from '../interfaces';
import { getLastDateOfNextMonth } from './utils';
interface IEtaForm {
route: {
params: {
selectedAnomalyId: string;
};
};
}
const EtaForm = (props: IEtaForm) => {
const { selectedAnomalyId } = props.route.params || {};
const showLoader = useAppSelector((state) => state?.anomalyTracker?.updatingForm);
const dispatch = useAppDispatch();
const {
control,
reset,
formState: { isValid },
handleSubmit,
} = useForm<IEtaFormData>({
defaultValues: {
at: '',
reason: '',
},
mode: 'onBlur',
});
const successCallback = () => {
reset();
toast({
text1: 'ETA submitted successfully',
type: 'success',
});
dispatch(getAnomalyDetails(AnomalyType.OPEN, true));
navigateToScreen(PageRouteEnum.CASE_DETAIL_STACK, {
screen: CaseDetailStackEnum.ANOMALY_TRACKER,
});
};
const submitETA = (data: IEtaFormData) => {
dispatch(updateAnomaly(selectedAnomalyId, { eta: data }, successCallback));
};
const maxDate = dateFormat(getLastDateOfNextMonth(), ISO_DATE_FORMAT);
return (
<SafeAreaView style={[GenericStyles.fill]}>
<NavigationHeader
title="Submit ETA"
titleStyle={styles.header}
onBack={goBack}
icon={Icon.close}
/>
{showLoader ? (
<View style={[GenericStyles.fill, GenericStyles.centerAlignedRow]}>
<ActivityIndicator size="large" color={COLORS.BASE.BLUE} />
</View>
) : (
<View style={[GenericStyles.fill, GenericStyles.justifyContentFlexEnd]}>
<ScrollView
contentContainerStyle={[GenericStyles.fill, GenericStyles.justifyContentFlexEnd]}
>
<View style={GenericStyles.mb20}>
<FormInput
control={control}
question={{
text: 'Select Date',
type: AnswerType.date,
}}
name="at"
isQuestionMandatory={true}
placeholder="Select date"
maxDate={maxDate}
rules={{
required: {
value: true,
message: VALIDATION_ERROR_MESSAGES.DATE,
},
}}
/>
<FormInput
control={control}
question={{
text: 'Enter reason',
type: AnswerType.text,
}}
name="reason"
isQuestionMandatory={true}
placeholder="Enter reason"
rules={{
required: {
value: true,
message: VALIDATION_ERROR_MESSAGES.REASON_REQUIRED,
},
minLength: {
value: 20,
message: VALIDATION_ERROR_MESSAGES.MIN_LENGTH,
},
}}
/>
</View>
</ScrollView>
<View
style={[
GenericStyles.row,
GenericStyles.alignCenter,
GenericStyles.spaceBetween,
GenericStyles.elevation10,
GenericStyles.p16,
GenericStyles.whiteBackground,
]}
>
<Button
title={'Submit'}
style={[GenericStyles.w100]}
disabled={!isValid}
showLoader={false}
onPress={handleSubmit(submitETA)}
/>
</View>
</View>
)}
</SafeAreaView>
);
};
export const styles = StyleSheet.create({
container: {
display: 'flex',
flexDirection: 'column',
justifyContent: 'flex-end',
borderTopColor: COLORS.BORDER.PRIMARY,
borderBottomColor: COLORS.BORDER.PRIMARY,
},
header: { fontSize: 16, fontWeight: '600' },
});
export default EtaForm;

View File

@@ -0,0 +1,156 @@
import React, { useEffect, useState } from 'react';
import { View, StyleSheet, SafeAreaView, ScrollView, ActivityIndicator } from 'react-native';
import NavigationHeader, { Icon } from '@rn-ui-lib/components/NavigationHeader';
import { GenericStyles } from '@rn-ui-lib/styles';
import { goBack, navigateToScreen } from '@components/utlis/navigationUtlis';
import Button from '@rn-ui-lib/components/Button';
import { useForm } from 'react-hook-form';
import FormInput from '@common/FormInput';
import { AnswerType } from '@components/form/interface';
import { useAppDispatch, useAppSelector } from '@hooks';
import { getAnomalyDetails, getRcaReasons, updateAnomaly } from '../AnomalyTrackerActions';
import { toast } from '@rn-ui-lib/components/toast';
import { PageRouteEnum } from '@screens/auth/ProtectedRouter';
import { CaseDetailStackEnum } from '@screens/caseDetails/CaseDetailStack';
import { COLORS } from '@rn-ui-lib/colors';
import { AnomalyType, VALIDATION_ERROR_MESSAGES } from '../constants';
import { IRcaFormData } from '../interfaces';
interface IRcaForm {
route: {
params: {
selectedAnomalyId: string;
};
};
}
const RcaForm = (props: IRcaForm) => {
const { selectedAnomalyId } = props.route.params || {};
const dispatch = useAppDispatch();
const {
control,
reset,
formState: { isValid },
handleSubmit,
} = useForm<IRcaFormData>({
defaultValues: {
comment: '',
reason: '',
},
mode: 'onBlur',
});
const [rcaReasonsLoading, setIsRcaReasonsLoading] = useState(true);
const rcaReasons = useAppSelector((state) => state?.anomalyTracker?.rcaReasons);
const showLoader = useAppSelector((state) => state?.anomalyTracker?.updatingForm);
useEffect(() => {
if (selectedAnomalyId) {
dispatch(getRcaReasons(selectedAnomalyId, setIsRcaReasonsLoading));
}
}, [selectedAnomalyId]);
const successCallback = () => {
reset();
toast({
text1: 'RCA submitted successfully',
type: 'success',
});
dispatch(getAnomalyDetails(AnomalyType.OPEN, true));
navigateToScreen(PageRouteEnum.CASE_DETAIL_STACK, {
screen: CaseDetailStackEnum.ANOMALY_TRACKER,
});
};
const submitRCA = (data: IRcaFormData) => {
dispatch(updateAnomaly(selectedAnomalyId, { rca: data }, successCallback));
};
return (
<SafeAreaView style={GenericStyles.fill}>
<NavigationHeader
title="Submit RCA"
titleStyle={styles.header}
onBack={goBack}
icon={Icon.close}
/>
{showLoader || rcaReasonsLoading ? (
<View style={[GenericStyles.fill, GenericStyles.centerAlignedRow]}>
<ActivityIndicator size="large" color={COLORS.BASE.BLUE} />
</View>
) : (
<View style={[GenericStyles.fill, GenericStyles.justifyContentFlexEnd]}>
<ScrollView contentContainerStyle={[GenericStyles.justifyContentFlexEnd]}>
<View style={GenericStyles.mb20}>
<FormInput
control={control}
question={{
text: rcaReasons?.[0]?.label || 'Select reason',
type: AnswerType.option,
}}
name="reason"
isQuestionMandatory={true}
answerOptions={rcaReasons?.[0]?.options}
placeholder="Select reason"
rules={{
required: {
value: true,
message: VALIDATION_ERROR_MESSAGES.REASON_REQUIRED,
},
}}
/>
<FormInput
control={control}
question={{
text: 'Enter reason',
type: AnswerType.text,
}}
name="comment"
isQuestionMandatory={true}
placeholder="Enter here"
rules={{
required: {
value: true,
message: VALIDATION_ERROR_MESSAGES.REQUIRED,
},
minLength: {
value: 20,
message: VALIDATION_ERROR_MESSAGES.MIN_LENGTH,
},
}}
/>
</View>
</ScrollView>
<View
style={[
GenericStyles.row,
GenericStyles.alignCenter,
GenericStyles.spaceBetween,
GenericStyles.elevation10,
GenericStyles.p16,
GenericStyles.whiteBackground,
]}
>
<Button
title={'Submit'}
style={[GenericStyles.w100]}
disabled={!isValid}
showLoader={false}
onPress={handleSubmit(submitRCA)}
/>
</View>
</View>
)}
</SafeAreaView>
);
};
export const styles = StyleSheet.create({
container: {
display: 'flex',
flexDirection: 'column',
justifyContent: 'flex-end',
borderTopColor: COLORS.BORDER.PRIMARY,
borderBottomColor: COLORS.BORDER.PRIMARY,
},
header: { fontSize: 16, fontWeight: '600' },
});
export default RcaForm;

View File

@@ -0,0 +1,6 @@
export const getLastDateOfNextMonth = () => {
const todaysDate = new Date();
const nextMonth = new Date(todaysDate.getFullYear(), todaysDate.getMonth() + 1, 1);
const lastDateOfNextMonth = new Date(nextMonth.getFullYear(), nextMonth.getMonth() + 1, 0);
return lastDateOfNextMonth;
};

View File

@@ -1,82 +1,103 @@
import { COLORS } from '@rn-ui-lib/colors';
import React, { useEffect, useState } from 'react';
import { View, StyleSheet } from 'react-native';
import React, { useState } from 'react';
import { View, StyleSheet, ScrollView, Pressable } 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;
}
import { ANOMALY_ACTIONS, IRcaEtaContainerProps } from './interfaces';
import { navigateToScreen } from '@components/utlis/navigationUtlis';
import { CaseDetailStackEnum } from '@screens/caseDetails/CaseDetailStack';
import { GenericStyles } from '@rn-ui-lib/styles';
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 selectedAnomalyId = item?.anomalyReferenceId as string;
const isEtaFilled = item?.estimatedTime || anomalyType === AnomalyType.RESOLVED;
const isRcaFilled = item?.rca || anomalyType === AnomalyType.RESOLVED;
const onETAClick = () => {
if (!(item?.estimatedTime || anomalyType === AnomalyType.RESOLVED)) return; //TODO - Redirect to fill ETA form
setSelectedAnomalyId(item?.anomalyReferenceId as string);
if (!isEtaFilled) {
navigateToScreen(CaseDetailStackEnum.ETA_FORM, { selectedAnomalyId });
return;
}
setShowETABottomSheet(true);
};
const onRCAClick = () => {
if (!(item?.rca || anomalyType === AnomalyType.RESOLVED)) return; //TODO - Redirect to fill ETA form
setSelectedAnomalyId(item?.anomalyReferenceId as string);
if (!isRcaFilled) {
navigateToScreen(CaseDetailStackEnum.RCA_FORM, { selectedAnomalyId });
return;
}
setShowRCABottomSheet(true);
};
const etaStatus = item?.estimatedTime
? dayjs(item?.estimatedTime?.at)?.format(BUSINESS_DATE_FORMAT)
: 'Unfilled';
const handleClose = () => {
setShowRCABottomSheet(false);
if (showRCABottomSheet) {
setShowRCABottomSheet(false);
return;
}
setShowETABottomSheet(false);
setSelectedAnomalyId('');
};
useEffect(() => {
if (selectedAnomalyId) dispatch(getActivityLogs(selectedAnomalyId));
}, [selectedAnomalyId]);
const handleUpdateButtonClick = () => {
if (showRCABottomSheet) {
navigateToScreen(CaseDetailStackEnum.RCA_FORM, { selectedAnomalyId });
} else {
navigateToScreen(CaseDetailStackEnum.ETA_FORM, { selectedAnomalyId });
}
};
return (
<View style={styles.container}>
<RcaEtaActionRow
anomalyType={anomalyType}
label={'RCA'}
label={ANOMALY_ACTIONS.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'}
buttonLabel={isRcaFilled ? 'View' : 'Fill'}
/>
<View style={styles.line} />
<RcaEtaActionRow
anomalyType={anomalyType}
label={'ETA'}
label={ANOMALY_ACTIONS.ETA}
onClick={onETAClick}
status={etaStatus}
statusColor={{ color: item?.estimatedTime ? COLORS.TEXT.GREEN : COLORS.TEXT.RED }}
buttonLabel={item?.estimatedTime || anomalyType === AnomalyType.RESOLVED ? 'View' : 'Fill'}
buttonLabel={isEtaFilled ? 'View' : 'Fill'}
/>
<BottomSheetWrapper
visible={showETABottomSheet || showRCABottomSheet}
HeaderNode={() => (
<BottomsheetHeader title={showRCABottomSheet ? 'RCA' : 'ETA'} handleClose={handleClose} />
<View style={GenericStyles.mb8}>
<BottomsheetHeader
title={showRCABottomSheet ? ANOMALY_ACTIONS.RCA : ANOMALY_ACTIONS.ETA}
handleClose={handleClose}
showUpdateButton={anomalyType === AnomalyType.OPEN}
handleUpdateButtonClick={handleUpdateButtonClick}
/>
</View>
)}
allowBackdropClose={true}
setVisible={handleClose}
heightPercentage={showETABottomSheet ? 26 : 30}
heightPercentage={showETABottomSheet ? 28 : 32}
>
<ViewRcaEtaDetails showETABottomSheet={showETABottomSheet} />
<ScrollView>
<Pressable>
<ViewRcaEtaDetails showETABottomSheet={showETABottomSheet} selectedAnomalyId={selectedAnomalyId}/>
</Pressable>
</ScrollView>
</BottomSheetWrapper>
</View>
);

View File

@@ -23,12 +23,18 @@ const SingleAnomalyDetails = (props: ISingleAnomalyDetailsProps) => {
<View>
<Text style={styles.textLabel}>Created on</Text>
<Text style={styles.date}>
{sanitizeString(dateFormat(new Date(item?.createdAt as string), BUSINESS_DATE_FORMAT))}
{sanitizeString(
item?.createdAt
? 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>
<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}
@@ -38,7 +44,11 @@ const SingleAnomalyDetails = (props: ISingleAnomalyDetailsProps) => {
<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))}
{sanitizeString(
item?.resolution?.at
? dateFormat(new Date(item?.resolution?.at), BUSINESS_DATE_FORMAT)
: ''
)}
</Text>
</View>
)}

View File

@@ -1,57 +1,87 @@
import React from 'react';
import { View, StyleSheet } from 'react-native';
import React, { useEffect } from 'react';
import { View, StyleSheet, ActivityIndicator } from 'react-native';
import RcaEtaQuestionRenderer from './RcaEtaQuestionRenderer';
import dayjs from 'dayjs';
import { BUSINESS_DATE_FORMAT } from '@rn-ui-lib/utils/dates';
import { useAppSelector } from '@hooks';
import { useAppDispatch, useAppSelector } from '@hooks';
import { GenericStyles } from '@rn-ui-lib/styles';
import { DATE_TIME_FORMAT, RCA_ETA_FORM_QUESTIONS } from './constants';
import { ActivityLog, ANOMALY_ACTIONS, EtaActivityLog, RcaActivityLog } from './interfaces';
import { COLORS } from '@rn-ui-lib/colors';
import { getActivityLogs } from './AnomalyTrackerActions';
const ViewRcaEtaDetails = (props: { showETABottomSheet: boolean }) => {
const { showETABottomSheet } = props;
const anomalyDetails = useAppSelector((state) => state?.anomalyTracker?.anomalyDetails?.data);
const { estimatedTime } = anomalyDetails || {};
const rcaDetails = useAppSelector((state) => state?.anomalyTracker?.acitvityLogs?.data); //To do same for eta after backend changes
const { details, type } = rcaDetails?.[0] || {};
const ViewRcaEtaDetails = (props: { showETABottomSheet: boolean; selectedAnomalyId: string }) => {
const { showETABottomSheet, selectedAnomalyId } = props;
const dispatch = useAppDispatch();
const activityLogs: ActivityLog[] | undefined = useAppSelector(
(state) => state?.anomalyTracker?.acitvityLogs?.data
);
const activityLogsLoading = useAppSelector(
(state) => state?.anomalyTracker?.acitvityLogs?.loading
);
if (showETABottomSheet)
useEffect(() => {
dispatch(getActivityLogs(selectedAnomalyId));
}, []);
if (activityLogsLoading)
return (
<View style={[GenericStyles.mh16, styles.mv24]}>
<RcaEtaQuestionRenderer questionText={RCA_ETA_FORM_QUESTIONS.FILLED_BY} answerText={''} />
<RcaEtaQuestionRenderer
questionText={RCA_ETA_FORM_QUESTIONS.DATE}
answerText={dayjs(estimatedTime?.at)?.format(BUSINESS_DATE_FORMAT)}
/>
<RcaEtaQuestionRenderer
questionText={RCA_ETA_FORM_QUESTIONS.REASON}
answerText={estimatedTime?.reason}
/>
<View style={[GenericStyles.fill, GenericStyles.centerAlignedRow, styles.position]}>
<ActivityIndicator size="large" color={COLORS.BASE.BLUE} />
</View>
);
if (showETABottomSheet) {
const etaData = activityLogs?.find(
(item) => item.type === ANOMALY_ACTIONS.ETA
) as EtaActivityLog;
const { details: etaDetails } = etaData || {};
return (
<View style={[GenericStyles.mh16, styles.mv24]}>
<RcaEtaQuestionRenderer
questionText={RCA_ETA_FORM_QUESTIONS.FILLED_BY}
answerText={etaDetails?.name}
/>
<RcaEtaQuestionRenderer
questionText={RCA_ETA_FORM_QUESTIONS.DATE}
answerText={etaDetails?.at ? dayjs(etaDetails?.at)?.format(BUSINESS_DATE_FORMAT) : ''}
/>
<RcaEtaQuestionRenderer
questionText={RCA_ETA_FORM_QUESTIONS.REASON}
answerText={etaDetails?.reason}
/>
</View>
);
}
const rcaData = activityLogs?.find((item) => item.type === ANOMALY_ACTIONS.RCA) as RcaActivityLog;
const { details: rcaDetails } = rcaData || {};
return (
<View style={[GenericStyles.mh16, styles.mv24]}>
<View style={[GenericStyles.mh16, styles.mv8]}>
<RcaEtaQuestionRenderer
questionText={RCA_ETA_FORM_QUESTIONS.FILLED_BY}
answerText={details?.name}
answerText={rcaDetails?.name}
/>
<RcaEtaQuestionRenderer
questionText={RCA_ETA_FORM_QUESTIONS.DATE_TIME}
answerText={dayjs(details?.at).format(DATE_TIME_FORMAT)}
answerText={rcaDetails?.at ? dayjs(rcaDetails?.at).format(DATE_TIME_FORMAT) : ''}
/>
<RcaEtaQuestionRenderer
questionText={RCA_ETA_FORM_QUESTIONS.TYPE}
answerText={details?.type}
answerText={rcaDetails?.reason}
/>
<RcaEtaQuestionRenderer
questionText={RCA_ETA_FORM_QUESTIONS.COMMENTS}
answerText={details?.comment}
answerText={rcaDetails?.comment || ''}
/>
</View>
);
};
const styles = StyleSheet.create({
mv24: { paddingVertical: 24 },
mv8: { marginVertical: 8 },
position: { flex: 1, marginTop: 62 },
});
export default ViewRcaEtaDetails;

View File

@@ -40,3 +40,15 @@ export const RCA_ETA_FORM_QUESTIONS = {
DATE: 'Date',
REASON: 'Reason',
};
export const VALIDATION_ERROR_MESSAGES = {
REQUIRED: 'This field is required',
MIN_LENGTH: 'Enter at least 20 characters',
REASON_REQUIRED: 'Please select a reason',
DATE: 'Please select a date',
};
export const FORM_STATUS = {
FILLED: 'Filled',
UNFILLED: 'Unfilled',
};

View File

@@ -7,15 +7,108 @@ export interface AnomalyItem {
subtype?: string;
priority?: string;
anomalyDetectedOn?: string;
raisedFor?: {};
raisedFor?: {
name: string;
id: string;
allocations: number;
agencyCode: string;
agencyName: string;
phoneNumber: string;
};
anomalyReferenceId?: string;
resolution?: {};
estimatedTime?: {};
rca?: {};
resolution?: {
at: string;
type: string;
name: string;
};
estimatedTime?: IEstimatedTime;
rca?: IRca;
resolutionTime?: string;
resolutionType?: string;
}
export interface PagesData {
pageNo: number;
totalPages: number;
pageSize: number;
totalElements: number;
}
export interface AnomalyResponse {
data: AnomalyItem[];
pages: PagesData;
}
export interface AnomalyCardHeaderProps {
item: AnomalyItem;
}
export interface IEstimatedTime {
at: string;
by: string;
reason: string;
status: string;
}
export interface IRca {
at: string;
by: string;
}
export interface ActivityDetails {
name: string;
at: string;
by: string;
reason: string;
comment?: string;
}
export interface RcaActivityLog {
type: ANOMALY_ACTIONS.RCA;
details: ActivityDetails;
}
export interface EtaActivityLog {
type: ANOMALY_ACTIONS.ETA;
details: ActivityDetails;
}
export type ActivityLog = RcaActivityLog | EtaActivityLog;
export interface IRcaEtaContainerProps {
item: AnomalyItem;
anomalyType: string;
}
export interface IEtaFormPayload {
eta: {
at: string;
reason: string;
};
}
export interface IRcaFormPayload {
rca: {
comment: string;
reason: string;
};
}
export interface IOption {
label: string;
value: string;
}
export enum ANOMALY_ACTIONS {
RCA = 'RCA',
ETA = 'ETA',
}
export interface IEtaFormData {
at: string;
reason: string;
}
export interface IRcaFormData {
comment: string;
reason: string;
}

View File

@@ -1,53 +0,0 @@
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

@@ -20,11 +20,16 @@ const InternalAgentDashboard = () => {
const totalClosedAnomalies =
useAppSelector((state) => state?.anomalyTracker?.closedAnomaliesList?.pages?.totalElements) ||
0;
const enableFieldAppAnomalyTracker = useAppSelector(
(state) => state?.user?.featureFlags?.enableFieldAppAnomalyTracker
);
const isOpenOrClosedAnomaliesPresent = totalOpenAnomalies + totalClosedAnomalies > 0;
return (
<>
{isOpenOrClosedAnomaliesPresent ? <AnomalyOverviewCard /> : null}
{enableFieldAppAnomalyTracker && isOpenOrClosedAnomaliesPresent ? (
<AnomalyOverviewCard />
) : null}
<PerformanceOverview performanceOverviewData={internalAgentPerformanceOverview} />
<PerformanceMeter />
<InternalAgentPerformanceCard />

View File

@@ -16,7 +16,7 @@ import { addClickstreamEvent } from '../../services/clickstreamEventService';
import DashboardHeader from './DashboardHeader';
import ExternalAgentDashboard from './ExternalAgentDashboard';
import InternalAgentDashboard from './InternalAgentDashboard';
import { getAnomalyDetails } from './AnomalyTracker/utils';
import { getAnomalyDetails } from './AnomalyTracker/AnomalyTrackerActions';
import { AnomalyType } from './AnomalyTracker/constants';
const Dashboard = () => {
@@ -32,15 +32,14 @@ const Dashboard = () => {
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(() => {
fetchAgentPerformanceMetrics();
if (!isExternalAgent) {
dispatch(getAnomalyDetails(AnomalyType.OPEN, false));
dispatch(getAnomalyDetails(AnomalyType.RESOLVED, false));
}
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_PERFORMANCE_DASHBOARD_PAGE_LOAD, {});
}, []);

View File

@@ -33,6 +33,8 @@ import OtherAddresses from '@screens/addresses/otherAddresses/OtherAddresses';
import Escalations from '@screens/escalations/Escalations';
import TopAddressMapView from '@screens/MapView/TopAddress';
import AnomalyTracker from '@screens/Dashboard/AnomalyTracker/AnomlayTracker';
import RcaForm from '@screens/Dashboard/AnomalyTracker/Forms.tsx/RcaForm';
import EtaForm from '@screens/Dashboard/AnomalyTracker/Forms.tsx/EtaForm';
const Stack = createNativeStackNavigator();
@@ -62,7 +64,9 @@ export enum CaseDetailStackEnum {
MAP_VIEW_TOP_ADDRESS = 'mapViewTopAddress',
OTHER_ADDRESSES = 'OtherAddresses',
ESCALATIONS = 'Escalations',
ANOMALY_TRACKER = 'ANOMALY_TRACKER',
ANOMALY_TRACKER = "anomalyTracker",
RCA_FORM = 'rcaForm',
ETA_FORM = 'etaForm',
}
const CaseDetailStack = () => {
@@ -130,6 +134,8 @@ const CaseDetailStack = () => {
<Stack.Screen name={CaseDetailStackEnum.OTHER_ADDRESSES} component={OtherAddresses} />
<Stack.Screen name={CaseDetailStackEnum.MAP_VIEW_TOP_ADDRESS} component={TopAddressMapView} />
<Stack.Screen name={CaseDetailStackEnum.ANOMALY_TRACKER} component={AnomalyTracker} />
<Stack.Screen name={CaseDetailStackEnum.RCA_FORM} component={RcaForm} />
<Stack.Screen name={CaseDetailStackEnum.ETA_FORM} component={EtaForm} />
</Stack.Navigator>
);
};

View File

@@ -127,6 +127,9 @@ const getRevivalNotificationHeading = {
const NotificationItem: React.FC<INotificationProps> = ({ data }) => {
const { id, clickable, template, scheduledAt, collectionCaseId, params, headerLabel } = data;
const enableFieldAppAnomalyTracker = useAppSelector(
(state) => state?.user?.featureFlags?.enableFieldAppAnomalyTracker
);
if (headerLabel) {
return (
<Text dark style={[GenericStyles.ph24, GenericStyles.pv12, styles.darkBg]} bold>
@@ -200,6 +203,15 @@ const NotificationItem: React.FC<INotificationProps> = ({ data }) => {
return;
}
if (templateName === NotificationTypes.ANOMALY_TRACKER_DETECTION) {
if (enableFieldAppAnomalyTracker) {
navigateToScreen(PageRouteEnum.CASE_DETAIL_STACK, {
screen: CaseDetailStackEnum.ANOMALY_TRACKER,
});
}
return;
}
const agentRevivalNotificationAction = AgentRevivalNotificationTemplateActionMap[templateName];
if (agentRevivalNotificationAction) {
handleRevivalNotificationNavigation(agentRevivalNotificationAction as string, id);