NTP-10375 | Tele-Cosmos Soft Release (#1000)
This commit is contained in:
2
.github/workflows/codePush.yml
vendored
2
.github/workflows/codePush.yml
vendored
@@ -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
|
||||
|
||||
6
.github/workflows/semgrep.yml
vendored
6
.github/workflows/semgrep.yml
vendored
@@ -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
|
||||
7
App.tsx
7
App.tsx
@@ -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;
|
||||
|
||||
Submodule RN-UI-LIB updated: a42531156f...019bc50b01
@@ -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)) ??
|
||||
|
||||
29
src/assets/icons/EscalationIcon.tsx
Normal file
29
src/assets/icons/EscalationIcon.tsx
Normal 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;
|
||||
32
src/assets/icons/FlagIcon.tsx
Normal file
32
src/assets/icons/FlagIcon.tsx
Normal 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
|
||||
22
src/assets/icons/PostOperativeHoursIcon.tsx
Normal file
22
src/assets/icons/PostOperativeHoursIcon.tsx
Normal 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;
|
||||
@@ -107,7 +107,6 @@ const BlockerScreen = (props: IBlockerScreen) => {
|
||||
|
||||
if (!flavorToUpdate) return;
|
||||
const currentBuildNumber = getBuildVersion();
|
||||
|
||||
if (
|
||||
currentBuildNumber &&
|
||||
!isNaN(currentBuildNumber) &&
|
||||
|
||||
@@ -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),
|
||||
|
||||
116
src/common/PostOperativeHours.tsx
Normal file
116
src/common/PostOperativeHours.tsx
Normal 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;
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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?.[
|
||||
|
||||
@@ -63,4 +63,3 @@ export const sendContentToWhatsapp = (
|
||||
fileName: string
|
||||
): Promise<boolean> =>
|
||||
DeviceUtilsModule?.sendContentToWhatsapp(message, imageUrl, mimeType, format, fileName);
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
// });
|
||||
};
|
||||
|
||||
29
src/reducer/appUpdateSlice.ts
Normal file
29
src/reducer/appUpdateSlice.ts
Normal 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;
|
||||
28
src/reducer/escalationSlice.ts
Normal file
28
src/reducer/escalationSlice.ts
Normal 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;
|
||||
@@ -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;
|
||||
|
||||
@@ -24,10 +24,10 @@ export const ADDRESSES_TABS = [
|
||||
key: 'address',
|
||||
label: 'Addresses',
|
||||
},
|
||||
{
|
||||
key: 'geolocation',
|
||||
label: 'Geolocations',
|
||||
},
|
||||
// {
|
||||
// key: 'geolocation',
|
||||
// label: 'Geolocations',
|
||||
// },
|
||||
];
|
||||
|
||||
export enum AddressGeolocationTabEnum {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -139,11 +139,11 @@ const RenderIcons: React.FC<IRenderIcons> = ({
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
{telephoneNumberMaskingEnabled ? (
|
||||
{/* {telephoneNumberMaskingEnabled ? (
|
||||
<Text style={styles.callAttempted}>
|
||||
Attempts: {totalGenuineCallsAttempted}/{totalGenuineCallsRequired}
|
||||
</Text>
|
||||
) : null}
|
||||
) : null} */}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
94
src/screens/escalations/Escalations.tsx
Normal file
94
src/screens/escalations/Escalations.tsx
Normal 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;
|
||||
115
src/screens/escalations/EscalationsList.tsx
Normal file
115
src/screens/escalations/EscalationsList.tsx
Normal 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;
|
||||
|
||||
|
||||
|
||||
153
src/screens/escalations/EscalationsSection.tsx
Normal file
153
src/screens/escalations/EscalationsSection.tsx
Normal 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;
|
||||
43
src/screens/escalations/actions.ts
Normal file
43
src/screens/escalations/actions.ts
Normal 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));
|
||||
});
|
||||
}
|
||||
35
src/screens/escalations/escalations.interfaces.ts
Normal file
35
src/screens/escalations/escalations.interfaces.ts
Normal 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[];
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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)}`;
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user