TP-20843 | firestore service and cases redux slice refactor (#1050)

This commit is contained in:
Aman Chaturvedi
2025-01-07 15:47:18 +05:30
committed by GitHub
parent ce9ae2d840
commit a2950322a8
12 changed files with 535 additions and 777 deletions

View File

@@ -27,7 +27,7 @@ import { clearAllAsyncStorage } from '../components/utlis/commonFunctions';
import { logError } from '../components/utlis/errorUtils';
import auth from '@react-native-firebase/auth';
import foregroundService from '../services/foregroundServices/foreground.service';
import { loggedOutCurrentUser } from '../hooks/useFirestoreUpdates';
import { loggedOutCurrentUser } from '@hooks/useFirestoreUpdates';
import { GenericFunctionArgs, GenericType } from '../common/GenericTypes';
import { GoogleSignin } from '@react-native-google-signin/google-signin';
import { resetConfig } from '../reducer/configSlice';
@@ -38,6 +38,7 @@ import { resetPerformanceData } from '@reducers/agentPerformanceSlice';
import { clearStorageEngine } from '../PersistStorageEngine';
import { resetNearbyCasesData } from '@reducers/nearbyCasesSlice';
import { resetActiveCallData } from '@reducers/activeCallSlice';
import { firestoreService } from '@services/firestoreService';
export interface GenerateOTPPayload {
phoneNumber: string;
@@ -198,6 +199,7 @@ export const handleGoogleLogout = async () => {
const firebaseSignout = async () => {
try {
await firestoreService.unsubscribeAll();
await auth().signOut();
} catch (error) {
logError(error as Error, 'Firebase signout error');

View File

@@ -1,74 +0,0 @@
import { AxiosResponse } from 'axios';
import axiosInstance, { ApiKeys, getApiUrl } from '../components/utlis/apiHelper';
import { logError } from '../components/utlis/errorUtils';
import { VisitPlanStatus } from '../reducer/userSlice';
import { FilterResponse } from '../screens/allCases/interface';
import { CaseDetail } from '../screens/caseDetails/interface';
export enum SyncStatus {
SEND_CASES = 'SEND_CASES',
FETCH_CASES = 'FETCH_CASES',
SKIP = 'SKIP',
}
interface IFilterSync {
filterComponentList: FilterResponse[];
}
export interface ISyncedCases {
cases: CaseDetail[];
filters: IFilterSync;
deletedCaseIds: string[];
payloadCreatedAt: number;
}
interface ICases {
caseId: string;
caseViewCreatedAt?: number;
}
export interface ISyncCaseIdPayload {
agentId: string;
cases: ICases[];
}
interface ICasesSyncStatus {
syncStatus: SyncStatus;
visitPlanStatus: VisitPlanStatus;
}
export const getCasesSyncStatus = async (userReferenceId: string) => {
try {
const url = getApiUrl(ApiKeys.CASES_SYNC_STATUS, {}, { userReferenceId });
const response: AxiosResponse<ICasesSyncStatus> = await axiosInstance.get(url, {
headers: { donotHandleError: true },
});
return response?.data;
} catch (err) {
logError(err as Error, 'Error getting sync status');
}
};
export const sendSyncCaseIds = async (payload: ISyncCaseIdPayload) => {
try {
const url = getApiUrl(ApiKeys.CASES_SEND_ID);
const response = await axiosInstance.post(url, payload, {
headers: { donotHandleError: true },
});
return response?.data;
} catch (err) {
logError(err as Error, 'Error sending case ids sync');
}
};
export const fetchCasesToSync = async (agentReferenceId: string) => {
//disabling this function since its conflicting with the new sync logic
return null;
try {
const url = getApiUrl(ApiKeys.FETCH_CASES, { agentReferenceId });
const response = await axiosInstance.get(url, { headers: { donotHandleError: true } });
return response?.data;
} catch (err) {
logError(err as Error, 'Error fetching cases to be synced');
}
};

View File

@@ -1,7 +1,6 @@
let ACTIVITY_TIME_ON_APP: number = 5; //5 seconds
let ACTIVITY_TIME_WINDOW_HIGH: number = 10; //10 minutes
let ACTIVITY_TIME_WINDOW_MEDIUM: number = 30; //30 minutes
let ENABLE_FIRESTORE_RESYNC = false;
let FIRESTORE_RESYNC_INTERVAL_IN_MINUTES = 15;
let DATA_SYNC_JOB_INTERVAL_IN_MINUTES = 10; // 10 minutes
let IMAGE_UPLOAD_JOB_INTERVAL_IN_MINUTES = 10; // 10 minutes
@@ -14,7 +13,6 @@ export const getActivityTimeOnApp = () => ACTIVITY_TIME_ON_APP;
export const getActivityTimeWindowHigh = () => ACTIVITY_TIME_WINDOW_HIGH;
export const getActivityTimeWindowMedium = () => ACTIVITY_TIME_WINDOW_MEDIUM;
export const getEnableFirestoreResync = () => ENABLE_FIRESTORE_RESYNC;
export const getFirestoreResyncIntervalInMinutes = () => FIRESTORE_RESYNC_INTERVAL_IN_MINUTES;
export const getDataSyncJobIntervalInMinutes = () => DATA_SYNC_JOB_INTERVAL_IN_MINUTES;
export const getImageUploadJobIntervalInMinutes = () => IMAGE_UPLOAD_JOB_INTERVAL_IN_MINUTES;
@@ -37,10 +35,6 @@ export const setActivityTimeWindowMedium = (activityTimeWindowMedium: number) =>
ACTIVITY_TIME_WINDOW_MEDIUM = activityTimeWindowMedium;
};
export const setEnableFirestoreResync = (enableFirestoreResync: boolean) => {
ENABLE_FIRESTORE_RESYNC = enableFirestoreResync;
};
export const setFirestoreResyncIntervalInMinutes = (firestoreResyncIntervalInMinutes: number) => {
FIRESTORE_RESYNC_INTERVAL_IN_MINUTES = firestoreResyncIntervalInMinutes;
};

View File

@@ -17,18 +17,7 @@ import { useAppDispatch, useAppSelector } from '../hooks';
import { dataSyncService } from '../services/dataSync.service';
import { DATA_SYNC_TIME_INTERVAL, IS_DATA_SYNC_REQUIRED } from '../constants/config';
import useIsLocationEnabled from '../hooks/useIsLocationEnabled';
import {
type ISyncCaseIdPayload,
type ISyncedCases,
SyncStatus,
fetchCasesToSync,
getCasesSyncStatus,
sendSyncCaseIds,
} from '../action/firebaseFallbackActions';
import { getSyncCaseIds } from '../components/utlis/firebaseFallbackUtils';
import { syncCasesByFallback } from '../reducer/allCasesSlice';
import { MILLISECONDS_IN_A_MINUTE, noop } from '../../RN-UI-LIB/src/utlis/common';
import { setCaseSyncLock, setLockData } from '../reducer/userSlice';
import { MILLISECONDS_IN_A_MINUTE} from '../../RN-UI-LIB/src/utlis/common';
import { getConfigData } from '../action/configActions';
import { AppStates } from '../types/appStates';
import { StorageKeys } from '../types/storageKeys';
@@ -37,7 +26,6 @@ import {
getActivityTimeOnApp,
getActivityTimeWindowMedium,
getActivityTimeWindowHigh,
getEnableFirestoreResync,
getDataSyncJobIntervalInMinutes,
getImageUploadJobIntervalInMinutes,
getVideoUploadJobIntervalInMinutes,
@@ -47,7 +35,7 @@ import {
} from './AgentActivityConfigurableConstants';
import { GlobalImageMap } from './CachedImage';
import { addClickstreamEvent } from '../services/clickstreamEventService';
import { CLICKSTREAM_EVENT_NAMES, LocalStorageKeys } from './Constants';
import { CLICKSTREAM_EVENT_NAMES } from './Constants';
import useResyncFirebase from '@hooks/useResyncFirebase';
import { imageSyncService, sendImagesToServer } from '@services/imageSyncService';
import { sendAudiosToServer } from '@services/audioSyncService';
@@ -82,7 +70,6 @@ interface ITrackingComponent {
children?: ReactNode;
}
let LAST_SYNC_STATUS = 'SKIP';
const ACTIVITY_TIME_WINDOW = 10; // 10 minutes
const TrackingComponent: React.FC<ITrackingComponent> = ({ children }) => {
@@ -90,11 +77,7 @@ const TrackingComponent: React.FC<ITrackingComponent> = ({ children }) => {
const dispatch = useAppDispatch();
const appState = useRef(AppState.currentState);
const isTeamLead = useAppSelector((state) => state.user.isTeamLead);
const caseSyncLock = useAppSelector((state) => state?.user?.caseSyncLock);
const referenceId = useAppSelector((state) => state.user.user?.referenceId!);
const pendingList = useAppSelector((state) => state.allCases.pendingList);
const pinnedList = useAppSelector((state) => state.allCases.pinnedList);
const handleTimeSync = async () => {
try {
@@ -113,46 +96,6 @@ const TrackingComponent: React.FC<ITrackingComponent> = ({ children }) => {
const resyncFirebase = useResyncFirebase();
const handleGetCaseSyncStatus = async () => {
if (caseSyncLock || getEnableFirestoreResync()) {
return;
}
dispatch(setCaseSyncLock(true));
try {
if (!isOnline) {
return;
}
const { syncStatus, visitPlanStatus } = (await getCasesSyncStatus(referenceId)) ?? {};
if (syncStatus) {
// Keep track of the last status received
LAST_SYNC_STATUS = syncStatus;
}
if (syncStatus === SyncStatus.SEND_CASES) {
const cases = getSyncCaseIds([...pendingList, ...pinnedList]);
const payload: ISyncCaseIdPayload = {
agentId: referenceId,
cases,
};
sendSyncCaseIds(payload);
} else if (syncStatus === SyncStatus.FETCH_CASES) {
const updatedDetails: ISyncedCases = await fetchCasesToSync(referenceId);
if (updatedDetails?.cases?.length) {
dispatch(syncCasesByFallback(updatedDetails));
}
}
if (visitPlanStatus) {
dispatch(
setLockData({
visitPlanStatus,
})
);
}
dispatch(setCaseSyncLock(false));
} catch (e) {
logError(e as Error, 'Error during fetching case sync status');
}
};
const handleUpdateActivity = async () => {
const foregroundTimestamp = await getItem(StorageKeys.APP_FOREGROUND_TIMESTAMP);
const backgroundTimestamp = await getItem(StorageKeys.APP_BACKGROUND_TIMESTAMP);
@@ -163,7 +106,6 @@ const TrackingComponent: React.FC<ITrackingComponent> = ({ children }) => {
}
const foregroundTime = dayJs(foregroundTimestamp);
const backgroundTime = dayJs(backgroundTimestamp);
const stateSetTime = dayJs(stateSetTimestamp);
const diffBetweenCurrentTimeAndForegroundTime =
@@ -323,15 +265,6 @@ const TrackingComponent: React.FC<ITrackingComponent> = ({ children }) => {
},
];
if (!isTeamLead) {
tasks.push({
taskId: FOREGROUND_TASKS.FIRESTORE_FALLBACK,
task: handleGetCaseSyncStatus,
delay: 5 * MILLISECONDS_IN_A_MINUTE, // 5 minutes
onLoop: true,
});
}
const handleDataSync = () => {
if (!isOnline) {
return;
@@ -371,7 +304,6 @@ const TrackingComponent: React.FC<ITrackingComponent> = ({ children }) => {
if (nextAppState === AppStates.ACTIVE) {
await setItem(StorageKeys.APP_FOREGROUND_TIMESTAMP, now);
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.AV_APP_FOREGROUND, { now });
handleGetCaseSyncStatus();
handleTimeSync();
dispatch(getConfigData());
CosmosForegroundService.start(tasks);
@@ -385,22 +317,11 @@ const TrackingComponent: React.FC<ITrackingComponent> = ({ children }) => {
appState.current = nextAppState;
};
// Fetch cases on login initially and set data
useEffect(() => {
(async () => {
if (!referenceId) {
return;
}
await handleGetCaseSyncStatus();
dispatch(getConfigData());
const isFirestoreResyncEnabled = getEnableFirestoreResync();
if (!isTeamLead && LAST_SYNC_STATUS !== SyncStatus.FETCH_CASES && !isFirestoreResyncEnabled) {
const updatedDetails: ISyncedCases = await fetchCasesToSync(referenceId);
if (updatedDetails?.cases?.length) {
dispatch(syncCasesByFallback(updatedDetails));
}
}
})();
if (!referenceId) {
return;
}
dispatch(getConfigData());
}, []);
useEffect(() => {

View File

@@ -1,24 +1,22 @@
import { useEffect, useRef } from 'react';
import firestore, { type FirebaseFirestoreTypes } from '@react-native-firebase/firestore';
import { type FirebaseFirestoreTypes } from '@react-native-firebase/firestore';
import auth from '@react-native-firebase/auth';
import { setFeedbackFilterTemplate } from '@reducers/feedbackFiltersSlice';
import { InteractionManager } from 'react-native';
import { useAppDispatch, useAppSelector } from '.';
import { setLoading, updateCaseDetailsFirestore } from '../reducer/allCasesSlice';
import { setLoading } from '../reducer/allCasesSlice';
import { type CaseDetail } from '../screens/caseDetails/interface';
import { CLICKSTREAM_EVENT_NAMES, FirestoreUpdateTypes } from '../common/Constants';
import { toast } from '../../RN-UI-LIB/src/components/toast';
import { updateCollectionTemplateData } from '../reducer/caseReducer';
import { type ILockData, MY_CASE_ITEM, setLockData, VisitPlanStatus } from '../reducer/userSlice';
import { setFilters } from '../reducer/filtersSlice';
import { ToastMessages } from '../screens/allCases/constants';
import { setCurrentProdAPK } from '../reducer/metadataSlice';
import { logError } from '../components/utlis/errorUtils';
import { type GenericFunctionArgs } from '../common/GenericTypes';
import { addClickstreamEvent } from '@services/clickstreamEventService';
import { setCsaFilters } from '@reducers/cosmosSupportSlice';
import { setActiveCallData, setCallingFeedbackNudgeBottomSheet } from '@reducers/activeCallSlice';
import { FEEDBACK_NUDGE_STATUS } from '@screens/caseDetails/CallingFlow/interfaces';
import { updateCases } from '@screens/caseDetails/utils/caseDetailsUtils';
import { firestoreService } from '@services/firestoreService';
export interface CaseUpdates {
updateType: string;
@@ -31,13 +29,12 @@ export const loggedOutCurrentUser = async () => {
}
};
const { subscribeToDoc, subscribeToCollection, unsubscribeAll } = firestoreService;
const isUserSignedIn = () => !!auth().currentUser;
const useFirestoreUpdates = () => {
const isTeamLead = useAppSelector((state) => state.user.isTeamLead);
const caseDetails = useAppSelector((state) => state.allCases.caseDetails);
const casesList = useAppSelector((state) => state.allCases.casesList);
const user = useAppSelector((state) => state.user.user);
const isLoggedIn = useAppSelector((state) => state.user.isLoggedIn);
const sessionDetails = useAppSelector((state) => state.user.sessionDetails);
@@ -50,82 +47,17 @@ const useFirestoreUpdates = () => {
useEffect(() => {
lockRef.current = lock;
}, [lock]);
let casesUnsubscribe: GenericFunctionArgs;
let collectionTemplateUnsubscribe: GenericFunctionArgs;
let filterUnsubscribe: GenericFunctionArgs;
let lockUnsubscribe: GenericFunctionArgs;
let feedbackFiltersUnsubscribe: GenericFunctionArgs;
let appUpdateUnsubscribe: GenericFunctionArgs;
let csaFiltersUnsubscribe: GenericFunctionArgs;
let activeCallDetailsUnsubscribe: GenericFunctionArgs;
const dispatch = useAppDispatch();
const showCaseUpdationToast = (newlyAddedCases: number, deletedCases: number) => {
let toastConfig: any = null;
const addedCasesText = newlyAddedCases
? `${newlyAddedCases} new case${newlyAddedCases > 1 ? 's' : ''} allocated`
: '';
const deletedCasesText = deletedCases
? `${deletedCases} case${deletedCases > 1 ? 's' : ''} de-allocated`
: '';
if (newlyAddedCases && deletedCases) {
toastConfig = {
type: 'info',
text1: `${addedCasesText} & ${deletedCasesText}`,
};
} else if (newlyAddedCases) {
toastConfig = { type: 'success', text1: addedCasesText };
} else if (deletedCases) {
toastConfig = { type: 'error', text1: deletedCasesText };
}
if (toastConfig) {
toast(toastConfig);
}
};
const handleCasesUpdate = async (querySnapshot: FirebaseFirestoreTypes.QuerySnapshot) => {
let newlyAddedCases = 0;
let deletedCases = 0;
const caseUpdates: CaseUpdates[] = [];
querySnapshot
.docChanges()
.forEach((documentSnapshot: FirebaseFirestoreTypes.DocumentChange) => {
InteractionManager.runAfterInteractions(() => {
const updateType = documentSnapshot.type;
const updatedCaseDetail = documentSnapshot.doc.data() as CaseDetail;
if (updateType === FirestoreUpdateTypes.ADDED) {
if (!caseDetails[updatedCaseDetail.id]) {
newlyAddedCases++;
caseUpdates.push({ updateType, updatedCaseDetail });
}
} else {
if (updateType === FirestoreUpdateTypes.REMOVED) {
deletedCases++;
}
caseUpdates.push({ updateType, updatedCaseDetail });
}
});
});
const isInitialLoad = casesList.length === 0;
InteractionManager.runAfterInteractions(() => {
requestAnimationFrame(() => {
InteractionManager.runAfterInteractions(() => {
dispatch(
updateCaseDetailsFirestore({
caseUpdates,
isInitialLoad,
isVisitPlanLocked: lockRef?.current?.visitPlanStatus === VisitPlanStatus.LOCKED,
selectedAgent,
})
);
});
});
});
!isInitialLoad && showCaseUpdationToast(newlyAddedCases, deletedCases);
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_SNAPSHOT_LISTENER, {
snapshot_path: 'handleCasesUpdate',
});
const isVisitPlanLocked = lockRef?.current?.visitPlanStatus === VisitPlanStatus.LOCKED;
dispatch(
updateCases({
selectedAgent,
isVisitPlanLocked,
querySnapshot,
})
);
};
const handleCollectionTemplateUpdate = (
@@ -166,17 +98,12 @@ const useFirestoreUpdates = () => {
snapshot: FirebaseFirestoreTypes.DocumentSnapshot<FirebaseFirestoreTypes.DocumentData>
) => {
const activeCallDetails = snapshot.data();
if(activeCallDetails?.feedbackNudgeStatus === FEEDBACK_NUDGE_STATUS.PENDING) {
if (activeCallDetails?.feedbackNudgeStatus === FEEDBACK_NUDGE_STATUS.PENDING) {
dispatch(setCallingFeedbackNudgeBottomSheet(true));
}
dispatch(setActiveCallData(activeCallDetails));
};
const handleError = (err: any, collectionPath?: string) => {
const errMsg = `Error while fetching fireStore snapshot: referenceId: ${user?.referenceId} collectionPath: ${collectionPath}`;
logError(err as Error, errMsg);
};
const handleAppUpdate = (
snapshot: FirebaseFirestoreTypes.DocumentSnapshot<FirebaseFirestoreTypes.DocumentData>
) => {
@@ -184,107 +111,51 @@ const useFirestoreUpdates = () => {
dispatch(setCurrentProdAPK(configData));
};
const subscribeToAppUpdate = () => {
const collectionPath = 'app-state/app-update';
return subscribeToDoc(handleAppUpdate, collectionPath);
};
const signInUserToFirebase = () => {
if (!sessionDetails) {
dispatch(setLoading(false));
return;
}
auth()
.signInWithCustomToken(sessionDetails?.firebaseToken)
.then((userCredential) => {
addFirestoreListeners();
})
.catch((error) => {
firestoreService.signInUserToFirebase(
sessionDetails.firebaseToken,
subscribeToFirestore,
() => {
dispatch(setLoading(false));
logError(error as Error, 'Error in signInUserToFirebase');
toast({
type: 'error',
text1: ToastMessages.FIRESTORE_SIGNIN_FAILED,
});
});
};
const subscribeToDoc = (successCb: GenericFunctionArgs, collectionPath: string) =>
firestore()
.doc(collectionPath)
.onSnapshot(
async (data) => {
successCb(data);
},
(err) => {
handleError(err, collectionPath);
}
);
const subscribeToCases = () => {
let refId = user?.referenceId;
if (isTeamLead && selectedAgent?.referenceId !== MY_CASE_ITEM.referenceId) {
refId = selectedAgent?.referenceId;
}
const collectionPath = `allocations/${refId}/cases`;
return firestore()
.collection(collectionPath)
.orderBy('totalOverdueAmount', 'asc') // It is descending order only, but acting weirdly. Need to check.
.onSnapshot(handleCasesUpdate, (err) => {
handleError(err, collectionPath);
});
};
const subscribeToCollectionTemplate = () => {
const collectionPath = `template/${isExternalAgent ? 'external' : 'inhouse'}_template`;
return subscribeToDoc(handleCollectionTemplateUpdate, collectionPath);
};
const subscribeToFilters = () => {
let refId = user?.referenceId;
if (isTeamLead && selectedAgent?.referenceId !== MY_CASE_ITEM.referenceId) {
refId = selectedAgent?.referenceId;
}
const collectionPath = `filters/${refId}`;
return subscribeToDoc(handleFilterUpdate, collectionPath);
};
const subscribeToFeedbackFilters = () => {
const feedbackFiltersPath = `feedback-filters/v2`;
return subscribeToDoc(handleFeedbackFilters, feedbackFiltersPath);
};
const subscribeToLocks = () => {
const lockPath = `locks/${user?.referenceId}`;
return subscribeToDoc(handleLockUpdate, lockPath);
};
const subscribeToCsaFilters = () => {
const collectionPath = 'global-filters/v1';
return subscribeToDoc(handleCsaFilters, collectionPath);
};
const subscribeToActiveCallDetailsUnsubscribe = () => {
const refId = user?.referenceId;
const collectionPath = `allocations/${refId}/agentActivity/callDetails`;
return subscribeToDoc(handleActiveCallDetails, collectionPath);
}
);
};
const subscribeToFirestore = () => {
addFirestoreListeners();
let refId = user?.referenceId;
if (isTeamLead && selectedAgent?.referenceId !== MY_CASE_ITEM.referenceId) {
refId = selectedAgent?.referenceId;
}
InteractionManager.runAfterInteractions(() => {
subscribeToCollection(
`allocations/${refId}/cases`,
(ref) => ref.orderBy('totalOverdueAmount', 'asc'),
handleCasesUpdate
);
subscribeToDoc(`filters/${refId}`, handleFilterUpdate);
subscribeToDoc(
`template/${isExternalAgent ? 'external' : 'inhouse'}_template`,
handleCollectionTemplateUpdate
);
subscribeToDoc(`locks/${user?.referenceId}`, handleLockUpdate);
subscribeToDoc(`feedback-filters/v2`, handleFeedbackFilters);
subscribeToDoc('global-filters/v1', handleCsaFilters);
subscribeToDoc(
`allocations/${user?.referenceId}/agentActivity/callDetails`,
handleActiveCallDetails
);
subscribeToDoc('app-state/app-update', handleAppUpdate);
});
};
function addFirestoreListeners() {
casesUnsubscribe = subscribeToCases();
filterUnsubscribe = subscribeToFilters();
collectionTemplateUnsubscribe = subscribeToCollectionTemplate();
lockUnsubscribe = subscribeToLocks();
feedbackFiltersUnsubscribe = subscribeToFeedbackFilters();
csaFiltersUnsubscribe = subscribeToCsaFilters();
activeCallDetailsUnsubscribe = subscribeToActiveCallDetailsUnsubscribe();
appUpdateUnsubscribe = subscribeToAppUpdate();
}
useEffect(() => {
if (!user?.referenceId) {
return;
@@ -294,48 +165,12 @@ const useFirestoreUpdates = () => {
return;
}
if (isUserSignedIn()) {
InteractionManager.runAfterInteractions(() => {
subscribeToFirestore();
});
subscribeToFirestore();
} else {
dispatch(setLoading(true));
signInUserToFirebase();
}
return () => {
casesUnsubscribe && casesUnsubscribe();
filterUnsubscribe && filterUnsubscribe();
collectionTemplateUnsubscribe && collectionTemplateUnsubscribe();
lockUnsubscribe && lockUnsubscribe();
feedbackFiltersUnsubscribe && feedbackFiltersUnsubscribe();
csaFiltersUnsubscribe && csaFiltersUnsubscribe();
activeCallDetailsUnsubscribe && activeCallDetailsUnsubscribe();
appUpdateUnsubscribe && appUpdateUnsubscribe();
};
}, [isLoggedIn, user?.referenceId, isExternalAgent]);
useEffect(() => {
if (!isTeamLead) {
return;
}
if (!selectedAgent?.referenceId) {
return;
}
if (!isLoggedIn || !sessionDetails?.firebaseToken) {
loggedOutCurrentUser();
return;
}
// unsubscribe from previous agent's cases
casesUnsubscribe && casesUnsubscribe();
filterUnsubscribe && filterUnsubscribe();
dispatch(setLoading(true));
// subscribe to new agent's cases
subscribeToCases();
subscribeToFilters();
return () => {
casesUnsubscribe && casesUnsubscribe();
filterUnsubscribe && filterUnsubscribe();
};
}, [selectedAgent, isTeamLead]);
}, [isLoggedIn, user?.referenceId, isExternalAgent, selectedAgent]);
};
export default useFirestoreUpdates;

View File

@@ -1,20 +1,19 @@
import firestore from '@react-native-firebase/firestore';
import { useAppDispatch, useAppSelector } from '@hooks';
import store, { type RootState } from '@store';
import { updateCaseDetailsFirestore } from '@reducers/allCasesSlice';
import { useAppDispatch } from '@hooks';
import store from '@store';
import { CLICKSTREAM_EVENT_NAMES, FirestoreUpdateTypes, SyncedSource } from '@common/Constants';
import axiosInstance, { ApiKeys, getApiUrl } from '@utils/apiHelper';
import { getSyncCaseIds } from '@utils/firebaseFallbackUtils';
import { logError } from '@utils/errorUtils';
import { addClickstreamEvent } from '@services/clickstreamEventService';
import { GenericObject } from '@common/GenericTypes';
import { setLastFirebaseResyncTimestamp } from '@reducers/metadataSlice';
import dayJs from 'dayjs';
import {
getEnableFirestoreResync,
getFirestoreResyncIntervalInMinutes,
} from '@common/AgentActivityConfigurableConstants';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { updateCases } from '@screens/caseDetails/utils/caseDetailsUtils';
import { CaseAllocationType } from '@screens/allCases/interface';
import { CaseDetail } from '@screens/caseDetails/interface';
import { CaseUpdates } from './useFirestoreUpdates';
import { getFirestoreResyncIntervalInMinutes } from '@common/AgentActivityConfigurableConstants';
const selectedAgentReferenceIDForMyCases = 'MY_CASES';
@@ -25,9 +24,9 @@ type CasesToFetchPayload = {
updatedCaseIds: string[];
};
};
const useResyncFirebase = () => {
const dispatch = useAppDispatch();
const refId = store?.getState()?.user?.user?.referenceId || '';
const selectedAgent = store?.getState()?.user?.selectedAgent;
const selectedAgentRefId = store?.getState()?.user?.selectedAgent?.referenceId || '';
@@ -46,36 +45,126 @@ const useResyncFirebase = () => {
});
};
const updateCaseInRedux = (
updateType: FirestoreUpdateTypes,
caseDetails: GenericObject,
selectedAgent: GenericObject
const fetchFirestoreCases = async (
caseIds: string[],
casesPath: string,
updateType: string = FirestoreUpdateTypes.ADDED
) => {
const caseDocs = await firestore()
.collection(casesPath)
.where('caseReferenceId', 'in', caseIds)
.get();
const firebaseAllocatedCases: CaseUpdates[] = [];
caseDocs?.forEach((doc) => {
const firebaseCase = doc.data() as CaseDetail;
if (!firebaseCase?.caseReferenceId) {
return;
}
firebaseAllocatedCases.push({
updateType,
updatedCaseDetail: firebaseCase,
});
void addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_FIREBASE_RESYNC, {
[updateType === FirestoreUpdateTypes.ADDED ? 'added' : 'update']:
firebaseCase.caseReferenceId,
syncedSource: SyncedSource.FIREBASE,
});
});
// If firebase case is not found, fetch from API
const firebaseCaseIdsSet = new Set(
firebaseAllocatedCases.map((firebaseCase) => firebaseCase.updatedCaseDetail.caseReferenceId)
);
const caseIdsToFetch = caseIds.filter((caseId) => !firebaseCaseIdsSet.has(caseId));
for (const caseId of caseIdsToFetch) {
try {
const res = await _getCaseDetailsFromApi(caseId);
const caseDetails = res?.data;
firebaseAllocatedCases.push({
updateType,
updatedCaseDetail: caseDetails,
});
void addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_FIREBASE_RESYNC, {
updated: caseId,
syncedSource: SyncedSource.API,
});
} catch (err) {
logError(err as Error, 'Error fetching cases from firestore');
}
}
return firebaseAllocatedCases;
};
const addCasesToRedux = async (allocatedCases: string[], casesPath: string) => {
if (!allocatedCases.length) {
return;
}
const firebaseAllocatedCases = await fetchFirestoreCases(allocatedCases, casesPath);
dispatch(
updateCaseDetailsFirestore({
caseUpdates: [
{
updateType: updateType,
updatedCaseDetail: caseDetails,
},
],
isInitialLoad: false,
isVisitPlanLocked: false,
updateCases({
selectedAgent,
caseUpdateList: firebaseAllocatedCases,
})
);
};
const removeCasesFromRedux = (unallocatedCases: string[]) => {
if (!unallocatedCases.length) {
return;
}
const unallocatedCasesUpdates: CaseUpdates[] = [];
unallocatedCases.forEach((caseId: string) => {
if (!caseId) {
return null;
}
unallocatedCasesUpdates.push({
updateType: FirestoreUpdateTypes.REMOVED,
updatedCaseDetail: {
caseType: CaseAllocationType.COLLECTION_CASE,
caseReferenceId: caseId,
id: '',
pinRank: null,
caseViewCreatedAt: 0,
} as CaseDetail,
});
void addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_FIREBASE_RESYNC, { deleted: caseId });
});
dispatch(
updateCases({
selectedAgent,
caseUpdateList: unallocatedCasesUpdates,
})
);
};
const modifyCasesInRedux = async (updatedCases: string[], casesPath: string) => {
if (!updatedCases.length) {
return;
}
const firebaseUpdatedCases = await fetchFirestoreCases(
updatedCases,
casesPath,
FirestoreUpdateTypes.MODIFIED
);
dispatch(
updateCases({
selectedAgent,
caseUpdateList: firebaseUpdatedCases,
})
);
};
return async (): Promise<void> => {
console.log('firebase resync called');
const now = dayJs().toString();
const FIRST_DATE = new Date(1970, 1, 1);
const lastFirebaseResyncTimestamp =
(await AsyncStorage.getItem('lastFirebaseResyncTimestamp')) || dayJs(FIRST_DATE).toString();
const minutesSinceLastResync = dayJs(now).diff(dayJs(lastFirebaseResyncTimestamp), 'minutes');
if (!getEnableFirestoreResync()) {
return;
}
if (minutesSinceLastResync < getFirestoreResyncIntervalInMinutes()) {
return;
}
@@ -93,117 +182,16 @@ const useResyncFirebase = () => {
}
);
const allocatedCases = casesToFetch?.data?.allocatedCaseIds;
const unallocatedCases = casesToFetch?.data?.deallocatedCaseIds;
const updatedCases = casesToFetch?.data?.updatedCaseIds;
const allocatedCases = casesToFetch?.data?.allocatedCaseIds || [];
const unallocatedCases = casesToFetch?.data?.deallocatedCaseIds || [];
const updatedCases = casesToFetch?.data?.updatedCaseIds || [];
allocatedCases.forEach((caseId: string) => {
if (!caseId) {
return null;
}
firestore()
.collection(casesPath)
.doc(caseId.toString())
.get({ source: 'server' })
.then((res) => {
const firebaseCase = res?.data() || {};
if (!firebaseCase?.caseReferenceId) {
throw new Error('could not find case in firebase');
}
dispatch(
updateCaseDetailsFirestore({
caseUpdates: [
{
updateType: FirestoreUpdateTypes.ADDED,
updatedCaseDetail: firebaseCase,
},
],
isInitialLoad: false,
isVisitPlanLocked: false,
selectedAgent,
})
);
void addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_FIREBASE_RESYNC, {
added: caseId,
syncedSource: SyncedSource.FIREBASE,
});
})
.catch((err) => {
logError(err as Error, 'Error fetching cases from firestore');
void _getCaseDetailsFromApi(caseId).then((res: { data: GenericObject }) => {
const caseDetails = res?.data;
updateCaseInRedux(FirestoreUpdateTypes.ADDED, caseDetails, selectedAgent);
void addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_FIREBASE_RESYNC, {
updated: caseId,
syncedSource: SyncedSource.API,
});
});
});
});
unallocatedCases.forEach((caseId: string) => {
if (!caseId) {
return null;
}
dispatch(
updateCaseDetailsFirestore({
caseUpdates: [
{
updateType: FirestoreUpdateTypes.REMOVED,
updatedCaseDetail: {
caseType: '',
caseReferenceId: caseId,
id: '',
pinRank: '',
caseViewCreatedAt: '',
},
},
],
isInitialLoad: false,
isVisitPlanLocked: false,
selectedAgent,
})
);
void addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_FIREBASE_RESYNC, { deleted: caseId });
});
updatedCases.forEach((caseId: string) => {
if (!caseId) {
return null;
}
firestore()
.collection(casesPath)
.doc(caseId.toString())
.get({ source: 'server' })
.then((res) => {
const firebaseCase = res?.data() || {};
if (!firebaseCase?.caseReferenceId) {
throw new Error('could not find case in firebase');
}
updateCaseInRedux(FirestoreUpdateTypes.MODIFIED, firebaseCase, selectedAgent);
void addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_FIREBASE_RESYNC, {
updated: caseId,
syncedSource: SyncedSource.FIREBASE,
});
})
.catch((err) => {
void _getCaseDetailsFromApi(caseId).then((res: { data: GenericObject }) => {
const caseDetails = res?.data || {};
updateCaseInRedux(FirestoreUpdateTypes.MODIFIED, caseDetails, selectedAgent);
void addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_FIREBASE_RESYNC, {
updated: caseId,
syncedSource: SyncedSource.API,
});
});
logError(err as Error, 'Error fetching cases from firestore');
console.log('cases err:', err);
});
});
addCasesToRedux(allocatedCases, casesPath);
removeCasesFromRedux(unallocatedCases);
modifyCasesInRedux(updatedCases, casesPath);
dispatch(setLastFirebaseResyncTimestamp(dayJs().toString()));
await AsyncStorage.setItem('lastFirebaseResyncTimestamp', dayJs().toString());
await Promise.resolve();
};
};

View File

@@ -1,49 +1,32 @@
import { createSlice } from '@reduxjs/toolkit';
import { toast } from '../../RN-UI-LIB/src/components/toast';
import { _map } from '../../RN-UI-LIB/src/utlis/common';
import {
findDocumentByDocumentType,
getLoanAccountNumber,
} from '../components/utlis/commonFunctions';
import { CLICKSTREAM_EVENT_NAMES, FirestoreUpdateTypes } from '../common/Constants';
import { getCurrentScreen, navigateToScreen } from '../components/utlis/navigationUtlis';
import { type CaseUpdates } from '../hooks/useFirestoreUpdates';
import { CLICKSTREAM_EVENT_NAMES } from '../common/Constants';
import { navigateToScreen } from '../components/utlis/navigationUtlis';
import { COMPLETED_STATUSES } from '../screens/allCases/constants';
import { CaseAllocationType, type ICaseItem, type IReportee } from '../screens/allCases/interface';
import {
type CaseDetail,
DOCUMENT_TYPE,
type IGeolocation,
} from '../screens/caseDetails/interface';
import { CaseAllocationType, type ICaseItem } from '../screens/allCases/interface';
import { type CaseDetail } from '../screens/caseDetails/interface';
import { addClickstreamEvent } from '../services/clickstreamEventService';
import { getVisitedWidgetsNodeList } from '../components/form/services/forms.service';
import { CollectionCaseWidgetId, CommonCaseWidgetId } from '../types/template.types';
import { type IAvatarUri } from '../action/caseListAction';
import { MY_CASE_ITEM } from './userSlice';
export type ICasesMap = Record<string, ICaseItem>;
interface IAllCasesSlice {
casesList: ICaseItem[];
casesListMap: ICasesMap;
intermediateTodoList: ICaseItem[];
intermediateTodoListMap: ICasesMap;
selectedTodoListMap: ICasesMap;
selectedTodoListCount: number;
initialPinnedRankCount: number;
pinnedRankCount: number;
loading: boolean;
newlyPinnedCases: number;
completedCases: number;
caseDetails: Record<string, CaseDetail>;
searchQuery: string;
visitPlansUpdating: boolean;
pendingList: ICaseItem[];
completedList: ICaseItem[];
pinnedList: ICaseItem[];
newVisitedCases: string[];
geolocations?: IGeolocation[];
selectedCaseId: string;
allCasesViewSearchQuery: string;
visitPlanSearchQuery: string;
@@ -51,45 +34,24 @@ interface IAllCasesSlice {
const initialState: IAllCasesSlice = {
casesList: [],
casesListMap: {},
intermediateTodoList: [],
intermediateTodoListMap: {},
selectedTodoListCount: 0,
selectedTodoListMap: {},
initialPinnedRankCount: 0,
pinnedRankCount: 0,
loading: false,
newlyPinnedCases: 0,
completedCases: 0,
caseDetails: {},
searchQuery: '',
visitPlansUpdating: false,
pendingList: [],
completedList: [],
pinnedList: [],
newVisitedCases: [],
selectedCaseId: '',
allCasesViewSearchQuery: '',
visitPlanSearchQuery: '',
};
const getCaseListComponents = (casesList: ICaseItem[], caseDetails: Record<string, CaseDetail>) => {
const pendingList: ICaseItem[] = [];
const completedList: ICaseItem[] = [];
const pinnedList: ICaseItem[] = [];
casesList.forEach((item) => {
const { caseReferenceId, pinRank } = item;
const { caseStatus } = caseDetails[caseReferenceId] || {};
const isCaseCompleted = COMPLETED_STATUSES.includes(caseStatus);
isCaseCompleted
? completedList.push(item)
: pinRank
? pinnedList.push(item)
: pendingList.push(item);
});
return { pendingList, completedList, pinnedList };
};
export const getUpdatedCollectionCaseDetail = ({
caseData,
answer,
@@ -101,8 +63,6 @@ export const getUpdatedCollectionCaseDetail = ({
updatedValue.isSynced = false;
updatedValue.isApiCalled = false;
updatedValue.taskStatus = 'completed';
// @deprecating
const { visitedWidgets } = answer;
const allWidget = answer.widgetContext;
const widgetContext = {};
@@ -127,16 +87,6 @@ export const getUpdatedCollectionCaseDetail = ({
return updatedValue;
};
const getCaseListItem = (
caseReferenceId: string,
pinRank?: number | null,
caseViewCreatedAt?: number
) => ({
caseReferenceId,
pinRank: pinRank || null,
caseViewCreatedAt,
});
const allCasesSlice = createSlice({
name: 'cases',
initialState,
@@ -144,166 +94,21 @@ const allCasesSlice = createSlice({
setLoading: (state, action) => {
state.loading = action.payload;
},
updateCaseDetailsFirestore: (state, action) => {
const { caseUpdates, isInitialLoad, isVisitPlanLocked, selectedAgent } = action.payload as {
caseUpdates: CaseUpdates[];
isInitialLoad: boolean;
isVisitPlanLocked: boolean;
selectedAgent: IReportee;
};
const newVisitCaseLoanIds: string[] = [];
const newVisitCollectionCases: string[] = [];
const removedVisitedCasesLoanIds: string[] = [];
caseUpdates.forEach(({ updateType, updatedCaseDetail }) => {
const { caseType, caseReferenceId, id, pinRank, caseViewCreatedAt } = updatedCaseDetail;
const caseId = caseReferenceId || id;
switch (updateType) {
case FirestoreUpdateTypes.MODIFIED: {
const index = state.casesList?.findIndex(
(caseItem) => caseItem.caseReferenceId?.toString() === caseId?.toString()
);
if (index !== -1) {
if (pinRank && !state.casesList[index].pinRank) {
// this is a new visit case
newVisitCaseLoanIds.push(
state.caseDetails[caseId]?.loanAccountNumber ??
state.caseDetails[caseId]?.loanDetails?.loanAccountNumber ??
0
);
if (caseType === CaseAllocationType.COLLECTION_CASE) {
newVisitCollectionCases.push(caseId);
}
}
if (!pinRank && state.casesList[index].pinRank) {
// this is a removed visit case
removedVisitedCasesLoanIds.push(
state.caseDetails[caseId]?.loanAccountNumber ??
state.caseDetails[caseId]?.loanDetails?.loanAccountNumber ??
0
);
}
state.casesList[index] = {
...state.casesList[index],
caseReferenceId: caseId,
pinRank: pinRank || null,
caseViewCreatedAt: caseViewCreatedAt,
};
}
let currentTask = null;
if (caseType !== CaseAllocationType.COLLECTION_CASE) {
const { tasks, currentTask: updatedCurrentTask } = updatedCaseDetail;
currentTask = tasks?.find((task) => task.taskType === (updatedCurrentTask as string));
}
state.caseDetails[caseId] = {
...updatedCaseDetail,
currentTask,
isSynced: true,
imageUri: state.caseDetails[caseId]?.imageUri,
};
break;
}
case FirestoreUpdateTypes.ADDED: {
if (state.caseDetails[caseId]) {
return;
}
if (pinRank && caseType === CaseAllocationType.COLLECTION_CASE) {
newVisitCollectionCases.push(caseId);
}
const caseListItem = getCaseListItem(caseId, pinRank, caseViewCreatedAt);
state.casesList.unshift(caseListItem);
let currentTask = null;
if (caseType !== CaseAllocationType.COLLECTION_CASE) {
const { tasks, currentTask: updatedCurrentTask } = updatedCaseDetail;
currentTask = tasks?.find(
(task) => task?.taskType === (updatedCurrentTask as string)
);
}
const imageUri =
findDocumentByDocumentType(
updatedCaseDetail.documents,
DOCUMENT_TYPE.OPTIMIZED_SELFIE
)?.uri || '';
state.caseDetails[caseId] = {
...updatedCaseDetail,
currentTask,
isSynced: true,
isNewlyAdded: !isInitialLoad,
imageUri,
};
break;
}
case FirestoreUpdateTypes.REMOVED: {
const index = state.casesList.findIndex(
(caseItem) => caseItem.caseReferenceId?.toString() === caseId?.toString()
);
const currentScreen = getCurrentScreen();
// Redirect to home screen if the case deletes which the agent is seeing
if (currentScreen?.name === 'caseDetail') {
const { caseId: id } = currentScreen.params;
if (id === caseId) {
navigateToScreen('Home');
}
}
if (index !== -1) {
state.casesList.splice(index, 1);
}
delete state.caseDetails[caseId];
break;
}
default:
break;
}
});
const { pendingList, completedList, pinnedList } = getCaseListComponents(
state.casesList,
state.caseDetails
);
updateCaseDetailsFromFirestore: (state, action) => {
const {
updatedCasesList,
updatedCaseDetails,
updatedLoading,
pendingList,
completedList,
pinnedList,
} = action.payload;
state.casesList = updatedCasesList;
state.caseDetails = updatedCaseDetails;
state.loading = updatedLoading;
state.pendingList = pendingList;
state.completedList = completedList;
state.pinnedList = pinnedList;
state.newVisitedCases = newVisitCollectionCases;
if (state.loading) {
if (selectedAgent && selectedAgent.referenceId !== MY_CASE_ITEM.referenceId) {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_AGENT_CASE_LOAD_SUCCESS, {
selectedAgent: selectedAgent.referenceId,
});
}
state.loading = false;
}
if (newVisitCaseLoanIds?.length > 0) {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_VISIT_PLAN_UPDATED, {
newPinCases: [...newVisitCaseLoanIds],
currentPinCases: pinnedList.map((item) =>
getLoanAccountNumber(state.caseDetails[item?.caseReferenceId])
),
});
if (!isVisitPlanLocked) {
toast({
type: 'info',
text1: `${newVisitCaseLoanIds.length} case${
newVisitCaseLoanIds.length > 1 ? 's' : ''
} added to the visit plan`,
});
}
}
if (removedVisitedCasesLoanIds.length > 0) {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_VISIT_PLAN_UPDATED, {
newUnpinCases: [...removedVisitedCasesLoanIds],
currentPinCases: pinnedList.map((item) =>
getLoanAccountNumber(state.caseDetails[item?.caseReferenceId])
),
});
if (!isVisitPlanLocked) {
toast({
type: 'info',
text1: `${removedVisitedCasesLoanIds.length} case${
removedVisitedCasesLoanIds.length > 1 ? 's' : ''
} removed from the visit plan`,
});
}
}
},
updateCaseDetail: (state, action) => {
const { caseKey, updatedCaseDetail } = action.payload;
@@ -348,7 +153,7 @@ const allCasesSlice = createSlice({
resetTodoList: (state) => {
state.intermediateTodoListMap = {};
state.newlyPinnedCases = 0;
state.pinnedRankCount = state.initialPinnedRankCount;
state.pinnedRankCount = 0;
},
setSelectedTodoListMap: (state, action: { payload: ICaseItem }) => {
const caseId = action.payload.caseReferenceId;
@@ -405,10 +210,6 @@ const allCasesSlice = createSlice({
state.caseDetails[id].isSynced = false;
}
},
updateCaseDetailBeforeApiCall: (state, action) => {
const { caseId } = action.payload;
state.caseDetails[caseId].isApiCalled = false;
},
toggleNewlyAddedCase: (state, action) => {
if (state.caseDetails[action.payload]) {
state.caseDetails[action.payload].isNewlyAdded = false;
@@ -418,44 +219,6 @@ const allCasesSlice = createSlice({
setVisitPlansUpdating: (state, action) => {
state.visitPlansUpdating = action.payload;
},
resetNewVisitedCases: (state) => {
state.newVisitedCases = [];
},
syncCasesByFallback: (state, action) => {
const { cases = [], deletedCaseIds = [], payloadCreatedAt } = action.payload;
cases.forEach((caseItem: CaseDetail | null) => {
if (!caseItem) {
return;
}
const { caseViewCreatedAt, caseReferenceId, isSynced, pinRank } = caseItem;
const isCaseAlreadyPresent = state.caseDetails[caseReferenceId];
if (
!isCaseAlreadyPresent ||
(isSynced && caseViewCreatedAt && caseViewCreatedAt < payloadCreatedAt)
) {
const caseListItem = getCaseListItem(caseReferenceId, pinRank, caseViewCreatedAt);
state.casesList.unshift(caseListItem);
const imageUri = isCaseAlreadyPresent
? state.caseDetails[caseReferenceId]?.imageUri
: findDocumentByDocumentType(caseItem.documents, DOCUMENT_TYPE.OPTIMIZED_SELFIE)?.uri ||
'';
state.caseDetails[caseReferenceId] = { ...caseItem, isSynced: true, imageUri };
}
});
const { pendingList, completedList, pinnedList } = getCaseListComponents(
state.casesList,
state.caseDetails
);
state.pendingList = pendingList;
state.completedList = completedList;
state.pinnedList = pinnedList;
deletedCaseIds.forEach((caseItem: CaseDetail) => {
const { caseViewCreatedAt, caseReferenceId } = caseItem;
if (caseViewCreatedAt && caseViewCreatedAt < payloadCreatedAt) {
delete state.caseDetails[caseReferenceId];
}
});
},
setCasesImageUri: (state, action) => {
const imageUris: IAvatarUri[] = action.payload;
imageUris.forEach(({ caseId, imageUri }) => {
@@ -486,17 +249,14 @@ export const {
resetSelectedTodoList,
updateCaseDetail,
updateSingleCase,
updateCaseDetailsFirestore,
toggleNewlyAddedCase,
resetCasesData,
updateCaseDetailBeforeApiCall,
setVisitPlansUpdating,
resetNewVisitedCases,
syncCasesByFallback,
setCasesImageUri,
setSelectedCaseId,
setAllCasesViewSearchQuery,
setVisitPlanSearchQuery,
updateCaseDetailsFromFirestore,
} = allCasesSlice.actions;
export default allCasesSlice.reducer;

View File

@@ -7,7 +7,7 @@ import { COLORS } from '../../../RN-UI-LIB/src/styles/colors';
import { useAppDispatch, useAppSelector } from '../../hooks';
import { setShowAgentSelectionBottomSheet } from '../../reducer/reporteesSlice';
import { IReportee } from './interface';
import { resetCasesData } from '../../reducer/allCasesSlice';
import { resetCasesData, setLoading } from '@reducers/allCasesSlice';
import fuzzySort from '../../../RN-UI-LIB/src/utlis/fuzzySort';
import fuzzysort from 'fuzzysort';
import { MY_CASE_ITEM, setSelectedAgent } from '../../reducer/userSlice';
@@ -15,6 +15,7 @@ import { resetFilters } from '../../reducer/filtersSlice';
import { setGlobalUserData } from '../../constants/Global';
import { addClickstreamEvent } from '../../services/clickstreamEventService';
import { CLICKSTREAM_EVENT_NAMES } from '../../common/Constants';
import { firestoreService } from '@services/firestoreService';
interface IAgentListItem {
agent: IReportee;
@@ -22,8 +23,17 @@ interface IAgentListItem {
searchQuery: string;
}
const unsubscribeFromPreviousDoc = (agentId?: string) => {
if (!agentId) {
return;
}
firestoreService.unsubscribeFromPath(`allocations/${agentId}/cases`);
firestoreService.unsubscribeFromPath(`filters/${agentId}`);
};
const AgentListItem: React.FC<IAgentListItem> = ({ agent, leftAdornment, searchQuery }) => {
const selectedAgent = useAppSelector((state) => state.user.selectedAgent) || MY_CASE_ITEM;
const userReferenceId = useAppSelector((state) => state.user.user?.referenceId);
const dispatch = useAppDispatch();
const handleAgentSelection = () => {
@@ -31,10 +41,16 @@ const AgentListItem: React.FC<IAgentListItem> = ({ agent, leftAdornment, searchQ
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_AGENT_SELECT_BUTTON_CLICKED, {
selectedAgentId,
});
if (selectedAgentId !== MY_CASE_ITEM.referenceId) {
unsubscribeFromPreviousDoc(selectedAgent.referenceId);
} else {
unsubscribeFromPreviousDoc(userReferenceId);
}
dispatch(setSelectedAgent(agent));
setGlobalUserData({ selectedAgentId });
dispatch(resetFilters());
dispatch(resetCasesData());
dispatch(setLoading(true));
dispatch(setShowAgentSelectionBottomSheet(false));
};

View File

@@ -125,7 +125,6 @@ const AllCasesMain = () => {
initCrashlytics(userState);
}
dispatch(setVisitPlansUpdating(false));
dispatch(setLoading(false));
dispatch(resetTodoList());
dispatch(resetSelectedTodoList());
}, []);

View File

@@ -0,0 +1,253 @@
import { FirebaseFirestoreTypes } from '@react-native-firebase/firestore';
import { ICaseItem, IReportee } from '@screens/allCases/interface';
import { InteractionManager } from 'react-native';
import { CaseDetail, DOCUMENT_TYPE } from '../interface';
import { CLICKSTREAM_EVENT_NAMES, FirestoreUpdateTypes } from '@common/Constants';
import store, { AppDispatch } from '@store';
import {
findDocumentByDocumentType,
getLoanAccountNumber,
} from '@components/utlis/commonFunctions';
import { getCurrentScreen, navigateToScreen } from '@components/utlis/navigationUtlis';
import { COMPLETED_STATUSES } from '@screens/allCases/constants';
import { MY_CASE_ITEM } from '@reducers/userSlice';
import { addClickstreamEvent } from '@services/clickstreamEventService';
import { toast } from '@rn-ui-lib/components/toast';
import { updateCaseDetailsFromFirestore } from '@reducers/allCasesSlice';
import { _map } from '@rn-ui-lib/utils/common';
interface UpdateCasesProps {
selectedAgent: IReportee | null;
isVisitPlanLocked?: boolean;
querySnapshot?: FirebaseFirestoreTypes.QuerySnapshot;
caseUpdateList?: Array<{ updateType: string; updatedCaseDetail: CaseDetail }>;
}
export const updateCases = (props: UpdateCasesProps) => (dispatch: AppDispatch) => {
const {
selectedAgent,
isVisitPlanLocked = false,
querySnapshot,
caseUpdateList,
} = props;
const caseDetails = store?.getState()?.allCases?.caseDetails || {};
const casesList = store?.getState()?.allCases?.casesList || [];
const loading = store?.getState()?.allCases?.loading || false;
const isInitialLoad = !casesList.length;
let newlyAddedCases = 0;
let deletedCases = 0;
let updatedCaseDetails = { ...caseDetails };
const newVisitCaseLoanIds: string[] = [];
const removedVisitedCasesLoanIds: string[] = [];
const newVisitCollectionCases: string[] = [];
let updatedLoading = loading;
const getCaseListComponents = (caseDetails: Record<string, CaseDetail>) => {
const pendingList: ICaseItem[] = [];
const completedList: ICaseItem[] = [];
const pinnedList: ICaseItem[] = [];
const updatedCasesList: ICaseItem[] = [];
_map(caseDetails, (caseReferenceId: string) => {
if (!caseDetails?.[caseReferenceId]) {
return;
}
const { caseStatus, pinRank, caseViewCreatedAt } = caseDetails?.[caseReferenceId];
const isCaseCompleted = COMPLETED_STATUSES.includes(caseStatus);
const caseItem = { caseReferenceId, pinRank, caseViewCreatedAt } as ICaseItem;
updatedCasesList.push(caseItem);
isCaseCompleted
? completedList.push(caseItem)
: pinRank
? pinnedList.push(caseItem)
: pendingList.push(caseItem);
});
return { pendingList, completedList, pinnedList, updatedCasesList };
};
const handleCaseAddition = (newCaseDetail: CaseDetail) => {
const { caseReferenceId, id, pinRank } = newCaseDetail || {};
const caseId = caseReferenceId || id;
if (caseDetails?.[caseId]) {
return;
}
newlyAddedCases++;
const newVisitCollectionCases: string[] = [];
if (pinRank) {
newVisitCollectionCases.push(caseId);
}
const imageUri =
findDocumentByDocumentType(newCaseDetail.documents, DOCUMENT_TYPE.OPTIMIZED_SELFIE)?.uri ||
'';
const newCaseDetails = {
...newCaseDetail,
isSynced: true,
isNewlyAdded: !isInitialLoad,
imageUri,
};
updatedCaseDetails = { ...updatedCaseDetails, [caseId]: newCaseDetails };
};
const handleCaseModification = (newCaseDetail: CaseDetail) => {
const { caseReferenceId, id, pinRank, caseViewCreatedAt } = newCaseDetail || {};
const caseId = caseReferenceId || id;
if (!updatedCaseDetails?.[caseId]) {
return;
}
const caseDetail = updatedCaseDetails[caseId];
if (pinRank && !caseDetail?.pinRank) {
// this is a new visit case
newVisitCaseLoanIds.push(caseDetails[caseId]?.loanAccountNumber);
newVisitCollectionCases.push(caseId);
}
if (!pinRank && caseDetail?.pinRank) {
// this is a removed visit case
removedVisitedCasesLoanIds.push(caseDetails[caseId]?.loanAccountNumber);
}
const newCaseDetails = {
...newCaseDetail,
isSynced: true,
imageUri: caseDetail?.imageUri,
};
updatedCaseDetails = { ...updatedCaseDetails, [caseId]: newCaseDetails };
};
const handleCaseRemoval = (newCaseDetail: CaseDetail) => {
const { caseReferenceId, id } = newCaseDetail || {};
const caseId = caseReferenceId || id;
if (!updatedCaseDetails?.[caseId]) {
return;
}
const currentScreen = getCurrentScreen();
// Redirect to home screen if the case deletes which the agent is seeing
if (currentScreen?.name === 'caseDetail') {
const { caseId: id } = currentScreen.params;
if (id === caseId) {
navigateToScreen('Home');
}
}
delete updatedCaseDetails[caseId];
deletedCases++;
};
const updateCaseDetails = (newCaseDetail: CaseDetail, updateType: string) => {
switch (updateType) {
case FirestoreUpdateTypes.ADDED:
handleCaseAddition(newCaseDetail);
break;
case FirestoreUpdateTypes.MODIFIED:
handleCaseModification(newCaseDetail);
break;
case FirestoreUpdateTypes.REMOVED:
handleCaseRemoval(newCaseDetail);
break;
default:
}
};
const showCaseUpdationToast = (newlyAddedCases: number, deletedCases: number) => {
let toastConfig: any = null;
const addedCasesText = newlyAddedCases
? `${newlyAddedCases} new case${newlyAddedCases > 1 ? 's' : ''} allocated`
: '';
const deletedCasesText = deletedCases
? `${deletedCases} case${deletedCases > 1 ? 's' : ''} de-allocated`
: '';
if (newlyAddedCases && deletedCases) {
toastConfig = {
type: 'info',
text1: `${addedCasesText} & ${deletedCasesText}`,
};
} else if (newlyAddedCases) {
toastConfig = { type: 'success', text1: addedCasesText };
} else if (deletedCases) {
toastConfig = { type: 'error', text1: deletedCasesText };
}
if (toastConfig) {
toast(toastConfig);
}
};
const processCases = async () => {
if (caseUpdateList?.length) {
// If we are providing cases list directly
caseUpdateList.forEach((caseUpdate) => {
const { updateType, updatedCaseDetail } = caseUpdate;
updateCaseDetails(updatedCaseDetail, updateType);
});
} else {
// If we are providing querySnapshot from firestore subscription
await querySnapshot
?.docChanges()
?.forEach((documentSnapshot: FirebaseFirestoreTypes.DocumentChange) => {
InteractionManager.runAfterInteractions(() => {
const updateType = documentSnapshot.type;
const updatedCaseDetail = documentSnapshot.doc.data() as CaseDetail;
updateCaseDetails(updatedCaseDetail, updateType);
});
});
}
const { pendingList, completedList, pinnedList, updatedCasesList } =
getCaseListComponents(updatedCaseDetails);
if (updatedLoading) {
if (selectedAgent && selectedAgent.referenceId !== MY_CASE_ITEM.referenceId) {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_AGENT_CASE_LOAD_SUCCESS, {
selectedAgent: selectedAgent.referenceId,
});
}
updatedLoading = false;
}
if (newVisitCaseLoanIds?.length > 0) {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_VISIT_PLAN_UPDATED, {
newPinCases: [...newVisitCaseLoanIds],
currentPinCases: pinnedList.map((item) =>
getLoanAccountNumber(updatedCaseDetails[item?.caseReferenceId])
),
});
if (!isVisitPlanLocked) {
toast({
type: 'info',
text1: `${newVisitCaseLoanIds.length} case${
newVisitCaseLoanIds.length > 1 ? 's' : ''
} added to the visit plan`,
});
}
}
if (removedVisitedCasesLoanIds.length > 0) {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_VISIT_PLAN_UPDATED, {
newUnpinCases: [...removedVisitedCasesLoanIds],
currentPinCases: pinnedList.map((item) =>
getLoanAccountNumber(updatedCaseDetails[item?.caseReferenceId])
),
});
if (!isVisitPlanLocked) {
toast({
type: 'info',
text1: `${removedVisitedCasesLoanIds.length} case${
removedVisitedCasesLoanIds.length > 1 ? 's' : ''
} removed from the visit plan`,
});
}
}
dispatch(
updateCaseDetailsFromFirestore({
updatedCasesList,
updatedCaseDetails,
updatedLoading,
pendingList,
completedList,
pinnedList,
})
);
};
requestAnimationFrame(() => {
InteractionManager.runAfterInteractions(async () => {
await processCases();
!isInitialLoad && showCaseUpdationToast(newlyAddedCases, deletedCases);
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_SNAPSHOT_LISTENER, {
snapshot_path: 'handleCasesUpdate',
});
});
});
};

View File

@@ -3,7 +3,6 @@ import {
setActivityTimeOnApp,
setActivityTimeWindowHigh,
setActivityTimeWindowMedium,
setEnableFirestoreResync,
setFirestoreResyncIntervalInMinutes,
setAudioUploadJobIntervalInMinutes,
setCalendarAndAccountsUploadJobIntervalInMinutes,
@@ -40,9 +39,6 @@ async function fetchUpdatedRemoteConfig() {
.getValue('ACTIVITY_TIME_WINDOW_MEDIUM')
.asNumber();
const BLACKLISTED_APPS = remoteConfig().getValue('BLACKLISTED_APPS').asString();
const ENABLE_FIRESTORE_RESYNC = remoteConfig()
.getValue('ENABLE_FIRESTORE_RESYNC')
.asBoolean();
const FIRESTORE_RESYNC_INTERVAL_IN_MINUTES = remoteConfig()
.getValue('FIRESTORE_RESYNC_INTERVAL_IN_MINUTES')
.asNumber();
@@ -68,7 +64,6 @@ async function fetchUpdatedRemoteConfig() {
setActivityTimeWindowHigh(ACTIVITY_TIME_WINDOW_HIGH);
setActivityTimeWindowMedium(ACTIVITY_TIME_WINDOW_MEDIUM);
setBlacklistedAppsList(BLACKLISTED_APPS);
setEnableFirestoreResync(ENABLE_FIRESTORE_RESYNC);
setFirestoreResyncIntervalInMinutes(FIRESTORE_RESYNC_INTERVAL_IN_MINUTES);
if(DATA_SYNC_JOB_INTERVAL_IN_MINUTES) setDataSyncJobIntervalInMinutes(DATA_SYNC_JOB_INTERVAL_IN_MINUTES);

View File

@@ -0,0 +1,69 @@
import firestore from '@react-native-firebase/firestore';
import auth from '@react-native-firebase/auth';
import { logError } from '../components/utlis/errorUtils';
const subscriptions = new Map();
const subscribeToDoc = (collectionPath, successCb) => {
if (subscriptions.has(collectionPath)) {
return;
}
const unsubscribe = firestore()
.doc(collectionPath)
.onSnapshot(successCb, (error) => {
logError(error, `Error in firetore subscription: ${collectionPath}`);
});
subscriptions.set(collectionPath, unsubscribe);
};
const subscribeToCollection = (collectionPath, queryFn, successCb) => {
if (subscriptions.has(collectionPath)) {
return;
}
let ref = firestore().collection(collectionPath);
if (queryFn) {
ref = queryFn(ref);
}
const unsubscribe = ref.onSnapshot(successCb, (error) => {
logError(error, `Error in firestore subscription: ${collectionPath}`);
});
subscriptions.set(collectionPath, unsubscribe);
};
const unsubscribeFromPath = (collectionPath) => {
const unsubscribe = subscriptions.get(collectionPath);
if (unsubscribe) {
unsubscribe();
subscriptions.delete(collectionPath);
}
};
const unsubscribeAll = () => {
subscriptions.forEach((unsubscribe, path) => {
unsubscribe();
});
subscriptions.clear();
};
const signInUserToFirebase = async (firebaseToken, onSuccess, onError) => {
try {
await auth().signInWithCustomToken(firebaseToken);
onSuccess && onSuccess();
} catch (error) {
onError && onError(error);
logError(error, 'Error in signInUserToFirebase');
}
};
export const firestoreService = {
subscribeToDoc,
subscribeToCollection,
unsubscribeFromPath,
unsubscribeAll,
signInUserToFirebase,
};