This commit is contained in:
yashmantri
2023-10-17 17:25:03 +05:30
32 changed files with 678 additions and 243 deletions

View File

@@ -313,7 +313,7 @@ dependencies {
implementation "com.github.anrwatchdog:anrwatchdog:1.4.0"
implementation 'com.navi.medici:alfred:v1.0.1'
implementation 'com.navi.medici:alfred:v1.0.2'
//noinspection GradleDynamicVersion
implementation "com.facebook.react:react-native:+" // From node_modules

View File

@@ -35,6 +35,7 @@
<queries>
<package android:name="com.whatsapp" />
<package android:name="com.whatsapp.w4b" />
</queries>
<uses-permission android:name="android.permission.USE_EXACT_ALARM"/>
<application

View File

@@ -21,6 +21,9 @@ import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.uimanager.NativeViewHierarchyManager;
import com.facebook.react.uimanager.UIBlock;
import com.facebook.react.uimanager.UIManagerModule;
import com.navi.alfred.AlfredManager;
import android.content.pm.PackageInfo;
@@ -28,6 +31,7 @@ import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Environment;
import android.os.Handler;
import android.os.Parcelable;
import android.util.Base64;
import android.os.Looper;
@@ -39,6 +43,7 @@ import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -71,7 +76,7 @@ public class DeviceUtilsModule extends ReactContextBaseJavaModule {
@Override
public void onActivityResult(Activity activity, int i, int i1, @Nullable Intent intent) {
if(i1 != RESULT_CANCELED) {
if (i == WHATSAPP_SHARE_REQUEST_CODE) {
if (i == WHATSAPP_SHARE_REQUEST_CODE && (imageFile!=null)) {
new File(Uri.fromFile(imageFile).getPath()).delete();
}
}
@@ -117,6 +122,10 @@ public class DeviceUtilsModule extends ReactContextBaseJavaModule {
promise.reject(err);
}
}
@ReactMethod
public void sendBottomSheetOpenSignal(Boolean isBottomSheetOpen) {
}
@ReactMethod
public void setUserId(String userId) {
@@ -154,16 +163,29 @@ public class DeviceUtilsModule extends ReactContextBaseJavaModule {
}
@ReactMethod
public void sendBottomSheetOpenSignal(Boolean isBottomSheetOpen) {
if (isBottomSheetOpen) {
View bottomSheetScreen = LayoutInflater.from(RNContext).inflate(R.layout.bottom_sheet_screen, null);
AlfredManager.INSTANCE.measureInflatedView(bottomSheetScreen, 1080, 540);
AlfredManager.INSTANCE.setCosmosBottomSheet(bottomSheetScreen);
} else {
AlfredManager.INSTANCE.setCosmosBottomSheet(null);
public void setBottomSheetView(Integer refID) {
if (refID != null) {
UIManagerModule uiManagerModule = RNContext.getNativeModule(UIManagerModule.class);
if (uiManagerModule != null) {
try {
uiManagerModule.addUIBlock(nativeViewHierarchyManager -> {
Log.d("Alfred", "setBottomSheetView nativeViewHierarchyManager:" + nativeViewHierarchyManager);
View view = nativeViewHierarchyManager.resolveView(refID);
Log.d("Alfred", "setBottomSheetView view:" + view);
AlfredManager.INSTANCE.setBottomSheetView(view);
});
} catch (Exception error) {
Log.d("Alfred", "setBottomSheetView error:" + error);
}
}
}
return;
}
@ReactMethod
public void clearBottomSheet() {
AlfredManager.INSTANCE.clearBottomSheetView();
}
private static File convertBase64ToFile(Context context,String base64Data) {
try {
byte[] decodedBytes = Base64.decode(base64Data, Base64.DEFAULT);
@@ -180,50 +202,87 @@ public class DeviceUtilsModule extends ReactContextBaseJavaModule {
}
}
public boolean isWhatsAppInstalled() {
public ArrayList<String> isWhatsAppInstalled() {
PackageManager packageManager = RNContext.getPackageManager();
List<PackageInfo> packages = packageManager.getInstalledPackages(PackageManager.GET_META_DATA);
ArrayList<String> appsInstalled = new ArrayList<String>();
for (PackageInfo packageInfo : packages) {
String packageName = packageInfo.packageName;
if(packageName.equals("com.whatsapp")){
return true;
if(packageName.equals("com.whatsapp") || packageName.equals("com.whatsapp.w4b")){
appsInstalled.add(packageName);
}
}
return false;
return appsInstalled;
}
public Intent getWhatsappShareIntent(String message, String imageUrl, String mimeType, String packageName) {
Intent sendIntent = new Intent();
sendIntent.setAction(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_TEXT, message);
if (imageUrl.equals("")) {
sendIntent.setType("text/plain");
sendIntent.setPackage(packageName);
} else {
sendIntent.setType(mimeType);
sendIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
imageFile = convertBase64ToFile(getReactApplicationContext(), imageUrl);
Uri fileUri = FileProvider.getUriForFile(getReactApplicationContext(), BuildConfig.APPLICATION_ID + ".provider", new File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
imageFile.getName()
)
);
sendIntent.putExtra(Intent.EXTRA_STREAM, fileUri);
sendIntent.setPackage(packageName);
}
return sendIntent;
}
@ReactMethod
public void sendFeedbackToWhatsapp(String message, String imageUrl, String mimeType, Promise promise) {
try{
if(!isWhatsAppInstalled()){
ArrayList<String> appsInstalled = isWhatsAppInstalled();
int numberOfAppsInstalled = appsInstalled.size();
if(numberOfAppsInstalled == 0){
promise.reject("errorCode", "1");
return;
}
Intent sendIntent = new Intent();
sendIntent.setAction(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_TEXT, message);
if(imageUrl.equals("")) {
sendIntent.setType("text/plain");
sendIntent.setPackage("com.whatsapp");
getCurrentActivity().startActivity(sendIntent);
} else {
sendIntent.setType(mimeType);
sendIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
imageFile = convertBase64ToFile(getReactApplicationContext(), imageUrl);
Uri fileUri = FileProvider.getUriForFile(getReactApplicationContext(), BuildConfig.APPLICATION_ID + ".provider", new File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
imageFile.getName()
)
);
sendIntent.putExtra(Intent.EXTRA_STREAM, fileUri);
sendIntent.setPackage("com.whatsapp");
getCurrentActivity().startActivityForResult(sendIntent, WHATSAPP_SHARE_REQUEST_CODE);
else if(numberOfAppsInstalled == 1) {
String packageName = appsInstalled.get(0);
Intent sendIntent = getWhatsappShareIntent(message, imageUrl, mimeType, packageName);
if(getCurrentActivity()!=null) {
getCurrentActivity().startActivityForResult(sendIntent, WHATSAPP_SHARE_REQUEST_CODE);
}
promise.resolve(true);
return;
}
else {
String packageName1 = appsInstalled.get(0);
String packageName2 = appsInstalled.get(1);
//Firing two intents, one for WhatsApp, another for WhatsApp business
Intent sendIntent1 = getWhatsappShareIntent(message, imageUrl, mimeType, packageName1);
Intent sendIntent2 = getWhatsappShareIntent(message, imageUrl, mimeType, packageName2);
ArrayList<Intent> appIntents = new ArrayList<>();
appIntents.add(sendIntent1);
appIntents.add(sendIntent2);
Intent defaultIntent = new Intent(android.content.Intent.ACTION_SEND);
defaultIntent.setType("text/plain");
defaultIntent.putExtra(android.content.Intent.EXTRA_TEXT, "Sharing to WhatsApp");
Intent chooserIntent = Intent.createChooser(defaultIntent, "Share via");
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, appIntents.toArray(new Parcelable[appIntents.size()]));
if (getCurrentActivity() != null) {
getCurrentActivity().startActivityForResult(chooserIntent, WHATSAPP_SHARE_REQUEST_CODE);
}
promise.resolve(true);
return;
}
promise.resolve(true);
return;
} catch (Error e){
promise.reject("errorCode","2");

View File

@@ -16,7 +16,7 @@ module.exports = {
'@constants': './src/constants',
'@screens': './src/screens',
'@services': './src/services',
'@types': './src/types',
'@interfaces': './src/types',
'@common': './src/common',
'@assets': './src/assets',
'@store': './src/store/store',

View File

@@ -2,24 +2,28 @@ import React from 'react';
import BottomSheet, {
BottomSheetProps,
} from '../../RN-UI-LIB/src/components/bottom_sheet/BottomSheet';
import { sendBottomSheetOpenSignal } from '../components/utlis/DeviceUtils';
import { NativeSyntheticEvent } from 'react-native';
import { clearBottomSheet, setBottomSheetView } from '../components/utlis/DeviceUtils';
interface IBottomSheetWrapperProps extends BottomSheetProps {}
const BottomSheetWrapper: React.FC<IBottomSheetWrapperProps> = (props) => {
const { children, onShow, onSwipeDownClose, onClose, ...restProps } = props;
const onCloseHandler = () => {
sendBottomSheetOpenSignal(false);
clearBottomSheet();
if (typeof onClose === 'function') onClose();
};
const onShowHandler = (event: NativeSyntheticEvent<any>) => {
sendBottomSheetOpenSignal(true);
if (typeof onShow === 'function') onShow(event);
const onAnimationEndHandler = (id: number | null) => {
if (!id) return;
setBottomSheetView(id);
};
return (
<BottomSheet onShow={onShowHandler} onClose={onCloseHandler} {...restProps}>
<BottomSheet
onShow={onShow}
onClose={onCloseHandler}
onBottomSheetAnimationEnd={onAnimationEndHandler}
{...restProps}
>
{children}
</BottomSheet>
);

View File

@@ -638,6 +638,23 @@ export const CLICKSTREAM_EVENT_NAMES = {
name: 'FA_PERFORMANCE_DASHBOARD_BROKEN_PTP_CASES_LOAD',
description: 'Performance Dashboard Broken PTP Cases Load',
},
// Nearby Cases
FA_NEARBY_CASES_BUTTON_CLICKED: {
name: 'FA_NEARBY_CASES_BUTTON_CLICKED',
description: 'FA_NEARBY_CASES_BUTTON_CLICKED',
},
FA_NEARBY_CASES_SCREEN_LOADED: {
name: 'FA_NEARBY_CASES_SCREEN_LOADED',
description: 'FA_NEARBY_CASES_SCREEN_LOADED',
},
FA_NEARBY_CASES_SCREEN_CLOSED: {
name: 'FA_NEARBY_CASES_SCREEN_CLOSED',
description: 'FA_NEARBY_CASES_SCREEN_CLOSED',
},
FA_NEARBY_CASE_CLICKED: {
name: 'FA_NEARBY_CASE_CLICKED',
description: 'FA_NEARBY_CASE_CLICKED',
},
} as const;
export enum MimeType {

View File

@@ -1,19 +1,27 @@
import React from 'react';
import Dropdown, { IDropdown } from '../../RN-UI-LIB/src/components/dropdown/Dropdown';
import { sendBottomSheetOpenSignal } from '../components/utlis/DeviceUtils';
import {
clearBottomSheet,
sendBottomSheetOpenSignal,
setBottomSheetView,
} from '../components/utlis/DeviceUtils';
const DropDownWrapper: React.FC<IDropdown> = (props) => {
const { onShow, onClose, children, ...remainingProps } = props;
const onShowHandler = () => {
if (typeof onShow === 'function') onShow();
sendBottomSheetOpenSignal(true);
};
const { onShow, onClose, onAnimationEnd, children, ...remainingProps } = props;
const onCloseHandler = () => {
if (typeof onClose === 'function') onClose();
sendBottomSheetOpenSignal(false);
clearBottomSheet();
};
const onAnimationEndHandler = (id: number | null) => {
if (!id) return;
console.log('dropdown opened', id);
setBottomSheetView(id);
};
return (
<Dropdown onShow={onShowHandler} onClose={onCloseHandler} {...remainingProps}>
<Dropdown onClose={onCloseHandler} onAnimationEnd={onAnimationEndHandler} {...remainingProps}>
{children}
</Dropdown>
);

View File

@@ -0,0 +1,45 @@
import React, { useEffect } from 'react';
import { Modal, NativeSyntheticEvent, View, findNodeHandle } from 'react-native';
import { IModalWrapper } from '../../RN-UI-LIB/src/components/modalWrapper/ModalWrapper';
import {
clearBottomSheet,
sendBottomSheetOpenSignal,
setBottomSheetView,
} from '../components/utlis/DeviceUtils';
import { GenericStyles } from '@rn-ui-lib/styles';
const ModalWrapperForAlfredV2: React.FC<IModalWrapper> = ({ children, ...props }) => {
const { onRequestClose, onShow, visible } = props;
const modalRef = React.useRef<View>(null);
const lastSent = React.useRef(visible);
const onRequestCloseHandler = (event: NativeSyntheticEvent<any>) => {
if (typeof onRequestClose === 'function') onRequestClose(event);
clearBottomSheet();
};
const onShowHandler = (event: NativeSyntheticEvent<any>) => {
if (typeof onShow === 'function') onShow(event);
const nodeId = findNodeHandle(modalRef.current);
lastSent.current = true;
setBottomSheetView(nodeId);
};
return (
<Modal
transparent={false}
onShow={onShowHandler}
animationType="none"
onRequestClose={onRequestCloseHandler}
{...props}
>
<View
ref={modalRef}
collapsable={false}
style={[GenericStyles.fill, GenericStyles.whiteBackground]}
>
{children}
</View>
</Modal>
);
};
export default ModalWrapperForAlfredV2;

View File

@@ -16,13 +16,13 @@ export const getVisitedWidgetsNodeList = (
let visitedWidgetsNodeList: string[] = [startingWidgetName];
let nextScreenName = '';
const MAX_WIDGET_SIZE = Object.keys(templateData.widget).length;
const MAX_WIDGET_SIZE = Object.keys(templateData?.widget || {}).length;
let iteration = 0;
while (nextScreenName !== CommonCaseWidgetId.END && iteration++ <= MAX_WIDGET_SIZE) {
const currentScreenName = visitedWidgetsNodeList[visitedWidgetsNodeList.length - 1];
const currentScreenName = visitedWidgetsNodeList?.[visitedWidgetsNodeList?.length - 1];
nextScreenName = getNextWidget(
templateData.widget[currentScreenName].conditionActions,
templateData?.widget?.[currentScreenName]?.conditionActions,
formWidgetContext
);
visitedWidgetsNodeList.push(nextScreenName);

View File

@@ -32,9 +32,14 @@ const FiltersContainer: React.FC<FilterContainerProps> = (props) => {
React.useState<Record<string, any>>(selectedFilters);
const filterGroupKeys = Object.keys(filters);
const filterKeys: Record<string, string[]> = {};
filterGroupKeys.forEach((filterGroupKey) => {
filterKeys[filterGroupKey] = Object.keys(filters[filterGroupKey].filters);
const visibleFilters = Object.keys(filters?.[filterGroupKey]?.filters || {}).filter((key) => {
if (filters?.[filterGroupKey]?.filters?.[key]?.visible) return key;
});
filterKeys?.[filterGroupKey] = visibleFilters;
});
const [selectedFilterKey, setSelectedFilterKey] = React.useState<ISelectedFilterKey>({
filterGroup: (filterGroupKeys && filterGroupKeys.length > 0 && filterGroupKeys[0]) || '',
filterKey:
@@ -43,6 +48,7 @@ const FiltersContainer: React.FC<FilterContainerProps> = (props) => {
filterKeys &&
filterKeys[filterGroupKeys[0]][0]) ||
'',
isSearchable: false,
});
const [filterSearchString, setFilterSearchString] = React.useState<string>('');
const dispatch = useAppDispatch();
@@ -133,19 +139,21 @@ const FiltersContainer: React.FC<FilterContainerProps> = (props) => {
<ScrollView>
{filterGroupKeys.map((filterGroupKey) => (
<>
<View
style={[
styles.silverBackground,
GenericStyles.row,
GenericStyles.alignCenter,
GenericStyles.spaceBetween,
styles.p8,
]}
>
<Heading type="h5" bold dark>
{filters[filterGroupKey].headerText}
</Heading>
</View>
{filterGroupKeys.length > 1 && (
<View
style={[
styles.silverBackground,
GenericStyles.row,
GenericStyles.alignCenter,
GenericStyles.spaceBetween,
styles.p8,
]}
>
<Heading type="h5" bold dark>
{filters?.[filterGroupKey]?.headerText}
</Heading>
</View>
)}
<View style={[styles.p8]}>
{filterKeys[filterGroupKey].map((filterKey) =>
filters[filterGroupKey].filters[filterKey]?.visible !== false ? (
@@ -163,6 +171,8 @@ const FiltersContainer: React.FC<FilterContainerProps> = (props) => {
setSelectedFilterKey({
filterGroup: filterGroupKey,
filterKey,
isSearchable:
filters?.[filterGroupKey]?.filters?.[filterKey]?.searchEnabled,
});
setFilterSearchString('');
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.AV_FILTERS_TAB_CLICKED, {
@@ -210,15 +220,17 @@ const FiltersContainer: React.FC<FilterContainerProps> = (props) => {
<View style={[styles.p8, styles.filterColumn]}>
{selectedFilterKey && (
<>
<View style={[GenericStyles.pt16, styles.ph7]}>
<TextInput
LeftComponent={<SearchIcon />}
placeholder="Search..."
defaultValue={filterSearchString}
onChangeText={handleOptionFilterChange}
testID="test_search"
/>
</View>
{selectedFilterKey.isSearchable && (
<View style={[GenericStyles.pt16, styles.ph7]}>
<TextInput
LeftComponent={<SearchIcon />}
placeholder="Search..."
defaultValue={filterSearchString}
onChangeText={handleOptionFilterChange}
testID="test_search"
/>
</View>
)}
<FilterOptions
selectedFilterKey={selectedFilterKey}
filterSearchString={filterSearchString}

View File

@@ -7,6 +7,7 @@ export interface FilterContainerProps {
export interface ISelectedFilterKey {
filterGroup: string;
filterKey: string;
isSearchable: boolean;
}
export interface IFilterOptionsProps {

View File

@@ -29,6 +29,10 @@ export const alfredSetUserId = (userId: string) => DeviceUtilsModule.setUserId(u
export const sendBottomSheetOpenSignal = (e: boolean) =>
DeviceUtilsModule.sendBottomSheetOpenSignal(e);
export const setBottomSheetView = (id: number | null) => DeviceUtilsModule.setBottomSheetView(id);
export const clearBottomSheet = () => DeviceUtilsModule.clearBottomSheet();
export const alfredSetEmailId = (emailId: string) => DeviceUtilsModule.setEmailId(emailId);
// sends feedback data to whatsapp.

View File

@@ -1,7 +1,8 @@
import { Permission, PermissionsAndroid, Platform } from 'react-native';
import { AppState, Permission, PermissionsAndroid, Platform } from 'react-native';
import { PermissionsToCheck } from '../../common/Constants';
import { checkNotifications } from 'react-native-permissions';
import CosmosForegroundService from '../../services/foregroundServices/foreground.service';
import { AppStates } from '@types/appStates';
let isNotificationPermissionEnabled = true;
@@ -32,6 +33,7 @@ export const getPermissionsToRequest = async () => {
if (permission === PermissionsAndroid.PERMISSIONS.POST_NOTIFICATION) {
const notificationPermission = await checkNotifications();
const notificationStatus = notificationPermission.status === 'granted';
isNotificationPermissionEnabled = notificationStatus;
if (!notificationStatus) {
permissionsToRequest.push(permission);
} else {
@@ -43,7 +45,6 @@ export const getPermissionsToRequest = async () => {
CosmosForegroundService.update();
}
}
isNotificationPermissionEnabled = notificationStatus;
continue;
}
const granted = await PermissionsAndroid.check(permission);

View File

@@ -353,7 +353,7 @@ export function getDistanceFromLatLonInKm(
latLong1: IGeolocationCoordinate,
latLong2: IGeolocationCoordinate
) {
if (!latLong1.latitude || !latLong1.longitude || !latLong2.latitude || !latLong2.longitude)
if (!latLong1?.latitude || !latLong1?.longitude || !latLong2?.latitude || !latLong2?.longitude)
return NaN;
const EARTH_RADIUS = 6371;
@@ -368,10 +368,3 @@ export function getDistanceFromLatLonInKm(
const distance = 2 * Math.atan2(Math.sqrt(intermediateResult), Math.sqrt(1 - intermediateResult));
return EARTH_RADIUS * distance;
}
export function insertCommasinAmount(amount: number | undefined) {
const reversedAmount = amount?.toString().split('').reverse().join('');
const groups = reversedAmount?.match(/.{1,3}/g);
const result = groups?.join(',').split('').reverse().join('');
return result;
}

View File

@@ -5,7 +5,7 @@ export const initCrashlytics = async (userState: IUserSlice) => {
if (!userState) return;
await Promise.all([
crashlytics().setUserId(userState.user?.emailId as string),
crashlytics().setUserId((userState.user?.emailId || '') as string),
crashlytics().setAttributes({
deviceId: userState.deviceId,
phoneNumber: userState.user?.phoneNumber as string,

View File

@@ -1,12 +1,12 @@
import React from 'react';
const useRefresh = (refreshAction: () => void) => {
const useRefresh = (refreshAction: () => void, refreshTime = 2000) => {
const [refreshing, setRefreshing] = React.useState(false);
const onRefresh = () => {
setRefreshing(true);
refreshAction();
setTimeout(() => setRefreshing(false), 2000);
setTimeout(() => setRefreshing(false), refreshTime);
};
return { refreshing, onRefresh };

View File

@@ -73,16 +73,15 @@ function AddressItem({
let relativeDistanceBwLatLong = 0;
if (isGroupedAddress) {
const addressGeolocationCoordinated: IGeolocationCoordinate = {
latitude: addressItem.latitude,
longitude: addressItem.longitude,
};
relativeDistanceBwLatLong = getDistanceFromLatLonInKm(
currentGeolocationCoordinates,
addressGeolocationCoordinated
);
}
const addressGeolocationCoordinated: IGeolocationCoordinate = {
latitude: addressItem.latitude,
longitude: addressItem.longitude,
};
relativeDistanceBwLatLong = getDistanceFromLatLonInKm(
currentGeolocationCoordinates,
addressGeolocationCoordinated
);
const handleAddFeedback = () => {
if (prefilledAddressScreenTemplate != null) {
const addressKey = '{{addressReferenceId}}';
@@ -138,22 +137,24 @@ function AddressItem({
<View
style={[styles.container, GenericStyles.columnDirection, containerStyle, , { flex: 1 }]}
>
<View style={[styles.container, GenericStyles.row, { alignItems: 'center' }]}>
<Text
numberOfLines={1}
ellipsizeMode="tail"
style={[styles.textContainer, styles.cardBoldTitle, { fontWeight: 'bold' }]}
>
{sanitizeString([addressItem?.pinCode, addressItem?.city].filter(Boolean).join(', '))}
</Text>
<Text numberOfLines={1} ellipsizeMode="tail" style={[GenericStyles.ml4]}>
{showRelativeDistance && relativeDistanceBwLatLong ? (
<>({relativeDistanceFormatter(relativeDistanceBwLatLong)} km away)</>
) : (
'--'
)}
</Text>
</View>
{addressItem?.pinCode || addressItem?.city || relativeDistanceBwLatLong ? (
<View style={[styles.container, GenericStyles.row, { alignItems: 'center' }]}>
<Text
numberOfLines={1}
ellipsizeMode="tail"
style={[styles.textContainer, styles.cardBoldTitle, { fontWeight: 'bold' }]}
>
{sanitizeString(
[addressItem?.pinCode ?? '', addressItem?.city ?? ''].filter(Boolean).join(', ')
)}
</Text>
<Text numberOfLines={1} ellipsizeMode="tail" style={[GenericStyles.ml4]}>
{showRelativeDistance && relativeDistanceBwLatLong ? (
<>({relativeDistanceFormatter(relativeDistanceBwLatLong)} km away)</>
) : null}
</Text>
</View>
) : null}
{lastFeedbackForAddress?.feedbackPresent ? (
<View style={[styles.container, { marginVertical: 8 }]}>
<View>

View File

@@ -29,6 +29,7 @@ import { type GenericFunctionArgs } from '../../common/GenericTypes';
import { toast } from '../../../RN-UI-LIB/src/components/toast';
import { ToastMessages } from '../allCases/constants';
import AddressSource from './AddressSource';
import relativeDistanceFormatter from './utils/relativeDistanceFormatter';
interface IAddressItem {
addressItem: IAddress;
@@ -167,8 +168,10 @@ function SimilarAddressItem({
{showRelativeDistance && relativeDistanceBwLatLong ? (
<>
<Text style={GenericStyles.tiny}>&nbsp;&nbsp;&#9679;&nbsp;&nbsp;</Text>
{!isNaN(relativeDistanceBwLatLong) ? relativeDistanceBwLatLong.toFixed(2) : '--'} km
away
{!isNaN(relativeDistanceBwLatLong)
? relativeDistanceFormatter(relativeDistanceBwLatLong)
: '--'}{' '}
km away
</>
) : null}
{showSource ? <AddressSource addressItem={addressItem} /> : null}

View File

@@ -4,10 +4,10 @@ const relativeDistanceFormatter = (relativeDistance: number) => {
return '--';
}
if (relativeDistance >= MAXIMUM_DISTANCE_WITH_DECIMAL) {
return Math.round(relativeDistance, 0);
return Math.round(relativeDistance);
}
return relativeDistance.toFixed(2);
return relativeDistance.toFixed(1);
};
export default relativeDistanceFormatter;

View File

@@ -1,12 +1,18 @@
import React, { useMemo } from 'react';
import { Text, View, ViewProps, StyleSheet } from 'react-native';
import { GenericStyles } from '../../../RN-UI-LIB/src/styles';
import { Text, View, ViewProps, StyleSheet, Pressable } from 'react-native';
import { GenericStyles, getShadowStyle } 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';
import { PageRouteEnum } from '@screens/auth/ProtectedRouter';
import { COLORS } from '@rn-ui-lib/colors';
import LocationIcon from '@assets/icons/LocationIcon';
import ArrowRightOutlineIcon from '@rn-ui-lib/icons/ArrowRightOutlineIcon';
import { addClickstreamEvent } from '@services/clickstreamEventService';
import { CLICKSTREAM_EVENT_NAMES } from '@common/Constants';
interface ICaseItemProps extends ViewProps {
caseDetailObj: ICaseItemCaseDetailObj;
@@ -16,6 +22,7 @@ interface ICaseItemProps extends ViewProps {
shouldBatchAvatar?: boolean;
allCasesView?: boolean;
isAgentDashboard?: boolean;
nearbyCaseView?: boolean;
}
const CaseItem: React.FC<ICaseItemProps> = ({
@@ -26,9 +33,10 @@ const CaseItem: React.FC<ICaseItemProps> = ({
shouldBatchAvatar = false,
allCasesView = false,
isAgentDashboard = false,
nearbyCaseView = false,
...restProps
}) => {
const { ADD_VISIT_PLAN, ATTEMPTED_CASES } = CaseTypes;
const { ADD_VISIT_PLAN, ATTEMPTED_CASES, NEARBY_CASES } = CaseTypes;
const { attemptedCount, totalPinnedCount } = useAppSelector((state) => ({
totalPinnedCount: state.allCases.pinnedList.length,
attemptedCount: state.allCases.pinnedList.filter(
@@ -42,6 +50,11 @@ const CaseItem: React.FC<ICaseItemProps> = ({
navigateToScreen('Cases');
};
const navigateToNearbyCases = () => {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_NEARBY_CASES_BUTTON_CLICKED);
navigateToScreen(PageRouteEnum.NEARBY_CASES);
};
const getCaseItemCaseDetailObj = useMemo((): ICaseItemCaseDetailObj => {
return caseDetailObj;
}, [
@@ -81,6 +94,19 @@ const CaseItem: React.FC<ICaseItemProps> = ({
</Text>
);
}
case NEARBY_CASES: {
return (
<Pressable onPress={navigateToNearbyCases}>
<View style={styles.nearByCasesContainer}>
<View style={[GenericStyles.row, GenericStyles.alignCenter]}>
<LocationIcon />
<Text style={styles.nearByCasesText}>View nearby cases</Text>
</View>
<ArrowRightOutlineIcon fillColor={COLORS.BACKGROUND.LIGHT} />
</View>
</Pressable>
);
}
default:
return (
<View {...restProps}>
@@ -91,6 +117,7 @@ const CaseItem: React.FC<ICaseItemProps> = ({
isTodoItem={isTodoItem}
allCasesView={allCasesView}
isAgentDashboard={isAgentDashboard}
nearbyCaseView={nearbyCaseView}
/>
</View>
);
@@ -103,5 +130,21 @@ const styles = StyleSheet.create({
paddingBottom: 6,
paddingHorizontal: 2,
},
nearByCasesContainer: {
...GenericStyles.row,
...GenericStyles.alignCenter,
...GenericStyles.spaceBetween,
...getShadowStyle(2),
...GenericStyles.ph12,
...GenericStyles.pv12,
...GenericStyles.br8,
...GenericStyles.mt16,
...GenericStyles.mb12,
...GenericStyles.whiteBackground,
},
nearByCasesText: {
...GenericStyles.pl4,
color: COLORS.TEXT.DARK,
},
});
export default CaseItem;

View File

@@ -56,6 +56,7 @@ import BottomSheetWrapper from '../../common/BottomSheetWrapper';
import { toast } from '../../../RN-UI-LIB/src/components/toast';
import { setFilteredListToast } from '../../reducer/allCasesSlice';
import { getFilterCount, getSelectedFilters } from '../Dashboard/utils';
import ModalWrapperForAlfredV2 from '@common/ModalWrapperForAlfredV2';
export const getItem = (item: Array<ICaseItem>, index: number) => item[index];
export const ESTIMATED_ITEM_SIZE = 250; // Average height of List item
@@ -210,6 +211,8 @@ const CasesList: React.FC<ICasesList> = ({
const filteredCasesListWithCTA = useMemo(() => {
if (!isVisitPlan) {
if (allCasesView && filteredCasesList?.length)
return [ListHeaderItems.NEARBY_CASES as ICaseItem, ...filteredCasesList];
return [...filteredCasesList];
}
if (isLockedVisitPlanStatus) {
@@ -354,7 +357,7 @@ const CasesList: React.FC<ICasesList> = ({
<View style={GenericStyles.ph12}>{listEmptyComponent}</View>
)}
</View>
<ModalWrapperForAlfred
<ModalWrapperForAlfredV2
animationType="slide"
animated
onRequestClose={() => {
@@ -370,7 +373,7 @@ const CasesList: React.FC<ICasesList> = ({
isVisitPlan={isVisitPlan}
isAgentDashboard={isAgentDashboard}
/>
</ModalWrapperForAlfred>
</ModalWrapperForAlfredV2>
<BottomSheetWrapper
HeaderNode={() => (
<View style={[...row, GenericStyles.ph16]}>

View File

@@ -1,4 +1,4 @@
import { StyleSheet, View } from 'react-native';
import { StyleProp, StyleSheet, View, ViewStyle } from 'react-native';
import React from 'react';
import { GenericStyles } from '../../../RN-UI-LIB/src/styles';
import Heading from '../../../RN-UI-LIB/src/components/Heading';
@@ -22,6 +22,8 @@ interface IEmptyList {
isFilterApplied?: boolean;
setShowAgentSelectionBottomSheet?: (val: boolean) => void;
isAgentDashboard?: boolean;
containerStyle?: StyleProp<ViewStyle>;
isNearByCase?: boolean;
}
const EmptyList: React.FC<IEmptyList> = (props) => {
@@ -31,6 +33,8 @@ const EmptyList: React.FC<IEmptyList> = (props) => {
isFilterApplied,
setShowAgentSelectionBottomSheet,
isAgentDashboard,
containerStyle,
isNearByCase,
} = props;
const {
isLockedVisitPlanStatus,
@@ -85,6 +89,9 @@ const EmptyList: React.FC<IEmptyList> = (props) => {
return EmptyListMessages.NO_ACTIVE_ALLOCATIONS_SELECTED_AGENT;
}
if (isNearByCase) {
return EmptyListMessages.NO_NEARBY_CASES_FOUND;
}
return EmptyListMessages.NO_PENDING_CASES;
};
@@ -130,7 +137,7 @@ const EmptyList: React.FC<IEmptyList> = (props) => {
return (
<View>
<View style={[GenericStyles.w100, styles.centerAbsolute]}>
<View style={[GenericStyles.w100, styles.centerAbsolute, containerStyle]}>
{renderIcon()}
<View style={[GenericStyles.mt12, styles.text]}>
<Heading

View File

@@ -33,6 +33,7 @@ import { toast } from '../../../RN-UI-LIB/src/components/toast';
import { COMPLETED_STATUSES, ToastMessages } from './constants';
import { VisitPlanStatus } from '../../reducer/userSlice';
import { PaymentStatus } from '../caseDetails/interface';
import relativeDistanceFormatter from '@screens/addressGeolocation/utils/relativeDistanceFormatter';
interface IListItem {
caseListItemDetailObj: ICaseItemCaseDetailObj;
@@ -41,6 +42,7 @@ interface IListItem {
shouldBatchAvatar?: boolean;
allCasesView?: boolean;
isAgentDashboard?: boolean;
nearbyCaseView?: boolean;
}
const paymentStatusMapping: Record<
@@ -63,7 +65,7 @@ const ListItem: React.FC<IListItem> = (props) => {
isTodoItem,
shouldBatchAvatar,
allCasesView,
isAgentDashboard,
nearbyCaseView,
} = props;
const {
id: caseId,
@@ -79,6 +81,7 @@ const ListItem: React.FC<IListItem> = (props) => {
interactionStatus,
caseVerdict,
totalOverdueAmount,
distanceInKm,
} = caseListItemDetailObj;
const isCollectionCaseType = caseType === CaseAllocationType.COLLECTION_CASE;
@@ -128,6 +131,11 @@ const ListItem: React.FC<IListItem> = (props) => {
screen: getCurrentScreen().name === 'Profile' ? 'Completed Cases' : getCurrentScreen().name, // todo: need to update use router
caseType,
});
if (nearbyCaseView) {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_NEARBY_CASE_CLICKED, {
caseId,
});
}
if (isCollectionCaseType) {
navigateToScreen('collectionCaseDetail', { caseId });
} else {
@@ -179,7 +187,11 @@ const ListItem: React.FC<IListItem> = (props) => {
const caseCompleted = COMPLETED_STATUSES.includes(caseStatus);
const showVisitPlanBtn =
!(caseCompleted || isCaseItemPinnedMainView) && !isTodoItem && !isCompleted && !isTeamLead;
!(caseCompleted || isCaseItemPinnedMainView) &&
!isTodoItem &&
!isCompleted &&
!isTeamLead &&
!nearbyCaseView;
return (
<Pressable onPress={handleCaseClick}>
@@ -211,6 +223,13 @@ const ListItem: React.FC<IListItem> = (props) => {
<Text style={[GenericStyles.fontSize12, styles.visitPlanText]}>In visit plan</Text>
</View>
)}
{nearbyCaseView && distanceInKm && (
<View style={[GenericStyles.absolute, styles.distanceContainer]}>
<Text style={[GenericStyles.fontSize12, styles.distanceText]}>
{relativeDistanceFormatter(distanceInKm)} km away
</Text>
</View>
)}
<View style={[styles.caseItemInfo]}>
<View style={styles.tag}>
{isCollectionCaseType ? (
@@ -334,6 +353,17 @@ const styles = StyleSheet.create({
visitPlanText: {
color: COLORS.TEXT.BLUE,
},
distanceContainer: {
right: 0,
top: 0,
padding: 8,
backgroundColor: COLORS.BACKGROUND.SILVER,
borderBottomLeftRadius: 4,
borderTopRightRadius: 4,
},
distanceText: {
color: COLORS.TEXT.BLACK,
},
});
export default memo(ListItem);

View File

@@ -0,0 +1,100 @@
import React, { useEffect, useState } from 'react';
import { goBack } from '@components/utlis/navigationUtlis';
import { useAppSelector } from '@hooks';
import useRefresh from '@hooks/useRefresh';
import { useIsFocused } from '@react-navigation/native';
import NavigationHeader from '@rn-ui-lib/components/NavigationHeader';
import { GenericStyles } from '@rn-ui-lib/styles';
import { INearbyCaseItemObj } from '@screens/caseDetails/interface';
import { FlashList } from '@shopify/flash-list';
import { ListRenderItemInfo, RefreshControl, StyleSheet, View } from 'react-native';
import CaseItem from './CaseItem';
import { ESTIMATED_ITEM_SIZE, ESTIMATED_LIST_SIZE } from './CasesList';
import EmptyList from './EmptyList';
import { CaseTypes, ICaseItem } from './interface';
import { getNearByCases } from './utils';
import { addClickstreamEvent } from '@services/clickstreamEventService';
import { CLICKSTREAM_EVENT_NAMES } from '@common/Constants';
const NearbyCases = () => {
const { deviceGeolocationCoordinate, caseDetails, pendingList, pinnedList } = useAppSelector(
(state) => ({
deviceGeolocationCoordinate: state.foregroundService?.deviceGeolocationCoordinate,
caseDetails: state.allCases?.caseDetails,
pendingList: state.allCases?.pendingList,
pinnedList: state.allCases?.pinnedList,
})
);
const [caseData, setCaseData] = useState<Array<INearbyCaseItemObj>>([]);
const isFocused = useIsFocused();
const handlePullToRefresh = () => {
const data = getNearByCases(
[...pinnedList, ...pendingList],
caseDetails,
deviceGeolocationCoordinate
);
setCaseData(data);
};
const { refreshing, onRefresh } = useRefresh(handlePullToRefresh, 1000);
useEffect(() => {
if (isFocused) {
onRefresh();
}
return () => {
isFocused && addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_NEARBY_CASES_SCREEN_CLOSED);
};
}, [isFocused]);
const renderListItem = (row: ListRenderItemInfo<ICaseItem>) => {
const caseDetailItem = row.item as INearbyCaseItemObj;
const { type } = row.item;
return (
<CaseItem
key={caseDetailItem?.distanceInKm}
renderingType={type}
caseDetailObj={caseDetailItem}
shouldBatchAvatar={true}
testID={`case-${type === CaseTypes.TODO ? 'todo' : ''}-${row.index}`}
nearbyCaseView
/>
);
};
return (
<View style={[GenericStyles.fill, GenericStyles.relative]}>
<NavigationHeader title="Nearby Cases" onBack={goBack} />
<View style={GenericStyles.fill}>
{caseData.length ? (
<FlashList
data={caseData}
keyboardShouldPersistTaps={'handled'}
scrollEventThrottle={16}
renderItem={renderListItem}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={handlePullToRefresh} />
}
contentContainerStyle={GenericStyles.p12}
estimatedItemSize={ESTIMATED_ITEM_SIZE}
estimatedListSize={ESTIMATED_LIST_SIZE}
/>
) : (
<View style={GenericStyles.ph12}>
<EmptyList containerStyle={styles.pt0} isNearByCase />
</View>
)}
</View>
</View>
);
};
const styles = StyleSheet.create({
pt0: {
paddingTop: 0,
},
});
export default NearbyCases;

View File

@@ -27,6 +27,10 @@ export const ListHeaderItems = {
type: CaseTypes.ATTEMPTED_CASES,
caseReferenceId: '-5',
},
NEARBY_CASES: {
type: CaseTypes.NEARBY_CASES,
caseReferenceId: '-6',
},
};
export const LIST_HEADER_ITEMS = [
@@ -49,6 +53,7 @@ export const EmptyListMessages = {
NO_ACTIVE_ALLOCATIONS_SELECTED_AGENT: 'Selected agent does not have any active allocations',
SELECT_AGENT: 'Select an agent to view cases',
SELECT_AGENT_SELECTED_AGENT: 'Select another agent to view cases',
NO_NEARBY_CASES_FOUND: 'No nearby cases found',
};
export const ToastMessages = {
@@ -88,3 +93,5 @@ export enum BOTTOM_TAB_ROUTES {
Profile = 'Profile',
Dashboard = 'Dashboard',
}
export const NEARBY_CASES_COUNT = 10;

View File

@@ -24,6 +24,7 @@ export enum CaseTypes {
BANNER,
ADD_VISIT_PLAN,
ATTEMPTED_CASES,
NEARBY_CASES,
}
export enum caseVerdict {
@@ -204,6 +205,7 @@ export interface IFilterMeta {
fieldsToCompare: IFieldToCompare[];
operator: CONDITIONAL_OPERATORS;
visible?: boolean;
searchEnabled: boolean;
}
export interface IQuickFilter extends IFilterMeta, Option {}
@@ -322,6 +324,7 @@ export interface ICaseItemAvatarCaseDetailObj extends IFetchDocumentCaseDetailOb
export interface ICaseItemCaseDetailObj extends CaseDetail {
isIntermediateOrSelectedTodoCaseItem?: boolean;
distanceInKm?: number;
}
export interface ISectionListData {

View File

@@ -1,4 +1,9 @@
import { CaseDetail, FeedbackStatus } from '../caseDetails/interface';
import { CLICKSTREAM_EVENT_NAMES } from '@common/Constants';
import { getDistanceFromLatLonInKm } from '@components/utlis/commonFunctions';
import { IGeoLocation } from '@interfaces/addressGeolocation.types';
import { addClickstreamEvent } from '@services/clickstreamEventService';
import { Address, CaseDetail, FeedbackStatus, INearbyCaseItemObj } from '../caseDetails/interface';
import { NEARBY_CASES_COUNT } from './constants';
import { ICaseItem, IReportee, ISectionListData } from './interface';
export const getAttemptedList = (
@@ -59,3 +64,50 @@ export const sectionListTranformData = (agentList: IReportee[]): ISectionListDat
return result;
};
export const getAddressLocation = (addresses: Address[] | undefined) => {
if (!addresses?.length) return null;
for (const address of addresses) {
if (address?.location?.latitude && address?.location?.longitude) {
return address.location;
}
}
return null;
};
export const getNearByCases = (
casesList: Array<ICaseItem>,
caseDetails: Record<string, CaseDetail>,
deviceGeolocationCoordinate: IGeoLocation
) => {
let caseDetailsData: Array<INearbyCaseItemObj> = [];
let caseIds: Array<string> = [];
casesList?.forEach((pinnedId) => {
const caseDetail = caseDetails?.[pinnedId.caseReferenceId];
const addressLocation = getAddressLocation(caseDetail?.addresses);
if (addressLocation) {
const distanceInKm = getDistanceFromLatLonInKm(addressLocation, deviceGeolocationCoordinate);
if (distanceInKm) {
caseIds.push(caseDetail.caseReferenceId);
caseDetailsData.push({
...caseDetail,
distanceInKm: distanceInKm,
});
}
}
});
caseDetailsData?.sort(
(a: INearbyCaseItemObj, b: INearbyCaseItemObj) => a.distanceInKm - b.distanceInKm
);
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_NEARBY_CASES_SCREEN_LOADED, {
userGeolocation: deviceGeolocationCoordinate,
caseIds: caseIds.slice(0, NEARBY_CASES_COUNT),
});
return caseDetailsData?.slice(0, NEARBY_CASES_COUNT);
};

View File

@@ -40,6 +40,7 @@ import { setDeviceGeolocation } from '@reducers/foregroundServiceSlice';
import FullScreenLoader from '../../../RN-UI-LIB/src/components/FullScreenLoader';
import PDFFullScreen from '../caseDetails/PDFFullScreen';
import ImageViewer from '../caseDetails/ImageViewer';
import NearbyCases from '@screens/allCases/NearbyCases';
const Stack = createNativeStackNavigator();
@@ -54,6 +55,7 @@ export enum PageRouteEnum {
CASH_COLLECTED = 'cashCollected',
DASHBOARD_MAIN = 'dashboardMain',
FILTERED_CASES = 'filteredCases',
NEARBY_CASES = 'nearbyCases',
GEOLOCATION_OLD_FEEDBACKS = 'geolocationOldFeedbacks',
}
@@ -193,6 +195,16 @@ const ProtectedRouter = () => {
}}
listeners={getScreenFocusListenerObj}
/>
<Stack.Screen
name={PageRouteEnum.NEARBY_CASES}
component={NearbyCases}
options={{
header: () => null,
animationDuration: SCREEN_ANIMATION_DURATION,
animation: 'slide_from_right',
}}
listeners={getScreenFocusListenerObj}
/>
<Stack.Screen
name={'vkycFull'}
component={VKYCFullScreen}

View File

@@ -10,8 +10,8 @@ import {
} from '../../../../RN-UI-LIB/src/utlis/dates';
import { CaseDetail, Address as IAddress, IGeolocation, VisitType } from '../interface';
import {
debounce,
getGoogleMapUrl,
insertCommasinAmount,
sanitizeString,
} from '../../../components/utlis/commonFunctions';
import {
@@ -33,6 +33,7 @@ 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';
interface IFeedbackDetailItem {
feedbackItem: IFeedback;
@@ -71,122 +72,133 @@ function getLocationLink(latitude: string, longitude: string): string {
return link;
}
const sendToWhatsappNative = (
message: string,
imageUrl: string,
mimeType: string,
caseDetails: CaseDetail,
agentId: string
) => {
sendFeedbackToWhatsapp(message, imageUrl, mimeType)
.then((res: boolean) => {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_SHARE_SUCCESSFUL, {
caseId: caseDetails?.id,
agentId: agentId,
});
})
.catch((err: Error) => {
if (err.message === '1') {
toast({
text1: ToastMessages.WHATSAPP_NOT_INSTALLED,
type: 'error',
});
} else {
toast({
text1: ToastMessages.WHATSAPP_FEEDBACK_SHARE_FAILURE,
type: 'error',
});
}
});
};
const sendToWhatsapp = (feedbackItem: IFeedback, caseDetails: CaseDetail, agentId: string) => {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_SHARE_FEEDBACK_CLICKED, {
caseId: caseDetails?.id,
agentId: agentId,
});
var 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*: ₹${insertCommasinAmount(caseDetails?.outstandingEmiDetails?.[0]?.emiAmount)}\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
);
var imageUrl = '';
const mimeType = 'image/*';
if (imagesList.length > 0) {
var 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 FeedbackDetailItem = ({
feedbackItem,
isExpanded,
caseId,
hideAddress,
}: IFeedbackDetailItem) => {
const isGeolocation = feedbackItem?.source?.sourceType === VisitType.GEOLOCATION;
const caseDetails = useAppSelector((state) => state.allCases.caseDetails[caseId]);
const { agentId } = useAppSelector((state) => ({ agentId: state.user.user?.referenceId!! }));
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 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]}>
@@ -273,8 +285,11 @@ const FeedbackDetailItem = ({
) : null}
<TouchableOpacity
activeOpacity={0.7}
onPress={() => sendToWhatsapp(feedbackItem, caseDetails, agentId)}
onPress={() => {
throttledSendToWhatsapp.current(feedbackItem, caseDetails, agentId);
}}
style={[GenericStyles.row, styles.BtnPadding]}
disabled={isWhastappSendLoading}
>
<IconLabel
text="Share"

View File

@@ -1,3 +1,4 @@
import { IGeolocationCoordinate } from '@interfaces/addressGeolocation.types';
import {
CaseAllocationType,
CaseStatuses,
@@ -80,6 +81,7 @@ export interface Address {
permanent: boolean;
zipCode?: any;
addressQualityStatus?: any;
location: IGeolocationCoordinate;
}
export interface Metadata {
'@class': string;
@@ -347,3 +349,7 @@ export enum PhoneNumberSource {
CRIF = 'CRIF',
CIBIL_CU = 'CIBIL_CU',
}
export interface INearbyCaseItemObj extends CaseDetail {
distanceInKm: number;
}

View File

@@ -1,6 +1,8 @@
import ForegroundService from '@supersami/rn-foreground-service';
import { logError } from '../../components/utlis/errorUtils';
import { GLOBAL } from '../../constants/Global';
import { AppState } from 'react-native';
import { AppStates } from '@types/appStates';
export interface IForegroundTask {
task: () => void;
@@ -34,6 +36,9 @@ class CosmosForegroundService {
private constructor() {}
static async start(tasks?: IForegroundTask[]) {
if (AppState.currentState !== AppStates.ACTIVE) {
return;
}
if (GLOBAL.IS_IMPERSONATED) {
return;
}
@@ -60,6 +65,9 @@ class CosmosForegroundService {
}
static async update() {
if (AppState.currentState !== AppStates.ACTIVE) {
return;
}
if (GLOBAL.IS_IMPERSONATED) {
return;
}

View File

@@ -23,7 +23,7 @@
"@constants/*": ["src/constants/*"],
"@screens/*": ["src/screens/*"],
"@services/*": ["src/services/*"],
"@types/*": ["src/types/*"],
"@interfaces/*": ["src/types/*"],
"@common/*": ["src/common/*"],
"@assets/*": ["src/assets/*"],
"@store": ["src/store/store"],