NTP-10375 | Tele-Cosmos Soft Release (#1000)

This commit is contained in:
Ashish Deo
2024-11-06 18:23:37 +05:30
committed by GitHub
parent 1ff9f6f824
commit 25d4730a93
40 changed files with 1205 additions and 250 deletions

View File

@@ -127,7 +127,7 @@ jobs:
# git config --local user.email "${{ github.actor }}@github.com"
git config --local user.name "${{ github.actor }}"
git tag $TAG_NAME
git push origin $TAG_NAME
git push origin $TAG_NAME --no-verify
env:
GITHUB_TOKEN: ${{ secrets.MY_REPO_PAT }}
- name: Create release tag

View File

@@ -15,13 +15,13 @@ on:
jobs:
central-semgrep:
name: Static code Analysis
uses: navi-infosec/central-semgrep-action/.github/workflows/central-semgrep.yml@master
uses: navi-infosec/central-semgrep-action/.github/workflows/central-semgrep.yml@using-token
with:
github-event-number: ${{github.event.number}}
github-event-name: ${{github.event_name}}
github-repository: ${{github.repository}}
secrets:
READ_SEMGREP_RULES: ${{secrets.READ_SEMGREP_RULES}}
READ_SEMGREP_RULES_TOKEN: ${{secrets.READ_SEMGREP_RULES_TOKEN}}
run-if-failed:
runs-on: [ self-hosted ]
@@ -38,4 +38,4 @@ jobs:
- name: Assign Reviewers
if: ${{ ( github.event.number != '' ) }}
uses: navi-infosec/security-oncall-action@v1.1
uses: navi-infosec/security-oncall-action@v1.1

View File

@@ -1,6 +1,5 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import { NavigationContainer } from '@react-navigation/native';
import * as Sentry from '@sentry/react-native';
import React, { useEffect } from 'react';
import {
AppState,
@@ -38,7 +37,6 @@ import { CLICKSTREAM_EVENT_NAMES, LocalStorageKeys } from '@common/Constants';
import ErrorBoundary from './src/common/ErrorBoundary';
import { getPermissionsToRequest } from '@utils/PermissionUtils';
import ScreenshotBlocker from './src/components/utlis/ScreenshotBlocker';
import { initSentry } from './src/components/utlis/sentry';
import { setItem } from './src/components/utlis/storageHelper';
import { ENV } from './src/constants/config';
import usePolling from './src/hooks/usePolling';
@@ -51,7 +49,6 @@ import fetchUpdatedRemoteConfig from './src/services/firebaseFetchAndUpdate.serv
import { StorageKeys } from './src/types/storageKeys';
import CodePushLoadingModal, { CodePushLoadingModalRef } from './CodePushModal';
initSentry();
if (ENV !== 'prod') {
// mockApiServer();
@@ -213,6 +210,4 @@ function App() {
);
}
const AppWithSentry = Sentry.wrap(App);
export default AppWithSentry;
export default App;

View File

@@ -34,7 +34,7 @@ const SUBMIT_FEEDBACK_API_VERSION = 5;
export const postPinnedList =
(pinnedCases: IPinnedCasesPayload[], updatedCaseList: ICaseItem[], type: string) =>
(dispatch: AppDispatch) => {
async (dispatch: AppDispatch) => {
dispatch(setVisitPlansUpdating(true));
let pinRankCount = 1;
const payload: IPinnedCasesPayload[] = pinnedCases.reduce((acc, pinnedCase) => {
@@ -44,7 +44,12 @@ export const postPinnedList =
});
return acc;
}, [] as IPinnedCasesPayload[]);
const url = getApiUrl(ApiKeys.PINNED_CASES);
const enableCaseCollectionManager =
(await getAsyncStorageItem(LocalStorageKeys.COSMOS_CASE_COLLECTION_MANAGER_ENABLE, true)) ??
false;
const url = getApiUrl(
enableCaseCollectionManager ? ApiKeys.PINNED_CASES_V2 : ApiKeys.PINNED_CASES
);
axiosInstance
.post(url, payload)
.then((response) => {
@@ -148,22 +153,34 @@ export type ISignedRequest = ISignedRequestItem[];
export const getSignedApi = async (
signedRequestPayload: ISignedRequest,
shouldBatch = false,
skipFirebaseUpdate = false,
skipFirebaseUpdate = false
): Promise<{ imageUrl: string }> => {
return new Promise((res) => {
if (shouldBatch) {
batchSignedApiRequest(signedRequestPayload, (results: any) => {
batchSignedApiRequest(
signedRequestPayload,
(results: any) => {
res({ imageUrl: results?.[signedRequestPayload[0].documentReferenceId] || '' });
}, skipFirebaseUpdate);
},
skipFirebaseUpdate
);
} else {
makeBulkSignedApiRequest(signedRequestPayload, (results: any) => {
makeBulkSignedApiRequest(
signedRequestPayload,
(results: any) => {
res({ imageUrl: results?.[signedRequestPayload[0].documentReferenceId] || '' });
}, skipFirebaseUpdate);
},
skipFirebaseUpdate
);
}
});
};
async function batchSignedApiRequest(payload: ISignedRequestItem[], callback: GenericFunctionArgs, skipFirebaseUpdate = false) {
async function batchSignedApiRequest(
payload: ISignedRequestItem[],
callback: GenericFunctionArgs,
skipFirebaseUpdate = false
) {
payload.forEach((item) => {
_signedApiCallBucket.push({ req: item, added_At: Date.now(), callback });
});
@@ -171,7 +188,7 @@ async function batchSignedApiRequest(payload: ISignedRequestItem[], callback: Ge
await makeBulkSignedApiRequest(
_signedApiCallBucket.map((a) => a.req),
_signedApiCallBucket.map((a) => a.callback),
skipFirebaseUpdate,
skipFirebaseUpdate
);
return;
} else if (!_signedApiCallBucketTimer) {
@@ -179,7 +196,7 @@ async function batchSignedApiRequest(payload: ISignedRequestItem[], callback: Ge
await makeBulkSignedApiRequest(
_signedApiCallBucket.map((a) => a.req),
_signedApiCallBucket.map((a) => a.callback),
skipFirebaseUpdate,
skipFirebaseUpdate
);
}, SIGNED_API_BUCKET_TIMEOUT);
}
@@ -188,7 +205,7 @@ async function batchSignedApiRequest(payload: ISignedRequestItem[], callback: Ge
async function makeBulkSignedApiRequest(
payload: ISignedRequestItem[],
callback: GenericFunctionArgs | GenericFunctionArgs[],
skipFirebaseUpdate = false,
skipFirebaseUpdate = false
) {
const enableCaseCollectionManager =
(await getAsyncStorageItem(LocalStorageKeys.COSMOS_CASE_COLLECTION_MANAGER_ENABLE, true)) ??

View File

@@ -0,0 +1,29 @@
import * as React from "react";
import Svg, { Rect, Mask, G, Path } from "react-native-svg";
const EscalationsIcon = () => (
<Svg
width={36}
height={36}
viewBox="0 0 36 36"
fill="none"
>
<Rect width={36} height={36} rx={18} fill="#FFE9E9" />
<Mask
id="a"
maskUnits="userSpaceOnUse"
x={8}
y={8}
width={20}
height={20}
>
<Path fill="#D9D9D9" d="M8 8H28V28H8z" />
</Mask>
<G mask="url(#a)">
<Path
d="M13.417 18.833V25.5c0 .236-.08.434-.24.594a.806.806 0 01-.594.24.806.806 0 01-.593-.24.807.807 0 01-.24-.594V11.333c0-.236.08-.434.24-.593.16-.16.357-.24.593-.24h11.271a.785.785 0 01.688.375c.07.111.114.233.135.365a.76.76 0 01-.052.406l-1.208 3.02 1.208 3.021a.76.76 0 01.052.407.966.966 0 01-.135.364.786.786 0 01-.688.375H13.417z"
fill="#EC5962"
/>
</G>
</Svg>
);
export default EscalationsIcon;

View File

@@ -0,0 +1,32 @@
import * as React from "react"
import { COLORS } from "@rn-ui-lib/colors";
import { IconProps } from "@rn-ui-lib/icons/types"
import Svg, { Path, Mask, G } from "react-native-svg"
const FlagIcon : React.FC<IconProps> = ({fillColor = COLORS.TEXT.RED , width=16, height=16}) => (
<Svg
width={width}
height={height}
viewBox="0 0 16 16"
fill="none"
>
<Mask
id="a"
maskUnits="userSpaceOnUse"
x={0}
y={0}
width={16}
height={16}
>
<Path fill="#D9D9D9" d="M0 0H16V16H0z" />
</Mask>
<G mask="url(#a)">
<Path
d="M4.333 8.667V14a.645.645 0 01-.191.475.645.645 0 01-.475.192.645.645 0 01-.475-.192A.645.645 0 013 14V2.667c0-.19.064-.348.192-.475A.645.645 0 013.667 2h9.016a.629.629 0 01.55.3.771.771 0 01.109.292.61.61 0 01-.042.325l-.967 2.416.967 2.417a.61.61 0 01.042.325.771.771 0 01-.109.292.629.629 0 01-.55.3h-8.35z"
fill={fillColor}
/>
</G>
</Svg>
);
export default FlagIcon

View File

@@ -0,0 +1,22 @@
import React from 'react';
import { Circle, G, Mask, Path, Rect, Svg } from 'react-native-svg';
const PostOperativeHoursIcon = () => {
return (
<Svg width="68" height="68" viewBox="0 0 68 68" fill="none">
<Circle cx="34" cy="34" r="34" fill="#F7F7F7" />
<Circle cx="34" cy="34" r="30" fill="#FFEBEB" />
<Mask id="mask0_2660_9963" maskUnits="userSpaceOnUse" x="13" y="13" width="42" height="42">
<Rect x="13" y="13" width="42" height="42" fill="#D9D9D9" />
</Mask>
<G mask="url(#mask0_2660_9963)">
<Path
d="M29.6316 27.6182C29.6316 27.7286 29.7211 27.8182 29.8316 27.8182H38.5895C38.6999 27.8182 38.7895 27.7286 38.7895 27.6182V24.7273C38.7895 23.4394 38.3443 22.3447 37.4539 21.4432C36.5636 20.5417 35.4825 20.0909 34.2105 20.0909C32.9386 20.0909 31.8575 20.5417 30.9671 21.4432C30.0768 22.3447 29.6316 23.4394 29.6316 24.7273V27.6182ZM34.3917 49.1421C34.476 49.2772 34.3802 49.4545 34.221 49.4545H25.0526C24.2132 49.4545 23.4945 49.1519 22.8967 48.5466C22.2989 47.9413 22 47.2136 22 46.3636V30.9091C22 30.0591 22.2989 29.3314 22.8967 28.7261C23.4945 28.1208 24.2132 27.8182 25.0526 27.8182H26.3789C26.4894 27.8182 26.5789 27.7286 26.5789 27.6182V24.7273C26.5789 22.5894 27.323 20.767 28.8112 19.2602C30.2993 17.7534 32.0991 17 34.2105 17C36.3219 17 38.1217 17.7534 39.6099 19.2602C41.098 20.767 41.8421 22.5894 41.8421 24.7273V27.6182C41.8421 27.7286 41.9316 27.8182 42.0421 27.8182H43.3684C44.2079 27.8182 44.9265 28.1208 45.5243 28.7261C46.1222 29.3314 46.4211 30.0591 46.4211 30.9091V32.6396C46.4211 32.7764 46.2867 32.8729 46.1556 32.8336C45.7796 32.7206 45.3911 32.6329 44.9901 32.5705C44.5511 32.5021 44.0772 32.4639 43.5684 32.4561C43.458 32.4544 43.3684 32.365 43.3684 32.2545V31.1091C43.3684 30.9986 43.2789 30.9091 43.1684 30.9091H25.2526C25.1422 30.9091 25.0526 30.9986 25.0526 31.1091V46.1636C25.0526 46.2741 25.1422 46.3636 25.2526 46.3636H32.9974C33.0837 46.3636 33.1603 46.4191 33.1878 46.501C33.3761 47.0613 33.5644 47.55 33.7526 47.967C33.9163 48.3296 34.1293 48.7213 34.3917 49.1421ZM43.3684 51C41.257 51 39.4572 50.2466 37.9691 48.7398C36.4809 47.233 35.7368 45.4106 35.7368 43.2727C35.7368 41.1348 36.4809 39.3125 37.9691 37.8057C39.4572 36.2989 41.257 35.5455 43.3684 35.5455C45.4798 35.5455 47.2796 36.2989 48.7678 37.8057C50.2559 39.3125 51 41.1348 51 43.2727C51 45.4106 50.2559 47.233 48.7678 48.7398C47.2796 50.2466 45.4798 51 43.3684 51ZM45.7445 46.7605C45.8228 46.8397 45.9508 46.8397 46.0291 46.7605L46.8165 45.9633C46.8934 45.8854 46.8934 45.7601 46.8165 45.6822L44.1893 43.0221C44.1523 42.9846 44.1316 42.9341 44.1316 42.8815V38.8364C44.1316 38.7259 44.042 38.6364 43.9316 38.6364H42.8053C42.6948 38.6364 42.6053 38.7259 42.6053 38.8364V43.4997C42.6053 43.5523 42.626 43.6028 42.663 43.6402L45.7445 46.7605Z"
fill="#E92C2C"
/>
</G>
</Svg>
);
};
export default PostOperativeHoursIcon;

View File

@@ -107,7 +107,6 @@ const BlockerScreen = (props: IBlockerScreen) => {
if (!flavorToUpdate) return;
const currentBuildNumber = getBuildVersion();
if (
currentBuildNumber &&
!isNaN(currentBuildNumber) &&

View File

@@ -456,7 +456,10 @@ export const CLICKSTREAM_EVENT_NAMES = {
name: 'FA_VIEW_PAST_FEEDBACK_PREV_PAGE_FAILED',
description: 'FA_VIEW_PAST_FEEDBACK_PREV_PAGE_FAILED',
},
FA_VIEW_PHOTO_CLICKED: { name: 'FA_VIEW_PHOTO_CLICKED', description: 'FA_VIEW_PHOTO_CLICKED' },
FA_VIEW_PHOTO_CLICKED: {
name: 'FA_VIEW_PHOTO_CLICKED',
description: 'FA_VIEW_PHOTO_CLICKED'
},
FA_CUSTOMER_DOCUMENT_CLICKED: {
name: 'FA_CUSTOMER_DOCUMENT_CLICKED',
description: 'FA_CUSTOMER_DOCUMENT_CLICKED',
@@ -494,6 +497,15 @@ export const CLICKSTREAM_EVENT_NAMES = {
name: 'FA_PERFORMANCE_DASHBOARD_PERFORMANCE_GRAPH_CLICKED',
description: 'When the user clicks on expand/collapse of the performance graph',
},
FA_VIEW_ALL_ESCALATIONS_SCREEN_CLICKED: {
name: 'FA_VIEW_ALL_ESCALATIONS_SCREEN_CLICKED',
description: 'FA_VIEW_ALL_ESCALATIONS_SCREEN_CLICKED',
},
FA_VIEW_ALL_ESCALATIONS_SCREEN_LANDED: {
name: 'FA_VIEW_ALL_ESCALATIONS_SCREEN_LANDED',
description: 'FA_VIEW_ALL_ESCALATIONS_SCREEN_LANDED',
},
// Notifications
FA_NOTIFICATION_ICON_CLICK: {
@@ -1320,7 +1332,58 @@ export const CLICKSTREAM_EVENT_NAMES = {
FA_UNSYNC_FEEDBACK_CAPTURED: {
name: 'FA_UNSYNC_FEEDBACK_CAPTURED',
description: 'Unsync feedback captured'
},
FA_CODEPUSH_UNKNOWN_ERROR: {
name : 'FA_CODEPUSH_UNKNOWN_ERROR',
description: 'Codepush unknown error'
},
FA_API_FAILED: {
name: 'FA_API_FAILED',
description: 'API failed'
},
// Apk Update
FA_APK_UPDATE_DOWNLOAD_STARTED: {
name: 'FA_APK_UPDATE_DOWNLOAD_STARTED',
description: 'APK update download started'
},
FA_APK_UPDATE_DOWNLOAD_SUCCESS: {
name: 'FA_APK_UPDATE_DOWNLOAD_SUCCESS',
description: 'APK update download completed'
},
FA_APK_UPDATE_DOWNLOAD_FAILED: {
name: 'FA_APK_UPDATE_DOWNLOAD_FAILED',
description: 'APK update download failed'
},
FA_APK_UPDATE_BUTTON_CLICKED: {
name: 'FA_APK_UPDATE_BUTTON_CLICKED',
description: 'APK update button clicked'
},
FA_APK_UPDATE_INSTALL_STARTED: {
name: 'FA_APK_UPDATE_INSTALL_STARTED',
description: 'APK update installation started'
},
FA_APK_UPDATE_INSTALL_FAILED: {
name: 'FA_APK_UPDATE_INSTALL_FAILED',
description: 'APK update installation failed'
},
FA_APK_UPDATE_FALLBACK_TRIGGERED: {
name: 'FA_APK_UPDATE_FALLBACK_TRIGGERED',
description: 'APK update fallback triggered'
},
FA_APK_UPDATE_CORRUPTED_FILE_DOWNLOADED: {
name: 'FA_APK_UPDATE_CORRUPTED_FILE_DOWNLOADED',
description: 'APK update corrupted file downloaded'
},
FA_APK_UPDATE_INSTALL_SUCCESS: {
name: 'FA_APK_UPDATE_INSTALL_SUCCESS',
description: 'APK update installation success'
},
FA_POST_OPERATIVE_HOURS_SCREEN_LOADED: {
name: 'FA_POST_OPERATIVE_HOURS_SCREEN_LOADED',
description: 'Post operative hours screen loaded'
}
} as const;
export enum MimeType {
@@ -1442,8 +1505,10 @@ export const BUTTON_PRESS_COUNT_FOR_IMPERSONATION = 5;
export const REQUEST_TYPE_TO_BLOCK_FOR_IMPERSONATION = ['post', 'put', 'patch', 'delete'];
export const REQUEST_TO_UNBLOCK_FOR_IMPERSONATION = [
getApiUrl(ApiKeys.GET_SIGNED_URL_V2),
getApiUrl(ApiKeys.GET_SIGNED_URL),
getApiUrl(ApiKeys.GET_SIGNED_URL_FOR_REPORTEE),
getApiUrl(ApiKeys.GET_SIGNED_URL_FOR_REPORTEE_V2),
getApiUrl(ApiKeys.LOGOUT),
getApiUrl(ApiKeys.PAST_FEEDBACK),
getApiUrl(ApiKeys.GET_CSA_TICKETS),

View File

@@ -0,0 +1,116 @@
import { getAgentDetail, logout } from '@actions/authActions';
import PostOperativeHoursIcon from '@assets/icons/PostOperativeHoursIcon';
import { useAppDispatch } from '@hooks';
import { AppStates } from '@interfaces/appStates';
import { COLORS } from '@rn-ui-lib/colors';
import Button from '@rn-ui-lib/components/Button';
import Text from '@rn-ui-lib/components/Text';
import LogoutIcon from '@rn-ui-lib/icons/LogoutIcon';
import { GenericStyles } from '@rn-ui-lib/styles';
import { addClickstreamEvent } from '@services/clickstreamEventService';
import React, { useEffect } from 'react';
import { AppState, AppStateStatus, StyleSheet } from 'react-native';
import { View } from 'react-native';
import { CLICKSTREAM_EVENT_NAMES } from './Constants';
const PostOperativeHours = () => {
const dispatch = useAppDispatch();
useEffect(() => {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_POST_OPERATIVE_HOURS_SCREEN_LOADED);
dispatch(getAgentDetail());
}, []);
const handleAppStateChange = (nextAppState: AppStateStatus) => {
if (nextAppState === AppStates.ACTIVE) {
dispatch(getAgentDetail());
}
};
useEffect(() => {
const appStateListener = AppState.addEventListener('change', handleAppStateChange);
return () => {
appStateListener.remove();
};
}, []);
return (
<View style={[GenericStyles.fill, GenericStyles.centerAligned]}>
<View
style={[
GenericStyles.centerAligned,
GenericStyles.p24,
styles.pt40,
GenericStyles.pb16,
GenericStyles.br8,
GenericStyles.border,
GenericStyles.whiteBackground,
]}
>
<View style={styles.iconContainer}>
<PostOperativeHoursIcon />
</View>
<Text style={styles.headerTxt}>You have logged in post operative hours!</Text>
<Text style={styles.description}>
Please{' '}
<Text dark style={styles.time}>
login between operative hours (8AM - 7PM)
</Text>{' '}
to access Cosmos
</Text>
<View style={[GenericStyles.w100]}>
<Button
onPress={() => dispatch(logout())}
title="Logout"
variant="primaryText"
style={[GenericStyles.pt12]}
textStyle={[GenericStyles.fontSize16, GenericStyles.fw500]}
leftIcon={
<View style={styles.refreshIcon}>
<LogoutIcon fillColor={COLORS.BASE.BLUE} />
</View>
}
underlayColor="transparent"
pressableWidthChange={false}
opacityChangeOnPress={true}
/>
</View>
</View>
</View>
);
};
const styles = StyleSheet.create({
iconContainer: {
position: 'absolute',
top: -30,
},
refreshIcon: {
marginRight: 8,
},
pt40: {
paddingTop: 40,
},
headerTxt: {
fontSize: 16,
color: COLORS.TEXT.DARK,
fontWeight: '500',
lineHeight: 20,
paddingBottom: 12,
paddingTop: 16,
textAlign: 'center',
},
description: {
fontSize: 14,
color: COLORS.TEXT.BLACK,
textAlign: 'center',
fontWeight: '400',
lineHeight: 18,
},
time: {
color: COLORS.TEXT.DARK,
fontWeight: '500',
},
});
export default PostOperativeHours;

View File

@@ -68,6 +68,7 @@ import store from '@store';
import useFirestoreUpdates from '@hooks/useFirestoreUpdates';
import { GLOBAL } from '@constants/Global';
export enum FOREGROUND_TASKS {
GEOLOCATION = 'GEOLOCATION',
TIME_SYNC = 'TIME_SYNC',
@@ -85,7 +86,7 @@ export enum FOREGROUND_TASKS {
DATA_SYNC_JOB = 'DATA_SYNC_JOB',
NEARBY_CASES_GEOLOCATION_CHECK = 'NEARBY_CASES_GEOLOCATION_CHECK',
COSMOS_SYNC_WITH_LONGHORN = 'COSMOS_SYNC_WITH_LONGHORN',
WIFI_DETAILS_SYNC = 'WIFI_DETAILS_SYNC'
WIFI_DETAILS_SYNC = 'WIFI_DETAILS_SYNC',
}
interface ITrackingComponent {

View File

@@ -109,20 +109,6 @@ const AddressSelection: React.FC<IAddressSelection> = (props) => {
};
if (isGeolocation) {
if (isLoading) {
return (
<View style={GenericStyles.mt10}>
{[...Array(7).keys()].map((_, index) => (
<LineLoader
key={index}
width="100%"
height={50}
style={[GenericStyles.br8, GenericStyles.mb20]}
/>
))}
</View>
);
}
if (!addresses?.length) {
return (
<View>
@@ -137,41 +123,54 @@ const AddressSelection: React.FC<IAddressSelection> = (props) => {
key={controllerName}
control={control}
rules={{ validate: (data) => validateInput(data, question.metadata.validators) }}
render={({ field: { onChange, value } }) => (
<View
style={[
GenericStyles.fill,
GenericStyles.whiteBackground,
GenericStyles.mt10,
GenericStyles.br8,
GenericStyles.pv24,
]}
>
render={({ field: { onChange, value } }) =>
isLoading ? (
<View style={GenericStyles.mt10}>
{[...Array(7).keys()].map((_, index) => (
<LineLoader
key={index}
width="100%"
height={50}
style={[GenericStyles.br8, GenericStyles.mb20]}
/>
))}
</View>
) : (
<View
style={[
GenericStyles.row,
GenericStyles.centerAligned,
GenericStyles.ph16,
GenericStyles.mb12,
GenericStyles.fill,
GenericStyles.whiteBackground,
GenericStyles.mt10,
GenericStyles.br8,
GenericStyles.pv24,
]}
>
<NoLocationsIcon />
<Text light style={GenericStyles.ml4}>
No nearby geolocations found
</Text>
<View
style={[
GenericStyles.row,
GenericStyles.centerAligned,
GenericStyles.ph16,
GenericStyles.mb12,
]}
>
<NoLocationsIcon />
<Text light style={GenericStyles.ml4}>
No nearby geolocations found
</Text>
</View>
<TouchableOpacity
activeOpacity={0.7}
onPress={reloadGeolocations}
style={[GenericStyles.row, GenericStyles.centerAligned]}
>
<Text style={[GenericStyles.p12]}>
<LoadingIcon fillColor={COLORS.BASE.BLUE} />
</Text>
<Text>Retry</Text>
</TouchableOpacity>
</View>
<TouchableOpacity
activeOpacity={0.7}
onPress={reloadGeolocations}
style={[GenericStyles.row, GenericStyles.centerAligned]}
>
<Text style={[GenericStyles.p12]}>
<LoadingIcon fillColor={COLORS.BASE.BLUE} />
</Text>
<Text>Retry</Text>
</TouchableOpacity>
</View>
)}
)
}
name={controllerName}
/>
</View>
@@ -199,6 +198,7 @@ const AddressSelection: React.FC<IAddressSelection> = (props) => {
orientation="vertical"
>
{addresses?.map((address) => {
if(isGeolocation) return <></>;
const addressLabel = isGeolocation
? (address as IGeolocation)?.tag
: getAddressString(address as Address);

View File

@@ -88,9 +88,10 @@ const ImageUploadV2: React.FC<IImageUpload> = (props) => {
dispatch(addIntermediateDocument({ caseId, fileUri, questionKey, imageWidth, imageHeight }));
};
const handleImageDelete = () => {
const handleImageDelete = (onChange: (...event: any[]) => void) => {
setImageId('');
dispatch(deleteIntermediateDocument({ caseId, questionKey: questionId }));
onChange(undefined);
};
const handleError = (error: string) => {
@@ -317,11 +318,11 @@ const ImageUploadV2: React.FC<IImageUpload> = (props) => {
{question.text}{' '}
{isQuestionMandatory(question) && <Text style={GenericStyles.redText}>*</Text>}
</Text>
{showClickPictureCTA ? (
<Controller
control={props.control}
rules={{ validate: (data) => validateInput(data, question.metadata.validators) }}
render={({ field: { onChange } }) => (
<Controller
control={props.control}
rules={{ validate: (data) => validateInput(data, question.metadata.validators) }}
render={({ field: { onChange } }) =>
showClickPictureCTA ? (
<>
<Pressable
style={[styles.clickContainer]}
@@ -344,33 +345,33 @@ const ImageUploadV2: React.FC<IImageUpload> = (props) => {
</Pressable>
) : null}
</>
)}
name={`widgetContext.${widgetId}.sectionContext.${props.sectionId}.questionContext.${questionId}`}
/>
) : (
<View>
<ImageBackground
style={[styles.image, { height: Number(imageHeightWrtAspectRatio) || 350 }]}
imageStyle={GenericStyles.br8}
source={{
uri: fileUri,
}}
onError={(error) => handleError('Error in image rendering')}
>
{!imageLoading && !imageError ? (
<TouchableOpacity onPress={handleImageDelete} style={styles.deleteButton}>
<DeleteIcon />
</TouchableOpacity>
) : null}
<ImagePlaceholder
loading={imageLoading}
imageError={imageError}
props={props}
onRetry={handleImageCapture}
/>
</ImageBackground>
</View>
)}
) : (
<View>
<ImageBackground
style={[styles.image, { height: Number(imageHeightWrtAspectRatio) || 350 }]}
imageStyle={GenericStyles.br8}
source={{
uri: fileUri,
}}
onError={(error) => handleError('Error in image rendering')}
>
{!imageLoading && !imageError ? (
<TouchableOpacity onPress={() => handleImageDelete(onChange)} style={styles.deleteButton}>
<DeleteIcon />
</TouchableOpacity>
) : null}
<ImagePlaceholder
loading={imageLoading}
imageError={imageError}
props={props}
onRetry={handleImageCapture}
/>
</ImageBackground>
</View>
)
}
name={`widgetContext.${widgetId}.sectionContext.${props.sectionId}.questionContext.${questionId}`}
/>
<ErrorMessage
show={
error?.widgetContext?.[widgetId]?.sectionContext?.[sectionId]?.questionContext?.[

View File

@@ -63,4 +63,3 @@ export const sendContentToWhatsapp = (
fileName: string
): Promise<boolean> =>
DeviceUtilsModule?.sendContentToWhatsapp(message, imageUrl, mimeType, format, fileName);

View File

@@ -6,15 +6,21 @@ import { GLOBAL } from '../../constants/Global';
import { _map, compareUrl } from '../../../RN-UI-LIB/src/utlis/common';
import { BASE_AV_APP_URL } from '../../constants/config';
import { logError } from './errorUtils';
import { sendApiToClickstreamEvent } from '../../services/clickstreamEventService';
import {
addClickstreamEvent,
sendApiToClickstreamEvent,
} from '../../services/clickstreamEventService';
import { handleLogout } from '../../action/authActions';
import {
API_ERROR_MESSAGE,
CLICKSTREAM_EVENT_NAMES,
REQUEST_TO_UNBLOCK_FOR_IMPERSONATION,
REQUEST_TYPE_TO_BLOCK_FOR_IMPERSONATION,
} from '../../common/Constants';
import { ToastMessages } from '../../screens/allCases/constants';
import { alfredHandleSWWEvent } from './DeviceUtils';
import { setWithinOperativeHours } from '@reducers/userSlice';
import store from '@store';
export enum ApiKeys {
GENERATE_OTP = 'GENERATE_OTP',
@@ -22,6 +28,7 @@ export enum ApiKeys {
ALL_CASES = 'ALL_CASES',
CASE_DETAIL = 'CASE_DETAIL',
PINNED_CASES = 'PINNED_CASES',
PINNED_CASES_V2 = 'PINNED_CASES_V2',
LOGOUT = 'LOGOUT',
FEEDBACK = 'FEEDBACK',
FILTERS = 'FILTERS',
@@ -98,7 +105,8 @@ export enum ApiKeys {
GENERATE_DYNAMIC_DOCUMENT = 'GENERATE_DYNAMIC_DOCUMENT',
DOWNLOAD_LATEST_APP = 'DOWNLOAD_LATEST_APP',
GET_SIGNED_URL_V2 = 'GET_SIGNED_URL_V2',
GET_SIGNED_URL_FOR_REPORTEE_V2 = 'GET_SIGNED_URL_FOR_REPORTEE_V2'
GET_SIGNED_URL_FOR_REPORTEE_V2 = 'GET_SIGNED_URL_FOR_REPORTEE_V2',
ALL_ESCALATIONS = 'ALL_ESCALATIONS',
}
export const API_URLS: Record<ApiKeys, string> = {} as Record<ApiKeys, string>;
@@ -107,6 +115,7 @@ API_URLS[ApiKeys.VERIFY_OTP] = '/auth/otp/verify';
API_URLS[ApiKeys.ALL_CASES] = '/cases/all-cases';
API_URLS[ApiKeys.CASE_DETAIL] = '/cases/get-cases';
API_URLS[ApiKeys.PINNED_CASES] = '/cases/pin';
API_URLS[ApiKeys.PINNED_CASES_V2] = '/cases/v2/pin';
API_URLS[ApiKeys.LOGOUT] = '/auth/logout';
API_URLS[ApiKeys.FEEDBACK] = '/cases/feedback';
API_URLS[ApiKeys.FILTERS] = '/cases/filters';
@@ -183,10 +192,12 @@ API_URLS[ApiKeys.FEE_WAIVER_HISTORY] = '/collection-cases/{loanAccountNumber}/wa
API_URLS[ApiKeys.FEE_WAIVER_V2] = '/loan/request/{loanAccountNumber}/adjust-component/v2';
API_URLS[ApiKeys.GET_PIN_CODES_DETAILS] = '/api/v1/pincodes/{pinCode}';
API_URLS[ApiKeys.SYNC_COSMOS_TO_LONGHORN] = '/sync/tele-cosmos-sync';
API_URLS[ApiKeys.CALL_CUSTOMER] = '/call-recording/call-request/{loanAccountNumber}/{telephoneReferenceId}';
API_URLS[ApiKeys.CALL_CUSTOMER] =
'/call-recording/call-request/{loanAccountNumber}/{telephoneReferenceId}';
API_URLS[ApiKeys.SYNC_ACTIVE_CALL_DETAILS] = '/call-recording/call-status';
API_URLS[ApiKeys.GET_CALL_HISTORY] = '/call-recording/call-history/{loanAccountNumber}';
API_URLS[ApiKeys.SYNC_CALL_FEEDBACK_NUDGE_DETAILS] =
'/call-recording/acknowledge-feedback-nudge/{callId}';
API_URLS[ApiKeys.FETCH_CUSTOMER_DOCUMENTS] = '/documents/{loanAccountNumber}';
API_URLS[ApiKeys.FETCH_AGENT_DOCUMENTS] = '/documents/agent';
@@ -194,6 +205,8 @@ API_URLS[ApiKeys.FETCH_DOCUMENT_SPECIFIC_LANGUAGE] = '/documents/language/{loanA
API_URLS[ApiKeys.SEND_COMMUNICATION_NAVI_ACCOUNT] = '/navi-communications/{loanAccountNumber}';
API_URLS[ApiKeys.GENERATE_DYNAMIC_DOCUMENT] = '/documents/generate/{loanAccountNumber}';
API_URLS[ApiKeys.DOWNLOAD_LATEST_APP] = 'https://longhorn.navi.com/api/app/download';
API_URLS[ApiKeys.ALL_ESCALATIONS] = '/customer-escalation';
API_URLS[ApiKeys.DOWNLOAD_LATEST_APP] = 'https://longhorn.navi.com/api/app/download';
export const API_STATUS_CODE = {
OK: 200,
@@ -205,6 +218,7 @@ export const API_STATUS_CODE = {
UNPROCESSABLE_CONTENT: 422,
INTERNAL_SERVER_ERROR: 500,
TOO_MANY_REQUESTS: 429,
GONE: 410,
};
const API_TIMEOUT_INTERVAL = 2e4; // 20s
@@ -246,15 +260,15 @@ const errorsToRetry = [500, 503];
const axiosInstance = axios.create({ timeout: API_TIMEOUT_INTERVAL });
axiosInstance.interceptors.request.use((request) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
request.retry = request?.retry < 4 ? request.retry : 3;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
request.headers['X-Auth-Source'] = 'mjolnir';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
request.retry = request?.retry < 4 ? request.retry : 3;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
request.delay = request?.delay > 2000 ? request.delay : 2000;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
request.headers['request-start-time'] = Date.now();
@@ -318,6 +332,10 @@ axiosInstance.interceptors.response.use(
const end = Date.now();
const milliseconds = end - Number(start);
sendApiToClickstreamEvent(response, milliseconds, false);
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_API_FAILED, {
statusCode: response?.status,
url: response?.config?.url,
});
const donotHandleErrorOnStatusCode = (config.headers.donotHandleErrorOnStatusCode || []).map(
Number
);
@@ -335,10 +353,12 @@ axiosInstance.interceptors.response.use(
? config.headers.showInSpecificComponents?.includes(getCurrentScreen().name)
: true)
) {
toast({
type: 'error',
text1: typeof errorString === 'string' ? errorString : API_ERROR_MESSAGE,
});
if (API_STATUS_CODE.GONE !== response.status) {
toast({
type: 'error',
text1: typeof errorString === 'string' ? errorString : API_ERROR_MESSAGE,
});
}
}
if ([API_STATUS_CODE.UNAUTHORIZED, API_STATUS_CODE.FORBIDDEN].includes(response.status)) {
@@ -346,14 +366,22 @@ axiosInstance.interceptors.response.use(
dispatch(handleLogout());
}
// Blocking cosmos after operative hours
if (API_STATUS_CODE.GONE === response.status && !GLOBAL.IS_IMPERSONATED) {
if (store?.getState().user.withinOperativeHours) {
dispatch(setWithinOperativeHours(false));
}
}
return Promise.reject(error);
}
config.retry -= 1;
const delayRetryRequest = new Promise<void>((resolve) => {
setTimeout(() => {
resolve();
}, 500);
}, config.delay);
});
config.delay *= 2;
return delayRetryRequest.then(() => axiosInstance(config));
}
);

View File

@@ -6,17 +6,17 @@ export const logError = (error: Error, extraInfo = '') => {
// Disable sentry in development mode
return;
}
Sentry.setTag('agentId', GLOBAL.AGENT_ID || 'not-logged-in');
Sentry.captureException(error, (scope) => {
scope.setExtra('ExtraInfo', extraInfo);
return scope;
});
// Sentry.setTag('agentId', GLOBAL.AGENT_ID || 'not-logged-in');
// Sentry.captureException(error, (scope) => {
// scope.setExtra('ExtraInfo', extraInfo);
// return scope;
// });
};
export const sentryCaptureMessage = (errorStr: string, extraInfo = '') => {
Sentry.setTag('agentId', GLOBAL.AGENT_ID || 'not-logged-in');
Sentry.captureMessage(errorStr, (scope) => {
scope.setExtra('ExtraInfo', extraInfo);
return scope;
});
// Sentry.setTag('agentId', GLOBAL.AGENT_ID || 'not-logged-in');
// Sentry.captureMessage(errorStr, (scope) => {
// scope.setExtra('ExtraInfo', extraInfo);
// return scope;
// });
};

View File

@@ -0,0 +1,29 @@
import { createSlice } from '@reduxjs/toolkit';
interface IAppUpdateSlice {
shouldUpdate: {
newApkCachedUrl: string;
switchToFallback: boolean;
};
}
const initialState: IAppUpdateSlice = {
shouldUpdate: {
newApkCachedUrl: '',
switchToFallback: false,
},
};
const AppUpdateSlice = createSlice({
name: 'appUpdate',
initialState,
reducers: {
setShouldUpdate: (state, action) => {
state.shouldUpdate = action.payload;
},
},
});
export const { setShouldUpdate } = AppUpdateSlice.actions;
export default AppUpdateSlice.reducer;

View File

@@ -0,0 +1,28 @@
import { createSlice } from '@reduxjs/toolkit';
import { IEscalationsSlice, pageData } from '@screens/escalations/escalations.interfaces';
const initialState = {
escalationData : [],
isLoading : false,
pageData: {} as pageData
} as IEscalationsSlice;
const escalationsSlice = createSlice({
name: 'escalations',
initialState,
reducers: {
setEscalationData: (state, action) => {
state.escalationData = action.payload;
},
setIsLoading: (state, action) => {
state.isLoading = action.payload;
},
setPageData: (state, action) => {
state.pageData = action.payload;
}
},
});
export const { setEscalationData, setIsLoading, setPageData } = escalationsSlice.actions;
export default escalationsSlice.reducer;

View File

@@ -77,6 +77,8 @@ export interface IUserSlice extends IUser {
isCallRecordingCosmosExotelEnabled: boolean;
};
employeeId: string;
is1To30FieldAgent: boolean;
withinOperativeHours: boolean;
}
const initialState: IUserSlice = {
@@ -105,6 +107,8 @@ const initialState: IUserSlice = {
isCallRecordingCosmosExotelEnabled: false,
},
employeeId: '',
is1To30FieldAgent: false,
withinOperativeHours: true,
};
export const userSlice = createSlice({
@@ -142,8 +146,15 @@ export const userSlice = createSlice({
state.agentAttendance = action.payload;
},
setUserAccessData: (state, action) => {
const { roles, isExternalAgent, isFieldTeamLeadOrAgencyManager, featureFlags, employeeId } =
action.payload || {};
const {
roles,
isExternalAgent,
is1To30FieldAgent,
isFieldTeamLeadOrAgencyManager,
featureFlags,
employeeId,
withinOperativeHours
} = action.payload || {};
if (roles?.length) {
state.isTeamLead = isFieldTeamLeadOrAgencyManager;
} else {
@@ -153,6 +164,11 @@ export const userSlice = createSlice({
state.agentRoles = roles;
state.featureFlags = featureFlags;
state.employeeId = employeeId;
state.is1To30FieldAgent = is1To30FieldAgent;
state.withinOperativeHours = withinOperativeHours;
},
setWithinOperativeHours: (state, action) => {
state.withinOperativeHours = action.payload;
},
},
});
@@ -165,6 +181,7 @@ export const {
setCaseSyncLock,
setAgentAttendance,
setUserAccessData,
setWithinOperativeHours
} = userSlice.actions;
export default userSlice.reducer;

View File

@@ -24,10 +24,10 @@ export const ADDRESSES_TABS = [
key: 'address',
label: 'Addresses',
},
{
key: 'geolocation',
label: 'Geolocations',
},
// {
// key: 'geolocation',
// label: 'Geolocations',
// },
];
export enum AddressGeolocationTabEnum {

View File

@@ -13,7 +13,6 @@ import {
import CaseItemAvatar from './CaseItemAvatar';
import {
CaseStatuses,
displayStatuses,
InteractionStatuses,
TaskTitleUIMapping,
ICaseItemAvatarCaseDetailObj,
@@ -34,6 +33,7 @@ import relativeDistanceFormatter from '@screens/addressGeolocation/utils/relativ
import { CaseDetailStackEnum } from '@screens/caseDetails/CaseDetailStack';
import { PageRouteEnum } from '@screens/auth/ProtectedRouter';
import LocationDistanceIcon from '@assets/icons/LocationDistanceIcon';
import FlagIcon from '@assets/icons/FlagIcon';
interface IListItem {
caseListItemDetailObj: ICaseItemCaseDetailObj;
@@ -77,13 +77,14 @@ const ListItem: React.FC<IListItem> = (props) => {
collectionTag,
paymentStatus,
dpdBucket,
dpdCycle,
pinRank,
isSynced,
isNewlyAdded,
interactionStatus,
caseVerdict,
totalOverdueAmount,
distanceInKm,
escalationData,
} = caseListItemDetailObj;
const isVisitPlanStatusLocked = useAppSelector(
@@ -191,6 +192,7 @@ const ListItem: React.FC<IListItem> = (props) => {
!isTeamLead &&
!nearbyCaseView;
const is1To30FieldAgent = useAppSelector((state) => state.user?.is1To30FieldAgent);
const distanceMapOfNearbyCases =
useAppSelector((state) => state.nearbyCasesSlice.caseReferenceIdToDistanceMap) || {};
const selectedTab = useAppSelector((state) => state?.nearbyCasesSlice?.sortTabSelected);
@@ -201,105 +203,144 @@ const ListItem: React.FC<IListItem> = (props) => {
width: showInVisitPlanTag
? TAG_CONTAINER_WIDTH.VISIT_PLAN_TAG
: showVisitPlanBtn
? TAG_CONTAINER_WIDTH.VISIT_PLAN_BUTTON
: TAG_CONTAINER_WIDTH.DEFAULT,
? TAG_CONTAINER_WIDTH.VISIT_PLAN_BUTTON
: TAG_CONTAINER_WIDTH.DEFAULT,
};
const isActiveEscalationCase = Number(escalationData?.activeEscalationCount) > 0;
const activeEscalationCount = Number(escalationData?.activeEscalationCount);
const pastEscalationCount = Number(escalationData?.pastEscalationCount);
const totalEscalationsCount = activeEscalationCount + pastEscalationCount;
return (
<Pressable onPress={handleCaseClick}>
<View>
</View>
<View
style={[
GenericStyles.row,
styles.listItem,
getShadowStyle(2),
{
backgroundColor: isNewlyAdded
? COLORS.BACKGROUND.ORANGE
: isCaseSelected
? COLORS.BACKGROUND.SILVER
: COLORS.BACKGROUND.PRIMARY,
? COLORS.BACKGROUND.SILVER
: COLORS.BACKGROUND.PRIMARY,
},
]}
>
<CaseItemAvatar
caseDetailObj={getCaseItemAvatarCaseDetailObj}
shouldBatchAvatar={shouldBatchAvatar}
/>
{showVisitPlanBtn ? (
<Pressable onPress={handleAvatarClick} style={styles.selectBtn}>
<RoundCheckIcon focused={isCaseSelected} />
</Pressable>
) : null}
{showInVisitPlanTag && (
<View style={[GenericStyles.absolute, styles.visitPlanContainer]}>
<Text style={[GenericStyles.fontSize12, styles.visitPlanText]}>In visit plan</Text>
</View>
)}
{nearbyCaseView && distanceInKm && (
<View style={[GenericStyles.absolute, styles.distanceContainer]}>
<Text style={[GenericStyles.fontSize12, styles.distanceText]}>
{relativeDistanceFormatter(distanceInKm)} km away
</Text>
</View>
)}
<View style={[styles.caseItemInfo]}>
<View style={[styles.tagContainer, widthStyle]}>
{paymentStatus ? (
<View style={[GenericStyles.mr8, GenericStyles.mb8]}>
<Tag
variant={paymentStatusMapping[paymentStatus]?.variant || TagVariant.alert}
text={(paymentStatusMapping[paymentStatus]?.label || paymentStatus) as string}
/>
</View>
) : null}
{collectionTag ? (
<View style={[GenericStyles.mr8]}>
<Tag variant={TagVariant.gray} text={collectionTag} />
</View>
) : null}
{!isVisitPlan && distanceOfCaseItem ? (
<View style={GenericStyles.mb4}>
<Tag
tagIcon={
<LocationDistanceIcon
iconColor={isNearestCaseView ? COLORS.TEXT.BLUE_DARK_2 : COLORS.TEXT.GREY_3}
backgroundColor={
isNearestCaseView ? COLORS.BACKGROUND.BLUE : COLORS.BACKGROUND.BLUE_LIGHT_3
}
/>
}
text={Number(distanceOfCaseItem?.toFixed(1)) + ' KM'}
variant={isNearestCaseView ? TagVariant.darkBlue : TagVariant.darkGray}
/>
</View>
) : null}
</View>
<Heading numberOfLines={1} type={'h5'} bold dark>
{customerName}
</Heading>
{taskTitle ? (
<Text dark bold ellipsizeMode="tail" style={styles.address}>
<Text light>
{/* @ts-ignore */}
<Text>{TaskTitleUIMapping[taskTitle]}</Text>
{displayAddress ? `: ${displayAddress}` : null}
{escalationData && totalEscalationsCount > 0 ? (
isActiveEscalationCase ? (
<View style={[styles.escalationContainer, GenericStyles.row, GenericStyles.alignCenter, GenericStyles.w100,styles.redContainer, {
backgroundColor: COLORS.BACKGROUND.RED
}]}>
<FlagIcon fillColor={COLORS.TEXT.RED} />
<Text small style={styles.activeEscalationText}>
{`${activeEscalationCount} Active Escalation${activeEscalationCount === 1 ? '' : 's'}`}
</Text>
</Text>
</View>
) : (
<Text dark bold ellipsizeMode="tail" style={styles.address}>
<Text light>{displayAddress}</Text>
</Text>
)}
<View>
<Text small style={[styles.caseStatusText, styles.borderTop]} bold>
Total due {formatAmount(totalOverdueAmount, false)}
{' '}DPD bucket {dpdBucket}
</Text>
{caseInteractionStatus ? (
<Text small style={styles.caseStatusText} bold>
{caseInteractionStatus}
<View style={[styles.escalationContainer, GenericStyles.row, GenericStyles.alignCenter, GenericStyles.w100,styles.yellowContainer, {
backgroundColor: COLORS.BACKGROUND.YELLOW_LIGHT
}]}>
<FlagIcon fillColor={COLORS.TEXT.YELLOW_LIGHT} />
<Text small style={styles.pastEscalationText}>
{`${pastEscalationCount} Past Escalation${pastEscalationCount === 1 ? '' : 's'}`}
</Text>
) : null}
</View>
)
) : null}
<View style={[GenericStyles.row, GenericStyles.p12]}>
<CaseItemAvatar
caseDetailObj={getCaseItemAvatarCaseDetailObj}
shouldBatchAvatar={shouldBatchAvatar}
/>
{showVisitPlanBtn ? (
<Pressable onPress={handleAvatarClick} style={styles.selectBtn}>
<RoundCheckIcon focused={isCaseSelected} />
</Pressable>
) : null}
{showInVisitPlanTag && (
<View style={[GenericStyles.absolute, styles.visitPlanContainer]}>
<Text style={[GenericStyles.fontSize12, styles.visitPlanText]}>In visit plan</Text>
</View>
)}
{nearbyCaseView && distanceInKm && (
<View style={[GenericStyles.absolute, styles.distanceContainer]}>
<Text style={[GenericStyles.fontSize12, styles.distanceText]}>
{relativeDistanceFormatter(distanceInKm)} km away
</Text>
</View>
)}
<View style={[styles.caseItemInfo]}>
<View style={[styles.tagContainer, widthStyle]}>
{paymentStatus ? (
<View style={[GenericStyles.mr8, GenericStyles.mb8]}>
<Tag
variant={paymentStatusMapping[paymentStatus]?.variant || TagVariant.alert}
text={(paymentStatusMapping[paymentStatus]?.label || paymentStatus) as string}
/>
</View>
) : null}
{collectionTag ? (
<View style={[GenericStyles.mr8]}>
<Tag variant={TagVariant.gray} text={collectionTag} />
</View>
) : null}
{!isVisitPlan && distanceOfCaseItem ? (
<View style={GenericStyles.mb4}>
<Tag
tagIcon={
<LocationDistanceIcon
iconColor={isNearestCaseView ? COLORS.TEXT.BLUE_DARK_2 : COLORS.TEXT.GREY_3}
backgroundColor={
isNearestCaseView ? COLORS.BACKGROUND.BLUE : COLORS.BACKGROUND.BLUE_LIGHT_3
}
/>
}
text={Number(distanceOfCaseItem?.toFixed(1)) + ' KM'}
variant={isNearestCaseView ? TagVariant.darkBlue : TagVariant.darkGray}
/>
</View>
) : null}
</View>
<Heading numberOfLines={1} type={'h5'} bold dark>
{customerName}
</Heading>
{taskTitle ? (
<Text dark bold ellipsizeMode="tail" style={styles.address}>
<Text light>
{/* @ts-ignore */}
<Text>{TaskTitleUIMapping[taskTitle]}</Text>
{displayAddress ? `: ${displayAddress}` : null}
</Text>
</Text>
) : (
<Text dark bold ellipsizeMode="tail" style={styles.address}>
<Text light>{displayAddress}</Text>
</Text>
)}
<View>
{is1To30FieldAgent ? (
<Text small style={[styles.caseStatusText, styles.borderTop]} bold>
Total due {formatAmount(totalOverdueAmount, false)}
{' '}DPD Cycle {dpdCycle}
</Text>
) : (
<Text small style={[styles.caseStatusText, styles.borderTop]} bold>
Total due {formatAmount(totalOverdueAmount, false)}
{' '}Bucket {dpdBucket}
</Text>
)}
{caseInteractionStatus ? (
<Text small style={styles.caseStatusText} bold>
{caseInteractionStatus}
</Text>
) : null}
</View>
</View>
</View>
</View>
@@ -309,10 +350,32 @@ const ListItem: React.FC<IListItem> = (props) => {
const styles = StyleSheet.create({
listItem: {
padding: 12,
borderRadius: 8,
marginVertical: 6,
marginBottom: 12,
position: 'relative',
overflow: 'hidden'
},
escalationContainer: {
height: 30,
paddingLeft: 12,
paddingRight: 8,
borderTopLeftRadius: 8,
borderTopRightRadius: 8,
borderWidth: 1,
},
redContainer :{
borderColor: COLORS.BORDER.RED,
},
yellowContainer :{
borderColor: COLORS.BORDER.YELLOW,
},
activeEscalationText: {
color: COLORS.TEXT.RED,
marginLeft: 4,
},
pastEscalationText: {
color: COLORS.TEXT.YELLOW_LIGHT,
marginLeft: 4,
},
avatarContainer: {
height: 40,

View File

@@ -344,3 +344,14 @@ export enum FiltePlaceholderText {
CASES = 'cases',
MY_CASES = 'my cases',
}
export interface IRecentEscalationDetails {
customerVoice: string;
createdAt: string;
}
export interface IEscalationSummary {
pastEscalationCount: number;
activeEscalationCount: number;
recentEscalationDetails: IRecentEscalationDetails;
}

View File

@@ -1,6 +1,6 @@
import { CLICKSTREAM_EVENT_NAMES } from '@common/Constants';
import { getDistanceFromLatLonInKm, isFunction } from '@components/utlis/commonFunctions';
import { IGeoLocation } from '@interfaces/addressGeolocation.types';
import { IGeoLocation, IGeolocationCoordinate } from '@interfaces/addressGeolocation.types';
import { addClickstreamEvent } from '@services/clickstreamEventService';
import {
Address,
@@ -88,13 +88,9 @@ export const sectionListTranformData = (agentList: IReportee[]): ISectionListDat
return result;
};
export const getAddressLocation = (addresses: Address[] | undefined) => {
if (!addresses?.length) return null;
for (const address of addresses) {
if (address?.location?.latitude && address?.location?.longitude) {
return address.location;
}
export const getAddressLocation = (address?: IGeolocationCoordinate) => {
if(address?.latitude && address?.longitude) {
return address;
}
return null;
};
@@ -108,7 +104,7 @@ export const getNearByCases = (
let caseIds: Array<string> = [];
casesList?.forEach((pinnedId) => {
const caseDetail = caseDetails?.[pinnedId.caseReferenceId];
const addressLocation = getAddressLocation(caseDetail?.addresses);
const addressLocation = getAddressLocation(caseDetail?.addressLocation);
if (addressLocation) {
const distanceInKm = getDistanceFromLatLonInKm(addressLocation, deviceGeolocationCoordinate);
@@ -194,7 +190,7 @@ export const updateNearbyCasesListAndLocation = (
let caseIdsToDistancesFromCurrentLocationMap: Map<string, number> = new Map();
allCasesList?.forEach((pinnedId) => {
const caseDetail = caseDetails?.[pinnedId?.caseReferenceId] || {};
const addressLocation = getAddressLocation(caseDetail?.addresses);
const addressLocation = getAddressLocation(caseDetail?.addressLocation);
if (addressLocation) {
const distanceInKm = getDistanceFromLatLonInKm(
addressLocation,

View File

@@ -28,6 +28,7 @@ import AdditionalGeolocations from '@screens/addressGeolocation/AdditionalGeoloc
import FeeWaiver from '@screens/emiSchedule/FeeWaiver';
import FeeWaiverHistory from '@screens/emiSchedule/FeeWaiverHistory';
import CallCustomer from './CallCustomer';
import Escalations from '@screens/escalations/Escalations';
const Stack = createNativeStackNavigator();
@@ -53,6 +54,7 @@ export enum CaseDetailStackEnum {
FEE_WAIVER = 'FeeWaiver',
FEE_WAIVER_HISTORY = 'FeeWaiverHistory',
CALL_CUSTOMER = 'CallCustomer',
ESCALATIONS = 'Escalations',
}
const CaseDetailStack = () => {
@@ -101,6 +103,7 @@ const CaseDetailStack = () => {
component={FeedbackDetailContainer}
/>
<Stack.Screen name={CaseDetailStackEnum.EMI_SCHEDULE} component={EmiSchedule} />
<Stack.Screen name={CaseDetailStackEnum.ESCALATIONS} component={Escalations} />
<Stack.Screen name={CaseDetailStackEnum.FEE_WAIVER} component={FeeWaiver} />
<Stack.Screen name={CaseDetailStackEnum.FEE_WAIVER_HISTORY} component={FeeWaiverHistory} />
<Stack.Screen name={CaseDetailStackEnum.ADD_NEW_NUMBER} component={AddNewNumber} />

View File

@@ -11,6 +11,7 @@ import { addClickstreamEvent } from '../../services/clickstreamEventService';
import { CLICKSTREAM_EVENT_NAMES } from '../../common/Constants';
import { ToastMessages } from '../allCases/constants';
import { toTileCase } from '../../components/utlis/commonFunctions';
import { useAppSelector } from '@hooks';
interface ICollectionCaseData {
caseData: CaseDetail;
@@ -22,12 +23,13 @@ const CollectionCaseData: React.FC<ICollectionCaseData> = ({ caseData }) => {
currentDpd,
loanAccountNumber,
dpdBucket,
dpdCycle,
pos,
collectionTag,
employmentDetail,
unpaidDays
} = caseData;
const is1To30FieldAgent = useAppSelector((state) => state.user?.is1To30FieldAgent );
return (
<View>
{fatherName && (
@@ -67,9 +69,15 @@ const CollectionCaseData: React.FC<ICollectionCaseData> = ({ caseData }) => {
GenericStyles.flexWrap,
]}
>
<Text style={[styles.greyText]} small>
DPD bucket {dpdBucket}
</Text>
{is1To30FieldAgent ? (
<Text style={[styles.greyText]} small>
DPD Cycle {dpdCycle}
</Text>
) : (
<Text style={[styles.greyText]} small>
Bucket {dpdBucket}
</Text>
)}
<View style={styles.lineStyle} />
<Text style={[styles.greyText]} small>
POS {formatAmount(pos)}

View File

@@ -6,9 +6,8 @@ import CaseDetailsHeader from './CaseDetailHeader';
import UserDetailsSection from './UserDetailsSection';
import Layout from '../layout/Layout';
import { _map } from '../../../RN-UI-LIB/src/utlis/common';
import { AppDispatch, RootState } from '../../store/store';
import { RootState } from '../../store/store';
import { getCaseUnifiedData, UnifiedCaseDetailsTypes } from '../../action/caseApiActions';
import useIsOnline from '../../hooks/useIsOnline';
import { CLICKSTREAM_EVENT_NAMES } from '../../common/Constants';
import { addClickstreamEvent } from '../../services/clickstreamEventService';
import ScreenshotBlocker from '../../components/utlis/ScreenshotBlocker';
@@ -21,9 +20,7 @@ import CollectionCaseDetailFooter from './CollectionCaseDetailFooter';
import FeedbackDetailsSection from './FeedbackDetailsSection';
import { COLORS } from '@rn-ui-lib/colors';
import { syncActiveCallDetails } from '@actions/callRecordingActions';
import { getCustomerDocuments } from '@screens/allCases/utils';
import { logError } from '@components/utlis/errorUtils';
import { GenericObject, GenericType } from '@common/GenericTypes';
import EscalationsSection from '@screens/escalations/EscalationsSection';
import { getUngroupedAddress } from '@actions/addressGeolocationAction';
interface ICaseDetails {
@@ -52,10 +49,14 @@ const CollectionCaseDetails: React.FC<ICaseDetails> = (props) => {
const duration = 300;
const dispatch = useAppDispatch();
const isFocused = useIsFocused();
const isOnline = useIsOnline();
const [isDocumentsLoading, setIsDocumentsLoading] = React.useState(false);
const {escalationData} = caseDetail || {};
const activeEscalationCount = Number(escalationData?.activeEscalationCount);
const pastEscalationCount = Number(escalationData?.pastEscalationCount);
const totalEscalationsCount = activeEscalationCount + pastEscalationCount;
useEffect(() => {
if (caseId) dispatch(setSelectedCaseId(caseId));
@@ -128,6 +129,7 @@ const CollectionCaseDetails: React.FC<ICaseDetails> = (props) => {
]}
>
<ViewAddressSection caseId={caseId} />
{escalationData && totalEscalationsCount > 0 ? (<EscalationsSection caseId={caseId} />): null}
<EmiDetailsSection caseId={caseId} />
<CollectMoneySection caseId={caseId} />
<FeedbackDetailsSection caseId={caseId} />

View File

@@ -22,9 +22,9 @@ const ViewAddressSection = ({ caseId }: IViewAddressSection) => {
const { addressString, loanAccountNumber, customerReferenceId, addressStringType } = caseDetail;
const getTabName = () => {
if (addressStringType === AddressTabType.GEO_LOCATION) {
return AddressGeolocationTabEnum.GEOLOCATION;
}
// if (addressStringType === AddressTabType.GEO_LOCATION) {
// return AddressGeolocationTabEnum.GEOLOCATION;
// }
return AddressGeolocationTabEnum.ADDRESS;
};

View File

@@ -284,7 +284,9 @@ export interface CaseDetail {
customerName: string;
pos: number;
dpdBucket: string;
dpdCycle: string;
addresses?: Address[];
addressLocation?: IGeolocationCoordinate;
currentAllocationReferenceId: string;
customerReferenceId: string;
caseViewCreatedAt?: number;
@@ -314,6 +316,18 @@ export interface CaseDetail {
employmentDetail?: EmploymentDetails;
unpaidDays?: number;
addressStringType?: string;
escalationData ?: escalationData;
}
export interface recentEscalationDetails {
createdAt : string;
customerVoice : string;
}
export interface escalationData {
activeEscalationCount : number;
pastEscalationCount : number;
recentEscalationDetails : recentEscalationDetails;
}
export interface AddressesGeolocationPayload {

View File

@@ -139,11 +139,11 @@ const RenderIcons: React.FC<IRenderIcons> = ({
);
})}
</View>
{telephoneNumberMaskingEnabled ? (
{/* {telephoneNumberMaskingEnabled ? (
<Text style={styles.callAttempted}>
Attempts: {totalGenuineCallsAttempted}/{totalGenuineCallsRequired}
</Text>
) : null}
) : null} */}
</>
);
};

View File

@@ -0,0 +1,94 @@
import React, { useEffect, useState } from 'react';
import { ScrollView, View } from 'react-native';
import { CLICKSTREAM_EVENT_NAMES } from '../../common/Constants';
import { ToastMessages } from '../allCases/constants';
import useIsOnline from '../../hooks/useIsOnline';
import { addClickstreamEvent } from '../../services/clickstreamEventService';
import { goBack } from '../../components/utlis/navigationUtlis';
import { toast } from '../../../RN-UI-LIB/src/components/toast';
import { GenericStyles } from '../../../RN-UI-LIB/src/styles';
import NavigationHeader from '../../../RN-UI-LIB/src/components/NavigationHeader';
import SuspenseLoader from '../../../RN-UI-LIB/src/components/suspense_loader/SuspenseLoader';
import LineLoader from '../../../RN-UI-LIB/src/components/suspense_loader/LineLoader';
import { COLORS } from '../../../RN-UI-LIB/src/styles/colors';
import Pagination from '../../../RN-UI-LIB/src/components/pagination/Pagination';
import { useAppDispatch, useAppSelector } from '@hooks';
import { RootState } from '@store';
import { getAllEscalations } from './actions';
import { setEscalationData } from '@reducers/escalationSlice';
import { IEscalationsDetails } from './escalations.interfaces';
import EscalationsList from './EscalationsList';
const Escalations: React.FC<IEscalationsDetails> = (props) => {
const {
route: {
params: { loanAccountNumber },
},
} = props;
const { escalationData, isLoading, pageData } = useAppSelector((state: RootState) => state?.escalationSlice);
const [currentPage, setCurrentPage] = useState(1);
const isOnline = useIsOnline();
const dispatch = useAppDispatch();
useEffect(() => {
dispatch(getAllEscalations(loanAccountNumber, { page_no: currentPage - 1, page_size: 10 }));
}, [currentPage]);
const totalEscalationsCount = pageData?.totalElements || 0;
useEffect(() => {
if (!isOnline) {
setCurrentPage(1);
}
}, [isOnline]);
useEffect(() => {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_VIEW_ALL_ESCALATIONS_SCREEN_LANDED);
return () => {
dispatch(setEscalationData([]));
};
}, []);
const handlePageChange = (page: number) => {
if (!isOnline) {
toast({ type: 'info', text1: ToastMessages.OFFLINE_MESSAGE });
return;
}
setCurrentPage(page);
};
return (
<View style={[GenericStyles.fill, { backgroundColor: COLORS.BACKGROUND.SILVER }]}>
<NavigationHeader title={`All Escalations (${totalEscalationsCount})`} onBack={goBack} />
<ScrollView style={[GenericStyles.ph16, GenericStyles.fill]} contentContainerStyle={GenericStyles.pb16}>
<SuspenseLoader
loading={isLoading}
fallBack={
<View style={GenericStyles.pt12}>
{[...Array(8).keys()].map(() => (
<LineLoader
width={'100%'}
height={150}
style={[GenericStyles.br6, GenericStyles.mb20]}
/>
))}
</View>
}
>
<EscalationsList escalationData={escalationData} />
</SuspenseLoader>
</ScrollView>
{escalationData?.length && pageData?.totalPages > 1 ? (
<Pagination
onPageChange={handlePageChange}
currentPage={currentPage}
totalPages={pageData.totalPages}
/>
) : null}
</View>
);
};
export default Escalations;

View File

@@ -0,0 +1,115 @@
import React from 'react';
import { ESCALATION_STATUS, IEscalationsList } from './escalations.interfaces';
import FlagIcon from '@assets/icons/FlagIcon';
import { COLORS } from '@rn-ui-lib/colors';
import Tag, { TagVariant } from '@rn-ui-lib/components/Tag';
import { GenericStyles, getShadowStyle } from '@rn-ui-lib/styles';
import { COSMOS_STANDARD_DATE_FORMAT, COSMOS_STANDARD_TIME_FORMAT } from '@rn-ui-lib/utils/dates';
import dayjs from 'dayjs';
import Text from '@rn-ui-lib/components/Text';
import { View, StyleSheet } from 'react-native';
const EscalationsList: React.FC<IEscalationsList> = (props) => {
const { escalationData } = props;
const noEscalationsFound = !(escalationData?.length);
if (noEscalationsFound) {
return (
<View
style={[
GenericStyles.centerAlignedRow,
GenericStyles.fill,
GenericStyles.mt32
]}
>
<Text>No escalations found for the customer </Text>
</View>
);
}
return <>
{escalationData?.map((escalation) => (
<View key={escalation.createdAt} >
<View
style={[
GenericStyles.whiteBackground,
GenericStyles.w100,
getShadowStyle(3),
GenericStyles.mt16,
styles.secondSection,
]}
>
<View style={[GenericStyles.row, GenericStyles.p16]}>
<View style={[styles.flagContainer, GenericStyles.mr16, GenericStyles.centerAligned, {
backgroundColor: escalation.status === ESCALATION_STATUS.OPEN ? COLORS.BACKGROUND.RED : COLORS.BACKGROUND.YELLOW_LIGHT
}, { borderColor: escalation.status === ESCALATION_STATUS.OPEN ? COLORS.BORDER.RED : COLORS.BORDER.YELLOW }]}>
<FlagIcon fillColor={escalation.status === ESCALATION_STATUS.OPEN ? COLORS.TEXT.RED : COLORS.TEXT.YELLOW_LIGHT} width={20} height={20} />
</View>
<View style={[styles.upperContainer]}>
<Text dark bold style={[GenericStyles.fontSize14, GenericStyles.mb4, styles.textColorGrayscale]}>
{escalation.voc}
</Text>
<View style={[GenericStyles.row, GenericStyles.spaceBetween]}>
<View style={[GenericStyles.row]}>
<Text style={[GenericStyles.fontSize13, GenericStyles.mr4, styles.textColorGrayscale2]}>
{dayjs(escalation.createdAt).format(COSMOS_STANDARD_DATE_FORMAT)}
</Text>
<Text style={[GenericStyles.fontSize13, styles.textColorGrayscale3]}>
{'|'}
</Text>
<Text style={[GenericStyles.fontSize13, GenericStyles.ml4, styles.textColorGrayscale2]}>
{dayjs(escalation.createdAt).format(COSMOS_STANDARD_TIME_FORMAT)}
</Text>
</View>
{escalation.status === ESCALATION_STATUS.OPEN ? (
<Tag variant={TagVariant.error} text="Open" />
) : (
<Tag variant={TagVariant.yellow} text="Closed" />
)}
</View>
</View>
</View>
<View style={[GenericStyles.borderTop, GenericStyles.w100]} />
<View style={[GenericStyles.p16]}>
<Text style={[styles.textColorGrayscale3]}>Description</Text>
<Text style={[styles.textColorGrayscale2, GenericStyles.fontSize13]}>{escalation.title}</Text>
</View>
</View>
</View>
)
)}
</>
}
const styles = StyleSheet.create({
secondSection: {
alignSelf: 'center',
borderRadius: 16,
width: '98%',
borderWidth: 1,
borderColor: COLORS.BACKGROUND.GREY_LIGHT,
},
textColorGrayscale: {
color: COLORS.TEXT.DARK,
},
textColorGrayscale2: {
color: COLORS.TEXT.BLACK,
},
textColorGrayscale3: {
color: COLORS.TEXT.LIGHT,
},
upperContainer: {
flex: 1
},
flagContainer: {
height: 40,
width: 40,
borderRadius: 20,
borderWidth: 1,
},
});
export default EscalationsList;

View File

@@ -0,0 +1,153 @@
import React from 'react';
import { StyleSheet, TouchableOpacity, View } from 'react-native';
import { COLORS } from '@rn-ui-lib/colors';
import { GenericStyles, getShadowStyle } from '@rn-ui-lib/styles';
import Text from '@rn-ui-lib/components/Text';
import Chevron from '@rn-ui-lib/icons/Chevron';
import { CLICKSTREAM_EVENT_NAMES } from '@common/Constants';
import { navigateToScreen } from '@components/utlis/navigationUtlis';
import { RootState } from '@store';
import { useAppSelector } from '@hooks';
import { addClickstreamEvent } from '@services/clickstreamEventService';
import { CaseDetailStackEnum } from '../caseDetails/CaseDetailStack';
import FlagIcon from '@assets/icons/FlagIcon';
import dayjs from 'dayjs';
import { COSMOS_STANDARD_DATE_FORMAT, COSMOS_STANDARD_TIME_FORMAT } from '@rn-ui-lib/utils/dates';
interface IEscalationsSection {
caseId: string;
}
const EscalationsSection = ({ caseId }: IEscalationsSection) => {
const caseDetail = useAppSelector((state: RootState) => state.allCases?.caseDetails?.[caseId]) || {};
const { escalationData, loanAccountNumber } = caseDetail || {};
const handleOnPressClick = () => {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_VIEW_ALL_ESCALATIONS_SCREEN_CLICKED, {
lan: loanAccountNumber,
});
navigateToScreen(CaseDetailStackEnum.ESCALATIONS, {
loanAccountNumber
})
};
const isActiveEscalationCase = Number(escalationData?.activeEscalationCount) > 0;
const activeEscalationCount = Number(escalationData?.activeEscalationCount);
const pastEscalationCount = Number(escalationData?.pastEscalationCount);
const totalEscalationsCount = activeEscalationCount + pastEscalationCount;
const escalationCreatedAt = escalationData?.recentEscalationDetails?.createdAt;
const customerVoice = escalationData?.recentEscalationDetails?.customerVoice;
const escalationRaisedDate = dayjs(escalationCreatedAt).format(COSMOS_STANDARD_DATE_FORMAT);
const escalationRaisedTime = dayjs(escalationCreatedAt).format(COSMOS_STANDARD_TIME_FORMAT);
return (
<View
style={[
GenericStyles.whiteBackground,
styles.br16,
GenericStyles.w100,
getShadowStyle(2),
GenericStyles.mt16,
]}
>
<View style={[GenericStyles.row, GenericStyles.alignCenter, GenericStyles.relative]}>
<TouchableOpacity
activeOpacity={0.7}
onPress={handleOnPressClick}
>
<View style={GenericStyles.fill}>
{isActiveEscalationCase ? (
<View style={[styles.escalationContainer, GenericStyles.row, GenericStyles.alignCenter, styles.redContainer, {
backgroundColor: COLORS.BACKGROUND.RED
}]}>
<FlagIcon fillColor={COLORS.TEXT.RED} />
<Text small style={styles.activeEscalationText}>
{`${activeEscalationCount} Active Escalation${activeEscalationCount === 1 ? '' : 's'}`}
</Text>
</View>
) : (
<View style={[styles.escalationContainer, GenericStyles.row, GenericStyles.alignCenter, styles.yellowContainer, {
backgroundColor: COLORS.BACKGROUND.YELLOW_LIGHT
}]}>
<FlagIcon fillColor={COLORS.TEXT.YELLOW_LIGHT} />
<Text small style={styles.pastEscalationText}>
{`${pastEscalationCount} Past Escalation${pastEscalationCount === 1 ? '' : 's'}`}
</Text>
</View>)
}
<View style={[GenericStyles.columnDirection, GenericStyles.pl16]}>
<Text dark bold style={[GenericStyles.fontSize14, GenericStyles.mb4]}>
{customerVoice || 'Escalation Against Agent'}
</Text>
<View style={[GenericStyles.row, GenericStyles.alignCenter]}>
<Text style={[GenericStyles.fontSize13]}>
{'Raised on: '}
</Text>
{escalationRaisedDate ? <>
<Text dark bold style={[GenericStyles.fontSize13, GenericStyles.ml4, GenericStyles.mr4]}>
{escalationRaisedDate}
</Text>
<Text style={[GenericStyles.fontSize13, styles.textColorGrayscale3]}>
{'|'}
</Text>
<Text dark bold style={[GenericStyles.fontSize13, GenericStyles.ml4]}>
{escalationRaisedTime}
</Text>
</> : <Text>Unknown</Text>}
</View>
</View>
<View style={[GenericStyles.row, GenericStyles.alignCenter, GenericStyles.pt12, GenericStyles.pb16, GenericStyles.pl16]}>
<Text style={[styles.viewAllButton]}>View all {`(${totalEscalationsCount})`}</Text>
<Chevron fillColor={COLORS.TEXT.BLUE} />
</View>
</View>
</TouchableOpacity>
</View>
</View>
);
};
export const styles = StyleSheet.create({
escalationContainer: {
height: 26,
marginBottom: 16,
paddingLeft: 16,
paddingRight: 16,
borderBottomRightRadius: 6,
borderTopLeftRadius: 16,
alignSelf: 'flex-start',
borderWidth: 1,
},
activeEscalationText: {
color: COLORS.TEXT.RED,
marginLeft: 4,
},
pastEscalationText: {
color: COLORS.TEXT.YELLOW_LIGHT,
marginLeft: 4,
},
redContainer: {
borderColor: COLORS.BORDER.RED,
},
yellowContainer: {
borderColor: COLORS.BORDER.YELLOW,
},
textColorGrayscale2: {
color: COLORS.TEXT.BLACK,
},
textColorGrayscale3: {
color: COLORS.TEXT.LIGHT,
},
viewAllButton: {
color: COLORS.TEXT.BLUE,
fontSize: 12,
lineHeight: 15,
marginRight: 10,
},
br16: {
borderRadius: 16,
},
});
export default EscalationsSection;

View File

@@ -0,0 +1,43 @@
import axiosInstance, { getApiUrl, ApiKeys } from "@components/utlis/apiHelper";
import { logError } from "@components/utlis/errorUtils";
import { setEscalationData, setIsLoading, setPageData } from "@reducers/escalationSlice";
import { AppDispatch } from "@store";
export interface IEscalationPayload {
page_no: number;
page_size: number;
}
export interface IAllEscalationsPayload {
lan: string;
escalationPayload: IEscalationPayload;
}
export const getAllEscalations = (
lan: string,
escalationPayload: IEscalationPayload = {
page_no: 10,
page_size: 0
}
) => (dispatch: AppDispatch) => {
dispatch(setIsLoading(true));
const payload = {
loan_account_number: lan,
...escalationPayload,
}
const url = getApiUrl(ApiKeys.ALL_ESCALATIONS, {}, payload );
return axiosInstance
.get(url)
.then((response) => {
if (response?.data) {
dispatch(setEscalationData(response.data.data));
dispatch(setPageData(response.data.pages));
}
})
.catch((err) => {
logError(err);
})
.finally(() => {
dispatch(setIsLoading(false));
});
}

View File

@@ -0,0 +1,35 @@
export enum ESCALATION_STATUS {
OPEN = 'OPEN',
CLOSED = 'CLOSED',
}
export interface IEscalationsSlice {
escalationData: IAllEscalationData[];
isLoading: boolean;
pageData: pageData;
}
export interface pageData {
totalPages: number;
totalElements: number;
}
export interface IAllEscalationData {
createdAt: string;
loanAccountNumber: string;
status: string;
title: string;
voc: string;
}
export interface IEscalationsDetails {
route: {
params: {
loanAccountNumber: string;
};
};
}
export interface IEscalationsList {
escalationData: IAllEscalationData[];
}

View File

@@ -364,6 +364,13 @@ const NotificationTemplate: React.FC<INotificationTemplateProps> = ({ data }) =>
<Text light>has been deallocated from your case list due to a pause in collection efforts.</Text>
</Text>
);
case NotificationTypes.CASE_ESCALATION_NOTIFICATION_TEMPLATE:
return (
<Text>
<Text light>raised a new{' '}</Text>
<Text bold dark>escalation</Text>
</Text>
);
default:
return <Text>New notification </Text>;
}

View File

@@ -21,6 +21,7 @@ import PerformanceLevelMayDropIcon from "@assets/icons/PerformanceLevelMayDropIc
import PerformanceLevelMayIncreaseIcon from "@assets/icons/PerformanceLevelMayIncreaseIcon";
import MissedCallNotificationIcon from '@rn-ui-lib/icons/MissedCallNotificationIcon';
import InfoNotificationIcon from '@assets/icons/InfoNotificationIcon';
import EscalationsIcon from '@assets/icons/EscalationIcon';
export enum NotificationTypes {
PAYMENT_MADE_TEMPLATE = 'PAYMENT_MADE_TEMPLATE',
@@ -61,6 +62,8 @@ export enum NotificationTypes {
AGENT_REVIVAL_PERFORMANCE_LEVEL_MAY_INCREASED_REMINDER_NOTIFICATION = 'AGENT_REVIVAL_PERFORMANCE_LEVEL_MAY_INCREASED_REMINDER_NOTIFICATION',
CUSTOMER_TRIED_CALLING_NOTIFICATION = 'CUSTOMER_TRIED_CALLING_NOTIFICATION',
COLLECTION_PAUSE_EFFORTS_CASE_DEALLOCATED = 'COLLECTION_PAUSE_EFFORTS_CASE_DEALLOCATED',
CASE_ESCALATION_NOTIFICATION_TEMPLATE='CASE_ESCALATION_NOTIFICATION_TEMPLATE',
COLLECTION_PAUSE_EFFORTS_CASE_DEALLOCATED = 'COLLECTION_PAUSE_EFFORTS_CASE_DEALLOCATED',
}
export const NotificationIconsMap = {
@@ -102,6 +105,8 @@ export const NotificationIconsMap = {
[NotificationTypes.AGENT_REVIVAL_PERFORMANCE_LEVEL_MAY_INCREASED_REMINDER_NOTIFICATION]: <PerformanceLevelMayIncreaseIcon />,
[NotificationTypes.CUSTOMER_TRIED_CALLING_NOTIFICATION]: <MissedCallNotificationIcon />,
[NotificationTypes.COLLECTION_PAUSE_EFFORTS_CASE_DEALLOCATED]: <InfoNotificationIcon />,
[NotificationTypes.CASE_ESCALATION_NOTIFICATION_TEMPLATE]: <EscalationsIcon/>,
[NotificationTypes.COLLECTION_PAUSE_EFFORTS_CASE_DEALLOCATED]: <InfoNotificationIcon />,
};
export enum WidgetStatus {

View File

@@ -21,15 +21,24 @@ export interface IForeclosureBreakup {
penaltyCharges: number;
closingInterest: number;
excessAvailable: number;
totalAmountPreWaiver?: number;
penaltyChargesPreWaiver?: number;
}
const ForeclosureBreakupAccordion: React.FC<IForeclosureBreakupAccordion> = ({
title,
foreclosureBreakup,
defaultExpanded,
}) => {
const { totalAmount, principal, interest, penaltyCharges, excessAvailable, closingInterest } =
foreclosureBreakup;
const {
totalAmount,
principal,
interest,
penaltyCharges,
excessAvailable,
closingInterest,
totalAmountPreWaiver,
penaltyChargesPreWaiver = 0,
} = foreclosureBreakup;
const foreclosureBreakupMap = useMemo(
() => [
{
@@ -43,6 +52,8 @@ const ForeclosureBreakupAccordion: React.FC<IForeclosureBreakupAccordion> = ({
{
label: 'Penalty charges',
value: penaltyCharges,
preAmount: penaltyChargesPreWaiver,
showPreAmount: penaltyChargesPreWaiver > 0,
},
{
label: 'Closing interest',
@@ -53,7 +64,7 @@ const ForeclosureBreakupAccordion: React.FC<IForeclosureBreakupAccordion> = ({
value: excessAvailable,
},
],
[principal, interest, penaltyCharges, excessAvailable, closingInterest]
[principal, interest, penaltyCharges, excessAvailable, closingInterest, penaltyChargesPreWaiver]
);
return (
<View style={GenericStyles.mt16}>
@@ -75,8 +86,8 @@ const ForeclosureBreakupAccordion: React.FC<IForeclosureBreakupAccordion> = ({
containerStyle={GenericStyles.silverBackground}
defaultExpanded={defaultExpanded}
>
{foreclosureBreakupMap.map(({ label, value }) =>
value > 0 ? (
{foreclosureBreakupMap.map(({ label, value, preAmount, showPreAmount }) =>
value > 0 || showPreAmount ? (
<View
style={[
GenericStyles.row,
@@ -91,7 +102,16 @@ const ForeclosureBreakupAccordion: React.FC<IForeclosureBreakupAccordion> = ({
loading={totalAmount === 0}
fallBack={<LineLoader width={60} height={10} />}
>
<Text dark>{formatAmount(value)}</Text>
<View style={GenericStyles.row}>
<Text style={[GenericStyles.mr8]} dark>
{formatAmount(value)}
</Text>
{showPreAmount ? (
<Text style={[styles.strikethrough, GenericStyles.mr8]} dark>
{formatAmount(preAmount)}
</Text>
) : null}
</View>
</SuspenseLoader>
</View>
) : null
@@ -109,4 +129,7 @@ const styles = StyleSheet.create({
color: COLORS.TEXT.GREEN,
marginRight: 14,
},
strikethrough: {
textDecorationLine: 'line-through',
},
});

View File

@@ -64,7 +64,7 @@ const sendToWhatsapp = (
let message = `*Visit Feedback* for ${sanitizeString(caseDetails?.customerName)}
_${sanitizeString(dateFormat(new Date(feedbackItem?.createdAt), 'DD MMM, YYYY | HH:mm a.'))}_\n
*LAN*: ${sanitizeString(caseDetails?.loanAccountNumber)}
*DPD Bucket*: ${sanitizeString(caseDetails?.dpdBucket)}
*Bucket*: ${sanitizeString(caseDetails?.dpdBucket)}
*EMI Amount*: ₹${getSanitizedCommaAmount(caseDetails?.currentOutstandingEmi)}\n
*Disposition*: ${sanitizeString(feedbackItem?.interactionStatus)}`;

View File

@@ -16,6 +16,7 @@ import repaymentsSlice from '../reducer/repaymentsSlice';
import feedbackHistorySlice from '../reducer/feedbackHistorySlice';
import notificationsSlice from '../reducer/notificationsSlice';
import MetadataSlice from '../reducer/metadataSlice';
import AppUpdateSlice from '../reducer/appUpdateSlice';
import foregroundServiceSlice from '../reducer/foregroundServiceSlice';
import feedbackImagesSlice from '../reducer/feedbackImagesSlice';
import configSlice from '../reducer/configSlice';
@@ -32,6 +33,7 @@ import commitmentTrackerSlice from '@reducers/commitmentTrackerSlice';
import nearbyCasesSlice from '@reducers/nearbyCasesSlice';
import activeCallSlice from '@reducers/activeCallSlice';
import documentsSlice from '@reducers/documentsSlice';
import escalationSlice from '@reducers/escalationSlice';
const rootReducer = combineReducers({
case: caseReducer,
@@ -47,6 +49,7 @@ const rootReducer = combineReducers({
address: addressSlice,
notifications: notificationsSlice,
metadata: MetadataSlice,
appUpdate: AppUpdateSlice,
foregroundService: foregroundServiceSlice,
feedbackImages: feedbackImagesSlice,
config: configSlice,
@@ -64,6 +67,7 @@ const rootReducer = combineReducers({
nearbyCasesSlice: nearbyCasesSlice,
activeCall: activeCallSlice,
documentsSlice: documentsSlice,
escalationSlice: escalationSlice,
});
const persistConfig = {
@@ -86,7 +90,8 @@ const persistConfig = {
'foregroundService',
'feedbackFilters',
'litmusExperiment',
'activeCall'
'activeCall',
'appUpdate'
],
blackList: [
'case',