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
This commit is contained in:
Himanshu Kansal
2023-03-17 13:26:18 +05:30
committed by GitHub Enterprise
parent c4749f37d8
commit 11f5f70c0e
12 changed files with 355 additions and 21 deletions

View File

@@ -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}
/>
<Stack.Screen
name={PageRouteEnum.PAST_FEEDBACK_DETAIL}
component={FeedbackDetailContainer}
options={{
header: () => null,
animationDuration: ANIMATION_DURATION,
animation: 'none',
}}
listeners={getScreenFocusListenerObj}
/>
{_map(avTemplate?.widget, key => (
<Stack.Screen
key={getTemplateRoute(key, CaseAllocationType.ADDRESS_VERIFICATION_CASE)}

View File

@@ -0,0 +1,31 @@
import axiosInstance, {
ApiKeys,
getApiUrl,
} from '../components/utlis/apiHelper';
import { logError } from '../components/utlis/errorUtils';
interface IPastFeedbacks {
account_number: string,
page_no: number,
page_size: number,
customerRecahble: boolean,
}
export const getPastFeedbacks =
(queryParamsPayload: IPastFeedbacks) => {
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);
})
};

View File

@@ -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<ApiKeys, string> = {} as Record<ApiKeys, string>;
@@ -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,

View File

@@ -8,13 +8,14 @@ import { IGeoLocation } from '../../types/addressGeolocation.types';
interface IGeolocationItem {
geolocationItem: IGeoLocation;
showSeparator?: boolean;
}
const SeparatorBorderComponent = () => {
return <View style={[styles.borderLine, { marginHorizontal: 16 }]}></View>;
}
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) => {
</Text>
</TouchableOpacity>
</View>
<SeparatorBorderComponent />
{
showSeparator ? <SeparatorBorderComponent /> : null
}
</View>
);
};

View File

@@ -313,7 +313,7 @@ const CollectionCaseDetails: React.FC<ICaseDetails> = props => {
<View style={[{ flexBasis: '35%' }]}>
<TouchableOpacity
activeOpacity={0.7}
onPress={console.log}
onPress={() => handleRouting(PageRouteEnum.PAST_FEEDBACK_DETAIL)}
style={GenericStyles.flex20}>
<Text style={[styles.textContainer, styles.feedbackBtn]}>
Open all feedbacks

View File

@@ -0,0 +1,8 @@
import React from 'react'
import Text from '../../../../RN-UI-LIB/src/components/Text'
export default function FeedbackDetailAnswerContainer() {
return (
<Text>FeedbackDetailAnswerContainer</Text>
)
}

View File

@@ -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<IFeedbackDetailContainer> = ({ route: routeParams }) => {
const {
params: { loanAccountNumber, activeFeedbackReferenceId },
} = routeParams;
const isOnline = useIsOnline();
const [feedbackList, setFeedbackList] = useState<IFeedback[]>([]);
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<GenericType>();
useEffect(() => {
if (!ref || !dataSourceCord) {
return;
}
ref.scrollTo({
x: 0,
y: dataSourceCord,
animated: true,
});
}, [ref, dataSourceCord])
return (
<View>
<NavigationHeader title={PAGE_TITLE} onBack={goBack} />
<ScrollView ref={x => setRef(x)}>
<View style={styles.container}>
{
feedbackList.map((feedback: IFeedback, index) => (
<View
key={index}
onLayout={(event) => {
const layout = event.nativeEvent.layout;
if (!dataSourceCord && feedback.referenceId === activeFeedbackReferenceId) {
setDataSourceCord(layout.y + SCROLL_LAYOUT_OFFSET)
}
}}>
<Accordion accordionStyle={[GenericStyles.p16, getShadowStyle(4)]}
isActive={feedback.referenceId === activeFeedbackReferenceId}
accordionHeader={<FeedbackDetailItem
key={feedback.referenceId}
feedbackItem={feedback}
/>}
customExpandUi={{ whenCollapsed: <Text style={styles.accordionExpandBtn}>View more</Text>, whenExpanded: <Text style={styles.accordionExpandBtn}>View less</Text> }}
>
<View>
<FeedbackDetailAnswerContainer />
</View>
</Accordion>
</View>
))
}
</View>
</ScrollView>
</View>
)
}
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;

View File

@@ -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 (
<View style={GenericStyles.columnDirection}>
<Text numberOfLines={1} ellipsizeMode="tail" style={[styles.textContainer, styles.cardBoldTitle, GenericStyles.pb4]}>
{sanitizeString(InteractionStatuses[feedbackItem.interactionStatus])}
</Text>
<Text numberOfLines={1} ellipsizeMode="tail" style={[styles.textContainer, styles.cardFooterText, GenericStyles.fontSize12]}>
{sanitizeString(dateFormat(new Date(feedbackItem.createdAt), BUSINESS_DATE_FORMAT))}
&nbsp;&nbsp;&#9679;&nbsp;&nbsp;
{sanitizeString(dateFormat(new Date(feedbackItem.createdAt), BUSINESS_TIME_FORMAT))}
</Text>
<View style={[GenericStyles.borderTop, GenericStyles.w100, GenericStyles.mv16]} />
<Text numberOfLines={3} ellipsizeMode="tail" style={[styles.textContainer, styles.cardLightTitle, GenericStyles.w100]}>
{
FIELD_FEEDBACKS.includes(feedbackItem.type) ?
sanitizeString(getAddress(feedbackItem.source as IAddress)) :
sanitizeString([(feedbackItem.source as ICallingFeedback)?.recipientNumber, feedbackItem.sourceType ? `(${feedbackItem.sourceType})` : ''].join(' '))
}
</Text>
{
(feedbackItem.interactionLatitude?.interactionLatitude && FIELD_FEEDBACKS.includes(feedbackItem.type)) ? (
<TouchableOpacity
activeOpacity={0.7}
onPress={() => setShowBottomSheet(true)}
style={[GenericStyles.row, GenericStyles.pt7, GenericStyles.alignCenter]}>
<Text style={[styles.textContainer, styles.geolocationBtn]}>
View geo location
</Text>
<Text style={GenericStyles.pt7}>
<ArrowSolidIcon fillColor={COLORS.BASE.BLUE} rotateY={180} />
</Text>
<BottomSheet
header="Geolocation(s)"
heightPercentage={getBottomSheetHeight(1)}
visible={showBottomSheet}
isSwipeable
setVisible={setShowBottomSheet}
>
<ScrollView>
<GeolocationItem showSeparator={false} geolocationItem={{
latitude: feedbackItem.metadata.interactionLatitude,
longitude: feedbackItem.metadata.interactionLongitude,
timeStamp: feedbackItem.createdAt,
distanceFromAgent: feedbackItem.metadata.userDistanceFromAddress,
}}/>
</ScrollView>
</BottomSheet>
</TouchableOpacity>
) : null
}
</View>
)
}
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;

