TP-34788 | Cosmos for AM/TLs

This commit is contained in:
Aman Chaturvedi
2023-09-07 13:34:19 +05:30
parent 46a510f62d
commit 958aa823d1
24 changed files with 625 additions and 110 deletions

View File

@@ -120,11 +120,14 @@ const App = () => {
return (
<Provider store={store}>
<PersistGate loading={<FullScreenLoader loading />} persistor={persistor}>
<PersistGate
loading={<FullScreenLoader loading isTranslucent={false} />}
persistor={persistor}
>
<NavigationContainer ref={navigationRef}>
<StatusBar backgroundColor={COLORS.BACKGROUND.INDIGO_DARK} />
<SuspenseLoader
fallBack={<FullScreenLoader loading />}
fallBack={<FullScreenLoader loading isTranslucent={false} />}
loading={!isGlobalDocumentMapLoaded}
children={
<ErrorBoundary>{permissions ? <AuthRouter /> : <Permissions />}</ErrorBoundary>

View File

@@ -41,7 +41,6 @@
"@react-navigation/native": "6.1.4",
"@react-navigation/native-stack": "6.9.4",
"@reduxjs/toolkit": "1.9.1",
"@sanar/react-native-highlight-text": "^1.0.2",
"@sentry/react-native": "5.5.0",
"@shopify/flash-list": "1.4.3",
"@supersami/rn-foreground-service": "^2.1.0",

View File

@@ -1,6 +1,6 @@
import { ToastMessages } from './../screens/allCases/constants';
import 'react-native-get-random-values';
import { IUser, setAuthData } from '../reducer/userSlice';
import { IUser, IUserRole, setAgentRole, setAuthData } from '../reducer/userSlice';
import axiosInstance, { ApiKeys, API_STATUS_CODE, getApiUrl } from '../components/utlis/apiHelper';
import {
resetLoginForm,
@@ -115,6 +115,7 @@ export const verifyGoogleSignIn = (idToken: string) => async (dispatch: AppDispa
});
dispatch(setVerifyOTPSuccess('Login Successfully!'));
dispatch(resetLoginForm());
dispatch(getAgentDetail());
}
} catch (error: GenericType) {
await handleGoogleLogout();
@@ -157,6 +158,7 @@ export const verifyOTP =
);
dispatch(setVerifyOTPSuccess('OTP verified'));
dispatch(resetLoginForm());
dispatch(getAgentDetail());
})
.catch((err) => {
dispatch(setVerifyOTPError('Invalid OTP entered. Kindly try again'));
@@ -229,6 +231,7 @@ export const handleImpersonatedUserLogin =
isImpersonated: true,
})
);
dispatch(getAgentDetail());
successCallback?.();
})
.catch((err) => {
@@ -241,3 +244,14 @@ export const handleImpersonatedUserLogin =
})
.finally(() => finallyCallback?.());
};
export const getAgentDetail = () => (dispatch: AppDispatch) => {
const url = getApiUrl(ApiKeys.GET_AGENT_DETAIL);
return axiosInstance.get(url).then((response) => {
if (response.status === API_STATUS_CODE.OK) {
const roles = response?.data?.roles || [];
const isTeamLead = roles?.includes(IUserRole.ROLE_TEAM_LEAD);
dispatch(setAgentRole(isTeamLead));
}
});
};

View File

@@ -0,0 +1,19 @@
import axiosInstance, { ApiKeys, getApiUrl } from '../components/utlis/apiHelper';
import { setReporteesList, setReporteesLoading } from '../reducer/reporteesSlice';
import { AppDispatch } from '../store/store';
export const getAgentsList = () => (dispatch: AppDispatch) => {
const url = getApiUrl(ApiKeys.REPORTEES); //, null, {pageNo: 1, pageSize: 10, fetchAll: 'true'});
dispatch(setReporteesLoading(true));
axiosInstance
.get(url)
.then((res) => {
const reporteesList = res.data?.reportees;
if (reporteesList) {
dispatch(setReporteesList(reporteesList));
}
})
.finally(() => {
dispatch(setReporteesLoading(false));
});
};

View File

