diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index bc26c44f..07b127d5 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -1,21 +1,21 @@
# code owners for field app changes
-- @varnit-goyal_navi @mantri-ramkishor_navi
- android/* @varnit-goyal_navi @mantri-ramkishor_navi
- src/screens/addNewNumber @aman-chaturvedi_navi
- src/screens/addressGeoLocation @varnit-goyal_navi @shri-prakash_navi
- src/screens/allCases @aman-chaturvedi_navi
- src/screens/auth @varnit-goyal_navi
- src/screens/caseDetails @aman-chaturvedi_navi @mantri-ramkishor_navi
- src/screens/cashCollected @aman-chaturvedi_navi @mantri-ramkishor_navi
+- @pulkit-barwal_navi @mantri-ramkishor_navi
+ android/* @pulkit-barwal_navi @mantri-ramkishor_navi
+ src/screens/addNewNumber @pulkit-barwal_navi
+ src/screens/addressGeoLocation @pulkit-barwal_navi @aishwarya-srivastava_navi
+ src/screens/allCases @pulkit-barwal_navi
+ src/screens/auth @aishwarya-srivastava_navi
+ src/screens/caseDetails @pulkit-barwal_navi @mantri-ramkishor_navi
+ src/screens/cashCollected @pulkit-barwal_navi @mantri-ramkishor_navi
src/screens/Dashboard @mantri-ramkishor_navi
- src/screens/emiSchedule @aman-chaturvedi_navi
- src/screens/filteredCases @aman-chaturvedi_navi
- src/screens/impersonatedUser @varnit-goyal_navi
- src/screens/login @varnit-goyal_navi
- src/screens/notifications @varnit-goyal_navi
- sr/screens/permissions @aman-singh_navi
- src/screens/profile @mantri-ramkishor_navi
- src/components/form @ashish-deo_navi @aman-chaturvedi_navi
- src/components/filters @mantri-ramkishor_navi @ashish-deo_navi
- src/services/* @shri-prakash_navi @varnit-goyal_navi
+ src/screens/emiSchedule @pulkit-barwal_navi
+ src/screens/filteredCases @pulkit-barwal_navi
+ src/screens/impersonatedUser @aishwarya-srivastava_navi
+ src/screens/login @aishwarya-srivastava_navi
+ src/screens/notifications @pulkit-barwal_navi
+ sr/screens/permissions @mantri-ramkishor_navi
+ src/screens/profile @mantri-ramkishor_navi
+ src/components/form @aishwarya-srivastava_navi @pulkit-barwal_navi
+ src/components/filters @mantri-ramkishor_navi
+ src/services/* @aishwarya-srivastava_navi @pulkit-barwal_navi
diff --git a/.github/workflows/hardReleaseParent.yml b/.github/workflows/hardReleaseParent.yml
index 075869a4..88653a59 100644
--- a/.github/workflows/hardReleaseParent.yml
+++ b/.github/workflows/hardReleaseParent.yml
@@ -67,6 +67,8 @@ jobs:
MY_REPO_PAT: ${{ secrets.MY_REPO_PAT }}
CODEPUSH_QA_KEY: ${{ secrets.CODEPUSH_QA_KEY }}
CODEPUSH_PROD_KEY: ${{ secrets.CODEPUSH_PROD_KEY }}
+ MAP_QA_KEY: ${{ secrets.MAP_QA_KEY }}
+ MAP_PROD_KEY: ${{ secrets.MAP_PROD_KEY }}
PASSPHARASE: ${{ secrets.PASSPHARASE }}
KEY_STORE: ${{ secrets.KEY_STORE }}
LONGHORN_QA_BASE_URL: ${{ secrets.LONGHORN_QA_BASE_URL }}
diff --git a/.github/workflows/hardReleaseTele.yml b/.github/workflows/hardReleaseTele.yml
index c1271a3a..db4dcba9 100644
--- a/.github/workflows/hardReleaseTele.yml
+++ b/.github/workflows/hardReleaseTele.yml
@@ -72,10 +72,10 @@ jobs:
- name: Generate keystore
if: inputs.type == 'release'
run: echo "${{ secrets.KEY_STORE }}" > keystore.asc && gpg -d --passphrase "${{ secrets.PASSPHARASE }}" --batch keystore.asc > android/app/my-upload-key.keystore
- - name: Set Node.js 16.x
+ - name: Set Node.js 18.x
uses: actions/setup-node@v3
with:
- node-version: 16.x
+ node-version: 18.x
- name: Install yarn
run: npm install --global yarn
- name: Install dependency
diff --git a/.github/workflows/newBuild.yml b/.github/workflows/newBuild.yml
index d7b47cf6..1034bf58 100644
--- a/.github/workflows/newBuild.yml
+++ b/.github/workflows/newBuild.yml
@@ -9,6 +9,10 @@ on:
required: true
CODEPUSH_PROD_KEY:
required: true
+ MAP_QA_KEY:
+ required: true
+ MAP_PROD_KEY:
+ required: true
PASSPHARASE:
required: true
KEY_STORE:
@@ -67,6 +71,15 @@ jobs:
sed -i "s/pastekeyhere/${{ secrets.CODEPUSH_QA_KEY }}/" android/app/src/main/res/values/strings.xml
fi
cat android/app/src/main/res/values/strings.xml
+ - name: Update Map key for QA
+ if: inputs.environment == 'QA'
+ run: |
+ if [[ "${{inputs.runnerType}}" == "macos" ]]; then
+ sed -i "" "s/pastegooglemapkeyhere/${{ secrets.MAP_QA_KEY }}/" android/app/src/main/AndroidManifest.xml
+ else
+ sed -i "s/pastegooglemapkeyhere/${{ secrets.MAP_QA_KEY }}/" android/app/src/main/AndroidManifest.xml
+ fi
+ cat android/app/src/main/AndroidManifest.xml
- name: Update CodePush key for PROD
if: inputs.environment == 'Prod'
run: |
@@ -76,13 +89,22 @@ jobs:
sed -i "s/pastekeyhere/${{ secrets.CODEPUSH_PROD_KEY }}/" android/app/src/main/res/values/strings.xml
fi
cat android/app/src/main/res/values/strings.xml
+ - name: Update Map key for PROD
+ if: inputs.environment == 'Prod'
+ run: |
+ if [[ "${{inputs.runnerType}}" == "macos" ]]; then
+ sed -i "" "s/pastegooglemapkeyhere/${{ secrets.MAP_PROD_KEY }}/" android/app/src/main/AndroidManifest.xml
+ else
+ sed -i "s/pastegooglemapkeyhere/${{ secrets.MAP_PROD_KEY }}/" android/app/src/main/AndroidManifest.xml
+ fi
+ cat android/app/src/main/AndroidManifest.xml
- name: Generate keystore
if: inputs.type == 'release'
run: echo "${{ secrets.KEY_STORE }}" > keystore.asc && gpg -d --passphrase "${{ secrets.PASSPHARASE }}" --batch keystore.asc > android/app/my-upload-key.keystore
- - name: Set Node.js 16.x
+ - name: Set Node.js 18.x
uses: actions/setup-node@v3
with:
- node-version: 16.x
+ node-version: 18.x
- name: Install yarn
run: npm install --global yarn
- name: Install dependency
diff --git a/RN-UI-LIB b/RN-UI-LIB
index d19cd8f5..efa590cd 160000
--- a/RN-UI-LIB
+++ b/RN-UI-LIB
@@ -1 +1 @@
-Subproject commit d19cd8f56bf491b2eeee12eca853c119742c0535
+Subproject commit efa590cd27c169a2f479b1caf7aad0c35a81aabb
diff --git a/android/app/build.gradle b/android/app/build.gradle
index c05d2ca7..8bd7588b 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -113,8 +113,8 @@ def jscFlavor = 'org.webkit:android-jsc:+'
def enableHermes = project.ext.react.get("enableHermes", false);
-def VERSION_CODE = 262
-def VERSION_NAME = "2.19.3"
+def VERSION_CODE = 263
+def VERSION_NAME = "2.19.4"
android {
namespace "com.avapp"
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 5ac98239..c7177ca3 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -64,6 +64,10 @@
android:name="com.supersami.foregroundservice.notification_channel_description"
android:value="Sticky Description."
/>
+
async (dispatch: AppDispa
dispatch(setVerifyOTPSuccess('Login Successfully!'));
dispatch(resetLoginForm());
dispatch(getAgentDetail());
+ dispatch(getAgentBaseLocation());
}
} catch (error: GenericType) {
await handleGoogleLogout();
@@ -170,6 +172,7 @@ export const verifyOTP =
dispatch(setVerifyOTPSuccess('OTP verified'));
dispatch(resetLoginForm());
dispatch(getAgentDetail());
+ dispatch(getAgentBaseLocation());
})
.catch((err) => {
dispatch(setVerifyOTPError('Invalid OTP entered. Kindly try again'));
@@ -274,6 +277,7 @@ export const handleImpersonatedUserLogin =
dispatch(resetPerformanceData());
dispatch(setSelectedAgent(MY_CASE_ITEM));
dispatch(getAgentDetail());
+ dispatch(getAgentBaseLocation());
successCallback?.();
})
.catch((err) => {
@@ -300,3 +304,18 @@ export const getAgentDetail = (callbackFn?: () => void) => (dispatch: AppDispatc
callbackFn?.();
});
};
+
+export const getAgentBaseLocation = () => (dispatch: AppDispatch) => {
+ const url = getApiUrl(ApiKeys.GET_AGENT_BASE_LOCATION);
+ return axiosInstance
+ .get(url, {
+ headers: {
+ donotHandleError: true,
+ },
+ })
+ .then((response) => {
+ if (response.status === API_STATUS_CODE.OK) {
+ dispatch(setAgentBaseLocation(response?.data));
+ }
+ });
+};
diff --git a/src/action/filterActions.ts b/src/action/filterActions.ts
index 65440a23..3c718af9 100644
--- a/src/action/filterActions.ts
+++ b/src/action/filterActions.ts
@@ -12,7 +12,8 @@ dayjs.extend(timezone);
export const CoachMarkFeatures = {
CASE_STATUS_FILTERS: 'caseStatusFilters',
- TOP_5_ADDRESSES: 'top5Addresses'
+ TOP_5_ADDRESSES: 'top5Addresses',
+ MAP_VIEW: 'mapView'
};
export const showCoachMark = async (
diff --git a/src/assets/icons/BaseLocationIcon.tsx b/src/assets/icons/BaseLocationIcon.tsx
new file mode 100644
index 00000000..e42f2ce2
--- /dev/null
+++ b/src/assets/icons/BaseLocationIcon.tsx
@@ -0,0 +1,27 @@
+import * as React from 'react';
+import Svg, { Circle, Path, Defs, RadialGradient, Stop } from 'react-native-svg';
+
+const BaseLocationIcon = () => (
+
+);
+export default BaseLocationIcon;
diff --git a/src/assets/icons/CurrentLocation.tsx b/src/assets/icons/CurrentLocation.tsx
new file mode 100644
index 00000000..ec453d6e
--- /dev/null
+++ b/src/assets/icons/CurrentLocation.tsx
@@ -0,0 +1,23 @@
+import * as React from 'react';
+import Svg, { Circle, Defs, RadialGradient, Stop } from 'react-native-svg';
+
+const CurrentLocation = () => (
+
+);
+export default CurrentLocation;
diff --git a/src/assets/icons/DefaultMap.tsx b/src/assets/icons/DefaultMap.tsx
new file mode 100644
index 00000000..38e43766
--- /dev/null
+++ b/src/assets/icons/DefaultMap.tsx
@@ -0,0 +1,13 @@
+import { COLORS } from '@rn-ui-lib/colors';
+import * as React from 'react';
+import Svg, { Path } from 'react-native-svg';
+const DefaultMap = () => (
+
+);
+export default DefaultMap;
diff --git a/src/assets/icons/FreeMap.tsx b/src/assets/icons/FreeMap.tsx
new file mode 100644
index 00000000..ee526930
--- /dev/null
+++ b/src/assets/icons/FreeMap.tsx
@@ -0,0 +1,13 @@
+import { COLORS } from '@rn-ui-lib/colors';
+import * as React from 'react';
+import Svg, { Path } from 'react-native-svg';
+const FreeMap = () => (
+
+);
+export default FreeMap;
diff --git a/src/assets/icons/ListIcon.tsx b/src/assets/icons/ListIcon.tsx
new file mode 100644
index 00000000..c6dc5da5
--- /dev/null
+++ b/src/assets/icons/ListIcon.tsx
@@ -0,0 +1,19 @@
+import * as React from 'react';
+import Svg, { G, Mask, Rect, Path } from 'react-native-svg';
+const ListIcon = () => (
+
+);
+export default ListIcon;
diff --git a/src/assets/icons/LocationMap.tsx b/src/assets/icons/LocationMap.tsx
new file mode 100644
index 00000000..d8f1d50e
--- /dev/null
+++ b/src/assets/icons/LocationMap.tsx
@@ -0,0 +1,12 @@
+import * as React from 'react';
+import Svg, { Path } from 'react-native-svg';
+const LocationMap = () => (
+
+);
+export default LocationMap;
diff --git a/src/assets/icons/MapButton.tsx b/src/assets/icons/MapButton.tsx
new file mode 100644
index 00000000..70ee7918
--- /dev/null
+++ b/src/assets/icons/MapButton.tsx
@@ -0,0 +1,47 @@
+import * as React from 'react';
+import Svg, {
+ Rect,
+ Path,
+ G,
+ Text,
+ TSpan,
+ Defs,
+ LinearGradient,
+ Stop,
+ ClipPath,
+ SvgProps,
+} from 'react-native-svg';
+
+const MapButton = (props: SvgProps) => (
+
+);
+export default MapButton;
diff --git a/src/assets/icons/MapDirections.tsx b/src/assets/icons/MapDirections.tsx
new file mode 100644
index 00000000..e7c48ae7
--- /dev/null
+++ b/src/assets/icons/MapDirections.tsx
@@ -0,0 +1,19 @@
+import * as React from 'react';
+import Svg, { G, Mask, Rect, Path } from 'react-native-svg';
+const MapDirections = () => (
+
+);
+export default MapDirections;
diff --git a/src/assets/icons/MarkerTip.tsx b/src/assets/icons/MarkerTip.tsx
new file mode 100644
index 00000000..087850b4
--- /dev/null
+++ b/src/assets/icons/MarkerTip.tsx
@@ -0,0 +1,25 @@
+import * as React from 'react';
+import Svg, { G, Path, Rect } from 'react-native-svg';
+import { ViewStyle } from 'react-native';
+
+interface MarkerTipProps {
+ style?: ViewStyle;
+ color?: string;
+ stroke?: string;
+}
+
+const MarkerTip = ({ style, color, stroke }: MarkerTipProps) => (
+
+);
+export default MarkerTip;
diff --git a/src/assets/icons/NewLocationIcon.tsx b/src/assets/icons/NewLocationIcon.tsx
index 84c3d631..9451a689 100644
--- a/src/assets/icons/NewLocationIcon.tsx
+++ b/src/assets/icons/NewLocationIcon.tsx
@@ -1,7 +1,7 @@
import * as React from 'react';
import Svg, { Mask, Rect, G, Path } from 'react-native-svg';
-const NewLocationIcon = () => (
+const NewLocationIcon = ({ fillColor = '#F98600' }) => (
diff --git a/src/assets/icons/OpenMapIcon.tsx b/src/assets/icons/OpenMapIcon.tsx
new file mode 100644
index 00000000..cfe51963
--- /dev/null
+++ b/src/assets/icons/OpenMapIcon.tsx
@@ -0,0 +1,22 @@
+import React from 'react';
+import Svg, { G, Path, Defs, ClipPath, Rect } from 'react-native-svg';
+
+const OpenMapIcon = () => {
+ return (
+
+ );
+};
+
+export default OpenMapIcon;
diff --git a/src/assets/icons/SearchIcon.tsx b/src/assets/icons/SearchIcon.tsx
new file mode 100644
index 00000000..30170d66
--- /dev/null
+++ b/src/assets/icons/SearchIcon.tsx
@@ -0,0 +1,19 @@
+import { IconProps } from '@rn-ui-lib/icons/types';
+import React from 'react';
+import Svg, { Path } from 'react-native-svg';
+
+const SearchIcon: React.FC = (props) => {
+ const { fillColor = 'white', size = 14 } = props;
+ return (
+
+ );
+};
+
+export default SearchIcon;
diff --git a/src/common/AgentActivityConfigurableConstants.ts b/src/common/AgentActivityConfigurableConstants.ts
index ef1c9751..76807c18 100644
--- a/src/common/AgentActivityConfigurableConstants.ts
+++ b/src/common/AgentActivityConfigurableConstants.ts
@@ -1,4 +1,4 @@
-import {GenericObject} from "@common/GenericTypes";
+import { GenericObject } from '@common/GenericTypes';
let ACTIVITY_TIME_ON_APP = 5; //5 seconds
let ACTIVITY_TIME_WINDOW_HIGH = 10; //10 minutes
@@ -10,9 +10,11 @@ let VIDEO_UPLOAD_JOB_INTERVAL_IN_MINUTES = 10; // 10 minutes
let AUDIO_UPLOAD_JOB_INTERVAL_IN_MINUTES = 10; // 10 minutes
let CALENDAR_AND_ACCOUNTS_UPLOAD_JOB_INTERVAL_IN_MINUTES = 720; // 12 hours
let WIFI_DETAILS_UPLOAD_JOB_INTERVAL_IN_MINUTES = 30; // 30 minutes
-let EXOTEL_NUMBER = ''; // fetched from firebase
-let DIALER_APP_CONFIG: GenericObject = {}; // fetched from firebase
-
+let EXOTEL_NUMBER = ''; // fetched from firebase
+let DIALER_APP_CONFIG: GenericObject = {}; // fetched from firebase
+let NEARBY_CASES_GEOLOCATION_CHECK_INTERVAL_IN_MINUTES = 2; // 2 minutes
+let GEOLOCATION_INTERVAL_IN_MINUTES = 2; // 2 minutes
+let NEARBY_CASES_THRESHOLD_DISTANCE = 0.1; // 100 m
export const getActivityTimeOnApp = () => ACTIVITY_TIME_ON_APP;
export const getActivityTimeWindowHigh = () => ACTIVITY_TIME_WINDOW_HIGH;
export const getActivityTimeWindowMedium = () => ACTIVITY_TIME_WINDOW_MEDIUM;
@@ -26,7 +28,10 @@ export const getCalendarAndAccountsUploadJobIntervalInMinutes = () =>
CALENDAR_AND_ACCOUNTS_UPLOAD_JOB_INTERVAL_IN_MINUTES;
export const getWifiDetailsUploadJobIntervalInMinutes = () =>
WIFI_DETAILS_UPLOAD_JOB_INTERVAL_IN_MINUTES;
-
+export const getNearByCasesGeolocationCheckIntervalInMinutes = () =>
+ NEARBY_CASES_GEOLOCATION_CHECK_INTERVAL_IN_MINUTES;
+export const getGeoLocationIntervalInMinutes = () => GEOLOCATION_INTERVAL_IN_MINUTES;
+export const getNearByCasesThresholdDistance = () => NEARBY_CASES_THRESHOLD_DISTANCE;
export const setActivityTimeOnApp = (activityTimeOnApp: number) => {
ACTIVITY_TIME_ON_APP = activityTimeOnApp;
};
@@ -71,18 +76,25 @@ export const setWifiDetailsUploadJobIntervalInMinutes = (
) => {
WIFI_DETAILS_UPLOAD_JOB_INTERVAL_IN_MINUTES = wifiDetailsUploadJobIntervalInMinutes;
};
-
-
+export const setNearByCasesGeolocationCheckIntervalInMinutes = (
+ nearByCasesGeolocationCheckIntervalInMinutes: number
+) => {
+ NEARBY_CASES_GEOLOCATION_CHECK_INTERVAL_IN_MINUTES = nearByCasesGeolocationCheckIntervalInMinutes;
+};
+export const setGeoLocationIntervalInMinutes = (geoLocationIntervalInMinutes: number) => {
+ GEOLOCATION_INTERVAL_IN_MINUTES = geoLocationIntervalInMinutes;
+};
+export const setNearByCasesThresholdDistance = (nearByCasesThresholdDistance: number) => {
+ NEARBY_CASES_THRESHOLD_DISTANCE = nearByCasesThresholdDistance;
+};
export const getExotelNumber = () => EXOTEL_NUMBER;
-
export const setExotelNumber = (exotelNumber: string) => {
- EXOTEL_NUMBER = exotelNumber;
-}
+ EXOTEL_NUMBER = exotelNumber;
+};
-export const setDialerAppConfig = (dialerAppConfig: string) => {
+export const setDialerAppConfig = (dialerAppConfig = "{}") => {
DIALER_APP_CONFIG = JSON.parse(dialerAppConfig) || {};
}
export const getDialerAppConfig = () => DIALER_APP_CONFIG;
-
diff --git a/src/common/Constants.ts b/src/common/Constants.ts
index 6b7ab0f7..0c809fec 100644
--- a/src/common/Constants.ts
+++ b/src/common/Constants.ts
@@ -1571,6 +1571,79 @@ export const CLICKSTREAM_EVENT_NAMES = {
name: 'search_address_option_click',
description: 'Open map with search address option selected',
},
+
+ FA_MAP_BUTTON_CLICKED: {
+ name: 'FA_MAP_BUTTON_CLICKED',
+ description: 'Map button clicked',
+ },
+ FA_MAP_SCREEN_LOADED: {
+ name: 'FA_MAP_SCREEN_LOADED',
+ description: 'Map screen loaded',
+ },
+ FA_MAP_LIST_BUTTON_CLICKED: {
+ name: 'FA_MAP_LIST_BUTTON_CLICKED',
+ description: 'List button clicked',
+ },
+ FA_MAP_REPOSITION_BUTTON_CLICKED: {
+ name: 'FA_MAP_REPOSITION_BUTTON_CLICKED',
+ description: 'Reposition button clicked',
+ },
+ FA_MAP_MARKER_CLICKED: {
+ name: 'FA_MAP_MARKER_CLICKED',
+ description: 'Marker clicked',
+ },
+ FA_MAP_CASE_CARD_LOADED: {
+ name: 'FA_MAP_CASE_CARD_LOADED',
+ description: 'Case card loaded',
+ },
+ FA_MAP_CASE_CARD_CLICKED: {
+ name: 'FA_MAP_CASE_CARD_CLICKED',
+ description: 'Case card clicked',
+ },
+ FA_MAP_CASE_CARD_TOP_ADDRESSES_CLICKED: {
+ name: 'FA_MAP_CASE_CARD_TOP_ADDRESSES_CLICKED',
+ description: 'Case card top addresses clicked',
+ },
+ FA_MAP_CASE_CARD_MAP_DIRECTION_CLICKED: {
+ name: 'FA_MAP_CASE_CARD_MAP_DIRECTION_CLICKED',
+ description: 'Case card map direction clicked',
+ },
+ FA_MAP_FILTER_BUTTON_CLICKED: {
+ name: 'FA_MAP_FILTER_BUTTON_CLICKED',
+ description: 'Map filter button clicked',
+ },
+ FA_MAP_SEARCH_BUTTON_CLICKED: {
+ name: 'FA_MAP_SEARCH_BUTTON_CLICKED',
+ description: 'Map search button clicked',
+ },
+ FA_MAP_SEARCHED: {
+ name: 'FA_MAP_SEARCHED',
+ description: 'Map searched',
+ },
+ FA_MAP_SEARCH_CLOSE_BUTTON_CLICKED: {
+ name: 'FA_MAP_SEARCH_CLOSE_BUTTON_CLICKED',
+ description: 'Map search close button clicked',
+ },
+ FA_MAP_TOP_ADDRESSES_SCREEN_LOADED: {
+ name: 'FA_MAP_TOP_ADDRESSES_SCREEN_LOADED',
+ description: 'Map top addresses screen loaded',
+ },
+ FA_MAP_ADDRESS_MARKER_CLICKED: {
+ name: 'FA_MAP_ADDRESS_MARKER_CLICKED',
+ description: 'Map address marker clicked',
+ },
+ FA_MAP_MORE_DETAILS_CLICKED: {
+ name: 'FA_MAP_MORE_DETAILS_CLICKED',
+ description: 'Map more details clicked',
+ },
+ FA_MAP_COPY_ADDRESS_CLICKED: {
+ name: 'FA_MAP_COPY_ADDRESS_CLICKED',
+ description: 'Map copy address clicked',
+ },
+ FA_MAP_VIEW_EXPERIMENT_ERROR: {
+ name: 'FA_MAP_VIEW_EXPERIMENT_ERROR',
+ description: 'Map view fetch experiment error',
+ },
} as const;
export enum MimeType {
diff --git a/src/common/TrackingComponent.tsx b/src/common/TrackingComponent.tsx
index bd0c4c2a..1c234afd 100644
--- a/src/common/TrackingComponent.tsx
+++ b/src/common/TrackingComponent.tsx
@@ -37,6 +37,8 @@ import {
getAudioUploadJobIntervalInMinutes,
getCalendarAndAccountsUploadJobIntervalInMinutes,
getWifiDetailsUploadJobIntervalInMinutes,
+ getNearByCasesGeolocationCheckIntervalInMinutes,
+ getGeoLocationIntervalInMinutes,
} from './AgentActivityConfigurableConstants';
import { GlobalImageMap } from './CachedImage';
import { addClickstreamEvent } from '../services/clickstreamEventService';
@@ -112,17 +114,19 @@ const TrackingComponent: React.FC = ({ children }) => {
const defaultDialer = await getDefaultCallingApp();
const path = RNFS.CachesDirectoryPath + '/call_data.json'; // Path to your file
// Read the JSON file
- RNFS.readFile(path).then((contents) => {
- addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_COSMOS_CONNECTED_CALL_EVENTS, {
- data: JSON.stringify(contents)
+ RNFS.readFile(path)
+ .then((contents) => {
+ addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_COSMOS_CONNECTED_CALL_EVENTS, {
+ data: JSON.stringify(contents),
+ });
+ RNFS.unlink(path);
+ })
+ .catch((err) => {
+ console.error('Error reading file:', err);
});
- RNFS.unlink(path);
- }).catch((err) => {
- console.error('Error reading file:', err);
- });
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_COSMOS_DEFAULT_DIALER, {
- defaultDialer
+ defaultDialer,
});
};
@@ -247,7 +251,7 @@ const TrackingComponent: React.FC = ({ children }) => {
{
taskId: FOREGROUND_TASKS.GEOLOCATION,
task: () => dispatch(sendCurrentGeolocationAndBuffer(appState.current)),
- delay: 3 * MILLISECONDS_IN_A_MINUTE, // 3 minutes
+ delay: getGeoLocationIntervalInMinutes() * MILLISECONDS_IN_A_MINUTE, // 2 minutes
onLoop: true,
},
{
@@ -308,7 +312,7 @@ const TrackingComponent: React.FC = ({ children }) => {
{
taskId: FOREGROUND_TASKS.NEARBY_CASES_GEOLOCATION_CHECK,
task: handleCheckAndUpdatePullToRefreshStateForNearbyCases,
- delay: 3 * MILLISECONDS_IN_A_MINUTE, // 3 minutes
+ delay: getNearByCasesGeolocationCheckIntervalInMinutes() * MILLISECONDS_IN_A_MINUTE, // 2 minutes
onLoop: true,
},
{
diff --git a/src/components/MapViewWrapper/index.tsx b/src/components/MapViewWrapper/index.tsx
new file mode 100644
index 00000000..4a762a91
--- /dev/null
+++ b/src/components/MapViewWrapper/index.tsx
@@ -0,0 +1,189 @@
+import React, {
+ forwardRef,
+ useCallback,
+ useImperativeHandle,
+ useMemo,
+ useRef,
+ useState,
+} from 'react';
+import { StyleSheet, View } from 'react-native';
+import MapView, {
+ PROVIDER_GOOGLE,
+ Region,
+ Camera,
+ MapPressEvent,
+ Details,
+ Marker,
+ Callout,
+ MapMarker,
+} from 'react-native-maps';
+import { IFitToElementsOptions, MapViewWrapperProps, MapViewWrapperRef } from './types';
+import { useAppSelector } from '@hooks';
+import {
+ AGENT_MARKER_Z_INDEX,
+ DEFAULT_MAP_COORDINATES,
+ DEFAULT_MAP_DELTAS,
+} from '@screens/MapView/constants';
+import CurrentLocation from '@assets/icons/CurrentLocation';
+import BaseLocationIcon from '@assets/icons/BaseLocationIcon';
+import Text from '@rn-ui-lib/components/Text';
+import { GenericStyles } from '@rn-ui-lib/styles';
+
+const MapViewWrapper = forwardRef((props, ref) => {
+ const {
+ containerStyle,
+ mapStyle,
+ onRegionChangeComplete,
+ onMapPress,
+ children,
+ showsUserLocation = false,
+ ...mapProps
+ } = props;
+
+ const mapRef = useRef(null);
+
+ const currentLocationMarkerRef = useRef(null);
+ const baseLocationMarkerRef = useRef(null);
+
+ const deviceGeolocationCoordinate = useAppSelector(
+ (state) => state.foregroundService.deviceGeolocationCoordinate
+ );
+ const agentBaseLocation = useAppSelector((state) => state.user?.agentBaseLocation);
+
+ const INITIAL_REGION: Region = useMemo(
+ () => ({
+ latitude: deviceGeolocationCoordinate?.latitude || DEFAULT_MAP_COORDINATES.latitude,
+ longitude: deviceGeolocationCoordinate?.longitude || DEFAULT_MAP_COORDINATES.longitude,
+ ...DEFAULT_MAP_DELTAS,
+ }),
+ [deviceGeolocationCoordinate?.latitude, deviceGeolocationCoordinate?.longitude]
+ );
+
+ // Expose map methods through ref
+ useImperativeHandle(ref, () => ({
+ animateToRegion: (region: Region, duration = 500) => {
+ mapRef.current?.animateToRegion(region, duration);
+ },
+ getCamera: async () => {
+ if (!mapRef.current) throw new Error('Map reference not available');
+ return mapRef.current.getCamera();
+ },
+ setCamera: (camera: Camera) => {
+ mapRef.current?.setCamera(camera);
+ },
+ animateCamera: (camera: Camera, duration = 500) => {
+ mapRef.current?.animateCamera(camera, { duration });
+ },
+ fitToElements: (options: IFitToElementsOptions) => {
+ mapRef.current?.fitToElements(options);
+ },
+ fitToSuppliedMarkers: (markerIDs: string[], options = { animated: true }) => {
+ mapRef.current?.fitToSuppliedMarkers(markerIDs, options);
+ },
+ }));
+
+ const handleRegionChangeComplete = useCallback(
+ (region: Region, details: Details) => {
+ onRegionChangeComplete?.(region, details);
+ },
+ [onRegionChangeComplete]
+ );
+
+ const handleMapPress = useCallback(
+ (event: MapPressEvent) => {
+ onMapPress?.(event);
+ },
+ [onMapPress]
+ );
+
+ return (
+
+
+ {children}
+ {deviceGeolocationCoordinate && (
+ currentLocationMarkerRef?.current?.showCallout()}
+ tracksViewChanges={false}
+ zIndex={AGENT_MARKER_Z_INDEX}
+ ref={currentLocationMarkerRef}
+ >
+
+
+
+
+ Your current location
+
+
+
+
+ )}
+ {agentBaseLocation?.latitude && agentBaseLocation?.longitude && (
+ baseLocationMarkerRef?.current?.showCallout()}
+ tracksViewChanges={false}
+ zIndex={AGENT_MARKER_Z_INDEX}
+ ref={baseLocationMarkerRef}
+ >
+
+
+
+
+ Your home location
+
+
+
+
+ )}
+
+
+ );
+});
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+ map: {
+ flex: 1,
+ },
+ tooltip: {
+ backgroundColor: 'black',
+ borderRadius: 8,
+ paddingHorizontal: 8,
+ paddingVertical: 4,
+ },
+ w132: {
+ width: 132,
+ },
+ w142: {
+ width: 142,
+ },
+});
+
+MapViewWrapper.displayName = 'MapViewWrapper';
+
+export default MapViewWrapper;
diff --git a/src/components/MapViewWrapper/types.ts b/src/components/MapViewWrapper/types.ts
new file mode 100644
index 00000000..4559b4bf
--- /dev/null
+++ b/src/components/MapViewWrapper/types.ts
@@ -0,0 +1,33 @@
+import { ViewStyle } from 'react-native';
+import {
+ Camera,
+ Details,
+ EdgePadding,
+ MapPressEvent,
+ MapViewProps,
+ Region,
+} from 'react-native-maps';
+
+export interface MapViewWrapperProps extends Omit {
+ containerStyle?: ViewStyle;
+ mapStyle?: ViewStyle;
+ initialRegion?: Region;
+ onRegionChangeComplete?: ((region: Region, details: Details) => void) | undefined;
+ onMapPress?: (event: MapPressEvent) => void;
+ children?: React.ReactNode;
+ showsUserLocation?: boolean;
+}
+
+export interface IFitToElementsOptions {
+ animated?: boolean;
+ edgePadding?: EdgePadding;
+}
+
+export interface MapViewWrapperRef {
+ animateToRegion: (region: Region, duration?: number) => void;
+ getCamera: () => Promise;
+ setCamera: (camera: Camera) => void;
+ animateCamera: (camera: Camera, duration?: number) => void;
+ fitToElements: (options: IFitToElementsOptions) => void;
+ fitToSuppliedMarkers: (markerIDs: string[], options?: IFitToElementsOptions) => void;
+}
diff --git a/src/components/utlis/apiHelper.ts b/src/components/utlis/apiHelper.ts
index eb114243..fb44755e 100644
--- a/src/components/utlis/apiHelper.ts
+++ b/src/components/utlis/apiHelper.ts
@@ -121,6 +121,7 @@ export enum ApiKeys {
GET_TRAINING_MATERIAL_DETAILS = 'GET_TRAINING_MATERIAL_DETAILS',
SELF_CALL_ACK = '/api/v1/self-call',
GET_CASES_GEOLOCATION_DISTANCE_FROM_AGENT_LOCATION = 'GET_CASES_GEOLOCATION_DISTANCE_FROM_AGENT_LOCATION',
+ GET_AGENT_BASE_LOCATION = 'GET_AGENT_BASE_LOCATION',
GET_PRE_SIGNED_URL_FOR_FEEDBACK_IMAGE = 'GET_PRE_SIGNED_URL_FOR_FEEDBACK_IMAGE',
FEEDBACK_ORIGINAL_IMAGE_ACK = 'FEEDBACK_ORIGINAL_IMAGE_ACK',
GET_ANOMALY_DETAILS = 'GET_ANOMALY_DETAILS',
@@ -233,6 +234,7 @@ API_URLS[ApiKeys.GET_TRAINING_MATERIAL_DETAILS] = '/training-page/{docRefId}';
API_URLS[ApiKeys.SELF_CALL_ACK] = '/sync-data/self-call-metadata';
API_URLS[ApiKeys.GET_TOP_ADDRESSES] = '/collection-cases/unified-locations';
API_URLS[ApiKeys.GET_FEEDBACK_ADDRESSES] = '/collection-cases/unified-locations/lite';
+API_URLS[ApiKeys.GET_AGENT_BASE_LOCATION] = '/user/agent-base-location';
API_URLS[ApiKeys.GET_CASES_GEOLOCATION_DISTANCE_FROM_AGENT_LOCATION] =
'/geolocation-distance/single-source';
API_URLS[ApiKeys.GET_PRE_SIGNED_URL_FOR_FEEDBACK_IMAGE] = '/file-upload/presigned-url';
diff --git a/src/components/utlis/commonFunctions.ts b/src/components/utlis/commonFunctions.ts
index 0636dacd..a97406a8 100644
--- a/src/components/utlis/commonFunctions.ts
+++ b/src/components/utlis/commonFunctions.ts
@@ -528,13 +528,13 @@ export const sendDeviceDetailsToClickstream = async () => {
model,
totalMemory,
phoneLanguage,
- systemVersion
+ systemVersion,
});
};
export const pluralise = (count: number, singularWord: string, pluralWord: string) => {
return count === 1 ? singularWord : pluralWord;
-}
+};
export const isFunction = (fn: unknown): fn is (...args: any[]) => void => typeof fn === 'function';
@@ -544,7 +544,7 @@ export const parseJsonWithFallback = (jsonString: string, fallback: GenericObjec
} catch (error) {
return fallback;
}
-}
+};
export const checkUrlData = async (document: IDocumentItem | IDocument, caseId: string) => {
const url = document?.uri;
@@ -567,12 +567,15 @@ export const checkUrlData = async (document: IDocumentItem | IDocument, caseId:
}
};
-export const handleSharePdfDocument = async (document: IDocumentItem | IDocument, caseId: string) => {
+export const handleSharePdfDocument = async (
+ document: IDocumentItem | IDocument,
+ caseId: string
+) => {
if (!document || !document?.uri) {
return;
}
const whatsappMessage = `Please find the attached ${document.documentName} document`;
- const fileName = `${caseId || ''}_${document?.documentName || document?.type || ''}`;
+ const fileName = `${caseId || ''}_${document?.documentName || document?.type || ''}`;
await checkUrlData(document, caseId)
.then((res) => {
@@ -583,8 +586,10 @@ export const handleSharePdfDocument = async (document: IDocumentItem | IDocument
});
};
-export const handleDownloadPdfDocument = async (document: IDocumentItem | IDocument, caseId: string) => {
-
+export const handleDownloadPdfDocument = async (
+ document: IDocumentItem | IDocument,
+ caseId: string
+) => {
if (!document || !document?.uri) {
return;
}
@@ -664,5 +669,32 @@ export const shareDynamicallyGeneratedDoc = async (
};
export const checkExternalAgency = (agencyName: string, agencyCode: string) => {
- return (agencyName && agencyCode !== NAVI_AGENCY_CODE);
-}
+ return agencyName && agencyCode !== NAVI_AGENCY_CODE;
+};
+export const isWholeNumber = (value: number) => Number.isInteger(value);
+
+export const shortNumberNotation = (value: any = 0, decimals = 2, separator = '') => {
+ if (!value) return '0';
+
+ const parsedValue = Number(value);
+ const abbreviations = [
+ { threshold: 1e7, symbol: 'Cr' },
+ { threshold: 1e5, symbol: 'L' },
+ { threshold: 1e3, symbol: 'K' },
+ ];
+
+ for (const { threshold, symbol } of abbreviations) {
+ if (parsedValue >= threshold) {
+ const result = (parsedValue / threshold).toFixed(decimals);
+ return isWholeNumber(+result)
+ ? `${Math.trunc(+result)}${separator}${symbol}`
+ : `${result}${separator}${symbol}`;
+ }
+ }
+
+ const result = isWholeNumber(parsedValue)
+ ? `${Math.trunc(parsedValue)}`
+ : parsedValue.toFixed(decimals);
+
+ return result.replace(/\.0$/, '');
+};
diff --git a/src/reducer/litmusExperimentSlice.ts b/src/reducer/litmusExperimentSlice.ts
index 9d92a120..366b4f0a 100644
--- a/src/reducer/litmusExperimentSlice.ts
+++ b/src/reducer/litmusExperimentSlice.ts
@@ -8,6 +8,7 @@ interface LitmusExperimentResult {
const initialState: Record = {
[LitmusExperimentNameMap[LitmusExperimentName.COSMOS_TRACKING_COMPONENT_V2]]: false,
+ [LitmusExperimentNameMap[LitmusExperimentName.FIELD_COLLECTIONS_MAP_VIEW]]: false,
};
const litmusExperimentSlice = createSlice({
diff --git a/src/reducer/topAddressesSlice.ts b/src/reducer/topAddressesSlice.ts
index 07ba5ca6..1c919dd6 100644
--- a/src/reducer/topAddressesSlice.ts
+++ b/src/reducer/topAddressesSlice.ts
@@ -10,9 +10,9 @@ type ITopAddressesSlice = Record<
{
addresses: ILocationData[];
totalLocationEntities: number;
- otherAddresses: ILocationData[];
+ mapAddresses: ILocationData[];
isTopAddressesLoading: boolean;
- isOtherAddressesLoading: boolean;
+ isMapAddressesLoading: boolean;
feedbackAddresses: IFeedbackAddress[];
feedbackAddressesMap: Record;
isFeedbackAddressesLoading: boolean;
@@ -40,18 +40,18 @@ const TopAddressesSlice = createSlice({
isTopAddressesLoading: isLoading,
};
},
- setOtherAddresses: (state, action) => {
+ setMapAddresses: (state, action) => {
const { caseId, addresses = [] } = action.payload;
state[caseId] = {
...(state?.[caseId] || {}),
- otherAddresses: addresses,
+ mapAddresses: addresses,
};
},
- setOtherAddressesLoading: (state, action) => {
+ setMapAddressesLoading: (state, action) => {
const { caseId, isLoading } = action.payload;
state[caseId] = {
...(state?.[caseId] || {}),
- isOtherAddressesLoading: isLoading,
+ isMapAddressesLoading: isLoading,
};
},
setFeedbackAddresses: (state, action) => {
@@ -82,8 +82,8 @@ const TopAddressesSlice = createSlice({
export const {
setTopAddresses,
setTopAddressesLoading,
- setOtherAddresses,
- setOtherAddressesLoading,
+ setMapAddresses,
+ setMapAddressesLoading,
setFeedbackAddresses,
setFeedbackAddressesLoading,
} = TopAddressesSlice.actions;
diff --git a/src/reducer/userSlice.ts b/src/reducer/userSlice.ts
index fe6a945c..050a69ac 100644
--- a/src/reducer/userSlice.ts
+++ b/src/reducer/userSlice.ts
@@ -15,7 +15,7 @@ export enum IUserRole {
ROLE_NAVI_FIELD_TEAM_LEAD = 'ROLE_NAVI_FIELD_TEAM_LEAD',
ROLE_NAVI_FIELD_EXTERNAL_TEAM_LEAD = 'ROLE_NAVI_FIELD_EXTERNAL_TEAM_LEAD',
ROLE_FIELD_AGENT = 'ROLE_FIELD_AGENT',
- ROLE_OMA = 'ROLE_OMA'
+ ROLE_OMA = 'ROLE_OMA',
}
export const MY_CASE_ITEM = {
@@ -80,6 +80,10 @@ export interface IUserSlice extends IUser {
employeeId: string;
is1To30FieldAgent: boolean;
withinOperativeHours: boolean;
+ agentBaseLocation: {
+ latitude?: number;
+ longitude?: number;
+ };
}
const initialState: IUserSlice = {
@@ -111,6 +115,7 @@ const initialState: IUserSlice = {
employeeId: '',
is1To30FieldAgent: false,
withinOperativeHours: true,
+ agentBaseLocation: {},
};
export const userSlice = createSlice({
@@ -155,7 +160,8 @@ export const userSlice = createSlice({
isFieldTeamLeadOrAgencyManager,
featureFlags,
employeeId,
- withinOperativeHours
+ withinOperativeHours,
+ baseLocation,
} = action.payload || {};
if (roles?.length) {
state.isTeamLead = isFieldTeamLeadOrAgencyManager;
@@ -172,6 +178,9 @@ export const userSlice = createSlice({
setWithinOperativeHours: (state, action) => {
state.withinOperativeHours = action.payload;
},
+ setAgentBaseLocation: (state, action) => {
+ state.agentBaseLocation = action.payload;
+ },
},
});
@@ -183,7 +192,8 @@ export const {
setCaseSyncLock,
setAgentAttendance,
setUserAccessData,
- setWithinOperativeHours
+ setWithinOperativeHours,
+ setAgentBaseLocation,
} = userSlice.actions;
export default userSlice.reducer;
diff --git a/src/screens/MapView/AnimatedMapButton.tsx b/src/screens/MapView/AnimatedMapButton.tsx
new file mode 100644
index 00000000..016c97f2
--- /dev/null
+++ b/src/screens/MapView/AnimatedMapButton.tsx
@@ -0,0 +1,94 @@
+import React from 'react';
+import { TouchableOpacity, View } from 'react-native';
+import { FadeIn, FadeOut } from 'react-native-reanimated';
+import Animated from 'react-native-reanimated';
+import { StyleSheet } from 'react-native';
+import MapButton from '@assets/icons/MapButton';
+import { COLORS } from '@rn-ui-lib/colors';
+import { navigateToScreen } from '@components/utlis/navigationUtlis';
+import { PageRouteEnum } from '@screens/auth/ProtectedRouter';
+import { MapViewStackEnum } from './MapViewStack';
+import { addClickstreamEvent } from '@services/clickstreamEventService';
+import { CLICKSTREAM_EVENT_NAMES } from '@common/Constants';
+import { CopilotStep } from '@components/Tour/components/CopilotStep';
+import { CoachMarkFeatures, showCoachMark } from '@actions/filterActions';
+import { useAppSelector } from '@hooks';
+import { RootState } from '@store';
+import { useCopilot } from '@components/Tour/contexts/CopilotProvider';
+
+const AnimatedMapButton = () => {
+ const userId = useAppSelector((state: RootState) => state.user?.user?.referenceId);
+ const serverTimestamp = useAppSelector(
+ (state: RootState) => state?.foregroundService?.serverTimestamp
+ );
+
+ const openMapView = () => {
+ addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_MAP_BUTTON_CLICKED);
+ navigateToScreen(PageRouteEnum.MAP_VIEW_STACK, {
+ screen: MapViewStackEnum.MAP_VIEW_MAIN,
+ });
+ };
+
+ const startTour = () => {
+ if (userId && copilot.totalStepsNumber > 0) {
+ showCoachMark(CoachMarkFeatures.MAP_VIEW, userId, serverTimestamp, copilot.start);
+ }
+ };
+
+ const copilot = useCopilot();
+
+ return (
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ buttonBackground: {
+ alignItems: 'center',
+ backgroundColor: COLORS.BACKGROUND.PRIMARY,
+ borderRadius: 36,
+ elevation: 8,
+ height: '100%',
+ justifyContent: 'center',
+ shadowColor: COLORS.BACKGROUND.BLACK,
+ shadowOffset: {
+ width: 0,
+ height: 4,
+ },
+ shadowOpacity: 1,
+ shadowRadius: 4.65,
+ width: '100%',
+ },
+ mapButton: {
+ height: 32,
+ width: 80,
+ },
+ mapButtonContainer: {
+ bottom: 10,
+ left: '50%',
+ marginLeft: -40,
+ position: 'absolute',
+ zIndex: 1,
+ },
+});
+
+export default AnimatedMapButton;
diff --git a/src/screens/MapView/CasesMap/CustomMarker.tsx b/src/screens/MapView/CasesMap/CustomMarker.tsx
new file mode 100644
index 00000000..df57069f
--- /dev/null
+++ b/src/screens/MapView/CasesMap/CustomMarker.tsx
@@ -0,0 +1,141 @@
+import React from 'react';
+import { InteractionStatuses } from '@screens/allCases/interface';
+import { getMarkerColors, getMarkerText } from '../utils';
+import { Marker } from 'react-native-maps';
+import { useAppSelector } from '@hooks';
+import { View, Text, StyleSheet } from 'react-native';
+import { COLORS } from '@rn-ui-lib/colors';
+import MarkerTip from '@assets/icons/MarkerTip';
+import { PaymentStatus } from '@screens/caseDetails/interface';
+import { PROMISE_TO_PAY } from '../constants';
+
+interface CustomMarkerProps {
+ index: number;
+ caseReferenceId: string;
+ handleMarkerPress: (markerId: string) => void;
+ isSelectedMarker: boolean;
+}
+
+const CustomMarker = (props: CustomMarkerProps) => {
+ const { index, caseReferenceId, handleMarkerPress, isSelectedMarker } = props;
+ const caseDetails = useAppSelector((state) => state.allCases.caseDetails);
+ const isVisited =
+ caseDetails[caseReferenceId]?.currentAllocationCycleStats?.caseVisited === 'true';
+ const paymentStatus = caseDetails[caseReferenceId]?.paymentStatus || PaymentStatus.Unpaid;
+ const isPTP =
+ caseDetails[caseReferenceId]?.currentMonthCaseInteractionStatus?.statusValue ===
+ PROMISE_TO_PAY || false;
+
+ const { backgroundColor, borderColor, textColor } = getMarkerColors(
+ paymentStatus,
+ isVisited,
+ isSelectedMarker,
+ isPTP
+ );
+
+ const renderMarkerIndicator = () => {
+ if (isSelectedMarker) {
+ return ;
+ } else if (
+ paymentStatus === PaymentStatus.Paid ||
+ paymentStatus === PaymentStatus['Partially Paid'] ||
+ isPTP
+ ) {
+ return ;
+ } else if (isVisited) {
+ return (
+
+ );
+ } else {
+ return (
+
+ );
+ }
+ };
+ if (
+ !caseDetails[caseReferenceId]?.addressLocation?.latitude ||
+ !caseDetails[caseReferenceId]?.addressLocation?.longitude
+ ) {
+ return null;
+ }
+
+ return (
+ handleMarkerPress(caseReferenceId)}
+ zIndex={index}
+ >
+
+
+
+ {getMarkerText(
+ paymentStatus,
+ isPTP,
+ caseDetails[caseReferenceId]?.totalOverdueAmount || 0
+ )}
+
+
+ {renderMarkerIndicator()}
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ markerBottom: {
+ bottom: 7,
+ },
+ markerContainer: {
+ alignItems: 'center',
+ },
+ markerPill: {
+ borderRadius: 20,
+ borderWidth: 2,
+ paddingHorizontal: 10,
+ paddingVertical: 2,
+ },
+ markerText: {
+ fontSize: 10,
+ fontWeight: '700',
+ },
+ selectedMarkerBottom: {
+ bottom: 8,
+ },
+ selectedMarkerPill: {
+ backgroundColor: COLORS.TEXT.BLUE_DARK,
+ borderColor: COLORS.BACKGROUND.PRIMARY,
+ borderRadius: 20,
+ borderWidth: 3,
+ },
+ selectedMarkerText: {
+ fontSize: 12,
+ },
+});
+
+export default CustomMarker;
diff --git a/src/screens/MapView/CasesMap/MapButtons.tsx b/src/screens/MapView/CasesMap/MapButtons.tsx
new file mode 100644
index 00000000..8e5bf1b7
--- /dev/null
+++ b/src/screens/MapView/CasesMap/MapButtons.tsx
@@ -0,0 +1,80 @@
+import React from 'react';
+import { StyleSheet, Text, TouchableOpacity } from 'react-native';
+import Animated, { SharedValue, useAnimatedStyle } from 'react-native-reanimated';
+import { COLORS } from '@rn-ui-lib/colors';
+import ListIcon from '@assets/icons/ListIcon';
+import { navigateToScreen } from '@components/utlis/navigationUtlis';
+import { PageRouteEnum } from '@screens/auth/ProtectedRouter';
+import { MapViewWrapperRef } from '@components/MapViewWrapper/types';
+import RepositionButton from '../RepositionButton';
+import { LIST_BUTTON_Z_INDEX, REPOSITION_BUTTON_Z_INDEX } from '../constants';
+import { addClickstreamEvent } from '@services/clickstreamEventService';
+import { CLICKSTREAM_EVENT_NAMES } from '@common/Constants';
+
+interface MapButtonsProps {
+ buttonPosition: SharedValue;
+ mapRef: React.RefObject;
+}
+
+const MapButtons = ({ buttonPosition, mapRef }: MapButtonsProps) => {
+ const buttonAnimatedStyle = useAnimatedStyle(() => {
+ return {
+ bottom: buttonPosition.value * 208, // 208 is the height of the card
+ };
+ });
+
+ const handleListCTA = () => {
+ addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_MAP_LIST_BUTTON_CLICKED);
+ navigateToScreen(PageRouteEnum.HOME);
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+ List
+
+
+ >
+ );
+};
+
+const styles = StyleSheet.create({
+ listButton: {
+ alignItems: 'center',
+ backgroundColor: COLORS.BACKGROUND.PRIMARY,
+ borderRadius: 50,
+ bottom: 15,
+ elevation: 4,
+ flexDirection: 'row',
+ gap: 4,
+ height: 32,
+ justifyContent: 'center',
+ shadowColor: COLORS.BACKGROUND.BLACK,
+ shadowOffset: { height: 4, width: 0 },
+ shadowOpacity: 0.25,
+ shadowRadius: 4,
+ width: 80,
+ },
+ listButtonContainer: {
+ left: '50%',
+ marginLeft: -40,
+ position: 'absolute',
+ zIndex: LIST_BUTTON_Z_INDEX,
+ },
+ listButtonText: {
+ color: COLORS.TEXT.BLUE,
+ fontSize: 13,
+ fontWeight: '600',
+ },
+ recenterButtonContainer: {
+ position: 'absolute',
+ right: 0,
+ },
+});
+
+export default MapButtons;
diff --git a/src/screens/MapView/CasesMap/MapHeader.tsx b/src/screens/MapView/CasesMap/MapHeader.tsx
new file mode 100644
index 00000000..43cfc25f
--- /dev/null
+++ b/src/screens/MapView/CasesMap/MapHeader.tsx
@@ -0,0 +1,106 @@
+import { useAppSelector } from '@hooks';
+import { SearchState } from '../FiltersAndSearch/interfaces';
+import React, { useCallback, useEffect, useState } from 'react';
+import NavigationHeader from '@rn-ui-lib/components/NavigationHeader';
+import { goBack } from '@components/utlis/navigationUtlis';
+import { Keyboard, StyleSheet } from 'react-native';
+import ModalWrapperForAlfredV2 from '@common/ModalWrapperForAlfredV2';
+import { CopilotProvider } from '@components/Tour/contexts/CopilotProvider';
+import FiltersContainer from '@components/screens/allCases/allCasesFilters/FiltersContainer';
+import FiltersAndSearch from '../FiltersAndSearch/FiltersAndSearch';
+import BackdropContainer from '../FiltersAndSearch/BackdropContainer';
+import RightActionableContainer from '../FiltersAndSearch/RightActionableContainer';
+import { clearBottomSheet } from '@components/utlis/DeviceUtils';
+import { getSearchQuery } from '@screens/Dashboard/utils';
+import { SEARCH_MAIN_CONTAINER_Z_INDEX } from '../constants';
+import { CLICKSTREAM_EVENT_NAMES } from '@common/Constants';
+import { addClickstreamEvent } from '@services/clickstreamEventService';
+
+interface IMapHeader {
+ filteredCasesList: {
+ caseReferenceId: string;
+ pinRank: number | null | undefined;
+ }[];
+ isFilterApplied: boolean;
+}
+
+const MapHeader = ({ filteredCasesList, isFilterApplied }: IMapHeader) => {
+ const [searchState, setSearchState] = useState(SearchState.HIDDEN);
+ const [showFilterModal, setShowFilterModal] = useState(false);
+ const existingSearchQuery = useAppSelector((state) => getSearchQuery(state, false, false));
+
+ const isSearchVisible = searchState !== SearchState.HIDDEN;
+ const isSearchFocused = searchState === SearchState.FOCUSED;
+
+ const toggleSearch = useCallback(() => {
+ addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_MAP_SEARCH_BUTTON_CLICKED, {
+ searchState,
+ });
+ setSearchState(searchState === SearchState.HIDDEN ? SearchState.FOCUSED : SearchState.HIDDEN);
+ }, [searchState]);
+
+ const toggleFilterModal = useCallback(() => {
+ Keyboard.dismiss();
+ addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_MAP_FILTER_BUTTON_CLICKED);
+ setShowFilterModal(!showFilterModal);
+ }, [showFilterModal]);
+
+ useEffect(() => {
+ if (existingSearchQuery?.length > 0) {
+ setSearchState(SearchState.VISIBLE); //To show search bar when previous search query is present as toggle is setting show search true
+ }
+ }, []);
+
+ return (
+ <>
+
+ }
+ />
+
+ {isSearchVisible && (
+
+ )}
+
+
+ {
+ setShowFilterModal((prev) => !prev);
+ clearBottomSheet();
+ }}
+ isVisitPlan={false}
+ isAgentDashboard={false}
+ />
+
+
+ >
+ );
+};
+
+const styles = StyleSheet.create({
+ mapContainer: {
+ flex: 1,
+ position: 'relative',
+ },
+ mainContainerStyle: {
+ zIndex: SEARCH_MAIN_CONTAINER_Z_INDEX,
+ },
+});
+
+export default MapHeader;
diff --git a/src/screens/MapView/CasesMap/MapWithCasesPins.tsx b/src/screens/MapView/CasesMap/MapWithCasesPins.tsx
new file mode 100644
index 00000000..ecf492ff
--- /dev/null
+++ b/src/screens/MapView/CasesMap/MapWithCasesPins.tsx
@@ -0,0 +1,124 @@
+import MapViewWrapper from '@components/MapViewWrapper';
+import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import CustomMarker from './CustomMarker';
+import { Details, Region } from 'react-native-maps';
+import { useAppSelector } from '@hooks';
+import { runOnJS, useSharedValue, withTiming } from 'react-native-reanimated';
+import { IconStates } from '../utils';
+import { RootState } from '@store';
+import UserCard from './UserCard';
+import MapButtons from './MapButtons';
+import { MapViewWrapperRef } from '@components/MapViewWrapper/types';
+import { InteractionManager } from 'react-native';
+import { useMapContext } from '../MapContext';
+import LoadingScreen from '../LoadingScreen';
+import { CLICKSTREAM_EVENT_NAMES } from '@common/Constants';
+import { addClickstreamEvent } from '@services/clickstreamEventService';
+
+interface IMapWithCasesPins {
+ filteredCasesList: {
+ caseReferenceId: string;
+ pinRank: number | null | undefined;
+ }[];
+}
+
+const MapWithCasesPins = React.memo(({ filteredCasesList }: IMapWithCasesPins) => {
+ const mapRef = useRef(null);
+ const buttonPosition = useSharedValue(0);
+ const cardPosition = useSharedValue(0);
+ const [visibleCard, setVisibleCard] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+ const { setIconState, iconState } = useMapContext();
+ const caseDetails = useAppSelector((state: RootState) => state?.allCases?.caseDetails);
+
+ const hideCard = () => {
+ if (!visibleCard) return;
+ buttonPosition.value = withTiming(0, { duration: 100 });
+ cardPosition.value = withTiming(0, { duration: 100 }, () => {
+ runOnJS(setVisibleCard)(null);
+ });
+ };
+
+ const fitAllMarkers = () => {
+ mapRef.current?.fitToElements({
+ animated: true,
+ edgePadding: { top: 400, bottom: 60, left: 40, right: 40 },
+ });
+ };
+
+ const handleMarkerPress = useCallback(
+ (markerId: string) => {
+ addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_MAP_MARKER_CLICKED, { markerId });
+ if (visibleCard === markerId) {
+ hideCard();
+ return;
+ }
+ mapRef?.current?.animateCamera({
+ center: {
+ latitude: caseDetails[markerId]?.addressLocation?.latitude,
+ longitude: caseDetails[markerId]?.addressLocation?.longitude,
+ },
+ heading: 0,
+ pitch: 0,
+ });
+ setVisibleCard(markerId);
+ buttonPosition.value = withTiming(1, { duration: 150 });
+ cardPosition.value = withTiming(1, { duration: 150 });
+ },
+ [visibleCard, caseDetails]
+ );
+
+ const handleRegionChange = (newRegion: Region, details: Details) => {
+ if (details?.isGesture && iconState !== IconStates.FREE) {
+ setIconState(IconStates.FREE);
+ }
+ };
+
+ const markers = useMemo(() => {
+ return filteredCasesList.map((address, index: number) => (
+
+ ));
+ }, [filteredCasesList, handleMarkerPress, visibleCard]);
+
+ const isLayoutReady = useRef(false);
+
+ useEffect(() => {
+ if (isLayoutReady.current && filteredCasesList?.length >= 0) {
+ InteractionManager.runAfterInteractions(() => {
+ hideCard();
+ fitAllMarkers();
+ });
+ }
+ }, [filteredCasesList]);
+
+ return (
+ <>
+ {isLoading ? : null}
+ {visibleCard && }
+
+ {
+ isLayoutReady.current = true;
+ setTimeout(() => {
+ setIsLoading(false);
+ }, 100);
+ }}
+ ref={mapRef}
+ onRegionChangeComplete={handleRegionChange}
+ >
+ {markers}
+
+
+ >
+ );
+});
+
+MapWithCasesPins.displayName = 'MapWithCasesPins';
+
+export default MapWithCasesPins;
diff --git a/src/screens/MapView/CasesMap/UserCard.tsx b/src/screens/MapView/CasesMap/UserCard.tsx
new file mode 100644
index 00000000..efcfb7ed
--- /dev/null
+++ b/src/screens/MapView/CasesMap/UserCard.tsx
@@ -0,0 +1,165 @@
+import React, { useEffect } from 'react';
+import { StyleSheet, View, TouchableOpacity, Pressable } from 'react-native';
+import Animated, { SharedValue, useAnimatedStyle } from 'react-native-reanimated';
+import { COLORS } from '@rn-ui-lib/colors';
+import { useAppSelector } from '@hooks';
+import RightChevronIcon from '@assets/icons/RightChevronIcon';
+import FeedbackStatus from '@screens/allCases/CaseItem/FeedbackStatus';
+import ProfileHeader from '../ProfileHeader';
+import { AddressTabType } from '@screens/addressGeolocation/constant';
+import { navigateToScreen } from '@components/utlis/navigationUtlis';
+import { PageRouteEnum } from '@screens/auth/ProtectedRouter';
+import { CaseDetailStackEnum } from '@screens/caseDetails/CaseDetailStack';
+import { GenericStyles } from '@rn-ui-lib/styles';
+import { CASE_CARD_Z_INDEX } from '../constants';
+import { CLICKSTREAM_EVENT_NAMES } from '@common/Constants';
+import { addClickstreamEvent } from '@services/clickstreamEventService';
+import CopyOutlineIcon from '@assets/icons/CopyOutlineIcon';
+import { copyAddressToClipboard } from '@screens/addressGeolocation/utils/copyAddressText';
+import Text from '@rn-ui-lib/components/Text';
+
+interface UserCardProps {
+ selectedCase: string;
+ cardPosition: SharedValue;
+}
+
+const UserCard: React.FC = ({ selectedCase, cardPosition }) => {
+ const caseDetail = useAppSelector((state) => state.allCases?.caseDetails?.[selectedCase]) || {};
+
+ const cardAnimatedStyle = useAnimatedStyle(() => {
+ return {
+ transform: [
+ {
+ translateY: (1 - cardPosition.value) * 220, // 220 is the height of the card here as the profile picture takes the extra space
+ },
+ ],
+ };
+ });
+
+ const onTopAddressesPress = () => {
+ addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_MAP_CASE_CARD_TOP_ADDRESSES_CLICKED, {
+ markerId: selectedCase,
+ });
+ navigateToScreen(PageRouteEnum.CASE_DETAIL_STACK, {
+ screen: CaseDetailStackEnum.MAP_VIEW_TOP_ADDRESS,
+ params: { caseId: selectedCase },
+ });
+ };
+
+ const handleCardPress = () => {
+ addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_MAP_CASE_CARD_CLICKED, {
+ markerId: selectedCase,
+ });
+ navigateToScreen(PageRouteEnum.CASE_DETAIL_STACK, {
+ screen: CaseDetailStackEnum.COLLECTION_CASE_DETAIL,
+ params: { caseId: selectedCase },
+ });
+ };
+
+ const handleCopyAddress = () => {
+ if (!caseDetail?.addressString) return;
+ addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_MAP_COPY_ADDRESS_CLICKED, {
+ caseId: selectedCase,
+ addressText: caseDetail?.addressString,
+ });
+ copyAddressToClipboard(caseDetail?.addressString);
+ };
+
+ const isGeolocation = caseDetail?.addressStringType === AddressTabType.GEO_LOCATION;
+
+ useEffect(() => {
+ addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_MAP_CASE_CARD_LOADED, {
+ markerId: selectedCase,
+ });
+ }, []);
+
+ return (
+
+ [pressed && { opacity: 0.9 }]} onPress={handleCardPress}>
+
+
+
+
+
+
+ {!isGeolocation && (
+
+ {caseDetail?.currentPinCode}
+ {' - '}
+
+ )}
+
+ {caseDetail?.addressString}
+
+ {!isGeolocation && (
+
+
+
+ )}
+
+
+ All addresses
+
+
+
+
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ addressContainer: {
+ alignItems: 'flex-start',
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ marginBottom: 4,
+ },
+ h66: {
+ height: 66,
+ },
+ addressText: {
+ color: COLORS.TEXT.LIGHT,
+ flex: 1,
+ fontSize: 12,
+ lineHeight: 18,
+ },
+ cardContainer: {
+ backgroundColor: COLORS.BACKGROUND.PRIMARY,
+ bottom: 0,
+ left: 0,
+ paddingBottom: 16,
+ paddingTop: 16,
+ position: 'absolute',
+ right: 0,
+ zIndex: CASE_CARD_Z_INDEX,
+ },
+ cardContent: {
+ flex: 1,
+ paddingHorizontal: 16,
+ },
+ dashedBorder: {
+ borderColor: COLORS.BORDER.PRIMARY,
+ borderStyle: 'dashed',
+ borderTopWidth: 1,
+ marginBottom: 8,
+ marginTop: 4,
+ },
+ topaddressesButtonText: {
+ color: COLORS.TEXT.BLUE,
+ fontSize: 12,
+ fontWeight: '500',
+ lineHeight: 18,
+ },
+});
+
+export default UserCard;
diff --git a/src/screens/MapView/FiltersAndSearch/BackdropContainer.tsx b/src/screens/MapView/FiltersAndSearch/BackdropContainer.tsx
new file mode 100644
index 00000000..9eb11fbc
--- /dev/null
+++ b/src/screens/MapView/FiltersAndSearch/BackdropContainer.tsx
@@ -0,0 +1,49 @@
+import React from 'react';
+import { IBackdropContainerProps } from './interfaces';
+import OverlayScreen from './OverlayScreen';
+import { COLORS } from '@rn-ui-lib/colors';
+import { StyleSheet } from 'react-native';
+import EmptyList from '@screens/allCases/EmptyList';
+
+const BackdropContainer = (props: IBackdropContainerProps) => {
+ const { isSearchFocused, showEmptyListComponent, isFilterApplied } = props;
+
+ return (
+ <>
+ {showEmptyListComponent ? (
+
+ }
+ overlayBackground={styles.overlayWhiteBackground}
+ />
+ ) : null}
+ {isSearchFocused ? >} /> : null}
+ >
+ );
+};
+
+const styles = StyleSheet.create({
+ overlayWhiteBackground: {
+ backgroundColor: COLORS.TEXT.WHITE,
+ opacity: 0.9,
+ },
+ pt0: {
+ paddingTop: 0,
+ },
+ customTextStyle: {
+ color: COLORS.TEXT.BLACK_24,
+ fontWeight: '600',
+ },
+ customSubTextStyle: {
+ color: COLORS.TEXT.BLACK,
+ fontWeight: '600',
+ },
+});
+export default BackdropContainer;
diff --git a/src/screens/MapView/FiltersAndSearch/FiltersAndSearch.tsx b/src/screens/MapView/FiltersAndSearch/FiltersAndSearch.tsx
new file mode 100644
index 00000000..a9f4d43d
--- /dev/null
+++ b/src/screens/MapView/FiltersAndSearch/FiltersAndSearch.tsx
@@ -0,0 +1,107 @@
+import TextInput from '@rn-ui-lib/components/TextInput';
+import SearchIcon from '@rn-ui-lib/icons/SearchIcon';
+import React, { useRef, useEffect, useState } from 'react';
+import { Animated, StyleSheet, View } from 'react-native';
+import { IFiltersAndSearchProps, SearchState } from './interfaces';
+import { COLORS } from '@rn-ui-lib/colors';
+import { useAppDispatch, useAppSelector } from '@hooks';
+import { getSearchQuery } from '@screens/Dashboard/utils';
+import { setAllCasesViewSearchQuery } from '@reducers/allCasesSlice';
+import { CLICKSTREAM_EVENT_NAMES } from '@common/Constants';
+import { addClickstreamEvent } from '@services/clickstreamEventService';
+
+const FiltersAndSearch = (props: IFiltersAndSearchProps) => {
+ const { setSearchState, showAutoFocus } = props;
+
+ const slideAnimation = useRef(new Animated.Value(-40)).current;
+
+ const existingSearchQuery = useAppSelector((state) => getSearchQuery(state, false, false));
+ const showAnimation = !existingSearchQuery;
+
+ const [searchQuery, setSearchQuery] = useState(existingSearchQuery || '');
+
+ const dispatch = useAppDispatch();
+
+ const handleSearchChange = (query: string) => {
+ setSearchQuery(query);
+ };
+
+ const handleSearchOnEnter = (query: string) => {
+ addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_MAP_SEARCHED, { query });
+ dispatch(setAllCasesViewSearchQuery(query));
+ };
+
+ const hideSearchBar = () => {
+ if (searchQuery?.length > 0) {
+ handleSearchChange('');
+ handleSearchOnEnter('');
+ }
+ addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_MAP_SEARCH_CLOSE_BUTTON_CLICKED);
+ setSearchState(SearchState.HIDDEN);
+ };
+
+ useEffect(() => {
+ if (showAnimation) {
+ Animated.timing(slideAnimation, {
+ toValue: 0,
+ duration: 300,
+ useNativeDriver: false,
+ }).start();
+ }
+ }, [showAnimation]);
+
+ return (
+
+
+ }
+ onChangeText={handleSearchChange}
+ onSubmitEditing={() => handleSearchOnEnter(searchQuery)}
+ placeholder={`Search in my cases`}
+ defaultValue={searchQuery}
+ value={searchQuery}
+ testID="test_search"
+ showClearIcon
+ closeIconHandler={hideSearchBar}
+ showCrossIconOnEmptySearch
+ onFocus={() => setSearchState(SearchState.FOCUSED)}
+ onBlur={() => setSearchState(SearchState.VISIBLE)}
+ autoFocus={showAutoFocus}
+ />
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ backgroundColor: COLORS.BACKGROUND.INDIGO,
+ paddingBottom: 12,
+ paddingHorizontal: 16,
+ },
+ textInput: {
+ flex: 1,
+ },
+ searchContainer: {
+ position: 'absolute',
+ width: '100%',
+ top: 54,
+ left: 0,
+ right: 0,
+ },
+});
+export default FiltersAndSearch;
diff --git a/src/screens/MapView/FiltersAndSearch/OverlayScreen.tsx b/src/screens/MapView/FiltersAndSearch/OverlayScreen.tsx
new file mode 100644
index 00000000..9bd7f84a
--- /dev/null
+++ b/src/screens/MapView/FiltersAndSearch/OverlayScreen.tsx
@@ -0,0 +1,30 @@
+import React from 'react';
+import { IOverlayScreenProps } from './interfaces';
+import { Keyboard, StyleSheet, TouchableWithoutFeedback, View } from 'react-native';
+
+const OverlayScreen = (props: IOverlayScreenProps) => {
+ const { children, overlayBackground, onDismiss } = props;
+ const handlePress = () => {
+ Keyboard.dismiss(); //To dismiss keyboard
+ if (onDismiss) onDismiss();
+ };
+ return (
+
+ {children}
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ right: 0,
+ bottom: 0,
+ opacity: 0.7,
+ backgroundColor: 'black',
+ },
+});
+
+export default OverlayScreen;
diff --git a/src/screens/MapView/FiltersAndSearch/RightActionableContainer.tsx b/src/screens/MapView/FiltersAndSearch/RightActionableContainer.tsx
new file mode 100644
index 00000000..5ffb8d16
--- /dev/null
+++ b/src/screens/MapView/FiltersAndSearch/RightActionableContainer.tsx
@@ -0,0 +1,85 @@
+import React from 'react';
+import { GenericStyles } from '@rn-ui-lib/styles';
+import FilterIcon from '@assets/icons/FilterIcon';
+import Text from '@rn-ui-lib/components/Text';
+import { COLORS } from '@rn-ui-lib/colors';
+import { View, TouchableHighlight, StyleSheet } from 'react-native';
+import SearchIcon from '@assets/icons/SearchIcon';
+import { IRightActionableContainerProps } from './interfaces';
+import { useAppSelector } from '@hooks';
+
+const RightActionableContainer = (props: IRightActionableContainerProps) => {
+ const { showSearch, toggleSearch, toggleFilterModal } = props;
+ const filterCount = useAppSelector((state) => state?.filters?.filterCount);
+
+ return (
+
+ {showSearch ? (
+
+
+
+ ) : null}
+
+
+
+ {filterCount > 0 && (
+
+ 9 ? styles.ph6 : styles.ph8,
+ ]}
+ >
+ {filterCount}
+
+
+ )}
+
+
+
+ );
+};
+const styles = StyleSheet.create({
+ iconContainerButton: {
+ height: 40,
+ width: 40,
+ borderRadius: 24,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ filterCountText: {
+ fontSize: 9,
+ fontWeight: '400',
+ height: 22,
+ },
+ filterCount: {
+ backgroundColor: COLORS.TEXT.BLUE,
+ paddingVertical: 0,
+ borderRadius: 12,
+ position: 'absolute',
+ opacity: 0.8,
+ top: 12,
+ right: -6,
+ },
+ filterIcon: {
+ backgroundColor: COLORS.BACKGROUND.PRIMARY,
+ },
+ ph6: {
+ paddingHorizontal: 6,
+ },
+ ph8: {
+ paddingHorizontal: 8,
+ },
+});
+export default RightActionableContainer;
diff --git a/src/screens/MapView/FiltersAndSearch/interfaces.ts b/src/screens/MapView/FiltersAndSearch/interfaces.ts
new file mode 100644
index 00000000..4d98307a
--- /dev/null
+++ b/src/screens/MapView/FiltersAndSearch/interfaces.ts
@@ -0,0 +1,30 @@
+import { StyleProp, ViewStyle } from 'react-native';
+
+export enum SearchState {
+ HIDDEN, // Search is not visible
+ VISIBLE, // Search is visible but not focused
+ FOCUSED, // Search is visible and focused
+}
+
+export interface IFiltersAndSearchProps {
+ setSearchState: React.Dispatch>;
+ showAutoFocus: boolean;
+}
+
+export interface IOverlayScreenProps {
+ children: React.ReactNode;
+ overlayBackground?: StyleProp;
+ onDismiss?: () => void;
+}
+
+export interface IRightActionableContainerProps {
+ showSearch: boolean;
+ toggleSearch: () => void;
+ toggleFilterModal: () => void;
+}
+
+export interface IBackdropContainerProps {
+ isSearchFocused: boolean;
+ showEmptyListComponent: boolean;
+ isFilterApplied: boolean;
+}
diff --git a/src/screens/MapView/LoadingScreen/index.tsx b/src/screens/MapView/LoadingScreen/index.tsx
new file mode 100644
index 00000000..8a93b558
--- /dev/null
+++ b/src/screens/MapView/LoadingScreen/index.tsx
@@ -0,0 +1,47 @@
+import { isFunction } from '@components/utlis/commonFunctions';
+import { COLORS } from '@rn-ui-lib/colors';
+import { GenericStyles } from '@rn-ui-lib/styles';
+import React, { useEffect } from 'react';
+import { ActivityIndicator, StyleSheet, TouchableWithoutFeedback, View } from 'react-native';
+import { OVERLAY_SCREEN_Z_INDEX } from '../constants';
+
+interface ILoadingScreen {
+ handleMapReady: () => void;
+}
+
+const LoadingScreen = (props: ILoadingScreen) => {
+ const { handleMapReady } = props;
+
+ useEffect(() => {
+ return () => {
+ if (isFunction(handleMapReady)) {
+ handleMapReady();
+ }
+ };
+ }, []);
+
+ return (
+
+
+
+
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ top: 0,
+ width: '100%',
+ height: '100%',
+ flex: 1,
+ position: 'absolute',
+ zIndex: OVERLAY_SCREEN_Z_INDEX,
+ opacity: 0.7,
+ backgroundColor: 'black',
+ },
+});
+
+export default LoadingScreen;
diff --git a/src/screens/MapView/MapContext.tsx b/src/screens/MapView/MapContext.tsx
new file mode 100644
index 00000000..b21ad2a0
--- /dev/null
+++ b/src/screens/MapView/MapContext.tsx
@@ -0,0 +1,40 @@
+import React, { createContext, useContext, useState } from 'react';
+import { IconStates } from './utils';
+import { ILocationData } from '@screens/addresses/interfaces';
+
+interface MapContextType {
+ iconState: IconStates;
+ setIconState: (state: IconStates) => void;
+ selectedPin: string | null;
+ setSelectedPin: (pin: string | null) => void;
+}
+
+const MapContext = createContext(undefined);
+
+export const MapProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
+ const [iconState, setIconState] = useState(IconStates.DEFAULT);
+ const [selectedPin, setSelectedPin] = useState(null);
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useMapContext = () => {
+ const context = useContext(MapContext);
+ if (!context) {
+ throw new Error('useMapContext must be used within MapProvider');
+ }
+ return context;
+};
+
+export default MapContext;
diff --git a/src/screens/MapView/MapViewStack.tsx b/src/screens/MapView/MapViewStack.tsx
new file mode 100644
index 00000000..fcbd9ae1
--- /dev/null
+++ b/src/screens/MapView/MapViewStack.tsx
@@ -0,0 +1,25 @@
+import React from 'react';
+import { getScreenFocusListenerObj } from '@components/utlis/commonFunctions';
+import { createNativeStackNavigator } from '@react-navigation/native-stack';
+import { DEFAULT_SCREEN_OPTIONS } from '@screens/auth/ProtectedRouter';
+import MapView from '.';
+
+const Stack = createNativeStackNavigator();
+
+export enum MapViewStackEnum {
+ MAP_VIEW_MAIN = 'mapViewMain',
+}
+
+const MapViewStack = () => {
+ return (
+
+
+
+ );
+};
+
+export default MapViewStack;
diff --git a/src/screens/MapView/ProfileHeader/CurvedBackground.tsx b/src/screens/MapView/ProfileHeader/CurvedBackground.tsx
new file mode 100644
index 00000000..7db6f05f
--- /dev/null
+++ b/src/screens/MapView/ProfileHeader/CurvedBackground.tsx
@@ -0,0 +1,20 @@
+import React from 'react';
+import { View } from 'react-native';
+import Svg, { G, Path } from 'react-native-svg';
+
+const CurvedBackground = () => (
+
+
+
+);
+
+export default CurvedBackground;
diff --git a/src/screens/MapView/ProfileHeader/ProfileAvatar.tsx b/src/screens/MapView/ProfileHeader/ProfileAvatar.tsx
new file mode 100644
index 00000000..9876adcb
--- /dev/null
+++ b/src/screens/MapView/ProfileHeader/ProfileAvatar.tsx
@@ -0,0 +1,72 @@
+import Text from '@rn-ui-lib/components/Text';
+import React from 'react';
+import { StyleSheet, View } from 'react-native';
+import FastImage from 'react-native-fast-image';
+import CurvedBackground from './CurvedBackground';
+import { COLORS } from '@rn-ui-lib/colors';
+import RNFS from 'react-native-fs';
+import { GlobalImageMap } from '@common/CachedImage';
+
+interface IProfileAvatar {
+ name: string;
+ caseId: string;
+}
+
+const ProfileAvatar: React.FC = ({ name, caseId }) => {
+ const cacheDirectory = RNFS.CachesDirectoryPath;
+ const cacheFilePath = `${cacheDirectory}/${caseId}.jpg`;
+
+ const renderProfileImage = () => {
+ if (GlobalImageMap[cacheFilePath]) {
+ return (
+
+ );
+ }
+
+ return (
+
+ {name.slice(0, 2).toUpperCase()}
+
+ );
+ };
+
+ return (
+
+
+ {renderProfileImage()}
+
+ );
+};
+
+const styles = StyleSheet.create({
+ profileImageContainer: {
+ position: 'absolute',
+ top: -41,
+ },
+ profileImage: {
+ left: 19,
+ bottom: 5,
+ width: 46,
+ height: 46,
+ borderRadius: 16,
+ },
+ placeholderImage: {
+ left: 19,
+ bottom: 5,
+ width: 46,
+ height: 46,
+ borderRadius: 16,
+ backgroundColor: COLORS.BACKGROUND.SILVER_LIGHT,
+ justifyContent: 'center',
+ alignItems: 'center',
+ borderWidth: 1,
+ borderColor: COLORS.BACKGROUND.PRIMARY,
+ },
+ placeholderText: {
+ fontSize: 16,
+ fontWeight: '600',
+ color: COLORS.TEXT.BLACK,
+ },
+});
+
+export default ProfileAvatar;
diff --git a/src/screens/MapView/ProfileHeader/index.tsx b/src/screens/MapView/ProfileHeader/index.tsx
new file mode 100644
index 00000000..cd301cd4
--- /dev/null
+++ b/src/screens/MapView/ProfileHeader/index.tsx
@@ -0,0 +1,119 @@
+import React, { useMemo } from 'react';
+import { View, StyleSheet, Linking } from 'react-native';
+import Text from '@rn-ui-lib/components/Text';
+import Tag, { TagVariant } from '@rn-ui-lib/components/Tag';
+import { GenericStyles } from '@rn-ui-lib/styles';
+import ProfileAvatar from './ProfileAvatar';
+import { useAppSelector } from '@hooks';
+import { paymentStatusMapping } from '@screens/allCases/utils';
+import { formatAmount } from '@rn-ui-lib/utils/amount';
+import { getDistanceFromLatLonInKm, getGoogleMapUrl } from '@components/utlis/commonFunctions';
+import relativeDistanceFormatter from '@screens/addressGeolocation/utils/relativeDistanceFormatter';
+import { LocationType } from '@screens/addresses/interfaces';
+import Button from '@rn-ui-lib/components/Button';
+import MapDirectionIcon from '@assets/icons/MapDirectionIcon';
+import { CLICKSTREAM_EVENT_NAMES } from '@common/Constants';
+import { addClickstreamEvent } from '@services/clickstreamEventService';
+
+interface ProfileHeaderProps {
+ caseId: string;
+ showDirections?: boolean;
+}
+
+const ProfileHeader: React.FC = ({ caseId, showDirections = false }) => {
+ const caseDetails = useAppSelector((state) => state.allCases?.caseDetails?.[caseId]);
+ const { customerName, totalOverdueAmount, paymentStatus, collectionTag, dpdBucket } =
+ caseDetails || {};
+ const deviceGeolocationCoordinate = useAppSelector(
+ (state) => state.foregroundService.deviceGeolocationCoordinate
+ );
+ const isGeoLocation = caseDetails?.addressStringType === LocationType.GEO_LOCATION;
+
+ const relativeDistanceBwLatLong = useMemo(() => {
+ const distance = getDistanceFromLatLonInKm(deviceGeolocationCoordinate, {
+ latitude: caseDetails?.addressLocation?.latitude,
+ longitude: caseDetails?.addressLocation?.longitude,
+ });
+ return `${relativeDistanceFormatter(distance)} km`;
+ }, [deviceGeolocationCoordinate]);
+
+ const handleDirectionsPress = () => {
+ addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_MAP_CASE_CARD_MAP_DIRECTION_CLICKED, {
+ markerId: caseId,
+ });
+ const mapUrl = getGoogleMapUrl(
+ caseDetails?.addressLocation?.latitude,
+ caseDetails?.addressLocation?.longitude
+ );
+
+ if (mapUrl) {
+ return Linking.openURL(mapUrl);
+ }
+ };
+
+ return (
+
+
+
+
+
+ {customerName}
+
+ {showDirections &&
+ (!isGeoLocation ? (
+
+ {relativeDistanceBwLatLong}
+
+ ) : (
+ }
+ pressableWidthChange={false}
+ opacityChangeOnPress={true}
+ buttonStyle={{ paddingVertical: 0 }}
+ />
+ ))}
+
+
+
+ {dpdBucket ? : null}
+ {paymentStatus ? (
+
+ ) : null}
+ {collectionTag ? : null}
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ position: 'relative',
+ },
+ gap8: {
+ gap: 8,
+ },
+ topRow: {
+ alignItems: 'center',
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ },
+});
+
+export default ProfileHeader;
diff --git a/src/screens/MapView/RepositionButton/index.tsx b/src/screens/MapView/RepositionButton/index.tsx
new file mode 100644
index 00000000..b44b159f
--- /dev/null
+++ b/src/screens/MapView/RepositionButton/index.tsx
@@ -0,0 +1,74 @@
+import React from 'react';
+import { StyleSheet, TouchableOpacity } from 'react-native';
+import { getIcon, IconStates } from '../utils';
+import { COLORS } from '@rn-ui-lib/colors';
+import { useAppSelector } from '@hooks';
+import { RootState } from '@store';
+import { useMapContext } from '../MapContext';
+import { MapViewWrapperRef } from '@components/MapViewWrapper/types';
+import { CLICKSTREAM_EVENT_NAMES } from '@common/Constants';
+import { addClickstreamEvent } from '@services/clickstreamEventService';
+
+interface IRepositionButtonProps {
+ mapRef: React.RefObject;
+}
+
+const RepositionButton = ({ mapRef }: IRepositionButtonProps) => {
+ const deviceGeolocationCoordinate = useAppSelector(
+ (state: RootState) => state.foregroundService?.deviceGeolocationCoordinate
+ );
+
+ const { iconState, setIconState } = useMapContext();
+
+ const handleMapReady = () => {
+ mapRef?.current?.fitToElements({
+ animated: true,
+ edgePadding: { top: 400, bottom: 60, left: 40, right: 40 },
+ });
+ };
+
+ const handleRecenterCTA = () => {
+ addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_MAP_REPOSITION_BUTTON_CLICKED, { iconState });
+ if (iconState === IconStates.DEFAULT || iconState === IconStates.FREE) {
+ if (deviceGeolocationCoordinate) {
+ mapRef?.current?.animateToRegion(
+ {
+ latitude: deviceGeolocationCoordinate.latitude,
+ longitude: deviceGeolocationCoordinate.longitude,
+ latitudeDelta: 0.05,
+ longitudeDelta: 0.05,
+ },
+ 500
+ );
+ }
+ } else {
+ handleMapReady();
+ }
+ setIconState(iconState === IconStates.LOCATION ? IconStates.DEFAULT : IconStates.LOCATION);
+ };
+
+ return (
+
+ {getIcon(iconState)}
+
+ );
+};
+
+const styles = StyleSheet.create({
+ recenterButton: {
+ alignItems: 'center',
+ backgroundColor: COLORS.BACKGROUND.PRIMARY,
+ borderRadius: 24,
+ elevation: 4,
+ height: 50,
+ right: 10,
+ bottom: 12,
+ justifyContent: 'center',
+ shadowColor: COLORS.BACKGROUND.BLACK,
+ shadowOffset: { height: 4, width: 0 },
+ shadowOpacity: 0.25,
+ shadowRadius: 4,
+ width: 50,
+ },
+});
+export default RepositionButton;
diff --git a/src/screens/MapView/TopAddress/AddressList.tsx b/src/screens/MapView/TopAddress/AddressList.tsx
new file mode 100644
index 00000000..1ec482a2
--- /dev/null
+++ b/src/screens/MapView/TopAddress/AddressList.tsx
@@ -0,0 +1,93 @@
+import React, { useEffect, useCallback } from 'react';
+import { FlatList, StyleSheet, View } from 'react-native';
+import { COLORS } from '@rn-ui-lib/colors';
+import AddressListItem from './AddressListItem';
+import ProfileHeader from '@screens/MapView/ProfileHeader';
+import { ILocationData } from '@screens/addresses/interfaces';
+import { useMapContext } from '../MapContext';
+import { useAppSelector } from '@hooks';
+import { RootState } from '@store';
+
+interface AddressListProps {
+ addresses: ILocationData[];
+ caseId: string;
+ flatListRef: React.RefObject>;
+ handlePinSelect: (address: ILocationData | null) => void;
+}
+
+const AddressList: React.FC = (props) => {
+ const { addresses, flatListRef, handlePinSelect, caseId } = props;
+ const { selectedPin } = useMapContext();
+
+ const addressId = useAppSelector(
+ (state: RootState) => state.allCases?.caseDetails[caseId]?.addressReferenceId
+ );
+
+ useEffect(() => {
+ if (!addresses?.length || !addressId) return;
+ const address = addresses?.find((address) => address?.referenceId === addressId);
+ if (!address || selectedPin === address?.referenceId) return;
+ handlePinSelect(address);
+ }, [addresses, addressId]);
+
+ const handleScrollToIndexFailed = useCallback((info: { index: number }) => {
+ const wait = new Promise((resolve) => setTimeout(resolve, 500));
+ wait.then(() => {
+ flatListRef.current?.scrollToIndex({
+ index: info.index,
+ animated: true,
+ viewPosition: 0.5,
+ });
+ });
+ }, []);
+
+ const renderAddressItem = useCallback(
+ ({ item }: { item: ILocationData }) => (
+ handlePinSelect(address)}
+ caseId={caseId}
+ />
+ ),
+ [selectedPin, handlePinSelect, caseId]
+ );
+
+ return (
+
+
+
+ item.referenceId}
+ renderItem={renderAddressItem}
+ showsVerticalScrollIndicator={false}
+ onScrollToIndexFailed={handleScrollToIndexFailed}
+ />
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: COLORS.BACKGROUND.PRIMARY,
+ paddingTop: 16,
+ elevation: 10,
+ shadowColor: 'black',
+ shadowOffset: {
+ width: 0,
+ height: 40,
+ },
+ shadowRadius: 10,
+ shadowOpacity: 1,
+ },
+ dottedLine: {
+ borderWidth: 0.5,
+ borderColor: COLORS.BORDER.PRIMARY,
+ borderStyle: 'dashed',
+ },
+});
+
+export default AddressList;
diff --git a/src/screens/MapView/TopAddress/AddressListItem.tsx b/src/screens/MapView/TopAddress/AddressListItem.tsx
new file mode 100644
index 00000000..0eeb106f
--- /dev/null
+++ b/src/screens/MapView/TopAddress/AddressListItem.tsx
@@ -0,0 +1,165 @@
+import React, { memo } from 'react';
+import { Pressable, StyleSheet, View } from 'react-native';
+import Text from '@rn-ui-lib/components/Text';
+import { COLORS } from '@rn-ui-lib/colors';
+import { GenericStyles } from '@rn-ui-lib/styles';
+import { ILocationData } from '@screens/addresses/interfaces';
+import { getFeedbackColors, isGeolocation } from '@screens/addresses/utils';
+import AddressItemHeader from '@screens/addresses/common/AddressItemHeader';
+import dayjs from 'dayjs';
+import { BUSINESS_DATE_FORMAT } from '@rn-ui-lib/utils/dates';
+import Button from '@rn-ui-lib/components/Button';
+import RightChevronIcon from '@assets/icons/RightChevronIcon';
+import { navigateToScreen } from '@components/utlis/navigationUtlis';
+import { CaseDetailStackEnum } from '@screens/caseDetails/CaseDetailStack';
+import CopyOutlineIcon from '@assets/icons/CopyOutlineIcon';
+import { addClickstreamEvent } from '@services/clickstreamEventService';
+import { CLICKSTREAM_EVENT_NAMES } from '@common/Constants';
+import { copyAddressToClipboard } from '@screens/addressGeolocation/utils/copyAddressText';
+
+interface AddressListItemProps {
+ address: ILocationData;
+ handlePinSelect: (address: ILocationData | null) => void;
+ isPinSelected: boolean;
+ caseId: string;
+}
+
+const AddressListItem: React.FC = memo(
+ ({ address, handlePinSelect, isPinSelected, caseId }) => {
+ const { latestFeedbackStatus, latestFeedbackTimestamp, promiseToPayDate, feedbackColourCode } =
+ address?.latestFeedback || {};
+
+ const { feedbackColor } = getFeedbackColors(latestFeedbackStatus, feedbackColourCode);
+
+ const openAddress = () => {
+ addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_MAP_MORE_DETAILS_CLICKED, {
+ addressId: address?.referenceId,
+ });
+ navigateToScreen(CaseDetailStackEnum.TOP_ADDRESSES, {
+ caseId,
+ scrollToIndex: address?.rank,
+ });
+ };
+
+ const handleAddressPress = () => {
+ if (isPinSelected) {
+ handlePinSelect(null);
+ return;
+ }
+ handlePinSelect(address);
+ };
+
+ const handleCopyAddress = () => {
+ addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_MAP_COPY_ADDRESS_CLICKED, {
+ caseId,
+ addressText: address?.addressText,
+ });
+ copyAddressToClipboard(address?.addressText);
+ };
+
+ return (
+ [
+ styles.container,
+ isPinSelected && styles.selectedPin,
+ { opacity: pressed ? 0.7 : 1 },
+ ]}
+ onPress={handleAddressPress}
+ >
+
+
+
+ {address?.addressText}
+ {!isGeolocation(address?.locationSubType) && (
+
+ {' '}
+
+
+
+
+ )}
+
+
+
+
+ Last feedback{' '}
+ {latestFeedbackTimestamp ? (
+
+ ({dayjs(latestFeedbackTimestamp).format(BUSINESS_DATE_FORMAT)})
+
+ ) : null}
+
+
+ {latestFeedbackStatus
+ ? `${latestFeedbackStatus} ${
+ promiseToPayDate
+ ? `on ${dayjs(promiseToPayDate).format(BUSINESS_DATE_FORMAT)}`
+ : ''
+ }`
+ : 'Unvisited address'}
+
+
+
+ }
+ pressableWidthChange={false}
+ opacityChangeOnPress={true}
+ buttonStyle={{ paddingVertical: 0 }}
+ />
+
+
+
+
+ );
+ }
+);
+
+AddressListItem.displayName = 'AddressListItem';
+
+const styles = StyleSheet.create({
+ container: {
+ width: '100%',
+ borderBottomWidth: 1,
+ borderBottomColor: COLORS.BORDER.PRIMARY,
+ },
+ selectedPin: {
+ backgroundColor: COLORS.BACKGROUND.BLUE,
+ },
+ addressText: {
+ flex: 1,
+ fontSize: 12,
+ color: COLORS.TEXT.LIGHT,
+ lineHeight: 18,
+ },
+ borderBottom0: {
+ borderBottomWidth: 0,
+ },
+});
+
+export default AddressListItem;
diff --git a/src/screens/MapView/TopAddress/MapWithAddresses.tsx b/src/screens/MapView/TopAddress/MapWithAddresses.tsx
new file mode 100644
index 00000000..bb54f20c
--- /dev/null
+++ b/src/screens/MapView/TopAddress/MapWithAddresses.tsx
@@ -0,0 +1,121 @@
+import React, { useRef } from 'react';
+import { useMapContext } from '../MapContext';
+import { FlatList, StyleSheet, View } from 'react-native';
+import { Details, Marker, Region } from 'react-native-maps';
+import MapViewWrapper from '@components/MapViewWrapper';
+import { MapViewWrapperRef } from '@components/MapViewWrapper/types';
+import AddressList from './AddressList';
+import { ILocationData } from '@screens/addresses/interfaces';
+import { useAppSelector } from '@hooks';
+import { RootState } from '@store';
+import { GenericStyles } from '@rn-ui-lib/styles';
+import Pin from './Pin';
+import { IconStates } from '../utils';
+import RepositionButton from '../RepositionButton';
+import LoadingScreen from '../LoadingScreen';
+import { TOP_ADDRESS_PIN_SELECTED_Z_INDEX } from '../constants';
+import { CLICKSTREAM_EVENT_NAMES } from '@common/Constants';
+import { addClickstreamEvent } from '@services/clickstreamEventService';
+
+interface IMapWithAddressesProps {
+ caseId: string;
+}
+
+const MapWithAddresses = ({ caseId }: IMapWithAddressesProps) => {
+ const mapRef = useRef(null);
+ const flatListRef = useRef>(null);
+ const { iconState, selectedPin, setIconState, setSelectedPin } = useMapContext();
+
+ const addresses = useAppSelector(
+ (state: RootState) => state.topAddresses?.[caseId]?.mapAddresses
+ );
+ const isMapAddressesLoading = useAppSelector(
+ (state: RootState) => state.topAddresses?.[caseId]?.isMapAddressesLoading
+ );
+
+ const handleMapReady = () => {
+ mapRef.current?.fitToElements({
+ animated: true,
+ edgePadding: { top: 80, bottom: 20, left: 40, right: 40 },
+ });
+ };
+
+ const handleRegionChange = (newRegion: Region, details: Details) => {
+ if (details?.isGesture && iconState !== IconStates.FREE) {
+ setIconState(IconStates.FREE);
+ }
+ };
+
+ const handlePinSelect = (address: ILocationData | null) => {
+ addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_MAP_ADDRESS_MARKER_CLICKED, {
+ addressId: address?.referenceId,
+ });
+ if (!address || selectedPin === address?.referenceId) {
+ setSelectedPin(null);
+ handleMapReady();
+ return;
+ }
+ setSelectedPin(address?.referenceId);
+ flatListRef?.current?.scrollToIndex({ index: address?.rank - 1, animated: true });
+ mapRef?.current?.animateCamera({
+ center: {
+ latitude: address.latitude,
+ longitude: address.longitude,
+ },
+ heading: 0,
+ pitch: 0,
+ });
+ };
+
+ return (
+
+ {isMapAddressesLoading ? : null}
+
+
+ {addresses?.map((address) => {
+ if (address?.latitude && address?.longitude) {
+ return (
+ handlePinSelect(address)}
+ key={address.referenceId}
+ identifier={address.referenceId}
+ tracksViewChanges={selectedPin === address?.referenceId}
+ zIndex={
+ selectedPin !== address?.referenceId
+ ? address?.rank
+ : TOP_ADDRESS_PIN_SELECTED_Z_INDEX
+ }
+ coordinate={{ latitude: address.latitude, longitude: address.longitude }}
+ >
+
+
+ );
+ }
+ })}
+
+
+
+
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ mapContainer: {
+ height: '60%',
+ },
+ recenterButtonContainer: {
+ position: 'absolute',
+ right: 0,
+ bottom: -5,
+ },
+});
+
+export default MapWithAddresses;
diff --git a/src/screens/MapView/TopAddress/Pin.tsx b/src/screens/MapView/TopAddress/Pin.tsx
new file mode 100644
index 00000000..b8708921
--- /dev/null
+++ b/src/screens/MapView/TopAddress/Pin.tsx
@@ -0,0 +1,66 @@
+import { ILocationData } from '@screens/addresses/interfaces';
+import React from 'react';
+import Svg, { Path, Rect, Text, TSpan } from 'react-native-svg';
+import { useMapContext } from '../MapContext';
+import { getPinColors } from '../utils';
+import { COLORS } from '@rn-ui-lib/colors';
+
+interface IPinProps {
+ address: ILocationData;
+}
+
+const Pin = ({ address }: IPinProps) => {
+ const { selectedPin } = useMapContext();
+
+ const { stroke, textColor, circleColor } = getPinColors(address, selectedPin);
+
+ if (address.referenceId === selectedPin) {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+};
+
+export default Pin;
diff --git a/src/screens/MapView/TopAddress/index.tsx b/src/screens/MapView/TopAddress/index.tsx
new file mode 100644
index 00000000..e2646a66
--- /dev/null
+++ b/src/screens/MapView/TopAddress/index.tsx
@@ -0,0 +1,53 @@
+import React, { useEffect } from 'react';
+import { StyleSheet, View } from 'react-native';
+import { COLORS } from '@rn-ui-lib/colors';
+import { goBack } from '@components/utlis/navigationUtlis';
+import NavigationHeader from '@rn-ui-lib/components/NavigationHeader';
+import { useAppDispatch } from '@hooks';
+import { getMapAddresses } from '@screens/addresses/actions';
+import { MapProvider } from '../MapContext';
+import MapWithAddresses from './MapWithAddresses';
+import { addClickstreamEvent } from '@services/clickstreamEventService';
+import { CLICKSTREAM_EVENT_NAMES } from '@common/Constants';
+
+interface ITopAddressMapView {
+ route: {
+ params: {
+ caseId: string;
+ };
+ };
+}
+
+const TopAddressMapView = (props: ITopAddressMapView) => {
+ const {
+ route: {
+ params: { caseId },
+ },
+ } = props;
+ const dispatch = useAppDispatch();
+
+ useEffect(() => {
+ dispatch(getMapAddresses(caseId));
+ addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_MAP_TOP_ADDRESSES_SCREEN_LOADED, {
+ caseId,
+ });
+ }, []);
+
+ return (
+
+
+
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: COLORS.BACKGROUND.SILVER_LIGHT,
+ },
+});
+
+export default TopAddressMapView;
diff --git a/src/screens/MapView/constants.ts b/src/screens/MapView/constants.ts
new file mode 100644
index 00000000..30af9e2b
--- /dev/null
+++ b/src/screens/MapView/constants.ts
@@ -0,0 +1,26 @@
+export const AGENT_MARKER_Z_INDEX = 1000;
+
+export const DEFAULT_MAP_COORDINATES = {
+ latitude: 12.9267184,
+ longitude: 77.6654294,
+};
+
+export const DEFAULT_MAP_DELTAS = {
+ latitudeDelta: 0.09422,
+ longitudeDelta: 0.091421,
+};
+
+export const SELECTED_PIN_Z_INDEX = 1001;
+
+export const OVERLAY_SCREEN_Z_INDEX = 1;
+
+export const CASE_CARD_Z_INDEX = 1;
+
+export const SEARCH_MAIN_CONTAINER_Z_INDEX = 5;
+
+export const LIST_BUTTON_Z_INDEX = 2;
+export const REPOSITION_BUTTON_Z_INDEX = 1;
+
+export const TOP_ADDRESS_PIN_SELECTED_Z_INDEX = 1001;
+
+export const PROMISE_TO_PAY = 'PROMISE_TO_PAY';
diff --git a/src/screens/MapView/hooks/useSearchAndFilter.ts b/src/screens/MapView/hooks/useSearchAndFilter.ts
new file mode 100644
index 00000000..721d6d37
--- /dev/null
+++ b/src/screens/MapView/hooks/useSearchAndFilter.ts
@@ -0,0 +1,70 @@
+import { useMemo } from 'react';
+import { useAppSelector } from '@hooks';
+import { Search } from '@rn-ui-lib/utils/search';
+import { LIST_HEADER_ITEMS } from '@screens/allCases/constants';
+import { ICaseItem } from '@screens/allCases/interface';
+import { CaseDetail } from '@screens/caseDetails/interface';
+import { evaluateFilterForCases } from '@components/screens/allCases/allCasesFilters/FilterUtils';
+import store from '@store';
+
+interface FilteredCasesResult {
+ filteredCasesList: ICaseItem[];
+ isFilterApplied: boolean;
+}
+
+export const useFilteredCases = (): FilteredCasesResult => {
+ const existingSearchQuery = useAppSelector((state) => state.allCases?.allCasesViewSearchQuery);
+ const filters = useAppSelector((state) => state.filters.filters);
+ const selectedFilters = useAppSelector((state) => state.filters.selectedFilters);
+ const filterCount = useAppSelector((state) => state?.filters?.filterCount);
+
+ const { filteredCasesList, isFilterApplied } = useMemo(() => {
+ const caseDetails = store?.getState()?.allCases?.caseDetails;
+ const pendingList = store?.getState()?.allCases?.pendingList;
+ const pinnedList = store?.getState()?.allCases?.pinnedList;
+ const completedList = store?.getState()?.allCases?.completedList;
+
+ const allCasesList = [...pendingList, ...pinnedList, ...completedList];
+
+ const filteredList = allCasesList?.filter((caseItem: ICaseItem) => {
+ return (
+ (caseItem?.type && LIST_HEADER_ITEMS.includes(caseItem.type)) ||
+ evaluateFilterForCases(caseDetails[caseItem?.caseReferenceId], filters, selectedFilters)
+ );
+ });
+
+ const listForSearch: CaseDetail[] = [];
+ filteredList.forEach((caseItem: ICaseItem) => {
+ listForSearch.push(caseDetails[caseItem?.caseReferenceId]);
+ });
+
+ const isFilterApplied = filterCount > 0 || existingSearchQuery?.length > 0;
+
+ if (!existingSearchQuery?.length) {
+ return { filteredCasesList: filteredList, isFilterApplied };
+ }
+
+ const searchResults = Search(existingSearchQuery, listForSearch || [], {
+ keys: [
+ 'customerInfo.customerName',
+ 'customerName',
+ 'currentTask.metadata.addressLine',
+ 'addressString',
+ 'customerInfo.primaryPhoneNumber',
+ 'primaryPhoneNumber',
+ 'loanAccountNumber',
+ ],
+ }) as Array<{ obj: CaseDetail }>;
+
+ const searchResultsList: ICaseItem[] = searchResults.map((filteredListItem) => {
+ const { caseReferenceId, id, pinRank } = filteredListItem?.obj || {};
+ return {
+ caseReferenceId: caseReferenceId || id,
+ pinRank: pinRank || null,
+ };
+ });
+ return { filteredCasesList: searchResultsList, isFilterApplied };
+ }, [filters, selectedFilters, existingSearchQuery]);
+
+ return { filteredCasesList, isFilterApplied };
+};
diff --git a/src/screens/MapView/index.tsx b/src/screens/MapView/index.tsx
new file mode 100644
index 00000000..edd35e12
--- /dev/null
+++ b/src/screens/MapView/index.tsx
@@ -0,0 +1,106 @@
+import React, { useCallback, useEffect, useState } from 'react';
+import { Keyboard, StyleSheet, View } from 'react-native';
+import { COLORS } from '@rn-ui-lib/colors';
+import { MapProvider } from './MapContext';
+import { useFilteredCases } from './hooks/useSearchAndFilter';
+import MapWithCasesPins from './CasesMap/MapWithCasesPins';
+import { addClickstreamEvent } from '@services/clickstreamEventService';
+import { CLICKSTREAM_EVENT_NAMES } from '@common/Constants';
+import NavigationHeader from '@rn-ui-lib/components/NavigationHeader';
+import RightActionableContainer from './FiltersAndSearch/RightActionableContainer';
+import FiltersAndSearch from './FiltersAndSearch/FiltersAndSearch';
+import { useAppSelector } from '@hooks';
+import { SearchState } from './FiltersAndSearch/interfaces';
+import { goBack } from '@components/utlis/navigationUtlis';
+import { SEARCH_MAIN_CONTAINER_Z_INDEX } from './constants';
+import BackdropContainer from './FiltersAndSearch/BackdropContainer';
+import ModalWrapperForAlfredV2 from '@common/ModalWrapperForAlfredV2';
+import { CopilotProvider } from '@components/Tour/contexts/CopilotProvider';
+import FiltersContainer from '@components/screens/allCases/allCasesFilters/FiltersContainer';
+import { clearBottomSheet } from '@components/utlis/DeviceUtils';
+
+const MapView = () => {
+ const { filteredCasesList, isFilterApplied } = useFilteredCases();
+ const [searchState, setSearchState] = useState(SearchState.HIDDEN);
+ const [showFilterModal, setShowFilterModal] = useState(false);
+ const existingSearchQuery = useAppSelector((state) => state.allCases?.allCasesViewSearchQuery);
+
+ const isSearchVisible = searchState !== SearchState.HIDDEN;
+ const isSearchFocused = searchState === SearchState.FOCUSED;
+
+ const toggleSearch = useCallback(() => {
+ addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_MAP_SEARCH_BUTTON_CLICKED, {
+ searchState,
+ });
+ setSearchState(searchState === SearchState.HIDDEN ? SearchState.FOCUSED : SearchState.HIDDEN);
+ }, [searchState]);
+
+ const toggleFilterModal = useCallback(() => {
+ Keyboard.dismiss();
+ addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_MAP_FILTER_BUTTON_CLICKED);
+ setShowFilterModal(!showFilterModal);
+ }, [showFilterModal]);
+
+ useEffect(() => {
+ addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_MAP_SCREEN_LOADED);
+ if (existingSearchQuery?.length > 0) {
+ setSearchState(SearchState.VISIBLE); //To show search bar when previous search query is present as toggle is setting show search true
+ }
+ }, []);
+
+ return (
+
+
+
+ }
+ />
+
+
+ {isSearchVisible ? (
+
+ ) : null}
+
+
+ {
+ setShowFilterModal((prev) => !prev);
+ clearBottomSheet();
+ }}
+ isVisitPlan={false}
+ isAgentDashboard={false}
+ />
+
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ backgroundColor: COLORS.BACKGROUND.SILVER_LIGHT,
+ flex: 1,
+ },
+ mainContainerStyle: {
+ zIndex: SEARCH_MAIN_CONTAINER_Z_INDEX,
+ },
+});
+
+export default MapView;
diff --git a/src/screens/MapView/utils.tsx b/src/screens/MapView/utils.tsx
new file mode 100644
index 00000000..a0cc96b8
--- /dev/null
+++ b/src/screens/MapView/utils.tsx
@@ -0,0 +1,123 @@
+import React from 'react';
+import DefaultMap from '@assets/icons/DefaultMap';
+import FreeMap from '@assets/icons/FreeMap';
+import LocationMap from '@assets/icons/LocationMap';
+import { PaymentStatus } from '@screens/caseDetails/interface';
+import { COLORS } from '@rn-ui-lib/colors';
+import { shortNumberNotation } from '@components/utlis/commonFunctions';
+import { ILocationData } from '@screens/addresses/interfaces';
+import { useAppSelector } from '@hooks';
+import { IUserRole } from '@reducers/userSlice';
+import { LitmusExperimentName, LitmusExperimentNameMap } from '@services/litmusExperiments.service';
+
+export const getMarkerColors = (
+ paymentStatus: PaymentStatus,
+ isVisited: boolean,
+ isSelected: boolean,
+ isPTP: boolean
+) => {
+ if (isSelected) {
+ return {
+ backgroundColor: '#00952A',
+ borderColor: COLORS.BACKGROUND.PRIMARY,
+ textColor: COLORS.BACKGROUND.PRIMARY,
+ };
+ }
+ if (
+ paymentStatus === PaymentStatus.Paid ||
+ paymentStatus === PaymentStatus['Partially Paid'] ||
+ isPTP
+ ) {
+ return {
+ backgroundColor: '#00952A',
+ borderColor: COLORS.BACKGROUND.PRIMARY,
+ textColor: COLORS.BACKGROUND.PRIMARY,
+ };
+ } else if (isVisited) {
+ return {
+ backgroundColor: COLORS.BACKGROUND.SECONDARY,
+ borderColor: COLORS.TEXT.GREY,
+ textColor: COLORS.BACKGROUND.PRIMARY,
+ };
+ }
+ return {
+ backgroundColor: COLORS.TEXT.GREY_2,
+ borderColor: COLORS.TEXT.GREY,
+ textColor: COLORS.TEXT.GREY_32465B,
+ };
+};
+
+export enum IconStates {
+ DEFAULT,
+ FREE,
+ LOCATION,
+}
+
+export const getIcon = (iconState: IconStates) => {
+ switch (iconState) {
+ case IconStates.DEFAULT:
+ return ;
+ case IconStates.FREE:
+ return ;
+ case IconStates.LOCATION:
+ return ;
+ default:
+ return ;
+ }
+};
+
+export const getMarkerText = (
+ paymentStatus: PaymentStatus,
+ isPTP: boolean,
+ totalOverdueAmount = 0
+) => {
+ if (paymentStatus === PaymentStatus.Paid) {
+ return 'Paid';
+ }
+ if (paymentStatus === PaymentStatus['Partially Paid']) {
+ return 'P.Paid';
+ }
+ if (isPTP) {
+ return 'PTP';
+ }
+ return '₹' + shortNumberNotation(totalOverdueAmount, 1);
+};
+
+export const getPinColors = (address: ILocationData, selectedPin: string | null) => {
+ // If address is selected
+ if (address?.referenceId === selectedPin) {
+ return {
+ textColor: COLORS.TEXT.WHITE,
+ stroke: COLORS.BORDER.BLUE_CC,
+ circleColor: COLORS.TEXT.BLUE_DARK,
+ };
+ }
+
+ // visited pins
+ if (address?.visited) {
+ return {
+ stroke: COLORS.TEXT.GREY,
+ textColor: COLORS.TEXT.WHITE,
+ circleColor: COLORS.BACKGROUND.SECONDARY,
+ };
+ }
+
+ // Default unvisited state
+ return {
+ stroke: COLORS.TEXT.GREY,
+ textColor: COLORS.BACKGROUND.SECONDARY,
+ circleColor: COLORS.TEXT.WHITE,
+ };
+};
+
+export const isMapViewVisible = () => {
+ const user = useAppSelector((state) => state.user);
+ const litmusExperiments = useAppSelector((state) => state.litmusExperiment);
+ const roles = user?.agentRoles;
+ const isFieldAgent = roles?.includes(IUserRole.ROLE_FIELD_AGENT);
+ const isMapViewExperimentEnabled =
+ litmusExperiments?.[LitmusExperimentNameMap[LitmusExperimentName.FIELD_COLLECTIONS_MAP_VIEW]] ||
+ false;
+
+ return isFieldAgent && roles?.length === 1 && isMapViewExperimentEnabled;
+};
diff --git a/src/screens/addresses/actions.ts b/src/screens/addresses/actions.ts
index 033ead00..8e21a618 100644
--- a/src/screens/addresses/actions.ts
+++ b/src/screens/addresses/actions.ts
@@ -2,13 +2,13 @@ import axiosInstance, { ApiKeys, getApiUrl } from '@components/utlis/apiHelper';
import {
setFeedbackAddresses,
setFeedbackAddressesLoading,
- setOtherAddresses,
- setOtherAddressesLoading,
+ setMapAddresses,
+ setMapAddressesLoading,
setTopAddresses,
setTopAddressesLoading,
} from '@reducers/topAddressesSlice';
import { AppDispatch } from '@store';
-import { PAGE_END } from './constants';
+import { PAGE_END, PAGE_START } from './constants';
import { ICaseItemLatLongData } from '@screens/allCases/interface';
import store from '@store';
import { getGeolocationDistance } from '@screens/allCases/allCasesActions';
@@ -84,30 +84,72 @@ export const getTopAddresses = (caseId: string, start: number) => async (dispatc
}
};
-export const getOtherAddresses = (caseId: string) => (dispatch: AppDispatch) => {
- dispatch(setOtherAddressesLoading({ caseId, isLoading: true }));
+export const getMapAddresses = (caseId: string) => async (dispatch: AppDispatch) => {
+ dispatch(setMapAddressesLoading({ caseId, isLoading: true }));
const url = getApiUrl(ApiKeys.GET_TOP_ADDRESSES);
- axiosInstance
- .get(url, {
+ try {
+ const response = await axiosInstance.get(url, {
params: {
caseReferenceId: caseId,
- startingRank: PAGE_END + 1,
+ startingRank: PAGE_START,
+ endingRank: PAGE_END,
},
- })
- .then((res) => {
- if (res?.data) {
- const { unifiedLocations = [] } = res?.data || {};
- dispatch(
- setOtherAddresses({
- caseId,
- addresses: unifiedLocations,
- })
- );
- }
- })
- .finally(() => {
- dispatch(setOtherAddressesLoading({ caseId, isLoading: false }));
});
+
+ if (response?.data) {
+ const { unifiedLocations = [] } = response.data || {};
+ dispatch(
+ setMapAddresses({
+ caseId,
+ addresses: unifiedLocations,
+ })
+ );
+
+ const deviceGeolocationCoordinate =
+ store?.getState()?.foregroundService?.deviceGeolocationCoordinate || {};
+ const agentId = store?.getState()?.user?.user?.referenceId!;
+ const source = {
+ id: agentId,
+ latitude: deviceGeolocationCoordinate?.latitude,
+ longitude: deviceGeolocationCoordinate?.longitude,
+ };
+
+ const destinations: ICaseItemLatLongData[] = [];
+ unifiedLocations?.forEach((location: ILocationData) => {
+ destinations.push({
+ id: location?.referenceId,
+ latitude: location?.latitude,
+ longitude: location?.longitude,
+ });
+ });
+
+ let addressDistanceMap: Map = new Map();
+ if (destinations.length > 0) {
+ addressDistanceMap = await getGeolocationDistance({
+ source,
+ destinations,
+ calculationType: DistanceCalculationType.GRAPHHOPPER,
+ fallbackType: DistanceCalculationType.HAVERSINE,
+ });
+
+ if (!addressDistanceMap || addressDistanceMap?.size === 0) {
+ destinations?.forEach((destination) => {
+ const distanceInKm = getDistanceFromLatLonInKm(
+ destination,
+ deviceGeolocationCoordinate
+ );
+ if (distanceInKm) {
+ addressDistanceMap?.set(destination?.id, distanceInKm);
+ }
+ });
+ }
+ dispatch(setAddressToDistanceMap(addressDistanceMap));
+ }
+ }
+ } catch (error) {
+ } finally {
+ dispatch(setMapAddressesLoading({ caseId, isLoading: false }));
+ }
};
export const getFeedbackAddresses = (caseId: string) => (dispatch: AppDispatch) => {
diff --git a/src/screens/addresses/common/AddressItemHeader.tsx b/src/screens/addresses/common/AddressItemHeader.tsx
index 47b968e5..2eda2f3c 100644
--- a/src/screens/addresses/common/AddressItemHeader.tsx
+++ b/src/screens/addresses/common/AddressItemHeader.tsx
@@ -127,7 +127,7 @@ const styles = StyleSheet.create({
paddingVertical: 8,
},
rankCircle: {
- marginLeft: 21,
+ marginLeft: 18,
width: 23,
height: 23,
borderRadius: 13,
diff --git a/src/screens/addresses/interfaces.ts b/src/screens/addresses/interfaces.ts
index 400ec36c..86ee9100 100644
--- a/src/screens/addresses/interfaces.ts
+++ b/src/screens/addresses/interfaces.ts
@@ -145,6 +145,7 @@ export interface ITopAddress {
params: {
loanAccountNumber: string;
caseId: string;
+ scrollToIndex?: number;
};
};
}
diff --git a/src/screens/addresses/otherAddresses/OtherAddressList.tsx b/src/screens/addresses/otherAddresses/OtherAddressList.tsx
index a0230811..cb90aefe 100644
--- a/src/screens/addresses/otherAddresses/OtherAddressList.tsx
+++ b/src/screens/addresses/otherAddresses/OtherAddressList.tsx
@@ -1,29 +1,24 @@
-import React from 'react';
+import React, { useEffect, useRef } from 'react';
import { ActivityIndicator, FlatList, RefreshControl, View } from 'react-native';
import OtherAddressItem from './OtherAddressItem';
import { GenericStyles } from '@rn-ui-lib/styles';
import Text from '@rn-ui-lib/components/Text';
import { useAppDispatch, useAppSelector } from '@hooks';
import { COLORS } from '@rn-ui-lib/colors';
-import { getOtherAddresses } from '../actions';
import { IOtherAddressList } from '../interfaces';
const OtherAddressList = (props: IOtherAddressList) => {
const { caseId } = props;
- const otherAddresses = useAppSelector(
- (state) => state.topAddresses?.[caseId]?.otherAddresses || []
- );
- const isOtherAddressesLoading = useAppSelector(
- (state) => state.topAddresses?.[caseId]?.isOtherAddressesLoading
+ const mapAddresses = useAppSelector((state) => state.topAddresses?.[caseId]?.mapAddresses || []);
+ const isMapAddressesLoading = useAppSelector(
+ (state) => state.topAddresses?.[caseId]?.isMapAddressesLoading
);
const dispatch = useAppDispatch();
- const onRefresh = React.useCallback(() => {
- dispatch(getOtherAddresses(caseId));
- }, []);
+ const onRefresh = React.useCallback(() => {}, []);
- if (isOtherAddressesLoading) {
+ if (isMapAddressesLoading) {
return (
@@ -33,9 +28,9 @@ const OtherAddressList = (props: IOtherAddressList) => {
return (
- {otherAddresses?.length > 0 ? (
+ {mapAddresses?.length > 0 ? (
}
contentContainerStyle={[GenericStyles.p16, GenericStyles.mb24]}
renderItem={({ item }) => }
diff --git a/src/screens/addresses/otherAddresses/OtherAddresses.tsx b/src/screens/addresses/otherAddresses/OtherAddresses.tsx
index 9a6cb56b..88dca9a8 100644
--- a/src/screens/addresses/otherAddresses/OtherAddresses.tsx
+++ b/src/screens/addresses/otherAddresses/OtherAddresses.tsx
@@ -6,7 +6,6 @@ import NavigationHeader from '@rn-ui-lib/components/NavigationHeader';
import { goBack } from '@components/utlis/navigationUtlis';
import { useAppDispatch, useAppSelector } from '@hooks';
import OtherAddressList from './OtherAddressList';
-import { getOtherAddresses } from '../actions';
import { addClickstreamEvent } from '@services/clickstreamEventService';
import { CLICKSTREAM_EVENT_NAMES } from '@common/Constants';
import { IOtherAddresses } from '../interfaces';
@@ -16,7 +15,7 @@ const OtherAddresses = ({ route: routeParams }: IOtherAddresses) => {
params: { caseId },
} = routeParams;
const otherAddresses = useAppSelector(
- (state) => state.topAddresses?.[caseId]?.otherAddresses || []
+ (state) => state.topAddresses?.[caseId]?.mapAddresses || []
);
const dispatch = useAppDispatch();
@@ -25,7 +24,6 @@ const OtherAddresses = ({ route: routeParams }: IOtherAddresses) => {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_OTHER_ADDRESSES_SCREEN_LOADED, {
caseId,
});
- dispatch(getOtherAddresses(caseId));
}, []);
const totalAddresses = otherAddresses?.length;
diff --git a/src/screens/addresses/topAddresses/TopAddresses.tsx b/src/screens/addresses/topAddresses/TopAddresses.tsx
index 2e8c15f0..5dbe1aa2 100644
--- a/src/screens/addresses/topAddresses/TopAddresses.tsx
+++ b/src/screens/addresses/topAddresses/TopAddresses.tsx
@@ -18,7 +18,7 @@ import TopAddressesList from './TopAddressesList';
const TopAddresses = ({ route: routeParams }: ITopAddress) => {
const {
- params: { caseId },
+ params: { caseId, scrollToIndex },
} = routeParams;
const addresses = useAppSelector((state) => state.topAddresses?.[caseId]?.addresses || []);
const customerReferenceId = useAppSelector(
@@ -64,7 +64,7 @@ const TopAddresses = ({ route: routeParams }: ITopAddress) => {
}
/>
-
+
);
diff --git a/src/screens/addresses/topAddresses/TopAddressesList.tsx b/src/screens/addresses/topAddresses/TopAddressesList.tsx
index c6b30cd6..55b4d76b 100644
--- a/src/screens/addresses/topAddresses/TopAddressesList.tsx
+++ b/src/screens/addresses/topAddresses/TopAddressesList.tsx
@@ -1,5 +1,12 @@
-import { ActivityIndicator, FlatList, RefreshControl, StyleSheet, View } from 'react-native';
-import React from 'react';
+import {
+ ActivityIndicator,
+ FlatList,
+ InteractionManager,
+ RefreshControl,
+ StyleSheet,
+ View,
+} from 'react-native';
+import React, { useEffect, useRef } from 'react';
import { GenericStyles } from '@rn-ui-lib/styles';
import { COLORS } from '@rn-ui-lib/colors';
import Text from '@rn-ui-lib/components/Text';
@@ -8,8 +15,14 @@ import { useAppDispatch, useAppSelector } from '@hooks';
import { PAGE_START } from '../constants';
import OtherAddressItem from '../otherAddresses/OtherAddressItem';
-const TopAddressesList = ({ caseId }: any) => {
+interface ITopAddressesList {
+ caseId: string;
+ scrollToIndex: number | null;
+}
+
+const TopAddressesList = ({ caseId, scrollToIndex }: ITopAddressesList) => {
const dispatch = useAppDispatch();
+ const flatListRef = useRef(null);
const isTopAddressesLoading = useAppSelector(
(state) => state.topAddresses?.[caseId]?.isTopAddressesLoading
@@ -19,6 +32,19 @@ const TopAddressesList = ({ caseId }: any) => {
const onRefresh = React.useCallback(() => {
dispatch(getTopAddresses(caseId, PAGE_START));
}, []);
+
+ useEffect(() => {
+ if (scrollToIndex != null && !isTopAddressesLoading) {
+ InteractionManager.runAfterInteractions(() => {
+ flatListRef.current?.scrollToIndex({
+ index: scrollToIndex - 1,
+ animated: true,
+ viewOffset: 16,
+ });
+ });
+ }
+ }, [scrollToIndex, isTopAddressesLoading]);
+
if (isTopAddressesLoading) {
return (
@@ -31,6 +57,7 @@ const TopAddressesList = ({ caseId }: any) => {
{addresses?.length > 0 ? (
}
contentContainerStyle={[GenericStyles.p16, GenericStyles.mb24]}
diff --git a/src/screens/allCases/CaseItem/FeedbackStatus.tsx b/src/screens/allCases/CaseItem/FeedbackStatus.tsx
index a152e90b..d516cd59 100644
--- a/src/screens/allCases/CaseItem/FeedbackStatus.tsx
+++ b/src/screens/allCases/CaseItem/FeedbackStatus.tsx
@@ -8,20 +8,22 @@ import { IFeedbackStatus } from '../interface';
import { feedbackStatusColorMapping } from '../utils';
const FeedbackStatus = (props: IFeedbackStatus) => {
- const { caseListItemDetailObj } = props;
+ const { caseListItemDetailObj, isMapView = true } = props;
const { currentMonthCaseInteractionStatus, lastMonthCaseInteractionStatus } =
caseListItemDetailObj || {};
return (
-
+ {!isMapView && }
-
+
Current month
{
-
+
Last month
{
const openCommitmentScreen = () => {
dispatch(setCommitmentOpenBottomSheet(true));
};
+
return (
<>
+ {isMapViewVisible() ? (
+
+
+
+ ) : null}
{shouldShowBanner ? (
;
isNearByCase?: boolean;
+ customTextStyle?: StyleProp;
+ customSubTextStyle?: StyleProp;
}
const EmptyList: React.FC = (props) => {
@@ -34,6 +36,8 @@ const EmptyList: React.FC = (props) => {
setShowAgentSelectionBottomSheet,
isAgentDashboard,
containerStyle,
+ customTextStyle,
+ customSubTextStyle,
isNearByCase,
} = props;
const {
@@ -144,12 +148,16 @@ const EmptyList: React.FC = (props) => {
dark
bold
type="h3"
- style={[GenericStyles.mt16, GenericStyles.centerAlignedText]}
+ style={[GenericStyles.mt16, GenericStyles.centerAlignedText, customTextStyle]}
>
{message}
{subMessage && !isAgentDashboard ? (
-
+
{subMessage}
) : null}
diff --git a/src/screens/allCases/interface.ts b/src/screens/allCases/interface.ts
index b5774ff7..4a148627 100644
--- a/src/screens/allCases/interface.ts
+++ b/src/screens/allCases/interface.ts
@@ -75,7 +75,7 @@ export enum CaseStatuses {
CLOSED = 'CLOSED', // if any of the task become verif success
FORCE_CLOSED = 'FORCE_CLOSED', // backend force closes the task
EXPIRED = 'EXPIRED', // unattended case for 30 days.
- ON_HOLD='ON_HOLD', // case paused
+ ON_HOLD = 'ON_HOLD', // case paused
}
export enum CaseStatusUIMapping {
@@ -86,7 +86,7 @@ export enum CaseStatusUIMapping {
CLOSED = 'Closed',
FORCE_CLOSED = 'Force closed',
EXPIRED = 'Expired',
- ON_HOLD = 'Paused'
+ ON_HOLD = 'Paused',
}
export enum CaseType {
@@ -381,8 +381,9 @@ export interface ICaseStatus {
isVisitPlan?: boolean;
}
-export interface IFeedbackStatus{
+export interface IFeedbackStatus {
caseListItemDetailObj: ICaseItemCaseDetailObj;
+ isMapView?: boolean;
}
export interface ICaseItemLatLongData {
diff --git a/src/screens/allCases/utils.ts b/src/screens/allCases/utils.ts
index 05f6c9f0..218cb4cc 100644
--- a/src/screens/allCases/utils.ts
+++ b/src/screens/allCases/utils.ts
@@ -3,19 +3,13 @@ import { getDistanceFromLatLonInKm, isFunction } from '@components/utlis/commonF
import { IGeoLocation, IGeolocationCoordinate } from '@interfaces/addressGeolocation.types';
import { addClickstreamEvent } from '@services/clickstreamEventService';
import {
- Address,
CaseDetail,
FeedbackStatus,
IDocumentItem,
INearbyCaseItemObj,
PaymentStatus,
} from '../caseDetails/interface';
-import {
- BOTTOM_TAB_ROUTES,
- NEARBY_CASES_COUNT,
- NEARBY_CASES_THRESHOLD_DISTANCE,
- ToastMessages,
-} from './constants';
+import { BOTTOM_TAB_ROUTES, NEARBY_CASES_COUNT, ToastMessages } from './constants';
import { ICaseItem, ICaseItemLatLongData, IReportee, ISectionListData } from './interface';
import store, { AppDispatch } from '@store';
import {
@@ -31,6 +25,7 @@ import { useWindowDimensions } from 'react-native';
import { toast } from '@rn-ui-lib/components/toast';
import { TagVariant } from '@rn-ui-lib/components/Tag';
import { COLORS } from '@rn-ui-lib/colors';
+import { getNearByCasesThresholdDistance } from '@common/AgentActivityConfigurableConstants';
import { getGeolocationDistance } from './allCasesActions';
export const getAttemptedList = (
@@ -157,7 +152,7 @@ const handleCheckShowPullToRefreshNearbyCases = (
nearbyCasesList?.length > 0 &&
!isNaN(distanceBetweenNearbyCasesListUpdatedLocationAndCurrentLocation) &&
distanceBetweenNearbyCasesListUpdatedLocationAndCurrentLocation >=
- NEARBY_CASES_THRESHOLD_DISTANCE
+ getNearByCasesThresholdDistance()
);
};
export const handleCheckAndUpdatePullToRefreshStateForNearbyCases = () => {
@@ -215,7 +210,7 @@ export const updateNearbyCasesListAndLocation = async (
source,
destinations,
calculationType: DistanceCalculationType.GRAPHHOPPER,
- fallbackType: DistanceCalculationType.HAVERSINE
+ fallbackType: DistanceCalculationType.HAVERSINE,
});
const casesListCopy = [...allCasesList];
if (
@@ -366,10 +361,10 @@ export const paymentStatusMapping: Record<
[PaymentStatus.Paid]: { label: PaymentStatus.Paid, variant: TagVariant.success },
[PaymentStatus['Partially Paid']]: {
label: PaymentStatus['Partially Paid'],
- variant: TagVariant.yellow,
+ variant: TagVariant.success,
},
- [PaymentStatus.Unpaid]: { label: PaymentStatus.Unpaid, variant: TagVariant.alert },
- [PaymentStatus.Closed]: { label: PaymentStatus.Closed, variant: TagVariant.error },
+ [PaymentStatus.Unpaid]: { label: PaymentStatus.Unpaid, variant: TagVariant.error },
+ [PaymentStatus.Closed]: { label: PaymentStatus.Closed, variant: TagVariant.success },
};
export const feedbackStatusColorMapping = {
diff --git a/src/screens/auth/ProtectedRouter.tsx b/src/screens/auth/ProtectedRouter.tsx
index 72635662..76ee0547 100644
--- a/src/screens/auth/ProtectedRouter.tsx
+++ b/src/screens/auth/ProtectedRouter.tsx
@@ -5,7 +5,11 @@ import {
import React, { useEffect } from 'react';
import { _map, MILLISECONDS_IN_A_MINUTE } from '../../../RN-UI-LIB/src/utlis/common';
import { getNotifications, notificationAction } from '../../action/notificationActions';
-import { LocalStorageKeys, SCREEN_ANIMATION_DURATION } from '../../common/Constants';
+import {
+ CLICKSTREAM_EVENT_NAMES,
+ LocalStorageKeys,
+ SCREEN_ANIMATION_DURATION,
+} from '../../common/Constants';
import {
getScreenFocusListenerObj,
setAsyncStorageItem,
@@ -17,7 +21,7 @@ import interactionsHandler from '../caseDetails/interactionsHandler';
import ImpersonatedUser from '../impersonatedUser';
import Notifications from '../notifications';
import TodoList from '../todoList/TodoList';
-import { getAgentDetail } from '../../action/authActions';
+import { getAgentBaseLocation, getAgentDetail } from '../../action/authActions';
import NearbyCases from '@screens/allCases/NearbyCases';
import CallingAgentRoutes from '../../miniModules/callingAgents/routes';
import usePolling from '@hooks/usePolling';
@@ -31,6 +35,9 @@ import getLitmusExperimentResult, {
LitmusExperimentNameMap,
} from '@services/litmusExperiments.service';
import { GLOBAL } from '@constants/Global';
+import MapViewStack from '@screens/MapView/MapViewStack';
+import { setLitmusExperimentResult } from '@reducers/litmusExperimentSlice';
+import { addClickstreamEvent } from '@services/clickstreamEventService';
const Stack = createNativeStackNavigator();
@@ -43,6 +50,7 @@ export enum PageRouteEnum {
IMPERSONATED_LOGIN = 'ImpersonatedUserLogin',
NEARBY_CASES = 'nearbyCases',
AGENT_ID_CARD = 'AgentIdCard',
+ MAP_VIEW_STACK = 'mapViewStack',
}
export const DEFAULT_SCREEN_OPTIONS: NativeStackNavigationOptions = {
@@ -65,6 +73,7 @@ const ProtectedRouter = () => {
if (isOnline) {
dispatch(getNotifications());
dispatch(getAgentDetail());
+ dispatch(getAgentBaseLocation());
if (isFieldApp()) {
dispatch(getSelfieDocument());
getLitmusExperimentResult(
@@ -75,6 +84,17 @@ const ProtectedRouter = () => {
).then((response) => {
setAsyncStorageItem(LocalStorageKeys.PROVIDE_AGENT_WITH_OPEN_MAP_OPTIONS, response);
});
+
+ getLitmusExperimentResult(LitmusExperimentName.FIELD_COLLECTIONS_MAP_VIEW, {
+ 'x-customer-id': GLOBAL.AGENT_ID,
+ }).then((response) => {
+ dispatch(
+ setLitmusExperimentResult({
+ experimentName: LitmusExperimentName.FIELD_COLLECTIONS_MAP_VIEW,
+ result: response,
+ })
+ );
+ });
}
}
}, [isOnline]);
@@ -102,6 +122,7 @@ const ProtectedRouter = () => {
<>
+
{
@@ -126,6 +128,7 @@ const CaseDetailStack = () => {
))}
+
);
diff --git a/src/screens/caseDetails/ViewAddressSection.tsx b/src/screens/caseDetails/ViewAddressSection.tsx
index d1073267..00918e49 100644
--- a/src/screens/caseDetails/ViewAddressSection.tsx
+++ b/src/screens/caseDetails/ViewAddressSection.tsx
@@ -1,10 +1,8 @@
import { CLICKSTREAM_EVENT_NAMES } from '@common/Constants';
-import { getLoanAccountNumber } from '@components/utlis/commonFunctions';
import { navigateToScreen } from '@components/utlis/navigationUtlis';
import { useAppSelector } from '@hooks';
import Button from '@rn-ui-lib/components/Button';
import Text from '@rn-ui-lib/components/Text';
-import Chevron from '@rn-ui-lib/icons/Chevron';
import { GenericStyles, getShadowStyle } from '@rn-ui-lib/styles';
import { addClickstreamEvent } from '@services/clickstreamEventService';
import { RootState } from '@store';
@@ -15,6 +13,9 @@ import { AddressGeolocationTabEnum, AddressTabType } from '@screens/addressGeolo
import { CaseStatuses } from '@screens/allCases/interface';
import LollipopIcon from '@assets/icons/LollipopIcon';
import AllocatedAddressDetails from './AllocatedAddressDetails';
+import OpenMapIcon from '@assets/icons/OpenMapIcon';
+import Chevron from '@rn-ui-lib/icons/Chevron';
+import { isMapViewVisible } from '@screens/MapView/utils';
interface IViewAddressSection {
caseId: string;
@@ -63,6 +64,12 @@ const ViewAddressSection = ({ caseId, roadDistance }: IViewAddressSection) => {
});
};
+ const openMapView = () => {
+ navigateToScreen(CaseDetailStackEnum.MAP_VIEW_TOP_ADDRESS, {
+ caseId,
+ });
+ };
+
return (
{
{addressString}
-
- }
- underlayColor="transparent"
- pressableWidthChange={false}
- opacityChangeOnPress={true}
- disabled={isCasePaused}
- />
+
+
+ }
+ underlayColor="transparent"
+ pressableWidthChange={false}
+ opacityChangeOnPress={true}
+ disabled={isCasePaused}
+ />
+ {isMapViewVisible() && (
+ }
+ pressableWidthChange={false}
+ opacityChangeOnPress={true}
+ disabled={isCasePaused}
+ />
+ )}
+
diff --git a/src/screens/caseDetails/interface.ts b/src/screens/caseDetails/interface.ts
index 9ef9e596..39301afe 100644
--- a/src/screens/caseDetails/interface.ts
+++ b/src/screens/caseDetails/interface.ts
@@ -234,7 +234,7 @@ export enum GeolocationSource {
export enum VisitType {
GEOLOCATION = 'GEOLOCATION',
ADDRESS = 'ADDRESS',
- SKIP_TRACING = 'SKIP_TRACING',
+ SKIP_TRACING = 'SKIP_TRACING'
}
export interface IGeolocation {
@@ -267,6 +267,18 @@ export interface EmploymentDetails {
export interface FeedbackStatusObj {
status: string;
color: string;
+ statusValue: string;
+}
+
+export interface CurrentAllocationCycleStats {
+ amountCollected: number;
+ atLeastOneEmiCollected: string;
+ caseVisited: string;
+ contactable: string;
+ nonContactable: string;
+ ptp: string;
+ ptpBroken: string;
+ ptpConverted: string;
}
export interface CaseDetail {
@@ -295,10 +307,12 @@ export interface CaseDetail {
addresses?: Address[];
addressLocation: IGeolocationCoordinate;
currentAllocationReferenceId: string;
+ currentAllocationCycleStats: CurrentAllocationCycleStats;
customerReferenceId: string;
caseViewCreatedAt?: number;
// collection case
addressString?: string;
+ addressReferenceId?: string;
currentOutstandingEmi?: number;
totalOverdueEmis?: number;
fatherName?: string;
@@ -329,6 +343,7 @@ export interface CaseDetail {
daysTillDeallocation: number;
businessVertical: string;
pausedTillDate?: string;
+ currentPinCode?: string;
}
export interface recentEscalationDetails {
diff --git a/src/screens/caseDetails/utils/postOperationalHourActions.tsx b/src/screens/caseDetails/utils/postOperationalHourActions.tsx
index bbb0e86a..ea149f09 100644
--- a/src/screens/caseDetails/utils/postOperationalHourActions.tsx
+++ b/src/screens/caseDetails/utils/postOperationalHourActions.tsx
@@ -1,12 +1,14 @@
-export const handlePostOperativeHourActivity = (timestamp: number)=> {
+import { ENV } from '@constants/config';
+
+export const handlePostOperativeHourActivity = (timestamp: number) => {
+ if (ENV === 'qa') return false;
if (timestamp) {
const todaysDate = new Date(timestamp);
const loginStartTime = new Date(todaysDate).setHours(8, 0, 0, 0);
const loginEndTime = new Date(todaysDate).setHours(18, 55, 0, 0);
const currentTime = todaysDate.getTime();
- const isPostOperational = currentTime < loginStartTime || currentTime > loginEndTime;
+ const isPostOperational = currentTime < loginStartTime || currentTime > loginEndTime;
return isPostOperational;
}
return false;
};
-
diff --git a/src/services/firebaseFetchAndUpdate.service.ts b/src/services/firebaseFetchAndUpdate.service.ts
index 22da7b0d..4824bba2 100644
--- a/src/services/firebaseFetchAndUpdate.service.ts
+++ b/src/services/firebaseFetchAndUpdate.service.ts
@@ -9,77 +9,105 @@ import {
setDataSyncJobIntervalInMinutes,
setImageUploadJobIntervalInMinutes,
setVideoUploadJobIntervalInMinutes,
- setWifiDetailsUploadJobIntervalInMinutes, setExotelNumber, setDialerAppConfig,
+ setWifiDetailsUploadJobIntervalInMinutes,
+ setExotelNumber,
+ setDialerAppConfig,
+ setNearByCasesGeolocationCheckIntervalInMinutes,
+ setGeoLocationIntervalInMinutes,
+ setNearByCasesThresholdDistance,
} from '../common/AgentActivityConfigurableConstants';
import { setBlacklistedAppsList } from './blacklistedApps.service';
-
const FIREBASE_FETCH_TIME = 15 * 60;
export let FIREBASE_FETCH_TIMESTAMP: number;
async function fetchUpdatedRemoteConfig() {
await remoteConfig().fetch(FIREBASE_FETCH_TIME); //15 minutes
remoteConfig()
- .activate()
- .then((fetchedRemotely) => {
- if (fetchedRemotely) {
- console.log('Configs were fetched.');
- } else {
- console.log('No configs were fetched.');
- }
- })
- .catch((error) => {
- console.error(error);
- })
- .finally(() => {
- const ACTIVITY_TIME_ON_APP = remoteConfig().getValue('ACTIVITY_TIME_ON_APP').asNumber();
- const ACTIVITY_TIME_WINDOW_HIGH = remoteConfig()
- .getValue('ACTIVITY_TIME_WINDOW_HIGH')
- .asNumber();
- const ACTIVITY_TIME_WINDOW_MEDIUM = remoteConfig()
- .getValue('ACTIVITY_TIME_WINDOW_MEDIUM')
- .asNumber();
- const BLACKLISTED_APPS = remoteConfig().getValue('BLACKLISTED_APPS').asString();
- const FIRESTORE_RESYNC_INTERVAL_IN_MINUTES = remoteConfig()
- .getValue('FIRESTORE_RESYNC_INTERVAL_IN_MINUTES')
- .asNumber();
- const DATA_SYNC_JOB_INTERVAL_IN_MINUTES = remoteConfig()
- .getValue('DATA_SYNC_JOB_INTERVAL_IN_MINUTES')
- .asNumber();
- const IMAGE_UPLOAD_JOB_INTERVAL_IN_MINUTES = remoteConfig()
- .getValue('IMAGE_UPLOAD_JOB_INTERVAL_IN_MINUTES')
- .asNumber();
- const VIDEO_UPLOAD_JOB_INTERVAL_IN_MINUTES = remoteConfig()
- .getValue('VIDEO_UPLOAD_JOB_INTERVAL_IN_MINUTES')
- .asNumber();
- const AUDIO_UPLOAD_JOB_INTERVAL_IN_MINUTES = remoteConfig()
- .getValue('AUDIO_UPLOAD_JOB_INTERVAL_IN_MINUTES')
- .asNumber();
- const CALENDAR_AND_ACCOUNTS_UPLOAD_JOB_INTERVAL_IN_MINUTES = remoteConfig()
- .getValue('CALENDAR_AND_ACCOUNTS_UPLOAD_JOB_INTERVAL_IN_MINUTES')
- .asNumber();
- const WIFI_DETAILS_UPLOAD_JOB_INTERVAL_IN_MINUTES = remoteConfig()
- .getValue('WIFI_DETAILS_UPLOAD_JOB_INTERVAL_IN_MINUTES')
- .asNumber();
- const EXOTEL_NUMBER = remoteConfig().getValue('EXOTEL_NUMBER').asString();
- const DIALER_APP_CONFIG = remoteConfig().getValue('DIALER_APP_CONFIG').asString();
- setActivityTimeOnApp(ACTIVITY_TIME_ON_APP);
- setActivityTimeWindowHigh(ACTIVITY_TIME_WINDOW_HIGH);
- setActivityTimeWindowMedium(ACTIVITY_TIME_WINDOW_MEDIUM);
- setBlacklistedAppsList(BLACKLISTED_APPS);
- setFirestoreResyncIntervalInMinutes(FIRESTORE_RESYNC_INTERVAL_IN_MINUTES);
- setDialerAppConfig(DIALER_APP_CONFIG);
+ .activate()
+ .then((fetchedRemotely) => {
+ if (fetchedRemotely) {
+ console.log('Configs were fetched.');
+ } else {
+ console.log('No configs were fetched.');
+ }
+ })
+ .catch((error) => {
+ console.error(error);
+ })
+ .finally(() => {
+ const ACTIVITY_TIME_ON_APP = remoteConfig().getValue('ACTIVITY_TIME_ON_APP').asNumber();
+ const ACTIVITY_TIME_WINDOW_HIGH = remoteConfig()
+ .getValue('ACTIVITY_TIME_WINDOW_HIGH')
+ .asNumber();
+ const ACTIVITY_TIME_WINDOW_MEDIUM = remoteConfig()
+ .getValue('ACTIVITY_TIME_WINDOW_MEDIUM')
+ .asNumber();
+ const BLACKLISTED_APPS = remoteConfig().getValue('BLACKLISTED_APPS').asString();
+ const FIRESTORE_RESYNC_INTERVAL_IN_MINUTES = remoteConfig()
+ .getValue('FIRESTORE_RESYNC_INTERVAL_IN_MINUTES')
+ .asNumber();
+ const DATA_SYNC_JOB_INTERVAL_IN_MINUTES = remoteConfig()
+ .getValue('DATA_SYNC_JOB_INTERVAL_IN_MINUTES')
+ .asNumber();
+ const IMAGE_UPLOAD_JOB_INTERVAL_IN_MINUTES = remoteConfig()
+ .getValue('IMAGE_UPLOAD_JOB_INTERVAL_IN_MINUTES')
+ .asNumber();
+ const VIDEO_UPLOAD_JOB_INTERVAL_IN_MINUTES = remoteConfig()
+ .getValue('VIDEO_UPLOAD_JOB_INTERVAL_IN_MINUTES')
+ .asNumber();
+ const AUDIO_UPLOAD_JOB_INTERVAL_IN_MINUTES = remoteConfig()
+ .getValue('AUDIO_UPLOAD_JOB_INTERVAL_IN_MINUTES')
+ .asNumber();
+ const CALENDAR_AND_ACCOUNTS_UPLOAD_JOB_INTERVAL_IN_MINUTES = remoteConfig()
+ .getValue('CALENDAR_AND_ACCOUNTS_UPLOAD_JOB_INTERVAL_IN_MINUTES')
+ .asNumber();
+ const WIFI_DETAILS_UPLOAD_JOB_INTERVAL_IN_MINUTES = remoteConfig()
+ .getValue('WIFI_DETAILS_UPLOAD_JOB_INTERVAL_IN_MINUTES')
+ .asNumber();
+ const NEARBY_CASES_GEOLOCATION_CHECK_INTERVAL_IN_MINUTES = remoteConfig()
+ .getValue('NEARBY_CASES_GEOLOCATION_CHECK_INTERVAL_IN_MINUTES')
+ .asNumber();
+ const GEOLOCATION_INTERVAL_IN_MINUTES = remoteConfig()
+ .getValue('GEOLOCATION_INTERVAL_IN_MINUTES')
+ .asNumber();
+ const NEARBY_CASES_THRESHOLD_DISTANCE = remoteConfig()
+ .getValue('NEARBY_CASES_THRESHOLD_DISTANCE')
+ .asNumber();
+ const EXOTEL_NUMBER = remoteConfig().getValue('EXOTEL_NUMBER').asString();
+ const DIALER_APP_CONFIG = remoteConfig().getValue('DIALER_APP_CONFIG').asString();
+ setActivityTimeOnApp(ACTIVITY_TIME_ON_APP);
+ setActivityTimeWindowHigh(ACTIVITY_TIME_WINDOW_HIGH);
+ setActivityTimeWindowMedium(ACTIVITY_TIME_WINDOW_MEDIUM);
+ setBlacklistedAppsList(BLACKLISTED_APPS);
+ setDialerAppConfig(DIALER_APP_CONFIG);
- if(DATA_SYNC_JOB_INTERVAL_IN_MINUTES) setDataSyncJobIntervalInMinutes(DATA_SYNC_JOB_INTERVAL_IN_MINUTES);
- if(IMAGE_UPLOAD_JOB_INTERVAL_IN_MINUTES) setImageUploadJobIntervalInMinutes(IMAGE_UPLOAD_JOB_INTERVAL_IN_MINUTES);
- if(VIDEO_UPLOAD_JOB_INTERVAL_IN_MINUTES) setVideoUploadJobIntervalInMinutes(VIDEO_UPLOAD_JOB_INTERVAL_IN_MINUTES);
- if(AUDIO_UPLOAD_JOB_INTERVAL_IN_MINUTES) setAudioUploadJobIntervalInMinutes(AUDIO_UPLOAD_JOB_INTERVAL_IN_MINUTES);
- if(CALENDAR_AND_ACCOUNTS_UPLOAD_JOB_INTERVAL_IN_MINUTES) setCalendarAndAccountsUploadJobIntervalInMinutes(
+ if (FIRESTORE_RESYNC_INTERVAL_IN_MINUTES)
+ setFirestoreResyncIntervalInMinutes(FIRESTORE_RESYNC_INTERVAL_IN_MINUTES);
+ if (DATA_SYNC_JOB_INTERVAL_IN_MINUTES)
+ setDataSyncJobIntervalInMinutes(DATA_SYNC_JOB_INTERVAL_IN_MINUTES);
+ if (IMAGE_UPLOAD_JOB_INTERVAL_IN_MINUTES)
+ setImageUploadJobIntervalInMinutes(IMAGE_UPLOAD_JOB_INTERVAL_IN_MINUTES);
+ if (VIDEO_UPLOAD_JOB_INTERVAL_IN_MINUTES)
+ setVideoUploadJobIntervalInMinutes(VIDEO_UPLOAD_JOB_INTERVAL_IN_MINUTES);
+ if (AUDIO_UPLOAD_JOB_INTERVAL_IN_MINUTES)
+ setAudioUploadJobIntervalInMinutes(AUDIO_UPLOAD_JOB_INTERVAL_IN_MINUTES);
+ if (CALENDAR_AND_ACCOUNTS_UPLOAD_JOB_INTERVAL_IN_MINUTES)
+ setCalendarAndAccountsUploadJobIntervalInMinutes(
CALENDAR_AND_ACCOUNTS_UPLOAD_JOB_INTERVAL_IN_MINUTES
);
- if(WIFI_DETAILS_UPLOAD_JOB_INTERVAL_IN_MINUTES) setWifiDetailsUploadJobIntervalInMinutes(WIFI_DETAILS_UPLOAD_JOB_INTERVAL_IN_MINUTES);
- if(EXOTEL_NUMBER) setExotelNumber(EXOTEL_NUMBER);
- FIREBASE_FETCH_TIMESTAMP = Date.now();
- });
+ if (WIFI_DETAILS_UPLOAD_JOB_INTERVAL_IN_MINUTES)
+ setWifiDetailsUploadJobIntervalInMinutes(WIFI_DETAILS_UPLOAD_JOB_INTERVAL_IN_MINUTES);
+ if (EXOTEL_NUMBER) setExotelNumber(EXOTEL_NUMBER);
+ FIREBASE_FETCH_TIMESTAMP = Date.now();
+ if (NEARBY_CASES_GEOLOCATION_CHECK_INTERVAL_IN_MINUTES)
+ setNearByCasesGeolocationCheckIntervalInMinutes(
+ NEARBY_CASES_GEOLOCATION_CHECK_INTERVAL_IN_MINUTES
+ );
+ if (GEOLOCATION_INTERVAL_IN_MINUTES)
+ setGeoLocationIntervalInMinutes(GEOLOCATION_INTERVAL_IN_MINUTES);
+ if (NEARBY_CASES_THRESHOLD_DISTANCE)
+ setNearByCasesThresholdDistance(NEARBY_CASES_THRESHOLD_DISTANCE);
+ });
}
export default fetchUpdatedRemoteConfig;
diff --git a/src/services/litmusExperiments.service.ts b/src/services/litmusExperiments.service.ts
index 43c25bfb..3a9abd6a 100644
--- a/src/services/litmusExperiments.service.ts
+++ b/src/services/litmusExperiments.service.ts
@@ -11,7 +11,8 @@ export enum LitmusExperimentName {
MS_CLARITY = 'ms_clarity',
ENABLE_IMAGE_GEO_TAGGING = 'enable_image_geotagging',
COSMOS_CASE_COLLECTION_MANAGER = 'cosmos_case_collection_manager',
- PROVIDE_AGENT_WITH_OPEN_MAP_OPTIONS = 'open_map_with_address_or_geocode'
+ PROVIDE_AGENT_WITH_OPEN_MAP_OPTIONS = 'open_map_with_address_or_geocode',
+ FIELD_COLLECTIONS_MAP_VIEW = 'field_collections_map_view',
}
export const LitmusExperimentNameMap = {
@@ -21,7 +22,8 @@ export const LitmusExperimentNameMap = {
[LitmusExperimentName.MS_CLARITY]: 'cosmos_ms_clarity',
[LitmusExperimentName.ENABLE_IMAGE_GEO_TAGGING]: 'enable_image_geotagging',
[LitmusExperimentName.COSMOS_CASE_COLLECTION_MANAGER]: 'cosmos_case_collection_manager',
- [LitmusExperimentName.PROVIDE_AGENT_WITH_OPEN_MAP_OPTIONS]: 'open_map_with_address_or_geocode'
+ [LitmusExperimentName.PROVIDE_AGENT_WITH_OPEN_MAP_OPTIONS]: 'open_map_with_address_or_geocode',
+ [LitmusExperimentName.FIELD_COLLECTIONS_MAP_VIEW]: 'field_collections_map_view',
};
const getLitmusExperimentResult = async (
diff --git a/yarn.lock b/yarn.lock
index bedaff91..23b2933a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3540,6 +3540,11 @@
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.7.tgz#4158d3105276773d5b7695cd4834b1722e4f37a8"
integrity sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==
+"@types/geojson@^7946.0.13":
+ version "7946.0.16"
+ resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.16.tgz#8ebe53d69efada7044454e3305c19017d97ced2a"
+ integrity sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==
+
"@types/graceful-fs@^4.1.2":
version "4.1.5"
resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.5.tgz#21ffba0d98da4350db64891f92a9e5db3cdb4e15"
@@ -9034,6 +9039,13 @@ minimist@^1.2.0, minimist@^1.2.3:
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
+minimisted@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/minimisted/-/minimisted-2.0.1.tgz#d059fb905beecf0774bc3b308468699709805cb1"
+ integrity sha512-1oPjfuLQa2caorJUM8HV8lGgWCc0qqAO1MNv/k05G4qslmsndV/5WdNZrqCiyqiz3wohia2Ij2B7w2Dr7/IyrA==
+ dependencies:
+ minimist "^1.2.5"
+
minipass@^4.2.4:
version "4.2.8"
resolved "https://registry.yarnpkg.com/minipass/-/minipass-4.2.8.tgz#f0010f64393ecfc1d1ccb5f582bcaf45f48e1a3a"
@@ -9044,13 +9056,6 @@ minipass@^4.2.4:
resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707"
integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==
-minimisted@^2.0.0:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/minimisted/-/minimisted-2.0.1.tgz#d059fb905beecf0774bc3b308468699709805cb1"
- integrity sha512-1oPjfuLQa2caorJUM8HV8lGgWCc0qqAO1MNv/k05G4qslmsndV/5WdNZrqCiyqiz3wohia2Ij2B7w2Dr7/IyrA==
- dependencies:
- minimist "^1.2.5"
-
miragejs@0.1.47:
version "0.1.47"
resolved "https://registry.yarnpkg.com/miragejs/-/miragejs-0.1.47.tgz#c4a8dff21adfc0ce3181d78987f11848d74c6869"
@@ -10199,6 +10204,13 @@ react-native-image-picker@4.10.2:
resolved "https://registry.yarnpkg.com/react-native-image-picker/-/react-native-image-picker-4.10.2.tgz#75b356c9eea70c2c4f5c1089f8758e2fa32f88a8"
integrity sha512-3h9PrA1dQ84rVeipzQE4eWTELvflSHNtJZN6rz7NkZyaxo9YZV8H/TswBpHwiS5YWlyu+zlLzSoWVa1opSu7GA==
+react-native-maps@1.13.2:
+ version "1.13.2"
+ resolved "https://registry.yarnpkg.com/react-native-maps/-/react-native-maps-1.13.2.tgz#3dfb3e18b0e77362b8939f11148da629e5bf1d04"
+ integrity sha512-7t2zEqS6VYGdNaZHt08nNJxH2YzYAv0gLNf/RbapD3vCYaLYF+sYV0COkQ5YYEEwqJb5I9OViwXuU9GkJXlVmw==
+ dependencies:
+ "@types/geojson" "^7946.0.13"
+
react-native-mmkv@2.11.0:
version "2.11.0"
resolved "https://registry.yarnpkg.com/react-native-mmkv/-/react-native-mmkv-2.11.0.tgz#51b9985f6a5c09fe9c16d8c1861cc2901856ace1"