Merge pull request #571 from navi-medici/feat/TP-34788

TP-34788 | Cosmos for AM/TLs
This commit is contained in:
Aman Chaturvedi
2023-09-14 21:03:19 +05:30
committed by GitHub
42 changed files with 1140 additions and 302 deletions

View File

@@ -135,7 +135,10 @@ function App() {
return (
<Provider store={store}>
<PersistGate loading={<FullScreenLoader loading />} persistor={persistor}>
<PersistGate
loading={<FullScreenLoader loading isTranslucent={false} />}
persistor={persistor}
>
<NavigationContainer
ref={navigationRef}
onStateChange={async (state) => {
@@ -157,7 +160,7 @@ function App() {
>
<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

@@ -216,11 +216,19 @@ android {
}
}
signingConfigs {
debug {
debug {
if (project.hasProperty('MYAPP_UPLOAD_STORE_FILE')) {
storeFile file(MYAPP_UPLOAD_STORE_FILE)
storePassword MYAPP_UPLOAD_STORE_PASSWORD
keyAlias MYAPP_UPLOAD_KEY_ALIAS
keyPassword MYAPP_UPLOAD_KEY_PASSWORD
}
else {
storeFile file('debug.keystore')
storePassword 'android'
keyAlias 'androiddebugkey'
keyPassword 'android'
}
}
release {
if (project.hasProperty('MYAPP_UPLOAD_STORE_FILE')) {

View File

@@ -1,6 +1,12 @@
import { ToastMessages } from './../screens/allCases/constants';
import 'react-native-get-random-values';
import { IUser, setAuthData } from '../reducer/userSlice';
import {
IUser,
MY_CASE_ITEM,
setAgentRole,
setAuthData,
setSelectedAgent,
} from '../reducer/userSlice';
import axiosInstance, { ApiKeys, API_STATUS_CODE, getApiUrl } from '../components/utlis/apiHelper';
import {
resetLoginForm,
@@ -18,7 +24,7 @@ import { GLOBAL, setGlobalUserData } from '../constants/Global';
import { resetCasesData } from '../reducer/allCasesSlice';
import { toast } from '../../RN-UI-LIB/src/components/toast';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { clearAllAsyncStorage, setAsyncStorageItem } from '../components/utlis/commonFunctions';
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';
@@ -28,6 +34,7 @@ import { GoogleSignin } from '@react-native-google-signin/google-signin';
import { resetConfig } from '../reducer/configSlice';
import { resetProfileData } from '../reducer/profileSlice';
import CosmosForegroundService from '../services/foregroundServices/foreground.service';
import { resetReportees } from '../reducer/reporteesSlice';
export interface GenerateOTPPayload {
phoneNumber: string;
@@ -115,6 +122,7 @@ export const verifyGoogleSignIn = (idToken: string) => async (dispatch: AppDispa
});
dispatch(setVerifyOTPSuccess('Login Successfully!'));
dispatch(resetLoginForm());
dispatch(getAgentDetail());
}
} catch (error: GenericType) {
await handleGoogleLogout();
@@ -157,6 +165,7 @@ export const verifyOTP =
);
dispatch(setVerifyOTPSuccess('OTP verified'));
dispatch(resetLoginForm());
dispatch(getAgentDetail());
})
.catch((err) => {
dispatch(setVerifyOTPError('Invalid OTP entered. Kindly try again'));
@@ -189,7 +198,13 @@ export const handleLogout = () => async (dispatch: AppDispatch) => {
await auth().signOut();
await handleGoogleLogout();
await clearAllAsyncStorage();
setGlobalUserData({ token: '', agentId: '', deviceId: '', isImpersonated: false });
setGlobalUserData({
token: '',
agentId: '',
deviceId: '',
isImpersonated: false,
selectedAgentId: '',
});
dispatch(
setAuthData({
sessionDetails: null,
@@ -201,6 +216,8 @@ export const handleLogout = () => async (dispatch: AppDispatch) => {
dispatch(resetCasesData());
dispatch(resetConfig());
dispatch(resetProfileData());
dispatch(resetReportees());
dispatch(setSelectedAgent(MY_CASE_ITEM));
} catch (err) {
logError(err as Error, 'Logout clear session details error');
}
@@ -229,6 +246,9 @@ export const handleImpersonatedUserLogin =
isImpersonated: true,
})
);
dispatch(resetReportees());
dispatch(setSelectedAgent(MY_CASE_ITEM));
dispatch(getAgentDetail());
successCallback?.();
})
.catch((err) => {
@@ -241,3 +261,13 @@ 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: string[] = response?.data?.roles || [];
dispatch(setAgentRole(roles));
}
});
};

View File

@@ -23,6 +23,8 @@ import { setFilters } from '../reducer/filtersSlice';
import { toast } from '../../RN-UI-LIB/src/components/toast';
import { ToastMessages } from '../screens/allCases/constants';
import { GenericFunctionArgs } from '../common/GenericTypes';
import { GLOBAL } from '../constants/Global';
import { MY_CASE_ITEM } from '../reducer/userSlice';
let _signedApiCallBucket: { req: any; added_At: number; callback: GenericFunctionArgs }[] = [];
let _signedApiCallBucketTimer: number = 0;
@@ -210,7 +212,11 @@ async function makeBulkSignedApiRequest(
payload: ISignedRequestItem[],
callback: GenericFunctionArgs | GenericFunctionArgs[]
) {
const url = getApiUrl(ApiKeys.GET_SIGNED_URL);
let url = getApiUrl(ApiKeys.GET_SIGNED_URL);
const reporteeReferenceId = GLOBAL?.SELECTED_AGENT_ID;
if (reporteeReferenceId && reporteeReferenceId !== MY_CASE_ITEM.referenceId) {
url = getApiUrl(ApiKeys.GET_SIGNED_URL_FOR_REPORTEE, {}, { reporteeReferenceId });
}
_signedApiCallBucket = [];
await axiosInstance
.post(url, payload, {

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);
dispatch(setReporteesLoading(true));
axiosInstance
.get(url)
.then((res) => {
const reporteesList = res.data?.data;
if (reporteesList) {
dispatch(setReporteesList(reporteesList));
}
})
.finally(() => {
dispatch(setReporteesLoading(false));
});
};

View File

@@ -0,0 +1,42 @@
import React from 'react';
import { View } from 'react-native';
import Svg, { Path, Mask, G, Rect } from 'react-native-svg';
import { COLORS } from '../../../RN-UI-LIB/src/styles/colors';
interface IArrowRightOutlineIcon {
width?: number;
height?: number;
fillColor?: string;
}
const ArrowDownOutlineIcon: React.FC<IArrowRightOutlineIcon> = ({
width = 24,
height = 24,
fillColor = COLORS.TEXT.WHITE,
}) => {
return (
<View>
<Svg width={width} height={height} viewBox={`0 0 ${width} ${height}`} fill="none">
<Mask
id="mask0_7859_44896"
mask-type="alpha"
maskUnits="userSpaceOnUse"
x="0"
y="0"
width={width}
height={height}
>
<Rect width={width} height={height} fill="#D9D9D9" />
</Mask>
<G mask="url(#mask0_7859_44896)">
<Path
d="M12 14.9758C11.8667 14.9758 11.7417 14.9549 11.625 14.9133C11.5083 14.8716 11.4 14.8008 11.3 14.7008L6.7 10.1008C6.51667 9.91745 6.425 9.68411 6.425 9.40078C6.425 9.11745 6.51667 8.88411 6.7 8.70078C6.88334 8.51745 7.11667 8.42578 7.4 8.42578C7.68334 8.42578 7.91667 8.51745 8.1 8.70078L12 12.6008L15.9 8.70078C16.0833 8.51745 16.3167 8.42578 16.6 8.42578C16.8833 8.42578 17.1167 8.51745 17.3 8.70078C17.4833 8.88411 17.575 9.11745 17.575 9.40078C17.575 9.68411 17.4833 9.91745 17.3 10.1008L12.7 14.7008C12.6 14.8008 12.4917 14.8716 12.375 14.9133C12.2583 14.9549 12.1333 14.9758 12 14.9758Z"
fill={fillColor}
/>
</G>
</Svg>
</View>
);
};
export default ArrowDownOutlineIcon;

View File

@@ -525,6 +525,27 @@ export const CLICKSTREAM_EVENT_NAMES = {
name: 'FA_GEOLOCATION_CAPTURING_FAILED',
description: 'Geolocation capturing failed',
},
// AM/TLs
FA_AGENT_LIST_DROPDOWN_CLICKED: {
name: 'FA_AGENT_LIST_DROPDOWN_CLICKED',
description: 'Agent list dropdown clicked',
},
FA_AGENT_LIST_BUTTON_CLICKED: {
name: 'FA_AGENT_LIST_BUTTON_CLICKED',
description: 'Agent list button clicked',
},
FA_AGENT_LIST_BOTTOMSHEET_LOAD_SUCCESS: {
name: 'FA_AGENT_LIST_BOTTOMSHEET_LOAD_SUCCESS',
description: 'Agent list bottomsheet load success',
},
FA_AGENT_SELECT_BUTTON_CLICKED: {
name: 'FA_AGENT_SELECT_BUTTON_CLICKED',
description: 'Agent select button clicked',
},
FA_AGENT_CASE_LOAD_SUCCESS: {
name: 'FA_AGENT_CASE_LOAD_SUCCESS',
description: 'Agent case load success',
},
} as const;
export enum MimeType {
@@ -634,6 +655,7 @@ export const REQUEST_TYPE_TO_BLOCK_FOR_IMPERSONATION = ['post', 'put', 'patch',
export const REQUEST_TO_UNBLOCK_FOR_IMPERSONATION = [
getApiUrl(ApiKeys.GET_SIGNED_URL),
getApiUrl(ApiKeys.GET_SIGNED_URL_FOR_REPORTEE),
getApiUrl(ApiKeys.LOGOUT),
];

View File

@@ -58,6 +58,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,
@@ -232,12 +233,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,
@@ -252,6 +247,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;
@@ -291,7 +295,7 @@ const TrackingComponent: React.FC<ITrackingComponent> = ({ children }) => {
}
await handleGetCaseSyncStatus();
dispatch(getConfigData());
if (LAST_SYNC_STATUS !== SyncStatus.FETCH_CASES) {
if (!isTeamLead && LAST_SYNC_STATUS !== SyncStatus.FETCH_CASES) {
const updatedDetails: ISyncedCases = await fetchCasesToSync(referenceId);
if (updatedDetails?.cases?.length) {
dispatch(syncCasesByFallback(updatedDetails));

View File

@@ -156,7 +156,13 @@ const ImageUpload: React.FC<IImageUpload> = (props) => {
</ImageBackground>
</View>
)}
<ErrorMessage show={error?.sectionContext?.[sectionId]?.questionContext?.[questionId]} />
<ErrorMessage
show={
error?.widgetContext?.[widgetId]?.sectionContext?.[sectionId]?.questionContext?.[
questionId
]
}
/>
</View>
);
};

View File

@@ -4,6 +4,8 @@ export function isQuestionMandatory(question: QuestionV1): boolean {
return !!question.metadata?.validators?.['required']?.value;
}
const DEFAULT_ERROR_MESSAGE = 'This is a mandatory question';
export function validateInput(data: { answer: any; type: string }, allRules: any): boolean {
let result = true;
const currentDate = new Date();
@@ -32,7 +34,7 @@ export function validateInput(data: { answer: any; type: string }, allRules: any
data?.answer === undefined ||
!`${data?.answer}`.trim().length
) {
result = rule.message || 'Required';
result = rule.message || DEFAULT_ERROR_MESSAGE;
break;
}
} else if (ruleName === Validators.PATTERN) {

View File

@@ -8,12 +8,19 @@ import { CLICKSTREAM_EVENT_NAMES } from '../../common/Constants';
import { useAppSelector } from '../../hooks';
import { addClickstreamEvent } from '../../services/clickstreamEventService';
import { getCurrentScreen, pushToScreen } from '../utlis/navigationUtlis';
import { MY_CASE_ITEM } from '../../reducer/userSlice';
const NotificationMenu = () => {
const { totalUnreadElements } = useAppSelector((state) => state.notifications);
const { isTeamLead, selectedAgent = MY_CASE_ITEM } = useAppSelector((state) => state.user);
const [isEnabled, setIsEnabled] = useState(false);
const showNotifications = !isTeamLead || selectedAgent?.referenceId === MY_CASE_ITEM.referenceId;
const handleNotificationPress = () => {
if (!showNotifications) {
return;
}
if (isEnabled) return;
setIsEnabled(true);
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_NOTIFICATION_ICON_CLICK, {
@@ -30,20 +37,24 @@ const NotificationMenu = () => {
style={GenericStyles.iconContainerButton}
onPress={handleNotificationPress}
>
<View style={[GenericStyles.ph8, GenericStyles.pv10]}>
<BellIcon />
<View style={[styles.notificationBadge, GenericStyles.alignCenter]}>
{totalUnreadElements ? (
<Text
bold
small
style={[styles.notificationNumber, { width: totalUnreadElements > 9 ? 24 : 16 }]}
>
{totalUnreadElements > 9 ? '9+' : totalUnreadElements}
</Text>
) : null}
{showNotifications ? (
<View style={[GenericStyles.ph8, GenericStyles.pv10]}>
<BellIcon />
<View style={[styles.notificationBadge, GenericStyles.alignCenter]}>
{totalUnreadElements ? (
<Text
bold
small
style={[styles.notificationNumber, { width: totalUnreadElements > 9 ? 24 : 16 }]}
>
{totalUnreadElements > 9 ? '9+' : totalUnreadElements}
</Text>
) : null}
</View>
</View>
</View>
) : (
<></>
)}
</TouchableHighlight>
);
};

View File

@@ -54,6 +54,9 @@ 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',
GET_SIGNED_URL_FOR_REPORTEE = 'GET_SIGNED_URL_FOR_REPORTEE',
}
export const API_URLS: Record<ApiKeys, string> = {} as Record<ApiKeys, string>;
@@ -70,8 +73,9 @@ API_URLS[ApiKeys.GENERATE_PAYMENT_LINK] = '/payments/send-payment-link';
API_URLS[ApiKeys.ADDRESSES_GEOLOCATION] = '/addresses-geolocations';
API_URLS[ApiKeys.NEW_ADDRESS] = '/addresses';
API_URLS[ApiKeys.GET_SIGNED_URL] = '/cases/get-signed-urls';
API_URLS[ApiKeys.GET_SIGNED_URL_FOR_REPORTEE] = '/cases/get-signed-urls-for-reportee';
API_URLS[ApiKeys.CASE_UNIFIED_DETAILS] = '/v2/collection-cases/unified-details/{loanAccountNumber}';
API_URLS[ApiKeys.UNGROUPED_ADDRESSES] = '/addresses/ungrouped/{loanAccountNumber}';
API_URLS[ApiKeys.UNGROUPED_ADDRESSES] = '/addresses/ungrouped-v2/{loanAccountNumber}';
API_URLS[ApiKeys.PAST_FEEDBACK] = '/feedback';
API_URLS[ApiKeys.PAST_FEEDBACK_ON_ADDRESSES] = '/feedback/v2';
API_URLS[ApiKeys.NOTIFICATIONS] = '/notification/fetch';
@@ -93,6 +97,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/all-field-reportees';
API_URLS[ApiKeys.GET_AGENT_DETAIL] = '/user/role-info';
export const API_STATUS_CODE = {
OK: 200,

View File

@@ -11,6 +11,7 @@ export const GLOBAL = {
AGENT_ID: '',
DEVICE_TYPE: DEVICE_TYPE_ENUM.MOBILE,
IS_IMPERSONATED: false,
SELECTED_AGENT_ID: '',
};
interface IGlobalUserData {
@@ -19,13 +20,15 @@ interface IGlobalUserData {
agentId?: string;
deviceType?: DEVICE_TYPE_ENUM;
isImpersonated?: boolean;
selectedAgentId?: string;
}
export const setGlobalUserData = (userData: IGlobalUserData) => {
const { token, deviceId, agentId, deviceType, isImpersonated } = userData;
const { token, deviceId, agentId, deviceType, isImpersonated, selectedAgentId } = userData;
if (!isNullOrUndefined(token)) GLOBAL.SESSION_TOKEN = `${token}`;
if (!isNullOrUndefined(deviceId)) GLOBAL.DEVICE_ID = `${deviceId}`;
if (!isNullOrUndefined(agentId)) GLOBAL.AGENT_ID = `${agentId}`;
if (!isNullOrUndefined(deviceType)) GLOBAL.DEVICE_TYPE = deviceType as DEVICE_TYPE_ENUM;
if (!isNullOrUndefined(isImpersonated)) GLOBAL.IS_IMPERSONATED = isImpersonated ?? false;
if (!isNullOrUndefined(selectedAgentId)) GLOBAL.SELECTED_AGENT_ID = `${selectedAgentId}`;
};

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(() => {
@@ -105,6 +106,7 @@ const useFirestoreUpdates = () => {
caseUpdates,
isInitialLoad,
isVisitPlanLocked: lockRef?.current?.visitPlanStatus === VisitPlanStatus.LOCKED,
selectedAgent: selectedAgent,
})
);
!isInitialLoad && showCaseUpdationToast(newlyAddedCases, deletedCases);
@@ -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?.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.
@@ -212,7 +212,11 @@ const useFirestoreUpdates = () => {
};
const subscribeToFilters = () => {
const collectionPath = `filters/${user?.referenceId}`;
let refId = user?.referenceId;
if (isTeamLead && selectedAgent?.referenceId !== MY_CASE_ITEM.referenceId) {
refId = selectedAgent?.referenceId;
}
const collectionPath = `filters/${refId}`;
return subscribeToDoc(handleFilterUpdate, collectionPath);
};
@@ -263,6 +267,30 @@ const useFirestoreUpdates = () => {
};
}, [isLoggedIn, user?.referenceId]);
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]);
useEffect(() => {
forceUninstallUnsubscribe = subscribeToForceUninstall();
}, []);

View File

@@ -12,6 +12,7 @@ import {
CaseAllocationType,
caseVerdict,
ICaseItem,
IReportee,
} from '../screens/allCases/interface';
import { CaseDetail, CONTEXT_TASK_STATUSES, DOCUMENT_TYPE } from '../screens/caseDetails/interface';
import { addClickstreamEvent } from '../services/clickstreamEventService';
@@ -19,6 +20,7 @@ import { getLoanAccountNumber } from '../components/utlis/commonFunctions';
import { getVisitedWidgetsNodeList } from '../components/form/services/forms.service';
import { CollectionCaseWidgetId, CommonCaseWidgetId } from '../types/template.types';
import { IAvatarUri } from '../action/caseListAction';
import { MY_CASE_ITEM } from './userSlice';
export type ICasesMap = { [key: string]: ICaseItem };
interface IAllCasesSlice {
@@ -237,14 +239,12 @@ const allCasesSlice = createSlice({
state.loading = action.payload;
},
updateCaseDetailsFirestore: (state, action) => {
const { caseUpdates, isInitialLoad, isVisitPlanLocked } = action.payload as {
const { caseUpdates, isInitialLoad, isVisitPlanLocked, selectedAgent } = action.payload as {
caseUpdates: CaseUpdates[];
isInitialLoad: boolean;
isVisitPlanLocked: boolean;
selectedAgent: IReportee;
};
if (state.loading) {
state.loading = false;
}
let newVisitCaseLoanIds: string[] = [];
let newVisitCollectionCases: string[] = [];
let removedVisitedCasesLoanIds: string[] = [];
@@ -356,6 +356,14 @@ const allCasesSlice = createSlice({
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, {
@@ -503,26 +511,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

@@ -73,9 +73,11 @@ const filtersSlice = createSlice({
});
state.filterCountVisitPlan = filterCount;
},
resetFilters: () => initialState,
},
});
export const { setFilters, setSelectedFilters, setSelectedFiltersVisitPlan } = filtersSlice.actions;
export const { setFilters, setSelectedFilters, setSelectedFiltersVisitPlan, resetFilters } =
filtersSlice.actions;
export default filtersSlice.reducer;

View File

@@ -0,0 +1,41 @@
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;
},
resetReportees: () => initialState,
},
});
export const {
setReporteesList,
setReporteesLoading,
setShowAgentSelectionBottomSheet,
resetReportees,
} = 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,18 @@ interface ISessionDetails {
export enum IUserRole {
SUPER_USER = 'ADDRESS_VERIFICATION:API:SUPER_USER',
ROLE_TEAM_LEAD = 'ROLE_TEAM_LEAD',
ROLE_NAVI_FIELD_TEAM_LEAD = 'ROLE_NAVI_FIELD_TEAM_LEAD',
ROLE_NAVI_FIELD_EXTERNAL_TEAM_LEAD = 'ROLE_NAVI_FIELD_EXTERNAL_TEAM_LEAD',
}
export const MY_CASE_ITEM = {
name: 'My Cases',
referenceId: 'MY_CASES',
agencyCode: '',
agencyName: '',
};
interface IUserDetails {
emailId: string;
referenceId: string;
@@ -47,6 +58,8 @@ export interface IUserSlice extends IUser {
clickstreamEvents: IClickstreamEvents[];
isImpersonated: boolean;
lock: ILockData;
selectedAgent: IReportee;
isTeamLead: boolean;
}
const initialState: IUserSlice = {
@@ -59,6 +72,8 @@ const initialState: IUserSlice = {
lock: {
visitPlanStatus: VisitPlanStatus.UNLOCKED,
},
selectedAgent: MY_CASE_ITEM,
isTeamLead: false,
};
export const userSlice = createSlice({
@@ -86,9 +101,25 @@ export const userSlice = createSlice({
state.lock = action.payload;
}
},
setSelectedAgent: (state, action) => {
state.selectedAgent = action.payload;
},
setAgentRole: (state, action) => {
if (action?.payload?.length) {
state.isTeamLead = action.payload.some((role: IUserRole) =>
[
IUserRole.ROLE_NAVI_FIELD_TEAM_LEAD,
IUserRole.ROLE_NAVI_FIELD_EXTERNAL_TEAM_LEAD,
].includes(role)
);
} else {
state.isTeamLead = 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,8 @@ const Profile: React.FC = () => {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_ID_CARD_CLICKED);
};
const showCompletedCases = !isTeamLead || selectedAgent?.referenceId === MY_CASE_ITEM.referenceId;
return (
<View style={[GenericStyles.fill]}>
<NavigationHeader
@@ -174,53 +180,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

@@ -88,58 +88,60 @@ const UngroupedAddressContainer: React.FC<IUngroupedAddress> = ({ route: routePa
};
return (
<ScrollView>
<View style={GenericStyles.fill}>
<NavigationHeader title={PAGE_TITLE} onBack={goBack} />
<SuspenseLoader
loading={loading}
fallBack={
<>
{[...Array(8).keys()].map(() => (
<LineLoader
width="100%"
height={75}
style={[GenericStyles.br6, { marginBottom: 20 }]}
/>
))}
</>
}
>
{ungroupedAddressList?.length ? (
<View>
{ungroupedAddressList.map((ungroupedAddressItem: IAddress) => (
<View>
<AddressItem
caseId={caseId}
showRelativeDistance
containerStyle={styles.addressItemContainer}
key={ungroupedAddressItem?.id}
addressItem={ungroupedAddressItem}
showActionButtons
handleOldFeedbackRouting={() => {
handleOpenOldFeedbacks(ungroupedAddressItem);
}}
handleCloseRouting={() => {
navigateToScreen(PageRouteEnum.ADDITIONAL_ADDRESSES, commonParams);
}}
showSource
<ScrollView>
<SuspenseLoader
loading={loading}
fallBack={
<>
{[...Array(8).keys()].map(() => (
<LineLoader
width="100%"
height={75}
style={[GenericStyles.br6, { marginBottom: 20 }]}
/>
</View>
))}
</View>
) : (
<View
style={[
styles.noAddressContainer,
GenericStyles.centerAligned,
GenericStyles.columnDirection,
]}
>
<HomeIcon />
<Text style={[styles.textContainer, styles.noAddressText]}>No addresses found</Text>
</View>
)}
</SuspenseLoader>
</ScrollView>
))}
</>
}
>
{ungroupedAddressList?.length ? (
<View>
{ungroupedAddressList.map((ungroupedAddressItem: IAddress) => (
<View>
<AddressItem
caseId={caseId}
showRelativeDistance
containerStyle={styles.addressItemContainer}
key={ungroupedAddressItem?.id}
addressItem={ungroupedAddressItem}
showActionButtons
handleOldFeedbackRouting={() => {
handleOpenOldFeedbacks(ungroupedAddressItem);
}}
handleCloseRouting={() => {
navigateToScreen(PageRouteEnum.ADDITIONAL_ADDRESSES, commonParams);
}}
showSource
/>
</View>
))}
</View>
) : (
<View
style={[
styles.noAddressContainer,
GenericStyles.centerAligned,
GenericStyles.columnDirection,
]}
>
<HomeIcon />
<Text style={[styles.textContainer, styles.noAddressText]}>No addresses found</Text>
</View>
)}
</SuspenseLoader>
</ScrollView>
</View>
);
};

View File

@@ -0,0 +1,88 @@
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';
import { resetFilters } from '../../reducer/filtersSlice';
import { setGlobalUserData } from '../../constants/Global';
import { addClickstreamEvent } from '../../services/clickstreamEventService';
import { CLICKSTREAM_EVENT_NAMES } from '../../common/Constants';
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 = () => {
const selectedAgentId = agent.referenceId;
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_AGENT_SELECT_BUTTON_CLICKED, {
selectedAgentId,
});
dispatch(setSelectedAgent(agent));
setGlobalUserData({ selectedAgentId });
dispatch(resetFilters());
dispatch(resetCasesData());
dispatch(setShowAgentSelectionBottomSheet(false));
};
return (
<Pressable
style={[
GenericStyles.row,
GenericStyles.justifyContentSpaceBetween,
GenericStyles.whiteBackground,
GenericStyles.p12,
GenericStyles.br8,
GenericStyles.border,
GenericStyles.alignCenter,
styles.shadow,
{
backgroundColor:
agent.referenceId === selectedAgent.referenceId
? 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,22 @@
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}
showClearIcon
/>
);
};
export default AgentListSearchbar;

View File

@@ -0,0 +1,127 @@
import {
Animated,
NativeScrollEvent,
NativeSyntheticEvent,
Pressable,
ScrollView,
SectionList,
StyleSheet,
View,
} from 'react-native';
import React, { useEffect, useMemo, useRef } 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';
import { GenericType } from '../../common/GenericTypes';
interface IAgentListView {
searchQuery: string;
scrollAnimation: Animated.Value;
}
const AgentListView: React.FC<IAgentListView> = ({ scrollAnimation, searchQuery }) => {
const { agentsList = [] } = useAppSelector((state) => state.reportees);
const listRef = useRef<GenericType>(null);
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]);
// Scroll the flatlist to top when search query changes
useEffect(() => {
if (listRef?.current) {
listRef.current.scrollToLocation({
sectionIndex: 0,
itemIndex: 0,
});
}
}, [searchQuery]);
const handleListScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
const offsetY = event.nativeEvent.contentOffset.y;
scrollAnimation.setValue(offsetY);
};
if (filteredAgentsList.length === 0) {
return (
<ScrollView contentContainerStyle={[GenericStyles.fill, styles.pt64]}>
<View style={GenericStyles.alignCenter}>
<NoCasesFoundIcon />
<Heading style={GenericStyles.pt16} type="h4">
No {agentsList.length === 0 ? 'reportees' : 'results'} found
</Heading>
{agentsList.length ? <Text light>Try searching something else</Text> : null}
</View>
</ScrollView>
);
}
return (
<SectionList
ref={listRef}
sections={filteredAgentsList}
keyExtractor={(item, index) => item.referenceId + index}
onScroll={handleListScroll}
contentContainerStyle={[GenericStyles.ph16, styles.pt8]}
keyboardShouldPersistTaps={'handled'}
renderItem={({ item }) => (
<Pressable style={[GenericStyles.pb16]}>
<AgentListItem
agent={item}
searchQuery={searchQuery}
leftAdornment={
item.referenceId === MY_CASE_ITEM.referenceId ? <ProfileSolidIcon /> : null
}
/>
</Pressable>
)}
renderSectionHeader={({ section: { title } }) => {
if (!title) {
return null;
}
return (
<Pressable
style={[GenericStyles.centerAlignedRow, GenericStyles.pb16, GenericStyles.pt16]}
>
<Text bold dark>
{title}
</Text>
<View style={styles.separatorLine} />
</Pressable>
);
}}
/>
);
};
const styles = StyleSheet.create({
separatorLine: {
height: 1,
flex: 1,
backgroundColor: COLORS.BORDER.PRIMARY,
marginLeft: 8,
},
pt64: {
marginTop: 64,
},
pt8: {
paddingTop: 8,
},
});
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,83 @@
import { Animated, 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';
import { CLICKSTREAM_EVENT_NAMES } from '../../common/Constants';
import { addClickstreamEvent } from '../../services/clickstreamEventService';
interface IAgentsListContainer {
showAgentSelectionBottomSheet: boolean;
}
const AgentsListContainer: React.FC<IAgentsListContainer> = ({ showAgentSelectionBottomSheet }) => {
const [searchQuery, setSearchQuery] = React.useState<string>('');
const { isLoading, agentsList } = useAppSelector((state) => state.reportees);
const dispatch = useAppDispatch();
useEffect(() => {
showAgentSelectionBottomSheet &&
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_AGENT_LIST_BOTTOMSHEET_LOAD_SUCCESS);
}, [showAgentSelectionBottomSheet]);
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

@@ -1,30 +1,30 @@
import { Animated, StyleSheet, View } from 'react-native';
import React 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 Filters from './Filters';
import NotificationMenu from '../../components/notificationMenu';
import HeaderLabel from './HeaderLabel';
interface ICaseListHeader {
filterCount: number;
searchQuery: string;
handleSearchChange: (val: string) => void;
toggleFilterModal: () => void;
headerHeight: Animated.AnimatedInterpolation<string | number>;
headerLabel: string;
showFilters: boolean;
setShowAgentSelectionBottomSheet: (val: boolean) => void;
filteredListCount: number;
isVisitPlan?: boolean;
}
const CaseListHeader: React.FC<ICaseListHeader> = ({
searchQuery,
filterCount,
handleSearchChange,
toggleFilterModal,
headerHeight,
headerLabel,
showFilters,
setShowAgentSelectionBottomSheet,
filteredListCount,
isVisitPlan,
}) => {
return (
@@ -40,15 +40,16 @@ const CaseListHeader: React.FC<ICaseListHeader> = ({
GenericStyles.pv10,
]}
>
<Heading type="h3" style={[styles.headerLabel]}>
{headerLabel}
</Heading>
<HeaderLabel
setShowAgentSelectionBottomSheet={setShowAgentSelectionBottomSheet}
filteredListCount={filteredListCount}
isVisitPlan={isVisitPlan}
/>
<NotificationMenu />
</View>
{showFilters && (
<Filters
searchQuery={searchQuery}
filterCount={filterCount}
handleSearchChange={handleSearchChange}
toggleFilterModal={toggleFilterModal}
isVisitPlan={isVisitPlan}
@@ -67,8 +68,5 @@ const styles = StyleSheet.create({
width: '100%',
backgroundColor: COLORS.BACKGROUND.INDIGO,
},
headerLabel: {
color: COLORS.BACKGROUND.PRIMARY,
},
});
export default CaseListHeader;

View File

@@ -5,6 +5,7 @@ import {
Modal,
NativeScrollEvent,
NativeSyntheticEvent,
Pressable,
StyleSheet,
View,
} from 'react-native';
@@ -12,12 +13,12 @@ import { GenericStyles, SCREEN_HEIGHT, SCREEN_WIDTH } from '../../../RN-UI-LIB/s
import { COLORS } from '../../../RN-UI-LIB/src/styles/colors';
import { RootState } from '../../store/store';
import { CaseTypes, ICaseItem, ICaseItemCaseDetailObj } from './interface';
import { EmptyListMessages, LIST_HEADER_ITEMS, ListHeaderItems } from './constants';
import { LIST_HEADER_ITEMS, ListHeaderItems } from './constants';
import CaseItem from './CaseItem';
import { Search } from '../../../RN-UI-LIB/src/utlis/search';
import FiltersContainer from '../../components/screens/allCases/allCasesFilters/FiltersContainer';
import EmptyList from './EmptyList';
import { useAppSelector } from '../../hooks';
import { useAppDispatch, useAppSelector } from '../../hooks';
import { addClickstreamEvent } from '../../services/clickstreamEventService';
import {
CLICKSTREAM_EVENT_NAMES,
@@ -40,9 +41,15 @@ import { getCurrentScreen } from '../../components/utlis/navigationUtlis';
import { useFocusEffect } from '@react-navigation/native';
import { FlashList } from '@shopify/flash-list';
import { VisitPlanStatus } from '../../reducer/userSlice';
import { dateFormat, DAY_MONTH_DATE_FORMAT } from '../../../RN-UI-LIB/src/utlis/dates';
import { getAttemptedList, getNonAttemptedList } from './utils';
import { GenericType } from '../../common/GenericTypes';
import Text from '../../../RN-UI-LIB/src/components/Text';
import BottomSheet from '../../../RN-UI-LIB/src/components/bottom_sheet/BottomSheet';
import Heading from '../../../RN-UI-LIB/src/components/Heading';
import { row } from '../emiSchedule/constants';
import CloseIcon from '../../../RN-UI-LIB/src/Icons/CloseIcon';
import AgentsListContainer from './AgentsListContainer';
import { setShowAgentSelectionBottomSheet } from '../../reducer/reporteesSlice';
export const getItem = (item: Array<ICaseItem>, index: number) => item[index];
export const ESTIMATED_ITEM_SIZE = 250; // Average height of List item
@@ -63,16 +70,27 @@ const CasesList: React.FC<ICasesList> = ({ casesList = [], isVisitPlan, allCases
selectedTodoListMap,
} = useAppSelector((state: RootState) => state.allCases);
const [searchQuery, setSearchQuery] = useState('');
const { filters, filterCount, selectedFilters, quickFiltersPresent, isLockedVisitPlanStatus } =
useAppSelector((state: RootState) => ({
filters: state.filters.filters,
filterCount: isVisitPlan ? state.filters.filterCountVisitPlan : state.filters.filterCount,
selectedFilters: isVisitPlan
? state.filters.selectedFiltersVisitPlan
: state.filters.selectedFilters,
quickFiltersPresent: state?.filters?.quickFilters?.length > 0,
isLockedVisitPlanStatus: state.user?.lock?.visitPlanStatus === VisitPlanStatus.LOCKED,
}));
const {
filters,
filterCount,
selectedFilters,
quickFiltersPresent,
isLockedVisitPlanStatus,
showAgentSelectionBottomSheet,
selectedAgent,
} = useAppSelector((state: RootState) => ({
filters: state.filters.filters,
filterCount: isVisitPlan ? state.filters.filterCountVisitPlan : state.filters.filterCount,
selectedFilters: isVisitPlan
? state.filters.selectedFiltersVisitPlan
: state.filters.selectedFilters,
quickFiltersPresent: state?.filters?.quickFilters?.length > 0,
isLockedVisitPlanStatus: state.user?.lock?.visitPlanStatus === VisitPlanStatus.LOCKED,
selectedAgent: state.user?.selectedAgent,
showAgentSelectionBottomSheet: state.reportees.showAgentSelectionBottomSheet,
}));
const dispatch = useAppDispatch();
const [showFilterModal, setShowFilterModal] = React.useState<boolean>(false);
const flashListRef = useRef<GenericType>(null);
@@ -93,6 +111,10 @@ const CasesList: React.FC<ICasesList> = ({ casesList = [], isVisitPlan, allCases
}, [])
);
const toggleAgentSelectionBottomSheet = () => {
dispatch(setShowAgentSelectionBottomSheet(!showAgentSelectionBottomSheet));
};
const headerHeight = scrollAnimation.interpolate({
inputRange: [0, HEADER_SCROLL_DISTANCE],
outputRange: isVisitPlan
@@ -101,6 +123,10 @@ const CasesList: React.FC<ICasesList> = ({ casesList = [], isVisitPlan, allCases
extrapolate: 'clamp',
});
useEffect(() => {
setSearchQuery('');
}, [selectedAgent]);
//TODO: clean these different heights
const headerHeightQuickFilters = scrollAnimation.interpolate({
inputRange: [0, HEADER_SCROLL_DISTANCE_WITH_QUICK_FILTERS],
@@ -144,41 +170,12 @@ const CasesList: React.FC<ICasesList> = ({ casesList = [], isVisitPlan, allCases
: filteredList;
const isFilterApplied = filterCount > 0 || searchQuery.length > 0;
const getEmptyListMessage = () => {
if (isLockedVisitPlanStatus && isVisitPlan) {
const currentDate = new Date();
return (
EmptyListMessages.GENERATING_VISIT_PLAN +
' ' +
dateFormat(currentDate, DAY_MONTH_DATE_FORMAT) +
'...'
);
}
if (isFilterApplied) {
return EmptyListMessages.NO_CASES_FOUND;
}
if (isVisitPlan) {
return EmptyListMessages.NO_VISIT_PLANS;
}
return EmptyListMessages.NO_PENDING_CASES;
};
const getSubMessage = () => {
if (isVisitPlan && isLockedVisitPlanStatus) {
return EmptyListMessages.GENERATING_VISIT_PLAN_SUB;
}
if (isFilterApplied) {
return EmptyListMessages.NO_CASES_FOUND_SUB;
}
};
const listEmptyComponent = (
<EmptyList
message={getEmptyListMessage()}
isVisitPlan={isVisitPlan}
isFilterApplied={isFilterApplied}
subMessage={getSubMessage()}
isLockedVisitPlanStatus={isLockedVisitPlanStatus}
setShowAgentSelectionBottomSheet={toggleAgentSelectionBottomSheet}
/>
);
@@ -272,17 +269,6 @@ const CasesList: React.FC<ICasesList> = ({ casesList = [], isVisitPlan, allCases
}
setShowFilterModal(!showFilterModal);
};
const getHeaderLabel = () => {
if (isVisitPlan) {
if (isLockedVisitPlanStatus) {
return 'Generating visit plan';
}
const currentDate = new Date();
const formattedDate = dateFormat(currentDate, DAY_MONTH_DATE_FORMAT);
return formattedDate;
}
return `My Cases (${filteredCasesList.length})`;
};
let listStyle = {};
@@ -300,31 +286,30 @@ const CasesList: React.FC<ICasesList> = ({ casesList = [], isVisitPlan, allCases
}
}
const headerHeightValue = quickFiltersPresent ? headerHeightQuickFilters : headerHeight;
const showFilters = isVisitPlan && isLockedVisitPlanStatus ? false : !!casesList.length;
return (
<View style={[GenericStyles.fill, styles.container]}>
<CaseListHeader
filterCount={filterCount}
searchQuery={searchQuery}
handleSearchChange={handleSearchChange}
toggleFilterModal={toggleFilterModal}
headerHeight={quickFiltersPresent ? headerHeightQuickFilters : headerHeight}
headerLabel={getHeaderLabel()}
showFilters={isVisitPlan && isLockedVisitPlanStatus ? false : !!casesList.length}
setShowAgentSelectionBottomSheet={toggleAgentSelectionBottomSheet}
filteredListCount={filteredCasesList?.length}
showFilters={showFilters}
isVisitPlan={isVisitPlan}
/>
{visitPlansUpdating ? (
<Animated.View
style={[
styles.fillOverlay,
{ top: quickFiltersPresent ? headerHeightQuickFilters : headerHeight },
]}
></Animated.View>
<Animated.View style={[styles.fillOverlay, { top: headerHeightValue }]}></Animated.View>
) : null}
<View style={GenericStyles.fill}>
{filteredCasesListWithCTA.length ? (
<FlashList
ref={flashListRef}
data={filteredCasesListWithCTA}
keyboardShouldPersistTaps={'handled'}
scrollEventThrottle={16}
contentContainerStyle={listStyle}
onScroll={handleListScroll}
@@ -353,6 +338,26 @@ const CasesList: React.FC<ICasesList> = ({ casesList = [], isVisitPlan, allCases
isVisitPlan={isVisitPlan}
/>
</Modal>
<BottomSheet
HeaderNode={() => (
<View style={[...row, GenericStyles.ph16]}>
<Heading dark type="h4">
View team's assigned cases
</Heading>
<Pressable onPress={toggleAgentSelectionBottomSheet} style={styles.p4}>
<CloseIcon color={COLORS.TEXT.LIGHT} />
</Pressable>
</View>
)}
heightPercentage={100}
allowBackdropClose={false}
visible={showAgentSelectionBottomSheet}
setVisible={toggleAgentSelectionBottomSheet}
>
<View style={[GenericStyles.mt16, GenericStyles.fill]}>
<AgentsListContainer showAgentSelectionBottomSheet={showAgentSelectionBottomSheet} />
</View>
</BottomSheet>
</View>
);
};
@@ -396,6 +401,9 @@ const styles = StyleSheet.create({
paddingTop: VISIT_PLAN_HEADER_HEIGHT_MAX_WITH_QUICK_FILTERS,
paddingBottom: 10,
},
p4: {
padding: 4,
},
});
export default CasesList;

View File

@@ -41,7 +41,7 @@ const CompletedCase: React.FC = () => {
keyExtractor={(item) => item.caseReferenceId}
getItemCount={(item) => item.length}
getItem={getItem}
ListEmptyComponent={<EmptyList message={EmptyListMessages.NO_COMPLETED_CASES} />}
ListEmptyComponent={<EmptyList isCompleted />}
/>
</View>
);

View File

@@ -2,31 +2,44 @@ import { StyleSheet, View } from 'react-native';
import React from 'react';
import { GenericStyles } from '../../../RN-UI-LIB/src/styles';
import Heading from '../../../RN-UI-LIB/src/components/Heading';
import EmptyPageCheckIcon from '../../assets/icons/EmptyPageCheckIcon';
import Button from '../../../RN-UI-LIB/src/components/Button';
import { navigateToScreen } from '../../components/utlis/navigationUtlis';
import NoCasesFoundIcon from '../../assets/icons/NoCasesFoundIcon';
import Text from '../../../RN-UI-LIB/src/components/Text';
import { useAppSelector } from '../../hooks';
import { RootState } from '../../store/store';
import GeneratingVisitPlan from '../../assets/icons/GeneratingVisitPlanIcon';
import NoCasesFoundIcon from '../../assets/icons/NoCasesFoundIcon';
import EmptyPageCheckIcon from '../../assets/icons/EmptyPageCheckIcon';
import { MY_CASE_ITEM, VisitPlanStatus } from '../../reducer/userSlice';
import { EmptyListMessages } from './constants';
import { navigateToScreen } from '../../components/utlis/navigationUtlis';
import { dateFormat, DAY_MONTH_DATE_FORMAT } from '../../../RN-UI-LIB/src/utlis/dates';
import { addClickstreamEvent } from '../../services/clickstreamEventService';
import { CLICKSTREAM_EVENT_NAMES } from '../../common/Constants';
interface IEmptyList {
message: string;
subMessage?: string;
isCompleted?: boolean;
isVisitPlan?: boolean;
isFilterApplied?: boolean;
isLockedVisitPlanStatus?: boolean;
setShowAgentSelectionBottomSheet?: (val: boolean) => void;
}
const EmptyList: React.FC<IEmptyList> = ({
message,
isVisitPlan,
isFilterApplied,
subMessage,
isLockedVisitPlanStatus,
}) => {
const handleCreateVisitPlan = () => {
navigateToScreen('Cases');
};
const EmptyList: React.FC<IEmptyList> = (props) => {
const { isCompleted, isVisitPlan, isFilterApplied, setShowAgentSelectionBottomSheet } = props;
const {
isLockedVisitPlanStatus,
isCasesLoading,
selectedAgent = MY_CASE_ITEM,
} = useAppSelector((state: RootState) => ({
isLockedVisitPlanStatus: state.user?.lock?.visitPlanStatus === VisitPlanStatus.LOCKED,
isCasesLoading: state.allCases.loading,
selectedAgent: state.user?.selectedAgent,
}));
const isTeamLead = useAppSelector((state) => state.user.isTeamLead);
if (isCasesLoading) {
return null;
}
const renderIcon = () => {
if (isLockedVisitPlanStatus && isVisitPlan) {
@@ -38,6 +51,76 @@ const EmptyList: React.FC<IEmptyList> = ({
return <EmptyPageCheckIcon />;
};
const getEmptyListMessage = () => {
if (isCompleted) {
return EmptyListMessages.NO_COMPLETED_CASES;
}
if (isLockedVisitPlanStatus && isVisitPlan) {
const currentDate = new Date();
return (
EmptyListMessages.GENERATING_VISIT_PLAN +
' ' +
dateFormat(currentDate, DAY_MONTH_DATE_FORMAT) +
'...'
);
}
if (isFilterApplied) {
return EmptyListMessages.NO_CASES_FOUND;
}
if (isVisitPlan) {
return EmptyListMessages.NO_VISIT_PLANS;
}
if (isTeamLead) {
if (selectedAgent.referenceId === MY_CASE_ITEM.referenceId) {
return EmptyListMessages.NO_ACTIVE_ALLOCATIONS;
}
return EmptyListMessages.NO_ACTIVE_ALLOCATIONS_SELECTED_AGENT;
}
return EmptyListMessages.NO_PENDING_CASES;
};
const getSubMessage = () => {
if (isVisitPlan && isLockedVisitPlanStatus) {
return EmptyListMessages.GENERATING_VISIT_PLAN_SUB;
}
if (isFilterApplied) {
return EmptyListMessages.NO_CASES_FOUND_SUB;
}
if (isTeamLead) {
if (selectedAgent.referenceId === MY_CASE_ITEM.referenceId) {
return EmptyListMessages.SELECT_AGENT;
}
return EmptyListMessages.SELECT_AGENT_SELECTED_AGENT;
}
};
const handleAgentSelectionCTAClick = () => {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_AGENT_LIST_BUTTON_CLICKED);
setShowAgentSelectionBottomSheet && setShowAgentSelectionBottomSheet(true);
};
const getBtnDetails = () => {
if (isVisitPlan && !isFilterApplied && !isLockedVisitPlanStatus) {
return {
btnHandler: () => navigateToScreen('Cases'),
btnText: 'Create a visit plan',
};
}
if (isTeamLead && setShowAgentSelectionBottomSheet) {
return {
btnHandler: handleAgentSelectionCTAClick,
btnText: 'Select Agent',
};
}
};
const message = getEmptyListMessage();
const subMessage = getSubMessage();
const btnDetails = getBtnDetails();
return (
<View>
<View style={[GenericStyles.w100, styles.centerAbsolute]}>
@@ -56,11 +139,11 @@ const EmptyList: React.FC<IEmptyList> = ({
{subMessage}
</Text>
) : null}
{isVisitPlan && !isFilterApplied && !isLockedVisitPlanStatus ? (
{btnDetails ? (
<Button
title={'Create a visit plan'}
title={btnDetails.btnText}
style={[GenericStyles.w100, GenericStyles.mt24]}
onPress={handleCreateVisitPlan}
onPress={btnDetails.btnHandler}
/>
) : null}
</View>

View File

@@ -13,7 +13,6 @@ import { VisitPlanStatus } from '../../reducer/userSlice';
import { FeedbackStatus } from '../caseDetails/interface';
interface IFilters {
filterCount: number;
searchQuery: string;
handleSearchChange: (val: string) => void;
toggleFilterModal: () => void;
@@ -22,20 +21,22 @@ interface IFilters {
const Filters: React.FC<IFilters> = ({
searchQuery,
filterCount,
handleSearchChange,
toggleFilterModal,
isVisitPlan,
}) => {
const { attemptedCount, totalPinnedCount, isVisitPlanStatusLocked } = useAppSelector((state) => ({
totalPinnedCount: state.allCases.pinnedList.length,
attemptedCount: state.allCases.pinnedList.filter(
(item) =>
state.allCases.caseDetails[item.caseReferenceId]?.feedbackStatus ===
FeedbackStatus.ATTEMPTED
)?.length,
isVisitPlanStatusLocked: state.user?.lock?.visitPlanStatus === VisitPlanStatus.LOCKED,
}));
const { attemptedCount, totalPinnedCount, isVisitPlanStatusLocked, filterCount } = useAppSelector(
(state) => ({
totalPinnedCount: state.allCases.pinnedList.length,
attemptedCount: state.allCases.pinnedList.filter(
(item) =>
state.allCases.caseDetails[item.caseReferenceId]?.feedbackStatus ===
FeedbackStatus.ATTEMPTED
)?.length,
isVisitPlanStatusLocked: state.user?.lock?.visitPlanStatus === VisitPlanStatus.LOCKED,
filterCount: isVisitPlan ? state.filters.filterCountVisitPlan : state.filters.filterCount,
})
);
const getBarWidth = () => {
if (!totalPinnedCount) {
return 0;
@@ -45,22 +46,17 @@ const Filters: React.FC<IFilters> = ({
return (
<View>
<View
style={[
GenericStyles.mh20,
GenericStyles.ph16,
GenericStyles.pb16,
GenericStyles.centerAlignedRow,
]}
>
<TextInput
style={styles.textInput}
LeftComponent={<SearchIcon />}
onChangeText={handleSearchChange}
placeholder={`Search in ${isVisitPlan ? 'visit plan' : 'my cases'}`}
defaultValue={searchQuery}
testID="test_search"
/>
<View style={[GenericStyles.mh16, GenericStyles.pb16, GenericStyles.centerAlignedRow]}>
<View style={GenericStyles.fill}>
<TextInput
LeftComponent={<SearchIcon />}
onChangeText={handleSearchChange}
placeholder={`Search in ${isVisitPlan ? 'visit plan' : 'my cases'}`}
defaultValue={searchQuery}
testID="test_search"
showClearIcon
/>
</View>
<IconButton
style={[GenericStyles.ml8, styles.filterIcon]}
testID="filter-btn"
@@ -162,7 +158,7 @@ const styles = StyleSheet.create({
},
textInput: {
flexBasis: '75%',
flex: 1,
},
chips: {
paddingHorizontal: 18,

View File

@@ -0,0 +1,88 @@
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 { MY_CASE_ITEM, VisitPlanStatus } from '../../reducer/userSlice';
import { dateFormat, DAY_MONTH_DATE_FORMAT } from '../../../RN-UI-LIB/src/utlis/dates';
import { addClickstreamEvent } from '../../services/clickstreamEventService';
import { CLICKSTREAM_EVENT_NAMES } from '../../common/Constants';
interface HeaderLabelProps {
setShowAgentSelectionBottomSheet: (val: boolean) => void;
filteredListCount: number;
isVisitPlan?: boolean;
}
const HeaderLabel: React.FC<HeaderLabelProps> = (props) => {
const { setShowAgentSelectionBottomSheet, filteredListCount, isVisitPlan } = props;
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 isTeamLead = useAppSelector((state) => state.user.isTeamLead);
const handleAgentSelectionCTAClick = () => {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_AGENT_LIST_DROPDOWN_CLICKED);
setShowAgentSelectionBottomSheet(true);
};
const getHeaderLabel = () => {
if (isVisitPlan) {
if (isLockedVisitPlanStatus) {
return 'Generating visit plan';
}
const currentDate = new Date();
const formattedDate = dateFormat(currentDate, DAY_MONTH_DATE_FORMAT);
return formattedDate;
}
return `${selectedAgent.name}${!loading ? ` (${filteredListCount})` : ''}`;
};
if (isTeamLead && !isVisitPlan) {
return (
<TouchableOpacity
style={GenericStyles.centerAlignedRow}
onPress={handleAgentSelectionCTAClick}
>
<Heading type="h3" style={[styles.headerLabel]}>
{getHeaderLabel()}
</Heading>
<View style={GenericStyles.mt2}>
<ArrowDownOutlineIcon />
</View>
</TouchableOpacity>
);
}
return (
<Heading type="h3" style={[styles.headerLabel]}>
{getHeaderLabel()}
</Heading>
);
};
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();
@@ -170,7 +171,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

@@ -45,6 +45,10 @@ export const EmptyListMessages = {
NO_CASES_FOUND_SUB: 'Try removing or adding different filters, or search something else',
GENERATING_VISIT_PLAN: 'Generating a visit plan for',
GENERATING_VISIT_PLAN_SUB: '*Any pending cases from previous visit plan will be added',
NO_ACTIVE_ALLOCATIONS: 'You dont have any active allocations',
NO_ACTIVE_ALLOCATIONS_SELECTED_AGENT: 'Selected agent does not have any active allocations',
SELECT_AGENT: 'Select an agent to view cases',
SELECT_AGENT_SELECTED_AGENT: 'Select another agent to view cases',
};
export const ToastMessages = {

View File

@@ -29,9 +29,10 @@ const AllCasesMain = () => {
);
const userState = useAppSelector((state: RootState) => state.user);
const dispatch = useAppDispatch();
const isTeamLead = useAppSelector((state) => state.user.isTeamLead);
const HOME_SCREENS: ITabScreen[] = useMemo(
() => [
const HOME_SCREENS: ITabScreen[] = useMemo(() => {
const bottomSheetScreens = [
{
name: BOTTOM_TAB_ROUTES.Cases,
component: () => (
@@ -39,19 +40,21 @@ const AllCasesMain = () => {
),
icon: CasesIcon,
},
{
];
if (!isTeamLead) {
bottomSheetScreens.push({
name: BOTTOM_TAB_ROUTES.VisitPlan,
component: () => <CasesList casesList={pinnedList} isVisitPlan />,
icon: VisitPlanIcon,
},
{
name: BOTTOM_TAB_ROUTES.Profile,
component: () => <Profile />,
icon: ProfileIcon,
},
],
[pendingList, pinnedList]
);
});
}
bottomSheetScreens.push({
name: BOTTOM_TAB_ROUTES.Profile,
component: () => <Profile />,
icon: ProfileIcon,
});
return bottomSheetScreens;
}, [pendingList, pinnedList, isTeamLead]);
const onTabPressHandler = (e: any) => {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_TAB_SWITCH, {
@@ -76,7 +79,7 @@ const AllCasesMain = () => {
<FullScreenLoader loading={loading} />
<BottomNavigator
screens={HOME_SCREENS}
initialRoute={HOME_SCREENS[1].name}
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 {
referenceId: 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({
referenceId: agent.referenceId,
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

@@ -36,6 +36,7 @@ const AuthRouter = () => {
agentId: user?.user?.referenceId,
deviceType: isTablet() ? DEVICE_TYPE_ENUM.TAB : DEVICE_TYPE_ENUM.MOBILE,
isImpersonated: user?.isImpersonated ?? false,
selectedAgentId: user?.selectedAgent?.referenceId,
});
// Sets the dispatch for apiHelper

View File

@@ -32,6 +32,7 @@ import Notifications from '../notifications';
import RegisterPayments from '../registerPayements/RegisterPayments';
import TodoList from '../todoList/TodoList';
import UngroupedAddressContainer from '../addressGeolocation/UngroupedAddressContainer';
import { getAgentDetail } from '../../action/authActions';
const Stack = createNativeStackNavigator();
@@ -50,10 +51,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) => {
@@ -82,6 +87,7 @@ const ProtectedRouter = () => {
useEffect(() => {
if (isOnline) {
dispatch(getNotifications());
dispatch(getAgentDetail());
}
}, [isOnline]);

View File

@@ -28,7 +28,7 @@ const interactionsHandler = () => {
docsToBeUploaded: state.feedbackImages.docsToBeUploaded,
}));
const { templateId } = useAppSelector(
(state) => state.case.templateData[CaseAllocationType.ADDRESS_VERIFICATION_CASE]
(state) => state.case.templateData[CaseAllocationType.ADDRESS_VERIFICATION_CASE] || {}
);
useEffect(() => {

View File

@@ -61,7 +61,6 @@ const OtpInput = () => {
otpToken,
};
dispatch(verifyOTP(payload));
reset();
};
const handleBackClick = () => {

View File

@@ -30,6 +30,7 @@ import foregroundServiceSlice from '../reducer/foregroundServiceSlice';
import feedbackImagesSlice from '../reducer/feedbackImagesSlice';
import configSlice from '../reducer/configSlice';
import profileSlice from '../reducer/profileSlice';
import reporteesSlice from '../reducer/reporteesSlice';
const rootReducer = combineReducers({
case: caseReducer,
@@ -50,6 +51,7 @@ const rootReducer = combineReducers({
feedbackImages: feedbackImagesSlice,
config: configSlice,
profile: profileSlice,
reportees: reporteesSlice,
});
const persistConfig = {
@@ -72,7 +74,7 @@ const persistConfig = {
'profile',
'foregroundService',
],
blackList: ['case', 'filters'],
blackList: ['case', 'filters', 'reportees'],
};
const persistedReducer = persistReducer(persistConfig, rootReducer);