Merge pull request #628 from navi-medici/WhatsappFeedbackEnhancment

Added option to share (WhatsApp) feedback on case details screen
This commit is contained in:
Ashish Deo
2023-10-31 21:53:50 +05:30
committed by GitHub
5 changed files with 293 additions and 149 deletions

View File

@@ -1,4 +1,4 @@
import React, { ReactNode, useEffect, useState } from 'react';
import React, { useRef, ReactNode, useEffect, useState } from 'react';
import { View, StyleSheet, TouchableOpacity, Linking, Platform, NativeModules } from 'react-native';
import Text from '../../../../RN-UI-LIB/src/components/Text';
import { GenericStyles } from '../../../../RN-UI-LIB/src/styles';
@@ -8,7 +8,7 @@ import {
BUSINESS_TIME_FORMAT,
dateFormat,
} from '../../../../RN-UI-LIB/src/utlis/dates';
import { CaseDetail, Address as IAddress, IGeolocation, VisitType } from '../interface';
import { Address, CaseDetail, Address as IAddress, IGeolocation, VisitType } from '../interface';
import {
debounce,
getGoogleMapUrl,
@@ -28,12 +28,8 @@ import { CLICKSTREAM_EVENT_NAMES } from '../../../common/Constants';
import GeolocationAddress from '../../../components/form/components/GeolocationAddress';
import IconLabel from '../../../common/IconLabel';
import WhatsAppFeedbackShareIcon from '../../../assets/icons/WhatsAppIcon';
import ReactNativeBlobUtil from 'react-native-blob-util';
import { useAppSelector } from '../../../hooks';
import { toast } from '../../../../RN-UI-LIB/src/components/toast';
import { ToastMessages } from '../../allCases/constants';
import { sendFeedbackToWhatsapp } from '../../../components/utlis/DeviceUtils';
import { getSanitizedCommaAmount } from '@rn-ui-lib/utils/amount';
import { shareToWhatsapp } from '../../../services/FeedbackWhatsApp';
interface IFeedbackDetailItem {
feedbackItem: IFeedback;
@@ -49,9 +45,9 @@ const feedbackTypeIcon: Record<FEEDBACK_TYPE, ReactNode> = {
CALL_BRIDGE: <CallIcon fillColor={COLORS.TEXT.LIGHT} />,
};
const getAddress = (address?: IAddress) => {
const getAddress = (address?: Address) => {
if (!address) return '';
return `${address.lineOne} ${address.lineTwo} ${address.pinCode} ${address.state}`.replace(
return `${address?.lineOne} ${address?.lineTwo} ${address?.pinCode} ${address?.state}`.replace(
/\s{2,}/g,
' '
);
@@ -67,138 +63,21 @@ const openGeolocation = (latitude: string, longitude: string) => {
return Linking.openURL(geolocationUrl);
};
function getLocationLink(latitude: string, longitude: string): string {
const link = 'https://www.google.com/maps/search/?api=1&query=' + latitude + ',' + longitude;
return link;
}
const FeedbackDetailItem = ({
feedbackItem,
isExpanded,
caseId,
hideAddress,
}: IFeedbackDetailItem) => {
const caseDetails = useAppSelector((state) => state.allCases.caseDetails[caseId]);
const { agentId } = useAppSelector((state) => ({ agentId: state.user.user?.referenceId!! }));
const { agentId, caseDetails } = useAppSelector((state) => ({
agentId: state.user.user?.referenceId || null,
caseDetails: state.allCases.caseDetails?.[caseId] || null,
}));
const [isWhastappSendLoading, setIsWhatsappSendLoading] = useState(false);
const isGeolocation = feedbackItem?.source?.sourceType === VisitType.GEOLOCATION;
const sendToWhatsappNative = (
message: string,
imageUrl: string,
mimeType: string,
caseDetails: CaseDetail,
agentId: string
) => {
setIsWhatsappSendLoading(true);
sendFeedbackToWhatsapp(message, imageUrl, mimeType)
.then((res: boolean) => {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_SHARE_SUCCESSFUL, {
caseId: caseDetails?.id,
agentId: agentId,
});
setIsWhatsappSendLoading(false);
})
.catch((err: Error) => {
setIsWhatsappSendLoading(false);
if (err.message === '1') {
toast({
text1: ToastMessages.WHATSAPP_NOT_INSTALLED,
type: 'error',
});
} else {
toast({
text1: ToastMessages.WHATSAPP_FEEDBACK_SHARE_FAILURE,
type: 'error',
});
}
});
};
const throttledSendToWhatsapp = React.useRef(debounce(shareToWhatsapp, 500));
const sendToWhatsapp = (feedbackItem: IFeedback, caseDetails: CaseDetail, agentId: string) => {
setIsWhatsappSendLoading(true);
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_SHARE_FEEDBACK_CLICKED, {
caseId: caseDetails?.id,
agentId: agentId,
});
let message = `*Visit Feedback* for ${sanitizeString(caseDetails?.customerName)}
_${sanitizeString(dateFormat(new Date(feedbackItem?.createdAt), 'DD MMM, YYYY | HH:mm a.'))}_\n
*LAN*: ${sanitizeString(caseDetails?.loanAccountNumber)}
*DPD Bucket*: ${sanitizeString(caseDetails?.dpdBucket)}
*EMI Amount*: ₹${getSanitizedCommaAmount(caseDetails?.currentOutstandingEmi)}\n
*Disposition*: ${sanitizeString(feedbackItem?.interactionStatus)}`;
const ptpDate = feedbackItem?.answerViews.filter(
(answer) => answer.questionName === 'PTP Date'
);
if (ptpDate.length > 0) {
message +=
' for ' +
sanitizeString(dateFormat(new Date(ptpDate[0].inputDate), 'DD MMM, YYYY')) +
'\n\n';
} else {
message += '\n\n';
}
message += '*Remarks*: ';
const answerList = feedbackItem?.answerViews?.filter(
(answer) => answer.questionName === 'Comments'
);
if (answerList.length > 0) {
message += sanitizeString(answerList[0]?.inputText) + '\n\n';
} else {
message += 'N.A.\n\n';
}
{
feedbackItem?.metadata?.interactionLatitude &&
feedbackItem?.metadata?.interactionLongitude &&
FIELD_FEEDBACKS.includes(feedbackItem?.type)
? (message +=
'*Location of feedback*: ' +
sanitizeString(
getLocationLink(
feedbackItem?.metadata?.interactionLatitude,
feedbackItem?.metadata?.interactionLongitude
)
) +
'\n\n')
: null;
}
const imagesList = feedbackItem?.answerViews.filter(
(answer) => answer.questionTag === OPTION_TAG.IMAGE_UPLOAD
);
let imageUrl = '';
const mimeType = 'image/*';
if (imagesList.length > 0) {
let imageUri = '';
imageUri = imagesList[0]?.inputText ? imagesList[0].inputText : '';
let imagePath = '';
ReactNativeBlobUtil.config({
fileCache: true,
})
.fetch('GET', imageUri)
.then((resp: any) => {
if (resp.info().status !== 200) {
return '';
} else {
imagePath = resp.path();
return resp.readFile('base64');
}
})
.then((base64Data: any) => {
imageUrl = base64Data;
sendToWhatsappNative(message, imageUrl, mimeType, caseDetails, agentId);
ReactNativeBlobUtil.fs.unlink(imagePath);
});
} else {
sendToWhatsappNative(message, imageUrl, mimeType, caseDetails, agentId);
}
};
const throttledSendToWhatsapp = React.useRef(debounce(sendToWhatsapp, 500));
return (
<View style={[styles.addressItem]}>
<View style={[GenericStyles.row, GenericStyles.alignCenter]}>
@@ -256,7 +135,7 @@ _${sanitizeString(dateFormat(new Date(feedbackItem?.createdAt), 'DD MMM, YYYY |
style={[styles.textContainer, styles.cardLightTitle, GenericStyles.w100]}
>
{FIELD_FEEDBACKS.includes(feedbackItem.type)
? sanitizeString(getAddress(feedbackItem.source as IAddress))
? sanitizeString(getAddress(feedbackItem?.source as unknown as Address))
: sanitizeString(
[
(feedbackItem.source as ICallingFeedback)?.recipientNumber,
@@ -271,6 +150,7 @@ _${sanitizeString(dateFormat(new Date(feedbackItem?.createdAt), 'DD MMM, YYYY |
<View style={styles.container}>
{!isGeolocation ? (
<TouchableOpacity
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
activeOpacity={0.7}
onPress={() =>
openGeolocation(
@@ -284,9 +164,15 @@ _${sanitizeString(dateFormat(new Date(feedbackItem?.createdAt), 'DD MMM, YYYY |
</TouchableOpacity>
) : null}
<TouchableOpacity
hitSlop={{ top: 10, bottom: 10, left: 0, right: 10 }}
activeOpacity={0.7}
onPress={() => {
throttledSendToWhatsapp.current(feedbackItem, caseDetails, agentId);
throttledSendToWhatsapp.current(
feedbackItem,
caseDetails,
agentId,
setIsWhatsappSendLoading
);
}}
style={[GenericStyles.row, styles.BtnPadding]}
disabled={isWhastappSendLoading}

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { StyleSheet, TouchableOpacity, View } from 'react-native';
import React, { useState } from 'react';
import { Linking, StyleSheet, TouchableOpacity, View } from 'react-native';
import { PageRouteEnum } from '../../auth/ProtectedRouter';
import Text from '../../../../RN-UI-LIB/src/components/Text';
import Chevron from '../../../../RN-UI-LIB/src/Icons/Chevron';
@@ -7,9 +7,18 @@ import UnsyncedIcon from '../../../../RN-UI-LIB/src/Icons/UnsyncedIcon';
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 { debounce, sanitizeString } from '../../../components/utlis/commonFunctions';
import { navigateToScreen } from '../../../components/utlis/navigationUtlis';
import { IFeedback, IUnSyncedFeedbackItem } from '../../../types/feedback.types';
import {
FIELD_FEEDBACKS,
IFeedback,
IUnSyncedFeedbackItem,
OPTION_TAG,
} from '../../../types/feedback.types';
import IconLabel from '@common/IconLabel';
import WhatsAppFeedbackShareIcon from '@assets/icons/WhatsAppFeedbackShareIcon';
import { useAppSelector } from '@hooks';
import { shareToWhatsapp } from '../../../services/FeedbackWhatsApp';
interface IFeedbackListItem {
feedbackItem: IFeedback | IUnSyncedFeedbackItem;
@@ -33,30 +42,63 @@ const FeedbackListItem: React.FC<IFeedbackListItem> = ({
};
navigateToScreen(route, { ...params, ...commonParams });
};
const [isWhastappSendLoading, setIsWhatsappSendLoading] = useState(false);
const { agentId, caseDetails } = useAppSelector((state) => ({
agentId: state.user.user?.referenceId || null,
caseDetails: state.allCases.caseDetails?.[caseId] || null,
}));
const throttledSendToWhatsapp = React.useRef(debounce(shareToWhatsapp, 500));
return (
<View style={[GenericStyles.ph24, GenericStyles.pt12]}>
<View style={[GenericStyles.ph16]}>
<TouchableOpacity
activeOpacity={0.7}
onPress={() => handleRouting(PageRouteEnum.PAST_FEEDBACK_DETAIL)}
style={[
GenericStyles.row,
GenericStyles.alignCenter,
showHorizontalLine && GenericStyles.pb16,
showHorizontalLine ? GenericStyles.pb16 : GenericStyles.pb4,
]}
>
{feedbackItem.isSynced === false ? (
<View style={GenericStyles.pr8}>
<View style={GenericStyles.pr16}>
<UnsyncedIcon />
</View>
) : null}
<View style={{ flexBasis: '90%' }}>
<Text style={styles.textHeading}>{sanitizeString(feedbackItem.interactionStatus)}</Text>
<Text style={styles.subText}>
{sanitizeString(
`${dateFormat(new Date(feedbackItem?.createdAt), BUSINESS_DATE_FORMAT)}`
)}
</Text>
<View style={styles.feedBox}>
<View style={styles.textBox}>
<Text style={styles.textHeading}>{sanitizeString(feedbackItem.interactionStatus)}</Text>
<Text style={styles.subText}>
{sanitizeString(
`${dateFormat(new Date(feedbackItem?.createdAt), BUSINESS_DATE_FORMAT)}`
)}
</Text>
</View>
{feedbackItem?.type === 'FIELD_VISIT' ? (
<View style={styles.ShareButton}>
<TouchableOpacity
hitSlop={{ top: 20, bottom: 20, left: 20, right: 20 }}
activeOpacity={0.7}
onPress={(e) => {
e.stopPropagation();
throttledSendToWhatsapp.current(
feedbackItem,
caseDetails,
agentId,
setIsWhatsappSendLoading
);
}}
disabled={isWhastappSendLoading}
>
<IconLabel
text="Share"
icon={<WhatsAppFeedbackShareIcon width={15} height={15} />}
textStyle={styles.ButtonText}
/>
</TouchableOpacity>
</View>
) : null}
</View>
{!(feedbackItem.isSynced === false) ? <Chevron /> : null}
</TouchableOpacity>
@@ -68,8 +110,8 @@ const FeedbackListItem: React.FC<IFeedbackListItem> = ({
const styles = StyleSheet.create({
textHeading: {
fontSize: 14,
lineHeight: 20,
paddingBottom: 4,
lineHeight: 30,
paddingTop: 4,
color: COLORS.TEXT.DARK,
},
subText: {
@@ -77,6 +119,23 @@ const styles = StyleSheet.create({
lineHeight: 18,
color: COLORS.TEXT.LIGHT,
},
feedBox: {
flexBasis: '90%',
},
textBox: {
marginLeft: 10,
marginTop: 5,
},
ShareButton: {
width: 75,
height: 35,
padding: 5,
justifyContent: 'flex-start',
marginLeft: 5,
},
ButtonText: {
color: COLORS.BASE.BLUE,
},
});
export default FeedbackListItem;

View File

@@ -208,6 +208,7 @@ export interface IGeolocation {
id: string;
visitType: VisitType;
sourceType: VisitType;
source?: string;
}
export interface EmploymentDetails {
employmentType: string;

View File

@@ -0,0 +1,196 @@
import { debounce, sanitizeString } from '../components/utlis/commonFunctions';
import { sendFeedbackToWhatsapp } from '@components/utlis/DeviceUtils';
import ReactNativeBlobUtil from 'react-native-blob-util';
import { addClickstreamEvent } from '../services/clickstreamEventService';
import { CLICKSTREAM_EVENT_NAMES } from '@common/Constants';
import { toast } from '@rn-ui-lib/components/toast';
import { ToastMessages } from '@screens/allCases/constants';
import { dateFormat } from '@rn-ui-lib/utils/dates';
import { getSanitizedCommaAmount } from '@rn-ui-lib/utils/amount';
import { FEEDBACK_TYPE, FIELD_FEEDBACKS, IAnswerView, OPTION_TAG } from '../types/feedback.types';
import { CaseDetail } from '@screens/caseDetails/interface';
import { InteractionStatuses } from '@screens/allCases/interface';
const getLocationLink = (latitude: string, longitude: string): string => {
const link = `https://www.google.com/maps/search/?api=1&query=${latitude},${longitude}`;
return link;
};
const QuestionTypes = {
PTP_DATE: 'PTP Date',
COMMENTS: 'Comments',
AMOUNT_PROMISED: 'Amount Promised',
};
const sendNativeToWhatsapp = (
message: string,
imageUrl: string,
mimeType: string,
caseDetails: { id: string },
agentId: string,
setIsWhatsappSendLoading: (isLoading: boolean) => void
) => {
setIsWhatsappSendLoading(true);
sendFeedbackToWhatsapp(message, imageUrl, mimeType)
.then((response) => {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_SHARE_SUCCESSFUL, {
caseId: caseDetails?.id,
agentId: agentId,
});
setIsWhatsappSendLoading(false);
})
.catch((error) => {
setIsWhatsappSendLoading(false);
if (error.message === '1') {
toast({
text1: ToastMessages.WHATSAPP_NOT_INSTALLED,
type: 'error',
});
} else {
toast({
text1: ToastMessages.WHATSAPP_FEEDBACK_SHARE_FAILURE,
type: 'error',
});
}
});
};
const sendToWhatsapp = (
feedbackItem: {
createdAt: string | number | Date;
interactionStatus: keyof typeof InteractionStatuses;
answerViews: IAnswerView[];
metadata: { interactionLatitude: string; interactionLongitude: string };
type: FEEDBACK_TYPE;
},
caseDetails: {
id: string;
customerName?: string;
loanAccountNumber?: string;
dpdBucket?: string;
currentOutstandingEmi?: number;
},
agentId: string,
setIsWhatsappSendLoading: (isLoading: boolean) => void
) => {
setIsWhatsappSendLoading(true);
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_SHARE_FEEDBACK_CLICKED, {
caseId: caseDetails?.id,
agentId: agentId,
});
let message = `*Visit Feedback* for ${sanitizeString(caseDetails?.customerName)}
_${sanitizeString(dateFormat(new Date(feedbackItem?.createdAt), 'DD MMM, YYYY | HH:mm a.'))}_\n
*LAN*: ${sanitizeString(caseDetails?.loanAccountNumber)}
*DPD Bucket*: ${sanitizeString(caseDetails?.dpdBucket)}
*EMI Amount*: ₹${getSanitizedCommaAmount(caseDetails?.currentOutstandingEmi)}\n
*Disposition*: ${sanitizeString(feedbackItem?.interactionStatus)}`;
const ptpDate = feedbackItem?.answerViews.filter(
(answer) => answer.questionName === QuestionTypes.PTP_DATE
);
if (ptpDate.length > 0) {
message +=
' for ' +
sanitizeString(dateFormat(new Date(ptpDate[0].inputDate as string), 'DD MMM, YYYY')) +
'\n\n';
} else {
message += '\n\n';
}
const ptpAmount = feedbackItem?.answerViews?.filter(
(amount) => amount.questionName === QuestionTypes.AMOUNT_PROMISED
);
if (ptpAmount.length > 0) {
message += '*PTP Amount*: ₹' + getSanitizedCommaAmount(ptpAmount[0].inputAmount) + '\n\n';
}
message += '*Remarks*: ';
const answerList = feedbackItem?.answerViews?.filter(
(answer) => answer.questionName === QuestionTypes.COMMENTS
);
if (answerList.length > 0) {
message += sanitizeString(answerList[0]?.inputText) + '\n\n';
} else {
message += 'N.A.\n\n';
}
if (
feedbackItem?.metadata?.interactionLatitude &&
feedbackItem?.metadata?.interactionLongitude &&
FIELD_FEEDBACKS.includes(feedbackItem?.type)
) {
message +=
'*Location of feedback*: ' +
sanitizeString(
getLocationLink(
feedbackItem?.metadata?.interactionLatitude,
feedbackItem?.metadata?.interactionLongitude
)
) +
'\n\n';
}
const imagesList = feedbackItem?.answerViews.filter(
(answer) => answer.questionTag === OPTION_TAG.IMAGE_UPLOAD
);
let imageUrl = '';
const mimeType = 'image/*';
if (imagesList.length > 0) {
let imageUri = '';
imageUri = imagesList[0]?.inputText ? imagesList[0].inputText : '';
let imagePath = '';
ReactNativeBlobUtil.config({
fileCache: true,
})
.fetch('GET', imageUri)
.then((resp) => {
if (resp.info().status !== 200) {
return '';
} else {
imagePath = resp.path();
return resp.readFile('base64');
}
})
.then((base64Data) => {
imageUrl = base64Data;
sendNativeToWhatsapp(
message,
imageUrl,
mimeType,
caseDetails,
agentId,
setIsWhatsappSendLoading
);
ReactNativeBlobUtil.fs.unlink(imagePath);
});
} else {
sendNativeToWhatsapp(
message,
imageUrl,
mimeType,
caseDetails,
agentId,
setIsWhatsappSendLoading
);
}
};
export const shareToWhatsapp = (
feedbackItem: {
createdAt: string | number | Date;
interactionStatus: keyof typeof InteractionStatuses;
answerViews: IAnswerView[];
metadata: { interactionLatitude: string; interactionLongitude: string };
type: FEEDBACK_TYPE;
},
caseDetails: {
id: string;
customerName?: string;
loanAccountNumber?: string;
dpdBucket?: string;
currentOutstandingEmi?: number;
},
agentId: string,
setIsWhatsappSendLoading: (isLoading: boolean) => void
) => sendToWhatsapp(feedbackItem, caseDetails, agentId, setIsWhatsappSendLoading);

View File

@@ -1,6 +1,7 @@
import { GenericType } from '../common/GenericTypes';
import { Address, InteractionStatuses } from '../screens/allCases/interface';
import { IGeolocation, PhoneNumberSource, VisitType } from '../screens/caseDetails/interface';
import { IAddress } from './addressGeolocation.types';
export enum AnswerType {
TEXT = 'TEXT',
@@ -78,6 +79,7 @@ export interface IUnSyncedFeedbackItem {
interactionStatus: InteractionStatuses;
createdAt: string;
isSynced: boolean;
type?: FEEDBACK_TYPE;
}
export const CALLING_FEEDBACKS = [FEEDBACK_TYPE.CALL_BRIDGE, FEEDBACK_TYPE.SELF_CALL];