View File

@@ -71,7 +71,7 @@ const OfflineFeedbackListContainer: React.FC<IOfflineFeedbackListContainer> = ({
const FeedbackListContainer: React.FC<IFeedbackListContainer> = ({ 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<IFeedbackListContainer> = ({ loanAccountNu
);
const handleRetryEvent = () => {
setRetryBtnToggle(retryBtnToggle => !retryBtnToggle);
setRetryBtnCount(retryBtnCount => retryBtnCount + 1);
}
useEffect(() => {
@@ -91,7 +91,13 @@ const FeedbackListContainer: React.FC<IFeedbackListContainer> = ({ 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 <OfflineFeedbackListContainer handleRetryEvent={handleRetryEvent} />
@@ -105,7 +111,7 @@ const FeedbackListContainer: React.FC<IFeedbackListContainer> = ({ loanAccountNu
<View>
{
feedbackList.map((feedbackItem: IFeedback, idx: number) => (
<FeedbackListItem feedbackItem={feedbackItem} showHorizontalLine={++idx !== feedbackList.length} />
<FeedbackListItem feedbackItem={feedbackItem} loanAccountNumber={loanAccountNumber} showHorizontalLine={++idx !== feedbackList.length} />
))
}
</View>

View File

@@ -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<IFeedbackListItem> = ({ feedbackItem, showHorizontalLine = true }) => {
const FeedbackListItem: React.FC<IFeedbackListItem> = ({ feedbackItem, loanAccountNumber, showHorizontalLine = true }) => {
const handleRouting = (route: PageRouteEnum, params: object | undefined = undefined) => {
const commonParams = {
loanAccountNumber,
activeFeedbackReferenceId: feedbackItem.referenceId
};
navigateToScreen(route, { ...params, ...commonParams })
}
return (
<View style={[
GenericStyles.ph24,
@@ -22,17 +34,17 @@ const FeedbackListItem: React.FC<IFeedbackListItem> = ({ feedbackItem, showHoriz
]}>
<TouchableOpacity
activeOpacity={0.7}
onPress={console.log}
style={[GenericStyles.row, GenericStyles.alignCenter, showHorizontalLine && GenericStyles.pb16 ]}
onPress={() => handleRouting(PageRouteEnum.PAST_FEEDBACK_DETAIL)}
style={[GenericStyles.row, GenericStyles.alignCenter, showHorizontalLine && GenericStyles.pb16]}
>
<View style={{ flexBasis: '95%' }}>
<Text
style={styles.textHeading}>
{sanitizeString(InteractionStatuses[feedbackItem.interactionStatus])}
{sanitizeString(InteractionStatuses[feedbackItem.interactionStatus])}
</Text>
<Text
style={styles.subText}>
{sanitizeString(`${dateFormat(new Date(feedbackItem?.createdAt), BUSINESS_DATE_FORMAT)}`)}
{sanitizeString(`${dateFormat(new Date(feedbackItem?.createdAt), BUSINESS_DATE_FORMAT)}`)}
</Text>
</View>
<Chevron />

View File

@@ -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 {

View File

@@ -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;
}
}
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];