TP-31976 | Merge branch 'master' of github.cmd.navi-tech.in:medici/Address-Verification-App into bug/TP-31976

This commit is contained in:
Aman Chaturvedi
2023-06-07 10:11:18 +05:30
24 changed files with 633 additions and 104 deletions

View File

@@ -81,7 +81,11 @@ export const syncCaseDetail =
const offlineImageIdList = getOfflineImageId(payload);
const url = getApiUrl(ApiKeys.FEEDBACK);
axiosInstance
.post(url, payload)
.post(url, payload, {
params: {
apiVersion: 2,
},
})
.then((res) => {
const caseType = payload.caseType;
dispatch(

View File

@@ -1,5 +1,7 @@
import { AxiosResponse } from 'axios';
import axiosInstance, { ApiKeys, getApiUrl } from '../components/utlis/apiHelper';
import { logError } from '../components/utlis/errorUtils';
import { VisitPlanStatus } from '../reducer/userSlice';
import { FilterResponse } from '../screens/allCases/interface';
import { CaseDetail } from '../screens/caseDetails/interface';
@@ -30,11 +32,18 @@ export interface ISyncCaseIdPayload {
cases: ICases[];
}
interface ICasesSyncStatus {
syncStatus: SyncStatus;
visitPlanStatus: VisitPlanStatus;
}
export const getCasesSyncStatus = async (userReferenceId: string) => {
try {
const url = getApiUrl(ApiKeys.CASES_SYNC_STATUS, {}, { userReferenceId });
const response = await axiosInstance.get(url, { headers: { donotHandleError: true } });
return response?.data?.syncStatus;
const response: AxiosResponse<ICasesSyncStatus> = await axiosInstance.get(url, {
headers: { donotHandleError: true },
});
return response?.data;
} catch (err) {
logError(err as Error, 'Error getting sync status');
}

View File

@@ -0,0 +1,114 @@
import * as React from 'react';
import Svg, { Path, Mask, G, SvgProps } from 'react-native-svg';
const GeneratingVisitPlan = (props: SvgProps) => {
return (
<Svg
width={160}
height={160}
viewBox="0 0 160 160"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<Path
d="M123.716 25.406H31.192c-.695 0-1.258.564-1.258 1.259v125.273c0 .695.563 1.259 1.258 1.259h92.524c.695 0 1.258-.564 1.258-1.259V26.665c0-.695-.563-1.259-1.258-1.259z"
fill="#0085FF"
stroke="#12183E"
strokeWidth={0.245714}
strokeLinejoin="round"
/>
<Path
d="M120.286 31.584v111.785c0 .509-.446.922-.997.922H36.702c-.551 0-.997-.413-.997-.922V31.584c0-.51.446-.922.997-.922h82.587c.551 0 .997.413.997.922z"
fill="#fff"
stroke="#12183E"
strokeWidth={0.5}
strokeLinejoin="round"
/>
<Path
d="M47.034 30.688s-1.7 2.256-1.486 5.26a1.38 1.38 0 001.38 1.284l59.786-.206c1.002-1.191.606-3.826.333-6.342H47.034v.004z"
fill="#545454"
/>
<Path
d="M79.133 22.562a1.678 1.678 0 11-3.333-.29 1.674 1.674 0 001.654 1.388c.83 0 1.515-.597 1.654-1.389.017.093.025.19.025.29z"
fill="#12183E"
/>
<Path
d="M101.649 26.665H89.52c0-6.662-5.4-12.061-12.061-12.061-6.663 0-12.062 5.399-12.062 12.061H53.268a5.63 5.63 0 00-5.63 5.63v1.882c0 .37.298.67.669.67h58.312c.37 0 .669-.3.669-.67v-1.881a5.63 5.63 0 00-5.631-5.631h-.008zm-25.844-4.394a1.679 1.679 0 113.306.58 1.679 1.679 0 01-3.306-.58z"
fill="#F7F7F7"
stroke="#12183E"
strokeWidth={0.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
<Path
d="M101.649 26.665H89.52c0-6.662-5.4-12.061-12.061-12.061-6.663 0-12.062 5.399-12.062 12.061H53.268a5.63 5.63 0 00-5.63 5.63v1.882c0 .37.298.67.669.67h58.312c.37 0 .669-.3.669-.67v-1.881a5.63 5.63 0 00-5.631-5.631h-.008zm-25.844-4.394a1.679 1.679 0 113.306.58 1.679 1.679 0 01-3.306-.58v0z"
stroke="#12183E"
strokeWidth={0.5}
strokeLinejoin="round"
/>
<Path
d="M53.615 55.512h-9.06a1.17 1.17 0 00-1.17 1.17v8.206c0 .646.523 1.17 1.17 1.17h9.06a1.17 1.17 0 001.17-1.17v-8.206a1.17 1.17 0 00-1.17-1.17zM53.615 95.09h-9.061a1.17 1.17 0 00-1.17 1.17v8.206c0 .646.524 1.17 1.17 1.17h9.06a1.17 1.17 0 001.17-1.17V96.26a1.17 1.17 0 00-1.17-1.17zM53.615 75.303h-9.061a1.17 1.17 0 00-1.17 1.17v8.206c0 .646.524 1.17 1.17 1.17h9.06a1.17 1.17 0 001.17-1.17v-8.206a1.17 1.17 0 00-1.17-1.17zM53.615 114.881h-9.061a1.17 1.17 0 00-1.17 1.17v8.206c0 .646.524 1.17 1.17 1.17h9.06a1.17 1.17 0 001.17-1.17v-8.206a1.17 1.17 0 00-1.17-1.17z"
fill="#fff"
stroke="#12183E"
strokeWidth={0.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
<Path
d="M61.652 117.285h25.744v2.341H61.652v-2.341zM61.653 123.135h45.637v2.34H61.653v-2.34zM61.652 57.488h25.744v2.34H61.652v-2.34zM61.653 63.34h45.637v2.34H61.653v-2.34zM61.652 77.031h25.744v2.34H61.652v-2.34zM61.653 82.88h45.637v2.341H61.653v-2.34zM61.652 96.572h25.744v2.34H61.652v-2.34zM61.653 102.424h45.637v2.34H61.653v-2.34z"
fill="#E1E1E1"
/>
<Path
d="M52.717 58.613l-4.574 4.574-2.287-2.287M52.717 78.367l-4.574 4.574-2.287-2.287M52.717 98.12l-4.574 4.573-2.287-2.287M52.717 117.869l-4.574 4.574-2.287-2.287"
stroke="#C5C5C5"
strokeWidth={1.17657}
strokeLinecap="round"
strokeLinejoin="round"
/>
<Path
d="M38.458 27.68c-10.449-1.942-11.67-3.306-12.956-14.213-1.83 10.828-3.14 12.087-13.656 13.364 10.449 1.941 11.67 3.305 12.956 14.212 1.83-10.828 3.141-12.087 13.656-13.364zM13.694 15.376c-4.366-.814-4.876-1.38-5.414-5.939-.764 4.523-1.313 5.05-5.706 5.584 4.366.813 4.876 1.38 5.414 5.938.765-4.523 1.313-5.05 5.706-5.583z"
fill="#fff"
stroke="#12183E"
strokeWidth={0.35}
strokeLinecap="round"
strokeLinejoin="round"
/>
<Path
d="M135.589 103.557c-12.835-6.789-28.743-1.892-35.532 10.942-6.789 12.834-1.893 28.742 10.942 35.531 12.834 6.789 28.742 1.893 35.531-10.941 6.789-12.835 1.893-28.742-10.941-35.532z"
fill="#fff"
stroke="#12183E"
strokeWidth={0.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
<Mask
id="a"
style={{
maskType: 'alpha',
}}
maskUnits="userSpaceOnUse"
x={106}
y={110}
width={33}
height={33}
>
<Path fill="#D9D9D9" d="M107 110.5H139V142.5H107z" />
</Mask>
<G mask="url(#a)">
<Path
d="M125.835 114.132c2.822.658 5.139 2.119 6.951 4.383 1.812 2.265 2.718 4.926 2.718 7.985 0 1.938-.437 3.766-1.311 5.485-.873 1.718-2.112 3.23-3.715 4.536h3.171a.813.813 0 01.837.838.816.816 0 01-.237.6.816.816 0 01-.6.237h-5.111c-.301 0-.55-.098-.747-.295a1.012 1.012 0 01-.295-.747v-5.111c0-.241.079-.441.237-.6a.816.816 0 01.6-.237.813.813 0 01.838.837v3.352c1.484-1.127 2.631-2.46 3.442-3.999a10.359 10.359 0 001.216-4.896c0-2.636-.775-4.931-2.324-6.885-1.549-1.954-3.558-3.223-6.027-3.807a.844.844 0 01-.464-.312.83.83 0 01-.185-.518c0-.294.101-.524.302-.69a.781.781 0 01.704-.156zm-5.67 24.735c-2.822-.657-5.139-2.116-6.951-4.376-1.812-2.26-2.718-4.924-2.718-7.991 0-1.938.437-3.764 1.31-5.478.874-1.715 2.113-3.225 3.716-4.531h-3.171a.816.816 0 01-.6-.237.816.816 0 01-.237-.6.813.813 0 01.837-.838h5.11a1.01 1.01 0 011.043 1.043v5.11a.813.813 0 01-.837.838.81.81 0 01-.838-.838v-3.351c-1.487 1.104-2.635 2.429-3.444 3.976a10.437 10.437 0 00-1.214 4.906c0 2.627.774 4.92 2.322 6.879 1.547 1.958 3.557 3.229 6.029 3.813a.848.848 0 01.464.312.83.83 0 01.185.518c0 .294-.101.524-.302.689a.775.775 0 01-.704.156z"
fill="#0276FE"
/>
</G>
<Path
d="M132.335 57.488h27.664M4.316 80.06h15.513M0 153.254h158.025"
stroke="#12183E"
strokeWidth={0.5}
strokeLinejoin="round"
/>
</Svg>
);
};
export default GeneratingVisitPlan;

View File

@@ -0,0 +1,32 @@
import * as React from 'react';
import Svg, { Rect, Path, SvgProps } from 'react-native-svg';
function NotificationVisitPlan(props: SvgProps) {
return (
<Svg
width={36}
height={36}
viewBox="0 0 36 36"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<Rect width={36} height={36} rx={18} fill="#E6F1FF" />
<Path
d="M16.22 10.59H10.8a.2.2 0 00-.2.2v5.42c0 .11.09.2.2.2h5.42a.2.2 0 00.2-.2v-5.42a.2.2 0 00-.2-.2zM16.22 19.58H10.8a.2.2 0 00-.2.2v5.42c0 .11.09.2.2.2h5.42a.2.2 0 00.2-.2v-5.42a.2.2 0 00-.2-.2z"
fill="#0276FE"
stroke="#0276FE"
strokeWidth={1.2}
strokeMiterlimit={10}
/>
<Path
d="M19.59 22.66l1.739 1.738a.2.2 0 00.282 0l3.95-3.949M19.59 13.66l1.739 1.74a.2.2 0 00.282 0l3.95-3.94"
stroke="#0276FE"
strokeWidth={1.2}
strokeMiterlimit={10}
/>
</Svg>
);
}
export default NotificationVisitPlan;

View File

@@ -494,8 +494,12 @@ export const PrefixJpegBase64Image = getPrefixBase64Image(MimeType['image/jpeg']
export const HEADER_HEIGHT_MAX = 132;
export const HEADER_HEIGHT_MIN = 80;
export const VISIT_PLAN_HEADER_HEIGHT_MAX = 183;
export const VISIT_PLAN_HEADER_HEIGHT_MIN = 130;
export const HEADER_HEIGHT_MAX_WITH_QUICK_FILTERS = HEADER_HEIGHT_MAX + 50;
export const HEADER_HEIGHT_MIN_WITH_QUICK_FILTERS = HEADER_HEIGHT_MIN + 50;
export const VISIT_PLAN_HEADER_HEIGHT_MAX_WITH_QUICK_FILTERS = VISIT_PLAN_HEADER_HEIGHT_MAX + 50;
export const VISIT_PLAN_HEADER_HEIGHT_MIN_WITH_QUICK_FILTERS = VISIT_PLAN_HEADER_HEIGHT_MIN + 50;
export const HEADER_SCROLL_DISTANCE = (HEADER_HEIGHT_MAX - HEADER_HEIGHT_MIN) * 2;
export const HEADER_SCROLL_DISTANCE_WITH_QUICK_FILTERS =
(HEADER_HEIGHT_MAX_WITH_QUICK_FILTERS - HEADER_HEIGHT_MIN_WITH_QUICK_FILTERS) * 2;

View File

@@ -24,6 +24,7 @@ import {
import { getSyncCaseIds } from '../components/utlis/firebaseFallbackUtils';
import { syncCasesByFallback } from '../reducer/allCasesSlice';
import { MILLISECONDS_IN_A_MINUTE } from '../../RN-UI-LIB/src/utlis/common';
import { VisitPlanStatus, setLockData } from '../reducer/userSlice';
export enum FOREGROUND_TASKS {
GEOLOCATION = 'GEOLOCATION',
@@ -81,8 +82,10 @@ const TrackingComponent: React.FC<ITrackingComponent> = ({ children }) => {
const handleGetCaseSyncStatus = async () => {
try {
const syncStatus = await getCasesSyncStatus(referenceId);
LAST_SYNC_STATUS = syncStatus;
const { syncStatus, visitPlanStatus } = (await getCasesSyncStatus(referenceId)) ?? {};
if (syncStatus) {
LAST_SYNC_STATUS = syncStatus;
}
if (syncStatus === SyncStatus.SEND_CASES) {
const cases = getSyncCaseIds([...pendingList, ...pinnedList]);
const payload: ISyncCaseIdPayload = {
@@ -96,6 +99,13 @@ const TrackingComponent: React.FC<ITrackingComponent> = ({ children }) => {
dispatch(syncCasesByFallback(updatedDetails));
}
}
if (visitPlanStatus) {
dispatch(
setLockData({
visitPlanStatus,
})
);
}
} catch (e) {
logError(e as Error, 'Error during fetching case sync status');
}
@@ -133,7 +143,8 @@ const TrackingComponent: React.FC<ITrackingComponent> = ({ children }) => {
const handleAppStateChange = async (nextAppState: AppStateStatus) => {
// App comes to foreground from background
if (appState.current.match(/inactive|background/) && nextAppState === 'active') {
if (nextAppState === 'active') {
handleGetCaseSyncStatus();
UnstoppableService.start(tasks);
if (bgTrackingTimeoutId.current) {
clearTimeout(bgTrackingTimeoutId.current);
@@ -169,7 +180,6 @@ const TrackingComponent: React.FC<ITrackingComponent> = ({ children }) => {
useEffect(() => {
if (isOnline) {
UnstoppableService.start(tasks);
AppState.addEventListener('change', handleAppStateChange);
} else {
if (UnstoppableService.isRunning()) {

View File

@@ -1,4 +1,4 @@
import { useEffect } from 'react';
import { useEffect, useRef } from 'react';
import firestore, { FirebaseFirestoreTypes } from '@react-native-firebase/firestore';
import { RootState } from '../store/store';
import { useAppDispatch, useAppSelector } from '.';
@@ -8,15 +8,13 @@ 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 { GLOBAL, setGlobalUserData } from '../constants/Global';
import { setAuthData } from '../reducer/userSlice';
import { ILockData, setLockData, VisitPlanStatus } from '../reducer/userSlice';
import { setFilters } from '../reducer/filtersSlice';
import { FormTemplateV1 } from '../types/template.types';
import { ToastMessages } from '../screens/allCases/constants';
import { setForceUninstallData } from '../reducer/metadataSlice';
import { logError } from '../components/utlis/errorUtils';
import { GenericFunctionArgs } from '../common/GenericTypes';
import axiosInstance, { ApiKeys, getApiUrl } from '../components/utlis/apiHelper';
export interface CaseUpdates {
updateType: string;
@@ -34,11 +32,18 @@ const isUserSignedIn = () => {
};
const useFirestoreUpdates = () => {
const reduxStoreData = useAppSelector((state: RootState) => state);
const {
user: { user, isLoggedIn, sessionDetails },
user: { user, isLoggedIn, sessionDetails, lock },
allCases: { caseDetails, casesList, loading },
} = reduxStoreData;
} = useAppSelector((state: RootState) => ({
user: state.user,
allCases: state.allCases,
}));
const lockRef = useRef<ILockData | null>(null);
useEffect(() => {
lockRef.current = lock;
}, [lock]);
let casesUnsubscribe: GenericFunctionArgs;
let avTemplateUnSubscriber: GenericFunctionArgs;
@@ -46,6 +51,7 @@ const useFirestoreUpdates = () => {
let configUnsubscribe: GenericFunctionArgs;
let filterUnsubscribe: GenericFunctionArgs;
let forceUninstallUnsubscribe: GenericFunctionArgs;
let lockUnsubscribe: GenericFunctionArgs;
const dispatch = useAppDispatch();
@@ -94,7 +100,13 @@ const useFirestoreUpdates = () => {
}
});
const isInitialLoad = casesList.length === 0;
dispatch(updateCaseDetailsFirestore({ caseUpdates, isInitialLoad }));
dispatch(
updateCaseDetailsFirestore({
caseUpdates,
isInitialLoad,
isVisitPlanLocked: lockRef?.current?.visitPlanStatus === VisitPlanStatus.LOCKED,
})
);
!isInitialLoad && showCaseUpdationToast(newlyAddedCases, deletedCases);
};
@@ -133,6 +145,12 @@ const useFirestoreUpdates = () => {
const filterResponse = snapshot.data();
filterResponse?.filterComponentList && dispatch(setFilters(filterResponse.filterComponentList));
};
const handleLockUpdate = (
snapshot: FirebaseFirestoreTypes.DocumentSnapshot<FirebaseFirestoreTypes.DocumentData>
) => {
const lockData = snapshot.data();
lockData && dispatch(setLockData(lockData));
};
const handleError = (err: any, collectionPath?: string) => {
const errMsg = `Error while fetching fireStore snapshot: referenceId: ${user?.referenceId} collectionPath: ${collectionPath}`;
@@ -203,6 +221,11 @@ const useFirestoreUpdates = () => {
return subscribeToDoc(handleConfigUpdate, collectionPath);
};
const subscribeToLocks = () => {
const lockPath = `locks/${user?.referenceId}`;
return subscribeToDoc(handleLockUpdate, lockPath);
};
const subscribeToFirestore = () => {
addFirestoreListeners();
configUnsubscribe = subscribeToUserConfig();
@@ -213,6 +236,7 @@ const useFirestoreUpdates = () => {
filterUnsubscribe = subscribeToFilters();
avTemplateUnSubscriber = subscribeToAvTemplate();
collectionTemplateUnsubscribe = subscribeToCollectionTemplate();
lockUnsubscribe = subscribeToLocks();
}
useEffect(() => {
@@ -235,6 +259,7 @@ const useFirestoreUpdates = () => {
configUnsubscribe && configUnsubscribe();
avTemplateUnSubscriber && avTemplateUnSubscriber();
collectionTemplateUnsubscribe && collectionTemplateUnsubscribe();
lockUnsubscribe && lockUnsubscribe();
};
}, [isLoggedIn, user?.referenceId]);

View File

@@ -235,9 +235,10 @@ const allCasesSlice = createSlice({
state.loading = action.payload;
},
updateCaseDetailsFirestore: (state, action) => {
const { caseUpdates, isInitialLoad } = action.payload as {
const { caseUpdates, isInitialLoad, isVisitPlanLocked } = action.payload as {
caseUpdates: CaseUpdates[];
isInitialLoad: boolean;
isVisitPlanLocked: boolean;
};
if (state.loading) {
state.loading = false;
@@ -354,12 +355,14 @@ const allCasesSlice = createSlice({
getLoanAccountNumber(state.caseDetails[item?.caseReferenceId])
),
});
toast({
type: 'info',
text1: `${newVisitCaseLoanIds.length} case${
newVisitCaseLoanIds.length > 1 ? 's' : ''
} added to the visit plan`,
});
if (!isVisitPlanLocked) {
toast({
type: 'info',
text1: `${newVisitCaseLoanIds.length} case${
newVisitCaseLoanIds.length > 1 ? 's' : ''
} added to the visit plan`,
});
}
return;
}
if (removedVisitedCasesLoanIds.length > 0) {
@@ -369,12 +372,14 @@ const allCasesSlice = createSlice({
getLoanAccountNumber(state.caseDetails[item?.caseReferenceId])
),
});
toast({
type: 'info',
text1: `${removedVisitedCasesLoanIds.length} case${
removedVisitedCasesLoanIds.length > 1 ? 's' : ''
} removed from the visit plan`,
});
if (!isVisitPlanLocked) {
toast({
type: 'info',
text1: `${removedVisitedCasesLoanIds.length} case${
removedVisitedCasesLoanIds.length > 1 ? 's' : ''
} removed from the visit plan`,
});
}
}
},
updateCaseDetail: (state, action) => {

View File

@@ -33,11 +33,20 @@ interface IClickstreamEvents {
attributes?: Record<string, any>;
}
export enum VisitPlanStatus {
LOCKED = 'LOCKED',
UNLOCKED = 'UNLOCKED',
}
export interface ILockData {
visitPlanStatus: VisitPlanStatus;
}
export interface IUserSlice extends IUser {
isLoggedIn: boolean;
deviceId: string;
clickstreamEvents: IClickstreamEvents[];
isImpersonated: boolean;
lock: ILockData;
}
const initialState: IUserSlice = {
@@ -47,6 +56,9 @@ const initialState: IUserSlice = {
user: null,
clickstreamEvents: [],
isImpersonated: false,
lock: {
visitPlanStatus: VisitPlanStatus.UNLOCKED,
},
};
export const userSlice = createSlice({
@@ -69,9 +81,12 @@ export const userSlice = createSlice({
state.deviceId = action.payload;
setGlobalUserData({ token: '', deviceId: action.payload });
},
setLockData: (state, action) => {
state.lock = action.payload;
},
},
});
export const { setAuthData, setDeviceId } = userSlice.actions;
export const { setAuthData, setDeviceId, setLockData } = userSlice.actions;
export default userSlice.reducer;

View File

@@ -1,10 +1,12 @@
import React, { useMemo } from 'react';
import { View, ViewProps } from 'react-native';
import { Text, View, ViewProps, StyleSheet } from 'react-native';
import { GenericStyles } from '../../../RN-UI-LIB/src/styles';
import { CaseTypes, ICaseItemCaseDetailObj } from './interface';
import ListItem from './ListItem';
import Button from '../../../RN-UI-LIB/src/components/Button';
import { navigateToScreen } from '../../components/utlis/navigationUtlis';
import { useAppSelector } from '../../hooks';
import { FeedbackStatus } from '../caseDetails/interface';
interface ICaseItemProps extends ViewProps {
caseDetailObj: ICaseItemCaseDetailObj;
@@ -22,7 +24,15 @@ const CaseItem: React.FC<ICaseItemProps> = ({
shouldBatchAvatar = false,
...restProps
}) => {
const { ADD_VISIT_PLAN } = CaseTypes;
const { ADD_VISIT_PLAN, ATTEMPTED_CASES } = CaseTypes;
const { attemptedCount, totalPinnedCount } = useAppSelector((state) => ({
totalPinnedCount: state.allCases.pinnedList.length,
attemptedCount: state.allCases.pinnedList.filter(
(item) =>
state.allCases.caseDetails[item.caseReferenceId]?.feedbackStatus ===
FeedbackStatus.ATTEMPTED
).length,
}));
const handleAddVisitPlan = () => {
navigateToScreen('Cases');
@@ -53,6 +63,18 @@ const CaseItem: React.FC<ICaseItemProps> = ({
/>
);
}
case ATTEMPTED_CASES: {
return (
<Text
style={[
styles.attemptedText,
attemptedCount === totalPinnedCount ? GenericStyles.pt16 : {},
]}
>
Attempted Cases
</Text>
);
}
default:
return (
<View {...restProps}>
@@ -67,4 +89,11 @@ const CaseItem: React.FC<ICaseItemProps> = ({
}
};
const styles = StyleSheet.create({
attemptedText: {
paddingTop: 18,
paddingBottom: 6,
paddingHorizontal: 2,
},
});
export default CaseItem;

View File

@@ -19,18 +19,25 @@ import { ToastMessages } from './constants';
import useIsOnline from '../../hooks/useIsOnline';
import { getLoanAccountNumber } from '../../components/utlis/commonFunctions';
import useCurrentRoute from '../../hooks/useCurrentRoute';
import { VisitPlanStatus } from '../../reducer/userSlice';
export const CasesActionButtons: React.FC = () => {
const dispatch = useAppDispatch();
const {
newlyPinnedCases,
selectedTodoListCount,
selectedTodoListMap,
casesList,
caseDetails,
visitPlansUpdating,
pinnedList,
} = useAppSelector((state: RootState) => state.allCases);
allCases: {
newlyPinnedCases,
selectedTodoListCount,
selectedTodoListMap,
casesList,
caseDetails,
visitPlansUpdating,
pinnedList,
},
isLockedVisitPlanStatus,
} = useAppSelector((state: RootState) => ({
allCases: state.allCases,
isLockedVisitPlanStatus: state.user.lock.visitPlanStatus === VisitPlanStatus.LOCKED,
}));
const isOnline = useIsOnline();
const currentRoute = useCurrentRoute();
@@ -49,6 +56,12 @@ export const CasesActionButtons: React.FC = () => {
};
const handleRemovePinnedList = () => {
if (isLockedVisitPlanStatus) {
toast({
type: 'info',
text1: ToastMessages.CASES_DELETION_DISABLED,
});
}
if (!isOnline) {
toast({ type: 'error', text1: ToastMessages.VISIT_PLAN_OFFLINE });
return;

View File

@@ -6,6 +6,7 @@ import {
NativeScrollEvent,
NativeSyntheticEvent,
StyleSheet,
Text,
View,
VirtualizedList,
} from 'react-native';
@@ -13,7 +14,7 @@ import { GenericStyles } from '../../../RN-UI-LIB/src/styles';
import { COLORS } from '../../../RN-UI-LIB/src/styles/colors';
import { RootState } from '../../store/store';
import { CaseTypes, ICaseItem, ICaseItemCaseDetailObj } from './interface';
import { EmptyListMessages, ListHeaderItems, LIST_HEADER_ITEMS } from './constants';
import { EmptyListMessages, 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';
@@ -28,13 +29,20 @@ import {
HEADER_HEIGHT_MIN_WITH_QUICK_FILTERS,
HEADER_SCROLL_DISTANCE,
HEADER_SCROLL_DISTANCE_WITH_QUICK_FILTERS,
VISIT_PLAN_HEADER_HEIGHT_MAX,
VISIT_PLAN_HEADER_HEIGHT_MAX_WITH_QUICK_FILTERS,
VISIT_PLAN_HEADER_HEIGHT_MIN,
VISIT_PLAN_HEADER_HEIGHT_MIN_WITH_QUICK_FILTERS,
} from '../../common/Constants';
import { CaseDetail } from '../caseDetails/interface';
import { CaseDetail, FeedbackStatus } from '../caseDetails/interface';
import CaseListHeader from './CaseListHeader';
import { evaluateFilterForCases } from '../../components/screens/allCases/allCasesFilters/FilterUtils';
import { debounce } from '../../components/utlis/commonFunctions';
import { getCurrentScreen } from '../../components/utlis/navigationUtlis';
import { useFocusEffect } from '@react-navigation/native';
import { VisitPlanStatus } from '../../reducer/userSlice';
import { dateFormat, DAY_MONTH_DATE_FORMAT } from '../../../RN-UI-LIB/src/utlis/dates';
import { getAttemptedList, getNonAttemptedList } from './utils';
export const getItem = (item: Array<ICaseItem>, index: number) => item[index];
@@ -52,16 +60,16 @@ const CasesList: React.FC<ICasesList> = ({ casesList = [], isVisitPlan }) => {
selectedTodoListMap,
} = useAppSelector((state: RootState) => state.allCases);
const [searchQuery, setSearchQuery] = useState('');
const { filters, filterCount, selectedFilters, quickFiltersPresent } = useAppSelector(
(state: RootState) => ({
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 [showFilterModal, setShowFilterModal] = React.useState<boolean>(false);
@@ -83,13 +91,21 @@ const CasesList: React.FC<ICasesList> = ({ casesList = [], isVisitPlan }) => {
const headerHeight = scrollAnimation.interpolate({
inputRange: [0, HEADER_SCROLL_DISTANCE],
outputRange: [HEADER_HEIGHT_MAX, HEADER_HEIGHT_MIN],
outputRange: isVisitPlan
? [VISIT_PLAN_HEADER_HEIGHT_MAX, VISIT_PLAN_HEADER_HEIGHT_MIN]
: [HEADER_HEIGHT_MAX, HEADER_HEIGHT_MIN],
extrapolate: 'clamp',
});
//TODO: clean these different heights
const headerHeightQuickFilters = scrollAnimation.interpolate({
inputRange: [0, HEADER_SCROLL_DISTANCE_WITH_QUICK_FILTERS],
outputRange: [HEADER_HEIGHT_MAX_WITH_QUICK_FILTERS, HEADER_HEIGHT_MIN_WITH_QUICK_FILTERS],
outputRange: isVisitPlan
? [
VISIT_PLAN_HEADER_HEIGHT_MAX_WITH_QUICK_FILTERS,
VISIT_PLAN_HEADER_HEIGHT_MIN_WITH_QUICK_FILTERS,
]
: [HEADER_HEIGHT_MAX_WITH_QUICK_FILTERS, HEADER_HEIGHT_MIN_WITH_QUICK_FILTERS],
extrapolate: 'clamp',
});
@@ -124,40 +140,73 @@ const CasesList: React.FC<ICasesList> = ({ casesList = [], isVisitPlan }) => {
: 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={
isFilterApplied
? EmptyListMessages.NO_CASES_FOUND
: isVisitPlan
? EmptyListMessages.NO_VISIT_PLANS
: EmptyListMessages.NO_PENDING_CASES
}
message={getEmptyListMessage()}
isVisitPlan={isVisitPlan}
isFilterApplied={isFilterApplied}
subMessage={isFilterApplied ? EmptyListMessages.NO_CASES_FOUND_SUB : ''}
subMessage={getSubMessage()}
isLockedVisitPlanStatus={isLockedVisitPlanStatus}
/>
);
return { filteredCasesList: filteredListAfterQuery, listEmptyComponent };
}, [casesList, caseDetails, filters, selectedFilters, searchQuery]);
}, [casesList, caseDetails, filters, selectedFilters, searchQuery, isLockedVisitPlanStatus]);
const filteredCasesListWithCTA = useMemo(() => {
if (!isVisitPlan) {
return filteredCasesList;
}
if (isLockedVisitPlanStatus) {
return [];
}
const visitPlanCount = filteredCasesList.length;
let visitPlanList: ICaseItem[] = [];
if (visitPlanCount) {
const attemptedFilteredList = getAttemptedList(filteredCasesList, caseDetails);
const nonAttemptedFilteredList = getNonAttemptedList(filteredCasesList, caseDetails);
visitPlanList = nonAttemptedFilteredList;
if (attemptedFilteredList?.length > 0) {
visitPlanList = [
...visitPlanList,
ListHeaderItems.ATTEMPTED_CASES_TEXT as ICaseItem,
...attemptedFilteredList,
];
}
if (!selectedTodoListCount) {
visitPlanList = [...filteredCasesList, ListHeaderItems.ADD_VISIT_PLAN as ICaseItem];
} else {
visitPlanList = [...filteredCasesList];
visitPlanList.push(ListHeaderItems.ADD_VISIT_PLAN as ICaseItem);
}
}
return visitPlanList;
}, [filteredCasesList, isVisitPlan, selectedTodoListCount]);
}, [filteredCasesList, isVisitPlan, selectedTodoListCount, isLockedVisitPlanStatus]);
const handleSearchChange = useCallback(
debounce((query: string) => {
@@ -199,6 +248,17 @@ const CasesList: React.FC<ICasesList> = ({ casesList = [], isVisitPlan }) => {
}
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})`;
};
return (
<View style={[GenericStyles.fill, styles.container]}>
@@ -208,12 +268,8 @@ const CasesList: React.FC<ICasesList> = ({ casesList = [], isVisitPlan }) => {
handleSearchChange={handleSearchChange}
toggleFilterModal={toggleFilterModal}
headerHeight={quickFiltersPresent ? headerHeightQuickFilters : headerHeight}
headerLabel={
isVisitPlan
? `Today's visit plan (${filteredCasesList.length})`
: `My Cases (${filteredCasesList.length})`
}
showFilters={!!casesList.length}
headerLabel={getHeaderLabel()}
showFilters={isVisitPlan && isLockedVisitPlanStatus ? false : !!casesList.length}
isVisitPlan={isVisitPlan}
/>
{visitPlansUpdating ? (
@@ -230,7 +286,13 @@ const CasesList: React.FC<ICasesList> = ({ casesList = [], isVisitPlan }) => {
onScroll={handleListScroll}
contentContainerStyle={[
filteredCasesListWithCTA.length ? null : GenericStyles.fill,
quickFiltersPresent ? styles.listWithQuickFilters : styles.list,
!isVisitPlan
? quickFiltersPresent
? styles.listWithQuickFilters
: styles.list
: quickFiltersPresent
? styles.visitPlanListWithQuickFilters
: styles.visitPlanList,
]}
initialNumToRender={4}
renderItem={renderListItem}
@@ -283,11 +345,21 @@ const styles = StyleSheet.create({
marginTop: HEADER_HEIGHT_MAX,
paddingBottom: HEADER_HEIGHT_MAX + 10,
},
visitPlanList: {
marginHorizontal: 12,
marginTop: VISIT_PLAN_HEADER_HEIGHT_MAX,
paddingBottom: VISIT_PLAN_HEADER_HEIGHT_MAX + 10,
},
listWithQuickFilters: {
marginHorizontal: 12,
marginTop: HEADER_HEIGHT_MAX_WITH_QUICK_FILTERS,
paddingBottom: HEADER_HEIGHT_MAX_WITH_QUICK_FILTERS + 10,
},
visitPlanListWithQuickFilters: {
marginHorizontal: 12,
marginTop: VISIT_PLAN_HEADER_HEIGHT_MAX_WITH_QUICK_FILTERS,
paddingBottom: VISIT_PLAN_HEADER_HEIGHT_MAX_WITH_QUICK_FILTERS + 10,
},
});
export default CasesList;

View File

@@ -7,23 +7,41 @@ 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 GeneratingVisitPlan from '../../assets/icons/GeneratingVisitPlanIcon';
interface IEmptyList {
message: string;
subMessage?: string;
isVisitPlan?: boolean;
isFilterApplied?: boolean;
isLockedVisitPlanStatus?: boolean;
}
const EmptyList: React.FC<IEmptyList> = ({ message, isVisitPlan, isFilterApplied, subMessage }) => {
const EmptyList: React.FC<IEmptyList> = ({
message,
isVisitPlan,
isFilterApplied,
subMessage,
isLockedVisitPlanStatus,
}) => {
const handleCreateVisitPlan = () => {
navigateToScreen('Cases');
};
const renderIcon = () => {
if (isLockedVisitPlanStatus && isVisitPlan) {
return <GeneratingVisitPlan />;
}
if (isFilterApplied || !isVisitPlan) {
return <NoCasesFoundIcon />;
}
return <EmptyPageCheckIcon />;
};
return (
<View style={styles.centerAbsolute}>
<View>
<View style={[GenericStyles.w100, styles.centerAbsolute]}>
{isFilterApplied || !isVisitPlan ? <NoCasesFoundIcon /> : <EmptyPageCheckIcon />}
{renderIcon()}
<View style={[GenericStyles.mt12, styles.text]}>
<Heading
dark
@@ -34,11 +52,11 @@ const EmptyList: React.FC<IEmptyList> = ({ message, isVisitPlan, isFilterApplied
{message}
</Heading>
{subMessage ? (
<Text light style={GenericStyles.centerAlignedText}>
<Text small light style={[GenericStyles.centerAlignedText, styles.subTextStyle]}>
{subMessage}
</Text>
) : null}
{isVisitPlan && !isFilterApplied ? (
{isVisitPlan && !isFilterApplied && !isLockedVisitPlanStatus ? (
<Button
title={'Create a visit plan'}
style={[GenericStyles.w100, GenericStyles.mt24]}
@@ -56,12 +74,14 @@ const styles = StyleSheet.create({
height: '100%',
justifyContent: 'center',
alignItems: 'center',
paddingHorizontal: 4,
paddingTop: 48,
},
text: {
paddingHorizontal: 30,
width: '100%',
},
subTextStyle: {
fontStyle: 'italic',
},
});
export default EmptyList;

View File

@@ -1,6 +1,6 @@
import { ScrollView, StyleSheet, View } from 'react-native';
import React from 'react';
import { GenericStyles } from '../../../RN-UI-LIB/src/styles';
import { GenericStyles, getShadowStyle } from '../../../RN-UI-LIB/src/styles';
import { COLORS } from '../../../RN-UI-LIB/src/styles/colors';
import TextInput from '../../../RN-UI-LIB/src/components/TextInput';
import SearchIcon from '../../../RN-UI-LIB/src/Icons/SearchIcon';
@@ -8,6 +8,9 @@ import IconButton from '../../../RN-UI-LIB/src/components/IconButton';
import FilterIcon from '../../assets/icons/FilterIcon';
import Text from '../../../RN-UI-LIB/src/components/Text';
import QuickFilters from '../../components/screens/allCases/allCasesFilters/QuickFilters';
import { useAppSelector } from '../../hooks';
import { VisitPlanStatus } from '../../reducer/userSlice';
import { FeedbackStatus } from '../caseDetails/interface';
interface IFilters {
filterCount: number;
@@ -24,6 +27,22 @@ const Filters: React.FC<IFilters> = ({
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 getBarWidth = () => {
if (!totalPinnedCount) {
return 0;
}
return attemptedCount / totalPinnedCount;
};
return (
<View>
<View
@@ -60,6 +79,44 @@ const Filters: React.FC<IFilters> = ({
onPress={toggleFilterModal}
/>
</View>
{isVisitPlan && !isVisitPlanStatusLocked ? (
<View
style={[
GenericStyles.w100,
GenericStyles.pb4,
{ backgroundColor: COLORS.BACKGROUND.GREY_LIGHT },
]}
>
<View
style={[
GenericStyles.row,
GenericStyles.alignCenter,
GenericStyles.spaceBetween,
styles.progressBarSection,
GenericStyles.ph16,
GenericStyles.pv12,
getShadowStyle(8),
]}
>
<Text dark bold style={GenericStyles.fw500}>
{attemptedCount} attempted
</Text>
<View style={styles.progressBarContainer}>
<View
style={[
styles.progressBar,
{
width: `${getBarWidth() * 100}%`,
},
]}
></View>
</View>
<Text light>{totalPinnedCount} cases today</Text>
</View>
</View>
) : null}
<ScrollView horizontal={true} showsHorizontalScrollIndicator={false} style={[styles.chips]}>
<QuickFilters isVisitPlan={isVisitPlan} />
</ScrollView>
@@ -111,6 +168,25 @@ const styles = StyleSheet.create({
paddingHorizontal: 18,
backgroundColor: COLORS.BACKGROUND.GREY_LIGHT,
},
progressBarContainer: {
height: 8,
flex: 0.9,
position: 'relative',
borderRadius: 10,
backgroundColor: COLORS.BORDER.PRIMARY,
overflow: 'hidden',
},
progressBar: {
position: 'absolute',
left: 0,
backgroundColor: COLORS.TEXT.GREEN,
height: '100%',
},
progressBarSection: {
backgroundColor: COLORS.BACKGROUND.PRIMARY,
marginHorizontal: 0,
marginVertical: 0,
},
});
export default Filters;

View File

@@ -4,7 +4,7 @@ import Heading from '../../../RN-UI-LIB/src/components/Heading';
import Text from '../../../RN-UI-LIB/src/components/Text';
import { GenericStyles, getShadowStyle } from '../../../RN-UI-LIB/src/styles';
import { getCurrentScreen, navigateToScreen } from '../../components/utlis/navigationUtlis';
import { useAppDispatch } from '../../hooks';
import { useAppDispatch, useAppSelector } from '../../hooks';
import {
setPinnedRank,
setSelectedTodoListMap,
@@ -30,6 +30,9 @@ import OnboardingIcon from '../../../RN-UI-LIB/src/Icons/OnboardingIcon';
import CollectionsIcon from '../../../RN-UI-LIB/src/Icons/CollectionsIcon';
import RoundCheckIcon from '../../../RN-UI-LIB/src/Icons/RoundCheckIcon';
import { getDocumentList } from '../../components/utlis/commonFunctions';
import { toast } from '../../../RN-UI-LIB/src/components/toast';
import { ToastMessages } from './constants';
import { VisitPlanStatus } from '../../reducer/userSlice';
interface IListItem {
caseListItemDetailObj: ICaseItemCaseDetailObj;
@@ -56,6 +59,9 @@ const ListItem: React.FC<IListItem> = (props) => {
} = caseListItemDetailObj;
const isCollectionCaseType = caseType === CaseAllocationType.COLLECTION_CASE;
const isVisitPlanStatusLocked = useAppSelector(
(state) => state.user.lock.visitPlanStatus === VisitPlanStatus.LOCKED
);
const dispatch = useAppDispatch();
@@ -73,6 +79,13 @@ const ListItem: React.FC<IListItem> = (props) => {
if (isTodoItem || caseStatus === CaseStatuses.CLOSED) {
return;
}
if (isVisitPlanStatusLocked) {
toast({
type: 'info',
text1: ToastMessages.CASES_SELECTION_DISABLED,
});
return;
}
if (pinRank) {
dispatch(
setSelectedTodoListMap({

View File

@@ -23,6 +23,10 @@ export const ListHeaderItems = {
type: CaseTypes.ADD_VISIT_PLAN,
caseReferenceId: '-4',
},
ATTEMPTED_CASES_TEXT: {
type: CaseTypes.ATTEMPTED_CASES,
caseReferenceId: '-5',
},
};
export const LIST_HEADER_ITEMS = [
@@ -39,6 +43,8 @@ export const EmptyListMessages = {
NO_VISIT_PLANS: 'Plan your day by creating a visit plan',
NO_CASES_FOUND: 'No results found',
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',
};
export const ToastMessages = {
@@ -61,4 +67,12 @@ export const ToastMessages = {
LOGIN_SUCCESSFUL: 'Login Successful!',
SSO_SERVER_SIGN_IN_ERROR: 'Error while signing in to cosmos',
NOT_ALLOWED_ERROR: 'Not allowed',
CASES_SELECTION_DISABLED: 'Case addition is disabled during the generation of visit plan',
CASES_DELETION_DISABLED: 'Case deletion is disabled during the generation of visit plan',
};
export enum BOTTOM_TAB_ROUTES {
Cases = 'Cases',
VisitPlan = 'Visit plan',
Profile = 'Profile',
}

View File

@@ -20,6 +20,7 @@ import {
} from '../../reducer/allCasesSlice';
import { addClickstreamEvent } from '../../services/clickstreamEventService';
import { CLICKSTREAM_EVENT_NAMES } from '../../common/Constants';
import { BOTTOM_TAB_ROUTES } from './constants';
const AllCasesMain = () => {
const { pendingList, pinnedList, loading } = useAppSelector((state) => state.allCases);
@@ -29,17 +30,17 @@ const AllCasesMain = () => {
const HOME_SCREENS: ITabScreen[] = useMemo(
() => [
{
name: 'Cases',
name: BOTTOM_TAB_ROUTES.Cases,
component: () => <CasesList casesList={pendingList} />,
icon: CasesIcon,
},
{
name: 'Visit plan',
name: BOTTOM_TAB_ROUTES.VisitPlan,
component: () => <CasesList casesList={pinnedList} isVisitPlan />,
icon: VisitPlanIcon,
},
{
name: 'Profile',
name: BOTTOM_TAB_ROUTES.Profile,
component: () => <Profile />,
icon: ProfileIcon,
},

View File

@@ -22,6 +22,7 @@ export enum CaseTypes {
CASE,
BANNER,
ADD_VISIT_PLAN,
ATTEMPTED_CASES,
}
export enum caseVerdict {

View File

@@ -0,0 +1,29 @@
import { CaseDetail, FeedbackStatus } from '../caseDetails/interface';
import { ICaseItem } from './interface';
export const getAttemptedList = (
filteredCasesList: ICaseItem[],
caseDetails: Record<string, CaseDetail>
) => {
return (
filteredCasesList.filter(
(item) => caseDetails[item.caseReferenceId]?.feedbackStatus === FeedbackStatus.ATTEMPTED
) as ICaseItem[]
).sort((a, b) => {
return (
(caseDetails[b.caseReferenceId]?.attemptedAt ?? 0) -
(caseDetails[a.caseReferenceId]?.attemptedAt ?? 0)
);
});
};
export const getNonAttemptedList = (
filteredCasesList: ICaseItem[],
caseDetails: Record<string, CaseDetail>
) => {
return filteredCasesList.filter((item) =>
caseDetails[item.caseReferenceId]?.feedbackStatus
? caseDetails[item.caseReferenceId].feedbackStatus === FeedbackStatus.NOT_ATTEMPTED
: true
);
};

View File

@@ -151,6 +151,11 @@ export interface IDocument {
type: DOCUMENT_TYPE;
}
export enum FeedbackStatus {
ATTEMPTED = 'ATTEMPTED',
NOT_ATTEMPTED = 'NOT_ATTEMPTED',
}
export interface CaseDetail {
id: string;
allocationReferenceId?: string;
@@ -195,6 +200,8 @@ export interface CaseDetail {
imageReferenceId?: string;
collectionTag?: 'Fresh' | 'Stab';
disbursementAmount?: number;
feedbackStatus?: FeedbackStatus;
attemptedAt?: number;
}
export interface AddressesGeolocationPayload {

View File

@@ -10,12 +10,13 @@ import { useAppDispatch, useAppSelector } from '../../hooks';
import { CaseDetail } from '../caseDetails/interface';
import NotificationTemplate from './NotificationTemplate';
import { CaseAllocationType } from '../allCases/interface';
import { pushToScreen } from '../../components/utlis/navigationUtlis';
import { navigateToScreen, pushToScreen } from '../../components/utlis/navigationUtlis';
import { hasNotCompletedAction, notificationAction } from '../../action/notificationActions';
import useIsOnline from '../../hooks/useIsOnline';
import { addNotificationToQueue } from '../../reducer/notificationsSlice';
import { addClickstreamEvent } from '../../services/clickstreamEventService';
import { CLICKSTREAM_EVENT_NAMES } from '../../common/Constants';
import { BOTTOM_TAB_ROUTES } from '../allCases/constants';
export interface INotification {
id: string;
@@ -36,10 +37,11 @@ export interface INotification {
paymentTime: string;
date: string;
time: string;
caseCount: number;
};
template: {
id: number;
templateName: keyof typeof NotificationTypes;
templateName: NotificationTypes;
};
actions: Record<string, string>[];
createdAt: number;
@@ -102,6 +104,10 @@ const NotificationItem: React.FC<INotificationProps> = ({ data }) => {
if (isActionNotCompleted) {
handleNotificationAction();
}
if (templateName === NotificationTypes.VISIT_PLAN_UPDATE_NOTIFICATION_TEMPLATE) {
navigateToScreen(BOTTOM_TAB_ROUTES.VisitPlan);
return;
}
if (!collectionCaseId || !clickable) {
return;
}
@@ -139,7 +145,9 @@ const NotificationItem: React.FC<INotificationProps> = ({ data }) => {
{notificationIcon}
<View style={[GenericStyles.pl16, GenericStyles.flex80]}>
<Heading type="h5" dark>
{customerName}
{templateName === NotificationTypes.VISIT_PLAN_UPDATE_NOTIFICATION_TEMPLATE
? 'New visit plan created!'
: customerName}
</Heading>
<NotificationTemplate data={data} />
<Text small>{getTimeDifference(scheduledAt)}</Text>

View File

@@ -24,6 +24,8 @@ const NotificationTemplate: React.FC<INotificationTemplateProps> = ({ data }) =>
paymentTime,
paymentMode,
paymentTimestamp,
date,
caseCount,
} = params || {};
switch (templateName) {
@@ -163,6 +165,12 @@ const NotificationTemplate: React.FC<INotificationTemplateProps> = ({ data }) =>
{paymentTime}
</Text>
);
case NotificationTypes.VISIT_PLAN_UPDATE_NOTIFICATION_TEMPLATE:
return (
<Text light>
for <Text dark>{date}</Text> with <Text dark>{caseCount}</Text> cases
</Text>
);
default:
return <Text>New notification</Text>;
}

View File

@@ -4,27 +4,7 @@ import NotificationIcon from '../../../RN-UI-LIB/src/Icons/NotificationIcon';
import PaymentFailedIcon from '../../../RN-UI-LIB/src/Icons/PaymentFailedIcon';
import PaymentSuccessIcon from '../../../RN-UI-LIB/src/Icons/PaymentSuccessIcon';
import PromiseToPayIcon from '../../../RN-UI-LIB/src/Icons/PromiseToPayIcon';
export const NotificationIconsMap = {
NEW_ADDRESS_ADDED_NOTIFICATION_TEMPLATE: <NewAddressIcon />,
ENACH_PAYMENT_MADE_TEMPLATE: <PaymentSuccessIcon />,
ENACH_PAYMENT_FAILED_TEMPLATE: <PaymentFailedIcon />,
PAYMENT_FAILED_TEMPLATE_V2: <PaymentFailedIcon />,
PAYMENT_FAILED_TEMPLATE: <PaymentFailedIcon />,
PAYMENT_MADE_TEMPLATE_V2: <PaymentSuccessIcon />,
PAYMENT_MADE_TEMPLATE: <PaymentSuccessIcon />,
PROMISED_TO_PAY_SCHEDULED_NOTIFICATION_TEMPLATE: <PromiseToPayIcon />,
NEW_TELEPHONE_ADDED_NOTIFICATION_TEMPLATE: <NewTelephoneIcon />,
LONGHORN_ALERTS_CHANGE_IN_DELIQUENCY: <NotificationIcon />,
LONGHORN_ALERTS_MULTIPLE_TRIGGER_ALERTS: <NotificationIcon />,
LONGHORN_ALERTS_CHANGE_IN_SCORE: <NotificationIcon />,
LONGHORN_ALERTS_NEW_PHONE_NUMBER_ADDED: <NotificationIcon />,
LONGHORN_ALERTS_CHANGE_IN_DELIQUENCY_ACCOUNTS: <NotificationIcon />,
LONGHORN_ALERTS_CHANGE_IN_UTILIZATION: <NotificationIcon />,
LONGHORN_ALERTS_NEW_TRADE_ACCOUNT: <NotificationIcon />,
LONGHORN_ALERTS_NEW_ENQUIRY: <NotificationIcon />,
REVISIT_SCHEDULED_NOTIFICATION_TEMPLATE: <NotificationIcon />,
};
import NotificationVisitPlanIcon from '../../assets/icons/NotificationVisitPlanIcon';
export enum NotificationTypes {
PAYMENT_MADE_TEMPLATE = 'PAYMENT_MADE_TEMPLATE',
@@ -45,8 +25,31 @@ export enum NotificationTypes {
REVISIT_SCHEDULED_NOTIFICATION_TEMPLATE = 'REVISIT_SCHEDULED_NOTIFICATION_TEMPLATE',
ENACH_PAYMENT_MADE_TEMPLATE = 'ENACH_PAYMENT_MADE_TEMPLATE',
ENACH_PAYMENT_FAILED_TEMPLATE = 'ENACH_PAYMENT_FAILED_TEMPLATE',
VISIT_PLAN_UPDATE_NOTIFICATION_TEMPLATE = 'VISIT_PLAN_UPDATE_NOTIFICATION_TEMPLATE',
}
export const NotificationIconsMap = {
[NotificationTypes.NEW_ADDRESS_ADDED_NOTIFICATION_TEMPLATE]: <NewAddressIcon />,
[NotificationTypes.ENACH_PAYMENT_MADE_TEMPLATE]: <PaymentSuccessIcon />,
[NotificationTypes.ENACH_PAYMENT_FAILED_TEMPLATE]: <PaymentFailedIcon />,
[NotificationTypes.PAYMENT_FAILED_TEMPLATE_V2]: <PaymentFailedIcon />,
[NotificationTypes.PAYMENT_FAILED_TEMPLATE]: <PaymentFailedIcon />,
[NotificationTypes.PAYMENT_MADE_TEMPLATE_V2]: <PaymentSuccessIcon />,
[NotificationTypes.PAYMENT_MADE_TEMPLATE]: <PaymentSuccessIcon />,
[NotificationTypes.PROMISED_TO_PAY_SCHEDULED_NOTIFICATION_TEMPLATE]: <PromiseToPayIcon />,
[NotificationTypes.NEW_TELEPHONE_ADDED_NOTIFICATION_TEMPLATE]: <NewTelephoneIcon />,
[NotificationTypes.LONGHORN_ALERTS_CHANGE_IN_DELIQUENCY]: <NotificationIcon />,
[NotificationTypes.LONGHORN_ALERTS_MULTIPLE_TRIGGER_ALERTS]: <NotificationIcon />,
[NotificationTypes.LONGHORN_ALERTS_CHANGE_IN_SCORE]: <NotificationIcon />,
[NotificationTypes.LONGHORN_ALERTS_NEW_PHONE_NUMBER_ADDED]: <NotificationIcon />,
[NotificationTypes.LONGHORN_ALERTS_CHANGE_IN_DELIQUENCY_ACCOUNTS]: <NotificationIcon />,
[NotificationTypes.LONGHORN_ALERTS_CHANGE_IN_UTILIZATION]: <NotificationIcon />,
[NotificationTypes.LONGHORN_ALERTS_NEW_TRADE_ACCOUNT]: <NotificationIcon />,
[NotificationTypes.LONGHORN_ALERTS_NEW_ENQUIRY]: <NotificationIcon />,
[NotificationTypes.REVISIT_SCHEDULED_NOTIFICATION_TEMPLATE]: <NotificationIcon />,
[NotificationTypes.VISIT_PLAN_UPDATE_NOTIFICATION_TEMPLATE]: <NotificationVisitPlanIcon />,
};
export enum WidgetStatus {
CLICKED = 'CLICKED',
}

View File

@@ -26,6 +26,7 @@ import useIsOnline from '../../hooks/useIsOnline';
import { getLoanAccountNumber } from '../../components/utlis/commonFunctions';
import CaseItem from '../allCases/CaseItem';
import { CaseDetail } from '../caseDetails/interface';
import { VisitPlanStatus } from '../../reducer/userSlice';
const TodoList = () => {
const {
@@ -35,7 +36,16 @@ const TodoList = () => {
intermediateTodoListMap,
caseDetails,
visitPlansUpdating,
} = useSelector((state: RootState) => state.allCases);
isVisitPlanStatusLocked,
} = useSelector((state: RootState) => ({
pinnedList: state.allCases.pinnedList,
intermediateTodoList: state.allCases.intermediateTodoList,
casesList: state.allCases.casesList,
intermediateTodoListMap: state.allCases.intermediateTodoListMap,
caseDetails: state.allCases.caseDetails,
visitPlansUpdating: state.allCases.visitPlansUpdating,
isVisitPlanStatusLocked: state.user.lock.visitPlanStatus === VisitPlanStatus.LOCKED,
}));
const dispatch = useAppDispatch();
const isOnline = useIsOnline();
@@ -53,6 +63,13 @@ const TodoList = () => {
dispatch(resetTodoList());
return;
}
if (isVisitPlanStatusLocked) {
toast({
type: 'info',
text1: ToastMessages.CASES_SELECTION_DISABLED,
});
return;
}
const updatedPinnedList: ICaseItem[] = [];
const pinnedCasesPayload: IPinnedCasesPayload[] = [];
const newPinedCasesLoanAccountNumbers: string[] = [];