@@ -55,6 +55,7 @@ const TrackingComponent: React.FC<ITrackingComponent> = ({ children }) => {
const isOnline = useIsOnline();
const dispatch = useAppDispatch();
const appState = useRef(AppState.currentState);
const isTeamLead = useAppSelector((state) => state.user.isTeamLead);
const {
referenceId,
@@ -193,12 +194,6 @@ const TrackingComponent: React.FC<ITrackingComponent> = ({ children }) => {
delay: 3 * MILLISECONDS_IN_A_MINUTE, // 3 minutes
onLoop: true,
},
{
taskId: FOREGROUND_TASKS.FIRESTORE_FALLBACK,
task: handleGetCaseSyncStatus,
delay: 5 * MILLISECONDS_IN_A_MINUTE, // 5 minutes
onLoop: true,
},
{
taskId: FOREGROUND_TASKS.UPDATE_AGENT_ACTIVENESS,
task: handleUpdateActiveness,
@@ -207,6 +202,15 @@ 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;

View File

@@ -54,6 +54,8 @@ export enum ApiKeys {
GLOBAL_CONFIG = 'GLOBAL_CONFIG',
UPLOAD_IMAGE_ID = 'UPLOAD_IMAGE_ID',
GET_DOCUMENTS = 'GET_DOCUMENTS',
REPORTEES = 'REPORTEES',
GET_AGENT_DETAIL = 'GET_AGENT_DETAIL',
}
export const API_URLS: Record<ApiKeys, string> = {} as Record<ApiKeys, string>;
@@ -93,6 +95,8 @@ API_URLS[ApiKeys.UPLOAD_FEEDBACK_IMAGES] = '/feedback/persist-original-images';
API_URLS[ApiKeys.GLOBAL_CONFIG] = '/global-config';
API_URLS[ApiKeys.UPLOAD_IMAGE_ID] = '/user/documents/selfie';
API_URLS[ApiKeys.GET_DOCUMENTS] = '/user/documents';
API_URLS[ApiKeys.REPORTEES] = '/user/reportees';
API_URLS[ApiKeys.GET_AGENT_DETAIL] = '/user/role-info';
export const API_STATUS_CODE = {
OK: 200,

View File

@@ -8,7 +8,7 @@ import { FirestoreUpdateTypes } from '../common/Constants';
import { toast } from '../../RN-UI-LIB/src/components/toast';
import auth from '@react-native-firebase/auth';
import { updateAvTemplateData, updateCollectionTemplateData } from '../reducer/caseReducer';
import { ILockData, setLockData, VisitPlanStatus } from '../reducer/userSlice';
import { ILockData, MY_CASE_ITEM, setLockData, VisitPlanStatus } from '../reducer/userSlice';
import { setFilters } from '../reducer/filtersSlice';
import { FormTemplateV1 } from '../types/template.types';
import { ToastMessages } from '../screens/allCases/constants';
@@ -33,12 +33,13 @@ const isUserSignedIn = () => {
const useFirestoreUpdates = () => {
const {
user: { user, isLoggedIn, sessionDetails, lock },
allCases: { caseDetails, casesList, loading },
user: { user, isLoggedIn, sessionDetails, lock, selectedAgent = MY_CASE_ITEM },
allCases: { caseDetails, casesList },
} = useAppSelector((state: RootState) => ({
user: state.user || {},
allCases: state.allCases,
}));
const isTeamLead = useAppSelector((state) => state.user.isTeamLead);
const lockRef = useRef<ILockData | null>(null);
useEffect(() => {
@@ -155,6 +156,7 @@ const useFirestoreUpdates = () => {
const handleError = (err: any, collectionPath?: string) => {
const errMsg = `Error while fetching fireStore snapshot: referenceId: ${user?.referenceId} collectionPath: ${collectionPath}`;
logError(err as Error, errMsg);
dispatch(setLoading(false));
};
const signInUserToFirebase = () => {
@@ -176,12 +178,6 @@ const useFirestoreUpdates = () => {
});
};
const subscribeToCollection = (successCb: GenericFunctionArgs, collectionPath: string) => {
return firestore()
.collection(collectionPath)
.onSnapshot(successCb, (err) => handleError(err, collectionPath));
};
const subscribeToDoc = (successCb: GenericFunctionArgs, collectionPath: string) => {
return firestore()
.doc(collectionPath)
@@ -189,7 +185,11 @@ const useFirestoreUpdates = () => {
};
const subscribeToCases = () => {
const collectionPath = `allocations/${user?.referenceId}/cases`;
let refId = user?.referenceId;
if (isTeamLead && selectedAgent?.agentReferenceId !== MY_CASE_ITEM.agentReferenceId) {
refId = selectedAgent?.agentReferenceId;
}
const collectionPath = `allocations/${refId}/cases`;
return firestore()
.collection(collectionPath)
.orderBy('totalOverdueAmount', 'asc') // It is descending order only, but acting weirdly. Need to check.
@@ -212,7 +212,11 @@ const useFirestoreUpdates = () => {
};
const subscribeToFilters = () => {
const collectionPath = `filters/${user?.referenceId}`;
let refId = user?.referenceId;
if (isTeamLead && selectedAgent?.agentReferenceId !== MY_CASE_ITEM.agentReferenceId) {
refId = selectedAgent?.agentReferenceId;
}
const collectionPath = `filters/${refId}`;
return subscribeToDoc(handleFilterUpdate, collectionPath);
};
@@ -263,6 +267,33 @@ const useFirestoreUpdates = () => {
};
}, [isLoggedIn, user?.referenceId]);
useEffect(() => {
if (!isTeamLead) {
return;
}
if (
!selectedAgent?.agentReferenceId ||
selectedAgent?.agentReferenceId === MY_CASE_ITEM.agentReferenceId
) {
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]);
useEffect(() => {
forceUninstallUnsubscribe = subscribeToForceUninstall();
}, []);

View File

@@ -242,9 +242,6 @@ const allCasesSlice = createSlice({
isInitialLoad: boolean;
isVisitPlanLocked: boolean;
};
if (state.loading) {
state.loading = false;
}
let newVisitCaseLoanIds: string[] = [];
let newVisitCollectionCases: string[] = [];
let removedVisitedCasesLoanIds: string[] = [];
@@ -356,6 +353,9 @@ const allCasesSlice = createSlice({
state.completedList = completedList;
state.pinnedList = pinnedList;
state.newVisitedCases = newVisitCollectionCases;
if (state.loading) {
state.loading = false;
}
if (newVisitCaseLoanIds?.length > 0) {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_VISIT_PLAN_UPDATED, {
@@ -503,26 +503,8 @@ const allCasesSlice = createSlice({
state.caseDetails[action.payload].isNewlyAdded = false;
}
},
resetCasesData: (state) => {
state.casesList = [];
state.casesListMap = {};
state.intermediateTodoList = [];
state.intermediateTodoListMap = {};
state.selectedTodoListCount = 0;
state.selectedTodoListMap = {};
state.initialPinnedRankCount = 0;
state.pinnedRankCount = 0;
state.loading = false;
state.filterList = [];
state.newlyPinnedCases = 0;
state.completedCases = 0;
state.caseDetails = {};
state.searchQuery = '';
state.isOnboarded = state.isOnboarded;
state.newVisitedCases = [];
state.pendingList = [];
state.pinnedList = [];
state.completedList = [];
resetCasesData: () => {
return initialState;
},
setVisitPlansUpdating: (state, action) => {
state.visitPlansUpdating = action.payload;

View File

@@ -0,0 +1,36 @@
import { createSlice } from '@reduxjs/toolkit';
import { IReportee } from '../screens/allCases/interface';
import { MY_CASE_ITEM } from './userSlice';
interface IReporteesSlice {
agentsList: IReportee[];
isLoading: boolean;
showAgentSelectionBottomSheet: boolean;
}
const initialState: IReporteesSlice = {
agentsList: [],
isLoading: false,
showAgentSelectionBottomSheet: false,
};
export const userSlice = createSlice({
name: 'reportees',
initialState,
reducers: {
setReporteesList: (state, action) => {
state.agentsList = [MY_CASE_ITEM, ...(action.payload || [])];
},
setReporteesLoading: (state, action) => {
state.isLoading = action.payload;
},
setShowAgentSelectionBottomSheet: (state, action) => {
state.showAgentSelectionBottomSheet = action.payload;
},
},
});
export const { setReporteesList, setReporteesLoading, setShowAgentSelectionBottomSheet } =
userSlice.actions;
export default userSlice.reducer;

View File

@@ -1,5 +1,6 @@
import { createSlice } from '@reduxjs/toolkit';
import { setGlobalUserData } from '../constants/Global';
import { IReportee } from '../screens/allCases/interface';
interface ISessionDetails {
sessionToken: string;
@@ -9,8 +10,16 @@ interface ISessionDetails {
export enum IUserRole {
SUPER_USER = 'ADDRESS_VERIFICATION:API:SUPER_USER',
ROLE_TEAM_LEAD = 'ROLE_TEAM_LEAD',
}
export const MY_CASE_ITEM = {
name: 'My Cases',
agentReferenceId: 'MY_CASES',
agencyCode: '',
agencyName: '',
};
interface IUserDetails {
emailId: string;
referenceId: string;
@@ -47,6 +56,8 @@ export interface IUserSlice extends IUser {
clickstreamEvents: IClickstreamEvents[];
isImpersonated: boolean;
lock: ILockData;
selectedAgent: IReportee;
isTeamLead: boolean;
}
const initialState: IUserSlice = {
@@ -59,6 +70,8 @@ const initialState: IUserSlice = {
lock: {
visitPlanStatus: VisitPlanStatus.UNLOCKED,
},
selectedAgent: MY_CASE_ITEM,
isTeamLead: false,
};
export const userSlice = createSlice({
@@ -86,9 +99,16 @@ export const userSlice = createSlice({
state.lock = action.payload;
}
},
setSelectedAgent: (state, action) => {
state.selectedAgent = action.payload;
},
setAgentRole: (state, action) => {
state.isTeamLead = action.payload ?? false;
},
},
});
export const { setAuthData, setDeviceId, setLockData } = userSlice.actions;
export const { setAuthData, setDeviceId, setLockData, setSelectedAgent, setAgentRole } =
userSlice.actions;
export default userSlice.reducer;

View File

@@ -30,7 +30,7 @@ import VersionNumber from 'react-native-version-number';
import { useFocusEffect } from '@react-navigation/native';
import { CaseDetail } from '../caseDetails/interface';
import CaseItem from '../allCases/CaseItem';
import { IUserRole } from '../../reducer/userSlice';
import { IUserRole, MY_CASE_ITEM } from '../../reducer/userSlice';
import QuestionMarkIcon from '../../assets/icons/QuestionMarkIcon';
import IDCardImageCapture from './IDCardImageCapture';
import AgentIdCard from './AgentIdCard';
@@ -53,6 +53,8 @@ const Profile: React.FC = () => {
caseDetails,
supportLink,
pendingCases,
selectedAgent,
isTeamLead,
} = useAppSelector((state: RootState) => ({
originalImageUri: state.profile.originalImageUri,
imageUri: state.profile.imageUri,
@@ -65,6 +67,8 @@ const Profile: React.FC = () => {
supportLink: state.config.data?.supportLink,
isUploadingImage: state.profile.isUploadingImage,
pendingCases: state.allCases.pendingList,
selectedAgent: state.user.selectedAgent,
isTeamLead: state.user.isTeamLead,
}));
useEffect(() => {
@@ -132,6 +136,9 @@ const Profile: React.FC = () => {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_ID_CARD_CLICKED);
};
const showCompletedCases =
!isTeamLead || selectedAgent?.agentReferenceId === MY_CASE_ITEM.agentReferenceId;
return (
<View style={[GenericStyles.fill]}>
<NavigationHeader
@@ -174,53 +181,55 @@ const Profile: React.FC = () => {
}
/>
<ScrollView>
<View
style={[
GenericStyles.ph16,
GenericStyles.pt16,
numberOfCompletedCases === 2 ? { paddingBottom: 6 } : {},
]}
>
{hideUploadImageBtn ? null : <IDCardImageCapture />}
{showCompletedCases ? (
<View
style={[
GenericStyles.row,
GenericStyles.alignCenter,
numberOfCompletedCases ? { paddingBottom: 12 } : GenericStyles.pb16,
GenericStyles.ph16,
GenericStyles.pt16,
numberOfCompletedCases === 2 ? { paddingBottom: 6 } : {},
]}
>
<View style={[GenericStyles.ml4, GenericStyles.mr8]}>
<GroupIcon />
</View>
<Text>Completed cases ({numberOfCompletedCases})</Text>
</View>
{numberOfCompletedCases
? completeCasesList.slice(0, 2).map((caseItem) => {
const caseDetailItem = caseDetails[caseItem.caseReferenceId] as CaseDetail;
return (
<CaseItem
key={caseItem.caseReferenceId}
caseDetailObj={caseDetailItem}
isCompleted={true}
/>
);
})
: null}
{numberOfCompletedCases > 2 ? (
<Button
title="View all completed cases"
variant="primaryText"
{hideUploadImageBtn ? null : <IDCardImageCapture />}
<View
style={[
GenericStyles.w100,
GenericStyles.br8,
GenericStyles.mt6,
GenericStyles.mb12,
GenericStyles.whiteBackground,
GenericStyles.row,
GenericStyles.alignCenter,
numberOfCompletedCases ? { paddingBottom: 12 } : GenericStyles.pb16,
]}
onPress={handleViewAllCases}
/>
) : null}
</View>
>
<View style={[GenericStyles.ml4, GenericStyles.mr8]}>
<GroupIcon />
</View>
<Text>Completed cases ({numberOfCompletedCases})</Text>
</View>
{numberOfCompletedCases
? completeCasesList.slice(0, 2).map((caseItem) => {
const caseDetailItem = caseDetails[caseItem.caseReferenceId] as CaseDetail;
return (
<CaseItem
key={caseItem.caseReferenceId}
caseDetailObj={caseDetailItem}
isCompleted={true}
/>
);
})
: null}
{numberOfCompletedCases > 2 ? (
<Button
title="View all completed cases"
variant="primaryText"
style={[
GenericStyles.w100,
GenericStyles.br8,
GenericStyles.mt6,
GenericStyles.mb12,
GenericStyles.whiteBackground,
]}
onPress={handleViewAllCases}
/>
) : null}
</View>
) : null}
<View style={[styles.logoutContainer, GenericStyles.whiteBackground]}>
<TouchableOpacity
onPress={handleLogout}

View File

@@ -0,0 +1,79 @@
import { Pressable, StyleSheet, View } from 'react-native';
import React from 'react';
import Text from '../../../RN-UI-LIB/src/components/Text';
import { GenericStyles } from '../../../RN-UI-LIB/src/styles';
import ArrowSolidIcon from '../../../RN-UI-LIB/src/Icons/ArrowSolidIcon';
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 fuzzySort from '../../../RN-UI-LIB/src/utlis/fuzzySort';
import fuzzysort from 'fuzzysort';
import { MY_CASE_ITEM, setSelectedAgent } from '../../reducer/userSlice';
interface IAgentListItem {
agent: IReportee;
leftAdornment?: React.ReactNode;
searchQuery: string;
}
const AgentListItem: React.FC<IAgentListItem> = ({ agent, leftAdornment, searchQuery }) => {
const selectedAgent = useAppSelector((state) => state.user.selectedAgent) || MY_CASE_ITEM;
const dispatch = useAppDispatch();
const handleAgentSelection = () => {
dispatch(setSelectedAgent(agent));
dispatch(resetCasesData());
dispatch(setShowAgentSelectionBottomSheet(false));
};
return (
<Pressable
style={[
GenericStyles.row,
GenericStyles.justifyContentSpaceBetween,
GenericStyles.whiteBackground,
GenericStyles.p12,
GenericStyles.br8,
GenericStyles.border,
GenericStyles.alignCenter,
GenericStyles.mb16,
styles.shadow,
{
backgroundColor:
agent.agentReferenceId === selectedAgent.agentReferenceId
? COLORS.BACKGROUND.BLUE_LIGHT_3
: COLORS.BACKGROUND.PRIMARY,
},
]}
onPress={handleAgentSelection}
>
<View style={[GenericStyles.row, GenericStyles.alignCenter]}>
{leftAdornment && <View style={[GenericStyles.mr4]}>{leftAdornment}</View>}
<Text>
{searchQuery.length > 0
? fuzzySort.highlight(fuzzysort.single(searchQuery, agent.name), (result: string) => (
<Text style={styles.highlight}>{result}</Text>
))
: agent.name}
</Text>
</View>
<ArrowSolidIcon size={10} fillColor={COLORS.BACKGROUND.LIGHT} rotateY={180} />
</Pressable>
);
};
const styles = StyleSheet.create({
shadow: {
elevation: 1,
},
highlight: {
backgroundColor: COLORS.BACKGROUND.ORANGE,
borderRadius: 4,
borderWidth: 1,
borderColor: COLORS.BORDER.ORANGE,
},
});
export default AgentListItem;

View File

@@ -0,0 +1,21 @@
import React from 'react';
import TextInput from '../../../RN-UI-LIB/src/components/TextInput';
import SearchIcon from '../../../RN-UI-LIB/src/Icons/SearchIcon';
interface IAgentListSearchbar {
searchQuery: string;
handleSearchChange: (val: string) => void;
}
const AgentListSearchbar: React.FC<IAgentListSearchbar> = ({ searchQuery, handleSearchChange }) => {
return (
<TextInput
LeftComponent={<SearchIcon />}
onChangeText={handleSearchChange}
placeholder="Search by agent"
defaultValue={searchQuery}
/>
);
};
export default AgentListSearchbar;

View File

@@ -0,0 +1,103 @@
import {
Animated,
NativeScrollEvent,
NativeSyntheticEvent,
SectionList,
StyleSheet,
View,
} from 'react-native';
import React, { useMemo } from 'react';
import Text from '../../../RN-UI-LIB/src/components/Text';
import { IReportee, ISectionListData } from './interface';
import { useAppSelector } from '../../hooks';
import { GenericStyles } from '../../../RN-UI-LIB/src/styles';
import { COLORS } from '../../../RN-UI-LIB/src/styles/colors';
import AgentListItem from './AgentListItem';
import { sectionListTranformData } from './utils';
import ProfileSolidIcon from '../../../RN-UI-LIB/src/Icons/ProfileSolidIcon';
import { Search } from '../../../RN-UI-LIB/src/utlis/search';
import NoCasesFoundIcon from '../../assets/icons/NoCasesFoundIcon';
import { MY_CASE_ITEM } from '../../reducer/userSlice';
import Heading from '../../../RN-UI-LIB/src/components/Heading';
interface IAgentListView {
searchQuery: string;
scrollAnimation: Animated.Value;
}
const AgentListView: React.FC<IAgentListView> = ({ scrollAnimation, searchQuery }) => {
const { agentsList = [] } = useAppSelector((state) => state.reportees);
const filteredAgentsList: ISectionListData[] = useMemo(() => {
const filteredList =
searchQuery.length > 0
? Search(searchQuery, agentsList, { keys: ['name'] }).map(
(item: { obj: IReportee }) => item.obj
)
: agentsList;
return sectionListTranformData(filteredList);
}, [searchQuery, agentsList]);
const handleListScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
const offsetY = event.nativeEvent.contentOffset.y;
scrollAnimation.setValue(offsetY);
};
if (filteredAgentsList.length === 0) {
return (
<View style={[GenericStyles.fill, GenericStyles.alignCenter, styles.mt64]}>
<View style={GenericStyles.alignCenter}>
<NoCasesFoundIcon />
<Heading style={GenericStyles.mt16} type="h4">
No {agentsList.length === 0 ? 'reportees' : 'results'} found
</Heading>
{agentsList.length ? <Text light>Try searching something else</Text> : null}
</View>
</View>
);
}
return (
<SectionList
sections={filteredAgentsList}
keyExtractor={(item) => item.agentReferenceId}
onScroll={handleListScroll}
contentContainerStyle={GenericStyles.ph16}
renderItem={({ item }) => (
<AgentListItem
agent={item}
searchQuery={searchQuery}
leftAdornment={
item.agentReferenceId === MY_CASE_ITEM.agentReferenceId ? <ProfileSolidIcon /> : null
}
/>
)}
renderSectionHeader={({ section: { title } }) => {
if (!title) {
return null;
}
return (
<View style={[GenericStyles.centerAlignedRow, GenericStyles.pb16]}>
<Text bold dark>
{title}
</Text>
<View style={styles.separatorLine} />
</View>
);
}}
/>
);
};
const styles = StyleSheet.create({
separatorLine: {
height: 1,
flex: 1,
backgroundColor: COLORS.BORDER.PRIMARY,
marginLeft: 8,
},
mt64: {
marginTop: 64,
},
});
export default AgentListView;

View File

@@ -0,0 +1,21 @@
import { View } from 'react-native';
import React from 'react';
import LineLoader from '../../../RN-UI-LIB/src/components/suspense_loader/LineLoader';
import { GenericStyles } from '../../../RN-UI-LIB/src/styles';
const AgentListViewLoading = () => {
return (
<View style={[GenericStyles.fill, GenericStyles.whiteBackground, GenericStyles.ph16]}>
{[...Array(8).keys()].map((_, index) => (
<LineLoader
key={index}
width={'100%'}
height={50}
style={[GenericStyles.br6, { marginBottom: 20 }]}
/>
))}
</View>
);
};
export default AgentListViewLoading;

View File

@@ -0,0 +1,72 @@
import { Animated, StyleSheet, View } from 'react-native';
import React, { useCallback, useEffect, useRef } from 'react';
import AgentListSearchbar from './AgentListSearchbar';
import { debounce } from '../../components/utlis/commonFunctions';
import AgentListView from './AgentListView';
import { useAppDispatch, useAppSelector } from '../../hooks';
import { getAgentsList } from '../../action/reporteesActions';
import { GenericStyles } from '../../../RN-UI-LIB/src/styles';
import { COLORS } from '../../../RN-UI-LIB/src/styles/colors';
import AgentListViewLoading from './AgentListViewLoading';
const AgentsListContainer = () => {
const [searchQuery, setSearchQuery] = React.useState<string>('');
const { isLoading, agentsList } = useAppSelector((state) => state.reportees);
const dispatch = useAppDispatch();
const scrollAnimation = useRef(new Animated.Value(0)).current;
useEffect(() => {
dispatch(getAgentsList());
}, []);
const handleSearchChange = useCallback(
debounce((query: string) => {
setSearchQuery(query);
}, 200),
[]
);
const elevateAnimatedValue = scrollAnimation.interpolate({
inputRange: [0, 100],
outputRange: [0, 6],
extrapolate: 'clamp',
});
const paddingBottomAnimatedValue = scrollAnimation.interpolate({
inputRange: [0, 100],
outputRange: [0, 10],
extrapolate: 'clamp',
});
if (isLoading && !agentsList.length) {
return <AgentListViewLoading />;
}
return (
<View style={[GenericStyles.fill, GenericStyles.whiteBackground]}>
<Animated.View
style={[{ paddingBottom: paddingBottomAnimatedValue }, GenericStyles.overflowHidden]}
>
<Animated.View
style={[
GenericStyles.ph16,
GenericStyles.pb16,
GenericStyles.whiteBackground,
{
borderColor: COLORS.BORDER.PRIMARY,
elevation: elevateAnimatedValue,
},
]}
>
<AgentListSearchbar searchQuery={searchQuery} handleSearchChange={handleSearchChange} />
</Animated.View>
</Animated.View>
<Animated.View style={[GenericStyles.fill]}>
<AgentListView searchQuery={searchQuery} scrollAnimation={scrollAnimation} />
</Animated.View>
</View>
);
};
export default AgentsListContainer;

View File

@@ -335,7 +335,7 @@ const CasesList: React.FC<ICasesList> = ({ casesList = [], isVisitPlan, allCases
HeaderNode={() => (
<View style={[...row, GenericStyles.ph16]}>
<Heading dark type="h4">
View teams assigned cases
View team's assigned cases
</Heading>
<TouchableOpacity activeOpacity={0.7} onPress={toggleAgentSelectionBottomSheet}>
<CloseIcon color={COLORS.TEXT.LIGHT} />
@@ -343,6 +343,7 @@ const CasesList: React.FC<ICasesList> = ({ casesList = [], isVisitPlan, allCases
</View>
)}
heightPercentage={100}
allowBackdropClose={false}
visible={showAgentSelectionBottomSheet}
setVisible={toggleAgentSelectionBottomSheet}
>

View File

@@ -23,12 +23,16 @@ interface IEmptyList {
const EmptyList: React.FC<IEmptyList> = (props) => {
const { isCompleted, isVisitPlan, isFilterApplied, setShowAgentSelectionBottomSheet } = props;
const { isLockedVisitPlanStatus } = useAppSelector((state: RootState) => ({
const { isLockedVisitPlanStatus, isCasesLoading } = useAppSelector((state: RootState) => ({
isLockedVisitPlanStatus: state.user?.lock?.visitPlanStatus === VisitPlanStatus.LOCKED,
isCasesLoading: state.allCases.loading,
}));
const isAgentTLOrAM = true;
const isTeamLead = useAppSelector((state) => state.user.isTeamLead);
if (isCasesLoading) {
return null;
}
const renderIcon = () => {
if (isLockedVisitPlanStatus && isVisitPlan) {
@@ -60,7 +64,7 @@ const EmptyList: React.FC<IEmptyList> = (props) => {
return EmptyListMessages.NO_VISIT_PLANS;
}
if (isAgentTLOrAM) {
if (isTeamLead) {
return EmptyListMessages.NO_ACTIVE_ALLOCATIONS;
}
@@ -74,7 +78,7 @@ const EmptyList: React.FC<IEmptyList> = (props) => {
if (isFilterApplied) {
return EmptyListMessages.NO_CASES_FOUND_SUB;
}
if (isAgentTLOrAM) {
if (isTeamLead) {
return EmptyListMessages.SELECT_AGENT;
}
};
@@ -87,7 +91,7 @@ const EmptyList: React.FC<IEmptyList> = (props) => {
};
}
if (isAgentTLOrAM && setShowAgentSelectionBottomSheet) {
if (isTeamLead && setShowAgentSelectionBottomSheet) {
return {
btnHandler: () => setShowAgentSelectionBottomSheet(true),
btnText: 'Select Agent',

View File

@@ -1,12 +1,12 @@
import { StyleSheet, TouchableOpacity, View } from 'react-native';
import React from 'react';
import { Animated, Easing, StyleSheet, TouchableOpacity, View } from 'react-native';
import React, { useEffect, useRef } from 'react';
import Heading from '../../../RN-UI-LIB/src/components/Heading';
import { COLORS } from '../../../RN-UI-LIB/src/styles/colors';
import { GenericStyles } from '../../../RN-UI-LIB/src/styles';
import ArrowDownOutlineIcon from '../../assets/icons/ArrowDownOutlineIcon';
import { useAppSelector } from '../../hooks';
import { RootState } from '../../store/store';
import { VisitPlanStatus } from '../../reducer/userSlice';
import { MY_CASE_ITEM, VisitPlanStatus } from '../../reducer/userSlice';
import { dateFormat, DAY_MONTH_DATE_FORMAT } from '../../../RN-UI-LIB/src/utlis/dates';
interface HeaderLabelProps {
@@ -18,11 +18,22 @@ interface HeaderLabelProps {
const HeaderLabel: React.FC<HeaderLabelProps> = (props) => {
const { setShowAgentSelectionBottomSheet, filteredListCount, isVisitPlan } = props;
const { isLockedVisitPlanStatus } = useAppSelector((state: RootState) => ({
const {
isLockedVisitPlanStatus,
selectedAgent = MY_CASE_ITEM,
loading,
} = useAppSelector((state: RootState) => ({
isLockedVisitPlanStatus: state.user?.lock?.visitPlanStatus === VisitPlanStatus.LOCKED,
selectedAgent: state.user.selectedAgent,
loading: state.allCases.loading,
}));
const opacityAnimation = useRef(new Animated.Value(0)).current;
const opacityStyle = { opacity: opacityAnimation };
const isTeamLead = useAppSelector((state) => state.user.isTeamLead);
const isAgentTLOrAM = true;
useEffect(() => {
animateElement();
}, [selectedAgent, isTeamLead]);
const getHeaderLabel = () => {
if (isVisitPlan) {
@@ -33,19 +44,44 @@ const HeaderLabel: React.FC<HeaderLabelProps> = (props) => {
const formattedDate = dateFormat(currentDate, DAY_MONTH_DATE_FORMAT);
return formattedDate;
}
return `My Cases (${filteredListCount})`;
return `${selectedAgent.name}${!loading ? ` (${filteredListCount})` : ''}`;
};
if (isAgentTLOrAM && !isVisitPlan) {
const animateElement = (iterations = 1) => {
if (iterations === 0) {
return;
}
Animated.timing(opacityAnimation, {
toValue: 0.5,
duration: 400,
useNativeDriver: true,
easing: Easing.linear,
}).start(() => {
Animated.timing(opacityAnimation, {
toValue: 0,
duration: 300,
useNativeDriver: true,
easing: Easing.linear,
}).start(() => {
animateElement(iterations - 1);
});
});
};
if (isTeamLead && !isVisitPlan) {
return (
<TouchableOpacity
style={GenericStyles.centerAlignedRow}
onPress={() => setShowAgentSelectionBottomSheet(true)}
>
<Animated.View style={[styles.animatedOverlay, opacityStyle]} />
<Heading type="h3" style={[styles.headerLabel]}>
{getHeaderLabel()}
</Heading>
<ArrowDownOutlineIcon />
<View style={GenericStyles.mt2}>
<ArrowDownOutlineIcon />
</View>
</TouchableOpacity>
);
}
@@ -57,10 +93,18 @@ const HeaderLabel: React.FC<HeaderLabelProps> = (props) => {
);
};
export default HeaderLabel;
const styles = StyleSheet.create({
headerLabel: {
color: COLORS.BACKGROUND.PRIMARY,
},
animatedOverlay: {
backgroundColor: COLORS.BACKGROUND.PRIMARY,
position: 'absolute',
width: '110%',
height: '90%',
borderRadius: 8,
paddingHorizontal: 10,
},
});
export default HeaderLabel;

View File

@@ -77,6 +77,7 @@ const ListItem: React.FC<IListItem> = (props) => {
const isVisitPlanStatusLocked = useAppSelector(
(state) => state.user?.lock?.visitPlanStatus === VisitPlanStatus.LOCKED
);
const isTeamLead = useAppSelector((state) => state.user.isTeamLead);
const dispatch = useAppDispatch();
@@ -174,7 +175,7 @@ const ListItem: React.FC<IListItem> = (props) => {
const caseCompleted = COMPLETED_STATUSES.includes(caseStatus);
const showVisitPlanBtn =
!(caseCompleted || isCaseItemPinnedMainView) && !isTodoItem && !isCompleted;
!(caseCompleted || isCaseItemPinnedMainView) && !isTodoItem && !isCompleted && !isTeamLead;
return (
<Pressable onPress={handleCaseClick}>

View File

@@ -29,7 +29,7 @@ const AllCasesMain = () => {
);
const userState = useAppSelector((state: RootState) => state.user);
const dispatch = useAppDispatch();
const isAgentTLOrAM = true;
const isTeamLead = useAppSelector((state) => state.user.isTeamLead);
const HOME_SCREENS: ITabScreen[] = useMemo(() => {
const bottomSheetScreens = [
@@ -41,7 +41,7 @@ const AllCasesMain = () => {
icon: CasesIcon,
},
];
if (!isAgentTLOrAM) {
if (!isTeamLead) {
bottomSheetScreens.push({
name: BOTTOM_TAB_ROUTES.VisitPlan,
component: () => <CasesList casesList={pinnedList} isVisitPlan />,
@@ -54,7 +54,7 @@ const AllCasesMain = () => {
icon: ProfileIcon,
});
return bottomSheetScreens;
}, [pendingList, pinnedList, isAgentTLOrAM]);
}, [pendingList, pinnedList, isTeamLead]);
const onTabPressHandler = (e: any) => {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_TAB_SWITCH, {
@@ -79,7 +79,7 @@ const AllCasesMain = () => {
<FullScreenLoader loading={loading} />
<BottomNavigator
screens={HOME_SCREENS}
initialRoute={isAgentTLOrAM ? BOTTOM_TAB_ROUTES.Cases : BOTTOM_TAB_ROUTES.VisitPlan}
initialRoute={isTeamLead ? BOTTOM_TAB_ROUTES.Cases : BOTTOM_TAB_ROUTES.VisitPlan}
onTabPress={(e) => onTabPressHandler(e)}
/>
<CasesActionButtons />

View File

@@ -320,3 +320,15 @@ export interface ICaseItemAvatarCaseDetailObj extends IFetchDocumentCaseDetailOb
export interface ICaseItemCaseDetailObj extends CaseDetail {
isIntermediateOrSelectedTodoCaseItem?: boolean;
}
export interface ISectionListData {
title: string;
data: IReportee[];
}
export interface IReportee {
agentReferenceId: string;
name: string;
agencyCode: string;
agencyName: string;
}

View File

@@ -1,5 +1,5 @@
import { CaseDetail, FeedbackStatus } from '../caseDetails/interface';
import { ICaseItem } from './interface';
import { ICaseItem, IReportee, ISectionListData } from './interface';
export const getAttemptedList = (
filteredCasesList: ICaseItem[],
@@ -27,3 +27,35 @@ export const getNonAttemptedList = (
: true
);
};
export const sectionListTranformData = (agentList: IReportee[]): ISectionListData[] => {
const result: ISectionListData[] = [];
// Create an object to map agency names to their corresponding data
const agencyMap: { [key: string]: IReportee[] } = {};
for (const agent of agentList) {
const agencyName = agent.agencyName;
if (!agencyMap[agencyName]) {
agencyMap[agencyName] = [];
}
agencyMap[agencyName].push({
agentReferenceId: agent.agentReferenceId,
name: agent.name,
agencyCode: agent.agencyCode,
agencyName: agent.agencyName,
});
}
// Transform the agencyMap into the desired result format
for (const agencyName in agencyMap) {
result.push({
title: agencyName,
data: agencyMap[agencyName],
});
}
return result;
};

View File

@@ -50,10 +50,14 @@ const ProtectedRouter = () => {
const { notificationsWithActions } = useAppSelector((state) => state.notifications);
const isOnline = useIsOnline();
const dispatch = useAppDispatch();
const isTeamLead = useAppSelector((state) => state.user.isTeamLead);
// Gets unified data for new visit plan cases
// TODO: Move this to another place
useEffect(() => {
if (isTeamLead) {
return;
}
if (newVisitedCases?.length) {
const loanAccountNumbers: string[] = [];
newVisitedCases.forEach((caseId) => {