Merge branch 'TP-41687-blacklisted-apps-v3' of github.com:navi-medici/address-verification-app into TP-41687-blacklisted-apps-v3

This commit is contained in:
ShriPrakashBajpai
2023-10-17 18:13:33 +05:30
23 changed files with 990 additions and 357 deletions

View File

@@ -1,4 +1,4 @@
name: Create-Apk-QA
name: generate-apk
on:
workflow_dispatch:
@@ -8,23 +8,31 @@ on:
required: true
type: choice
options:
- qa
- dev
- QA
- Prod
flavor:
description: Choose build flavour
required: true
type: choice
options:
- fieldAgents
- callingAgents
type:
description: Choose build type
required: true
type: choice
options:
- debug
- release
version_code:
description: Enter app version code (example, 292)
required: false
required: true
type: string
default: "292"
version_name:
description: Enter app version name (example, 3.2.1)
required: false
required: true
type: string
default: "3.2.1"
jobs:
generate:
runs-on: [ default ]
@@ -69,21 +77,21 @@ jobs:
run: chmod +x android/gradlew
- name: Create local.properties
run: cd android && touch local.properties && echo "sdk.dir = /home/USERNAME/Android/Sdk" > local.properties
- name: Assemble with Stacktrace - QA debug
if: ((github.event.inputs.environment == 'qa' || inputs.environment == 'qa') && (github.event.inputs.type == 'debug' || inputs.type == 'debug'))
run: yarn move:qa && yarn debug && cd android && ./gradlew assembleDebug
- name: Assemble with Stacktrace - Dev debug
if: ((github.event.inputs.environment == 'dev' || inputs.environment == 'dev') && (github.event.inputs.type == 'debug' || inputs.type == 'debug'))
run: yarn move:dev && yarn debug && cd android && ./gradlew assembleDebug
- name: Assemble with Stacktrace - QA release
if: ((github.event.inputs.environment == 'qa' || inputs.environment == 'qa') && (github.event.inputs.type == 'release' || inputs.type == 'release'))
run: yarn move:qa && cd android && ./gradlew assembleRelease
- name: Assemble with Stacktrace - Dev release
if: ((github.event.inputs.environment == 'dev' || inputs.environment == 'dev') && (github.event.inputs.type == 'debug' || inputs.type == 'release'))
run: yarn move:dev && cd android && ./gradlew assembleRelease
- name: Assemble with Stacktrace - Field QA release
if: ((github.event.inputs.environment == 'QA' || inputs.environment == 'QA') && (github.event.flavor.type == 'fieldAgents' || inputs.flavor == 'fieldAgents'))
run: yarn move:qa && cd android && ./gradlew assemblefieldAgentsQARelease
- name: Assemble with Stacktrace - Field PROD release
if: ((github.event.inputs.environment == 'Prod' || inputs.environment == 'Prod') && (github.event.flavor.type == 'fieldAgents' || inputs.flavor == 'fieldAgents'))
run: yarn move:prod && cd android && ./gradlew assemblefieldAgentsProdRelease
- name: Assemble with Stacktrace - Calling QA release
if: ((github.event.inputs.environment == 'QA' || inputs.environment == 'QA') && (github.event.flavor.type == 'callingAgents' || inputs.flavor == 'callingAgents'))
run: yarn move:qa && cd android && ./gradlew assemblefieldAgentsQARelease
- name: Assemble with Stacktrace - Calling PROD release
if: ((github.event.inputs.environment == 'Prod' || inputs.environment == 'Prod') && (github.event.flavor.type == 'callingAgents' || inputs.flavor == 'callingAgents'))
run: yarn move:prod && cd android && ./gradlew assemblefieldAgentsProdRelease
- name: Upload APK as Artifact
uses: actions/upload-artifact@v3
with:
name: app-${{ github.event.inputs.type || inputs.type }}
path: android/app/build/outputs/apk/${{ github.event.inputs.type || inputs.type }}/
name: app-${{ github.event.inputs.type || inputs.type }}-v${{ github.event.inputs.version_code || inputs.version_code }}-name-${{github.event.inputs.version_name || inputs.version_name}}
path: android/app/build/outputs/apk/${{ github.event.inputs.flavor || inputs.flavor }}${{github.event.inputs.environment || inputs.environment}}/${{github.event.inputs.type || inputs.type}}
retention-days: 30

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();
}
}
@@ -144,6 +149,10 @@ public class DeviceUtilsModule extends ReactContextBaseJavaModule {
promise.reject(err);
}
}
@ReactMethod
public void sendBottomSheetOpenSignal(Boolean isBottomSheetOpen) {
}
@ReactMethod
public void setUserId(String userId) {
@@ -181,16 +190,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);
@@ -207,50 +229,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

