diff --git a/.github/workflows/codePush.yml b/.github/workflows/codePush.yml
index 748c5f0c..d1fdea92 100644
--- a/.github/workflows/codePush.yml
+++ b/.github/workflows/codePush.yml
@@ -3,26 +3,29 @@ name: code-push-cli
on:
workflow_dispatch:
inputs:
- environment:
- description: Choose build environment
- required: true
- type: choice
- options:
- - QA
- - Prod
- target_versions:
- description: please enter target versions
- required: true
- type: string
- default: "2.3.4"
- description:
- description: Enter please add change log
- required: true
- type: string
- default: "login sso"
+ environment:
+ description: Choose build environment
+ required: true
+ type: choice
+ options:
+ - QA
+ - Prod
+ target_versions:
+ description: please enter target versions
+ required: true
+ type: string
+ default: '2.3.4'
+ description:
+ description: Enter please add change log
+ required: true
+ type: string
+ default: 'login sso'
jobs:
generate:
- runs-on: [ default ]
+ runs-on: [default]
+ outputs:
+ package_version: ${{ steps.get_version.outputs.version }}
+ build_number: ${{ steps.get_package_info.outputs.buildNumber }}
steps:
- name: Checkout
uses: actions/checkout@v2
@@ -33,10 +36,15 @@ jobs:
uses: actions/setup-node@v3
with:
node-version: 16.x
+ - name: Get version from package.json
+ id: get_version
+ run: |
+ echo "::set-output name=version::$(node -p "require('./package.json').version")"
+ echo "::set-output name=buildNumber::$(node -p "require('./package.json').buildNumber")"
- name: Install yarn
run: npm install --global yarn
- name: Install appcenter cli
- run: npm install -g appcenter-cli
+ run: npm install -g appcenter-cli
- name: Install dependency
run: yarn
- name: AppCenter login
@@ -47,3 +55,32 @@ jobs:
- name: CodePush Prod
if: ((github.event.inputs.environment == 'Prod' || inputs.environment == 'Prod'))
run: yarn move:prod && appcenter codepush release-react -a nfa-navi.com/nfa-app -d Production -t "${{github.event.inputs.target_versions}}" --description "${{github.event.inputs.description}}"
+
+ create_release_tag:
+ needs: generate
+ runs-on: ubuntu-latest
+ if: success() && (github.event.inputs.environment == 'Prod') # Only create tag for Prod releases, have put QA for testing
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v2
+
+ - name: Create release tag
+ run: |
+ TAG_NAME="${{ needs.generate.outputs.package_version }}"
+ RELEASE_NAME="$TAG_NAME (build ${{ needs.generate.outputs.build_number }})"
+ DESCRIPTION="${{ github.event.inputs.description }}"
+
+ # Create a release tag using the GitHub API
+ curl \
+ -X POST \
+ -H "Authorization: token ${{ secrets.MY_REPO_PAT }}" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "tag_name": "'$TAG_NAME'",
+ "target_commitish": "main",
+ "name": "'$RELEASE_NAME'",
+ "body": "'$DESCRIPTION'",
+ "draft": false,
+ "prerelease": false
+ }' \
+ https://api.github.com/repos/${{ github.repository }}/releases
diff --git a/android/app/build.gradle b/android/app/build.gradle
index e40edbc3..d7838baf 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -134,8 +134,8 @@ def reactNativeArchitectures() {
return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
}
-def VERSION_CODE = 127
-def VERSION_NAME = "2.7.9"
+def VERSION_CODE = 137
+def VERSION_NAME = "2.8.8"
android {
ndkVersion rootProject.ext.ndkVersion
diff --git a/package.json b/package.json
index b397175e..8c754163 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "AV_APP",
- "version": "2.7.9",
- "buildNumber": "127",
+ "version": "2.8.8",
+ "buildNumber": "137",
"private": true,
"scripts": {
"android:dev": "yarn move:dev && react-native run-android",
diff --git a/src/action/addressGeolocationAction.ts b/src/action/addressGeolocationAction.ts
index 07c230a5..e6178fe8 100644
--- a/src/action/addressGeolocationAction.ts
+++ b/src/action/addressGeolocationAction.ts
@@ -20,6 +20,10 @@ export const getAddressesGeolocation =
.post(url, payload)
.then((response) => {
if (response?.data) {
+ addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_GEOLOCATION_LOADED, {
+ loanAccountNumber: payload.loanAccountNumber,
+ customerReferenceId: payload.customerReferenceId,
+ });
dispatch(setAddressGeolocation(response.data));
return;
}
@@ -56,23 +60,26 @@ export const addAddress =
});
};
-export const getUngroupedAddress = (loanAccountNumber: string, infoToGet: UnifiedCaseDetailsTypes[]) => {
+export const getUngroupedAddress = (
+ loanAccountNumber: string,
+ infoToGet: UnifiedCaseDetailsTypes[]
+) => {
const queryParams = { ...initialUrlParams };
- for (const key of infoToGet) {
- queryParams[key] = true;
- }
- addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_UNIFIED_ENTITY_REQUESTED, {
- lan: loanAccountNumber || '',
- requestedEntities: JSON.stringify(infoToGet || []) || ''
- });
- const url = getApiUrl(ApiKeys.CASE_UNIFIED_DETAILS_V4, { loanAccountNumber }, queryParams);
- return axiosInstance
+ for (const key of infoToGet) {
+ queryParams[key] = true;
+ }
+ addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_UNIFIED_ENTITY_REQUESTED, {
+ lan: loanAccountNumber || '',
+ requestedEntities: JSON.stringify(infoToGet || []) || '',
+ });
+ const url = getApiUrl(ApiKeys.CASE_UNIFIED_DETAILS_V4, { loanAccountNumber }, queryParams);
+ return axiosInstance
.get(url)
.then((response) => {
if (response.status === API_STATUS_CODE.OK) {
const ungroupedAddressesWithFeedbacks: IUngroupedAddressWithFeedbacks = {
- ungroupedAddresses: response?.data?.ungroupedAddresses || [],
- ungroupedAddressFeedbacks: response?.data?.addressFeedbacks || []
+ ungroupedAddresses: response?.data?.ungroupedAddresses || [],
+ ungroupedAddressFeedbacks: response?.data?.addressFeedbacks || [],
};
return ungroupedAddressesWithFeedbacks;
}
@@ -83,3 +90,19 @@ export const getUngroupedAddress = (loanAccountNumber: string, infoToGet: Unifie
throw new Error(err);
});
};
+
+export const getSimilarGeolocationTimestamps = (clusterId: string) => {
+ const url = getApiUrl(ApiKeys.SIMILAR_GEOLOCATION_TIMESTAMPS, { clusterId });
+ return axiosInstance
+ .get(url)
+ .then((response) => {
+ if (response.status === API_STATUS_CODE.OK) {
+ return response.data;
+ }
+ throw response;
+ })
+ .catch((err) => {
+ logError(err);
+ throw new Error(err);
+ });
+};
diff --git a/src/action/authActions.ts b/src/action/authActions.ts
index 579db69c..9f35b57f 100644
--- a/src/action/authActions.ts
+++ b/src/action/authActions.ts
@@ -198,9 +198,17 @@ export const handleGoogleLogout = async () => {
}
};
-export const handleLogout = () => async (dispatch: AppDispatch) => {
+const firebaseSignout = async () => {
try {
await auth().signOut();
+ } catch (error) {
+ logError(error as Error, 'Firebase signout error');
+ }
+}
+
+export const handleLogout = () => async (dispatch: AppDispatch) => {
+ try {
+ await firebaseSignout();
await handleGoogleLogout();
await clearAllAsyncStorage();
await clearStorageEngine();
diff --git a/src/action/dataActions.ts b/src/action/dataActions.ts
index 11043e90..971c2ef3 100644
--- a/src/action/dataActions.ts
+++ b/src/action/dataActions.ts
@@ -30,7 +30,7 @@ let _signedApiCallBucket: { req: any; added_At: number; callback: GenericFunctio
let _signedApiCallBucketTimer: number = 0;
const SIGNED_API_BUCKET_SIZE = 10;
const SIGNED_API_BUCKET_TIMEOUT = 5 * 1000;
-const SUBMIT_FEEDBACK_API_VERSION = 3;
+const SUBMIT_FEEDBACK_API_VERSION = 4;
export const postPinnedList =
(pinnedCases: IPinnedCasesPayload[], updatedCaseList: ICaseItem[], type: string) =>
diff --git a/src/assets/icons/AddressIcon.tsx b/src/assets/icons/AddressIcon.tsx
new file mode 100644
index 00000000..b9e3716a
--- /dev/null
+++ b/src/assets/icons/AddressIcon.tsx
@@ -0,0 +1,31 @@
+import * as React from "react"
+import Svg, { Rect, Path } from "react-native-svg"
+
+function AddressIcon() {
+ return (
+
+ )
+}
+
+export default AddressIcon
diff --git a/src/assets/icons/CallbackNotificationIcon.tsx b/src/assets/icons/CallbackNotificationIcon.tsx
new file mode 100644
index 00000000..ef5c1711
--- /dev/null
+++ b/src/assets/icons/CallbackNotificationIcon.tsx
@@ -0,0 +1,44 @@
+import * as React from 'react';
+import Svg, { Defs, ClipPath, Path, Image, Mask, G, Use } from 'react-native-svg';
+
+function CallbackNotificationIcon() {
+ return (
+
+ );
+}
+
+export default CallbackNotificationIcon;
diff --git a/src/common/BlockerScreen.tsx b/src/common/BlockerScreen.tsx
index 67bdc2f8..fb10f4d4 100644
--- a/src/common/BlockerScreen.tsx
+++ b/src/common/BlockerScreen.tsx
@@ -36,7 +36,6 @@ const BlockerScreen = (props: IBlockerScreen) => {
);
const { isWifiOrCellularOn, appState } = useAppSelector((state) => state.metadata);
-
const [shouldUpdate, setShouldUpdate] = useState();
const [showActionBtnLoader, setShowActionBtnLoader] = useState(false);
@@ -72,15 +71,14 @@ const BlockerScreen = (props: IBlockerScreen) => {
setForceReinstallData(undefined);
}, [JSON.stringify(forceUninstallData || {})]);
-
React.useEffect(() => {
if (!appState) return;
const buildToCompare = GLOBAL.BUILD_FLAVOUR;
if (!buildToCompare) return;
let flavorToUpdate: IAppState;
- if(GLOBAL.BUILD_FLAVOUR.includes('fieldAgents')){
+ if (GLOBAL.BUILD_FLAVOUR.includes('fieldAgents')) {
flavorToUpdate = appState?.fieldAgent;
- }else {
+ } else {
flavorToUpdate = appState?.telecallingAgents;
}
@@ -110,6 +108,12 @@ const BlockerScreen = (props: IBlockerScreen) => {
return () => appStateChange.remove();
}, []);
+ React.useEffect(() => {
+ if (!isWifiOrCellularOn) {
+ addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_INTERNET_BLOCKER_SCREEN_LOAD);
+ }
+ }, [isWifiOrCellularOn]);
+
const handleDownloadNewApp = () => {
if (forceReinstallData?.reinstall_endpoint) {
openApkDownloadLink(forceReinstallData?.reinstall_endpoint);
@@ -122,9 +126,9 @@ const BlockerScreen = (props: IBlockerScreen) => {
const handleAppUpdate = () => {
let appUpdateUrl;
- if(GLOBAL.BUILD_FLAVOUR.includes(BUILD_FOR_FIELD)){
+ if (GLOBAL.BUILD_FLAVOUR.includes(BUILD_FOR_FIELD)) {
appUpdateUrl = appState?.fieldAgent.currentProdAPK;
- }else {
+ } else {
appUpdateUrl = appState?.telecallingAgents.currentProdAPK;
}
if (appUpdateUrl) {
@@ -143,7 +147,6 @@ const BlockerScreen = (props: IBlockerScreen) => {
}
};
-
if (shouldUpdate) {
const { heading, instructions } = BLOCKER_SCREEN_DATA.UNINSTALL_APP;
return (
diff --git a/src/common/Constants.ts b/src/common/Constants.ts
index cc503bcb..cdf55e9d 100644
--- a/src/common/Constants.ts
+++ b/src/common/Constants.ts
@@ -776,6 +776,49 @@ export const CLICKSTREAM_EVENT_NAMES = {
name: 'FA_FEEDBACK_API_CALLED',
description: 'FA_FEEDBACK_API_CALLED',
},
+
+ //ADDRESSES AND GEOLOCATIONS
+ FA_GEOLOCATION_TAB_CLICKED: {
+ name: 'FA_GEOLOCATION_TAB_CLICKED',
+ description: 'FA_GEOLOCATION_TAB_CLICKED',
+ },
+ FA_ADDRESS_TAB_CLICKED: {
+ name: 'FA_ADDRESS_TAB_CLICKED',
+ description: 'FA_ADDRESS_TAB_CLICKED',
+ },
+ FA_GEOLOCATION_LOADED: {
+ name: 'FA_GEOLOCATION_LOADED',
+ description: 'FA_GEOLOCATION_LOADED',
+ },
+ FA_GEOLOCATION_TIMESTAMP_CLICKED: {
+ name: 'FA_GEOLOCATION_TIMESTAMP_CLICKED',
+ description: 'FA_GEOLOCATION_TIMESTAMP_CLICKED',
+ },
+ FA_GEOLOCATION_TIMESTAMP_LOADED: {
+ name: 'FA_GEOLOCATION_TIMESTAMP_LOADED',
+ description: 'FA_GEOLOCATION_TIMESTAMP',
+ },
+ FA_GEOLOCATION_ADDITIONAL_GL_CLICKED: {
+ name: 'FA_GEOLOCATION_ADDITIONAL_GL_CLICKED',
+ description: 'FA_GEOLOCATION_ADDITIONAL_GL_CLICKED',
+ },
+ FA_GEOLOCATION_ADDITIONAL_GL_LOADED: {
+ name: 'FA_GEOLOCATION_ADDITIONAL_GL_LOADED',
+ description: 'FA_GEOLOCATION_ADDITIONAL_GL_LOADED',
+ },
+ FA_ADDRESS_OLD_FEEDBACK_CLICKED: {
+ name: 'FA_ADDRESS_OLD_FEEDBACK_CLICKED',
+ description: 'FA_ADDRESS_OLD_FEEDBACK_CLICKED',
+ },
+ FA_ADDRESS_ADD_FEEDBACK_CLICKED: {
+ name: 'FA_ADDRESS_ADD_FEEDBACK_CLICKED',
+ description: 'FA_ADDRESS_ADD_FEEDBACK_CLICKED',
+ },
+ FA_INTERNET_BLOCKER_SCREEN_LOAD: {
+ name: 'FA_INTERNET_BLOCKER_SCREEN_LOAD',
+ description: 'FA_INTERNET_BLOCKER_SCREEN_LOAD',
+ }
+
} as const;
export enum MimeType {
@@ -892,3 +935,12 @@ export const REQUEST_TO_UNBLOCK_FOR_IMPERSONATION = [
];
export const NAVI_AGENCY_CODE = '1000';
+
+export const HIT_SLOP = {
+ top: 8,
+ bottom: 8,
+ left: 8,
+ right: 8,
+};
+
+export const LITMUS_URL = 'https://longhorn.navi.com/litmus';
diff --git a/src/common/TrackingComponent.tsx b/src/common/TrackingComponent.tsx
index 334da4ad..e6191ffb 100644
--- a/src/common/TrackingComponent.tsx
+++ b/src/common/TrackingComponent.tsx
@@ -1,4 +1,4 @@
-import { type ReactNode, useEffect, useRef, useCallback } from 'react';
+import { type ReactNode, useEffect, useRef } from 'react';
import { type NativeEventSubscription, AppState, type AppStateStatus } from 'react-native';
import dayJs from 'dayjs';
import RNFS from 'react-native-fs';
@@ -11,13 +11,13 @@ import CosmosForegroundService, {
} from '../services/foregroundServices/foreground.service';
import useIsOnline from '../hooks/useIsOnline';
import {
- type IGeolocationPayload,
+ IGeolocationPayload,
getSyncTime,
+ sendCurrentGeolocationAndBuffer,
sendLocationAndActivenessToServer,
} from '../hooks/capturingApi';
import { isTimeDifferenceWithinRange } from '../components/utlis/commonFunctions';
-import { setDeviceGeolocationsBuffer, setIsTimeSynced } from '../reducer/foregroundServiceSlice';
-import { CaptureGeolocation } from '../components/form/services/geoLocation.service';
+import { setIsTimeSynced } from '../reducer/foregroundServiceSlice';
import { logError } from '../components/utlis/errorUtils';
import { useAppDispatch, useAppSelector } from '../hooks';
import { dataSyncService } from '../services/dataSync.service';
@@ -33,7 +33,7 @@ import {
} from '../action/firebaseFallbackActions';
import { getSyncCaseIds } from '../components/utlis/firebaseFallbackUtils';
import { syncCasesByFallback } from '../reducer/allCasesSlice';
-import {MILLISECONDS_IN_A_MINUTE, noop} from '../../RN-UI-LIB/src/utlis/common';
+import { MILLISECONDS_IN_A_MINUTE, noop } from '../../RN-UI-LIB/src/utlis/common';
import { setCaseSyncLock, setLockData } from '../reducer/userSlice';
import { getConfigData } from '../action/configActions';
import { AppStates } from '../types/appStates';
@@ -42,13 +42,19 @@ import { AgentActivity } from '../types/agentActivity';
import {
getActivityTimeOnApp,
getActivityTimeWindowMedium,
- getActivityTimeWindowHigh, getEnableFirestoreResync, getFirestoreResyncIntervalInMinutes,
+ getActivityTimeWindowHigh,
+ getEnableFirestoreResync,
} from './AgentActivityConfigurableConstants';
import { GlobalImageMap } from './CachedImage';
import { addClickstreamEvent } from '../services/clickstreamEventService';
import { CLICKSTREAM_EVENT_NAMES } from './Constants';
-import useResyncFirebase from "@hooks/useResyncFirebase";
-import AsyncStorage from "@react-native-async-storage/async-storage";
+import useResyncFirebase from '@hooks/useResyncFirebase';
+import { CaptureGeolocation } from '@components/form/services/geoLocation.service';
+import getLitmusExperimentResult, {
+ LitmusExperimentName,
+ LitmusExperimentNameMap,
+} from '@services/litmusExperiments.service';
+import { setLitmusExperimentResult } from '@reducers/litmusExperimentSlice';
export enum FOREGROUND_TASKS {
GEOLOCATION = 'GEOLOCATION',
@@ -59,7 +65,7 @@ export enum FOREGROUND_TASKS {
UPDATE_AGENT_ACTIVITY = 'UPDATE_AGENT_ACTIVITY',
DELETE_CACHE = 'DELETE_CACHE',
FETCH_DATA_FROM_FIREBASE = 'FETCH_DATA_FROM_FIREBASE',
- FIREBASE_RESYNC = 'FIREBASE_RESYNC'
+ FIREBASE_RESYNC = 'FIREBASE_RESYNC',
}
interface ITrackingComponent {
@@ -69,28 +75,66 @@ interface ITrackingComponent {
let LAST_SYNC_STATUS = 'SKIP';
const ACTIVITY_TIME_WINDOW = 10; // 10 minutes
-
const TrackingComponent: React.FC = ({ children }) => {
const isOnline = useIsOnline();
const dispatch = useAppDispatch();
const appState = useRef(AppState.currentState);
- const isTeamLead = useAppSelector((state) => state.user.isTeamLead);
- const deviceId = useAppSelector((state) => state?.user?.deviceId);
- const caseSyncLock = useAppSelector((state) => state?.user?.caseSyncLock);
- const lastFirebaseResyncTimestamp = useAppSelector((state) => state?.metadata?.lastFirebaseResyncTimestamp);
-
const {
+ isTeamLead,
+ caseSyncLock,
+ isTrackingComponentV2Enabled,
referenceId,
pendingList = [],
pinnedList = [],
geolocations = [],
+ deviceId,
+ userId,
} = useAppSelector((state) => ({
+ isTeamLead: state.user.isTeamLead,
+ caseSyncLock: state?.user?.caseSyncLock,
+ isTrackingComponentV2Enabled: state.litmusExperiment?.isTrackingComponentV2Enabled,
referenceId: state.user.user?.referenceId!,
pendingList: state.allCases.pendingList,
pinnedList: state.allCases.pinnedList,
geolocations: state.foregroundService.deviceGeolocationsBuffer,
+ deviceId: state?.user?.deviceId,
+ userId: state?.user?.user?.referenceId,
}));
+ const handleSendGeolocation = async () => {
+ try {
+ const location = await CaptureGeolocation.fetchLocation(`${Date.now()}`, 0, appState.current);
+ if (!location) {
+ return;
+ }
+ const isActiveOnApp: string | boolean = (await getItem(StorageKeys.IS_USER_ACTIVE)) || false;
+ const userActivityonApp: string =
+ (await getItem(StorageKeys.USER_ACTIVITY_ON_APP)) || AgentActivity.LOW;
+
+ const geolocation: IGeolocationPayload = {
+ latitude: location.latitude,
+ longitude: location.longitude,
+ accuracy: location.accuracy,
+ timestamp: Date.now(),
+ isActiveOnApp: Boolean(isActiveOnApp),
+ userActivityOnApp: String(userActivityonApp),
+ deviceId: deviceId,
+ };
+ dispatch(sendLocationAndActivenessToServer([geolocation]));
+ } catch (e: any) {
+ logError(e, 'Error during background location sending.');
+ }
+ };
+
+ useEffect(() => {
+ if (!isOnline || isTrackingComponentV2Enabled) {
+ return;
+ }
+ if (geolocations.length > 0) {
+ dispatch(sendLocationAndActivenessToServer(geolocations, true));
+ }
+ }, [geolocations, isOnline, isTrackingComponentV2Enabled]);
+
const handleTimeSync = async () => {
try {
if (!isOnline) {
@@ -106,42 +150,6 @@ const TrackingComponent: React.FC = ({ children }) => {
}
};
- const handleSendGeolocation = async () => {
- try {
- const location = await CaptureGeolocation.fetchLocation(`${Date.now()}`, 0, appState.current);
- if (!location) {
- return;
- }
- const isActiveOnApp: string | boolean = (await getItem(StorageKeys.IS_USER_ACTIVE)) || false;
- const userActivityonApp: string =
- (await getItem(StorageKeys.USER_ACTIVITY_ON_APP)) || AgentActivity.LOW;
-
- const geolocation: IGeolocationPayload = {
- latitude: location.latitude,
- longitude: location.longitude,
- accuracy: location.accuracy,
- timestamp: Date.now(),
- isActiveOnApp: Boolean(isActiveOnApp),
- userActivityOnApp: String(userActivityonApp),
- deviceId : deviceId,
- };
- dispatch(setDeviceGeolocationsBuffer(geolocation));
- dispatch(sendLocationAndActivenessToServer([geolocation]));
- } catch (e: any) {
- logError(e, 'Error during background location sending.');
- }
- };
-
- useEffect(() => {
- if (!isOnline) {
- return;
- }
- if (geolocations.length > 0) {
- dispatch(sendLocationAndActivenessToServer(geolocations, true));
- }
- }, [geolocations, isOnline]);
-
-
const resyncFirebase = useResyncFirebase();
const handleGetCaseSyncStatus = async () => {
@@ -173,9 +181,9 @@ const TrackingComponent: React.FC = ({ children }) => {
}
if (visitPlanStatus) {
dispatch(
- setLockData({
- visitPlanStatus,
- })
+ setLockData({
+ visitPlanStatus,
+ })
);
}
dispatch(setCaseSyncLock(false));
@@ -199,18 +207,18 @@ const TrackingComponent: React.FC = ({ children }) => {
const stateSetTime = dayJs(stateSetTimestamp);
const diffBetweenCurrentTimeAndForegroundTime =
- dayJs().diff(foregroundTime, 'seconds') < 0 ? 0 : dayJs().diff(foregroundTime, 'seconds');
+ dayJs().diff(foregroundTime, 'seconds') < 0 ? 0 : dayJs().diff(foregroundTime, 'seconds');
const diffBetweenCurrentTimeAndSetStateTime =
- dayJs().diff(stateSetTime, 'minutes') < 0 ? 0 : dayJs().diff(stateSetTime, 'minutes');
+ dayJs().diff(stateSetTime, 'minutes') < 0 ? 0 : dayJs().diff(stateSetTime, 'minutes');
const ACTIVITY_TIME_ON_APP = getActivityTimeOnApp();
const ACTIVITY_TIME_WINDOW_HIGH = getActivityTimeWindowHigh();
const ACTIVITY_TIME_WINDOW_MEDIUM = getActivityTimeWindowMedium();
const isStateSetTimeWithinHighRange =
- diffBetweenCurrentTimeAndSetStateTime < ACTIVITY_TIME_WINDOW_HIGH;
+ diffBetweenCurrentTimeAndSetStateTime < ACTIVITY_TIME_WINDOW_HIGH;
const isStateSetTimeWithinMediumRange =
- diffBetweenCurrentTimeAndSetStateTime < ACTIVITY_TIME_WINDOW_MEDIUM;
+ diffBetweenCurrentTimeAndSetStateTime < ACTIVITY_TIME_WINDOW_MEDIUM;
const isForegroundTimeAfterBackground = dayJs(foregroundTimestamp).isAfter(backgroundTimestamp);
if (AppState.currentState === AppStates.ACTIVE) {
@@ -238,42 +246,42 @@ const TrackingComponent: React.FC = ({ children }) => {
const currentDate = new Date().getTime();
RNFS.readdir(directoryPath)
- .then((files) => {
- for (const file of files) {
- const filePath = `${directoryPath}/${file}`;
- if (!file.endsWith('jpg') || !file.endsWith('pdf')) {
- continue;
- }
- RNFS.stat(filePath)
- .then(async (fileStat) => {
- // Calculate the age of the file in milliseconds
- const fileAgeMs = currentDate - new Date(fileStat.mtime).getTime();
-
- // Check if the file is older than 30 days (30 days = 30 * 24 * 60 * 60 * 1000 milliseconds)
-
- if (fileAgeMs > 30 * 24 * 60 * 60 * 1000) {
- delete GlobalImageMap[filePath];
- await RNFS.unlink(filePath); // Delete the file
- }
- })
- .then(() => {
- console.log(`Deleted old file: ${file}`);
- })
- .catch((error) => {
- console.error(`Error deleting file: ${file}`, error);
- });
+ .then((files) => {
+ for (const file of files) {
+ const filePath = `${directoryPath}/${file}`;
+ if (!file.endsWith('jpg') || !file.endsWith('pdf')) {
+ continue;
}
- })
- .catch((error) => {
- console.error('Error reading directory:', error);
- });
+ RNFS.stat(filePath)
+ .then(async (fileStat) => {
+ // Calculate the age of the file in milliseconds
+ const fileAgeMs = currentDate - new Date(fileStat.mtime).getTime();
+
+ // Check if the file is older than 30 days (30 days = 30 * 24 * 60 * 60 * 1000 milliseconds)
+
+ if (fileAgeMs > 30 * 24 * 60 * 60 * 1000) {
+ delete GlobalImageMap[filePath];
+ await RNFS.unlink(filePath); // Delete the file
+ }
+ })
+ .then(() => {
+ console.log(`Deleted old file: ${file}`);
+ })
+ .catch((error) => {
+ console.error(`Error deleting file: ${file}`, error);
+ });
+ }
+ })
+ .catch((error) => {
+ console.error('Error reading directory:', error);
+ });
};
const handleFetchUpdatedDataFromFirebase = async () => {
const currentTimestamp: number = Date.now();
if (
- FIREBASE_FETCH_TIMESTAMP &&
- currentTimestamp - FIREBASE_FETCH_TIMESTAMP > 15 * MILLISECONDS_IN_A_MINUTE
+ FIREBASE_FETCH_TIMESTAMP &&
+ currentTimestamp - FIREBASE_FETCH_TIMESTAMP > 15 * MILLISECONDS_IN_A_MINUTE
) {
fetchUpdatedRemoteConfig();
}
@@ -288,7 +296,9 @@ const TrackingComponent: React.FC = ({ children }) => {
},
{
taskId: FOREGROUND_TASKS.GEOLOCATION,
- task: handleSendGeolocation,
+ task: isTrackingComponentV2Enabled
+ ? () => dispatch(sendCurrentGeolocationAndBuffer(appState.current))
+ : handleSendGeolocation,
delay: 3 * MILLISECONDS_IN_A_MINUTE, // 3 minutes
onLoop: true,
},
@@ -343,8 +353,8 @@ const TrackingComponent: React.FC = ({ children }) => {
const foregroundTime = dayJs(foregroundTimestamp);
const backgroundTime = dayJs(backgroundTimestamp);
const diffBetweenBackgroundAndForegroundTime = dayJs(backgroundTime).diff(
- foregroundTime,
- 'seconds'
+ foregroundTime,
+ 'seconds'
);
const ACTIVITY_TIME_ON_APP = getActivityTimeOnApp();
@@ -354,6 +364,32 @@ const TrackingComponent: React.FC = ({ children }) => {
}
};
+ const handleGeolocationTaskUpdate = async () => {
+ const trackingComponentExperimentName = LitmusExperimentName.COSMOS_TRACKING_COMPONENT_V2;
+ const trackingComponentExperiment = await getLitmusExperimentResult(
+ trackingComponentExperimentName,
+ { 'x-customer-id': userId }
+ );
+ // If the tracking component experiment is changed, update to new geolocation task
+ if (trackingComponentExperiment !== isTrackingComponentV2Enabled) {
+ const updatedGeolocationTask = {
+ taskId: FOREGROUND_TASKS.GEOLOCATION,
+ task: trackingComponentExperiment
+ ? () => dispatch(sendCurrentGeolocationAndBuffer(appState.current))
+ : handleSendGeolocation,
+ delay: 3 * MILLISECONDS_IN_A_MINUTE,
+ onLoop: true,
+ };
+ CosmosForegroundService.updateTask(updatedGeolocationTask);
+ dispatch(
+ setLitmusExperimentResult({
+ experimentName: trackingComponentExperimentName,
+ result: trackingComponentExperiment,
+ })
+ );
+ }
+ };
+
const handleAppStateChange = async (nextAppState: AppStateStatus) => {
// App comes to foreground from background
const now = dayJs().toString();
@@ -361,9 +397,11 @@ const TrackingComponent: React.FC = ({ children }) => {
await setItem(StorageKeys.APP_FOREGROUND_TIMESTAMP, now);
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.AV_APP_FOREGROUND, { now });
handleGetCaseSyncStatus();
+ handleTimeSync();
dispatch(getConfigData());
CosmosForegroundService.start(tasks);
resyncFirebase();
+ handleGeolocationTaskUpdate();
}
if (nextAppState === AppStates.BACKGROUND) {
await setItem(StorageKeys.APP_BACKGROUND_TIMESTAMP, now);
@@ -395,6 +433,7 @@ const TrackingComponent: React.FC = ({ children }) => {
let appStateSubscription: NativeEventSubscription;
appStateSubscription = AppState.addEventListener('change', handleAppStateChange);
CosmosForegroundService.start(tasks);
+ handleGeolocationTaskUpdate();
return () => {
appStateSubscription?.remove();
};
@@ -405,4 +444,4 @@ const TrackingComponent: React.FC = ({ children }) => {
return <>{children}>;
};
-export default TrackingComponent;
\ No newline at end of file
+export default TrackingComponent;
diff --git a/src/components/filters/FilterOptions.tsx b/src/components/filters/FilterOptions.tsx
index fc0955e2..59af0c6a 100644
--- a/src/components/filters/FilterOptions.tsx
+++ b/src/components/filters/FilterOptions.tsx
@@ -1,10 +1,11 @@
-import { View } from 'react-native';
+import {StyleSheet, View} from 'react-native';
import React from 'react';
import { IFilters, TFilterOptions } from './Filters';
import RadioGroup from '../../../RN-UI-LIB/src/components/radio_button/RadioGroup';
import RNRadioButton from '../../../RN-UI-LIB/src/components/radio_button/RadioButton';
import { GenericStyles } from '../../../RN-UI-LIB/src/styles';
import Checkbox from '../../../RN-UI-LIB/src/components/chechbox/Checkbox';
+import Text from "@rn-ui-lib/components/Text";
interface IFilterOptions {
filters: IFilters;
@@ -96,7 +97,7 @@ const FilterOptions: React.FC = ({
orientation="vertical"
>
{filter.options.map((option) => (
-
+
))}
);
@@ -119,4 +120,11 @@ const FilterOptions: React.FC = ({
return null;
};
+const styles = StyleSheet.create({
+ radioOptionSubLabel: {
+ marginLeft: 35,
+ marginTop: -10
+ }
+});
+
export default FilterOptions;
diff --git a/src/components/filters/Filters.tsx b/src/components/filters/Filters.tsx
index 892284ee..903c5370 100644
--- a/src/components/filters/Filters.tsx
+++ b/src/components/filters/Filters.tsx
@@ -11,6 +11,7 @@ import NavigationHeader from '../../../RN-UI-LIB/src/components/NavigationHeader
interface FilterOption {
value: string;
label: string;
+ subLabels?: [string];
}
interface FilterData {
diff --git a/src/components/form/AnswerRender.tsx b/src/components/form/AnswerRender.tsx
index 961dd07d..3ddbcf4e 100644
--- a/src/components/form/AnswerRender.tsx
+++ b/src/components/form/AnswerRender.tsx
@@ -11,6 +11,8 @@ import { getAddressString, getPhoneNumberString, memoize } from '../utlis/common
import { getBase64ImageFromOfflineDb } from '../../services/casePayload.transformer';
import { formatAmount } from '../../../RN-UI-LIB/src/utlis/amount';
import GeolocationAddressAnswer from './components/GeolocationAddressAnswer';
+import dayjs from 'dayjs';
+import { BUSINESS_DATE_FORMAT, CUSTOM_ISO_DATE_FORMAT } from '@rn-ui-lib/utils/dates';
const RATING_COMPONENT = 'Rating';
const MAX_RATING = 5;
@@ -131,7 +133,9 @@ const AnswerRender: React.FC = (props) => {
case AnswerType.phoneNumber:
return ;
case AnswerType.date:
- return ;
+ return ;
+ case AnswerType.time:
+ return ;
default:
return ;
}
diff --git a/src/components/form/components/AddressSelection.tsx b/src/components/form/components/AddressSelection.tsx
index 11cdabfb..25723745 100644
--- a/src/components/form/components/AddressSelection.tsx
+++ b/src/components/form/components/AddressSelection.tsx
@@ -1,6 +1,6 @@
-import { View } from 'react-native';
+import { TouchableOpacity, View } from 'react-native';
import React from 'react';
-import { useAppSelector } from '../../../hooks';
+import { useAppDispatch, useAppSelector } from '../../../hooks';
import { CaseAllocationType } from '../../../screens/allCases/interface';
import { validateInput } from '../services/validation.service';
import { Control, Controller } from 'react-hook-form';
@@ -14,6 +14,13 @@ import { AnswerType } from '../interface';
import { Address, GeolocationSource, IGeolocation } from '../../../screens/caseDetails/interface';
import GeolocationAddress from './GeolocationAddress';
import { GenericStyles } from '../../../../RN-UI-LIB/src/styles';
+import { RootState } from '@store';
+import NoLocationsIcon from '@rn-ui-lib/icons/NoLocationIcon';
+import Text from '@rn-ui-lib/components/Text';
+import LineLoader from '@rn-ui-lib/components/suspense_loader/LineLoader';
+import { UnifiedCaseDetailsTypes, getCaseUnifiedData } from '@actions/caseApiActions';
+import LoadingIcon from '@rn-ui-lib/icons/LoadingIcon';
+import { COLORS } from '@rn-ui-lib/colors';
interface IAddressSelection {
questionType: string;
@@ -40,10 +47,25 @@ export const AddressSourceMap: Record = {
const AddressSelection: React.FC = (props) => {
const { caseId, questionId, error, widgetId, sectionId, control, questionType } = props;
- const currentCase = useAppSelector((state) => state.allCases.caseDetails[caseId]);
+ const { currentCase, addressGeolocation, isLoading, loanAccountNumber } = useAppSelector(
+ (state) => {
+ const currentCase = state.allCases.caseDetails[caseId];
+ const loanAccountNumber = currentCase?.loanAccountNumber;
+ const addresses = state.address?.[loanAccountNumber];
+ return {
+ currentCase,
+ loanAccountNumber,
+ addressGeolocation: addresses?.addressesAndGeoLocations || {},
+ isLoading: addresses?.isLoading || false,
+ };
+ }
+ );
const caseType = currentCase?.caseType || CaseAllocationType.ADDRESS_VERIFICATION_CASE;
const template = useAppSelector((state) => state.case.templateData[caseType]);
const question = template.questions[questionId];
+
+ const dispatch = useAppDispatch();
+
const handleChange = (change: string | null, onChange: (...event: any[]) => void) => {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.AV_FORM_ELEMENT_CHANGED, {
caseId,
@@ -62,10 +84,71 @@ const AddressSelection: React.FC = (props) => {
const isGeolocation = question?.tag === VisitTypeSelection.GEOLOCATION_SELECTION;
- const addresses = (isGeolocation ? currentCase?.geolocations : currentCase?.addresses) || [];
+ const addresses =
+ (isGeolocation ? addressGeolocation?.geoLocations : currentCase?.addresses) || [];
const controllerName = `widgetContext.${widgetId}.sectionContext.${sectionId}.questionContext.${questionId}`;
+ const reloadGeolocations = () => {
+ dispatch(
+ getCaseUnifiedData([loanAccountNumber], [UnifiedCaseDetailsTypes.ADDRESS_AND_GEOLOCATIONS])
+ );
+ };
+
+ if (isGeolocation) {
+ if (isLoading) {
+ return (
+
+ {[...Array(7).keys()].map((_, index) => (
+
+ ))}
+
+ );
+ }
+ if (!addresses?.length) {
+ return (
+
+
+
+
+ No nearby geolocations found
+
+
+
+
+
+
+ Retry
+
+
+ );
+ }
+ }
+
return (
= (props) => {
}
return (
= ({
isFeedbackView,
handlePageRouting,
}) => {
- const { latitude, longitude, capturedTimestamp, tag, id, primarySource } = address;
+ const {
+ latitude,
+ longitude,
+ timestamp,
+ tag,
+ id,
+ primarySource,
+ pingCount = 0,
+ clusterLabels = [],
+ } = address;
const { deviceGeolocationCoordinate, prefilledAddressScreenTemplate } = useAppSelector(
(state) => ({
deviceGeolocationCoordinate: state.foregroundService?.deviceGeolocationCoordinate,
@@ -50,6 +73,7 @@ const GeolocationAddress: React.FC = ({
})
);
const dispatch = useAppDispatch();
+ const showSimilarGeolocationsCTA = pingCount > 1 && !isFeedbackView;
const relativeDistanceBwLatLong = getDistanceFromLatLonInKm(deviceGeolocationCoordinate, {
latitude,
@@ -62,16 +86,24 @@ const GeolocationAddress: React.FC = ({
: '';
const isFeedbackPresent = lastFeedbackForGeolocation?.feedbackPresent;
+ const isDataSutramPrimarySource = primarySource === GeolocationSource.DATA_SUTRAM;
- const { addressDate, addressTime } = useMemo(() => {
- const timestamp = new Date(Number(capturedTimestamp));
- if (timestamp.toString() === 'Invalid Date') {
- return { addressDate: '', addressTime: '', isFeedbackPresent };
+ const { addressDate, addressTime, lastFeedbackTimestampDate } = useMemo(() => {
+ const date = new Date(Number(timestamp));
+ const lastFeedbackTimestamp = new Date(
+ Number(lastFeedbackForGeolocation?.latestFeedbackTimestamp)
+ );
+ if (date.toString() === 'Invalid Date') {
+ return { addressDate: '', addressTime: '', lastFeedbackTimestampDate: '' };
}
- const addressDate = dateFormat(timestamp, 'DD MMM YYYY');
- const addressTime = dateFormat(timestamp, 'hh:mm A');
- return { addressDate, addressTime };
- }, [lastFeedbackForGeolocation, capturedTimestamp]);
+ const addressDate = dateFormat(date, COSMOS_STANDARD_DATE_FORMAT);
+ const addressTime = dateFormat(date, COSMOS_STANDARD_TIME_FORMAT);
+ const lastFeedbackTimestampDate = dateFormat(
+ lastFeedbackTimestamp,
+ COSMOS_STANDARD_DATE_FORMAT
+ );
+ return { addressDate, addressTime, lastFeedbackTimestampDate };
+ }, [lastFeedbackForGeolocation, timestamp]);
const handleCloseRouting = () => handlePageRouting?.(CaseDetailStackEnum.ADDRESS_GEO);
@@ -79,7 +111,11 @@ const GeolocationAddress: React.FC = ({
if (!caseId) {
return;
}
- addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_GEOLOCATION_ADD_FEEDBACK_CLICKED);
+ addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_GEOLOCATION_ADD_FEEDBACK_CLICKED, {
+ loanAccountNumber,
+ caseReferenceId: caseId,
+ geolocationId: id,
+ });
if (prefilledAddressScreenTemplate) {
const addressKey = '{{addressReferenceId}}';
const { visitedWidgets, widgetContext } = getCollectionFeedbackOnAddressPreDefinedJourney(
@@ -98,27 +134,33 @@ const GeolocationAddress: React.FC = ({
})
);
if (visitedWidgets?.length) {
- _map(visitedWidgets, (visited) =>
- navigateToScreen(getTemplateRoute(visited, CaseAllocationType.COLLECTION_CASE), {
- caseId: caseId,
- journey: TaskTitleUIMapping.COLLECTION_FEEDBACK,
- handleCloseRouting,
- })
- );
- return;
+ const lastVisitedWidget = visitedWidgets[visitedWidgets.length - 1];
+ navigateToScreen(getTemplateRoute(lastVisitedWidget, CaseAllocationType.COLLECTION_CASE), {
+ caseId: caseId,
+ journey: TaskTitleUIMapping.COLLECTION_FEEDBACK,
+ handleCloseRouting,
+ });
}
}
};
const openGeolocation = () => {
- addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_GEOLOCATION_OPEN_MAP_CLICKED);
+ addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_GEOLOCATION_OPEN_MAP_CLICKED, {
+ loanAccountNumber,
+ caseReferenceId: caseId,
+ geolocationId: id,
+ });
const geolocationUrl = getGoogleMapUrl(address?.latitude, address?.longitude);
if (!geolocationUrl) return;
return Linking.openURL(geolocationUrl);
};
const openOldFeedbacks = () => {
- addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_GEOLOCATION_OLD_FEEDBACK_CLICKED);
+ addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_GEOLOCATION_OLD_FEEDBACK_CLICKED, {
+ loanAccountNumber,
+ caseReferenceId: caseId,
+ geolocationId: id,
+ });
handlePageRouting?.(CaseDetailStackEnum.GEOLOCATION_OLD_FEEDBACKS, {
geolocation: address,
addressReferenceIds: [address?.id],
@@ -127,105 +169,164 @@ const GeolocationAddress: React.FC = ({
});
};
+ const handleSimilarLocationCTA = () => {
+ if (showSimilarGeolocationsCTA) {
+ addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_GEOLOCATION_TIMESTAMP_CLICKED, {
+ loanAccountNumber,
+ caseReferenceId: caseId,
+ geolocationId: id,
+ });
+ navigateToScreen(CaseDetailStackEnum.SIMILAR_GEOLOCATIONS, { ...address, caseId });
+ }
+ };
+
+ const Container: React.ElementType = !isFeedbackView ? TouchableOpacity : View;
+
return (
-
-
-
- {tag}
-
- {roundoffRelativeDistanceBwLatLong ? (
- ({roundoffRelativeDistanceBwLatLong} km away)
- ) : null}
-
-
- {addressDate ? {addressDate} : null}
- {addressTime ? (
+
+
+
+
+ {tag}{' '}
+ {roundoffRelativeDistanceBwLatLong ? (
+ ({roundoffRelativeDistanceBwLatLong} km)
+ ) : null}
+
- {addressTime}
+ {addressDate ? {addressDate} : null}
+ {addressTime ? (
+
+ {', '}
+ {addressTime}
+
+ ) : null}
+
+ {showSimilarGeolocationsCTA ? : null}
+
+
+ {!isFeedbackView ? (
+
+ {isFeedbackPresent ? (
+ <>
+
+ Last visit:{' '}
+
+ {' '}
+ {lastFeedbackForGeolocation?.latestFeedbackStatus || ''}{' '}
+
+
+ {lastFeedbackTimestampDate ? (
+
+ Visited on:{' '}
+
+ {' '}
+ {lastFeedbackTimestampDate || ''}
+
+
+ ) : null}
+ >
+ ) : (
+
+ Unvisited location
+
+ )}
) : null}
- {(primarySource && primarySource === GeolocationSource.DATA_SUTRAM) ? (
-
-
- Skip Tracing
+
+ {!isFeedbackView && pingCount && pingCount >= 1 ? (
+
+
+ Similar locations:{' '}
+
+ {pingCount - 1}
+
+
+ }
+ variant={TagVariant.primary}
+ style={styles.similarLocations}
+ />
+
+ {clusterLabels?.length ? (
+ <>
+
+
+ {clusterLabels.map((label: string) => {
+ return label ? (
+
+ ) : null;
+ })}
+
+ >
+ ) : null}
) : null}
-
- {!isFeedbackView ? (
- isFeedbackPresent ? (
-
- Last visit:{' '}
-
+ {showOpenMap ? (
+
+
+ Open map
+
+
+ ) : null}
+ {showAddFeedback ? (
+
- {' '}
- {lastFeedbackForGeolocation?.latestFeedbackStatus || ''}{' '}
-
-
- ) : (
-
- Last visit:{' '}
-
- Unvisited location
-
-
- )
- ) : null}
-
- {showOpenMap ? (
-
-
- Open map
-
-
- ) : null}
- {showAddFeedback ? (
-
-
- Add feedback
-
-
- ) : null}
- {showOldFeedbacks && isFeedbackPresent ? (
-
-
- Old feedbacks
-
-
- ) : (
-
- )}
-
+
+ Add feedback
+
+
+ ) : null}
+ {showOldFeedbacks && isFeedbackPresent ? (
+
+
+ Old feedbacks
+
+
+ ) : (
+
+ )}
+
+
);
};
-export default GeolocationAddress;
-
const styles = StyleSheet.create({
circleSeparator: {
width: 4,
height: 4,
borderRadius: 2,
backgroundColor: COLORS.BACKGROUND.GRAY_B1,
- marginHorizontal: 8,
+ marginHorizontal: 4,
marginTop: 2,
},
openMapBtn: {
@@ -239,4 +340,26 @@ const styles = StyleSheet.create({
fontSize: 12,
fontWeight: 'bold',
},
+ lastFeedbackTimestampDate: {
+ lineHeight: 18,
+ fontWeight: '700',
+ },
+ similarLocations: {
+ marginVertical: 2,
+ marginRight: 2,
+ },
+ verticalLine: {
+ height: 17,
+ width: 1,
+ backgroundColor: COLORS.BORDER.PRIMARY,
+ margin: 4,
+ },
+ fb24: {
+ flexBasis: '24%',
+ },
+ tagText: {
+ lineHeight: 18,
+ },
});
+
+export default GeolocationAddress;
diff --git a/src/components/form/components/GeolocationAddressAnswer.tsx b/src/components/form/components/GeolocationAddressAnswer.tsx
index e4970886..cf45dbc4 100644
--- a/src/components/form/components/GeolocationAddressAnswer.tsx
+++ b/src/components/form/components/GeolocationAddressAnswer.tsx
@@ -4,6 +4,7 @@ import { useAppSelector } from '../../../hooks';
import { VisitTypeSelection } from './AddressSelection';
import GeolocationAddress from './GeolocationAddress';
import Text from '../../../../RN-UI-LIB/src/components/Text';
+import { RootState } from '@store';
interface IGeolocationAddressAnswer {
caseId: string;
@@ -17,6 +18,11 @@ const GeolocationAddressAnswer: React.FC = ({
addressId,
}) => {
const currentCase = useAppSelector((state) => state.allCases.caseDetails[caseId]);
+ const loanAccountNumber = currentCase?.loanAccountNumber;
+ const { addressGeolocation } = useAppSelector((state: RootState) => ({
+ addressGeolocation: state.address?.[loanAccountNumber]?.addressesAndGeoLocations || {},
+ isLoading: state.address?.[loanAccountNumber]?.isLoading || false,
+ }));
const isGeolocation = metadata?.type === VisitTypeSelection.GEOLOCATION_SELECTION;
const getAddressFromId = memoize((id: string) => {
@@ -24,10 +30,11 @@ const GeolocationAddressAnswer: React.FC = ({
});
if (isGeolocation) {
- if (currentCase?.geolocations) {
- const index = currentCase?.geolocations?.findIndex((a) => a.id === addressId);
+ const geolocations = addressGeolocation?.geoLocations;
+ if (geolocations?.length) {
+ const index = geolocations?.findIndex((a) => a.id === addressId);
if (index !== -1) {
- const address = currentCase?.geolocations?.[index];
+ const address = geolocations?.[index];
return ;
}
}
diff --git a/src/components/form/components/TimeInput.tsx b/src/components/form/components/TimeInput.tsx
index 03b991e1..2938a991 100644
--- a/src/components/form/components/TimeInput.tsx
+++ b/src/components/form/components/TimeInput.tsx
@@ -55,7 +55,7 @@ const TimeInput: React.FC = (props) => {
const outputTime = convertTo24HourFormat(text);
onChange({
answer: outputTime,
- type: AnswerType.date,
+ type: AnswerType.time,
});
};
diff --git a/src/components/form/index.tsx b/src/components/form/index.tsx
index 3d3496ca..0ffba372 100644
--- a/src/components/form/index.tsx
+++ b/src/components/form/index.tsx
@@ -5,7 +5,6 @@ import Geolocation from 'react-native-geolocation-service';
import { SafeAreaView } from 'react-native-safe-area-context';
import Button from '../../../RN-UI-LIB/src/components/Button';
import Heading from '../../../RN-UI-LIB/src/components/Heading';
-import Text from '../../../RN-UI-LIB/src/components/Text';
import ArrowSolidIcon from '../../../RN-UI-LIB/src/Icons/ArrowSolidIcon';
import { GenericStyles } from '../../../RN-UI-LIB/src/styles';
import { COLORS } from '../../../RN-UI-LIB/src/styles/colors';
@@ -47,6 +46,7 @@ import NudgeSuspiciousFeedbackBottomSheet from './NudgeSuspiciousFeedbackBottomS
import { API_STATUS_CODE } from '../utlis/apiHelper';
import NavigationHeader, { Icon } from '../../../RN-UI-LIB/src/components/NavigationHeader';
import { CaseDetailStackEnum } from '@screens/caseDetails/CaseDetailStack';
+import { useNavigation, useRoute } from '@react-navigation/native';
interface IWidget {
route: {
@@ -59,6 +59,10 @@ interface IWidget {
};
}
+enum NavigationActions {
+ GO_BACK = 'GO_BACK',
+}
+
const Widget: React.FC = (props) => {
const [isJourneyFirstScreen, setIsJourneyFirstScreen] = useState(true);
const [showNudgeBottomSheet, setNudgeBottomSheet] = useState(false);
@@ -67,30 +71,28 @@ const Widget: React.FC = (props) => {
const { params } = props.route;
const { caseId, journey, handleCloseRouting } = params;
const caseKey = useRef('');
- const {
- caseType,
- templateData,
- caseData,
- dataToBeValidated,
- docsToBeUploaded,
- intermediateDocsToBeUploaded,
- } = useAppSelector((state) => {
- const caseType =
- state.allCases.caseDetails[caseId]?.caseType || CaseAllocationType.ADDRESS_VERIFICATION_CASE;
- return {
- caseType,
- templateData: state.case.templateData[caseType],
- caseData: state.allCases.caseDetails[caseId],
- dataToBeValidated: state.case.caseForm?.[caseId]?.[journey],
- docsToBeUploaded: state.feedbackImages.docsToBeUploaded,
- intermediateDocsToBeUploaded: state.feedbackImages.intermediateDocsToBeUploaded,
- };
- });
+ const disableFormInteractionUpdate = useRef(false);
+ const { caseType, templateData, caseData, dataToBeValidated, intermediateDocsToBeUploaded } =
+ useAppSelector((state) => {
+ const caseType =
+ state.allCases.caseDetails[caseId]?.caseType ||
+ CaseAllocationType.ADDRESS_VERIFICATION_CASE;
+ return {
+ caseType,
+ templateData: state.case.templateData[caseType],
+ caseData: state.allCases.caseDetails[caseId],
+ dataToBeValidated: state.case.caseForm?.[caseId]?.[journey],
+ docsToBeUploaded: state.feedbackImages.docsToBeUploaded,
+ intermediateDocsToBeUploaded: state.feedbackImages.intermediateDocsToBeUploaded,
+ };
+ });
const name = getWidgetNameFromRoute(props.route.name, caseType);
const { sections, conditionActions: widgetConditionActions, isLeaf } = templateData.widget[name];
const sectionMap = templateData.sections;
const [error, setError] = useState();
const dispatch = useAppDispatch();
+ const navigation = useNavigation();
+ const route = useRoute();
useEffect(() => {
let isFirst = false;
for (const journey of Object.values(templateData.journey)) {
@@ -125,6 +127,49 @@ const Widget: React.FC = (props) => {
return () => subscription.unsubscribe();
}, [watch]);
+ useEffect(() => {
+ const beforeRemoveListener = navigation.addListener('beforeRemove', (e) => {
+ // If leaf widget, do nothing
+ if (isLeaf && disableFormInteractionUpdate.current) {
+ return;
+ }
+
+ // Delete interaction on going back the stack
+ if (e.data.action.type === NavigationActions.GO_BACK) {
+ dispatch(
+ deleteInteraction({
+ caseId,
+ journeyId: journey,
+ widgetId: name,
+ })
+ );
+ return;
+ }
+
+ const numberOfRoutes = navigation.getState().routes.length;
+ const lastScreen = navigation.getState().routes[numberOfRoutes - 1];
+
+ const isLastScreen = lastScreen.name === route.name;
+
+ // Update form interaction when the last screen is about to be unmounted
+ if (isLastScreen) {
+ const submitHandler = handleSubmit((answer) => {
+ dispatch(
+ updateInteraction({
+ caseId,
+ journeyId: journey,
+ widgetId: name,
+ answer,
+ })
+ );
+ });
+ submitHandler();
+ }
+ });
+
+ return beforeRemoveListener;
+ }, [navigation, route]);
+
const onSubmit = (data: any) => {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.AV_FORM_NEXT_BUTTON_CLICKED, {
caseId,
@@ -221,6 +266,7 @@ const Widget: React.FC = (props) => {
};
const handleSubmitJourney = async (data: any, coords: Geolocation.GeoCoordinates) => {
+ disableFormInteractionUpdate.current = true;
dispatch(
updateInteraction({
caseId,
@@ -371,13 +417,6 @@ const Widget: React.FC = (props) => {
journeyId: journey,
widgetId: name,
});
- dispatch(
- deleteInteraction({
- caseId,
- journeyId: journey,
- widgetId: name,
- })
- );
goBack();
};
@@ -398,9 +437,6 @@ const Widget: React.FC = (props) => {
Add feedback for {caseData?.customerInfo?.customerName || caseData?.customerName}
}
- subTitle={
- isJourneyNameExists ? {templateData?.journey?.[journey].name} : null
- }
icon={Icon.close}
onBack={handleCloseIconPress}
/>
@@ -444,17 +480,15 @@ const Widget: React.FC = (props) => {
styles.borderTop,
]}
>
- {!isJourneyFirstScreen && (
- }
- />
- )}
+ }
+ />
) : null}
{lastFeedbackForAddress?.feedbackPresent ? (
-
+
- Last visit:
+ Last visit:
- Visited on:
+
+ Visited on:{' '}
+
- Last visit:
Add Feedback
@@ -234,8 +225,8 @@ function AddressItem({
Old feedbacks
diff --git a/src/screens/addressGeolocation/GeolocationContainer.tsx b/src/screens/addressGeolocation/GeolocationContainer.tsx
index 174b7316..1a42baf5 100644
--- a/src/screens/addressGeolocation/GeolocationContainer.tsx
+++ b/src/screens/addressGeolocation/GeolocationContainer.tsx
@@ -1,17 +1,22 @@
-import React from 'react';
-import { StyleSheet, View } from 'react-native';
+import React, { useEffect, useMemo } from 'react';
+import { StyleSheet, TouchableOpacity, View } from 'react-native';
import Text from '../../../RN-UI-LIB/src/components/Text';
import { GenericStyles } from '../../../RN-UI-LIB/src/styles';
import { COLORS } from '../../../RN-UI-LIB/src/styles/colors';
import CustomLocationIcon from '../../assets/icons/CustomLocationIcon';
-import GeolocationAddress from '../../components/form/components/GeolocationAddress';
-import CustomLocationSmallIcon from '../../assets/icons/CustomLocationSmallIcon';
import { IGeolocation } from '../caseDetails/interface';
import { GenericFunctionArgs } from '../../common/GenericTypes';
-import { IGeoLocation } from '../../types/addressGeolocation.types';
import { useAppSelector } from '../../hooks';
-import { RootState } from '../../store/store';
+import { getDistanceFromLatLonInKm } from '@components/utlis/commonFunctions';
+import NoLocationIcon from '@rn-ui-lib/icons/NoLocationIcon';
+import LocationFillIcon from '@rn-ui-lib/icons/LocationFillIcon';
+import { navigateToScreen } from '@components/utlis/navigationUtlis';
+import GeolocationItem from './GeolocationItem';
+import { CaseDetailStackEnum } from '@screens/caseDetails/CaseDetailStack';
+import { CLICKSTREAM_EVENT_NAMES, HIT_SLOP } from '@common/Constants';
+import { addClickstreamEvent } from '@services/clickstreamEventService';
+const NEARBY_DISTANCE_THRESHOLD_IN_KM = 100;
interface IGeolocationContainer {
caseId: string;
loanAccountNumber: string;
@@ -25,9 +30,17 @@ const GeolocationContainer: React.FC = ({
loanAccountNumber,
handlePageRouting,
}) => {
- const { addressFeedbacks } = useAppSelector((state: RootState) => ({
- addressFeedbacks: state.address?.[loanAccountNumber]?.addressFeedbacks || [],
+ const { currentGeolocationCoordinates } = useAppSelector((state) => ({
+ currentGeolocationCoordinates: state.foregroundService?.deviceGeolocationCoordinate,
}));
+
+ useEffect(() => {
+ addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_GEOLOCATION_ADDITIONAL_GL_LOADED, {
+ loanAccountNumber,
+ caseId,
+ });
+ }, []);
+
if (!geolocationList?.length) {
return (
= ({
);
}
+ const { nearbyLocations, farAwayLocations } = useMemo(() => {
+ const nearbyLocations: IGeolocation[] = [];
+ const farAwayLocations: IGeolocation[] = [];
+ geolocationList.forEach((geolocation: IGeolocation) => {
+ const distance = getDistanceFromLatLonInKm(currentGeolocationCoordinates, {
+ latitude: geolocation?.latitude,
+ longitude: geolocation?.longitude,
+ });
+ if (!distance || isNaN(distance)) {
+ farAwayLocations.push(geolocation);
+ }
+ if (distance <= NEARBY_DISTANCE_THRESHOLD_IN_KM) {
+ nearbyLocations.push(geolocation);
+ } else {
+ farAwayLocations.push(geolocation);
+ }
+ });
+ return { nearbyLocations, farAwayLocations };
+ }, [geolocationList]);
+
+ const handleAdditionalGeolocations = () => {
+ addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_GEOLOCATION_ADDITIONAL_GL_CLICKED, {
+ caseId,
+ loanAccountNumber,
+ });
+ navigateToScreen(CaseDetailStackEnum.ADDITIONAL_GEOLOCATIONS, {
+ additionalGeolocations: farAwayLocations,
+ caseId,
+ loanAccountNumber,
+ handlePageRouting,
+ });
+ };
+
return (
-
- {geolocationList.map((geolocation: IGeolocation) => {
- const lastFeedbackForGeolocation = addressFeedbacks.find(
- (addressFeedback) => addressFeedback?.addressReferenceId === geolocation.id
- );
- return (
-
-
-
+
+ {nearbyLocations?.length ? (
+ nearbyLocations.map((geolocation: IGeolocation, index: number) => (
+
+ ))
+ ) : (
+
+
+
+ No nearby geolocations found
+
+
+ )}
+ {farAwayLocations?.length ? (
+ 0 && GenericStyles.mt12,
+ GenericStyles.fill,
+ ]}
+ >
+
+
+
+
+
+ Additional geolocations
+
+ View all geolocations
+
-
- );
- })}
+
+ ) : null}
);
};
const styles = StyleSheet.create({
- geoLocationItemStyle: {
- paddingLeft: 15,
- paddingTop: 20,
- },
textContainer: {
fontSize: 14,
lineHeight: 20,
@@ -89,19 +162,11 @@ const styles = StyleSheet.create({
color: '#BCBCBC',
fontWeight: '600',
},
- geolocationIcon: {
- backgroundColor: COLORS.BACKGROUND.SILVER,
- borderRadius: 4,
- width: 24,
- height: 24,
- alignItems: 'center',
- justifyContent: 'center',
- marginRight: 8,
- },
- geolocationItem: {
- borderBottomWidth: 1,
- borderBottomColor: COLORS.BORDER.PRIMARY,
- paddingVertical: 16,
+ actionBtn: {
+ fontSize: 13,
+ lineHeight: 20,
+ color: COLORS.TEXT.BLUE,
+ marginTop: 8,
},
});
diff --git a/src/screens/addressGeolocation/GeolocationItem.tsx b/src/screens/addressGeolocation/GeolocationItem.tsx
index 2cade0b5..bcc7681b 100644
--- a/src/screens/addressGeolocation/GeolocationItem.tsx
+++ b/src/screens/addressGeolocation/GeolocationItem.tsx
@@ -1,151 +1,61 @@
+import { StyleSheet, View } from 'react-native';
import React from 'react';
-import { Linking, StyleSheet, Text, type TextStyle, TouchableOpacity, View } from 'react-native';
-import { GenericStyles } from '../../../RN-UI-LIB/src/styles';
-import { COLORS } from '../../../RN-UI-LIB/src/styles/colors';
-import {
- BUSINESS_DATE_FORMAT,
- BUSINESS_TIME_FORMAT,
- dateFormat,
-} from '../../../RN-UI-LIB/src/utlis/dates';
-import {
- getDistanceFromLatLonInKm,
- getGoogleMapUrl,
- sanitizeString,
-} from '../../components/utlis/commonFunctions';
-import {
- type IGeoLocation,
- type IGeolocationCoordinate,
-} from '../../types/addressGeolocation.types';
-import { addClickstreamEvent } from '../../services/clickstreamEventService';
-import { CLICKSTREAM_EVENT_NAMES } from '../../common/Constants';
-import { type RootState } from '../../store/store';
-import { useAppSelector } from '../../hooks';
-import CustomLocationSmallIcon from '../../assets/icons/CustomLocationSmallIcon';
-import relativeDistanceFormatter from './utils/relativeDistanceFormatter';
+import GeolocationAddress from '@components/form/components/GeolocationAddress';
+import { useAppSelector } from '@hooks';
+import { RootState } from '@store';
+import { GenericStyles } from '@rn-ui-lib/styles';
+import { GenericFunctionArgs } from '@common/GenericTypes';
+import { IGeolocation } from '@screens/caseDetails/interface';
+import { COLORS } from '@rn-ui-lib/colors';
interface IGeolocationItem {
- geolocationItem: IGeoLocation;
- showSeparator?: boolean;
- highlightIcon?: boolean;
- containerStyle?: TextStyle;
+ geolocation: IGeolocation;
+ caseId: string;
+ loanAccountNumber: string;
+ handlePageRouting?: GenericFunctionArgs;
+ isLastItem?: boolean;
}
-function SeparatorBorderComponent() {
- return ;
-}
-
-function GeolocationItem({
- geolocationItem,
- showSeparator = true,
- highlightIcon = false,
- containerStyle,
-}: IGeolocationItem) {
- const currentGeolocationCoordinates: IGeolocationCoordinate = useAppSelector(
- (state: RootState) => state.foregroundService?.deviceGeolocationCoordinate
+const GeolocationItem: React.FC = ({
+ geolocation,
+ caseId,
+ loanAccountNumber,
+ isLastItem,
+ handlePageRouting,
+}) => {
+ const { addressFeedbacks } = useAppSelector((state: RootState) => ({
+ addressFeedbacks: state.address?.[loanAccountNumber]?.addressFeedbacks || [],
+ }));
+ const lastFeedbackForGeolocation = addressFeedbacks.find(
+ (addressFeedback) => addressFeedback?.addressReferenceId === geolocation.id
);
-
- const addressGeolocationCoordinated: IGeolocationCoordinate = {
- latitude: geolocationItem?.latitude,
- longitude: geolocationItem?.longitude,
- };
-
- const relativeDistanceBwLatLong = getDistanceFromLatLonInKm(
- currentGeolocationCoordinates,
- addressGeolocationCoordinated
- );
-
- const locationDate = dateFormat(new Date(geolocationItem?.timestamp), BUSINESS_DATE_FORMAT);
- const locationTime = dateFormat(new Date(geolocationItem?.timestamp), BUSINESS_TIME_FORMAT);
-
- const openGeolocation = async () => {
- addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_VIEW_MAP_GEO_CLICKED, {
- latitude: geolocationItem.latitude,
- longitude: geolocationItem.longitude,
- });
- const geolocationUrl = getGoogleMapUrl(geolocationItem?.latitude, geolocationItem?.longitude);
- if (!geolocationUrl) return;
-
- return await Linking.openURL(geolocationUrl);
- };
-
return (
-
-
-
-
-
-
-
-
- {relativeDistanceFormatter(relativeDistanceBwLatLong)} km away
-
-
- {sanitizeString(locationDate)}
- ●
- {sanitizeString(locationTime)}
-
-
- Open map
-
-
-
-
- {showSeparator ? (
-
-
-
- ) : null}
+
+
);
-}
+};
const styles = StyleSheet.create({
- container: {
+ geolocationItem: {
backgroundColor: COLORS.BACKGROUND.PRIMARY,
- },
- iconContainer: {
- borderRadius: 4,
- width: 24,
- height: 24,
- backgroundColor: COLORS.BACKGROUND.GREY_D9,
- alignItems: 'center',
- justifyContent: 'center',
- },
- contentContainer: {
- fontSize: 14,
- paddingHorizontal: 8,
- },
- titleText: {
- fontSize: 14,
- lineHeight: 20,
- color: COLORS.TEXT.DARK,
- },
- textContainer: {
- fontSize: 12,
- lineHeight: 18,
- color: COLORS.TEXT.LIGHT,
- },
- openMapBtn: {
- fontSize: 13,
- lineHeight: 20,
- color: COLORS.TEXT.BLUE,
- },
- borderLine: {
- borderWidth: 0.5,
- borderColor: COLORS.BORDER.PRIMARY,
- },
- dotStyle: {
- fontSize: 11,
- color: COLORS.TEXT.LIGHT,
+ padding: 16,
},
});
diff --git a/src/screens/addressGeolocation/GeolocationTimestamps.tsx b/src/screens/addressGeolocation/GeolocationTimestamps.tsx
new file mode 100644
index 00000000..e9ba266e
--- /dev/null
+++ b/src/screens/addressGeolocation/GeolocationTimestamps.tsx
@@ -0,0 +1,103 @@
+import { StyleSheet, View } from 'react-native';
+import React from 'react';
+import Text from '@rn-ui-lib/components/Text';
+import { GenericStyles } from '@rn-ui-lib/styles';
+import { COLORS } from '@rn-ui-lib/colors';
+import { COSMOS_STANDARD_DATE_TIME_FORMAT, dateFormat } from '@rn-ui-lib/utils/dates';
+
+interface IGeolocationTimestamps {
+ geolocationTimestamps: string[];
+ totalNumberOfTimestamps: number;
+ oldestGeolocationTimestamp?: string;
+}
+
+const GeolocationTimestamps: React.FC = ({
+ geolocationTimestamps = [],
+ totalNumberOfTimestamps,
+ oldestGeolocationTimestamp,
+}) => {
+ const timestampsCountLessThanTotal = geolocationTimestamps?.length < totalNumberOfTimestamps;
+ return (
+
+ {geolocationTimestamps?.map((timestamp, index) => {
+ const formattedDate = dateFormat(new Date(timestamp), COSMOS_STANDARD_DATE_TIME_FORMAT);
+ const isLastTimestamp =
+ index === geolocationTimestamps.length - 1;
+ const showDottedLine = isLastTimestamp && timestampsCountLessThanTotal;
+ const hideLine = isLastTimestamp && !timestampsCountLessThanTotal;
+ return (
+
+
+
+
+
+ {!hideLine ? : null}
+
+
+ {formattedDate}
+ {isLastTimestamp && timestampsCountLessThanTotal ? (
+
+ {totalNumberOfTimestamps - geolocationTimestamps?.length - 1} more locations
+
+ ) : null}
+
+
+ );
+ })}
+ {timestampsCountLessThanTotal && oldestGeolocationTimestamp ? (
+
+
+
+
+
+
+
+
+ {dateFormat(new Date(oldestGeolocationTimestamp), COSMOS_STANDARD_DATE_TIME_FORMAT)}
+
+
+
+ ) : null}
+
+ );
+};
+const styles = StyleSheet.create({
+ stepperLine: {
+ alignItems: 'center',
+ },
+ line: {
+ width: 1,
+ backgroundColor: COLORS.TEXT.GREY_1,
+ height: 40,
+ marginRight: 16,
+ top: 8,
+ },
+ dottedLine: {
+ width: 1,
+ height: 125,
+ marginRight: 16,
+ top: 8,
+ borderStyle: 'dashed',
+ borderRightWidth: 1,
+ borderColor: COLORS.TEXT.GREY_1,
+ },
+ whiteDot: {
+ width: 10,
+ height: 10,
+ borderRadius: 5,
+ backgroundColor: COLORS.BACKGROUND.PRIMARY,
+ marginRight: 16,
+ top: 8,
+ zIndex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ blueDot: {
+ width: 8,
+ height: 8,
+ borderRadius: 4,
+ backgroundColor: COLORS.BACKGROUND.BLUE_LIGHT_4,
+ },
+});
+
+export default GeolocationTimestamps;
diff --git a/src/screens/addressGeolocation/SimilarAddressItem.tsx b/src/screens/addressGeolocation/SimilarAddressItem.tsx
index de158584..5df46dd2 100644
--- a/src/screens/addressGeolocation/SimilarAddressItem.tsx
+++ b/src/screens/addressGeolocation/SimilarAddressItem.tsx
@@ -173,7 +173,7 @@ function SimilarAddressItem({
{!isNaN(relativeDistanceBwLatLong)
? relativeDistanceFormatter(relativeDistanceBwLatLong)
: '--'}{' '}
- km away
+ km
>
) : null}
{showSource ? : null}
diff --git a/src/screens/addressGeolocation/SimilarGeolocations.tsx b/src/screens/addressGeolocation/SimilarGeolocations.tsx
new file mode 100644
index 00000000..b1466d40
--- /dev/null
+++ b/src/screens/addressGeolocation/SimilarGeolocations.tsx
@@ -0,0 +1,79 @@
+import { ScrollView, StyleSheet, View } from 'react-native';
+import React, { useEffect, useState } from 'react';
+import Layout from '@screens/layout/Layout';
+import NavigationHeader from '@rn-ui-lib/components/NavigationHeader';
+import { goBack } from '@components/utlis/navigationUtlis';
+import { IGeolocation } from '@screens/caseDetails/interface';
+import { GenericStyles } from '@rn-ui-lib/styles';
+import Text from '@rn-ui-lib/components/Text';
+import GeolocationTimestamps from './GeolocationTimestamps';
+import { getSimilarGeolocationTimestamps } from '@actions/addressGeolocationAction';
+import SuspenseLoader from '@rn-ui-lib/components/suspense_loader/SuspenseLoader';
+import FullScreenLoader from '@rn-ui-lib/components/FullScreenLoader';
+import { addClickstreamEvent } from '@services/clickstreamEventService';
+import { CLICKSTREAM_EVENT_NAMES } from '@common/Constants';
+
+interface ISimilarGeolocations {
+ route: {
+ params: IGeolocation & { caseId: string };
+ };
+}
+
+interface IGeolocationTimestamps {
+ timestamps: string[];
+ clusterId: string;
+}
+
+const SimilarGeolocations: React.FC = (props) => {
+ const { params } = props.route || {};
+ const { tag, pingCount, clusterId, minTimestamp, caseId, id } = params || {};
+ const [isLoading, setIsLoading] = useState(false);
+ const [geolocationTimestamps, setGeolocationTimestamps] = useState(
+ null
+ );
+
+ useEffect(() => {
+ setIsLoading(true);
+ getSimilarGeolocationTimestamps(clusterId)
+ .then((res) => {
+ if (res) {
+ addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_GEOLOCATION_TIMESTAMP_LOADED, {
+ addressId: id,
+ clusterId,
+ customerReferenceId: caseId,
+ });
+ setGeolocationTimestamps(res);
+ }
+ })
+ .finally(() => {
+ setIsLoading(false);
+ });
+ }, []);
+
+ return (
+
+
+
+
+ }>
+
+ Time of locations
+
+
+
+
+
+
+ );
+};
+
+export default SimilarGeolocations;
+
+const styles = StyleSheet.create({});
diff --git a/src/screens/addressGeolocation/constant.ts b/src/screens/addressGeolocation/constant.ts
index b6e872b9..ebd82fbf 100644
--- a/src/screens/addressGeolocation/constant.ts
+++ b/src/screens/addressGeolocation/constant.ts
@@ -18,3 +18,29 @@ export const PRIMARY_SOURCE_MAPPING = {
[PrimarySourcesType.SKIP_TRACING]: 'Skip Tracing',
[PrimarySourcesType.ACCOUNT_AGGREGATOR]: 'Account Aggregator',
};
+
+export const ADDRESSES_TABS = [
+ {
+ key: 'address',
+ label: 'Addresses',
+ },
+ {
+ key: 'geolocation',
+ label: 'Geolocations',
+ },
+];
+
+export enum AddressGeolocationTabEnum {
+ ADDRESS = 'address',
+ GEOLOCATION = 'geolocation',
+}
+
+export interface IAddressGeolocation {
+ route: {
+ params: {
+ loanAccountNumber: string;
+ customerReferenceId: string;
+ caseId: string;
+ };
+ };
+}
diff --git a/src/screens/addressGeolocation/index.tsx b/src/screens/addressGeolocation/index.tsx
index d12db629..cc319c00 100644
--- a/src/screens/addressGeolocation/index.tsx
+++ b/src/screens/addressGeolocation/index.tsx
@@ -14,7 +14,7 @@ import PlusIcon from '../../../RN-UI-LIB/src/Icons/PlusIcon';
import useIsOnline from '../../hooks/useIsOnline';
import OfflineScreen from '../../common/OfflineScreen';
import { getCaseUnifiedData, UnifiedCaseDetailsTypes } from '../../action/caseApiActions';
-import { CLICKSTREAM_EVENT_NAMES } from '../../common/Constants';
+import { CLICKSTREAM_EVENT_NAMES, HIT_SLOP } from '../../common/Constants';
import { addClickstreamEvent } from '../../services/clickstreamEventService';
import HomeIconSmall from '../../assets/icons/HomeIconSmall';
import { IAddressFeedback, setAddressLoading } from '../../reducer/addressSlice';
@@ -27,20 +27,12 @@ import { getUngroupedAddress } from '../../action/addressGeolocationAction';
import { type IAddress } from '../../types/addressGeolocation.types';
import filterFarAwayMetaAddresses from './utils/FilterFarAwayMetaAddresses';
import { MAXIMUM_ALLOWED_DISTANCE_FOR_GROUPED_ADDRESSES } from './constants';
+import CustomTabs from '@rn-ui-lib/components/customTabs/CustomTabs';
import { CaseDetailStackEnum } from '@screens/caseDetails/CaseDetailStack';
+import { ADDRESSES_TABS, AddressGeolocationTabEnum, IAddressGeolocation } from './constant';
const PAGE_TITLE = 'All addresses';
-interface IAddressGeolocation {
- route: {
- params: {
- loanAccountNumber: string;
- customerReferenceId: string;
- caseId: string;
- };
- };
-}
-
export interface IUngroupedAddressWithFeedbacks {
ungroupedAddresses: IAddress[];
ungroupedAddressFeedbacks: IAddressFeedback[];
@@ -52,6 +44,7 @@ const AddressGeolocation: React.FC = ({ route: routeParams
} = routeParams;
const [ungroupedAddress, setUngroupedAddress] = useState([]);
+ const [selectedTab, setSelectedTab] = useState(AddressGeolocationTabEnum.ADDRESS);
const [ungroupedAddressFeedbacks, setUngroupedAddressFeedbacks] = useState(
[]
);
@@ -123,9 +116,7 @@ const AddressGeolocation: React.FC = ({ route: routeParams
await fetchUngroupedAddress(loanAccountNumber);
return null;
}
- if (addressGeolocation?.groupedAddresses?.length > 0) {
- getUngroupedAddress();
- }
+ getUngroupedAddress();
}, [addressGeolocation?.groupedAddresses, isLoading]);
useEffect(() => {
@@ -161,6 +152,18 @@ const AddressGeolocation: React.FC = ({ route: routeParams
})();
}, []);
+ const handleTabChange = (tab: string) => {
+ if (tab !== selectedTab) {
+ if (tab === AddressGeolocationTabEnum.GEOLOCATION) {
+ addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_GEOLOCATION_TAB_CLICKED, commonParams);
+ }
+ if (tab === AddressGeolocationTabEnum.ADDRESS) {
+ addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_ADDRESS_TAB_CLICKED, commonParams);
+ }
+ setSelectedTab(tab);
+ }
+ };
+
if (!isOnline) {
return (
@@ -169,8 +172,14 @@ const AddressGeolocation: React.FC = ({ route: routeParams
return (
-
+
+
= ({ route: routeParams
>
}
>
-
-
- {ungroupedAddress?.length > 0 ? (
-
-
-
-
-
-
-
- Additional addresses
-
- {
- handleRouting(CaseDetailStackEnum.ADDITIONAL_ADDRESSES, {
- fetchUngroupedAddress,
- ungroupedAddressFeedbacks,
- });
- addClickstreamEvent(
- CLICKSTREAM_EVENT_NAMES.FA_ADDITIONAL_ADDRESSES_BUTTON_CLICKED
- );
- }}
- >
- View all addresses
-
+
+
+
+ {ungroupedAddress?.length > 0 ? (
+
+
+
+
+
+
+
+ Additional addresses
+
+
+ handleRouting(CaseDetailStackEnum.ADDITIONAL_ADDRESSES, {
+ fetchUngroupedAddress,
+ ungroupedAddressFeedbacks,
+ })
+ }
+ >
+ View all addresses
+
+
-
- ) : null}
- User geolocations
-
+ ) : null}
+
+
+
+
@@ -274,9 +283,9 @@ const styles = StyleSheet.create({
},
iconContainer: {
borderRadius: 4,
- width: 24,
- height: 24,
- backgroundColor: COLORS.BACKGROUND.SILVER,
+ width: 20,
+ height: 20,
+ backgroundColor: COLORS.BORDER.PRIMARY,
alignItems: 'center',
justifyContent: 'center',
},
@@ -290,6 +299,9 @@ const styles = StyleSheet.create({
fontWeight: '500',
color: COLORS.TEXT.BLACK_24,
},
+ hidden: {
+ display: 'none',
+ },
});
export default AddressGeolocation;
diff --git a/src/screens/allCases/index.tsx b/src/screens/allCases/index.tsx
index b8f92447..4704f625 100644
--- a/src/screens/allCases/index.tsx
+++ b/src/screens/allCases/index.tsx
@@ -196,7 +196,10 @@ const AllCasesMain = () => {
};
useEffect(() => {
- initCrashlytics(userState);
+ if(!__DEV__) {
+ // Disable crashlytics in dev mode
+ initCrashlytics(userState);
+ }
dispatch(setVisitPlansUpdating(false));
dispatch(setLoading(false));
dispatch(resetTodoList());
diff --git a/src/screens/auth/AuthRouter.tsx b/src/screens/auth/AuthRouter.tsx
index 76244362..40f4f2b6 100644
--- a/src/screens/auth/AuthRouter.tsx
+++ b/src/screens/auth/AuthRouter.tsx
@@ -1,8 +1,7 @@
import { AppState, StyleSheet } from 'react-native';
import React, { useEffect } from 'react';
-import { useSelector } from 'react-redux';
import { getUniqueId, isTablet } from 'react-native-device-info';
-import { useAppDispatch } from '../../hooks';
+import { useAppDispatch, useAppSelector } from '../../hooks';
import { type RootState } from '../../store/store';
import { setAgentAttendance, setDeviceId } from '../../reducer/userSlice';
import { DEVICE_TYPE_ENUM, setGlobalUserData } from '../../constants/Global';
@@ -21,15 +20,13 @@ import {
alfredSetPhoneNumber,
alfredSetUserId,
} from '../../components/utlis/DeviceUtils';
-import { getAppVersion, getPhoneNumberString } from '../../components/utlis/commonFunctions';
-import AnswerRender from '../../components/form/AnswerRender';
+import { getAppVersion } from '../../components/utlis/commonFunctions';
import useScreenshotTracking from '../../hooks/useScreenshotTracking';
import { getSyncTime } from '@hooks/capturingApi';
-
function AuthRouter() {
const dispatch = useAppDispatch();
- const user = useSelector((state: RootState) => state.user);
+ const user = useAppSelector((state: RootState) => state.user);
const { isLoggedIn, deviceId, sessionDetails, isTeamLead, agentAttendance } = user || {};
useNativeButtons();
@@ -67,7 +64,6 @@ function AuthRouter() {
};
const CHECK_ATTENDANCE_TIME = 10000;
-
useEffect(() => {
const appStateChange = AppState.addEventListener('change', async (change) => {
if (change !== 'active') return;
diff --git a/src/screens/caseDetails/CaseDetailStack.tsx b/src/screens/caseDetails/CaseDetailStack.tsx
index 6c638f18..5921f641 100644
--- a/src/screens/caseDetails/CaseDetailStack.tsx
+++ b/src/screens/caseDetails/CaseDetailStack.tsx
@@ -23,6 +23,8 @@ import AddNewNumber from '@screens/addNewNumber';
import ViewRequestHistory from '@screens/cosmosSupport/ViewRequestHistory';
import { RequestDetail } from '@screens/cosmosSupport';
import RequestSupport from '@screens/cosmosSupport/RequestSupport';
+import SimilarGeolocations from '@screens/addressGeolocation/SimilarGeolocations';
+import AdditionalGeolocations from '@screens/addressGeolocation/AdditionalGeolocations';
const Stack = createNativeStackNavigator();
@@ -43,6 +45,8 @@ export enum CaseDetailStackEnum {
VIEW_RequestHistory = 'viewRequestHistory',
VIEW_REQUEST_DETAIL = 'viewRequestDetail',
RAISE_REQUEST = 'raiseRequest',
+ SIMILAR_GEOLOCATIONS = 'SimilarGeolocations',
+ ADDITIONAL_GEOLOCATIONS = 'AdditionalGeolocations',
}
const CaseDetailStack = () => {
@@ -65,6 +69,14 @@ const CaseDetailStack = () => {
+
+
= ({ caseData }) => {
pos,
collectionTag,
employmentDetail,
+ unpaidDays
} = caseData;
return (
@@ -36,7 +37,7 @@ const CollectionCaseData: React.FC = ({ caseData }) => {
)}
- Current DPD {currentDpd}
+ Current DPD {unpaidDays ? unpaidDays : currentDpd}
{loanAccountNumber && (
= (props) => {
useEffect(() => {
// If landed via notification action
if (notificationId) {
- if (showPhoneNumberBottomsheet) {
+ if (showPhoneNumberBottomsheet && showPhoneNumberBottomsheet !== 'false') {
setShowPhoneNumberSheet(true);
}
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_PUSH_NOTIFICATION_SCREEN_LOADED, {
@@ -108,18 +111,11 @@ const CollectionCaseDetails: React.FC = (props) => {
const isOnline = useIsOnline();
const caseDetail = useAppSelector((state) => state.allCases.caseDetails[caseId]!!);
- const data = useAppSelector(
+ const preFilledFormData = useAppSelector(
(state) => state.case.caseForm?.[caseId]?.[TaskTitleUIMapping.COLLECTION_FEEDBACK]
);
- const {
- addressString,
- phoneNumbers,
- currentOutstandingEmi,
- loanAccountNumber,
- totalOverdueAmount,
- pos,
- } = caseDetail;
+ const { addressString, phoneNumbers, loanAccountNumber, totalOverdueAmount, pos } = caseDetail;
const feedbackList: IFeedback[] = useAppSelector(
(state: RootState) => state.feedbackHistory?.[loanAccountNumber as string]?.data || []
@@ -127,12 +123,25 @@ const CollectionCaseDetails: React.FC = (props) => {
const allCasesDetails = useAppSelector((state) => state.allCases.caseDetails);
+ useEffect(() => {
+ if (caseId) dispatch(setSelectedCaseId(caseId));
+
+ return () => {
+ dispatch(setSelectedCaseId(''));
+ };
+ }, [caseId]);
+
useEffect(() => {
if (!loanAccountNumber) {
return;
}
- if (isOnline || !feedbackList) {
- dispatch(getCaseUnifiedData([loanAccountNumber], [UnifiedCaseDetailsTypes.FEEDBACKS]));
+ if (isOnline) {
+ dispatch(
+ getCaseUnifiedData(
+ [loanAccountNumber],
+ [UnifiedCaseDetailsTypes.FEEDBACKS, UnifiedCaseDetailsTypes.ADDRESS_AND_GEOLOCATIONS]
+ )
+ );
}
}, [caseDetail]);
@@ -167,7 +176,7 @@ const CollectionCaseDetails: React.FC = (props) => {
const [showOutstandingAmountBottomSheet, setShowOutstandingAmountBottomSheet] =
React.useState(false);
const [showPhoneNumberSheet, setShowPhoneNumberSheet] = React.useState(
- !!showPhoneNumberBottomsheet
+ showPhoneNumberBottomsheet !== 'false' && showPhoneNumberBottomsheet
);
const {
totalOverdueAmount: outstandingTotalOverDueAmount,
@@ -191,13 +200,26 @@ const CollectionCaseDetails: React.FC = (props) => {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.AV_CASE_DETAILS_ADD_FEEDBACK_CLICKED, {
caseId: caseId,
caseType: caseDetail?.caseType,
- journey: 'COLLECTION_FEEDBACK',
+ journey: TaskTitleUIMapping.COLLECTION_FEEDBACK,
});
- if (data?.visitedWidgets?.length) {
- _map(data.visitedWidgets, (visited) =>
+ if (preFilledFormData?.visitedWidgets?.length) {
+ const lastFormInteractionTs = preFilledFormData?.updatedAt || 0;
+ // If Date.now() is greater than updatedAt by 60mins, then we will navigate to the first widget
+ const isTimeExpired = Date.now() - lastFormInteractionTs > FEEDBACK_FORM_RESPONSE_EXPIRY_TIME;
+ if (isTimeExpired) {
+ navigateToScreen(
+ getTemplateRoute(CollectionCaseWidgetId.START, CaseAllocationType.COLLECTION_CASE),
+ {
+ caseId: caseId,
+ journey: TaskTitleUIMapping.COLLECTION_FEEDBACK,
+ }
+ );
+ return;
+ }
+ _map(preFilledFormData.visitedWidgets, (visited) =>
navigateToScreen(getTemplateRoute(visited, CaseAllocationType.COLLECTION_CASE), {
caseId: caseId,
- journey: 'COLLECTION_FEEDBACK',
+ journey: TaskTitleUIMapping.COLLECTION_FEEDBACK,
})
);
return;
@@ -206,7 +228,7 @@ const CollectionCaseDetails: React.FC = (props) => {
getTemplateRoute(CollectionCaseWidgetId.START, CaseAllocationType.COLLECTION_CASE),
{
caseId: caseId,
- journey: 'COLLECTION_FEEDBACK',
+ journey: TaskTitleUIMapping.COLLECTION_FEEDBACK,
}
);
};
@@ -513,7 +535,14 @@ const CollectionCaseDetails: React.FC = (props) => {
/>
+ ) : null
+ }
variant="primary"
onPress={handleAddFeedback}
testID={'test_add_feedback'}
diff --git a/src/screens/caseDetails/feedback/FeedbackDetailAnswerContainer.tsx b/src/screens/caseDetails/feedback/FeedbackDetailAnswerContainer.tsx
index 54fbb234..0da4d7bd 100644
--- a/src/screens/caseDetails/feedback/FeedbackDetailAnswerContainer.tsx
+++ b/src/screens/caseDetails/feedback/FeedbackDetailAnswerContainer.tsx
@@ -9,6 +9,7 @@ import { AnswerType, IAnswerView, OPTION_TAG } from '../../../types/feedback.typ
import { addClickstreamEvent } from '../../../services/clickstreamEventService';
import { CLICKSTREAM_EVENT_NAMES } from '../../../common/Constants';
import FeedbackDetailImageItem from './FeedbackDetailImageItem';
+import {formatAmount} from "@rn-ui-lib/utils/amount";
const getAnswerText = (answer: IAnswerView) => {
switch (answer?.answerType) {
@@ -21,6 +22,8 @@ const getAnswerText = (answer: IAnswerView) => {
return sanitizeString(dateFormat(new Date(answer.inputDate as string), BUSINESS_DATE_FORMAT));
case AnswerType.AMOUNT:
return `₹ ${answer.inputAmount}`;
+ case AnswerType.PHONE_NUMBER:
+ return sanitizeString(answer.inputText);
default:
return sanitizeString();
}
@@ -64,7 +67,7 @@ const FeedbackDetailAnswerContainer: React.FC =
{getQuestionText(answerItem)}
- {getAnswerText(answerItem)}
+ {getQuestionText(answerItem) === 'Amount Promised'? formatAmount(Number(getAnswerText(answerItem).split(' ')[1])): getAnswerText(answerItem)}
))}
diff --git a/src/screens/caseDetails/feedback/FeedbackDetailContainer.tsx b/src/screens/caseDetails/feedback/FeedbackDetailContainer.tsx
index c0f3dea2..19cbec78 100644
--- a/src/screens/caseDetails/feedback/FeedbackDetailContainer.tsx
+++ b/src/screens/caseDetails/feedback/FeedbackDetailContainer.tsx
@@ -240,7 +240,7 @@ const FeedbackDetailContainer: React.FC = ({ route: ro
}
style={getShadowStyle(2)}
title="Filters"
- buttonStyle={[GenericStyles.ph12, { paddingVertical: 8 }]}
+ buttonStyle={[GenericStyles.ph12, GenericStyles.pv8]}
onPress={() => setShowFilterModal(true)}
/>
{feedbackFilterCount ? (
diff --git a/src/screens/caseDetails/feedback/FeedbackDetailItem.tsx b/src/screens/caseDetails/feedback/FeedbackDetailItem.tsx
index 7926b635..a9329a5a 100644
--- a/src/screens/caseDetails/feedback/FeedbackDetailItem.tsx
+++ b/src/screens/caseDetails/feedback/FeedbackDetailItem.tsx
@@ -30,6 +30,8 @@ import IconLabel from '../../../common/IconLabel';
import WhatsAppFeedbackShareIcon from '../../../assets/icons/WhatsAppIcon';
import { useAppSelector } from '../../../hooks';
import { shareToWhatsapp } from '../../../services/FeedbackWhatsApp';
+import SmsIcon from "@assets/icons/SmsIcon";
+import Button from '@rn-ui-lib/components/Button';
interface IFeedbackDetailItem {
feedbackItem: IFeedback;
@@ -38,11 +40,12 @@ interface IFeedbackDetailItem {
caseId: string;
}
-const feedbackTypeIcon: Record = {
+export const feedbackTypeIcon: Record = {
FIELD_VISIT: ,
INHOUSE_FIELD_VISIT: ,
SELF_CALL: ,
CALL_BRIDGE: ,
+ GEN_AI_BOT_FIELD: ,
};
const getAddress = (address?: Address) => {
@@ -81,74 +84,75 @@ const FeedbackDetailItem = ({
return (
- {feedbackTypeIcon[feedbackItem.type] ? (
- {feedbackTypeIcon[feedbackItem.type]}
- ) : null}
+ {feedbackTypeIcon[feedbackItem.type] ? (
+ {feedbackTypeIcon[feedbackItem.type]}
+ ) : null}
+
+ {sanitizeString(feedbackItem.interactionStatus)}
+
+
- {sanitizeString(feedbackItem.interactionStatus)}
+ {sanitizeString(dateFormat(new Date(feedbackItem.createdAt), BUSINESS_DATE_FORMAT))}
+ ●
+ {sanitizeString(dateFormat(new Date(feedbackItem.createdAt), BUSINESS_TIME_FORMAT))}
-
-
- {sanitizeString(dateFormat(new Date(feedbackItem.createdAt), BUSINESS_DATE_FORMAT))}
- ●
- {sanitizeString(dateFormat(new Date(feedbackItem.createdAt), BUSINESS_TIME_FORMAT))}
-
- {isGeolocation ? (
- !hideAddress ? (
-
+
+
+ ) : null
+ ) : (
+
-
-
- ) : null
- ) : (
-
- {FIELD_FEEDBACKS.includes(feedbackItem.type)
- ? sanitizeString(getAddress(feedbackItem?.source as unknown as Address))
- : sanitizeString(
- [
- (feedbackItem.source as ICallingFeedback)?.recipientNumber,
- feedbackItem.sourceText ? `(${feedbackItem.sourceText})` : '',
- ].join(' ')
- )}
-
- )}
+ {FIELD_FEEDBACKS.includes(feedbackItem.type)
+ ? sanitizeString(getAddress(feedbackItem?.source as unknown as Address))
+ : sanitizeString(
+ [
+ (feedbackItem.source as ICallingFeedback)?.recipientNumber,
+ feedbackItem.sourceText ? `(${feedbackItem.sourceText})` : '',
+ ].join(' ')
+ )}
+
+ )}
{feedbackItem.metadata?.interactionLatitude && FIELD_FEEDBACKS.includes(feedbackItem.type) ? (
<>
-
+
= ({
- {sanitizeString(feedbackItem.interactionStatus)}
+
+ {feedbackItem.type && feedbackTypeIcon[feedbackItem.type] ? (
+
+ {feedbackTypeIcon[feedbackItem.type]}
+
+ ) : null}
+
+ {sanitizeString(feedbackItem.interactionStatus)}
+
+
{sanitizeString(
`${dateFormat(new Date(feedbackItem?.createdAt), BUSINESS_DATE_FORMAT)}`
@@ -109,6 +131,9 @@ const styles = StyleSheet.create({
paddingTop: 4,
color: COLORS.TEXT.DARK,
},
+ capitalized: {
+ textTransform: "capitalize"
+ },
subText: {
fontSize: 12,
lineHeight: 18,
@@ -131,6 +156,9 @@ const styles = StyleSheet.create({
ButtonText: {
color: COLORS.BASE.BLUE,
},
+ ml_4: {
+ marginLeft: -4,
+ },
});
export default FeedbackListItem;
diff --git a/src/screens/caseDetails/interface.ts b/src/screens/caseDetails/interface.ts
index 437096b5..020ac8db 100644
--- a/src/screens/caseDetails/interface.ts
+++ b/src/screens/caseDetails/interface.ts
@@ -226,7 +226,7 @@ export interface IGeolocation {
secondarySource: string;
customer_id: string;
tag: string;
- capturedTimestamp: string;
+ timestamp: string;
createdAt: string;
updatedAt: string;
metadata: string;
@@ -234,6 +234,10 @@ export interface IGeolocation {
visitType: VisitType;
sourceType: VisitType;
source?: string;
+ pingCount: number;
+ clusterLabels: string[];
+ clusterId: string;
+ minTimestamp: string;
}
export interface EmploymentDetails {
employmentType: string;
@@ -291,6 +295,7 @@ export interface CaseDetail {
forceSubmit?: boolean;
geolocations?: IGeolocation[];
employmentDetail?: EmploymentDetails;
+ unpaidDays?: number;
}
export interface AddressesGeolocationPayload {
@@ -379,3 +384,8 @@ export enum PhoneNumberSource {
export interface INearbyCaseItemObj extends CaseDetail {
distanceInKm: number;
}
+
+export enum GeolocationSourceMap {
+ VKYC = 'VKYC',
+ DATA_SUTRAM = 'Skip Tracing',
+}
diff --git a/src/screens/cosmosSupport/RequestSupport.tsx b/src/screens/cosmosSupport/RequestSupport.tsx
index 400e2363..ce2e4c85 100644
--- a/src/screens/cosmosSupport/RequestSupport.tsx
+++ b/src/screens/cosmosSupport/RequestSupport.tsx
@@ -1,5 +1,5 @@
import FullScreenLoaderWrapper from '@common/FullScreenLoaderWrapper';
-import { goBack } from '@components/utlis/navigationUtlis';
+import { goBack, navigateToScreen } from '@components/utlis/navigationUtlis';
import { useAppDispatch, useAppSelector } from '@hooks';
import Button from '@rn-ui-lib/components/Button';
import NavigationHeader from '@rn-ui-lib/components/NavigationHeader';
@@ -14,210 +14,195 @@ import { Controller, useForm } from 'react-hook-form';
import { SafeAreaView, ScrollView, StyleSheet, View } from 'react-native';
import { createTicket, getDataForTicketCreation } from './actions';
import { CreateTicketPayload } from './constant/types';
-import {MAX_COMENT_LENGTH} from "@screens/cosmosSupport/constant";
-import {COLORS} from "@rn-ui-lib/colors";
-
-
-
+import { MAX_COMENT_LENGTH } from '@screens/cosmosSupport/constant';
+import { COLORS } from '@rn-ui-lib/colors';
+import { CaseDetailStackEnum } from '@screens/caseDetails/CaseDetailStack';
const LoadingChips = () => {
- return (
-
- {[...Array(5)].map((type, index) => (
-
- ))}
-
- )
-
-}
+ return (
+
+ {[...Array(5)].map((type, index) => (
+
+ ))}
+
+ );
+};
interface RequestSupportProps {
- route: {
- params: {
- caseDetail: {
- customerName: string;
- loanAccountNumber: number;
- };
- };
- }
+ route: {
+ params: {
+ caseDetail: {
+ customerName: string;
+ loanAccountNumber: number;
+ };
+ };
+ };
}
-
const RequestSupport: React.FC = (props) => {
+ const { caseDetail } = props?.route?.params;
- const { caseDetail } = props?.route?.params;
+ const dispatch = useAppDispatch();
- const dispatch = useAppDispatch();
+ const [isLoading, setIsLoading] = React.useState(true);
+ const { clicked, data } = useAppSelector((state) => ({
+ clicked: state?.cosmosSupport?.ticketCreationData?.isLoading,
+ data: state?.cosmosSupport?.ticketCreationData?.ticketCreationData,
+ }));
+ const {
+ control,
+ handleSubmit,
+ formState: { errors },
+ watch,
+ reset,
+ } = useForm({});
- const [isLoading, setIsLoading] = React.useState(true);
- const { clicked, data } = useAppSelector(state => ({
- clicked: state?.cosmosSupport?.ticketCreationData?.isLoading,
- data: state?.cosmosSupport?.ticketCreationData?.ticketCreationData
- }));
+ const watchRequestType = watch('requestType');
- const {
- control,
- handleSubmit,
- formState: { errors },
- watch,
- reset
- } = useForm({});
+ useEffect(() => {
+ dispatch(getDataForTicketCreation(String(caseDetail?.loanAccountNumber)));
+ }, []);
+ const handleCreation = () => {
+ reset();
+ navigateToScreen(CaseDetailStackEnum.VIEW_RequestHistory, { caseDetail: caseDetail, tabId: 1 });
+ };
- const watchRequestType = watch('requestType');
-
-
- useEffect(() => {
- dispatch(getDataForTicketCreation(String(caseDetail?.loanAccountNumber)));
- }, []);
-
-
- const handleCreation = () => {
- reset();
- }
-
- const handleSubmitForm = (data: any) => {
-
- const payload: CreateTicketPayload = {
- supportRequestType: data?.requestType,
- supportRequestCreatorType: 'FE',
- loanAccountNumber: String(caseDetail?.loanAccountNumber),
- comment: data?.requestDescription
- };
- dispatch(createTicket(payload, handleCreation));
- }
-
- const handleBackPress = () => {
- goBack();
+ const handleSubmitForm = (data: any) => {
+ const payload: CreateTicketPayload = {
+ supportRequestType: data?.requestType,
+ supportRequestCreatorType: 'FE',
+ loanAccountNumber: String(caseDetail?.loanAccountNumber),
+ comment: data?.requestDescription,
};
- const noData = Boolean(data?.requestForm?.length && data?.requestForm?.length === 0);
- return (
-
-
+ dispatch(createTicket(payload, handleCreation));
+ };
-
-
- Select request type
+ const handleBackPress = () => {
+ goBack();
+ };
+ const noData = Boolean(data?.requestForm?.length && data?.requestForm?.length === 0);
+ return (
+
+
- }
- >
-
- {data?.requestForm?.map((type, index) => (
- (
- onChange(data)}
- checked={value === type.value}
- meta={type.value}
- textStyles={styles.lineHeight20}
- />
- )}
- />
- ))}
-
-
+
+
+ Select request type
- {noData ? null : (
-
- = MAX_COMENT_LENGTH}
- maxLength={MAX_COMENT_LENGTH}
- />
- = MAX_COMENT_LENGTH ? styles.error: {}]}>{value?.length || 0}/300
-
-
- )}
- />}
-
-
-
- }>
+
+ {data?.requestForm?.map((type, index) => (
+ (
+ onChange(data)}
+ checked={value === type.value}
+ meta={type.value}
+ textStyles={styles.lineHeight20}
+ />
+ )}
/>
+ ))}
-
-
- )
-}
+
+ {noData ? null : (
+ (
+
+ = MAX_COMENT_LENGTH}
+ maxLength={MAX_COMENT_LENGTH}
+ />
+ = MAX_COMENT_LENGTH ? styles.error : {}]}
+ >
+ {value?.length || 0}/300
+
+
+ )}
+ />
+ )}
+
+
+
+
+
+
+
+ );
+};
const styles = StyleSheet.create({
- navigationContainerStyle: {
- paddingVertical: 9
- },
- navigationContainerSubtitle: {
- marginTop: 0
- },
- navigationContainerTitle: {
- fontWeight: '500',
- lineHeight: 20
- },
- tagContainer: {
- borderRadius: 40,
- padding: 4
- },
- textInput: {
- textAlignVertical: 'top',
- minHeight: 100,
- },
- mv10: {
- marginVertical: 10
- },
- count: {
- position: 'absolute',
- bottom: -20,
- right: 0
- },
- lineHeight20:{
- lineHeight: 20
- },
- error: {
- color: COLORS.TEXT.RED
- }
+ navigationContainerStyle: {
+ paddingVertical: 9,
+ },
+ navigationContainerSubtitle: {
+ marginTop: 0,
+ },
+ navigationContainerTitle: {
+ fontWeight: '500',
+ lineHeight: 20,
+ },
+ tagContainer: {
+ borderRadius: 40,
+ padding: 4,
+ },
+ textInput: {
+ textAlignVertical: 'top',
+ minHeight: 100,
+ },
+ mv10: {
+ marginVertical: 10,
+ },
+ count: {
+ position: 'absolute',
+ bottom: -20,
+ right: 0,
+ },
+ lineHeight20: {
+ lineHeight: 20,
+ },
+ error: {
+ color: COLORS.TEXT.RED,
+ },
});
-
-export default RequestSupport
\ No newline at end of file
+export default RequestSupport;
diff --git a/src/screens/cosmosSupport/ViewRequestHistory.tsx b/src/screens/cosmosSupport/ViewRequestHistory.tsx
index cc4b560c..90eab1c6 100644
--- a/src/screens/cosmosSupport/ViewRequestHistory.tsx
+++ b/src/screens/cosmosSupport/ViewRequestHistory.tsx
@@ -38,15 +38,16 @@ interface IViewRequestHistory {
loanAccountNumber: number;
};
from?: From;
+ tabId?: number
};
};
}
const ViewRequestHistory = (props: IViewRequestHistory) => {
- const { from, caseDetail } = props?.route?.params;
+ const { tabId, from, caseDetail } = props?.route?.params;
const layout = useWindowDimensions();
- const [index, setIndex] = React.useState(0);
+ const [index, setIndex] = React.useState(tabId || 0);
const dispatch = useAppDispatch();
const {summary} = useAppSelector(state => ({
summary: state.cosmosSupport.caseSummary
diff --git a/src/screens/login/index.tsx b/src/screens/login/index.tsx
index ceb52795..3730bb91 100644
--- a/src/screens/login/index.tsx
+++ b/src/screens/login/index.tsx
@@ -92,12 +92,6 @@ function Login() {
}
throw userInfo;
} catch (error: GenericType) {
- if (error?.code !== GoogleSignInError.SIGN_IN_ACTION_CANCELLED) {
- toast({
- text1: ToastMessages.GENERIC_ERROR_TOAST,
- type: 'error',
- });
- }
await handleGoogleLogout();
logError(error);
}
diff --git a/src/screens/notifications/NotificationItem.tsx b/src/screens/notifications/NotificationItem.tsx
index 8d33527f..3aa11e76 100644
--- a/src/screens/notifications/NotificationItem.tsx
+++ b/src/screens/notifications/NotificationItem.tsx
@@ -48,6 +48,9 @@ export interface INotification {
caseCount: number;
cashCommitted: number;
visitsCommitted: number;
+ callbackDate?: string;
+ phoneNumber?: string;
+ callbackTime?: string;
};
template: {
id: number;
@@ -146,7 +149,9 @@ const NotificationItem: React.FC = ({ data }) => {
if (caseType === CaseAllocationType.COLLECTION_CASE) {
const notificationAction = NotificationTemplateActionMap[templateName];
if (notificationAction) {
- handleNotificationNavigation(notificationAction, collectionCaseId);
+ handleNotificationNavigation({
+ caseId: collectionCaseId
+ }, notificationAction as string);
return;
}
navigateToScreen(PageRouteEnum.CASE_DETAIL_STACK, {
diff --git a/src/screens/notifications/NotificationTemplate.tsx b/src/screens/notifications/NotificationTemplate.tsx
index 51f5e5a6..4b34abcd 100644
--- a/src/screens/notifications/NotificationTemplate.tsx
+++ b/src/screens/notifications/NotificationTemplate.tsx
@@ -5,6 +5,10 @@ import { INotification } from './NotificationItem';
import { NotificationTypes } from './constants';
import { formatAmount } from '../../../RN-UI-LIB/src/utlis/amount';
import Heading from '../../../RN-UI-LIB/src/components/Heading';
+import dayjs from "dayjs";
+import customParseFormat from "dayjs/plugin/customParseFormat";
+import {BUSINESS_DATE_FORMAT, CUSTOM_ISO_DATE_FORMAT} from "@rn-ui-lib/utils/dates";
+dayjs.extend(customParseFormat);
interface INotificationTemplateProps {
data: INotification;
@@ -29,6 +33,7 @@ const NotificationTemplate: React.FC = ({ data }) =>
caseCount,
cashCommitted,
visitsCommitted,
+ phoneNumber
} = params || {};
switch (templateName) {
@@ -230,6 +235,31 @@ const NotificationTemplate: React.FC = ({ data }) =>
);
+ case NotificationTypes.BOT_PROMISED_TO_PAY_NOTIFICATION:
+ return (
+
+ has promised to pay on
+ {dayjs(promisedDate, CUSTOM_ISO_DATE_FORMAT).format(BUSINESS_DATE_FORMAT)}
+ on reminder chat
+
+ );
+ case NotificationTypes.BOT_REMINDER_CALLBACK_NOTIFICATION:
+ return (
+
+ has requested a callback on
+ {phoneNumber}
+ on reminder chat
+
+ );
+ case NotificationTypes.BOT_REQUESTED_VISIT_NOTIFICATION:
+ return (
+
+ has requested a field visit on
+ {revisitDate? `${dayjs(revisitDate, CUSTOM_ISO_DATE_FORMAT).format(BUSINESS_DATE_FORMAT)} `: null}
+ {revisitDate? on : null}
+ reminder chat
+
+ );
default:
return New notification ;
}
diff --git a/src/screens/notifications/constants.tsx b/src/screens/notifications/constants.tsx
index b82b97c0..3cd840c3 100644
--- a/src/screens/notifications/constants.tsx
+++ b/src/screens/notifications/constants.tsx
@@ -9,6 +9,9 @@ import PaymentSuccessIcon from '../../../RN-UI-LIB/src/Icons/PaymentSuccessIcon'
import PromiseToPayIcon from '../../../RN-UI-LIB/src/Icons/PromiseToPayIcon';
import NotificationVisitPlanIcon from '../../assets/icons/NotificationVisitPlanIcon';
import CSAIncomingRequestIcon from '@assets/icons/CSAIncomingRequestIcon';
+import AddressIcon from "@assets/icons/AddressIcon";
+import CallIcon from "@rn-ui-lib/icons/CallIcon";
+import CallbackNotificationIcon from "@assets/icons/CallbackNotificationIcon";
export enum NotificationTypes {
PAYMENT_MADE_TEMPLATE = 'PAYMENT_MADE_TEMPLATE',
@@ -36,6 +39,9 @@ export enum NotificationTypes {
AGENT_DAILY_COMMITMENT = 'AGENT_DAILY_COMMITMENT',
SUPPORT_REQUEST_RECEIVED = 'SUPPORT_REQUEST_RECEIVED',
SUPPORT_REQUEST_RESOLVED = 'SUPPORT_REQUEST_RESOLVED',
+ BOT_REMINDER_CALLBACK_NOTIFICATION = 'REQUESTED_CALLBACK_GEN_AI_BOT_FIELD_SCHEDULED_NOTIFICATION_TEMPLATE',
+ BOT_PROMISED_TO_PAY_NOTIFICATION = 'PROMISED_TO_PAY_GEN_AI_BOT_FIELD_SCHEDULED_NOTIFICATION_TEMPLATE',
+ BOT_REQUESTED_VISIT_NOTIFICATION = 'REVISIT_GEN_AI_BOT_SCHEDULED_NOTIFICATION_TEMPLATE',
}
export const NotificationIconsMap = {
@@ -64,6 +70,9 @@ export const NotificationIconsMap = {
[NotificationTypes.AGENT_DAILY_COMMITMENT]: ,
[NotificationTypes.SUPPORT_REQUEST_RECEIVED]: ,
[NotificationTypes.SUPPORT_REQUEST_RESOLVED]: ,
+ [NotificationTypes.BOT_PROMISED_TO_PAY_NOTIFICATION]: ,
+ [NotificationTypes.BOT_REMINDER_CALLBACK_NOTIFICATION]: ,
+ [NotificationTypes.BOT_REQUESTED_VISIT_NOTIFICATION]: ,
};
export enum WidgetStatus {
diff --git a/src/services/casePayload.transformer.ts b/src/services/casePayload.transformer.ts
index 16fa28d0..3cbbfb2d 100644
--- a/src/services/casePayload.transformer.ts
+++ b/src/services/casePayload.transformer.ts
@@ -58,7 +58,8 @@ export const extractQuestionContext = async (answer: Answer): Promise {
const { DEVICE_ID: deviceId, AGENT_ID: agentId, IS_IMPERSONATED } = GLOBAL;
- if (IS_IMPERSONATED) {
+ if (IS_IMPERSONATED || __DEV__) {
+ // Disabled clickstream tracking for impersonated users and in dev mode
return;
}
const { name, description } = eventDesc;
diff --git a/src/services/foregroundServices/foreground.service.ts b/src/services/foregroundServices/foreground.service.ts
index e128a652..7d7b5d7d 100644
--- a/src/services/foregroundServices/foreground.service.ts
+++ b/src/services/foregroundServices/foreground.service.ts
@@ -10,7 +10,7 @@ export interface IForegroundTask {
onLoop?: boolean | undefined;
taskId: string | undefined;
onSuccess?: () => void | undefined;
- onError?: (e: any) => void | undefined;
+ onError?: () => void | undefined;
}
const FOREGROUND_SERVICE_ID = 1244;
@@ -89,6 +89,7 @@ class CosmosForegroundService {
public static clearTasks() {
CosmosForegroundService.fgsTasks = [];
+ ForegroundService.remove_all_tasks();
}
public static async stopAll() {
@@ -107,6 +108,15 @@ class CosmosForegroundService {
logError(e, `Error stopping task: ${taskId}`);
}
}
+
+ public static async updateTask(updatedTask: IForegroundTask) {
+ const { task, ...rest } = updatedTask;
+ try {
+ await ForegroundService.update_task(task, rest);
+ } catch (e: any) {
+ logError(e, `Error updating task: ${rest.taskId}`);
+ }
+ }
}
export default CosmosForegroundService;
diff --git a/src/services/litmusExperiments.service.ts b/src/services/litmusExperiments.service.ts
new file mode 100644
index 00000000..b21a53a0
--- /dev/null
+++ b/src/services/litmusExperiments.service.ts
@@ -0,0 +1,33 @@
+import { LITMUS_URL } from '@common/Constants';
+import { GenericObject } from '@common/GenericTypes';
+import axiosInstance from '@components/utlis/apiHelper';
+import { logError } from '@components/utlis/errorUtils';
+
+export enum LitmusExperimentName {
+ COSMOS_TRACKING_COMPONENT_V2 = 'cosmos_tracking_component_v2',
+}
+
+export const LitmusExperimentNameMap = {
+ [LitmusExperimentName.COSMOS_TRACKING_COMPONENT_V2]: 'isTrackingComponentV2Enabled',
+};
+
+const getLitmusExperimentResult = async (
+ experimentName: string,
+ stickynessConfig: GenericObject
+): Promise => {
+ const litmusApi = `${LITMUS_URL}?name=${experimentName}`;
+ try {
+ const response = await axiosInstance.get(litmusApi, {
+ headers: {
+ 'X-Target': 'LITMUS',
+ ...stickynessConfig,
+ },
+ });
+ return response.data?.result || false;
+ } catch (error) {
+ logError(error as Error, 'litmus experiment error');
+ return false;
+ }
+};
+
+export default getLitmusExperimentResult;
diff --git a/src/store/store.ts b/src/store/store.ts
index baa34d38..74c5e218 100644
--- a/src/store/store.ts
+++ b/src/store/store.ts
@@ -38,6 +38,7 @@ import agentPerformanceSlice from '../reducer/agentPerformanceSlice';
import telephoneNumbersSlice from '../reducer/telephoneNumbersSlice';
import { getStorageEngine } from '../PersistStorageEngine';
import cosmosSupportSlice from '@reducers/cosmosSupportSlice';
+import litmusExperimentSlice from '@reducers/litmusExperimentSlice';
const rootReducer = combineReducers({
case: caseReducer,
@@ -63,7 +64,8 @@ const rootReducer = combineReducers({
feedbackFilters: feedbackFiltersSlice,
agentPerformance: agentPerformanceSlice,
telephoneNumbers: telephoneNumbersSlice,
- cosmosSupport: cosmosSupportSlice
+ cosmosSupport: cosmosSupportSlice,
+ litmusExperiment: litmusExperimentSlice,
});
const persistConfig = {
@@ -86,6 +88,7 @@ const persistConfig = {
'profile',
'foregroundService',
'feedbackFilters',
+ 'litmusExperiment',
],
blackList: ['case', 'filters', 'reportees', 'agentPerformance'],
};
@@ -105,3 +108,4 @@ export default store;
export type RootState = ReturnType;
export type AppDispatch = typeof store.dispatch;
+export type AppGetState = typeof store.getState;
diff --git a/src/types/feedback.types.ts b/src/types/feedback.types.ts
index ffa858a0..4079b54d 100644
--- a/src/types/feedback.types.ts
+++ b/src/types/feedback.types.ts
@@ -10,6 +10,7 @@ export enum AnswerType {
AMOUNT = 'AMOUNT',
OPTIONS = 'OPTIONS',
REMINDER = 'REMINDER',
+ PHONE_NUMBER = 'PHONE_NUMBER',
}
export enum OPTION_TAG {
@@ -44,6 +45,7 @@ export enum FEEDBACK_TYPE {
INHOUSE_FIELD_VISIT = 'INHOUSE_FIELD_VISIT',
CALL_BRIDGE = 'CALL_BRIDGE',
SELF_CALL = 'SELF_CALL',
+ GEN_AI_BOT_FIELD = 'GEN_AI_BOT_FIELD',
}
export interface FIELD_FEEDBACK_METADATA {
diff --git a/src/types/template.types.ts b/src/types/template.types.ts
index c177687b..1982419a 100644
--- a/src/types/template.types.ts
+++ b/src/types/template.types.ts
@@ -113,6 +113,7 @@ export interface IVisitedWidgetContext {
};
};
};
+ updatedAt?: number;
}
export enum CommonCaseWidgetId {