From 11f5f70c0e7b6029997cbbcb3c48467e1d44f552 Mon Sep 17 00:00:00 2001 From: Himanshu Kansal Date: Fri, 17 Mar 2023 13:26:18 +0530 Subject: [PATCH] TP-20465 | Feedback Detail Screen (#156) * TP-20465 | Feedback Detail Screen * TP-20465 | Fix integration bugs * TP-20465 | Added scroll Animation * TP-20465 | Minor Fix * TP-20465 | API integration + feedbackAction --- ProtectedRouter.tsx | 14 +- src/action/feedbackActions.ts | 31 +++++ src/components/utlis/apiHelper.ts | 4 +- .../addressGeolocation/GeolocationItem.tsx | 8 +- .../caseDetails/CollectionCaseDetail.tsx | 2 +- .../FeedbackDetailAnswerContainer.tsx | 8 ++ .../feedback/FeedbackDetailContainer.tsx | 130 ++++++++++++++++++ .../feedback/FeedbackDetailItem.tsx | 106 ++++++++++++++ .../feedback/FeedbackListContainer.tsx | 14 +- .../caseDetails/feedback/FeedbackListItem.tsx | 22 ++- src/types/addressGeolocation.types.ts | 4 +- src/types/feedback.types.ts | 33 ++++- 12 files changed, 355 insertions(+), 21 deletions(-) create mode 100644 src/action/feedbackActions.ts create mode 100644 src/screens/caseDetails/feedback/FeedbackDetailAnswerContainer.tsx create mode 100644 src/screens/caseDetails/feedback/FeedbackDetailContainer.tsx create mode 100644 src/screens/caseDetails/feedback/FeedbackDetailItem.tsx diff --git a/ProtectedRouter.tsx b/ProtectedRouter.tsx index 8affe824..2af27c29 100644 --- a/ProtectedRouter.tsx +++ b/ProtectedRouter.tsx @@ -31,6 +31,7 @@ import { CaseAllocationType } from "./src/screens/allCases/interface"; import { getTemplateRoute } from "./src/components/utlis/navigationUtlis"; import { resetNewVisitedCases } from './src/reducer/allCasesSlice'; import { getCaseUnifiedData, UnifiedCaseDetailsTypes } from './src/action/caseApiActions'; +import FeedbackDetailContainer from './src/screens/caseDetails/feedback/FeedbackDetailContainer'; const ANIMATION_DURATION = 300; @@ -40,7 +41,8 @@ export enum PageRouteEnum { PAYMENTS = 'registerPayments', ADDRESS_GEO = 'addressGeolocation', NEW_ADDRESS = 'newAddress', - COLLECTION_CASE_DETAIL = 'collectionCaseDetail' + COLLECTION_CASE_DETAIL = 'collectionCaseDetail', + PAST_FEEDBACK_DETAIL = 'pastFeedbackDetail' } const ProtectedRouter = () => { @@ -180,6 +182,16 @@ const ProtectedRouter = () => { }} listeners={getScreenFocusListenerObj} /> + null, + animationDuration: ANIMATION_DURATION, + animation: 'none', + }} + listeners={getScreenFocusListenerObj} + /> {_map(avTemplate?.widget, key => ( { + const url = getApiUrl(ApiKeys.PAST_FEEDBACK); + return axiosInstance + .post(url, {}, { + params: queryParamsPayload + }) + .then(response => { + if (response?.data) { + return response.data; + } + throw response; + }) + .catch(err => { + logError(err); + }) + }; + diff --git a/src/components/utlis/apiHelper.ts b/src/components/utlis/apiHelper.ts index 7e141296..6450189a 100644 --- a/src/components/utlis/apiHelper.ts +++ b/src/components/utlis/apiHelper.ts @@ -25,7 +25,8 @@ export enum ApiKeys { ADDRESSES_GEOLOCATION, NEW_ADDRESS, GET_SIGNED_URL, - CASE_UNIFIED_DETAILS + CASE_UNIFIED_DETAILS, + PAST_FEEDBACK, } export const API_URLS: Record = {} as Record; @@ -43,6 +44,7 @@ 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.CASE_UNIFIED_DETAILS] = '/collection-cases/unified-details/{loanAccountNumber}'; +API_URLS[ApiKeys.PAST_FEEDBACK] = '/field-app/feedbacks'; export const API_STATUS_CODE = { OK: 200, diff --git a/src/screens/addressGeolocation/GeolocationItem.tsx b/src/screens/addressGeolocation/GeolocationItem.tsx index 7de87842..544c9de7 100644 --- a/src/screens/addressGeolocation/GeolocationItem.tsx +++ b/src/screens/addressGeolocation/GeolocationItem.tsx @@ -8,13 +8,14 @@ import { IGeoLocation } from '../../types/addressGeolocation.types'; interface IGeolocationItem { geolocationItem: IGeoLocation; + showSeparator?: boolean; } const SeparatorBorderComponent = () => { return ; } -const GeolocationItem = ({ geolocationItem }: IGeolocationItem) => { +const GeolocationItem = ({ geolocationItem, showSeparator = true }: IGeolocationItem) => { const locationDate = dateFormat(new Date(geolocationItem?.timeStamp), BUSINESS_DATE_FORMAT); const locationTime = dateFormat(new Date(geolocationItem?.timeStamp), BUSINESS_TIME_FORMAT); @@ -42,7 +43,10 @@ const GeolocationItem = ({ geolocationItem }: IGeolocationItem) => { - + { + showSeparator ? : null + } + ); }; diff --git a/src/screens/caseDetails/CollectionCaseDetail.tsx b/src/screens/caseDetails/CollectionCaseDetail.tsx index 27a846ad..f2596eb1 100644 --- a/src/screens/caseDetails/CollectionCaseDetail.tsx +++ b/src/screens/caseDetails/CollectionCaseDetail.tsx @@ -313,7 +313,7 @@ const CollectionCaseDetails: React.FC = props => { handleRouting(PageRouteEnum.PAST_FEEDBACK_DETAIL)} style={GenericStyles.flex20}> Open all feedbacks diff --git a/src/screens/caseDetails/feedback/FeedbackDetailAnswerContainer.tsx b/src/screens/caseDetails/feedback/FeedbackDetailAnswerContainer.tsx new file mode 100644 index 00000000..b3b52b58 --- /dev/null +++ b/src/screens/caseDetails/feedback/FeedbackDetailAnswerContainer.tsx @@ -0,0 +1,8 @@ +import React from 'react' +import Text from '../../../../RN-UI-LIB/src/components/Text' + +export default function FeedbackDetailAnswerContainer() { + return ( + FeedbackDetailAnswerContainer + ) +} diff --git a/src/screens/caseDetails/feedback/FeedbackDetailContainer.tsx b/src/screens/caseDetails/feedback/FeedbackDetailContainer.tsx new file mode 100644 index 00000000..73912ce0 --- /dev/null +++ b/src/screens/caseDetails/feedback/FeedbackDetailContainer.tsx @@ -0,0 +1,130 @@ +import React, { useEffect, useState } from 'react' +import { ScrollView, StyleSheet, View } from 'react-native'; +import Accordion from '../../../../RN-UI-LIB/src/components/accordian/Accordian'; +import NavigationHeader from '../../../../RN-UI-LIB/src/components/NavigationHeader'; +import Text from '../../../../RN-UI-LIB/src/components/Text'; +import { GenericStyles, getShadowStyle } from '../../../../RN-UI-LIB/src/styles'; +import { COLORS } from '../../../../RN-UI-LIB/src/styles/colors'; +import { getPastFeedbacks } from '../../../action/feedbackActions'; +import { GenericType } from '../../../common/GenericTypes'; +import { logError } from '../../../components/utlis/errorUtils'; +import { goBack } from '../../../components/utlis/navigationUtlis'; +import { useAppSelector } from '../../../hooks'; +import useIsOnline from '../../../hooks/useIsOnline'; +import { RootState } from '../../../store/store'; +import { IFeedback } from '../../../types/feedback.types'; +import FeedbackDetailAnswerContainer from './FeedbackDetailAnswerContainer'; +import FeedbackDetailItem from './FeedbackDetailItem'; + +const PAGE_TITLE = 'All feedbacks'; +const SCROLL_LAYOUT_OFFSET = -10; + +interface IFeedbackDetailContainer { + route: { + params: { + loanAccountNumber: string; + activeFeedbackReferenceId?: string; + }; + }; +} + +const FeedbackDetailContainer: React.FC = ({ route: routeParams }) => { + const { + params: { loanAccountNumber, activeFeedbackReferenceId }, + } = routeParams; + + const isOnline = useIsOnline(); + + const [feedbackList, setFeedbackList] = useState([]); + + const feedbackListFromCache = useAppSelector( + (state: RootState) => state.feedbackHistory?.data?.[loanAccountNumber], + ) + + useEffect(() => { + if(isOnline) { + getPastFeedbacks({ + account_number: loanAccountNumber, + page_no: 0, + page_size: 100, + customerRecahble: false, + }).then((res: IFeedback[]) => { + if (res) { + setFeedbackList(res); + } + throw res; + }) + .catch(err => { + logError(err); + setFeedbackList(feedbackListFromCache); + }) + } else { + setFeedbackList(feedbackListFromCache); + } + }, [feedbackListFromCache]); + + const [dataSourceCord, setDataSourceCord] = useState(0); + const [ref, setRef] = useState(); + + useEffect(() => { + if (!ref || !dataSourceCord) { + return; + } + ref.scrollTo({ + x: 0, + y: dataSourceCord, + animated: true, + }); + }, [ref, dataSourceCord]) + + return ( + + + setRef(x)}> + + { + feedbackList.map((feedback: IFeedback, index) => ( + { + const layout = event.nativeEvent.layout; + if (!dataSourceCord && feedback.referenceId === activeFeedbackReferenceId) { + setDataSourceCord(layout.y + SCROLL_LAYOUT_OFFSET) + } + }}> + } + customExpandUi={{ whenCollapsed: View more, whenExpanded: View less }} + > + + + + + + )) + } + + + + ) +} + +const styles = StyleSheet.create({ + container: { + paddingVertical: 20, + paddingHorizontal: 16, + backgroundColor: COLORS.BACKGROUND.PRIMARY, + }, + accordionExpandBtn: { + fontSize: 13, + fontWeight: '500', + lineHeight: 20, + color: COLORS.TEXT.BLUE + } +}); + +export default FeedbackDetailContainer; \ No newline at end of file diff --git a/src/screens/caseDetails/feedback/FeedbackDetailItem.tsx b/src/screens/caseDetails/feedback/FeedbackDetailItem.tsx new file mode 100644 index 00000000..bb4eaec5 --- /dev/null +++ b/src/screens/caseDetails/feedback/FeedbackDetailItem.tsx @@ -0,0 +1,106 @@ +import React from 'react' +import { View, StyleSheet, TouchableOpacity, ScrollView } from 'react-native'; +import BottomSheet from '../../../../RN-UI-LIB/src/components/bottom_sheet/BottomSheet'; +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'; +import { BUSINESS_DATE_FORMAT, BUSINESS_TIME_FORMAT, dateFormat } from '../../../../RN-UI-LIB/src/utlis/dates'; +import { getDynamicBottomSheetHeightPercentageFn, sanitizeString } from '../../../components/utlis/commonFunctions'; +import { FIELD_FEEDBACKS, ICallingFeedback, IFeedback } from '../../../types/feedback.types'; +import GeolocationItem from '../../addressGeolocation/GeolocationItem'; +import { InteractionStatuses } from '../../allCases/interface'; +import { Address as IAddress } from '../interface'; + + +interface IFeedbackDetailItem { + feedbackItem: IFeedback +} + +const getAddress = (address?: IAddress) => { + if (!address) return ''; + return (`${address.lineOne} ${address.lineTwo} ${address.pinCode} ${address.state}`).replace(/\s{2,}/g, ' '); +} + +const FeedbackDetailItem = ({ feedbackItem }: IFeedbackDetailItem) => { + + const [showBottomSheet, setShowBottomSheet] = React.useState(false); + + const getBottomSheetHeight = getDynamicBottomSheetHeightPercentageFn(); + + return ( + + + {sanitizeString(InteractionStatuses[feedbackItem.interactionStatus])} + + + {sanitizeString(dateFormat(new Date(feedbackItem.createdAt), BUSINESS_DATE_FORMAT))} +   ●   + {sanitizeString(dateFormat(new Date(feedbackItem.createdAt), BUSINESS_TIME_FORMAT))} + + + + { + FIELD_FEEDBACKS.includes(feedbackItem.type) ? + sanitizeString(getAddress(feedbackItem.source as IAddress)) : + sanitizeString([(feedbackItem.source as ICallingFeedback)?.recipientNumber, feedbackItem.sourceType ? `(${feedbackItem.sourceType})` : ''].join(' ')) + } + + { + (feedbackItem.interactionLatitude?.interactionLatitude && FIELD_FEEDBACKS.includes(feedbackItem.type)) ? ( + setShowBottomSheet(true)} + style={[GenericStyles.row, GenericStyles.pt7, GenericStyles.alignCenter]}> + + View geo location + + + + + + + + + + + ) : null + } + + ) +} + +const styles = StyleSheet.create({ + textContainer: { + fontSize: 14, + lineHeight: 20, + }, + cardBoldTitle: { + fontWeight: '600', + color: COLORS.TEXT.DARK + }, + cardLightTitle: { + fontWeight: '400', + color: '#BCBCBC' + }, + cardFooterText: { + fontWeight: '400', + color: COLORS.TEXT.LIGHT + }, + geolocationBtn: { + color: COLORS.BASE.BLUE + } +}); + +export default FeedbackDetailItem; \ No newline at end of file diff --git a/src/screens/caseDetails/feedback/FeedbackListContainer.tsx b/src/screens/caseDetails/feedback/FeedbackListContainer.tsx index 7bd4ee87..141a7aea 100644 --- a/src/screens/caseDetails/feedback/FeedbackListContainer.tsx +++ b/src/screens/caseDetails/feedback/FeedbackListContainer.tsx @@ -71,7 +71,7 @@ const OfflineFeedbackListContainer: React.FC = ({ const FeedbackListContainer: React.FC = ({ loanAccountNumber }) => { - const [retryBtnToggle, setRetryBtnToggle] = useState(false); + const [retryBtnCount, setRetryBtnCount] = useState(0); const dispatch = useAppDispatch(); const isOnline = useIsOnline(); @@ -81,7 +81,7 @@ const FeedbackListContainer: React.FC = ({ loanAccountNu ); const handleRetryEvent = () => { - setRetryBtnToggle(retryBtnToggle => !retryBtnToggle); + setRetryBtnCount(retryBtnCount => retryBtnCount + 1); } useEffect(() => { @@ -91,7 +91,13 @@ const FeedbackListContainer: React.FC = ({ loanAccountNu if (isOnline || !feedbackList) { dispatch(getCaseUnifiedData([loanAccountNumber], [UnifiedCaseDetailsTypes.FEEDBACKS])) } - }, [isOnline, retryBtnToggle, feedbackList]); + }, []); + + useEffect(() => { + if (retryBtnCount && isOnline) { + dispatch(getCaseUnifiedData([loanAccountNumber], [UnifiedCaseDetailsTypes.FEEDBACKS])) + } + }, [retryBtnCount]); if (!isOnline && !feedbackList) { return @@ -105,7 +111,7 @@ const FeedbackListContainer: React.FC = ({ loanAccountNu { feedbackList.map((feedbackItem: IFeedback, idx: number) => ( - + )) } diff --git a/src/screens/caseDetails/feedback/FeedbackListItem.tsx b/src/screens/caseDetails/feedback/FeedbackListItem.tsx index 29dce2ef..a3f0e378 100644 --- a/src/screens/caseDetails/feedback/FeedbackListItem.tsx +++ b/src/screens/caseDetails/feedback/FeedbackListItem.tsx @@ -1,20 +1,32 @@ import React from 'react' import { StyleSheet, TouchableOpacity, View } from 'react-native'; +import { PageRouteEnum } from '../../../../ProtectedRouter'; import Text from '../../../../RN-UI-LIB/src/components/Text' import Chevron from '../../../../RN-UI-LIB/src/Icons/Chevron'; import { GenericStyles } from '../../../../RN-UI-LIB/src/styles'; import { COLORS } from '../../../../RN-UI-LIB/src/styles/colors'; import { BUSINESS_DATE_FORMAT, dateFormat } from '../../../../RN-UI-LIB/src/utlis/dates'; import { sanitizeString } from '../../../components/utlis/commonFunctions'; +import { navigateToScreen } from '../../../components/utlis/navigationUtlis'; import { IFeedback } from '../../../types/feedback.types' import { InteractionStatuses } from '../../allCases/interface'; interface IFeedbackListItem { feedbackItem: IFeedback, showHorizontalLine?: boolean, + loanAccountNumber: string, } -const FeedbackListItem: React.FC = ({ feedbackItem, showHorizontalLine = true }) => { +const FeedbackListItem: React.FC = ({ feedbackItem, loanAccountNumber, showHorizontalLine = true }) => { + + const handleRouting = (route: PageRouteEnum, params: object | undefined = undefined) => { + const commonParams = { + loanAccountNumber, + activeFeedbackReferenceId: feedbackItem.referenceId + }; + navigateToScreen(route, { ...params, ...commonParams }) + } + return ( = ({ feedbackItem, showHoriz ]}> handleRouting(PageRouteEnum.PAST_FEEDBACK_DETAIL)} + style={[GenericStyles.row, GenericStyles.alignCenter, showHorizontalLine && GenericStyles.pb16]} > - {sanitizeString(InteractionStatuses[feedbackItem.interactionStatus])} + {sanitizeString(InteractionStatuses[feedbackItem.interactionStatus])} - {sanitizeString(`${dateFormat(new Date(feedbackItem?.createdAt), BUSINESS_DATE_FORMAT)}`)} + {sanitizeString(`${dateFormat(new Date(feedbackItem?.createdAt), BUSINESS_DATE_FORMAT)}`)} diff --git a/src/types/addressGeolocation.types.ts b/src/types/addressGeolocation.types.ts index 24497898..1e6edc5c 100644 --- a/src/types/addressGeolocation.types.ts +++ b/src/types/addressGeolocation.types.ts @@ -24,8 +24,8 @@ export interface IAddressLevel1 { export interface IGeoLocation { latitude: number, longitude: number, - timeStamp: number, - distanceFromAgent: number + timeStamp: number | string, + distanceFromAgent?: number } export interface AddressL1Details { diff --git a/src/types/feedback.types.ts b/src/types/feedback.types.ts index a3ed9843..48386763 100644 --- a/src/types/feedback.types.ts +++ b/src/types/feedback.types.ts @@ -1,5 +1,6 @@ import { GenericType } from "../common/GenericTypes"; import { Address, InteractionStatuses } from "../screens/allCases/interface"; +import { PhoneNumberSource } from "../screens/caseDetails/interface"; export enum AnswerType { TEXT = 'TEXT', @@ -24,17 +25,39 @@ export interface IAnswerView { inputText?: string; } +export enum FEEDBACK_TYPE { + FIELD_VISIT = 'FIELD_VISIT', + INHOUSE_FIELD_VISIT = 'INHOUSE_FIELD_VISIT', + CALL_BRIDGE = 'CALL_BRIDGE', + SELF_CALL = 'SELF_CALL' +} + +export interface FIELD_FEEDBACK_METADATA { + interactionLatitude: string, + interactionLongitude: string, + userDistanceFromAddress: string +} + +export interface ICallingFeedback { + recipientName: string, + recipientNumber: string +} + export interface IFeedback { [x: string]: any; + id: string; referenceId: string; - type: string; + type: FEEDBACK_TYPE; accountNumber: string; createdAt: string; - metadata: GenericType; - sourceType: string; + metadata: FIELD_FEEDBACK_METADATA | GenericType; + sourceType: PhoneNumberSource; answerViews: IAnswerView[]; imageUrls: string[]; agentReferenceId: string - source: Address | GenericType; + source: Address | ICallingFeedback; interactionStatus: InteractionStatuses; -} \ No newline at end of file +} + +export const CALLING_FEEDBACKS = [FEEDBACK_TYPE.CALL_BRIDGE, FEEDBACK_TYPE.SELF_CALL]; +export const FIELD_FEEDBACKS = [FEEDBACK_TYPE.FIELD_VISIT, FEEDBACK_TYPE.INHOUSE_FIELD_VISIT]; \ No newline at end of file