@@ -9,6 +9,13 @@ interface IPastFeedbacksPayload {
addressReferenceIds?: string; // required to fetch past feedback on addresses
}
export interface IFilterPayload {
filters: Array<{
filterName: string;
selectedValue: string[];
}>;
}
interface IPastFeedbacksOnAddressesPayload {
loanAccountNumber: string;
pageNo?: number;
@@ -16,12 +23,13 @@ interface IPastFeedbacksOnAddressesPayload {
addressReferenceIds?: string;
}
export const getPastFeedbacks = (queryParamsPayload: IPastFeedbacksPayload) => {
const url = getApiUrl(ApiKeys.PAST_FEEDBACK);
export const getPastFeedbacks = (
queryParamsPayload: IPastFeedbacksPayload,
filterPayload: IFilterPayload
) => {
const url = getApiUrl(ApiKeys.PAST_FEEDBACK, {}, queryParamsPayload);
return axiosInstance
.get(url, {
params: queryParamsPayload,
})
.post(url, filterPayload)
.then((response) => {
if (response?.data) {
return {

View File

@@ -1,10 +1,13 @@
import * as React from 'react';
import Svg, { Path } from 'react-native-svg';
const FilterIcon = () => (
import { IconProps } from '../../../RN-UI-LIB/src/Icons/types';
import { COLORS } from '../../../RN-UI-LIB/src/styles/colors';
const FilterIcon: React.FC<IconProps> = ({ fillColor = COLORS.BACKGROUND.LIGHT }) => (
<Svg width={16} height={18} viewBox="0 0 16 18" fill="none">
<Path
d="M15.8456 0.780302C15.7312 0.54479 15.5585 0.347411 15.3463 0.209792C15.1341 0.0721637 14.8906 -0.000412239 14.6423 1.76134e-06H1.35695C1.10879 -0.000250239 0.865321 0.0723077 0.653091 0.209774C0.440852 0.34724 0.267994 0.544331 0.153345 0.779573C0.0386958 1.01482 -0.0133408 1.27919 0.00291016 1.54387C0.0191527 1.80855 0.10306 2.06339 0.245487 2.2806L0.265695 2.3112L5.89461 10.2042V17.1C5.89453 17.2629 5.93596 17.4228 6.01426 17.5626C6.09257 17.7024 6.20498 17.8169 6.33937 17.8937C6.47375 17.9706 6.62515 18.007 6.77747 17.9991C6.92971 17.9912 7.07714 17.9393 7.20395 17.8488L9.73 16.0488C9.84527 15.9666 9.93975 15.8552 10.0052 15.7246C10.0705 15.5939 10.1047 15.4481 10.1047 15.3V10.2033L15.7538 2.2833C15.8968 2.06596 15.981 1.81071 15.9971 1.54558C16.0133 1.28045 15.9608 1.01567 15.8456 0.780302Z"
fill="#969696"
fill={fillColor}
/>
</Svg>
);

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

@@ -747,6 +747,7 @@ export const REQUEST_TO_UNBLOCK_FOR_IMPERSONATION = [
getApiUrl(ApiKeys.GET_SIGNED_URL),
getApiUrl(ApiKeys.GET_SIGNED_URL_FOR_REPORTEE),
getApiUrl(ApiKeys.LOGOUT),
getApiUrl(ApiKeys.PAST_FEEDBACK),
];
export const NAVI_AGENCY_CODE = '1000';

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

@@ -0,0 +1,122 @@
import { View } from 'react-native';
import React from 'react';
import { IFilters, TFilterOptions } from './Filters';
import RadioGroup from '../../../RN-UI-LIB/src/components/radio_button/RadioGroup';
import RNRadioButton from '../../../RN-UI-LIB/src/components/radio_button/RadioButton';
import { GenericStyles } from '../../../RN-UI-LIB/src/styles';
import Checkbox from '../../../RN-UI-LIB/src/components/chechbox/Checkbox';
interface IFilterOptions {
filters: IFilters;
selectedFilter: string;
selectedFilterOptions: TFilterOptions;
setSelectedFilterOptions: React.Dispatch<React.SetStateAction<TFilterOptions>>;
}
enum FilterTypes {
RADIO_GROUP = 'RADIO_GROUP',
CHECKBOX_GROUP = 'CHECKBOX_GROUP',
}
const FilterOptions: React.FC<IFilterOptions> = ({
filters,
selectedFilter,
selectedFilterOptions,
setSelectedFilterOptions,
}) => {
if (!selectedFilter || !filters?.[selectedFilter]) {
return null;
}
const handleSingleOptionChange = (value: string) => {
if (selectedFilter) {
const selectedOption = !!selectedFilterOptions?.[selectedFilter]?.filters?.[value];
const currentSelectedFilterCount =
selectedFilterOptions?.[selectedFilter]?.selectedFilterCount || 0;
if (selectedOption && currentSelectedFilterCount === 1) {
// delete the selected filter if we are unchecking the only selected option
setSelectedFilterOptions((prev) => {
const updatedOptions = { ...prev };
delete updatedOptions[selectedFilter];
return updatedOptions;
});
return;
}
setSelectedFilterOptions({
...selectedFilterOptions,
[selectedFilter]: {
filters: {
[value]: !selectedOption,
},
selectedFilterCount: selectedOption ? 0 : 1,
},
});
}
};
const handleMultiOptionChange = (isChecked: boolean, values: string) => {
if (selectedFilter) {
const currentFilters = selectedFilterOptions?.[selectedFilter]?.filters || {};
const currentSelectedFilterCount =
selectedFilterOptions?.[selectedFilter]?.selectedFilterCount || 0;
if (!isChecked && currentSelectedFilterCount === 1) {
// delete the selected filter if we are unchecking the only selected option
setSelectedFilterOptions((prev) => {
const updatedOptions = { ...prev };
delete updatedOptions[selectedFilter];
return updatedOptions;
});
return;
}
setSelectedFilterOptions({
...selectedFilterOptions,
[selectedFilter]: {
filters: {
...currentFilters,
[values]: isChecked,
},
selectedFilterCount: isChecked
? currentSelectedFilterCount + 1
: currentSelectedFilterCount - 1,
},
});
}
};
const filter = filters[selectedFilter];
const selectionType = filter.filterType;
const selectedOptions = selectedFilterOptions[selectedFilter]?.filters || {};
if (selectionType === FilterTypes.RADIO_GROUP) {
const selectedOption = Object.keys(selectedOptions)[0];
return (
<RadioGroup
value={selectedOptions[selectedOption] ? selectedOption : ''}
onValueChange={(value) => handleSingleOptionChange(value)}
orientation="vertical"
>
{filter.options.map((option) => (
<RNRadioButton key={option.value} value={option.label || ''} id={option.value || ''} />
))}
</RadioGroup>
);
} else if (selectionType === FilterTypes.CHECKBOX_GROUP) {
return (
<>
{filter.options.map((option) => (
<View key={option.value} style={GenericStyles.mb2}>
<Checkbox
checked={selectedOptions[option.value] || false}
label={option.label || ''}
onSelectionChange={(value) => handleMultiOptionChange(value, option.value)}
/>
</View>
))}
</>
);
}
return null;
};
export default FilterOptions;

View File

@@ -0,0 +1,196 @@
import React, { useEffect, useState } from 'react';
import { View, ScrollView, StyleSheet, PixelRatio, Pressable } from 'react-native';
import { GenericStyles } from '../../../RN-UI-LIB/src/styles';
import Text from '../../../RN-UI-LIB/src/components/Text';
import Button from '../../../RN-UI-LIB/src/components/Button';
import { COLORS } from '../../../RN-UI-LIB/src/styles/colors';
import { _map } from '../../../RN-UI-LIB/src/utlis/common';
import FilterOptions from './FilterOptions';
import NavigationHeader from '../../../RN-UI-LIB/src/components/NavigationHeader';
interface FilterOption {
value: string;
label: string;
}
interface FilterData {
filterType: string;
displayText: string;
options: FilterOption[];
name: string;
}
export interface IFilters {
[key: string]: FilterData;
}
interface IFilterOptions {
filters: Record<string, boolean>;
selectedFilterCount: number;
}
export type TFilterOptions = Record<string, IFilterOptions | null>;
const Filters: React.FC<{
header: string;
filters: IFilters;
defaultSelectedFilters: TFilterOptions;
closeFilterModal: () => void;
onFilterChange: (selectedFilters: TFilterOptions) => void;
}> = ({ header, filters, defaultSelectedFilters, closeFilterModal, onFilterChange }) => {
// Default select first filter
const [selectedFilter, setSelectedFilter] = useState<string>(Object.keys(filters)[0]);
const [selectedFilterOptions, setSelectedFilterOptions] =
useState<TFilterOptions>(defaultSelectedFilters);
useEffect(() => {
if (defaultSelectedFilters) {
setSelectedFilterOptions(defaultSelectedFilters);
}
}, [defaultSelectedFilters]);
const handleFilterChange = (filterName: string) => {
setSelectedFilter(filterName);
};
const handleSubmit = () => {
onFilterChange(selectedFilterOptions);
closeFilterModal();
};
const handleClearAll = () => {
onFilterChange({});
setSelectedFilterOptions({});
};
return (
<View style={[GenericStyles.fill, GenericStyles.whiteBackground]}>
<NavigationHeader title={header} onBack={closeFilterModal} />
<View style={[GenericStyles.row, GenericStyles.fill]}>
<ScrollView style={styles.leftSection}>
{/* @ts-expect-error */}
{_map(filters, (filterKey) => {
const filter = filters[filterKey];
const filterName = filter.name;
return (
<Pressable
key={filterName}
onPress={() => handleFilterChange(filterName)}
style={[
GenericStyles.row,
GenericStyles.spaceBetween,
GenericStyles.ph12,
{ paddingVertical: 8 },
GenericStyles.br6,
GenericStyles.mb8,
GenericStyles.alignCenter,
{
backgroundColor:
selectedFilter === filterName
? COLORS.BACKGROUND.BLUE
: COLORS.BACKGROUND.PRIMARY,
},
]}
>
<Text
style={[
GenericStyles.fill,
{
color: selectedFilter === filterName ? COLORS.TEXT.BLUE : COLORS.TEXT.BLACK,
},
]}
>
{filter.displayText}
</Text>
{selectedFilterOptions[filterName]?.selectedFilterCount ? (
<View
style={[
styles.filterCountContainer,
selectedFilter === filterName
? styles.filterCountSelected
: styles.filterCount,
]}
>
<Text
style={[selectedFilter === filterName && GenericStyles.whiteText]}
bold
dark
small
>
{selectedFilterOptions[filterName]?.selectedFilterCount}
</Text>
</View>
) : (
<View style={styles.filterCountContainer} />
)}
</Pressable>
);
})}
</ScrollView>
<ScrollView style={styles.rightSection}>
<FilterOptions
filters={filters}
selectedFilter={selectedFilter}
selectedFilterOptions={selectedFilterOptions}
setSelectedFilterOptions={setSelectedFilterOptions}
/>
</ScrollView>
</View>
<View style={GenericStyles.row}>
<Button
variant="secondary"
title="Clear all"
onPress={handleClearAll}
style={GenericStyles.fill}
buttonStyle={styles.btn}
/>
<Button
title="Apply"
onPress={handleSubmit}
style={GenericStyles.fill}
buttonStyle={styles.btn}
/>
</View>
</View>
);
};
const styles = StyleSheet.create({
leftSection: {
width: '47%',
borderRightWidth: 1,
borderRightColor: COLORS.BORDER.PRIMARY,
paddingHorizontal: 8,
paddingVertical: 16,
},
rightSection: {
width: '53%',
paddingVertical: 16,
},
btn: {
margin: 0,
borderRadius: 0,
borderBottomWidth: 0,
borderTopWidth: 1,
borderTopColor: COLORS.BORDER.PRIMARY,
},
filterCountContainer: {
height: PixelRatio.roundToNearestPixel(25),
width: PixelRatio.roundToNearestPixel(25),
},
filterCount: {
backgroundColor: COLORS.BACKGROUND.SILVER,
borderColor: COLORS.BORDER.PRIMARY,
borderWidth: 1,
borderRadius: 20,
alignItems: 'center',
},
filterCountSelected: {
backgroundColor: COLORS.TEXT.BLUE,
borderRadius: 20,
alignItems: 'center',
},
});
export default Filters;

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

@@ -79,7 +79,7 @@ API_URLS[ApiKeys.GET_SIGNED_URL] = '/cases/get-signed-urls';
API_URLS[ApiKeys.GET_SIGNED_URL_FOR_REPORTEE] = '/cases/get-signed-urls-for-reportee';
API_URLS[ApiKeys.CASE_UNIFIED_DETAILS] = '/v3/collection-cases/unified-details/{loanAccountNumber}';
API_URLS[ApiKeys.UNGROUPED_ADDRESSES] = '/addresses/ungrouped-v2/{loanAccountNumber}';
API_URLS[ApiKeys.PAST_FEEDBACK] = '/feedback';
API_URLS[ApiKeys.PAST_FEEDBACK] = '/feedback/filters';
API_URLS[ApiKeys.PAST_FEEDBACK_ON_ADDRESSES] = '/feedback/v2';
API_URLS[ApiKeys.NOTIFICATIONS] = '/notification/fetch';
API_URLS[ApiKeys.NOTIFICATION_ACTION] = '/notification/action';

View File

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

@@ -15,6 +15,7 @@ import { ToastMessages } from '../screens/allCases/constants';
import { setForceUninstallData } from '../reducer/metadataSlice';
import { logError } from '../components/utlis/errorUtils';
import { GenericFunctionArgs } from '../common/GenericTypes';
import { setFeedbackFilterTemplate } from '@reducers/feedbackFiltersSlice';
export interface CaseUpdates {
updateType: string;
@@ -53,6 +54,7 @@ const useFirestoreUpdates = () => {
let filterUnsubscribe: GenericFunctionArgs;
let forceUninstallUnsubscribe: GenericFunctionArgs;
let lockUnsubscribe: GenericFunctionArgs;
let feedbackFiltersUnsubscribe: GenericFunctionArgs;
const dispatch = useAppDispatch();
@@ -154,6 +156,13 @@ const useFirestoreUpdates = () => {
lockData && dispatch(setLockData(lockData));
};
const handleFeedbackFilters = (
snapshot: FirebaseFirestoreTypes.DocumentSnapshot<FirebaseFirestoreTypes.DocumentData>
) => {
const feedbackFilters = snapshot.data();
dispatch(setFeedbackFilterTemplate(feedbackFilters));
};
const handleError = (err: any, collectionPath?: string) => {
const errMsg = `Error while fetching fireStore snapshot: referenceId: ${user?.referenceId} collectionPath: ${collectionPath}`;
logError(err as Error, errMsg);
@@ -217,9 +226,15 @@ const useFirestoreUpdates = () => {
refId = selectedAgent?.referenceId;
}
const collectionPath = `filters/${refId}`;
return subscribeToDoc(handleFilterUpdate, collectionPath);
};
const subscribeToFeedbackFilters = () => {
const feedbackFiltersPath = `feedback-filters/v1`;
return subscribeToDoc(handleFeedbackFilters, feedbackFiltersPath);
};
const subscribeToUserConfig = () => {
const collectionPath = `config/${user?.referenceId}`;
return subscribeToDoc(handleConfigUpdate, collectionPath);
@@ -241,6 +256,7 @@ const useFirestoreUpdates = () => {
avTemplateUnSubscriber = subscribeToAvTemplate();
collectionTemplateUnsubscribe = subscribeToCollectionTemplate();
lockUnsubscribe = subscribeToLocks();
feedbackFiltersUnsubscribe = subscribeToFeedbackFilters();
}
useEffect(() => {
@@ -264,6 +280,7 @@ const useFirestoreUpdates = () => {
avTemplateUnSubscriber && avTemplateUnSubscriber();
collectionTemplateUnsubscribe && collectionTemplateUnsubscribe();
lockUnsubscribe && lockUnsubscribe();
feedbackFiltersUnsubscribe && feedbackFiltersUnsubscribe();
};
}, [isLoggedIn, user?.referenceId]);

View File

@@ -0,0 +1,33 @@
import { TFilterOptions } from '@components/filters/Filters';
import { createSlice } from '@reduxjs/toolkit';
import { IFilter } from '@screens/allCases/interface';
interface IFeedbackFiltersSlice {
feedbackFiltersTemplate: IFilter | null;
selectedCaseFilters: Record<string, TFilterOptions>;
}
const initialState: IFeedbackFiltersSlice = {
selectedCaseFilters: {},
feedbackFiltersTemplate: null,
};
const FeedbackFiltersSlice = createSlice({
name: 'feedbackFilters',
initialState,
reducers: {
setFeedbackFilterTemplate: (state, action) => {
if (action.payload) {
state.feedbackFiltersTemplate = action.payload;
}
},
setFeedbackFilters: (state, action) => {
const { loanAccountNumber, filters } = action.payload;
state.selectedCaseFilters[loanAccountNumber] = filters;
},
},
});
export const { setFeedbackFilterTemplate, setFeedbackFilters } = FeedbackFiltersSlice.actions;
export default FeedbackFiltersSlice.reducer;

View File

@@ -1,5 +1,6 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { IFeedback } from '../types/feedback.types';
import { TFilterOptions } from '../components/filters/Filters';
interface IFeedbackHistoryState {
[loanAccountNumber: string]: {
@@ -19,6 +20,7 @@ const FeedbackHistorySlice = createSlice({
setFeedbackHistory: (state, action) => {
const { loanAccountNumber, feedbacks, totalPages } = action.payload;
state[loanAccountNumber] = {
...(state[loanAccountNumber] || {}),
data: feedbacks,
timestamp: new Date().toISOString(),
isLoading: false,
@@ -32,7 +34,7 @@ const FeedbackHistorySlice = createSlice({
const payloadData = action.payload;
payloadData.loanAccountNumbers.forEach((loanAccNumber) => {
state[loanAccNumber] = {
...(state?.[loanAccNumber] || []),
...(state?.[loanAccNumber] || {}),
isLoading: payloadData.isLoading,
};
});

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
@@ -354,7 +355,7 @@ const CasesList: React.FC<ICasesList> = ({
<View style={GenericStyles.ph12}>{listEmptyComponent}</View>
)}
</View>
<ModalWrapperForAlfred
<ModalWrapperForAlfredV2
animationType="slide"
animated
onRequestClose={() => {
@@ -370,7 +371,7 @@ const CasesList: React.FC<ICasesList> = ({
isVisitPlan={isVisitPlan}
isAgentDashboard={isAgentDashboard}
/>
</ModalWrapperForAlfred>
</ModalWrapperForAlfredV2>
<BottomSheetWrapper
HeaderNode={() => (
<View style={[...row, GenericStyles.ph16]}>

View File

@@ -128,14 +128,13 @@ const ProtectedRouter = () => {
// Firestore listener hook
useFirestoreUpdates();
React.useEffect(() => {
// Watching Position for significant change
CaptureGeolocation.watchLocation((location: DeviceLocation) =>
dispatch(setDeviceGeolocation(location))
dispatch(setDeviceGeolocation(location))
);
}, []);
if (isLoading) return <FullScreenLoader loading={isLoading} />;
return (

View File

@@ -1,11 +1,15 @@
import React, { useCallback, useEffect, useState } from 'react';
import { RefreshControl, ScrollView, StyleSheet, View } from 'react-native';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Modal, RefreshControl, 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, SCREEN_HEIGHT, getShadowStyle } from '../../../../RN-UI-LIB/src/styles';
import { COLORS } from '../../../../RN-UI-LIB/src/styles/colors';
import { getPastFeedbacks, getPastFeedbacksOnAddresses } from '../../../action/feedbackActions';
import {
IFilterPayload,
getPastFeedbacks,
getPastFeedbacksOnAddresses,
} from '../../../action/feedbackActions';
import { GenericType } from '../../../common/GenericTypes';
import { logError } from '../../../components/utlis/errorUtils';
import { goBack } from '../../../components/utlis/navigationUtlis';
@@ -25,6 +29,10 @@ import { setFeedbackHistoryLoading } from '../../../reducer/feedbackHistorySlice
import SuspenseLoader from '../../../../RN-UI-LIB/src/components/suspense_loader/SuspenseLoader';
import LineLoader from '../../../../RN-UI-LIB/src/components/suspense_loader/LineLoader';
import NoPastFeedbackIcon from '../../../assets/icons/NoPastFeedbackIcon';
import Button from '../../../../RN-UI-LIB/src/components/Button';
import FilterIcon from '../../../assets/icons/FilterIcon';
import Filters, { TFilterOptions } from '../../../components/filters/Filters';
import { _map } from '../../../../RN-UI-LIB/src/utlis/common';
import ChevronDown from '../../../assets/icons/ChevronDown';
import ChevronUp from '../../../assets/icons/ChevronUp';
@@ -46,6 +54,21 @@ interface IFeedbackDetailContainer {
};
}
const getFiltersPayload = (selectedFilters: TFilterOptions) => {
const payload: IFilterPayload = { filters: [] };
_map(selectedFilters, (filterName: string) => {
const filter = selectedFilters[filterName];
if (!filter || !filter.filters) {
return;
}
payload.filters.push({
filterName,
selectedValue: Object.keys(filter.filters),
});
});
return payload;
};
const FeedbackDetailContainer: React.FC<IFeedbackDetailContainer> = ({ route: routeParams }) => {
const {
params: {
@@ -60,13 +83,17 @@ const FeedbackDetailContainer: React.FC<IFeedbackDetailContainer> = ({ route: ro
const isPastFeedbackOnAddress = addressText || addressReferenceIds?.length;
const [isExpanded, setIsExpanded] = useState<boolean>(false);
const [feedbackFilters, setFeedbackFilters] = useState<TFilterOptions>({});
const isOnline = useIsOnline();
const dispatch = useAppDispatch();
const { feedbackListFromCache, feedbackTotalPages } = useAppSelector((state: RootState) => ({
feedbackListFromCache: state.feedbackHistory?.[loanAccountNumber as string]?.data || [],
feedbackTotalPages: state.feedbackHistory?.[loanAccountNumber as string]?.totalPages || 0,
}));
const { feedbackListFromCache, feedbackTotalPages, feedbackFiltersTemplate } = useAppSelector(
(state: RootState) => ({
feedbackListFromCache: state.feedbackHistory?.[loanAccountNumber as string]?.data || [],
feedbackTotalPages: state.feedbackHistory?.[loanAccountNumber as string]?.totalPages || 0,
feedbackFiltersTemplate: state.feedbackFilters?.feedbackFiltersTemplate || {},
})
);
const [feedbackList, setFeedbackList] = useState<IFeedback[]>(
!isPastFeedbackOnAddress ? feedbackListFromCache : []
@@ -75,53 +102,65 @@ const FeedbackDetailContainer: React.FC<IFeedbackDetailContainer> = ({ route: ro
const [loading, setLoading] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
const [dataSourceCord, setDataSourceCord] = useState(0);
const [ref, setRef] = useState<GenericType>();
const [showFilterModal, setShowFilterModal] = useState(false);
const fetchFeedbacks = useCallback(() => {
const getPastFeedbackApiFn = isPastFeedbackOnAddress
? getPastFeedbacksOnAddresses
: getPastFeedbacks;
const fetchFeedbacks = useCallback(
(filtersPayload: IFilterPayload) => {
const getPastFeedbackApiFn = isPastFeedbackOnAddress
? getPastFeedbacksOnAddresses
: getPastFeedbacks;
if (isPastFeedbackOnAddress) {
dispatch(
setFeedbackHistoryLoading({ loanAccountNumbers: [loanAccountNumber], isLoading: true })
);
}
getPastFeedbackApiFn({
loan_account_number: loanAccountNumber,
page_no: currentPage - 1,
page_size: FEEDBACK_PER_PAGE,
customerRecahble: false,
addressReferenceIds,
})
.then((res: { data: IFeedback[]; totalPage: number } | GenericType) => {
if (res?.data?.length) {
setFeedbackList(res.data);
setTotalPage(res.totalPage);
return;
}
throw res;
})
.catch((err) => {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_VIEW_PAST_FEEDBACK_NAVIGATION_PAGE_FAILED, {
targetPageNumber: currentPage,
lan: loanAccountNumber,
if (isPastFeedbackOnAddress) {
dispatch(
setFeedbackHistoryLoading({ loanAccountNumbers: [loanAccountNumber], isLoading: true })
);
}
getPastFeedbackApiFn(
{
loan_account_number: loanAccountNumber,
page_no: currentPage - 1,
page_size: FEEDBACK_PER_PAGE,
customerRecahble: false,
addressReferenceIds,
},
filtersPayload
)
.then((res: { data: IFeedback[]; totalPage: number } | GenericType) => {
if (res?.data) {
setFeedbackList(res.data || []);
setTotalPage(res.totalPage || 0);
return;
}
throw res;
})
.catch((err) => {
addClickstreamEvent(
CLICKSTREAM_EVENT_NAMES.FA_VIEW_PAST_FEEDBACK_NAVIGATION_PAGE_FAILED,
{
targetPageNumber: currentPage,
lan: loanAccountNumber,
}
);
logError(err);
})
.finally(() => {
setLoading(false);
});
logError(err);
})
.finally(() => {
setLoading(false);
});
}, [currentPage]);
},
[currentPage]
);
useEffect(() => {
if (!isPastFeedbackOnAddress && currentPage == 1) {
setFeedbackList(feedbackListFromCache);
} else {
setLoading(true);
setFeedbackList([]);
}
fetchFeedbacks();
setLoading(true);
const filtersPayload = getFiltersPayload(feedbackFilters);
fetchFeedbacks(filtersPayload);
}, [currentPage]);
useEffect(() => {
@@ -139,9 +178,6 @@ const FeedbackDetailContainer: React.FC<IFeedbackDetailContainer> = ({ route: ro
}
}, []);
const [dataSourceCord, setDataSourceCord] = useState(0);
const [ref, setRef] = useState<GenericType>();
useEffect(() => {
if (!ref || !dataSourceCord) {
return;
@@ -171,6 +207,19 @@ const FeedbackDetailContainer: React.FC<IFeedbackDetailContainer> = ({ route: ro
});
};
const handleFilterChange = (filters: TFilterOptions) => {
const filtersPayload = getFiltersPayload(filters);
if (currentPage === 1) {
fetchFeedbacks(filtersPayload);
setLoading(true);
} else {
setCurrentPage(1);
}
setFeedbackFilters(filters);
};
const feedbackFilterCount = Object.keys(feedbackFilters)?.length;
return (
<Layout>
<View style={[GenericStyles.fill, GenericStyles.whiteBackground]}>
@@ -178,9 +227,43 @@ const FeedbackDetailContainer: React.FC<IFeedbackDetailContainer> = ({ route: ro
title={!isPastFeedbackOnAddress ? FEEDBACK_PAGE_TITLE : ADDRESS_FEEDBACK_PAGE_TITLE}
onBack={goBack}
/>
{!isPastFeedbackOnAddress ? (
<View style={[GenericStyles.mlAuto, GenericStyles.pv12, GenericStyles.ph16]}>
<Button
variant="secondary"
leftIcon={
<View style={GenericStyles.mr12}>
<FilterIcon
fillColor={feedbackFilterCount ? COLORS.TEXT.BLUE : COLORS.BACKGROUND.LIGHT}
/>
</View>
}
style={getShadowStyle(2)}
title="Filters"
buttonStyle={[GenericStyles.ph12, { paddingVertical: 8 }]}
onPress={() => setShowFilterModal(true)}
/>
{feedbackFilterCount ? (
<View style={[styles.filterCount]}>
<Text style={[GenericStyles.whiteText, { marginTop: -3 }]} small bold>
{feedbackFilterCount}
</Text>
</View>
) : null}
</View>
) : null}
<ScrollView
refreshControl={<RefreshControl refreshing={loading} onRefresh={fetchFeedbacks} />}
style={[GenericStyles.ph16, GenericStyles.mt16]}
refreshControl={
<RefreshControl
refreshing={false}
onRefresh={() => {
const filtersPayload = getFiltersPayload(feedbackFilters);
fetchFeedbacks(filtersPayload);
}}
/>
}
style={[GenericStyles.ph16, GenericStyles.fill]}
ref={(x) => setRef(x)}
>
<SuspenseLoader
@@ -197,112 +280,131 @@ const FeedbackDetailContainer: React.FC<IFeedbackDetailContainer> = ({ route: ro
</>
}
>
<View>
{feedbackList?.length ? (
<>
{isPastFeedbackOnAddress ? (
<View style={styles.addressCard}>
<Text style={[styles.addressText, { color: COLORS.TEXT.LIGHT }]}>
Feedback for
</Text>
<Text style={[styles.addressText, { color: COLORS.TEXT.DARK }]}>
{addressText}
</Text>
</View>
) : null}
{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);
}
{feedbackList?.length ? (
<>
{isPastFeedbackOnAddress ? (
<View style={styles.addressCard}>
<Text style={[styles.addressText, { color: COLORS.TEXT.LIGHT }]}>
Feedback for
</Text>
<Text style={[styles.addressText, { color: COLORS.TEXT.DARK }]}>
{addressText}
</Text>
</View>
) : null}
{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.br8,
getShadowStyle(4),
GenericStyles.ph0,
GenericStyles.overflowHidden,
GenericStyles.ph16,
styles.accordianPadding,
]}
isActive={feedback.referenceId === activeFeedbackReferenceId}
touchableDelay={50}
touchableOpacity={0.8}
accordionHeader={
<FeedbackDetailItem
key={feedback.referenceId}
feedbackItem={feedback}
isExpanded={isExpanded}
caseId={caseId}
/>
}
customExpandUi={{
whenCollapsed: <ChevronDown />,
whenExpanded: <ChevronUp />,
}}
onExpanded={(value) => {
setIsExpanded(value);
}}
>
<Accordion
accordionStyle={[
GenericStyles.br8,
getShadowStyle(4),
GenericStyles.ph0,
GenericStyles.overflowHidden,
GenericStyles.ph16,
styles.accordianPadding,
]}
isActive={feedback.referenceId === activeFeedbackReferenceId}
touchableDelay={50}
touchableOpacity={0.8}
accordionHeader={
<FeedbackDetailItem
key={feedback.referenceId}
feedbackItem={feedback}
isExpanded={isExpanded}
caseId={caseId}
/>
}
customExpandUi={{
whenCollapsed: <ChevronDown />,
whenExpanded: <ChevronUp />,
}}
onExpanded={(value) => {
setIsExpanded(value);
}}
>
<FeedbackDetailAnswerContainer
answerList={feedback.answerViews}
activeFeedbackReferenceId={feedback.referenceId}
loanAccountNumber={loanAccountNumber}
/>
</Accordion>
</View>
))}
<Pagination
onPageChange={handlePageChange}
currentPage={currentPage}
totalPages={totalPage}
onNextPage={(page) =>
handlePageChangeClickstream(
page,
CLICKSTREAM_EVENT_NAMES.FA_VIEW_PAST_FEEDBACK_NEXT_PAGE_CLICKED
)
}
onPrevPage={(page) =>
handlePageChangeClickstream(
page,
CLICKSTREAM_EVENT_NAMES.FA_VIEW_PAST_FEEDBACK_PREV_PAGE_CLICKED
)
}
onFirstPage={(page) =>
handlePageChangeClickstream(
page,
CLICKSTREAM_EVENT_NAMES.FA_VIEW_PAST_FEEDBACK_FIRST_PAGE_CLICKED
)
}
onLastPage={(page) =>
handlePageChangeClickstream(
page,
CLICKSTREAM_EVENT_NAMES.FA_VIEW_PAST_FEEDBACK_LAST_PAGE_CLICKED
)
}
/>
</>
) : (
<View
style={[
styles.noFeedbackContainer,
GenericStyles.centerAligned,
GenericStyles.columnDirection,
]}
>
<NoPastFeedbackIcon />
<Text style={[styles.textContainer, styles.noFeedbackText]}>
No previous feedback found
</Text>
</View>
)}
</View>
<FeedbackDetailAnswerContainer
answerList={feedback.answerViews}
activeFeedbackReferenceId={feedback.referenceId}
loanAccountNumber={loanAccountNumber}
/>
</Accordion>
</View>
))}
</>
) : (
<View
style={[
styles.noFeedbackContainer,
GenericStyles.centerAligned,
GenericStyles.columnDirection,
GenericStyles.fill,
]}
>
<NoPastFeedbackIcon />
<Text style={[styles.textContainer, styles.noFeedbackText]}>
No previous feedback found
</Text>
</View>
)}
</SuspenseLoader>
</ScrollView>
{feedbackList?.length && totalPage > 1 && !loading ? (
<Pagination
onPageChange={handlePageChange}
currentPage={currentPage}
totalPages={totalPage}
onNextPage={(page) =>
handlePageChangeClickstream(
page,
CLICKSTREAM_EVENT_NAMES.FA_VIEW_PAST_FEEDBACK_NEXT_PAGE_CLICKED
)
}
onPrevPage={(page) =>
handlePageChangeClickstream(
page,
CLICKSTREAM_EVENT_NAMES.FA_VIEW_PAST_FEEDBACK_PREV_PAGE_CLICKED
)
}
onFirstPage={(page) =>
handlePageChangeClickstream(
page,
CLICKSTREAM_EVENT_NAMES.FA_VIEW_PAST_FEEDBACK_FIRST_PAGE_CLICKED
)
}
onLastPage={(page) =>
handlePageChangeClickstream(
page,
CLICKSTREAM_EVENT_NAMES.FA_VIEW_PAST_FEEDBACK_LAST_PAGE_CLICKED
)
}
/>
) : null}
{!isPastFeedbackOnAddress ? (
<Modal
animationType="slide"
animated
onRequestClose={() => {
setShowFilterModal((prev) => !prev);
}}
visible={showFilterModal}
>
<Filters
header="Feedback Filters"
defaultSelectedFilters={feedbackFilters}
closeFilterModal={() => setShowFilterModal((prev) => !prev)}
filters={feedbackFiltersTemplate}
onFilterChange={handleFilterChange}
/>
</Modal>
) : null}
</View>
</Layout>
);
@@ -329,8 +431,7 @@ const styles = StyleSheet.create({
fontWeight: '500',
},
noFeedbackContainer: {
height: SCREEN_HEIGHT - 56,
backgroundColor: COLORS.BACKGROUND.PRIMARY,
marginTop: '50%',
},
accordionExpandBtn: {
fontSize: 13,
@@ -338,6 +439,17 @@ const styles = StyleSheet.create({
lineHeight: 20,
color: COLORS.TEXT.BLUE,
},
filterCount: {
backgroundColor: COLORS.TEXT.BLUE,
width: 18,
height: 18,
borderRadius: 9,
alignItems: 'center',
position: 'absolute',
opacity: 0.8,
top: 7,
right: 11,
},
accordianPadding: {
paddingTop: 16,
paddingBottom: 8,

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

@@ -32,9 +32,9 @@ import configSlice from '../reducer/configSlice';
import profileSlice from '../reducer/profileSlice';
import reporteesSlice from '../reducer/reporteesSlice';
import blacklistedAppsInstalledSlice from '@reducers/blacklistedAppsInstalledSlice';
import feedbackFiltersSlice from '@reducers/feedbackFiltersSlice';
import agentPerformanceSlice from '../reducer/agentPerformanceSlice';
const rootReducer = combineReducers({
case: caseReducer,
loginInfo: loginSlice,
@@ -56,6 +56,7 @@ const rootReducer = combineReducers({
profile: profileSlice,
reportees: reporteesSlice,
blacklistAppsInstalled: blacklistedAppsInstalledSlice,
feedbackFilters: feedbackFiltersSlice,
agentPerformance: agentPerformanceSlice,
});
@@ -78,6 +79,7 @@ const persistConfig = {
'config',
'profile',
'foregroundService',
'feedbackFilters',
],
blackList: ['case', 'filters', 'reportees', 'agentPerformance'],
};