From be252740858aef6b66a933b710deabe052631c1e Mon Sep 17 00:00:00 2001 From: Mantri Ramkishor Date: Tue, 6 May 2025 14:50:40 +0530 Subject: [PATCH 1/2] NTP-58799 | Persist original image (#1157) --- .../com/avapp/photoModule/PhotoModule.java | 1 - src/action/caseApiActions.ts | 125 ++++++++++++------ src/common/Constants.ts | 13 ++ src/components/utlis/apiHelper.ts | 6 +- 4 files changed, 101 insertions(+), 44 deletions(-) diff --git a/android/app/src/main/java/com/avapp/photoModule/PhotoModule.java b/android/app/src/main/java/com/avapp/photoModule/PhotoModule.java index ea301ee0..5317d0b0 100644 --- a/android/app/src/main/java/com/avapp/photoModule/PhotoModule.java +++ b/android/app/src/main/java/com/avapp/photoModule/PhotoModule.java @@ -270,7 +270,6 @@ public class PhotoModule extends ReactContextBaseJavaModule { String longitude = attributes.getString("longitude"); exif.setAttribute(ExifInterface.TAG_IMAGE_DESCRIPTION, json.toString()); - exif.setAttribute(ExifInterface.TAG_ARTIST, "Aman Chaturvedi (1932)"); String currentDateTime = getCurrentFormattedDateTime(); exif.setAttribute(ExifInterface.TAG_DATETIME, currentDateTime); diff --git a/src/action/caseApiActions.ts b/src/action/caseApiActions.ts index 8fb50d07..51c56c9c 100644 --- a/src/action/caseApiActions.ts +++ b/src/action/caseApiActions.ts @@ -10,6 +10,8 @@ import { type IDocument, removeDocumentByQuestionKey } from '../reducer/feedback import { addClickstreamEvent } from '@services/clickstreamEventService'; import { CLICKSTREAM_EVENT_NAMES } from '@common/Constants'; import { PAST_FEEDBACK_PAGE_SIZE } from '@screens/caseDetails/feedback/pastFeedbackCommon'; +import RNBlobUtil from 'react-native-blob-util'; +import logger from '@components/utlis/logger'; export const getRepaymentsData = (loanAccountNumber: string, caseId: string, caseBusinessVertical: string) => (dispatch: AppDispatch) => { dispatch(setRepaymentsLoading({ loanAccountNumber, isLoading: true })); @@ -119,54 +121,93 @@ export const getFeedbackHistory = (loanAccountNumber: string) => (dispatch: AppD export const uploadImages = (caseKey: string, documents: Record, interactionReferenceId: string) => - (dispatch: AppDispatch) => { + async (dispatch: AppDispatch) => { if (!documents || !interactionReferenceId) { return; } - _map(documents, (questionKey, index) => { + for (const questionKey of Object.keys(documents)) { const fileDoc = documents[questionKey]; - if (!fileDoc) { - return; + if (!fileDoc || !fileDoc.fileUri) { + continue; } - const { fileUri } = fileDoc; - const formData = new FormData(); - formData.append( - 'originalImageData', - JSON.stringify({ - interactionReferenceId, - questionKey, - }) - ); - formData.append('image', { - uri: fileUri, - name: `image_${interactionReferenceId}_${index}`, - type: 'image/jpeg', - } as any); - const url = getApiUrl(ApiKeys.UPLOAD_FEEDBACK_IMAGES); - axiosInstance - .put(url, formData, { - headers: { - 'Content-Type': 'multipart/form-data', - donotHandleError: true, + const mimeType = 'image/jpeg'; + try { + // Get pre-signed URL + const presignRes = await axiosInstance.post( + getApiUrl(ApiKeys.GET_PRE_SIGNED_URL_FOR_FEEDBACK_IMAGE), + { + interactionReferenceId, + questionKey, + mimeType, }, - timeout: 5 * MILLISECONDS_IN_A_MINUTE, - }) - .then((res) => { - dispatch(removeDocumentByQuestionKey({ caseKey, questionKey })); - }) - .catch((err) => { - if (err?.response?.status === API_STATUS_CODE.NOT_FOUND) { - dispatch(removeDocumentByQuestionKey({ caseKey, questionKey })); - addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_WRONG_QUESTION_KEY, { - error: err, - questionKey, - interactionReferenceId, - }); + { + headers: { doNotHandleError: true }, } - addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_PERSIST_ORIGINAL_IMAGE_FAILURE, { - payload: formData, - }); - logError(err as Error, 'Error uploading image to document service'); + ); + if (!presignRes?.data?.presignedUrl || !presignRes?.data?.id) { + throw new Error('Failed to get presigned URL'); + } + + addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_PERSIST_ORIGINAL_IMAGE_PRESIGNED_SUCCESS, { + payload: { + questionKey, + interactionReferenceId, + }, }); - }); + + // Upload image to pre-signed URL + const fileStats = await RNBlobUtil.fs.stat(fileDoc.fileUri); + await RNBlobUtil.fetch( + 'PUT', + presignRes?.data?.presignedUrl, + { + 'Content-Type': mimeType, + }, + RNBlobUtil.wrap(fileStats.path) + ); + + addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_PERSIST_ORIGINAL_IMAGE_S3_SUCCESS, { + payload: { + questionKey, + interactionReferenceId, + }, + }); + + // Acknowledge + await axiosInstance.post( + getApiUrl(ApiKeys.FEEDBACK_ORIGINAL_IMAGE_ACK), + {}, + { + params: { id: presignRes?.data?.id }, + headers: { doNotHandleError: true }, + } + ); + addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_PERSIST_ORIGINAL_IMAGE_ACKNOWLEDGE_SUCCESS, { + payload: { + questionKey, + interactionReferenceId, + }, + }); + + // Remove document from redux + dispatch(removeDocumentByQuestionKey({ caseKey, questionKey })); + } catch (err) { + logger({ + msg: `Image upload failed`, + type: 'error', + extras: { + error: err, + questionKey, + interactionReferenceId, + }, + }); + addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_PERSIST_ORIGINAL_IMAGE_FAILURE, { + payload: { + questionKey, + interactionReferenceId, + }, + }); + continue; + } + } }; diff --git a/src/common/Constants.ts b/src/common/Constants.ts index 6193e1c3..2f23119e 100644 --- a/src/common/Constants.ts +++ b/src/common/Constants.ts @@ -1433,6 +1433,19 @@ export const CLICKSTREAM_EVENT_NAMES = { description: 'Failed to persist original image', }, + FA_PERSIST_ORIGINAL_IMAGE_PRESIGNED_SUCCESS: { + name: 'FA_PERSIST_ORIGINAL_IMAGE_PRESIGNED_SUCCESS', + description: 'Successfully presigned generated for original image', + }, + FA_PERSIST_ORIGINAL_IMAGE_S3_SUCCESS: { + name: 'FA_PERSIST_ORIGINAL_IMAGE_S3_SUCCESS', + description: 'Successfully persisted original image to S3', + }, + FA_PERSIST_ORIGINAL_IMAGE_ACKNOWLEDGE_SUCCESS: { + name: 'FA_PERSIST_ORIGINAL_IMAGE_ACKNOWLEDGE_SUCCESS', + description: 'Successfully acknowledged original image', + }, + // Filter coachmarks FA_FILTER_COACHMARKS_LOADED: { name: 'FA_FILTER_COACHMARKS_LOADED', diff --git a/src/components/utlis/apiHelper.ts b/src/components/utlis/apiHelper.ts index f508e4b4..737db3b1 100644 --- a/src/components/utlis/apiHelper.ts +++ b/src/components/utlis/apiHelper.ts @@ -120,7 +120,9 @@ export enum ApiKeys { GET_TRAINING_MATERIAL_LIST = 'GET_TRAINING_MATERIAL_LIST', 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_CASES_GEOLOCATION_DISTANCE_FROM_AGENT_LOCATION = "GET_CASES_GEOLOCATION_DISTANCE_FROM_AGENT_LOCATION", + GET_PRE_SIGNED_URL_FOR_FEEDBACK_IMAGE = 'GET_PRE_SIGNED_URL_FOR_FEEDBACK_IMAGE', + FEEDBACK_ORIGINAL_IMAGE_ACK = 'FEEDBACK_ORIGINAL_IMAGE_ACK', } export const API_URLS: Record = {} as Record; @@ -230,6 +232,8 @@ 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_CASES_GEOLOCATION_DISTANCE_FROM_AGENT_LOCATION] = '/geolocation-distance/single-source' +API_URLS[ApiKeys.GET_PRE_SIGNED_URL_FOR_FEEDBACK_IMAGE] = '/file-upload/presigned-url'; +API_URLS[ApiKeys.FEEDBACK_ORIGINAL_IMAGE_ACK] = '/file-upload/acknowledge'; export const API_STATUS_CODE = { OK: 200, From 91c1df2099469cebd2f32986571f2fa867038509 Mon Sep 17 00:00:00 2001 From: Pulkit Barwal Date: Tue, 6 May 2025 16:22:12 +0530 Subject: [PATCH 2/2] NTP-55956 (#1161) --- src/reducer/nearbyCasesSlice.ts | 5 ++ src/screens/addresses/actions.ts | 84 ++++++++++++++----- .../addresses/common/AddressItemHeader.tsx | 16 ++-- .../caseDetails/AllocatedAddressDetails.tsx | 38 ++++----- 4 files changed, 93 insertions(+), 50 deletions(-) diff --git a/src/reducer/nearbyCasesSlice.ts b/src/reducer/nearbyCasesSlice.ts index c812b0c2..cf86dac7 100644 --- a/src/reducer/nearbyCasesSlice.ts +++ b/src/reducer/nearbyCasesSlice.ts @@ -8,6 +8,7 @@ const initialState = { locationNearbyCasesListUpdated: {} as IGeolocationCoordinate, isPullToRefreshNearbyCasesVisible: false as boolean, caseReferenceIdToDistanceMap: new Map() as Map, + addressToDistanceMap: new Map() as Map, sortTabSelected: TABS_KEYS.HIGHEST_OD as string, }; @@ -30,6 +31,9 @@ export const nearbyCasesListSlice = createSlice({ setSortTabSelected: (state, action) => { state.sortTabSelected = action.payload; }, + setAddressToDistanceMap: (state, action) => { + state.addressToDistanceMap = action.payload; + }, resetNearbyCasesData: () => initialState, }, }); @@ -40,6 +44,7 @@ export const { setIsPullToRefreshNearbyCasesVisible, setCaseReferenceIdToDistanceMap, setSortTabSelected, + setAddressToDistanceMap, resetNearbyCasesData, } = nearbyCasesListSlice.actions; diff --git a/src/screens/addresses/actions.ts b/src/screens/addresses/actions.ts index e71cb3d4..67e30575 100644 --- a/src/screens/addresses/actions.ts +++ b/src/screens/addresses/actions.ts @@ -9,30 +9,76 @@ import { } from '@reducers/topAddressesSlice'; import { AppDispatch } from '@store'; import { PAGE_END } from './constants'; +import { ICaseItemLatLongData } from '@screens/allCases/interface'; +import store from '@store'; +import { getGeolocationDistance } from '@screens/allCases/allCasesActions'; +import { getDistanceFromLatLonInKm } from '@components/utlis/commonFunctions'; +import { setAddressToDistanceMap } from '@reducers/nearbyCasesSlice'; +import { ILocationData } from '@screens/addresses/interfaces'; -export const getTopAddresses = (caseId: string, start: number) => (dispatch: AppDispatch) => { +export const getTopAddresses = (caseId: string, start: number) => async (dispatch: AppDispatch) => { dispatch(setTopAddressesLoading({ caseId, isLoading: true })); const url = getApiUrl(ApiKeys.GET_TOP_ADDRESSES); - axiosInstance - .get(url, { + + try { + const response = await axiosInstance.get(url, { params: { caseReferenceId: caseId, startingRank: start }, - }) - .then((res) => { - if (res?.data) { - const { unifiedLocations = [], totalLocationEntities = 0 } = res?.data || {}; - dispatch( - setTopAddresses({ - caseId, - addresses: unifiedLocations, - totalLocationEntities, - }) - ); - } - }) - .catch((error) => {}) - .finally(() => { - dispatch(setTopAddressesLoading({ caseId, isLoading: false })); }); + + if (response?.data) { + const { unifiedLocations = [], totalLocationEntities = 0 } = response.data || {}; + dispatch( + setTopAddresses({ + caseId, + addresses: unifiedLocations, + totalLocationEntities, + }) + ); + + 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, + }); + + if (!addressDistanceMap || addressDistanceMap?.size === 0) { + destinations?.forEach((destination) => { + const distanceInKm = getDistanceFromLatLonInKm( + destination, + deviceGeolocationCoordinate + ); + if (distanceInKm) { + addressDistanceMap?.set(destination?.id, distanceInKm); + } + }); + } + store.dispatch(setAddressToDistanceMap(addressDistanceMap)); + } + } + } catch (error) { + console.error('Error fetching top addresses:', error); + } finally { + dispatch(setTopAddressesLoading({ caseId, isLoading: false })); + } }; export const getOtherAddresses = (caseId: string) => (dispatch: AppDispatch) => { diff --git a/src/screens/addresses/common/AddressItemHeader.tsx b/src/screens/addresses/common/AddressItemHeader.tsx index 4732b037..47b968e5 100644 --- a/src/screens/addresses/common/AddressItemHeader.tsx +++ b/src/screens/addresses/common/AddressItemHeader.tsx @@ -33,17 +33,11 @@ const AddressItemHeader = (props: ITopAddressItemHeader) => { const { pinCode, city, latitude, longitude, rank, visited, locationSubType } = locationDetails || {}; - const deviceGeolocationCoordinate = useAppSelector( - (state) => state.foregroundService?.deviceGeolocationCoordinate + const addressToDistanceMap = useAppSelector( + (state) => state.nearbyCasesSlice?.addressToDistanceMap ); + const distanceOfAddress = addressToDistanceMap?.get(locationDetails?.referenceId); const [isModalVisible, setIsModalVisible] = useState(false); - const relativeDistanceBwLatLong = useMemo(() => { - const distance = getDistanceFromLatLonInKm(deviceGeolocationCoordinate, { - latitude, - longitude, - }); - return `${relativeDistanceFormatter(distance)} km`; - }, [deviceGeolocationCoordinate]); const addressString = useMemo(() => { return [pinCode, city].filter(Boolean).join(', '); @@ -91,14 +85,14 @@ const AddressItemHeader = (props: ITopAddressItemHeader) => { latitude={latitude} longitude={longitude} handleOpenMapForAddresses={handleOpenMapForAddresses} - relativeDistanceBwLatLong={relativeDistanceBwLatLong} + relativeDistanceBwLatLong={`${Number(distanceOfAddress?.toFixed(1))} km`} style={GenericStyles.pr16} isSimilarAddressPage={isSimilarAddressPage} /> ) : (