Merge branch 'master' into TP-57833

This commit is contained in:
Mantri Ramkishor
2024-02-29 17:55:48 +05:30
committed by GitHub
14 changed files with 153 additions and 86 deletions

View File

@@ -134,8 +134,8 @@ def reactNativeArchitectures() {
return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
}
def VERSION_CODE = 127
def VERSION_NAME = "2.7.9"
def VERSION_CODE = 128
def VERSION_NAME = "2.7.10"
android {
ndkVersion rootProject.ext.ndkVersion

View File

@@ -1,7 +1,7 @@
{
"name": "AV_APP",
"version": "2.7.9",
"buildNumber": "127",
"version": "2.7.10",
"buildNumber": "128",
"private": true,
"scripts": {
"android:dev": "yarn move:dev && react-native run-android",

View File

@@ -11,6 +11,8 @@ import { getAddressString, getPhoneNumberString, memoize } from '../utlis/common
import { getBase64ImageFromOfflineDb } from '../../services/casePayload.transformer';
import { formatAmount } from '../../../RN-UI-LIB/src/utlis/amount';
import GeolocationAddressAnswer from './components/GeolocationAddressAnswer';
import dayjs from 'dayjs';
import { BUSINESS_DATE_FORMAT, CUSTOM_ISO_DATE_FORMAT } from '@rn-ui-lib/utils/dates';
const RATING_COMPONENT = 'Rating';
const MAX_RATING = 5;
@@ -131,7 +133,9 @@ const AnswerRender: React.FC<IAnswerRender> = (props) => {
case AnswerType.phoneNumber:
return <DarkBoldText text={getPhoneNumberStringFromNumber(answer.answer)} />;
case AnswerType.date:
return <DarkBoldText text={answer.answer} />;
return <DarkBoldText text={dayjs(answer?.answer, CUSTOM_ISO_DATE_FORMAT).format(BUSINESS_DATE_FORMAT)} />;
case AnswerType.time:
return <DarkBoldText text={dayjs(answer?.answer, 'HH:mm').format('hh:mm A')} />;
default:
return <DarkBoldText text={NA_TEXT} />;
}

View File

@@ -12,6 +12,7 @@ import { CLICKSTREAM_EVENT_NAMES } from '../../../common/Constants';
import { AnswerType } from '../interface';
import WebBasedDatePicker from '../../../../RN-UI-LIB/src/components/WebBasedDatePicker';
import {
BUSINESS_DATE_FORMAT,
CUSTOM_ISO_DATE_FORMAT,
DefaultPickerModeVisibleFormatMapping,
IDateTimePickerMode,
@@ -109,7 +110,7 @@ const DateInput: React.FC<IDateInput> = (props) => {
}
return (
<WebBasedDatePicker
displayFormat="DD-MM-YYYY"
displayFormat={BUSINESS_DATE_FORMAT}
value={
question.metadata.defaultValue === DateValue.CURRENT
? Date().toString()

View File

@@ -62,6 +62,7 @@ const GeolocationAddress: React.FC<IGeolocationAddress> = ({
: '';
const isFeedbackPresent = lastFeedbackForGeolocation?.feedbackPresent;
const isDataSutramPrimarySource = primarySource === GeolocationSource.DATA_SUTRAM;
const { addressDate, addressTime } = useMemo(() => {
const timestamp = new Date(Number(capturedTimestamp));
@@ -98,14 +99,12 @@ const GeolocationAddress: React.FC<IGeolocationAddress> = ({
})
);
if (visitedWidgets?.length) {
_map(visitedWidgets, (visited) =>
navigateToScreen(getTemplateRoute(visited, CaseAllocationType.COLLECTION_CASE), {
caseId: caseId,
journey: TaskTitleUIMapping.COLLECTION_FEEDBACK,
handleCloseRouting,
})
);
return;
const lastVisitedWidget = visitedWidgets[visitedWidgets.length - 1];
navigateToScreen(getTemplateRoute(lastVisitedWidget, CaseAllocationType.COLLECTION_CASE), {
caseId: caseId,
journey: TaskTitleUIMapping.COLLECTION_FEEDBACK,
handleCloseRouting,
});
}
}
};
@@ -145,7 +144,7 @@ const GeolocationAddress: React.FC<IGeolocationAddress> = ({
<Text small>{addressTime}</Text>
</View>
) : null}
{(primarySource && primarySource === GeolocationSource.DATA_SUTRAM) ? (
{isDataSutramPrimarySource ? (
<View style={[GenericStyles.row, GenericStyles.alignCenter]}>
<View style={styles.circleSeparator} />
<Text small>Skip Tracing</Text>

View File

@@ -55,7 +55,7 @@ const TimeInput: React.FC<ITimeInput> = (props) => {
const outputTime = convertTo24HourFormat(text);
onChange({
answer: outputTime,
type: AnswerType.date,
type: AnswerType.time,
});
};

View File

@@ -5,7 +5,6 @@ import Geolocation from 'react-native-geolocation-service';
import { SafeAreaView } from 'react-native-safe-area-context';
import Button from '../../../RN-UI-LIB/src/components/Button';
import Heading from '../../../RN-UI-LIB/src/components/Heading';
import Text from '../../../RN-UI-LIB/src/components/Text';
import ArrowSolidIcon from '../../../RN-UI-LIB/src/Icons/ArrowSolidIcon';
import { GenericStyles } from '../../../RN-UI-LIB/src/styles';
import { COLORS } from '../../../RN-UI-LIB/src/styles/colors';
@@ -47,6 +46,7 @@ import NudgeSuspiciousFeedbackBottomSheet from './NudgeSuspiciousFeedbackBottomS
import { API_STATUS_CODE } from '../utlis/apiHelper';
import NavigationHeader, { Icon } from '../../../RN-UI-LIB/src/components/NavigationHeader';
import { CaseDetailStackEnum } from '@screens/caseDetails/CaseDetailStack';
import { useNavigation, useRoute } from '@react-navigation/native';
interface IWidget {
route: {
@@ -59,6 +59,10 @@ interface IWidget {
};
}
enum NavigationActions {
GO_BACK = 'GO_BACK',
}
const Widget: React.FC<IWidget> = (props) => {
const [isJourneyFirstScreen, setIsJourneyFirstScreen] = useState(true);
const [showNudgeBottomSheet, setNudgeBottomSheet] = useState(false);
@@ -67,30 +71,28 @@ const Widget: React.FC<IWidget> = (props) => {
const { params } = props.route;
const { caseId, journey, handleCloseRouting } = params;
const caseKey = useRef<string>('');
const {
caseType,
templateData,
caseData,
dataToBeValidated,
docsToBeUploaded,
intermediateDocsToBeUploaded,
} = useAppSelector((state) => {
const caseType =
state.allCases.caseDetails[caseId]?.caseType || CaseAllocationType.ADDRESS_VERIFICATION_CASE;
return {
caseType,
templateData: state.case.templateData[caseType],
caseData: state.allCases.caseDetails[caseId],
dataToBeValidated: state.case.caseForm?.[caseId]?.[journey],
docsToBeUploaded: state.feedbackImages.docsToBeUploaded,
intermediateDocsToBeUploaded: state.feedbackImages.intermediateDocsToBeUploaded,
};
});
const disableFormInteractionUpdate = useRef(false);
const { caseType, templateData, caseData, dataToBeValidated, intermediateDocsToBeUploaded } =
useAppSelector((state) => {
const caseType =
state.allCases.caseDetails[caseId]?.caseType ||
CaseAllocationType.ADDRESS_VERIFICATION_CASE;
return {
caseType,
templateData: state.case.templateData[caseType],
caseData: state.allCases.caseDetails[caseId],
dataToBeValidated: state.case.caseForm?.[caseId]?.[journey],
docsToBeUploaded: state.feedbackImages.docsToBeUploaded,
intermediateDocsToBeUploaded: state.feedbackImages.intermediateDocsToBeUploaded,
};
});
const name = getWidgetNameFromRoute(props.route.name, caseType);
const { sections, conditionActions: widgetConditionActions, isLeaf } = templateData.widget[name];
const sectionMap = templateData.sections;
const [error, setError] = useState();
const dispatch = useAppDispatch();
const navigation = useNavigation();
const route = useRoute();
useEffect(() => {
let isFirst = false;
for (const journey of Object.values(templateData.journey)) {
@@ -125,6 +127,49 @@ const Widget: React.FC<IWidget> = (props) => {
return () => subscription.unsubscribe();
}, [watch]);
useEffect(() => {
const beforeRemoveListener = navigation.addListener('beforeRemove', (e) => {
// If leaf widget, do nothing
if (isLeaf && disableFormInteractionUpdate.current) {
return;
}
// Delete interaction on going back the stack
if (e.data.action.type === NavigationActions.GO_BACK) {
dispatch(
deleteInteraction({
caseId,
journeyId: journey,
widgetId: name,
})
);
return;
}
const numberOfRoutes = navigation.getState().routes.length;
const lastScreen = navigation.getState().routes[numberOfRoutes - 1];
const isLastScreen = lastScreen.name === route.name;
// Update form interaction when the last screen is about to be unmounted
if (isLastScreen) {
const submitHandler = handleSubmit((answer) => {
dispatch(
updateInteraction({
caseId,
journeyId: journey,
widgetId: name,
answer,
})
);
});
submitHandler();
}
});
return beforeRemoveListener;
}, [navigation, route]);
const onSubmit = (data: any) => {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.AV_FORM_NEXT_BUTTON_CLICKED, {
caseId,
@@ -221,6 +266,7 @@ const Widget: React.FC<IWidget> = (props) => {
};
const handleSubmitJourney = async (data: any, coords: Geolocation.GeoCoordinates) => {
disableFormInteractionUpdate.current = true;
dispatch(
updateInteraction({
caseId,
@@ -371,13 +417,6 @@ const Widget: React.FC<IWidget> = (props) => {
journeyId: journey,
widgetId: name,
});
dispatch(
deleteInteraction({
caseId,
journeyId: journey,
widgetId: name,
})
);
goBack();
};
@@ -398,9 +437,6 @@ const Widget: React.FC<IWidget> = (props) => {
Add feedback for {caseData?.customerInfo?.customerName || caseData?.customerName}
</Heading>
}
subTitle={
isJourneyNameExists ? <Text light>{templateData?.journey?.[journey].name}</Text> : null
}
icon={Icon.close}
onBack={handleCloseIconPress}
/>
@@ -444,17 +480,15 @@ const Widget: React.FC<IWidget> = (props) => {
styles.borderTop,
]}
>
{!isJourneyFirstScreen && (
<Button
variant={'secondary'}
style={[styles.autoFlex, styles.mH16]}
title={'Back'}
testID={'test_back'}
disabled={isLeaf && isSubmitting}
onPress={handleBackButton}
leftIcon={<ArrowSolidIcon size={10} />}
/>
)}
<Button
variant={'secondary'}
style={[styles.autoFlex, styles.mH16]}
title={'Back'}
testID={'test_back'}
disabled={isLeaf && isSubmitting}
onPress={handleBackButton}
leftIcon={<ArrowSolidIcon size={10} />}
/>
<Button
style={[styles.autoFlex, styles.mH16]}
title={isLeaf ? 'Submit' : 'Next'}

View File

@@ -7,6 +7,7 @@ export enum AnswerType {
'address' = 'address',
'phoneNumber' = 'phoneNumber',
'date' = 'date',
'time' = 'time',
}
// @deprecated

View File

@@ -1,7 +1,6 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { CaseAllocationType, ICaseItem } from '../screens/allCases/interface';
import { FormTemplateV1, IVisitedWidgetContext } from '../types/template.types';
import { mockTemplate } from '../template';
interface ICaseReducer {
value: number;
@@ -54,7 +53,7 @@ export const caseSlice = createSlice({
state.caseForm = data;
},
updateInteraction: (state, action) => {
const { caseId, journeyId, widgetId, answer } = action.payload;
const { caseId, journeyId, widgetId, answer} = action.payload;
const data = state.caseForm || {};
if (!data[caseId]) {
data[caseId] = {};
@@ -74,20 +73,24 @@ export const caseSlice = createSlice({
...answer.widgetContext,
};
data[caseId][journeyId].visitedWidgets = visited;
// Update time of interaction
data[caseId][journeyId].updatedAt = Date.now();
state.caseForm = data;
},
deleteInteraction: (state, action) => {
const { caseId, journeyId, widgetId } = action.payload;
const data = state.caseForm;
const data = state.caseForm || {};
if (!data[caseId] || !data[caseId][journeyId]) return;
const visited = data[caseId][journeyId]?.visitedWidgets;
if (visited[visited.length - 1] === widgetId) {
visited.pop();
}
data[caseId][journeyId].visitedWidgets = visited;
data[caseId][journeyId].updatedAt = Date.now();
state.caseForm = data;
},
deleteJourney: (state, action) => {
const { caseId, journeyId, widgetId, answer } = action.payload;
const { caseId, journeyId } = action.payload;
const data = state.caseForm;
delete data[caseId][journeyId];
state.caseForm = data;
@@ -106,12 +109,12 @@ export const caseSlice = createSlice({
export const {
updateInteraction,
deleteInteraction,
updateAvTemplateData,
updateCollectionTemplateData,
deleteJourney,
updateAlternateHeader,
updatePreDefinedCaseFormJourney,
deleteInteraction
} = caseSlice.actions;
export default caseSlice.reducer;

View File

@@ -100,12 +100,11 @@ function AddressItem({
})
);
if (visitedWidgets?.length) {
_map(visitedWidgets, (visited) => {
navigateToScreen(getTemplateRoute(visited, CaseAllocationType.COLLECTION_CASE), {
caseId,
journey: TaskTitleUIMapping.COLLECTION_FEEDBACK,
handleCloseRouting,
});
const lastVisitedWidget = visitedWidgets[visitedWidgets.length - 1];
navigateToScreen(getTemplateRoute(lastVisitedWidget, CaseAllocationType.COLLECTION_CASE), {
caseId,
journey: TaskTitleUIMapping.COLLECTION_FEEDBACK,
handleCloseRouting,
});
}
}

View File

@@ -28,7 +28,7 @@ import {
IOutstandingEmiDetail,
TaskTitleUIMapping,
} from '../allCases/interface';
import { _map } from '../../../RN-UI-LIB/src/utlis/common';
import { MILLISECONDS_IN_A_MINUTE, _map } from '../../../RN-UI-LIB/src/utlis/common';
import FeedbackListContainer from './feedback/FeedbackListContainer';
import { RootState } from '../../store/store';
import { IFeedback, IUnSyncedFeedbackItem } from '../../types/feedback.types';
@@ -43,6 +43,7 @@ import ScreenshotBlocker from '../../components/utlis/ScreenshotBlocker';
import { useIsFocused } from '@react-navigation/native';
import { setSelectedCaseId } from '../../reducer/allCasesSlice';
import { CaseDetailStackEnum } from './CaseDetailStack';
import ArrowSolidIcon from '@rn-ui-lib/icons/ArrowSolidIcon';
interface ICaseDetails {
route: {
@@ -54,6 +55,8 @@ interface ICaseDetails {
};
}
const FEEDBACK_FORM_RESPONSE_EXPIRY_TIME = 60 * MILLISECONDS_IN_A_MINUTE;
const getOutstandingAmountBreakUp = (
outstandingEmiDetails: IOutstandingEmiDetail[] = [],
totalOverdueAmount: number = 0
@@ -108,18 +111,11 @@ const CollectionCaseDetails: React.FC<ICaseDetails> = (props) => {
const isOnline = useIsOnline();
const caseDetail = useAppSelector((state) => state.allCases.caseDetails[caseId]!!);
const data = useAppSelector(
const preFilledFormData = useAppSelector(
(state) => state.case.caseForm?.[caseId]?.[TaskTitleUIMapping.COLLECTION_FEEDBACK]
);
const {
addressString,
phoneNumbers,
currentOutstandingEmi,
loanAccountNumber,
totalOverdueAmount,
pos,
} = caseDetail;
const { addressString, phoneNumbers, loanAccountNumber, totalOverdueAmount, pos } = caseDetail;
const feedbackList: IFeedback[] = useAppSelector(
(state: RootState) => state.feedbackHistory?.[loanAccountNumber as string]?.data || []
@@ -127,6 +123,14 @@ const CollectionCaseDetails: React.FC<ICaseDetails> = (props) => {
const allCasesDetails = useAppSelector((state) => state.allCases.caseDetails);
useEffect(() => {
if (caseId) dispatch(setSelectedCaseId(caseId));
return () => {
dispatch(setSelectedCaseId(''));
};
}, [caseId]);
useEffect(() => {
if (!loanAccountNumber) {
return;
@@ -191,13 +195,26 @@ const CollectionCaseDetails: React.FC<ICaseDetails> = (props) => {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.AV_CASE_DETAILS_ADD_FEEDBACK_CLICKED, {
caseId: caseId,
caseType: caseDetail?.caseType,
journey: 'COLLECTION_FEEDBACK',
journey: TaskTitleUIMapping.COLLECTION_FEEDBACK,
});
if (data?.visitedWidgets?.length) {
_map(data.visitedWidgets, (visited) =>
if (preFilledFormData?.visitedWidgets?.length) {
const lastFormInteractionTs = preFilledFormData?.updatedAt || 0;
// If Date.now() is greater than updatedAt by 60mins, then we will navigate to the first widget
const isTimeExpired = Date.now() - lastFormInteractionTs > FEEDBACK_FORM_RESPONSE_EXPIRY_TIME;
if (isTimeExpired) {
navigateToScreen(
getTemplateRoute(CollectionCaseWidgetId.START, CaseAllocationType.COLLECTION_CASE),
{
caseId: caseId,
journey: TaskTitleUIMapping.COLLECTION_FEEDBACK,
}
);
return;
}
_map(preFilledFormData.visitedWidgets, (visited) =>
navigateToScreen(getTemplateRoute(visited, CaseAllocationType.COLLECTION_CASE), {
caseId: caseId,
journey: 'COLLECTION_FEEDBACK',
journey: TaskTitleUIMapping.COLLECTION_FEEDBACK,
})
);
return;
@@ -206,7 +223,7 @@ const CollectionCaseDetails: React.FC<ICaseDetails> = (props) => {
getTemplateRoute(CollectionCaseWidgetId.START, CaseAllocationType.COLLECTION_CASE),
{
caseId: caseId,
journey: 'COLLECTION_FEEDBACK',
journey: TaskTitleUIMapping.COLLECTION_FEEDBACK,
}
);
};
@@ -513,7 +530,14 @@ const CollectionCaseDetails: React.FC<ICaseDetails> = (props) => {
/>
<Button
style={[styles.feedbackButton]}
title="Add new feedback"
title={preFilledFormData ? 'Continue feedback' : 'Add new feedback'}
rightIcon={
preFilledFormData ? (
<View style={[GenericStyles.ml8]}>
<ArrowSolidIcon rotateY={180} size={10} fillColor={COLORS.TEXT.WHITE} />
</View>
) : null
}
variant="primary"
onPress={handleAddFeedback}
testID={'test_add_feedback'}

View File

@@ -58,7 +58,8 @@ export const extractQuestionContext = async (answer: Answer): Promise<IQuestionC
if (
answer.type === AnswerType.date ||
answer.type === AnswerType.phoneNumber ||
answer.type === AnswerType.address
answer.type === AnswerType.address ||
answer.type === AnswerType.time
) {
answer = { ...answer, type: AnswerType.text };
}

View File

@@ -113,6 +113,7 @@ export interface IVisitedWidgetContext {
};
};
};
updatedAt?: number;
}
export enum CommonCaseWidgetId {