From 72a88befb288d26cbedb754f9adb4ebb4f9b6ba3 Mon Sep 17 00:00:00 2001 From: Aman Chaturvedi Date: Wed, 13 Dec 2023 15:55:37 +0530 Subject: [PATCH 01/69] TP-39794 | Geolocation clustering --- .../GeolocationContainer.tsx | 3 - src/screens/addressGeolocation/index.tsx | 123 +++++++++++------- 2 files changed, 78 insertions(+), 48 deletions(-) diff --git a/src/screens/addressGeolocation/GeolocationContainer.tsx b/src/screens/addressGeolocation/GeolocationContainer.tsx index 174b7316..faaaf948 100644 --- a/src/screens/addressGeolocation/GeolocationContainer.tsx +++ b/src/screens/addressGeolocation/GeolocationContainer.tsx @@ -51,9 +51,6 @@ const GeolocationContainer: React.FC = ({ ); return ( - - - = ({ route: routeParams } = routeParams; const [ungroupedAddress, setUngroupedAddress] = useState([]); - + const [selectedTab, setSelectedTab] = useState(AddressGeolocationTabEnum.ADDRESS); const [retryBtnToggle, setRetryBtnToggle] = useState(false); const dispatch = useAppDispatch(); @@ -141,6 +158,12 @@ const AddressGeolocation: React.FC = ({ route: routeParams })(); }, []); + const handleTabChange = (tab: string) => { + if (tab !== selectedTab) { + setSelectedTab(tab); + } + }; + if (!isOnline) { return ( @@ -149,8 +172,14 @@ const AddressGeolocation: React.FC = ({ route: routeParams return ( - + + = ({ route: routeParams } > - - {ungroupedAddress?.length > 0 ? ( - - - - - - - - Additional addresses - - - handleRouting(PageRouteEnum.ADDITIONAL_ADDRESSES, { - fetchUngroupedAddress, - }) - } - > - View all addresses - + {selectedTab === AddressGeolocationTabEnum.ADDRESS ? ( + <> + + {ungroupedAddress?.length > 0 ? ( + + + + + + + + Additional addresses + + + handleRouting(PageRouteEnum.ADDITIONAL_ADDRESSES, { + fetchUngroupedAddress, + }) + } + > + View all addresses + + + - - - ) : null} - User geolocations - + ) : null} + + ) : ( + + )} From 444e3ce6cd3843774e1d13492cc4701ff5cac570 Mon Sep 17 00:00:00 2001 From: Aman Chaturvedi Date: Wed, 10 Jan 2024 14:57:01 +0530 Subject: [PATCH 02/69] TP-39794 | Geolocation clustering --- src/action/addressGeolocationAction.ts | 43 +++-- src/action/authActions.ts | 10 +- .../form/components/GeolocationAddress.tsx | 140 +++++++++----- src/components/utlis/apiHelper.ts | 2 + .../AdditionalGeolocations.tsx | 42 ++++ .../addressGeolocation/AddressContainer.tsx | 2 - .../GeolocationContainer.tsx | 143 +++++++++----- .../addressGeolocation/GeolocationItem.tsx | 182 +++++------------- .../GeolocationTimestamps.tsx | 111 +++++++++++ .../SimilarGeolocations.tsx | 66 +++++++ src/screens/addressGeolocation/index.tsx | 12 +- src/screens/auth/ProtectedRouter.tsx | 24 +++ src/screens/caseDetails/interface.ts | 2 + src/screens/login/index.tsx | 6 - 14 files changed, 533 insertions(+), 252 deletions(-) create mode 100644 src/screens/addressGeolocation/AdditionalGeolocations.tsx create mode 100644 src/screens/addressGeolocation/GeolocationTimestamps.tsx create mode 100644 src/screens/addressGeolocation/SimilarGeolocations.tsx diff --git a/src/action/addressGeolocationAction.ts b/src/action/addressGeolocationAction.ts index 07c230a5..f0cccfee 100644 --- a/src/action/addressGeolocationAction.ts +++ b/src/action/addressGeolocationAction.ts @@ -56,23 +56,26 @@ export const addAddress = }); }; -export const getUngroupedAddress = (loanAccountNumber: string, infoToGet: UnifiedCaseDetailsTypes[]) => { +export const getUngroupedAddress = ( + loanAccountNumber: string, + infoToGet: UnifiedCaseDetailsTypes[] +) => { const queryParams = { ...initialUrlParams }; - for (const key of infoToGet) { - queryParams[key] = true; - } - addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_UNIFIED_ENTITY_REQUESTED, { - lan: loanAccountNumber || '', - requestedEntities: JSON.stringify(infoToGet || []) || '' - }); - const url = getApiUrl(ApiKeys.CASE_UNIFIED_DETAILS_V4, { loanAccountNumber }, queryParams); - return axiosInstance + for (const key of infoToGet) { + queryParams[key] = true; + } + addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_UNIFIED_ENTITY_REQUESTED, { + lan: loanAccountNumber || '', + requestedEntities: JSON.stringify(infoToGet || []) || '', + }); + const url = getApiUrl(ApiKeys.CASE_UNIFIED_DETAILS_V4, { loanAccountNumber }, queryParams); + return axiosInstance .get(url) .then((response) => { if (response.status === API_STATUS_CODE.OK) { const ungroupedAddressesWithFeedbacks: IUngroupedAddressWithFeedbacks = { - ungroupedAddresses: response?.data?.ungroupedAddresses || [], - ungroupedAddressFeedbacks: response?.data?.addressFeedbacks || [] + ungroupedAddresses: response?.data?.ungroupedAddresses || [], + ungroupedAddressFeedbacks: response?.data?.addressFeedbacks || [], }; return ungroupedAddressesWithFeedbacks; } @@ -83,3 +86,19 @@ export const getUngroupedAddress = (loanAccountNumber: string, infoToGet: Unifie throw new Error(err); }); }; + +export const getSimilarGeolocationTimestamps = (geolocationId: string) => { + const url = getApiUrl(ApiKeys.SIMILAR_GEOLOCATION_TIMESTAMPS, { geolocationId }); + return axiosInstance + .get(url) + .then((response) => { + if (response.status === API_STATUS_CODE.OK) { + return response.data; + } + throw response; + }) + .catch((err) => { + logError(err); + throw new Error(err); + }); +}; diff --git a/src/action/authActions.ts b/src/action/authActions.ts index 4d8b2b14..c93edda6 100644 --- a/src/action/authActions.ts +++ b/src/action/authActions.ts @@ -197,9 +197,17 @@ export const handleGoogleLogout = async () => { } }; -export const handleLogout = () => async (dispatch: AppDispatch) => { +const firebaseSignout = async () => { try { await auth().signOut(); + } catch (error) { + logError(error as Error, 'Firebase signout error'); + } +} + +export const handleLogout = () => async (dispatch: AppDispatch) => { + try { + await firebaseSignout(); await handleGoogleLogout(); await clearAllAsyncStorage(); await clearStorageEngine(); diff --git a/src/components/form/components/GeolocationAddress.tsx b/src/components/form/components/GeolocationAddress.tsx index d6e3ac9e..587eae0b 100644 --- a/src/components/form/components/GeolocationAddress.tsx +++ b/src/components/form/components/GeolocationAddress.tsx @@ -17,6 +17,8 @@ import { addClickstreamEvent } from '../../../services/clickstreamEventService'; import { CLICKSTREAM_EVENT_NAMES } from '../../../common/Constants'; import { PageRouteEnum } from '../../../screens/auth/ProtectedRouter'; import { IAddressFeedback } from '../../../reducer/addressSlice'; +import Tag, { TagVariant } from '@rn-ui-lib/components/Tag'; +import ArrowSolidIcon from '@rn-ui-lib/icons/ArrowSolidIcon'; interface IGeolocationAddress { address: IGeolocation; @@ -41,7 +43,15 @@ const GeolocationAddress: React.FC = ({ isFeedbackView, handlePageRouting, }) => { - const { latitude, longitude, capturedTimestamp, tag, id } = address; + const { + latitude, + longitude, + capturedTimestamp, + tag, + id, + clusterCardinality = 0, + clusterLabels = [], + } = address; const { deviceGeolocationCoordinate, prefilledAddressScreenTemplate } = useAppSelector( (state) => ({ deviceGeolocationCoordinate: state.foregroundService?.deviceGeolocationCoordinate, @@ -63,14 +73,18 @@ const GeolocationAddress: React.FC = ({ const isFeedbackPresent = lastFeedbackForGeolocation?.feedbackPresent; - const { addressDate, addressTime } = useMemo(() => { + const { addressDate, addressTime, lastFeedbackTimestampDate } = useMemo(() => { const timestamp = new Date(Number(capturedTimestamp)); + const lastFeedbackTimestamp = new Date( + Number(lastFeedbackForGeolocation?.latestFeedbackTimestamp) + ); if (timestamp.toString() === 'Invalid Date') { - return { addressDate: '', addressTime: '', isFeedbackPresent }; + return { addressDate: '', addressTime: '', lastFeedbackTimestampDate: '' }; } const addressDate = dateFormat(timestamp, 'DD MMM YYYY'); - const addressTime = dateFormat(timestamp, 'hh:mm A'); - return { addressDate, addressTime }; + const addressTime = dateFormat(timestamp, 'HH:mm A'); + const lastFeedbackTimestampDate = dateFormat(lastFeedbackTimestamp, 'DD MMM YYYY'); + return { addressDate, addressTime, lastFeedbackTimestampDate }; }, [lastFeedbackForGeolocation, capturedTimestamp]); const handleCloseRouting = () => handlePageRouting?.(PageRouteEnum.ADDRESS_GEO); @@ -126,52 +140,96 @@ const GeolocationAddress: React.FC = ({ }); }; + const handleSimilarLocationCTA = () => { + navigateToScreen(PageRouteEnum.SIMILAR_GEOLOCATIONS, address); + } + return ( - - - - {tag} + + + + {tag}{' '} + {roundoffRelativeDistanceBwLatLong ? ( + ({roundoffRelativeDistanceBwLatLong}km away) + ) : null} - {roundoffRelativeDistanceBwLatLong ? ( - ({roundoffRelativeDistanceBwLatLong}km away) + {!isFeedbackView ? ( + + + ) : null} {addressDate ? {addressDate} : null} {addressTime ? ( - - {addressTime} + , {addressTime} ) : null} - {!isFeedbackView ? ( - isFeedbackPresent ? ( + {!isFeedbackView && clusterCardinality && clusterCardinality > 1 ? ( + - Last visit:{' '} - - {' '} - {lastFeedbackForGeolocation?.latestFeedbackStatus || ''}{' '} - + Similar locations: {clusterCardinality} {clusterLabels?.length ? 'and ' : ''} - ) : ( - - Last visit:{' '} + {clusterLabels?.length ? ( + + {clusterLabels.map((label: string) => ( + + ))} + + ) : null} + + ) : null} + {!isFeedbackView ? ( + + {isFeedbackPresent ? ( + <> + + Last feedback:{' '} + + {' '} + {lastFeedbackForGeolocation?.latestFeedbackStatus || ''}{' '} + + + {lastFeedbackTimestampDate ? ( + + Visited on:{' '} + + {' '} + {lastFeedbackTimestampDate || ''} + + + ) : null} + + ) : ( Unvisited location - - ) + )} + ) : null} {showOpenMap ? ( @@ -210,17 +268,7 @@ const GeolocationAddress: React.FC = ({ ); }; -export default GeolocationAddress; - const styles = StyleSheet.create({ - circleSeparator: { - width: 4, - height: 4, - borderRadius: 2, - backgroundColor: COLORS.BACKGROUND.GRAY_B1, - marginHorizontal: 8, - marginTop: 2, - }, openMapBtn: { fontSize: 13, lineHeight: 20, @@ -233,3 +281,5 @@ const styles = StyleSheet.create({ fontWeight: 'bold', }, }); + +export default GeolocationAddress; diff --git a/src/components/utlis/apiHelper.ts b/src/components/utlis/apiHelper.ts index 06ab2286..60b33848 100644 --- a/src/components/utlis/apiHelper.ts +++ b/src/components/utlis/apiHelper.ts @@ -67,6 +67,7 @@ export enum ApiKeys { DAILY_COMMITMENT = 'DAILY_COMMITMENT', GET_PTP_AMOUNT = 'GET_PTP_AMOUNT', GET_VISIBILITY_STATUS = 'GET_VISIBILITY_STATUS', + SIMILAR_GEOLOCATION_TIMESTAMPS = 'SIMILAR_GEOLOCATION_TIMESTAMPS', } export const API_URLS: Record = {} as Record; @@ -118,6 +119,7 @@ API_URLS[ApiKeys.GET_CASE_DETAILS_FROM_API] = '/collection-cases/minimal-collect API_URLS[ApiKeys.DAILY_COMMITMENT] = '/daily-commitment'; API_URLS[ApiKeys.GET_PTP_AMOUNT] = '/ptps-due-view/agent-detail'; API_URLS[ApiKeys.GET_VISIBILITY_STATUS] = '/daily-commitment/visibility'; +API_URLS[ApiKeys.SIMILAR_GEOLOCATION_TIMESTAMPS] = '/geolocation-cluster/{geolocationId}/similar-locations-info'; export const API_STATUS_CODE = { OK: 200, diff --git a/src/screens/addressGeolocation/AdditionalGeolocations.tsx b/src/screens/addressGeolocation/AdditionalGeolocations.tsx new file mode 100644 index 00000000..61c62b23 --- /dev/null +++ b/src/screens/addressGeolocation/AdditionalGeolocations.tsx @@ -0,0 +1,42 @@ +import { ScrollView } from 'react-native'; +import React from 'react'; +import Layout from '@screens/layout/Layout'; +import NavigationHeader from '@rn-ui-lib/components/NavigationHeader'; +import { goBack } from '@components/utlis/navigationUtlis'; +import { IGeolocation } from '@screens/caseDetails/interface'; +import GeolocationItem from './GeolocationItem'; +import { GenericFunctionArgs } from '@common/GenericTypes'; + +interface IAdditionalGeolocation { + route: { + params: { + additionalGeolocations: IGeolocation[]; + caseId: string; + loanAccountNumber: string; + handlePageRouting: GenericFunctionArgs; + }; + }; +} + +const AdditionalGeolocations: React.FC = (props) => { + const { params } = props.route || {}; + const { additionalGeolocations, caseId, loanAccountNumber, handlePageRouting } = params || {}; + return ( + + + + {additionalGeolocations?.map((geolocation: IGeolocation, index: number) => ( + + ))} + + + ); +}; + +export default AdditionalGeolocations; diff --git a/src/screens/addressGeolocation/AddressContainer.tsx b/src/screens/addressGeolocation/AddressContainer.tsx index 710684b1..e0c2e28a 100644 --- a/src/screens/addressGeolocation/AddressContainer.tsx +++ b/src/screens/addressGeolocation/AddressContainer.tsx @@ -8,11 +8,9 @@ import { type IAddress, type IGroupedAddressesItem } from '../../types/addressGe import AddressItem from './AddressItem'; import { addClickstreamEvent } from '../../services/clickstreamEventService'; import { CLICKSTREAM_EVENT_NAMES } from '../../common/Constants'; -import GeolocationItem from './GeolocationItem'; import { PageRouteEnum } from '../auth/ProtectedRouter'; import { type GenericFunctionArgs } from '../../common/GenericTypes'; import SimilarAddressItem from './SimilarAddressItem'; -import filterFarAwayMetaAddresses from './utils/FilterFarAwayMetaAddresses'; import filterNearMetaAddresses from './utils/FilterNearMetaAddresses'; import { useAppSelector } from '../../hooks'; import { MAXIMUM_ALLOWED_DISTANCE_FOR_GROUPED_ADDRESSES } from './constants'; diff --git a/src/screens/addressGeolocation/GeolocationContainer.tsx b/src/screens/addressGeolocation/GeolocationContainer.tsx index faaaf948..b09d5712 100644 --- a/src/screens/addressGeolocation/GeolocationContainer.tsx +++ b/src/screens/addressGeolocation/GeolocationContainer.tsx @@ -1,17 +1,20 @@ -import React from 'react'; -import { StyleSheet, View } from 'react-native'; +import React, { useMemo } from 'react'; +import { StyleSheet, TouchableOpacity, View } from 'react-native'; import Text from '../../../RN-UI-LIB/src/components/Text'; import { GenericStyles } from '../../../RN-UI-LIB/src/styles'; import { COLORS } from '../../../RN-UI-LIB/src/styles/colors'; import CustomLocationIcon from '../../assets/icons/CustomLocationIcon'; -import GeolocationAddress from '../../components/form/components/GeolocationAddress'; -import CustomLocationSmallIcon from '../../assets/icons/CustomLocationSmallIcon'; import { IGeolocation } from '../caseDetails/interface'; import { GenericFunctionArgs } from '../../common/GenericTypes'; -import { IGeoLocation } from '../../types/addressGeolocation.types'; import { useAppSelector } from '../../hooks'; -import { RootState } from '../../store/store'; +import { getDistanceFromLatLonInKm } from '@components/utlis/commonFunctions'; +import NoLocationIcon from '@rn-ui-lib/icons/NoLocationIcon'; +import LocationFillIcon from '@rn-ui-lib/icons/LocationFillIcon'; +import { navigateToScreen } from '@components/utlis/navigationUtlis'; +import { PageRouteEnum } from '@screens/auth/ProtectedRouter'; +import GeolocationItem from './GeolocationItem'; +const NEARBY_DISTANCE_THRESHOLD_IN_KM = 100; interface IGeolocationContainer { caseId: string; loanAccountNumber: string; @@ -25,9 +28,10 @@ const GeolocationContainer: React.FC = ({ loanAccountNumber, handlePageRouting, }) => { - const { addressFeedbacks } = useAppSelector((state: RootState) => ({ - addressFeedbacks: state.address?.[loanAccountNumber]?.addressFeedbacks || [], + const { currentGeolocationCoordinates } = useAppSelector((state) => ({ + currentGeolocationCoordinates: state.foregroundService?.deviceGeolocationCoordinate, })); + if (!geolocationList?.length) { return ( = ({ ); } + const { nearbyLocations, farAwayLocations } = useMemo(() => { + const nearbyLocations: IGeolocation[] = []; + const farAwayLocations: IGeolocation[] = []; + geolocationList.forEach((geolocation: IGeolocation) => { + const distance = getDistanceFromLatLonInKm(currentGeolocationCoordinates, { + latitude: geolocation?.latitude, + longitude: geolocation?.longitude, + }); + if (!distance || isNaN(distance)) { + farAwayLocations.push(geolocation); + } + if (distance <= NEARBY_DISTANCE_THRESHOLD_IN_KM) { + nearbyLocations.push(geolocation); + } else { + farAwayLocations.push(geolocation); + } + }); + return { nearbyLocations, farAwayLocations }; + }, [geolocationList]); + + const handleAdditionalGeolocations = () => { + navigateToScreen(PageRouteEnum.ADDITIONAL_GEOLOCATIONS, { + additionalGeolocations: farAwayLocations, + caseId, + loanAccountNumber, + handlePageRouting, + }); + }; + return ( - - {geolocationList.map((geolocation: IGeolocation) => { - const lastFeedbackForGeolocation = addressFeedbacks.find( - (addressFeedback) => addressFeedback?.addressReferenceId === geolocation.id - ); - return ( - - + + {nearbyLocations?.length ? ( + nearbyLocations.map((geolocation: IGeolocation, index: number) => ( + + )) + ) : ( + + + + No nearby geolocations found + + + )} + {farAwayLocations?.length ? ( + 0 && GenericStyles.mt12, + GenericStyles.fill, + ]} + > + + + + + + Additional geolocations + + View all geolocations + + - ); - })} + + ) : null} ); }; const styles = StyleSheet.create({ - geoLocationItemStyle: { - paddingLeft: 15, - paddingTop: 20, - }, textContainer: { fontSize: 14, lineHeight: 20, @@ -86,19 +149,11 @@ const styles = StyleSheet.create({ color: '#BCBCBC', fontWeight: '600', }, - geolocationIcon: { - backgroundColor: COLORS.BACKGROUND.SILVER, - borderRadius: 4, - width: 24, - height: 24, - alignItems: 'center', - justifyContent: 'center', - marginRight: 8, - }, - geolocationItem: { - borderBottomWidth: 1, - borderBottomColor: COLORS.BORDER.PRIMARY, - paddingVertical: 16, + actionBtn: { + fontSize: 13, + lineHeight: 20, + color: COLORS.TEXT.BLUE, + marginTop: 8, }, }); diff --git a/src/screens/addressGeolocation/GeolocationItem.tsx b/src/screens/addressGeolocation/GeolocationItem.tsx index 2cade0b5..bcc7681b 100644 --- a/src/screens/addressGeolocation/GeolocationItem.tsx +++ b/src/screens/addressGeolocation/GeolocationItem.tsx @@ -1,151 +1,61 @@ +import { StyleSheet, View } from 'react-native'; import React from 'react'; -import { Linking, StyleSheet, Text, type TextStyle, TouchableOpacity, View } from 'react-native'; -import { GenericStyles } from '../../../RN-UI-LIB/src/styles'; -import { COLORS } from '../../../RN-UI-LIB/src/styles/colors'; -import { - BUSINESS_DATE_FORMAT, - BUSINESS_TIME_FORMAT, - dateFormat, -} from '../../../RN-UI-LIB/src/utlis/dates'; -import { - getDistanceFromLatLonInKm, - getGoogleMapUrl, - sanitizeString, -} from '../../components/utlis/commonFunctions'; -import { - type IGeoLocation, - type IGeolocationCoordinate, -} from '../../types/addressGeolocation.types'; -import { addClickstreamEvent } from '../../services/clickstreamEventService'; -import { CLICKSTREAM_EVENT_NAMES } from '../../common/Constants'; -import { type RootState } from '../../store/store'; -import { useAppSelector } from '../../hooks'; -import CustomLocationSmallIcon from '../../assets/icons/CustomLocationSmallIcon'; -import relativeDistanceFormatter from './utils/relativeDistanceFormatter'; +import GeolocationAddress from '@components/form/components/GeolocationAddress'; +import { useAppSelector } from '@hooks'; +import { RootState } from '@store'; +import { GenericStyles } from '@rn-ui-lib/styles'; +import { GenericFunctionArgs } from '@common/GenericTypes'; +import { IGeolocation } from '@screens/caseDetails/interface'; +import { COLORS } from '@rn-ui-lib/colors'; interface IGeolocationItem { - geolocationItem: IGeoLocation; - showSeparator?: boolean; - highlightIcon?: boolean; - containerStyle?: TextStyle; + geolocation: IGeolocation; + caseId: string; + loanAccountNumber: string; + handlePageRouting?: GenericFunctionArgs; + isLastItem?: boolean; } -function SeparatorBorderComponent() { - return ; -} - -function GeolocationItem({ - geolocationItem, - showSeparator = true, - highlightIcon = false, - containerStyle, -}: IGeolocationItem) { - const currentGeolocationCoordinates: IGeolocationCoordinate = useAppSelector( - (state: RootState) => state.foregroundService?.deviceGeolocationCoordinate +const GeolocationItem: React.FC = ({ + geolocation, + caseId, + loanAccountNumber, + isLastItem, + handlePageRouting, +}) => { + const { addressFeedbacks } = useAppSelector((state: RootState) => ({ + addressFeedbacks: state.address?.[loanAccountNumber]?.addressFeedbacks || [], + })); + const lastFeedbackForGeolocation = addressFeedbacks.find( + (addressFeedback) => addressFeedback?.addressReferenceId === geolocation.id ); - - const addressGeolocationCoordinated: IGeolocationCoordinate = { - latitude: geolocationItem?.latitude, - longitude: geolocationItem?.longitude, - }; - - const relativeDistanceBwLatLong = getDistanceFromLatLonInKm( - currentGeolocationCoordinates, - addressGeolocationCoordinated - ); - - const locationDate = dateFormat(new Date(geolocationItem?.timestamp), BUSINESS_DATE_FORMAT); - const locationTime = dateFormat(new Date(geolocationItem?.timestamp), BUSINESS_TIME_FORMAT); - - const openGeolocation = async () => { - addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_VIEW_MAP_GEO_CLICKED, { - latitude: geolocationItem.latitude, - longitude: geolocationItem.longitude, - }); - const geolocationUrl = getGoogleMapUrl(geolocationItem?.latitude, geolocationItem?.longitude); - if (!geolocationUrl) return; - - return await Linking.openURL(geolocationUrl); - }; - return ( - - - - - - - - - {relativeDistanceFormatter(relativeDistanceBwLatLong)} km away - - - {sanitizeString(locationDate)} -   ●   - {sanitizeString(locationTime)} - - - Open map - - - - - {showSeparator ? ( - - - - ) : null} + + ); -} +}; const styles = StyleSheet.create({ - container: { + geolocationItem: { backgroundColor: COLORS.BACKGROUND.PRIMARY, - }, - iconContainer: { - borderRadius: 4, - width: 24, - height: 24, - backgroundColor: COLORS.BACKGROUND.GREY_D9, - alignItems: 'center', - justifyContent: 'center', - }, - contentContainer: { - fontSize: 14, - paddingHorizontal: 8, - }, - titleText: { - fontSize: 14, - lineHeight: 20, - color: COLORS.TEXT.DARK, - }, - textContainer: { - fontSize: 12, - lineHeight: 18, - color: COLORS.TEXT.LIGHT, - }, - openMapBtn: { - fontSize: 13, - lineHeight: 20, - color: COLORS.TEXT.BLUE, - }, - borderLine: { - borderWidth: 0.5, - borderColor: COLORS.BORDER.PRIMARY, - }, - dotStyle: { - fontSize: 11, - color: COLORS.TEXT.LIGHT, + padding: 16, }, }); diff --git a/src/screens/addressGeolocation/GeolocationTimestamps.tsx b/src/screens/addressGeolocation/GeolocationTimestamps.tsx new file mode 100644 index 00000000..7a3256e9 --- /dev/null +++ b/src/screens/addressGeolocation/GeolocationTimestamps.tsx @@ -0,0 +1,111 @@ +import { StyleSheet, View } from 'react-native'; +import React from 'react'; +import Text from '@rn-ui-lib/components/Text'; +import { GenericStyles } from '@rn-ui-lib/styles'; +import { COLORS } from '@rn-ui-lib/colors'; +import { dateFormat } from '@rn-ui-lib/utils/dates'; + +interface IGeolocationTimestamps { + geolocationTimestamps: string[]; + totalNumberOfTimestamps: number; + oldestGeolocationTimestamp?: string; +} + +//TODO: Remove this mock data +const MOCK_TIMESTAMPS = [ + 1701723474000, 1701723474001, 1701723474002, 1701723474003, 1701723474004, 1701723474005, + 1701723474006, 1701723474007, 1701723474008, 1701723474009, 1701723474010, 1701723474011, + 1701723474012, 1701723474013, 1701723474014, 1701723474015, 1701723474016, 1701723474017, + 1701723474018, 1701723474019, 1701723474020, 1701723474021, 1701723474022, 1701723474023, + 1701723474024, 1701723474025, 1701723474026, 1212121212121, +]; + +const GeolocationTimestamps: React.FC = ({ + geolocationTimestamps = [], + totalNumberOfTimestamps = 78, + oldestGeolocationTimestamp = 1701724444444, +}) => { + console.log('totalNumberOfTimestamps', totalNumberOfTimestamps); + const timestampsCountLessThanTotal = MOCK_TIMESTAMPS?.length < totalNumberOfTimestamps; + return ( + + {MOCK_TIMESTAMPS?.map((timestamp, index) => { + const formattedDate = dateFormat(new Date(timestamp), 'MMM DD, YYYY | HH:mm A'); + const isLastTimestamp = + index === MOCK_TIMESTAMPS.length - 1 && timestampsCountLessThanTotal; + return ( + + + + + + + + + {formattedDate} + {isLastTimestamp ? ( + + + {totalNumberOfTimestamps - MOCK_TIMESTAMPS?.length - 1} more locations + + ) : null} + + + ); + })} + {timestampsCountLessThanTotal && oldestGeolocationTimestamp ? ( + + + + + + + + + {dateFormat(new Date(oldestGeolocationTimestamp), 'MMM DD, YYYY | HH:mm A')} + + + + ) : null} + + ); +}; +const styles = StyleSheet.create({ + stepperLine: { + alignItems: 'center', + }, + line: { + width: 1, + backgroundColor: COLORS.TEXT.GREY_1, + height: 40, + marginRight: 16, + top: 8, + }, + dottedLine: { + width: 1, + height: 125, + marginRight: 16, + top: 8, + borderStyle: 'dashed', + borderRightWidth: 1, + borderColor: COLORS.TEXT.GREY_1, + }, + whiteDot: { + width: 10, + height: 10, + borderRadius: 5, + backgroundColor: COLORS.BACKGROUND.PRIMARY, + marginRight: 16, + top: 8, + zIndex: 1, + alignItems: 'center', + justifyContent: 'center', + }, + blueDot: { + width: 8, + height: 8, + borderRadius: 4, + backgroundColor: COLORS.BACKGROUND.BLUE_LIGHT_4, + }, +}); + +export default GeolocationTimestamps; diff --git a/src/screens/addressGeolocation/SimilarGeolocations.tsx b/src/screens/addressGeolocation/SimilarGeolocations.tsx new file mode 100644 index 00000000..6589e62e --- /dev/null +++ b/src/screens/addressGeolocation/SimilarGeolocations.tsx @@ -0,0 +1,66 @@ +import { ScrollView, StyleSheet, View } from 'react-native'; +import React, { useEffect, useState } from 'react'; +import Layout from '@screens/layout/Layout'; +import NavigationHeader from '@rn-ui-lib/components/NavigationHeader'; +import { goBack } from '@components/utlis/navigationUtlis'; +import { IGeolocation } from '@screens/caseDetails/interface'; +import { GenericStyles } from '@rn-ui-lib/styles'; +import Text from '@rn-ui-lib/components/Text'; +import GeolocationTimestamps from './GeolocationTimestamps'; +import { getSimilarGeolocationTimestamps } from '@actions/addressGeolocationAction'; + +interface ISimilarGeolocations { + route: { + params: IGeolocation; + }; +} + +interface IGeolocationTimestamps { + similarLocationTimestamps: string[]; + oldestLocationTimestamp: string; +} + +const SimilarGeolocations: React.FC = (props) => { + const { params } = props.route || {}; + const { tag, clusterCardinality, id } = params || {}; + const [isLoading, setIsLoading] = useState(false); + const [geolocationTimestamps, setGeolocationTimestamps] = useState( + null + ); + + useEffect(() => { + setIsLoading(true); + getSimilarGeolocationTimestamps(id) + .then((res) => { + setGeolocationTimestamps(res?.similarLocationTimestamps || []); + }) + .finally(() => { + setIsLoading(false); + }); + }, []); + + return ( + + + + + + Similar location's timestamp + + + + + + ); +}; + +export default SimilarGeolocations; + +const styles = StyleSheet.create({}); diff --git a/src/screens/addressGeolocation/index.tsx b/src/screens/addressGeolocation/index.tsx index e7fc5fc7..53bb0d2c 100644 --- a/src/screens/addressGeolocation/index.tsx +++ b/src/screens/addressGeolocation/index.tsx @@ -218,7 +218,7 @@ const AddressGeolocation: React.FC = ({ route: routeParams } > - + {selectedTab === AddressGeolocationTabEnum.ADDRESS ? ( <> = ({ route: routeParams = ({ route: routeParams - + Additional addresses { @@ -239,6 +243,26 @@ const ProtectedRouter = () => { }} listeners={getScreenFocusListenerObj} /> + null, + animationDuration: SCREEN_ANIMATION_DURATION, + animation: 'slide_from_right', + }} + listeners={getScreenFocusListenerObj} + /> + null, + animationDuration: SCREEN_ANIMATION_DURATION, + animation: 'none', + }} + listeners={getScreenFocusListenerObj} + /> Date: Wed, 7 Feb 2024 13:10:25 +0530 Subject: [PATCH 03/69] TP-48911 | Feedback form interaction revamp --- .../form/components/GeolocationAddress.tsx | 16 ++--- src/components/form/index.tsx | 57 ++++++++------- src/reducer/caseReducer.ts | 15 +++- .../addressGeolocation/AddressItem.tsx | 11 ++- .../caseDetails/CollectionCaseDetail.tsx | 71 ++++++++++--------- src/types/template.types.ts | 1 + 6 files changed, 98 insertions(+), 73 deletions(-) diff --git a/src/components/form/components/GeolocationAddress.tsx b/src/components/form/components/GeolocationAddress.tsx index 639fbc4c..925e3567 100644 --- a/src/components/form/components/GeolocationAddress.tsx +++ b/src/components/form/components/GeolocationAddress.tsx @@ -98,14 +98,12 @@ const GeolocationAddress: React.FC = ({ }) ); if (visitedWidgets?.length) { - _map(visitedWidgets, (visited) => - navigateToScreen(getTemplateRoute(visited, CaseAllocationType.COLLECTION_CASE), { - caseId: caseId, - journey: TaskTitleUIMapping.COLLECTION_FEEDBACK, - handleCloseRouting, - }) - ); - return; + const lastVisitedWidget = visitedWidgets[visitedWidgets.length - 1]; + navigateToScreen(getTemplateRoute(lastVisitedWidget, CaseAllocationType.COLLECTION_CASE), { + caseId: caseId, + journey: TaskTitleUIMapping.COLLECTION_FEEDBACK, + handleCloseRouting, + }); } } }; @@ -145,7 +143,7 @@ const GeolocationAddress: React.FC = ({ {addressTime} ) : null} - {(primarySource && primarySource === GeolocationSource.DATA_SUTRAM) ? ( + {primarySource && primarySource === GeolocationSource.DATA_SUTRAM ? ( Skip Tracing diff --git a/src/components/form/index.tsx b/src/components/form/index.tsx index b8365b5a..a4a94108 100644 --- a/src/components/form/index.tsx +++ b/src/components/form/index.tsx @@ -1,14 +1,13 @@ import React, { useEffect, useRef, useState } from 'react'; import { useForm } from 'react-hook-form'; -import { ScrollView, StyleSheet, TouchableHighlight, View } from 'react-native'; +import { ScrollView, StyleSheet, View } from 'react-native'; import Geolocation from 'react-native-geolocation-service'; import { SafeAreaView } from 'react-native-safe-area-context'; import Button from '../../../RN-UI-LIB/src/components/Button'; import Heading from '../../../RN-UI-LIB/src/components/Heading'; import Text from '../../../RN-UI-LIB/src/components/Text'; import ArrowSolidIcon from '../../../RN-UI-LIB/src/Icons/ArrowSolidIcon'; -import CloseIcon from '../../../RN-UI-LIB/src/Icons/CloseIcon'; -import { GenericStyles, getShadowStyle } from '../../../RN-UI-LIB/src/styles'; +import { GenericStyles } from '../../../RN-UI-LIB/src/styles'; import { COLORS } from '../../../RN-UI-LIB/src/styles/colors'; import { syncCaseDetail } from '../../action/dataActions'; import { CLICKSTREAM_EVENT_NAMES } from '../../common/Constants'; @@ -19,7 +18,12 @@ import { getUpdatedCollectionCaseDetail, updateCaseDetail, } from '../../reducer/allCasesSlice'; -import { deleteInteraction, deleteJourney, updateInteraction } from '../../reducer/caseReducer'; +import { + deleteInteraction, + deleteJourney, + updateInteraction, + updateInteractionTs, +} from '../../reducer/caseReducer'; import { CaseAllocationType } from '../../screens/allCases/interface'; import { getUnSyncedCase } from '../../screens/caseDetails/interactionsHandler'; import Layout from '../../screens/layout/Layout'; @@ -28,7 +32,7 @@ import { getTransformedCollectionCaseItem, } from '../../services/casePayload.transformer'; import { addClickstreamEvent } from '../../services/clickstreamEventService'; -import { CommonCaseWidgetId, FormTemplateV1 } from '../../types/template.types'; +import { CommonCaseWidgetId } from '../../types/template.types'; import { getTemplateRoute, getWidgetNameFromRoute, @@ -67,25 +71,20 @@ const Widget: React.FC = (props) => { const { params } = props.route; const { caseId, journey, handleCloseRouting } = params; const caseKey = useRef(''); - const { - caseType, - templateData, - caseData, - dataToBeValidated, - docsToBeUploaded, - intermediateDocsToBeUploaded, - } = useAppSelector((state) => { - const caseType = - state.allCases.caseDetails[caseId]?.caseType || CaseAllocationType.ADDRESS_VERIFICATION_CASE; - return { - caseType, - templateData: state.case.templateData[caseType], - caseData: state.allCases.caseDetails[caseId], - dataToBeValidated: state.case.caseForm?.[caseId]?.[journey], - docsToBeUploaded: state.feedbackImages.docsToBeUploaded, - intermediateDocsToBeUploaded: state.feedbackImages.intermediateDocsToBeUploaded, - }; - }); + const { caseType, templateData, caseData, dataToBeValidated, intermediateDocsToBeUploaded } = + useAppSelector((state) => { + const caseType = + state.allCases.caseDetails[caseId]?.caseType || + CaseAllocationType.ADDRESS_VERIFICATION_CASE; + return { + caseType, + templateData: state.case.templateData[caseType], + caseData: state.allCases.caseDetails[caseId], + dataToBeValidated: state.case.caseForm?.[caseId]?.[journey], + docsToBeUploaded: state.feedbackImages.docsToBeUploaded, + intermediateDocsToBeUploaded: state.feedbackImages.intermediateDocsToBeUploaded, + }; + }); const name = getWidgetNameFromRoute(props.route.name, caseType); const { sections, conditionActions: widgetConditionActions, isLeaf } = templateData.widget[name]; const sectionMap = templateData.sections; @@ -102,6 +101,16 @@ const Widget: React.FC = (props) => { setIsJourneyFirstScreen(isFirst); }, [templateData, name]); + useEffect(() => { + return () => { + const isFirstWidget = templateData?.journey?.COLLECTION_FEEDBACK?.startWidget === name; + if (isFirstWidget) { + // Update form interaction time if first screen gets unmounted + dispatch(updateInteractionTs({ caseId, journeyId: journey, widgetId: name })); + } + }; + }, []); + const { control, setValue, diff --git a/src/reducer/caseReducer.ts b/src/reducer/caseReducer.ts index 347206d5..d26cb621 100644 --- a/src/reducer/caseReducer.ts +++ b/src/reducer/caseReducer.ts @@ -1,7 +1,6 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { CaseAllocationType, ICaseItem } from '../screens/allCases/interface'; import { FormTemplateV1, IVisitedWidgetContext } from '../types/template.types'; -import { mockTemplate } from '../template'; interface ICaseReducer { value: number; @@ -74,6 +73,8 @@ export const caseSlice = createSlice({ ...answer.widgetContext, }; data[caseId][journeyId].visitedWidgets = visited; + // Update time of interaction + data[caseId][journeyId].updatedAt = Date.now(); state.caseForm = data; }, deleteInteraction: (state, action) => { @@ -84,14 +85,23 @@ export const caseSlice = createSlice({ visited.pop(); } data[caseId][journeyId].visitedWidgets = visited; + data[caseId][journeyId].updatedAt = Date.now(); state.caseForm = data; }, deleteJourney: (state, action) => { - const { caseId, journeyId, widgetId, answer } = action.payload; + const { caseId, journeyId } = action.payload; const data = state.caseForm; delete data[caseId][journeyId]; state.caseForm = data; }, + updateInteractionTs: (state, action) => { + const { caseId, journeyId } = action.payload; + const data = state.caseForm || {}; + if (data[caseId]?.[journeyId]) { + data[caseId][journeyId].updatedAt = Date.now(); + state.caseForm = data; + } + }, updateAvTemplateData: (state, action: PayloadAction) => { state.templateData[CaseAllocationType.ADDRESS_VERIFICATION_CASE] = action.payload; }, @@ -112,6 +122,7 @@ export const { deleteJourney, updateAlternateHeader, updatePreDefinedCaseFormJourney, + updateInteractionTs, } = caseSlice.actions; export default caseSlice.reducer; diff --git a/src/screens/addressGeolocation/AddressItem.tsx b/src/screens/addressGeolocation/AddressItem.tsx index 97a380e5..99433dfa 100644 --- a/src/screens/addressGeolocation/AddressItem.tsx +++ b/src/screens/addressGeolocation/AddressItem.tsx @@ -100,12 +100,11 @@ function AddressItem({ }) ); if (visitedWidgets?.length) { - _map(visitedWidgets, (visited) => { - navigateToScreen(getTemplateRoute(visited, CaseAllocationType.COLLECTION_CASE), { - caseId, - journey: TaskTitleUIMapping.COLLECTION_FEEDBACK, - handleCloseRouting, - }); + const lastVisitedWidget = visitedWidgets[visitedWidgets.length - 1]; + navigateToScreen(getTemplateRoute(lastVisitedWidget, CaseAllocationType.COLLECTION_CASE), { + caseId, + journey: TaskTitleUIMapping.COLLECTION_FEEDBACK, + handleCloseRouting, }); } } diff --git a/src/screens/caseDetails/CollectionCaseDetail.tsx b/src/screens/caseDetails/CollectionCaseDetail.tsx index 85d80536..33759ec5 100644 --- a/src/screens/caseDetails/CollectionCaseDetail.tsx +++ b/src/screens/caseDetails/CollectionCaseDetail.tsx @@ -1,8 +1,7 @@ -import React, { useEffect, useRef, useState } from 'react'; +import React, { useEffect, useRef } from 'react'; import { Animated, Pressable, - RefreshControl, SafeAreaView, ScrollView, StyleSheet, @@ -41,7 +40,6 @@ import { addClickstreamEvent } from '../../services/clickstreamEventService'; import { getLoanAccountNumber } from '../../components/utlis/commonFunctions'; import EmiBreakupBottomSheet from '../emiSchedule/EmiBreakupBottomSheet'; import { CollectionCaseWidgetId } from '../../types/template.types'; -import { useFocusEffect } from '@react-navigation/native'; import ScreenshotBlocker from '../../components/utlis/ScreenshotBlocker'; import { useIsFocused } from '@react-navigation/native'; import { setSelectedCaseId } from '../../reducer/allCasesSlice'; @@ -81,6 +79,23 @@ const CollectionCaseDetails: React.FC = (props) => { }, } = props; + const dispatch = useAppDispatch(); + const isFocused = useIsFocused(); + const isOnline = useIsOnline(); + + const caseDetail = useAppSelector((state) => state.allCases.caseDetails[caseId]!!); + const preFilledFormData = useAppSelector( + (state) => state.case.caseForm?.[caseId]?.[TaskTitleUIMapping.COLLECTION_FEEDBACK] + ); + + const { addressString, phoneNumbers, loanAccountNumber, totalOverdueAmount, pos } = caseDetail; + + const feedbackList: IFeedback[] = useAppSelector( + (state: RootState) => state.feedbackHistory?.[loanAccountNumber as string]?.data || [] + ); + + const allCasesDetails = useAppSelector((state) => state.allCases.caseDetails); + useEffect(() => { if (caseId) dispatch(setSelectedCaseId(caseId)); @@ -89,30 +104,6 @@ const CollectionCaseDetails: React.FC = (props) => { }; }, [caseId]); - const dispatch = useAppDispatch(); - const isFocused = useIsFocused(); - const isOnline = useIsOnline(); - - const caseDetail = useAppSelector((state) => state.allCases.caseDetails[caseId]!!); - const data = useAppSelector( - (state) => state.case.caseForm?.[caseId]?.[TaskTitleUIMapping.COLLECTION_FEEDBACK] - ); - - const { - addressString, - phoneNumbers, - currentOutstandingEmi, - loanAccountNumber, - totalOverdueAmount, - pos, - } = caseDetail; - - const feedbackList: IFeedback[] = useAppSelector( - (state: RootState) => state.feedbackHistory?.[loanAccountNumber as string]?.data || [] - ); - - const allCasesDetails = useAppSelector((state) => state.allCases.caseDetails); - useEffect(() => { if (!loanAccountNumber) { return; @@ -177,8 +168,21 @@ const CollectionCaseDetails: React.FC = (props) => { caseType: caseDetail?.caseType, journey: 'COLLECTION_FEEDBACK', }); - if (data?.visitedWidgets?.length) { - _map(data.visitedWidgets, (visited) => + if (preFilledFormData?.visitedWidgets?.length) { + const lastFormInteractionTs = preFilledFormData?.updatedAt || 0; + // If Date.now() is greater than updatedAt by 60mins, then we will navigate to the first widget + const isTimeExpired = (Date.now() - lastFormInteractionTs) > 10 * 1000; //60 * 60 * 1000; + if (isTimeExpired) { + navigateToScreen( + getTemplateRoute(CollectionCaseWidgetId.START, CaseAllocationType.COLLECTION_CASE), + { + caseId: caseId, + journey: 'COLLECTION_FEEDBACK', + } + ); + return; + } + _map(preFilledFormData.visitedWidgets, (visited) => navigateToScreen(getTemplateRoute(visited, CaseAllocationType.COLLECTION_CASE), { caseId: caseId, journey: 'COLLECTION_FEEDBACK', @@ -295,7 +299,7 @@ const CollectionCaseDetails: React.FC = (props) => { } - underlayColor='transparent' + underlayColor="transparent" pressableWidthChange={false} opacityChangeOnPress={true} /> @@ -462,7 +466,10 @@ const CollectionCaseDetails: React.FC = (props) => { onPress={() => { handleRouting(PageRouteEnum.PAST_FEEDBACK_DETAIL); }} - style={({ pressed })=>[GenericStyles.flex20, { opacity: pressed ? 0.7 : 1 }]} + style={({ pressed }) => [ + GenericStyles.flex20, + { opacity: pressed ? 0.7 : 1 }, + ]} > Open all feedbacks @@ -491,7 +498,7 @@ const CollectionCaseDetails: React.FC = (props) => { />