6
App.tsx
6
App.tsx
@@ -119,11 +119,7 @@ const App = () => {
|
||||
}
|
||||
/>
|
||||
</NavigationContainer>
|
||||
{
|
||||
<KeyboardAvoidingView behavior="position">
|
||||
<ToastContainer config={toastConfigs} position="bottom" />
|
||||
</KeyboardAvoidingView>
|
||||
}
|
||||
<ToastContainer config={toastConfigs} position="top" topOffset={18} />
|
||||
</PersistGate>
|
||||
</Provider>
|
||||
);
|
||||
|
||||
Submodule RN-UI-LIB updated: 38b19feac8...01b763782e
@@ -53,6 +53,7 @@
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/app_name"
|
||||
android:screenOrientation="portrait"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
|
||||
android:launchMode="singleTask"
|
||||
android:windowSoftInputMode="stateUnspecified|adjustPan"
|
||||
|
||||
72
android/app/src/main/java/com/avapp/DeviceUtilsModule.java
Normal file
72
android/app/src/main/java/com/avapp/DeviceUtilsModule.java
Normal file
@@ -0,0 +1,72 @@
|
||||
package com.avapp;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.location.LocationManager;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.facebook.react.bridge.ActivityEventListener;
|
||||
import com.facebook.react.bridge.Promise;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
|
||||
import org.json.JSONArray;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class DeviceUtilsModule extends ReactContextBaseJavaModule implements ActivityEventListener {
|
||||
private ReactApplicationContext RNContext;
|
||||
|
||||
public DeviceUtilsModule(@Nullable ReactApplicationContext reactContext){
|
||||
super(reactContext);
|
||||
RNContext = reactContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "DeviceUtilsModule";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(Activity activity, int i, int i1, @Nullable Intent intent) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNewIntent(Intent intent) {
|
||||
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void isLocationEnabled (Promise promise) {
|
||||
try {
|
||||
LocationManager locationManager = (LocationManager) RNContext.getApplicationContext().getSystemService(Context.LOCATION_SERVICE);
|
||||
Boolean isEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);
|
||||
promise.resolve(isEnabled);
|
||||
} catch (Exception err) {
|
||||
promise.reject(err);
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void getAllInstalledApp (Promise promise) {
|
||||
try {
|
||||
PackageManager packageManager = RNContext.getPackageManager();
|
||||
List<ApplicationInfo> packages = packageManager.getInstalledApplications(PackageManager.GET_META_DATA);
|
||||
JSONArray jsonArray = new JSONArray();
|
||||
|
||||
for (ApplicationInfo applicationInfo : packages) {
|
||||
jsonArray.put(applicationInfo.packageName);
|
||||
}
|
||||
promise.resolve( jsonArray.toString());
|
||||
}catch (Exception err){
|
||||
promise.reject( err);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.avapp;
|
||||
|
||||
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.bridge.NativeModule;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.uimanager.ViewManager;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class DeviceUtilsModulePackage implements ReactPackage {
|
||||
@Override
|
||||
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<NativeModule> createNativeModules(
|
||||
ReactApplicationContext reactContext) {
|
||||
List<NativeModule> modules = new ArrayList<>();
|
||||
|
||||
modules.add(new DeviceUtilsModule(reactContext));
|
||||
|
||||
return modules;
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,7 @@ public class MainApplication extends Application implements ReactApplication {
|
||||
List<ReactPackage> packages = new PackageList(this).getPackages();
|
||||
// Packages that cannot be autolinked yet can be added manually here, for example:
|
||||
// packages.add(new MyReactNativePackage());
|
||||
packages.add(new DeviceUtilsModulePackage());
|
||||
return packages;
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import { GoogleSignin } from '@react-native-google-signin/google-signin';
|
||||
|
||||
export interface GenerateOTPPayload {
|
||||
phoneNumber: string;
|
||||
otpToken?: string;
|
||||
}
|
||||
|
||||
export interface VerifyOTPPayload {
|
||||
@@ -44,12 +45,16 @@ export interface ImpersonationPayload {
|
||||
}
|
||||
|
||||
export const generateOTP =
|
||||
({ phoneNumber }: GenerateOTPPayload, isResendOTP?: boolean) =>
|
||||
({ phoneNumber, otpToken }: GenerateOTPPayload, isResendOTP?: boolean) =>
|
||||
(dispatch: Dispatch) => {
|
||||
const url = getApiUrl(ApiKeys.GENERATE_OTP);
|
||||
dispatch(setFormLoading(true));
|
||||
axiosInstance
|
||||
.post(url, { phoneNumber: Number(phoneNumber) }, { headers: { donotHandleError: true } })
|
||||
.post(
|
||||
url,
|
||||
{ phoneNumber: Number(phoneNumber), otpToken },
|
||||
{ headers: { donotHandleError: true } }
|
||||
)
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
if (response?.data?.data?.otpToken) {
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
import { ToastMessages } from '../screens/allCases/constants';
|
||||
import { ILoanIdToValue } from './../reducer/paymentSlice';
|
||||
import { setAsyncStorageItem } from './../components/utlis/commonFunctions';
|
||||
import { Dispatch } from '@reduxjs/toolkit';
|
||||
import { appendLoanIdToValue, setLoading, setPaymentLink } from '../reducer/paymentSlice';
|
||||
import axiosInstance, {
|
||||
ApiKeys,
|
||||
API_STATUS_CODE,
|
||||
getApiUrl,
|
||||
getErrorMessage,
|
||||
isAxiosError,
|
||||
} from '../components/utlis/apiHelper';
|
||||
import { toast } from '../../RN-UI-LIB/src/components/toast';
|
||||
import { CLICKSTREAM_EVENT_NAMES } from '../common/Constants';
|
||||
import { addClickstreamEvent } from '../services/clickstreamEventService';
|
||||
import { logError } from '../components/utlis/errorUtils';
|
||||
|
||||
export interface GeneratePaymentPayload {
|
||||
alternateContactNumber: string;
|
||||
@@ -36,82 +34,72 @@ export interface ILoanIdValue
|
||||
Pick<GeneratePaymentPayload, 'alternateContactNumber' | 'customAmount'> {}
|
||||
|
||||
export const generatePaymentLinkAction =
|
||||
(payload: GeneratePaymentPayload, loanIdToValue: ILoanIdToValue) => (dispatch: Dispatch) => {
|
||||
(payload: GeneratePaymentPayload) => (dispatch: Dispatch) => {
|
||||
const url = getApiUrl(ApiKeys.GENERATE_PAYMENT_LINK);
|
||||
dispatch(setLoading(true));
|
||||
const currentTime = Date.now();
|
||||
const { loanAccountNumber } = payload;
|
||||
const value = loanIdToValue?.[loanAccountNumber];
|
||||
// if (
|
||||
// value &&
|
||||
// Number(value?.expiresAt) > currentTime &&
|
||||
// value?.alternateContactNumber === payload.alternateContactNumber &&
|
||||
// value?.customAmount.amount === payload.customAmount.amount
|
||||
// ) {
|
||||
// dispatch(setPaymentLink(value.paymentLink));
|
||||
// dispatch(setLoading(false));
|
||||
// return;
|
||||
// }
|
||||
axiosInstance
|
||||
.post(url, payload)
|
||||
.then((response) => {
|
||||
if (response.status === API_STATUS_CODE.OK) {
|
||||
const responseData = response?.data;
|
||||
const { paymentLink, retriesLeft } = responseData;
|
||||
if (paymentLink) {
|
||||
dispatch(setPaymentLink(paymentLink));
|
||||
const storePayload = {
|
||||
...responseData,
|
||||
customAmount: payload.customAmount,
|
||||
alternateContactNumber: payload.alternateContactNumber,
|
||||
};
|
||||
dispatch(
|
||||
appendLoanIdToValue({
|
||||
[loanAccountNumber]: storePayload,
|
||||
})
|
||||
);
|
||||
// await setAsyncStorageItem(
|
||||
// LocalStorageKeys.LOAN_ID_TO_VALUE,
|
||||
// {
|
||||
// ...loanIdToValue,
|
||||
// [loanAccountNumber]: storePayload,
|
||||
// },
|
||||
// );
|
||||
toast({
|
||||
type: 'success',
|
||||
text1: `${ToastMessages.PAYMENT_LINK_SUCCESS} ${retriesLeft} tr${
|
||||
retriesLeft > 1 ? 'ies' : 'y'
|
||||
} remaining.`,
|
||||
});
|
||||
|
||||
dispatch(setLoading(true));
|
||||
try {
|
||||
axiosInstance
|
||||
.post(url, payload)
|
||||
.then((response) => {
|
||||
if (response?.status === API_STATUS_CODE.OK) {
|
||||
const responseData = response.data;
|
||||
const { paymentLink, retriesLeft } = responseData || {};
|
||||
if (paymentLink) {
|
||||
dispatch(setPaymentLink(paymentLink));
|
||||
const storePayload = {
|
||||
...responseData,
|
||||
customAmount: payload.customAmount,
|
||||
alternateContactNumber: payload.alternateContactNumber,
|
||||
};
|
||||
dispatch(
|
||||
appendLoanIdToValue({
|
||||
[loanAccountNumber]: storePayload,
|
||||
})
|
||||
);
|
||||
toast({
|
||||
type: 'success',
|
||||
text1: `${ToastMessages.PAYMENT_LINK_SUCCESS} ${retriesLeft} tr${
|
||||
retriesLeft > 1 ? 'ies' : 'y'
|
||||
} remaining.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
if (isAxiosError(err)) {
|
||||
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_SEND_PAYMENT_LINK_FAILED, {
|
||||
amount: payload.customAmount,
|
||||
lan: payload.loanAccountNumber,
|
||||
phoneNumber: payload.alternateContactNumber,
|
||||
});
|
||||
if (err?.response?.status === 429) {
|
||||
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_SEND_PAYMENT_LINK_FAILED_LIMIT_REACHED, {
|
||||
})
|
||||
.catch((err) => {
|
||||
logError(err);
|
||||
if (isAxiosError(err)) {
|
||||
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_SEND_PAYMENT_LINK_FAILED, {
|
||||
amount: payload.customAmount,
|
||||
lan: payload.loanAccountNumber,
|
||||
phoneNumber: payload.alternateContactNumber,
|
||||
});
|
||||
toast({
|
||||
type: 'error',
|
||||
text1: ToastMessages.PAYMENT_LINK_RETRY,
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
type: 'error',
|
||||
text1: ToastMessages.PAYMENT_LINK_ERROR,
|
||||
});
|
||||
if (err?.response?.status === 429) {
|
||||
addClickstreamEvent(
|
||||
CLICKSTREAM_EVENT_NAMES.FA_SEND_PAYMENT_LINK_FAILED_LIMIT_REACHED,
|
||||
{
|
||||
amount: payload.customAmount,
|
||||
lan: payload.loanAccountNumber,
|
||||
phoneNumber: payload.alternateContactNumber,
|
||||
}
|
||||
);
|
||||
toast({
|
||||
type: 'error',
|
||||
text1: ToastMessages.PAYMENT_LINK_RETRY,
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
type: 'error',
|
||||
text1: ToastMessages.PAYMENT_LINK_ERROR,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
dispatch(setLoading(false));
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
dispatch(setLoading(false));
|
||||
});
|
||||
} catch (err) {
|
||||
dispatch(setLoading(false));
|
||||
}
|
||||
};
|
||||
|
||||
78
src/assets/icons/EmptyScheduledListIcon.tsx
Normal file
78
src/assets/icons/EmptyScheduledListIcon.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import * as React from 'react';
|
||||
import Svg, { Path, Rect, Circle } from 'react-native-svg';
|
||||
|
||||
const EmptyScheduledListIcon = () => (
|
||||
<Svg width={181} height={103} viewBox="0 0 181 103" fill="none">
|
||||
<Path
|
||||
d="M34.6897 102.46H20.9695L33.0756 10.7637L34.6897 102.46Z"
|
||||
fill="#545454"
|
||||
stroke="#12183E"
|
||||
strokeWidth={0.565625}
|
||||
/>
|
||||
<Path
|
||||
d="M142.747 102.447H34.5993L33.0133 12.1618C32.9979 11.2874 33.7025 10.5703 34.5771 10.5703H139.596C140.449 10.5703 141.145 11.2539 141.16 12.1068L142.747 102.447Z"
|
||||
fill="white"
|
||||
stroke="#12183E"
|
||||
strokeWidth={0.565625}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<Path
|
||||
d="M141.416 26.5777H33.238L33.0092 11.3375C32.9961 10.4646 33.7001 9.75 34.573 9.75H139.59C140.443 9.75 141.139 10.434 141.154 11.2872L141.416 26.5777Z"
|
||||
fill="#F7F7F7"
|
||||
stroke="#12183E"
|
||||
strokeWidth={0.565625}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<Rect
|
||||
x={57.7374}
|
||||
y={0.59375}
|
||||
width={4.78646}
|
||||
height={19.1458}
|
||||
rx={0.790366}
|
||||
fill="white"
|
||||
stroke="#12183E"
|
||||
strokeWidth={0.565625}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<Rect
|
||||
x={117.417}
|
||||
y={0.59375}
|
||||
width={4.78646}
|
||||
height={19.1458}
|
||||
rx={0.790366}
|
||||
fill="white"
|
||||
stroke="#12183E"
|
||||
strokeWidth={0.565625}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<Path
|
||||
d="M112.648 65.1756C112.648 52.6463 102.488 42.4863 89.9586 42.4863C77.4293 42.4863 67.2693 52.6463 67.2693 65.1756C67.2693 77.705 77.4293 87.8649 89.9586 87.8649C96.2233 87.8649 101.896 85.3339 106.006 81.2233"
|
||||
stroke="#12183E"
|
||||
strokeWidth={0.565625}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<Circle cx={90.0276} cy={65.2908} r={20.1834} fill="#EEEEEE" />
|
||||
<Path
|
||||
d="M99.4657 73.941L90.3287 64.8039V51.3379"
|
||||
stroke="#12183E"
|
||||
strokeWidth={0.565625}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<Path
|
||||
d="M106.017 60.4157L112.698 65.1755L117.458 58.4941"
|
||||
stroke="#12183E"
|
||||
strokeWidth={0.565625}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<Path d="M151.632 49.9648H181" stroke="#12183E" strokeWidth={0.565625} strokeLinejoin="round" />
|
||||
<Path d="M0 73.9297H16.4687" stroke="#12183E" strokeWidth={0.565625} strokeLinejoin="round" />
|
||||
</Svg>
|
||||
);
|
||||
export default EmptyScheduledListIcon;
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { ReactNode, useCallback, useEffect, useState } from 'react';
|
||||
import React, { ReactNode, useCallback, useState } from 'react';
|
||||
import { Linking } from 'react-native';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { RootState } from '../store/store';
|
||||
@@ -7,17 +7,14 @@ import { getAppVersion } from '../components/utlis/commonFunctions';
|
||||
import BlockerInstructions from './BlockerInstructions';
|
||||
import { BLOCKER_SCREEN_DATA } from './Constants';
|
||||
import { useAppDispatch, useAppSelector } from '../hooks';
|
||||
import Geolocation from 'react-native-geolocation-service';
|
||||
import { setIsDeviceLocationEnabled } from '../reducer/foregroundServiceSlice';
|
||||
import { logError } from '../components/utlis/errorUtils';
|
||||
import { MILLISECONDS_IN_A_SECOND } from '../../RN-UI-LIB/src/utlis/common';
|
||||
import { toast } from '../../RN-UI-LIB/src/components/toast';
|
||||
import { locationEnabled } from '../components/utlis/DeviceUtils';
|
||||
|
||||
interface IBlockerScreen {
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
const RETRY_GEOLOCATION_TIME = 10 * MILLISECONDS_IN_A_SECOND; // 10 seconds
|
||||
const RETRY_GEOLOCATION_STEPS =
|
||||
'Unable to retrieve location. Kindly follow the given steps and try again.';
|
||||
|
||||
@@ -65,36 +62,15 @@ const BlockerScreen = (props: IBlockerScreen) => {
|
||||
await Linking.openSettings();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let timeoutId = -1;
|
||||
if (showActionBtnLoader) {
|
||||
timeoutId = setTimeout(() => {
|
||||
setShowActionBtnLoader(false);
|
||||
toast({ type: 'info', text1: RETRY_GEOLOCATION_STEPS });
|
||||
}, RETRY_GEOLOCATION_TIME);
|
||||
}
|
||||
return () => {
|
||||
if (timeoutId !== -1) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
};
|
||||
}, [showActionBtnLoader]);
|
||||
|
||||
const handleLocationAccess = async () => {
|
||||
setShowActionBtnLoader(true);
|
||||
Geolocation.getCurrentPosition(
|
||||
() => {
|
||||
setShowActionBtnLoader(false);
|
||||
if (!isDeviceLocationEnabled) {
|
||||
dispatch(setIsDeviceLocationEnabled(true));
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
setShowActionBtnLoader(false);
|
||||
logError(error as any, 'Unable to get location on retry button');
|
||||
},
|
||||
{ enableHighAccuracy: true }
|
||||
);
|
||||
const isLocationEnabled = await locationEnabled();
|
||||
if (isLocationEnabled) {
|
||||
dispatch(setIsDeviceLocationEnabled(isLocationEnabled));
|
||||
} else {
|
||||
setShowActionBtnLoader(false);
|
||||
toast({ type: 'info', text1: RETRY_GEOLOCATION_STEPS });
|
||||
}
|
||||
};
|
||||
|
||||
if (forceReinstallData?.reinstall_endpoint) {
|
||||
|
||||
@@ -382,6 +382,14 @@ export const CLICKSTREAM_EVENT_NAMES = {
|
||||
name: 'FA_VIEW_PAST_FEEDBACK_PREV_PAGE_CLICKED',
|
||||
description: 'FA_VIEW_PAST_FEEDBACK_PREV_PAGE_CLICKED',
|
||||
},
|
||||
FA_VIEW_PAST_FEEDBACK_FIRST_PAGE_CLICKED: {
|
||||
name: 'FA_VIEW_PAST_FEEDBACK_FIRST_PAGE_CLICKED',
|
||||
description: 'FA_VIEW_PAST_FEEDBACK_FIRST_PAGE_CLICKED',
|
||||
},
|
||||
FA_VIEW_PAST_FEEDBACK_LAST_PAGE_CLICKED: {
|
||||
name: 'FA_VIEW_PAST_FEEDBACK_LAST_PAGE_CLICKED',
|
||||
description: 'FA_VIEW_PAST_FEEDBACK_LAST_PAGE_CLICKED',
|
||||
},
|
||||
FA_VIEW_PAST_FEEDBACK_NAVIGATION_PAGE_FAILED: {
|
||||
name: 'FA_VIEW_PAST_FEEDBACK_PREV_PAGE_FAILED',
|
||||
description: 'FA_VIEW_PAST_FEEDBACK_PREV_PAGE_FAILED',
|
||||
|
||||
@@ -10,7 +10,7 @@ import NoWifiIcon from '../assets/icons/NoWifiIcon';
|
||||
import { GenericFunctionArgs } from './GenericTypes';
|
||||
|
||||
interface IOfflineScreen {
|
||||
handleRetryEvent: GenericFunctionArgs;
|
||||
handleRetryEvent?: GenericFunctionArgs;
|
||||
goBack: GenericFunctionArgs;
|
||||
pageTitle: string;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ const TrackingComponent: React.FC<ITrackingComponent> = ({ children }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const appState = useRef(AppState.currentState);
|
||||
const bgTrackingTimeoutId = useRef<number>();
|
||||
const user = useAppSelector((state) => state.user);
|
||||
|
||||
const handleTimeSync = async () => {
|
||||
try {
|
||||
@@ -46,8 +47,8 @@ const TrackingComponent: React.FC<ITrackingComponent> = ({ children }) => {
|
||||
|
||||
const handleSendGeolocation = async () => {
|
||||
try {
|
||||
const location = await CaptureGeolocation.fetchLocation(Date.now() + '', 0);
|
||||
if (location) {
|
||||
const location = await CaptureGeolocation.fetchLocation(Date.now() + '', 0, appState.current);
|
||||
if (location && user.isLoggedIn) {
|
||||
await sendLocationToServer(location);
|
||||
}
|
||||
} catch (e: any) {
|
||||
@@ -112,14 +113,6 @@ const TrackingComponent: React.FC<ITrackingComponent> = ({ children }) => {
|
||||
|
||||
useIsLocationEnabled();
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (UnstoppableService.isRunning()) {
|
||||
UnstoppableService.stopAll();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ const OTPInput: React.FC<OTPInputProps> = ({
|
||||
value={otp[i]}
|
||||
maxLength={1}
|
||||
keyboardType="number-pad"
|
||||
onChangeText={handleOnChangeText}
|
||||
// onChangeText={handleOnChangeText}
|
||||
onKeyPress={({ nativeEvent }) => handleKeyPress(i, nativeEvent.key)}
|
||||
containerStyle={styles.inputContainer}
|
||||
style={styles.input}
|
||||
|
||||
53
src/components/expandableImage/ExpandableImage.tsx
Normal file
53
src/components/expandableImage/ExpandableImage.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import React from 'react';
|
||||
import { GenericStyles } from '../../../RN-UI-LIB/src/styles';
|
||||
import WebView from 'react-native-webview';
|
||||
import NavigationHeader from '../../../RN-UI-LIB/src/components/NavigationHeader';
|
||||
|
||||
interface IExpandableImage {
|
||||
imageSrc?: string;
|
||||
close?: () => void;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
const imageHtml = (imageSrc?: string) => `
|
||||
<html>
|
||||
<head>
|
||||
<title>Image Viewer</title>
|
||||
<style>
|
||||
body{
|
||||
height: 100vh;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background: #000000;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page pinch-zoom-parent">
|
||||
<div class="pinch-zoom">
|
||||
<img style="width: calc(100vw - 32px); max-height: calc(100vh - 32px)" src=${imageSrc} />
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
const ExpandableImage: React.FC<IExpandableImage> = ({ imageSrc, title, close }) => {
|
||||
return (
|
||||
<View style={GenericStyles.fill}>
|
||||
<NavigationHeader onBack={close} title={title} />
|
||||
<WebView
|
||||
scalesPageToFit={true}
|
||||
bounces={false}
|
||||
scrollEnabled={false}
|
||||
source={{ html: imageHtml(imageSrc) }}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExpandableImage;
|
||||
@@ -81,7 +81,6 @@ const Widget: React.FC<IWidget> = (props) => {
|
||||
}, [templateData, name]);
|
||||
|
||||
const dataToBeValidated = useAppSelector((state) => state.case.caseForm?.[caseId]?.[journey]);
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
@@ -180,6 +179,7 @@ const Widget: React.FC<IWidget> = (props) => {
|
||||
caseData,
|
||||
answer: data,
|
||||
coords,
|
||||
templateId: templateData.templateId,
|
||||
});
|
||||
if (isOnline) {
|
||||
setIsSubmitting(true);
|
||||
@@ -259,7 +259,7 @@ const Widget: React.FC<IWidget> = (props) => {
|
||||
);
|
||||
toast({
|
||||
type: 'info',
|
||||
text1: "Feedback will be submitted automatically once you're back online",
|
||||
text1: ToastMessages.FEEDBACK_SUBMITTED_OFFLINE,
|
||||
});
|
||||
navigateToScreen('caseDetail', {
|
||||
journey: journey,
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { PermissionsAndroid } from 'react-native';
|
||||
import { AppStateStatus, PermissionsAndroid } from 'react-native';
|
||||
import Geolocation from 'react-native-geolocation-service';
|
||||
import { toast } from '../../../../RN-UI-LIB/src/components/toast';
|
||||
import { logError } from '../../utlis/errorUtils';
|
||||
import { PositionError } from '../../../hooks/useIsLocationEnabled';
|
||||
const FIVE_MIN = 5 * 60 * 1000;
|
||||
export const requestLocationPermission = async () => {
|
||||
try {
|
||||
@@ -47,7 +46,8 @@ export class CaptureGeolocation {
|
||||
}
|
||||
static async fetchLocation(
|
||||
resourceId: string,
|
||||
cacheTTL: number = FIVE_MIN
|
||||
cacheTTL: number = FIVE_MIN,
|
||||
appState: AppStateStatus = 'active'
|
||||
): Promise<Geolocation.GeoCoordinates | undefined> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
let cachedLocation = CaptureGeolocation.capturedLocation?.[resourceId];
|
||||
@@ -60,7 +60,7 @@ export class CaptureGeolocation {
|
||||
}
|
||||
CaptureGeolocation.setCapturing(resourceId, true);
|
||||
const isLocationOn = await requestLocationPermission();
|
||||
if (!isLocationOn) {
|
||||
if (!isLocationOn && appState === 'active') {
|
||||
CaptureGeolocation.setCapturing(resourceId, false);
|
||||
toast({
|
||||
type: 'error',
|
||||
@@ -81,10 +81,6 @@ export class CaptureGeolocation {
|
||||
logError(error as any, 'Unable to get location');
|
||||
CaptureGeolocation.setCapturing(resourceId, false);
|
||||
reject(error);
|
||||
const { PERMISSION_DENIED, POSITION_UNAVAILABLE } = PositionError;
|
||||
if (error.code === PERMISSION_DENIED || error.code === POSITION_UNAVAILABLE) {
|
||||
return;
|
||||
}
|
||||
toast({
|
||||
type: 'error',
|
||||
text1: 'Error getting geolocation' + JSON.stringify(error || {}),
|
||||
|
||||
@@ -1,39 +1,50 @@
|
||||
import { Pressable, StyleSheet, View } from 'react-native';
|
||||
import React from 'react';
|
||||
import { getCurrentScreen, pushToScreen } from '../utlis/navigationUtlis';
|
||||
import { GenericStyles } from '../../../RN-UI-LIB/src/styles';
|
||||
import React, { useState } from 'react';
|
||||
import { StyleSheet, TouchableHighlight, View } from 'react-native';
|
||||
import BellIcon from '../../../RN-UI-LIB/src/Icons/BellIcon';
|
||||
import { COLORS } from '../../../RN-UI-LIB/src/styles/colors';
|
||||
import { useAppSelector } from '../../hooks';
|
||||
import Text from '../../../RN-UI-LIB/src/components/Text';
|
||||
import { addClickstreamEvent } from '../../services/clickstreamEventService';
|
||||
import { GenericStyles } from '../../../RN-UI-LIB/src/styles';
|
||||
import { COLORS } from '../../../RN-UI-LIB/src/styles/colors';
|
||||
import { CLICKSTREAM_EVENT_NAMES } from '../../common/Constants';
|
||||
import { useAppSelector } from '../../hooks';
|
||||
import { addClickstreamEvent } from '../../services/clickstreamEventService';
|
||||
import { getCurrentScreen, pushToScreen } from '../utlis/navigationUtlis';
|
||||
|
||||
const NotificationMenu = () => {
|
||||
const { totalUnreadElements } = useAppSelector((state) => state.notifications);
|
||||
const [isEnabled, setIsEnabled] = useState(false);
|
||||
|
||||
const handleNotificationPress = () => {
|
||||
if (isEnabled) return;
|
||||
setIsEnabled(true);
|
||||
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_NOTIFICATION_ICON_CLICK, {
|
||||
screenName: getCurrentScreen()?.name,
|
||||
});
|
||||
pushToScreen('Notifications');
|
||||
setTimeout(() => setIsEnabled(false), 400);
|
||||
};
|
||||
|
||||
return (
|
||||
<Pressable style={[GenericStyles.ph8, GenericStyles.pv10]} onPress={handleNotificationPress}>
|
||||
<BellIcon />
|
||||
<View style={[styles.notificationBadge, GenericStyles.alignCenter]}>
|
||||
{totalUnreadElements ? (
|
||||
<Text
|
||||
bold
|
||||
small
|
||||
style={[styles.notificationNumber, { width: totalUnreadElements > 9 ? 24 : 16 }]}
|
||||
>
|
||||
{totalUnreadElements > 9 ? '9+' : totalUnreadElements}
|
||||
</Text>
|
||||
) : null}
|
||||
<TouchableHighlight
|
||||
underlayColor={COLORS.HIGHLIGHTER.DARK_BUTTON}
|
||||
activeOpacity={1}
|
||||
style={GenericStyles.iconContainerButton}
|
||||
onPress={handleNotificationPress}
|
||||
>
|
||||
<View style={[GenericStyles.ph8, GenericStyles.pv10]}>
|
||||
<BellIcon />
|
||||
<View style={[styles.notificationBadge, GenericStyles.alignCenter]}>
|
||||
{totalUnreadElements ? (
|
||||
<Text
|
||||
bold
|
||||
small
|
||||
style={[styles.notificationNumber, { width: totalUnreadElements > 9 ? 24 : 16 }]}
|
||||
>
|
||||
{totalUnreadElements > 9 ? '9+' : totalUnreadElements}
|
||||
</Text>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
</Pressable>
|
||||
</TouchableHighlight>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -43,7 +54,7 @@ const styles = StyleSheet.create({
|
||||
position: 'absolute',
|
||||
height: 16,
|
||||
marginLeft: 18,
|
||||
marginTop: -4,
|
||||
marginTop: 4,
|
||||
borderRadius: 8,
|
||||
zIndex: 100,
|
||||
},
|
||||
|
||||
10
src/components/utlis/DeviceUtils.ts
Normal file
10
src/components/utlis/DeviceUtils.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { NativeModules } from 'react-native';
|
||||
|
||||
const { DeviceUtilsModule } = NativeModules; // this is the same name we returned in getName function.
|
||||
|
||||
// returns true if enabled, and false if disabled.
|
||||
export const locationEnabled = (): Promise<boolean> => DeviceUtilsModule.isLocationEnabled();
|
||||
|
||||
// returns array of all the installed packages.
|
||||
export const getAllInstalledApp = (): Promise<Array<string>> =>
|
||||
DeviceUtilsModule.getAllInstalledApp();
|
||||
@@ -46,7 +46,7 @@ export enum ApiKeys {
|
||||
}
|
||||
|
||||
export const API_URLS: Record<ApiKeys, string> = {} as Record<ApiKeys, string>;
|
||||
API_URLS[ApiKeys.GENERATE_OTP] = '/auth/otp/generate';
|
||||
API_URLS[ApiKeys.GENERATE_OTP] = '/auth/request-otp';
|
||||
API_URLS[ApiKeys.VERIFY_OTP] = '/auth/otp/verify';
|
||||
API_URLS[ApiKeys.ALL_CASES] = '/cases/all-cases';
|
||||
API_URLS[ApiKeys.CASE_DETAIL] = '/cases/get-cases';
|
||||
@@ -65,7 +65,7 @@ API_URLS[ApiKeys.NOTIFICATIONS] = '/notification/fetch';
|
||||
API_URLS[ApiKeys.NOTIFICATION_ACTION] = '/notification/action';
|
||||
API_URLS[ApiKeys.NOTIFICATION_DELIVERED] = '/notification/delivered';
|
||||
API_URLS[ApiKeys.SEND_LOCATION] = '/geolocations/agents';
|
||||
API_URLS[ApiKeys.VERIFY_GOOGLE_SIGN_IN] = '/auth/session/internal/exchange';
|
||||
API_URLS[ApiKeys.VERIFY_GOOGLE_SIGN_IN] = '/auth/session/internal/exchange/v2';
|
||||
API_URLS[ApiKeys.SYNC_TIME] = '/sync/server-timestamp';
|
||||
API_URLS[ApiKeys.IS_DATA_SYNC_REQUIRED] = '/sync-data/is-sync-required';
|
||||
API_URLS[ApiKeys.GET_PRE_SIGNED_URL_DATA_SYNC] = '/sync-data/get-pre-signed-url';
|
||||
|
||||
@@ -27,7 +27,7 @@ export const sendLocationToServer = async (location: GeoCoordinates) => {
|
||||
export const getSyncTime = async () => {
|
||||
try {
|
||||
const url = getApiUrl(ApiKeys.SYNC_TIME);
|
||||
const response = await axiosInstance.get(url);
|
||||
const response = await axiosInstance.get(url, { headers: { donotHandleError: true } });
|
||||
return response?.data?.currentTimestamp;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
|
||||
@@ -180,7 +180,10 @@ const useFirestoreUpdates = () => {
|
||||
|
||||
const subscribeToCases = () => {
|
||||
const collectionPath = `allocations/${user?.referenceId}/cases`;
|
||||
return subscribeToCollection(handleCasesUpdate, collectionPath);
|
||||
return firestore()
|
||||
.collection(collectionPath)
|
||||
.orderBy('totalOverdueAmount', 'asc') // It is descending order only, but acting weirdly. Need to check.
|
||||
.onSnapshot(handleCasesUpdate, (err) => handleError(err, collectionPath));
|
||||
};
|
||||
|
||||
const subscribeToAvTemplate = () => {
|
||||
|
||||
@@ -1,58 +1,33 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import Geolocation from 'react-native-geolocation-service';
|
||||
import { useAppDispatch, useAppSelector } from '.';
|
||||
import { setIsDeviceLocationEnabled } from '../reducer/foregroundServiceSlice';
|
||||
import usePolling from './usePolling';
|
||||
import { locationEnabled } from '../components/utlis/DeviceUtils';
|
||||
import { logError } from '../components/utlis/errorUtils';
|
||||
import { MILLISECONDS_IN_A_SECOND } from '../../RN-UI-LIB/src/utlis/common';
|
||||
|
||||
enum LocationState {
|
||||
INITIATING = 'INITIATING',
|
||||
ENABLED = 'ENABLED',
|
||||
DISABLED = 'DISABLED',
|
||||
}
|
||||
|
||||
export enum PositionError {
|
||||
PERMISSION_DENIED = 1,
|
||||
POSITION_UNAVAILABLE = 2,
|
||||
}
|
||||
const CHECK_DEVICE_LOCATION_INTERVAL = 2 * MILLISECONDS_IN_A_SECOND;
|
||||
|
||||
const useIsLocationEnabled = () => {
|
||||
const [geolocationPosition, setGeolocationPosition] = useState<LocationState>(
|
||||
LocationState.INITIATING
|
||||
);
|
||||
const { isDeviceLocationEnabled } = useAppSelector((state) => state.foregroundService);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
let watchId: number | null = null;
|
||||
watchId = Geolocation.watchPosition(
|
||||
(pos) => {
|
||||
setGeolocationPosition(LocationState.ENABLED);
|
||||
},
|
||||
(err) => {
|
||||
// When device has no location service access
|
||||
const { PERMISSION_DENIED, POSITION_UNAVAILABLE } = PositionError;
|
||||
if (err.code === PERMISSION_DENIED || err.code === POSITION_UNAVAILABLE) {
|
||||
setGeolocationPosition(LocationState.DISABLED);
|
||||
}
|
||||
},
|
||||
{ enableHighAccuracy: true, distanceFilter: 0, showLocationDialog: false }
|
||||
);
|
||||
return () => {
|
||||
if (watchId) {
|
||||
Geolocation.clearWatch(watchId);
|
||||
const checkLocationEnabled = async () => {
|
||||
try {
|
||||
const isLocationEnabled = await locationEnabled();
|
||||
if (!isDeviceLocationEnabled && isLocationEnabled) {
|
||||
dispatch(setIsDeviceLocationEnabled(isLocationEnabled));
|
||||
return;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
if (isDeviceLocationEnabled && !isLocationEnabled) {
|
||||
dispatch(setIsDeviceLocationEnabled(isLocationEnabled));
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
logError(err as Error, 'Error while using location module');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (geolocationPosition === LocationState.INITIATING) {
|
||||
return;
|
||||
}
|
||||
if (geolocationPosition === LocationState.ENABLED && !isDeviceLocationEnabled) {
|
||||
dispatch(setIsDeviceLocationEnabled(true));
|
||||
} else if (geolocationPosition === LocationState.DISABLED && isDeviceLocationEnabled) {
|
||||
dispatch(setIsDeviceLocationEnabled(false));
|
||||
}
|
||||
}, [geolocationPosition, isDeviceLocationEnabled]);
|
||||
usePolling(checkLocationEnabled, CHECK_DEVICE_LOCATION_INTERVAL);
|
||||
|
||||
return isDeviceLocationEnabled;
|
||||
};
|
||||
|
||||
38
src/hooks/usePolling.ts
Normal file
38
src/hooks/usePolling.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
type PollingCallback = () => void;
|
||||
|
||||
const usePolling = (callback: PollingCallback, interval: number): (() => void) => {
|
||||
const savedCallback = useRef<PollingCallback>(callback);
|
||||
const intervalIdRef = useRef<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
savedCallback.current = callback;
|
||||
}, [callback]);
|
||||
|
||||
useEffect(() => {
|
||||
const poll = () => {
|
||||
savedCallback.current();
|
||||
};
|
||||
|
||||
intervalIdRef.current = setInterval(poll, interval);
|
||||
|
||||
return () => {
|
||||
if (intervalIdRef.current) {
|
||||
clearInterval(intervalIdRef.current);
|
||||
intervalIdRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [interval]);
|
||||
|
||||
const stop = () => {
|
||||
if (intervalIdRef.current) {
|
||||
clearInterval(intervalIdRef.current);
|
||||
intervalIdRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
return stop;
|
||||
};
|
||||
|
||||
export default usePolling;
|
||||
@@ -81,7 +81,7 @@ const getCaseListComponents = (casesList: ICaseItem[], caseDetails: Record<strin
|
||||
return { pendingList, completedList, pinnedList };
|
||||
};
|
||||
|
||||
export const getUpdatedCollectionCaseDetail = ({ caseData, answer, coords }: any) => {
|
||||
export const getUpdatedCollectionCaseDetail = ({ caseData, answer, coords, templateId }: any) => {
|
||||
const updatedValue = { ...caseData };
|
||||
updatedValue.isSynced = false;
|
||||
updatedValue.isApiCalled = false;
|
||||
@@ -99,6 +99,7 @@ export const getUpdatedCollectionCaseDetail = ({ caseData, answer, coords }: any
|
||||
};
|
||||
updatedValue.answer = newAnswer;
|
||||
updatedValue.coords = coords;
|
||||
updatedValue.templateId = templateId;
|
||||
return updatedValue;
|
||||
};
|
||||
|
||||
@@ -194,6 +195,7 @@ export const getUpdatedAVCaseDetail = ({
|
||||
updatedValue.caseVerdict = caseVerdict.EXHAUSTED;
|
||||
}
|
||||
updatedValue.coords = coords;
|
||||
updatedValue.templateId = templateId;
|
||||
return updatedValue;
|
||||
};
|
||||
|
||||
|
||||
@@ -84,8 +84,21 @@ const Profile: React.FC = () => {
|
||||
<View style={GenericStyles.fill}>
|
||||
<NavigationHeader title={name} subTitle={phoneNumber} showAvatarIcon />
|
||||
<ScrollView>
|
||||
<View style={[GenericStyles.p16, GenericStyles.mt8]}>
|
||||
<View style={[GenericStyles.row, GenericStyles.alignCenter]}>
|
||||
<View
|
||||
style={[
|
||||
GenericStyles.ph16,
|
||||
GenericStyles.pt16,
|
||||
GenericStyles.whiteBackground,
|
||||
numberOfCompletedCases === 2 ? { paddingBottom: 6 } : {},
|
||||
]}
|
||||
>
|
||||
<View
|
||||
style={[
|
||||
GenericStyles.row,
|
||||
GenericStyles.alignCenter,
|
||||
numberOfCompletedCases ? { paddingBottom: 12 } : GenericStyles.pb16,
|
||||
]}
|
||||
>
|
||||
<View style={[GenericStyles.ml4, GenericStyles.mr8]}>
|
||||
<GroupIcon />
|
||||
</View>
|
||||
@@ -104,40 +117,39 @@ const Profile: React.FC = () => {
|
||||
style={[
|
||||
GenericStyles.w100,
|
||||
GenericStyles.br8,
|
||||
GenericStyles.mt8,
|
||||
GenericStyles.mb8,
|
||||
GenericStyles.mt6,
|
||||
GenericStyles.mb12,
|
||||
GenericStyles.whiteBackground,
|
||||
]}
|
||||
onPress={handleViewAllCases}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
<View style={styles.logoutContainer}>
|
||||
<View style={[styles.logoutContainer, GenericStyles.whiteBackground]}>
|
||||
<TouchableOpacity
|
||||
onPress={handleLogout}
|
||||
style={[GenericStyles.row, GenericStyles.alignCenter]}
|
||||
style={[GenericStyles.row, GenericStyles.centerAligned, GenericStyles.fill]}
|
||||
>
|
||||
<LogoutIcon />
|
||||
<Text style={styles.logoutText}>Logout</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<Pressable
|
||||
style={[
|
||||
GenericStyles.row,
|
||||
GenericStyles.centerAligned,
|
||||
GenericStyles.fill,
|
||||
GenericStyles.w100,
|
||||
GenericStyles.mt8,
|
||||
]}
|
||||
onPress={appVersionClickHandler}
|
||||
>
|
||||
<Text bold dark style={styles.version}>
|
||||
App Version: {getAppVersion()} Gradle Version: {VersionNumber.appVersion} Gradle Build
|
||||
No: {VersionNumber.buildVersion}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</ScrollView>
|
||||
<Pressable
|
||||
style={[
|
||||
GenericStyles.row,
|
||||
GenericStyles.centerAligned,
|
||||
GenericStyles.fill,
|
||||
GenericStyles.w100,
|
||||
styles.appDetailsText,
|
||||
]}
|
||||
onPress={appVersionClickHandler}
|
||||
>
|
||||
<Text bold dark style={styles.version}>
|
||||
App Version: {getAppVersion()}
|
||||
Gradle Version: {VersionNumber.appVersion}
|
||||
Gradle Build No: {VersionNumber.buildVersion}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -146,9 +158,9 @@ export default Profile;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
logoutContainer: {
|
||||
backgroundColor: COLORS.BACKGROUND.PRIMARY,
|
||||
paddingVertical: 16,
|
||||
paddingHorizontal: 34,
|
||||
paddingHorizontal: 20,
|
||||
marginTop: 16,
|
||||
},
|
||||
logoutText: {
|
||||
marginLeft: 6,
|
||||
@@ -156,12 +168,8 @@ const styles = StyleSheet.create({
|
||||
version: {
|
||||
fontSize: 10,
|
||||
color: COLORS.TEXT.GREY,
|
||||
paddingLeft: 10,
|
||||
paddingBottom: 10,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
appDetailsText: {
|
||||
position: 'absolute',
|
||||
bottom: 20,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -56,22 +56,32 @@ const AddressContainer: React.FC<IAddressContainer> = ({ addressList }) => {
|
||||
}
|
||||
}}
|
||||
customExpandUi={{
|
||||
whenCollapsed: <Text style={addressAccordionToggleStyle}>View Details</Text>,
|
||||
whenExpanded: <Text style={addressAccordionToggleStyle}>Hide Details</Text>,
|
||||
whenCollapsed: address?.similarAddresses.length ? (
|
||||
<Text style={addressAccordionToggleStyle}>View Details</Text>
|
||||
) : (
|
||||
<></>
|
||||
),
|
||||
whenExpanded: address?.similarAddresses.length ? (
|
||||
<Text style={addressAccordionToggleStyle}>Hide Details</Text>
|
||||
) : (
|
||||
<></>
|
||||
),
|
||||
}}
|
||||
>
|
||||
<View>
|
||||
<Text style={[styles.textContainer, styles.accordionDetailHeading]}>
|
||||
Similar Addresses
|
||||
</Text>
|
||||
{address?.similarAddresses?.map((similarAddress: any) => (
|
||||
<AddressItem
|
||||
key={similarAddress?.addressDTO?.externalReferenceId}
|
||||
addressItem={similarAddress?.addressDTO}
|
||||
containerStyle={styles.card}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
{address?.similarAddresses.length ? (
|
||||
<View>
|
||||
<Text style={[styles.textContainer, styles.accordionDetailHeading]}>
|
||||
Similar Addresses
|
||||
</Text>
|
||||
{address?.similarAddresses?.map((similarAddress: any) => (
|
||||
<AddressItem
|
||||
key={similarAddress?.addressDTO?.externalReferenceId}
|
||||
addressItem={similarAddress?.addressDTO}
|
||||
containerStyle={styles.card}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
) : null}
|
||||
</Accordion>
|
||||
))}
|
||||
</View>
|
||||
|
||||
@@ -139,6 +139,7 @@ const NewAddressContainer: React.FC<INewAddressContainer> = ({ route: routeParam
|
||||
onBlur={onBlur}
|
||||
placeholder="Enter here"
|
||||
value={value}
|
||||
required
|
||||
/>
|
||||
)}
|
||||
name="lineOne"
|
||||
@@ -156,6 +157,7 @@ const NewAddressContainer: React.FC<INewAddressContainer> = ({ route: routeParam
|
||||
onBlur={onBlur}
|
||||
placeholder="Enter here"
|
||||
value={value}
|
||||
required
|
||||
/>
|
||||
)}
|
||||
name="lineTwo"
|
||||
@@ -175,6 +177,7 @@ const NewAddressContainer: React.FC<INewAddressContainer> = ({ route: routeParam
|
||||
onBlur={onBlur}
|
||||
placeholder="Enter here"
|
||||
value={value}
|
||||
required
|
||||
/>
|
||||
)}
|
||||
name="pinCode"
|
||||
@@ -192,6 +195,7 @@ const NewAddressContainer: React.FC<INewAddressContainer> = ({ route: routeParam
|
||||
onBlur={onBlur}
|
||||
placeholder="Enter here"
|
||||
value={value}
|
||||
required
|
||||
/>
|
||||
)}
|
||||
name="city"
|
||||
|
||||
@@ -263,8 +263,16 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
selectBtn: {
|
||||
position: 'absolute',
|
||||
top: 12,
|
||||
right: 12,
|
||||
paddingTop: 12,
|
||||
right: 0,
|
||||
paddingRight: 12,
|
||||
width: 80,
|
||||
height: 80,
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-start',
|
||||
alignItems: 'flex-end',
|
||||
fill: 1,
|
||||
zIndex: 100,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -267,13 +267,11 @@ export enum InteractionStatuses {
|
||||
export interface IOutstandingEmiDetail {
|
||||
referenceId: string;
|
||||
emiAmount: number;
|
||||
otherFees: number;
|
||||
emiPenaltyCharges: number;
|
||||
}
|
||||
|
||||
export interface IOutstandingAmountBreakup {
|
||||
emiAmount: number;
|
||||
otherFees: number;
|
||||
emiPenaltyCharges: number;
|
||||
totalOverdueAmount: number;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import TrackingComponent from '../../common/TrackingComponent';
|
||||
import useFCM from '../../hooks/useFCM';
|
||||
import { NetworkStatusService } from '../../services/network-monitoring.service';
|
||||
import BlockerScreen from '../../common/BlockerScreen';
|
||||
import UnstoppableService from '../../services/foregroundServices/foreground.service';
|
||||
|
||||
const AuthRouter = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
@@ -46,6 +47,14 @@ const AuthRouter = () => {
|
||||
Linking.addEventListener('url', (event) => handleUrl(event.url));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoggedIn) {
|
||||
if (UnstoppableService.isRunning()) {
|
||||
UnstoppableService.stopAll();
|
||||
}
|
||||
}
|
||||
}, [isLoggedIn]);
|
||||
|
||||
// Firebase cloud messaging listener
|
||||
useFCM();
|
||||
|
||||
|
||||
@@ -2,35 +2,35 @@ import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { _map } from '../../../RN-UI-LIB/src/utlis/common';
|
||||
import { UnifiedCaseDetailsTypes, getCaseUnifiedData } from '../../action/caseApiActions';
|
||||
import { getNotifications, notificationAction } from '../../action/notificationActions';
|
||||
import { SCREEN_ANIMATION_DURATION } from '../../common/Constants';
|
||||
import Widget from '../../components/form';
|
||||
import { getScreenFocusListenerObj } from '../../components/utlis/commonFunctions';
|
||||
import { getTemplateRoute } from '../../components/utlis/navigationUtlis';
|
||||
import { useAppDispatch, useAppSelector } from '../../hooks';
|
||||
import useFirestoreUpdates from '../../hooks/useFirestoreUpdates';
|
||||
import useIsOnline from '../../hooks/useIsOnline';
|
||||
import { resetNewVisitedCases } from '../../reducer/allCasesSlice';
|
||||
import { RootState } from '../../store/store';
|
||||
import Profile from '../Profile';
|
||||
import AddNewNumber from '../addNewNumber';
|
||||
import AddressGeolocation from '../addressGeolocation';
|
||||
import NewAddressContainer from '../addressGeolocation/NewAddressContainer';
|
||||
import AllCasesMain from '../allCases';
|
||||
import CompletedCase from '../allCases/CompletedCase';
|
||||
import { CaseAllocationType } from '../allCases/interface';
|
||||
import CaseDetails from '../caseDetails/CaseDetails';
|
||||
import CollectionCaseDetails from '../caseDetails/CollectionCaseDetail';
|
||||
import interactionsHandler from '../caseDetails/interactionsHandler';
|
||||
import Profile from '../Profile';
|
||||
import RegisterPayments from '../registerPayements/RegisterPayments';
|
||||
import TodoList from '../todoList/TodoList';
|
||||
import { RootState } from '../../store/store';
|
||||
import { CaseAllocationType } from '../allCases/interface';
|
||||
import { getTemplateRoute } from '../../components/utlis/navigationUtlis';
|
||||
import { resetNewVisitedCases } from '../../reducer/allCasesSlice';
|
||||
import { getCaseUnifiedData, UnifiedCaseDetailsTypes } from '../../action/caseApiActions';
|
||||
import FeedbackDetailContainer from '../caseDetails/feedback/FeedbackDetailContainer';
|
||||
import EmiSchedule from '../emiSchedule';
|
||||
import AddNewNumber from '../addNewNumber';
|
||||
import Notifications from '../notifications';
|
||||
import { getNotifications, notificationAction } from '../../action/notificationActions';
|
||||
import useIsOnline from '../../hooks/useIsOnline';
|
||||
import CustomerProfile from '../caseDetails/CustomerProfile';
|
||||
import VKYCFullScreen from '../caseDetails/VKYCFullScreen';
|
||||
import { SCREEN_ANIMATION_DURATION } from '../../common/Constants';
|
||||
import { getScreenFocusListenerObj } from '../../components/utlis/commonFunctions';
|
||||
import FeedbackDetailContainer from '../caseDetails/feedback/FeedbackDetailContainer';
|
||||
import interactionsHandler from '../caseDetails/interactionsHandler';
|
||||
import EmiSchedule from '../emiSchedule';
|
||||
import ImpersonatedUser from '../impersonatedUser';
|
||||
import Notifications from '../notifications';
|
||||
import RegisterPayments from '../registerPayements/RegisterPayments';
|
||||
import TodoList from '../todoList/TodoList';
|
||||
|
||||
const Stack = createNativeStackNavigator();
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ const CollectionCaseData: React.FC<ICollectionCaseData> = ({ caseData }) => {
|
||||
</Text>
|
||||
)}
|
||||
<View style={[GenericStyles.row, GenericStyles.mv8]}>
|
||||
<Text style={[GenericStyles.chip, GenericStyles.whiteBackground, GenericStyles.mr8]} small>
|
||||
<Text style={[GenericStyles.chip, GenericStyles.whiteBackground]} small>
|
||||
Current DPD {currentDpd}
|
||||
</Text>
|
||||
{loanAccountNumber && <LANChip loanAccountNumber={loanAccountNumber} />}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
Animated,
|
||||
Pressable,
|
||||
@@ -34,13 +34,12 @@ import { _map } from '../../../RN-UI-LIB/src/utlis/common';
|
||||
import FeedbackListContainer from './feedback/FeedbackListContainer';
|
||||
import { RootState } from '../../store/store';
|
||||
import { IFeedback, IUnSyncedFeedbackItem } from '../../types/feedback.types';
|
||||
import { setFeedbackHistory } from '../../reducer/feedbackHistorySlice';
|
||||
import OutstandingAmountBreakupBottomSheet from './OutstandingAmountBreakupBottomSheet';
|
||||
import { getCaseUnifiedData, UnifiedCaseDetailsTypes } from '../../action/caseApiActions';
|
||||
import useIsOnline from '../../hooks/useIsOnline';
|
||||
import { CLICKSTREAM_EVENT_NAMES } from '../../common/Constants';
|
||||
import { addClickstreamEvent } from '../../services/clickstreamEventService';
|
||||
import { getLoanAccountNumber } from '../../components/utlis/commonFunctions';
|
||||
import EmiBreakupBottomSheet from '../emiSchedule/EmiBreakupBottomSheet';
|
||||
|
||||
interface ICaseDetails {
|
||||
route: {
|
||||
@@ -56,15 +55,12 @@ const getOutstandingAmountBreakUp = (
|
||||
) => {
|
||||
const outstandingAmountBreakup: IOutstandingAmountBreakup = {
|
||||
emiAmount: 0,
|
||||
otherFees: 0,
|
||||
emiPenaltyCharges: 0,
|
||||
totalOverdueAmount: totalOverdueAmount,
|
||||
};
|
||||
_map(outstandingEmiDetails, (record) => {
|
||||
outstandingAmountBreakup.emiAmount =
|
||||
outstandingAmountBreakup.emiAmount + (record.emiAmount ? Number(record.emiAmount) : 0);
|
||||
outstandingAmountBreakup.otherFees =
|
||||
outstandingAmountBreakup.otherFees + (record.otherFees ? Number(record.otherFees) : 0);
|
||||
outstandingAmountBreakup.emiPenaltyCharges =
|
||||
outstandingAmountBreakup.emiPenaltyCharges +
|
||||
(record.emiPenaltyCharges ? Number(record.emiPenaltyCharges) : 0);
|
||||
@@ -143,6 +139,11 @@ const CollectionCaseDetails: React.FC<ICaseDetails> = (props) => {
|
||||
const [showOutstandingAmountBottomSheet, setShowOutstandingAmountBottomSheet] =
|
||||
React.useState(false);
|
||||
const [showPhoneNumberSheet, setShowPhoneNumberSheet] = React.useState(false);
|
||||
const {
|
||||
totalOverdueAmount: outstandingTotalOverDueAmount,
|
||||
emiAmount,
|
||||
emiPenaltyCharges,
|
||||
} = getOutstandingAmountBreakUp(caseDetail.outstandingEmiDetails, caseDetail.totalOverdueAmount);
|
||||
|
||||
const handleCustomerCall = () => {
|
||||
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.AV_CASE_DETAILS_CALL_CUSTOMER_CLICKED, {
|
||||
@@ -279,68 +280,98 @@ const CollectionCaseDetails: React.FC<ICaseDetails> = (props) => {
|
||||
styles.secondSection,
|
||||
getShadowStyle(2),
|
||||
GenericStyles.mt16,
|
||||
GenericStyles.pv24,
|
||||
GenericStyles.ph16,
|
||||
]}
|
||||
>
|
||||
<View style={[GenericStyles.row, GenericStyles.alignCenter, GenericStyles.ph8]}>
|
||||
<View>
|
||||
<View style={[GenericStyles.row, GenericStyles.pb4, GenericStyles.alignCenter]}>
|
||||
<Text style={[GenericStyles.redText, GenericStyles.fontSize16]}>
|
||||
{formatAmount(totalOverdueAmount)}
|
||||
</Text>
|
||||
<Text
|
||||
style={[GenericStyles.pl4, GenericStyles.redText, GenericStyles.fontSize12]}
|
||||
<View style={[GenericStyles.row, GenericStyles.alignCenter, GenericStyles.relative]}>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
GenericStyles.pv24,
|
||||
{
|
||||
flexBasis: '50%',
|
||||
},
|
||||
GenericStyles.centerAligned,
|
||||
]}
|
||||
activeOpacity={0.7}
|
||||
onPress={() => {
|
||||
addClickstreamEvent(
|
||||
CLICKSTREAM_EVENT_NAMES.FA_TOTAL_OUTSTANDING_BREAKUP_CLICKED,
|
||||
{ lan: loanAccountNumber }
|
||||
);
|
||||
setShowOutstandingAmountBottomSheet(true);
|
||||
}}
|
||||
>
|
||||
<View style={GenericStyles.fill}>
|
||||
<View
|
||||
style={[
|
||||
GenericStyles.row,
|
||||
GenericStyles.pb4,
|
||||
GenericStyles.alignCenter,
|
||||
GenericStyles.justifyStart,
|
||||
]}
|
||||
>
|
||||
Total due
|
||||
</Text>
|
||||
</View>
|
||||
{caseDetail.outstandingEmiDetails &&
|
||||
caseDetail.outstandingEmiDetails.length > 0 && (
|
||||
<View style={[GenericStyles.row, GenericStyles.alignCenter]}>
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
addClickstreamEvent(
|
||||
CLICKSTREAM_EVENT_NAMES.FA_TOTAL_OUTSTANDING_BREAKUP_CLICKED,
|
||||
{ lan: loanAccountNumber }
|
||||
);
|
||||
setShowOutstandingAmountBottomSheet(true);
|
||||
}}
|
||||
style={[GenericStyles.row, GenericStyles.alignCenter]}
|
||||
>
|
||||
<Text style={[styles.emiCardCtas]}>View total overdue amt</Text>
|
||||
<Text style={[GenericStyles.redText, GenericStyles.fontSize16]}>
|
||||
{formatAmount(totalOverdueAmount)}
|
||||
</Text>
|
||||
<Text
|
||||
style={[GenericStyles.pl4, GenericStyles.redText, GenericStyles.fontSize12]}
|
||||
>
|
||||
Total due
|
||||
</Text>
|
||||
</View>
|
||||
{caseDetail.outstandingEmiDetails &&
|
||||
caseDetail.outstandingEmiDetails.length > 0 && (
|
||||
<View style={[GenericStyles.row, GenericStyles.alignCenter]}>
|
||||
<Text style={[styles.emiCardCtas]}>View breakup</Text>
|
||||
<Chevron fillColor={COLORS.TEXT.BLUE} />
|
||||
</Pressable>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<View style={styles.verticalLine} />
|
||||
<View>
|
||||
<View style={[GenericStyles.row, GenericStyles.pb4, GenericStyles.alignCenter]}>
|
||||
<Text style={[styles.yellowText, GenericStyles.fontSize16]}>
|
||||
{caseDetail?.outstandingEmiDetails?.length}
|
||||
</Text>
|
||||
<Text style={[GenericStyles.pl4, styles.yellowText, GenericStyles.fontSize12]}>
|
||||
Overdue EMI's
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<View style={[GenericStyles.row, GenericStyles.alignCenter]}>
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_VIEW_EMI_SCHEDULE_CLICKED, {
|
||||
lan: loanAccountNumber,
|
||||
});
|
||||
navigateToScreen(PageRouteEnum.EMI_SCHEDULE, { loanAccountNumber, caseId });
|
||||
}}
|
||||
style={[GenericStyles.row, GenericStyles.alignCenter]}
|
||||
>
|
||||
</TouchableOpacity>
|
||||
<View style={[styles.verticalLine]} />
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
GenericStyles.pv24,
|
||||
{
|
||||
flexBasis: '50%',
|
||||
},
|
||||
GenericStyles.centerAligned,
|
||||
]}
|
||||
activeOpacity={0.7}
|
||||
onPress={() => {
|
||||
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_VIEW_EMI_SCHEDULE_CLICKED, {
|
||||
lan: loanAccountNumber,
|
||||
});
|
||||
navigateToScreen(PageRouteEnum.EMI_SCHEDULE, { loanAccountNumber, caseId });
|
||||
}}
|
||||
>
|
||||
<View style={GenericStyles.fill}>
|
||||
<View style={[GenericStyles.row, GenericStyles.pb4, GenericStyles.alignCenter]}>
|
||||
<Text style={[styles.yellowText, GenericStyles.fontSize16]}>
|
||||
{caseDetail?.outstandingEmiDetails?.length}
|
||||
</Text>
|
||||
<Text
|
||||
style={[GenericStyles.pl4, styles.yellowText, GenericStyles.fontSize12]}
|
||||
>
|
||||
Overdue EMI's
|
||||
</Text>
|
||||
</View>
|
||||
<View style={[GenericStyles.row, GenericStyles.alignCenter]}>
|
||||
<Text style={[styles.emiCardCtas]}>EMI schedule</Text>
|
||||
<Chevron fillColor={COLORS.TEXT.BLUE} />
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<View style={styles.horizontalLine} />
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
GenericStyles.whiteBackground,
|
||||
styles.secondSection,
|
||||
getShadowStyle(2),
|
||||
GenericStyles.mt16,
|
||||
]}
|
||||
>
|
||||
<Button
|
||||
onPress={collectMoneyCta}
|
||||
title="Collect money"
|
||||
@@ -365,7 +396,7 @@ const CollectionCaseDetails: React.FC<ICaseDetails> = (props) => {
|
||||
}
|
||||
rightIcon={
|
||||
<View style={styles.rightButtonIcon}>
|
||||
<Chevron fillColor={COLORS.BACKGROUND.LIGHT} />
|
||||
<Chevron fillColor={COLORS.TEXT.BLUE} />
|
||||
</View>
|
||||
}
|
||||
/>
|
||||
@@ -443,13 +474,12 @@ const CollectionCaseDetails: React.FC<ICaseDetails> = (props) => {
|
||||
setShowPhoneNumberBottomSheet={setShowPhoneNumberSheet}
|
||||
handleAddNewNumberCta={handleAddNewNumberCta}
|
||||
/>
|
||||
<OutstandingAmountBreakupBottomSheet
|
||||
outstandingEmiDetails={getOutstandingAmountBreakUp(
|
||||
caseDetail.outstandingEmiDetails,
|
||||
caseDetail.totalOverdueAmount
|
||||
)}
|
||||
setShowOutstandingAmountBreakupBottomSheet={setShowOutstandingAmountBottomSheet}
|
||||
showOutstandingAmountBreakupBottomSheet={showOutstandingAmountBottomSheet}
|
||||
<EmiBreakupBottomSheet
|
||||
openBottomSheet={showOutstandingAmountBottomSheet}
|
||||
setOpenBottomSheet={setShowOutstandingAmountBottomSheet}
|
||||
totalUnpaidEmiPenaltyCharges={emiPenaltyCharges}
|
||||
totalUnpaidEmiAmount={emiAmount}
|
||||
totalOverDueAmount={outstandingTotalOverDueAmount}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
</Layout>
|
||||
@@ -472,9 +502,11 @@ export const styles = StyleSheet.create({
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
padding: 12,
|
||||
marginBottom: 8,
|
||||
justifyContent: 'space-around',
|
||||
alignItems: 'center',
|
||||
backgroundColor: COLORS.BACKGROUND.PRIMARY,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: COLORS.BORDER.PRIMARY,
|
||||
},
|
||||
chevronContainer: {
|
||||
marginLeft: 14,
|
||||
@@ -511,8 +543,9 @@ export const styles = StyleSheet.create({
|
||||
verticalLine: {
|
||||
height: 32,
|
||||
width: 1,
|
||||
marginHorizontal: 24,
|
||||
backgroundColor: COLORS.BORDER.PRIMARY,
|
||||
position: 'absolute',
|
||||
left: '50%',
|
||||
},
|
||||
leftButtonIcon: {
|
||||
backgroundColor: COLORS.TEXT.GREEN,
|
||||
@@ -524,21 +557,25 @@ export const styles = StyleSheet.create({
|
||||
alignContent: 'center',
|
||||
justifyContent: 'center',
|
||||
position: 'relative',
|
||||
marginLeft: 16,
|
||||
},
|
||||
rightButtonIcon: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end',
|
||||
paddingRight: 6,
|
||||
paddingRight: 22,
|
||||
},
|
||||
collectCtaStyle1: {
|
||||
flex: 1,
|
||||
width: '100%',
|
||||
height: 68,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
collectCtaStyle2: {
|
||||
flex: 1,
|
||||
marginTop: 36,
|
||||
width: '100%',
|
||||
borderRadius: 0,
|
||||
},
|
||||
collectLeftStyle: {
|
||||
color: COLORS.TEXT.WHITE,
|
||||
|
||||
@@ -154,10 +154,6 @@ const CustomerProfile: React.FC<ICustomerProfile> = (props) => {
|
||||
<Text style={[styles.errorText]} bold>
|
||||
Error loading image
|
||||
</Text>
|
||||
) : errorModalImage ? (
|
||||
<Text style={[styles.errorText]} bold>
|
||||
Error loading image
|
||||
</Text>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -28,19 +28,25 @@ const LANChip: React.FC<ILANChip> = ({ loanAccountNumber }) => {
|
||||
});
|
||||
}
|
||||
};
|
||||
const isClipped = loanAccountNumber?.length >= 13;
|
||||
const clippedStyle = isClipped ? GenericStyles.fill : {};
|
||||
|
||||
return (
|
||||
<TouchableHighlight style={[GenericStyles.ml8]} onPress={copyLAN}>
|
||||
<TouchableHighlight style={[GenericStyles.ml8, clippedStyle]} onPress={copyLAN}>
|
||||
<View
|
||||
style={[
|
||||
GenericStyles.chip,
|
||||
GenericStyles.whiteBackground,
|
||||
GenericStyles.row,
|
||||
GenericStyles.alignCenter,
|
||||
GenericStyles.centerAligned,
|
||||
]}
|
||||
>
|
||||
<Text small>LAN {loanAccountNumber}</Text>
|
||||
<View style={[GenericStyles.ml4]}>
|
||||
<View style={clippedStyle}>
|
||||
<Text numberOfLines={1} ellipsizeMode={isClipped ? 'tail' : undefined} small>
|
||||
LAN {loanAccountNumber}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={isClipped ? {} : GenericStyles.ml4}>
|
||||
<CopyIcon />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { IOutstandingAmountBreakup } from '../allCases/interface';
|
||||
import { StyleSheet, TouchableOpacity, View } from 'react-native';
|
||||
import { GenericStyles } from '../../../RN-UI-LIB/src/styles';
|
||||
import Heading from '../../../RN-UI-LIB/src/components/Heading';
|
||||
import CloseIcon from '../../../RN-UI-LIB/src/Icons/CloseIcon';
|
||||
import { COLORS } from '../../../RN-UI-LIB/src/styles/colors';
|
||||
import BottomSheet from '../../../RN-UI-LIB/src/components/bottom_sheet/BottomSheet';
|
||||
import Text from '../../../RN-UI-LIB/src/components/Text';
|
||||
import { formatAmount } from '../../../RN-UI-LIB/src/utlis/amount';
|
||||
|
||||
interface IOutstandingAmountBreakupProps {
|
||||
outstandingEmiDetails: IOutstandingAmountBreakup;
|
||||
setShowOutstandingAmountBreakupBottomSheet: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
showOutstandingAmountBreakupBottomSheet: boolean;
|
||||
}
|
||||
|
||||
const OutstandingAmountBreakupBottomSheet: React.FC<IOutstandingAmountBreakupProps> = ({
|
||||
outstandingEmiDetails,
|
||||
setShowOutstandingAmountBreakupBottomSheet,
|
||||
showOutstandingAmountBreakupBottomSheet,
|
||||
}) => {
|
||||
return (
|
||||
<BottomSheet
|
||||
heightPercentage={40}
|
||||
visible={showOutstandingAmountBreakupBottomSheet}
|
||||
HeaderNode={() => (
|
||||
<View
|
||||
style={[
|
||||
GenericStyles.row,
|
||||
GenericStyles.alignCenter,
|
||||
GenericStyles.justifyContentSpaceBetween,
|
||||
GenericStyles.p16,
|
||||
]}
|
||||
>
|
||||
<Heading dark type="h4">
|
||||
Total outstanding amount breakup
|
||||
</Heading>
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.7}
|
||||
onPress={() => setShowOutstandingAmountBreakupBottomSheet((prev) => !prev)}
|
||||
>
|
||||
<CloseIcon color={COLORS.TEXT.LIGHT} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
setVisible={() => setShowOutstandingAmountBreakupBottomSheet((prev) => !prev)}
|
||||
>
|
||||
<View style={[styles.container]}>
|
||||
<View
|
||||
style={[
|
||||
GenericStyles.row,
|
||||
GenericStyles.fill,
|
||||
GenericStyles.spaceBetween,
|
||||
GenericStyles.pb16,
|
||||
]}
|
||||
>
|
||||
<Text light>{`EMI Amount`}</Text>
|
||||
<Text dark>{formatAmount(outstandingEmiDetails.emiAmount)}</Text>
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
GenericStyles.row,
|
||||
GenericStyles.fill,
|
||||
GenericStyles.spaceBetween,
|
||||
GenericStyles.pb16,
|
||||
]}
|
||||
>
|
||||
<Text light>{`EMI Penalty Charges`}</Text>
|
||||
<Text dark>{formatAmount(outstandingEmiDetails.emiPenaltyCharges)}</Text>
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
GenericStyles.row,
|
||||
GenericStyles.fill,
|
||||
GenericStyles.spaceBetween,
|
||||
GenericStyles.pb16,
|
||||
]}
|
||||
>
|
||||
<Text light>{`Other Fees`}</Text>
|
||||
<Text dark>{formatAmount(outstandingEmiDetails.otherFees)}</Text>
|
||||
</View>
|
||||
<View style={[styles.horizontalLine]} />
|
||||
<View
|
||||
style={[
|
||||
GenericStyles.row,
|
||||
GenericStyles.fill,
|
||||
GenericStyles.spaceBetween,
|
||||
GenericStyles.pt16,
|
||||
]}
|
||||
>
|
||||
<Text dark>Total Overdue Amount</Text>
|
||||
<Text style={[GenericStyles.redText]}>
|
||||
{formatAmount(outstandingEmiDetails.totalOverdueAmount)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</BottomSheet>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
paddingHorizontal: 16,
|
||||
paddingBottom: 30,
|
||||
flex: 1,
|
||||
},
|
||||
horizontalLine: {
|
||||
backgroundColor: COLORS.TEXT.LIGHT,
|
||||
width: '100%',
|
||||
height: 0.5,
|
||||
},
|
||||
});
|
||||
|
||||
export default OutstandingAmountBreakupBottomSheet;
|
||||
@@ -166,6 +166,7 @@ const UserDetailsSection: React.FC<IUserDetailsSection> = (props) => {
|
||||
const styles = StyleSheet.create({
|
||||
infoContainer: {
|
||||
marginLeft: 16,
|
||||
flex: 1,
|
||||
},
|
||||
imageStyle: {
|
||||
height: 500,
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { ImageBackground, StyleSheet, View } from 'react-native';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import Text from '../../../../RN-UI-LIB/src/components/Text';
|
||||
import { GenericStyles } from '../../../../RN-UI-LIB/src/styles';
|
||||
import { COLORS } from '../../../../RN-UI-LIB/src/styles/colors';
|
||||
import {
|
||||
BUSINESS_DATE_FORMAT,
|
||||
BUSINESS_TIME_FORMAT,
|
||||
dateFormat,
|
||||
} from '../../../../RN-UI-LIB/src/utlis/dates';
|
||||
import { BUSINESS_DATE_FORMAT, dateFormat } from '../../../../RN-UI-LIB/src/utlis/dates';
|
||||
import { sanitizeString } from '../../../components/utlis/commonFunctions';
|
||||
import { AnswerType, IAnswerView, OPTION_TAG } from '../../../types/feedback.types';
|
||||
import { addClickstreamEvent } from '../../../services/clickstreamEventService';
|
||||
import { CLICKSTREAM_EVENT_NAMES } from '../../../common/Constants';
|
||||
import FeedbackDetailImageItem from './FeedbackDetailImageItem';
|
||||
|
||||
const getAnswerText = (answer: IAnswerView) => {
|
||||
switch (answer?.answerType) {
|
||||
@@ -34,6 +31,9 @@ interface IFeedbackDetailAnswerContainer {
|
||||
loanAccountNumber: string;
|
||||
activeFeedbackReferenceId: string;
|
||||
}
|
||||
export const getQuestionText = (answerItem: IAnswerView) => {
|
||||
return (answerItem.questionKey ? answerItem.questionName : answerItem.questionText) || '--';
|
||||
};
|
||||
|
||||
const FeedbackDetailAnswerContainer: React.FC<IFeedbackDetailAnswerContainer> = ({
|
||||
answerList,
|
||||
@@ -44,10 +44,6 @@ const FeedbackDetailAnswerContainer: React.FC<IFeedbackDetailAnswerContainer> =
|
||||
(answer) => answer.questionTag === OPTION_TAG.IMAGE_UPLOAD
|
||||
);
|
||||
|
||||
const getQuestionText = (answerItem: IAnswerView) => {
|
||||
return (answerItem.questionKey ? answerItem.questionName : answerItem.questionText) || '--';
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_CASE_DETAILS_INDIVIDUAL_FEEDBACK_CLICKED, {
|
||||
lan: loanAccountNumber,
|
||||
@@ -58,16 +54,7 @@ const FeedbackDetailAnswerContainer: React.FC<IFeedbackDetailAnswerContainer> =
|
||||
return (
|
||||
<View style={[styles.container]}>
|
||||
{imageAnswerList.map((image) => (
|
||||
<View style={[GenericStyles.columnDirection, GenericStyles.mv8]}>
|
||||
<Text style={[styles.textContainer, styles.questionText]}>{getQuestionText(image)}</Text>
|
||||
<ImageBackground
|
||||
style={[styles.image, GenericStyles.mt8]}
|
||||
imageStyle={styles.br8}
|
||||
source={{
|
||||
uri: image.inputText,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<FeedbackDetailImageItem image={image} />
|
||||
))}
|
||||
{answerList
|
||||
.filter((answer) => answer.questionTag !== OPTION_TAG.IMAGE_UPLOAD)
|
||||
@@ -86,10 +73,6 @@ const FeedbackDetailAnswerContainer: React.FC<IFeedbackDetailAnswerContainer> =
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
padding: 16,
|
||||
backgroundColor: COLORS.TEXT.GREY_LIGHT,
|
||||
},
|
||||
textContainer: {
|
||||
fontSize: 12,
|
||||
lineHeight: 18,
|
||||
@@ -99,17 +82,13 @@ const styles = StyleSheet.create({
|
||||
questionText: {
|
||||
color: COLORS.TEXT.LIGHT,
|
||||
},
|
||||
container: {
|
||||
padding: 16,
|
||||
backgroundColor: COLORS.TEXT.GREY_LIGHT,
|
||||
},
|
||||
answerText: {
|
||||
color: COLORS.TEXT.DARK,
|
||||
},
|
||||
br8: {
|
||||
borderRadius: 8,
|
||||
},
|
||||
image: {
|
||||
width: '100%',
|
||||
height: 350,
|
||||
resizeMode: 'contain',
|
||||
},
|
||||
});
|
||||
|
||||
export default FeedbackDetailAnswerContainer;
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Dimensions, RefreshControl, ScrollView, StyleSheet, View } from 'react-native';
|
||||
import { 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 Pagination from '../../../../RN-UI-LIB/src/components/Pagination';
|
||||
import Text from '../../../../RN-UI-LIB/src/components/Text';
|
||||
import { GenericStyles, SCREEN_HEIGHT, getShadowStyle } from '../../../../RN-UI-LIB/src/styles';
|
||||
import { GenericStyles, getShadowStyle } from '../../../../RN-UI-LIB/src/styles';
|
||||
import { COLORS } from '../../../../RN-UI-LIB/src/styles/colors';
|
||||
import { getPastFeedbacks } from '../../../action/feedbackActions';
|
||||
import { GenericType } from '../../../common/GenericTypes';
|
||||
@@ -18,6 +17,10 @@ import FeedbackDetailAnswerContainer from './FeedbackDetailAnswerContainer';
|
||||
import FeedbackDetailItem from './FeedbackDetailItem';
|
||||
import { addClickstreamEvent } from '../../../services/clickstreamEventService';
|
||||
import { CLICKSTREAM_EVENT_NAMES } from '../../../common/Constants';
|
||||
import Pagination from '../../../../RN-UI-LIB/src/components/pagination/Pagination';
|
||||
import Layout from '../../layout/Layout';
|
||||
import { toast } from '../../../../RN-UI-LIB/src/components/toast';
|
||||
import { ToastMessages } from './../../../screens/allCases/constants';
|
||||
|
||||
const PAGE_TITLE = 'All feedbacks';
|
||||
const SCROLL_LAYOUT_OFFSET = 10;
|
||||
@@ -103,70 +106,101 @@ const FeedbackDetailContainer: React.FC<IFeedbackDetailContainer> = ({ route: ro
|
||||
});
|
||||
}, [ref, dataSourceCord]);
|
||||
|
||||
const handlePageChange = (page: number) => {
|
||||
if (!isOnline) {
|
||||
toast({ type: 'info', text1: ToastMessages.OFFLINE_MESSAGE });
|
||||
return;
|
||||
}
|
||||
setCurrentPage(page);
|
||||
};
|
||||
|
||||
const handlePageChangeClickstream = (
|
||||
page: number,
|
||||
eventName: { name: string; description: string }
|
||||
) => {
|
||||
addClickstreamEvent(eventName, {
|
||||
currentPageNumber: page,
|
||||
lan: loanAccountNumber,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={[GenericStyles.fill, GenericStyles.whiteBackground]}>
|
||||
<NavigationHeader title={PAGE_TITLE} onBack={goBack} />
|
||||
<ScrollView
|
||||
refreshControl={<RefreshControl refreshing={loading} />}
|
||||
style={[GenericStyles.ph16, GenericStyles.mt16]}
|
||||
ref={(x) => setRef(x)}
|
||||
>
|
||||
{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.pv12, GenericStyles.br8, getShadowStyle(4)]}
|
||||
isActive={feedback.referenceId === activeFeedbackReferenceId}
|
||||
accordionHeader={
|
||||
<FeedbackDetailItem
|
||||
key={feedback.referenceId}
|
||||
feedbackItem={feedback}
|
||||
isExpanded={isExpanded}
|
||||
/>
|
||||
}
|
||||
customExpandUi={{
|
||||
whenCollapsed: <Text style={styles.accordionExpandBtn}>View more</Text>,
|
||||
whenExpanded: <Text style={styles.accordionExpandBtn}>View less</Text>,
|
||||
}}
|
||||
onExpanded={(value) => {
|
||||
setIsExpanded(value);
|
||||
<Layout>
|
||||
<View style={[GenericStyles.fill, GenericStyles.whiteBackground]}>
|
||||
<NavigationHeader title={PAGE_TITLE} onBack={goBack} />
|
||||
<ScrollView
|
||||
refreshControl={<RefreshControl refreshing={loading} />}
|
||||
style={[GenericStyles.ph16, GenericStyles.mt16]}
|
||||
ref={(x) => setRef(x)}
|
||||
>
|
||||
{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);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FeedbackDetailAnswerContainer
|
||||
answerList={feedback.answerViews}
|
||||
activeFeedbackReferenceId={feedback.referenceId}
|
||||
loanAccountNumber={loanAccountNumber}
|
||||
/>
|
||||
</Accordion>
|
||||
</View>
|
||||
))}
|
||||
</ScrollView>
|
||||
<Pagination
|
||||
onNext={() => {
|
||||
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_VIEW_PAST_FEEDBACK_NEXT_PAGE_CLICKED, {
|
||||
currentPageNumber: currentPage,
|
||||
lan: loanAccountNumber,
|
||||
});
|
||||
setCurrentPage((page) => page + 1);
|
||||
}}
|
||||
onPrev={() => {
|
||||
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_VIEW_PAST_FEEDBACK_PREV_PAGE_CLICKED, {
|
||||
currentPageNumber: currentPage,
|
||||
lan: loanAccountNumber,
|
||||
});
|
||||
setCurrentPage((page) => page - 1);
|
||||
}}
|
||||
currentPage={currentPage}
|
||||
totalPages={feedbackTotalPages}
|
||||
/>
|
||||
</View>
|
||||
<Accordion
|
||||
accordionStyle={[GenericStyles.pv12, GenericStyles.br8, getShadowStyle(4)]}
|
||||
isActive={feedback.referenceId === activeFeedbackReferenceId}
|
||||
accordionHeader={
|
||||
<FeedbackDetailItem
|
||||
key={feedback.referenceId}
|
||||
feedbackItem={feedback}
|
||||
isExpanded={isExpanded}
|
||||
/>
|
||||
}
|
||||
customExpandUi={{
|
||||
whenCollapsed: <Text style={styles.accordionExpandBtn}>View more</Text>,
|
||||
whenExpanded: <Text style={styles.accordionExpandBtn}>View less</Text>,
|
||||
}}
|
||||
onExpanded={(value) => {
|
||||
setIsExpanded(value);
|
||||
}}
|
||||
>
|
||||
<FeedbackDetailAnswerContainer
|
||||
answerList={feedback.answerViews}
|
||||
activeFeedbackReferenceId={feedback.referenceId}
|
||||
loanAccountNumber={loanAccountNumber}
|
||||
/>
|
||||
</Accordion>
|
||||
</View>
|
||||
))}
|
||||
</ScrollView>
|
||||
<Pagination
|
||||
onPageChange={handlePageChange}
|
||||
currentPage={currentPage}
|
||||
totalPages={feedbackTotalPages}
|
||||
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>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
83
src/screens/caseDetails/feedback/FeedbackDetailImageItem.tsx
Normal file
83
src/screens/caseDetails/feedback/FeedbackDetailImageItem.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import { Image, Modal, StyleSheet, TouchableOpacity, View } from 'react-native';
|
||||
import React, { useState } from 'react';
|
||||
import Text from '../../../../RN-UI-LIB/src/components/Text';
|
||||
import { GenericStyles } from '../../../../RN-UI-LIB/src/styles';
|
||||
import IconButton from '../../../../RN-UI-LIB/src/components/IconButton';
|
||||
import ExpandIcon from '../../../../RN-UI-LIB/src/Icons/ExpandIcon';
|
||||
import { IAnswerView } from '../../../types/feedback.types';
|
||||
import { COLORS } from '../../../../RN-UI-LIB/src/styles/colors';
|
||||
import { getQuestionText } from './FeedbackDetailAnswerContainer';
|
||||
import ExpandableImage from '../../../components/expandableImage/ExpandableImage';
|
||||
|
||||
interface IFeedbackDetailImageItem {
|
||||
image: IAnswerView;
|
||||
}
|
||||
|
||||
const IMAGE_HEIGHT = 131;
|
||||
const IMAGE_WIDTH = 105;
|
||||
|
||||
const FeedbackDetailImageItem: React.FC<IFeedbackDetailImageItem> = ({ image }) => {
|
||||
const [isImageExpanded, setIsImageExpanded] = useState(false);
|
||||
|
||||
const handleExpandImage = () => {
|
||||
setIsImageExpanded(true);
|
||||
};
|
||||
|
||||
const handleExpandedImageClose = () => {
|
||||
setIsImageExpanded(false);
|
||||
};
|
||||
|
||||
const questionText = getQuestionText(image);
|
||||
|
||||
return (
|
||||
<View style={[GenericStyles.mv8]}>
|
||||
<Text style={[styles.textContainer, styles.questionText]}>{questionText}</Text>
|
||||
<TouchableOpacity activeOpacity={1} onPress={handleExpandImage}>
|
||||
<Image
|
||||
style={[styles.image, GenericStyles.mt8, GenericStyles.br8]}
|
||||
source={{
|
||||
uri: image.inputText,
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
icon={<ExpandIcon />}
|
||||
round
|
||||
size={28}
|
||||
style={styles.expandIcon}
|
||||
onPress={handleExpandImage}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<Modal visible={isImageExpanded} transparent animationType="fade">
|
||||
<ExpandableImage
|
||||
imageSrc={image.inputText}
|
||||
title={questionText}
|
||||
close={handleExpandedImageClose}
|
||||
/>
|
||||
</Modal>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
textContainer: {
|
||||
fontSize: 12,
|
||||
lineHeight: 18,
|
||||
fontWeight: '400',
|
||||
width: '100%',
|
||||
},
|
||||
questionText: {
|
||||
color: COLORS.TEXT.LIGHT,
|
||||
},
|
||||
image: {
|
||||
height: IMAGE_HEIGHT,
|
||||
width: IMAGE_WIDTH,
|
||||
},
|
||||
expandIcon: {
|
||||
position: 'absolute',
|
||||
bottom: 8,
|
||||
left: IMAGE_WIDTH - 36,
|
||||
backgroundColor: COLORS.BACKGROUND.PRIMARY,
|
||||
},
|
||||
});
|
||||
|
||||
export default FeedbackDetailImageItem;
|
||||
@@ -74,7 +74,7 @@ const PhoneNumberSelectionBottomSheet: React.FC<PhoneNumberSelectionBottomSheetP
|
||||
);
|
||||
})}
|
||||
</ScrollView>
|
||||
<ToastContainer config={toastConfigs} position="bottom" />
|
||||
<ToastContainer config={toastConfigs} position="top" />
|
||||
</BottomSheet>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -10,23 +10,36 @@ import Text from '../../../RN-UI-LIB/src/components/Text';
|
||||
import { IEmiItem } from './EmiScheduleItem';
|
||||
import { formatAmount } from '../../../RN-UI-LIB/src/utlis/amount';
|
||||
import { getNumberWithRankSuffix } from '../../../RN-UI-LIB/src/utlis/common';
|
||||
import { getDynamicBottomSheetHeightPercentageFn } from '../../components/utlis/commonFunctions';
|
||||
|
||||
interface IEmiBreakupBottomSheet {
|
||||
openBottomSheet: boolean;
|
||||
setOpenBottomSheet: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
emiItem: IEmiItem;
|
||||
listNumber: number;
|
||||
listNumber?: number;
|
||||
totalUnpaidEmiAmount: number;
|
||||
totalUnpaidEmiPenaltyCharges: number;
|
||||
totalOverDueAmount: number;
|
||||
}
|
||||
const EmiBreakupBottomSheet: React.FC<IEmiBreakupBottomSheet> = (props) => {
|
||||
const { openBottomSheet, setOpenBottomSheet, emiItem, listNumber } = props;
|
||||
const {
|
||||
openBottomSheet,
|
||||
setOpenBottomSheet,
|
||||
listNumber,
|
||||
totalUnpaidEmiPenaltyCharges,
|
||||
totalOverDueAmount,
|
||||
totalUnpaidEmiAmount,
|
||||
} = props;
|
||||
const height = getDynamicBottomSheetHeightPercentageFn();
|
||||
return (
|
||||
<BottomSheet
|
||||
heightPercentage={40}
|
||||
heightPercentage={height(3)}
|
||||
visible={openBottomSheet}
|
||||
HeaderNode={() => (
|
||||
<View style={[...row, GenericStyles.p16]}>
|
||||
<Heading dark type="h4">
|
||||
{getNumberWithRankSuffix(listNumber)} EMI breakup
|
||||
{listNumber
|
||||
? `${getNumberWithRankSuffix(listNumber)} EMI breakup`
|
||||
: 'Total due breakup'}
|
||||
</Heading>
|
||||
<TouchableOpacity activeOpacity={0.7} onPress={() => setOpenBottomSheet((prev) => !prev)}>
|
||||
<CloseIcon color={COLORS.TEXT.LIGHT} />
|
||||
@@ -37,15 +50,13 @@ const EmiBreakupBottomSheet: React.FC<IEmiBreakupBottomSheet> = (props) => {
|
||||
>
|
||||
<View style={[GenericStyles.p16]}>
|
||||
<View style={[...row, GenericStyles.pb16]}>
|
||||
<Text light>EMI</Text>
|
||||
<Text dark>{formatAmount(emiItem?.totalUnpaidEmiAmount ?? 0)}</Text>
|
||||
<Text light>EMI amount</Text>
|
||||
<Text dark>{formatAmount(totalUnpaidEmiAmount)}</Text>
|
||||
</View>
|
||||
<View style={[...row, GenericStyles.pb16]}>
|
||||
<Text light>EMI Penalty charges</Text>
|
||||
<Text light>EMI penalty charges</Text>
|
||||
<Text dark bold>
|
||||
{formatAmount(
|
||||
emiItem?.totalUnpaidEmiPenaltyCharges ?? emiItem?.totalUnpaidOtherFees ?? 0
|
||||
)}
|
||||
{formatAmount(totalUnpaidEmiPenaltyCharges)}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.horizontalLine} />
|
||||
@@ -53,9 +64,7 @@ const EmiBreakupBottomSheet: React.FC<IEmiBreakupBottomSheet> = (props) => {
|
||||
<Text dark bold>
|
||||
Total Overdue Amount
|
||||
</Text>
|
||||
<Text style={{ color: COLORS.TEXT.RED }}>
|
||||
{formatAmount(emiItem?.totalOverDueAmount ?? 0)}
|
||||
</Text>
|
||||
<Text style={{ color: COLORS.TEXT.RED }}>{formatAmount(totalOverDueAmount)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</BottomSheet>
|
||||
|
||||
@@ -30,13 +30,19 @@ interface IEmiScheduleItem {
|
||||
listNumber: number;
|
||||
emiItem: IEmiItem;
|
||||
selectedTab: EmiSelectedTab;
|
||||
isLast: boolean;
|
||||
}
|
||||
|
||||
const EmiScheduleItem: React.FC<IEmiScheduleItem> = ({ listNumber, emiItem, selectedTab }) => {
|
||||
const EmiScheduleItem: React.FC<IEmiScheduleItem> = ({
|
||||
listNumber,
|
||||
emiItem,
|
||||
selectedTab,
|
||||
isLast,
|
||||
}) => {
|
||||
const [openBottomSheet, setOpenBottomSheet] = React.useState(false);
|
||||
|
||||
const { totalOverDueAmount, totalUnpaidEmiAmount, totalUnpaidEmiPenaltyCharges } = emiItem;
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={[styles.container, isLast ? GenericStyles.mb24 : {}]}>
|
||||
<View style={styles.leftContainer}>
|
||||
<Text dark style={[GenericStyles.pl12, GenericStyles.fontSize12]}>
|
||||
{getNumberWithRankSuffix(listNumber)}
|
||||
@@ -105,7 +111,9 @@ const EmiScheduleItem: React.FC<IEmiScheduleItem> = ({ listNumber, emiItem, sele
|
||||
</View>
|
||||
</View>
|
||||
<EmiBreakupBottomSheet
|
||||
emiItem={emiItem}
|
||||
totalOverDueAmount={totalOverDueAmount ?? 0}
|
||||
totalUnpaidEmiAmount={totalUnpaidEmiAmount ?? 0}
|
||||
totalUnpaidEmiPenaltyCharges={totalUnpaidEmiPenaltyCharges ?? 0}
|
||||
openBottomSheet={openBottomSheet}
|
||||
setOpenBottomSheet={setOpenBottomSheet}
|
||||
listNumber={listNumber}
|
||||
|
||||
@@ -8,13 +8,25 @@ import EmptyPaidListIcon from '../../assets/icons/EmptyPaidListIcon';
|
||||
import EmptyUnpaidListIcon from '../../assets/icons/EmptyUnpaidListIcon';
|
||||
import { useAppSelector } from '../../hooks';
|
||||
import { getFilteredData } from './utils';
|
||||
import { _map } from '../../../RN-UI-LIB/src/utlis/common';
|
||||
import LineLoader from '../../../RN-UI-LIB/src/components/suspense_loader/LineLoader';
|
||||
import SuspenseLoader from '../../../RN-UI-LIB/src/components/suspense_loader/SuspenseLoader';
|
||||
import EmptyScheduledListIcon from '../../assets/icons/EmptyScheduledListIcon';
|
||||
|
||||
export enum EmiSelectedTab {
|
||||
UNPAID = 'UNPAID',
|
||||
PAID = 'PAID',
|
||||
SCHEDULED = 'SCHEDULED',
|
||||
ALL = 'ALL',
|
||||
}
|
||||
|
||||
const emiTypes = {
|
||||
[EmiSelectedTab.UNPAID]: 'Unpaid',
|
||||
[EmiSelectedTab.PAID]: 'Paid',
|
||||
[EmiSelectedTab.SCHEDULED]: 'Scheduled',
|
||||
[EmiSelectedTab.ALL]: 'All',
|
||||
};
|
||||
|
||||
const pressableChipStyle: StyleProp<ViewStyle> = {
|
||||
marginTop: 0,
|
||||
};
|
||||
@@ -25,7 +37,8 @@ interface IEmiScheduleTab {
|
||||
const EmiScheduleTab: React.FC<IEmiScheduleTab> = (props) => {
|
||||
const { loanAccountNumber } = props;
|
||||
const [selectedTab, setSelectedTab] = useState<EmiSelectedTab>(EmiSelectedTab.UNPAID);
|
||||
const emiData = useAppSelector((state) => state.emiSchedule?.[loanAccountNumber]?.data);
|
||||
const emiEcheduleData = useAppSelector((state) => state.emiSchedule?.[loanAccountNumber]) || {};
|
||||
const { data: emiData, isLoading } = emiEcheduleData;
|
||||
const filteredData =
|
||||
selectedTab !== EmiSelectedTab.ALL ? getFilteredData(emiData, selectedTab) : emiData;
|
||||
|
||||
@@ -36,60 +49,74 @@ const EmiScheduleTab: React.FC<IEmiScheduleTab> = (props) => {
|
||||
}}
|
||||
>
|
||||
<View style={[GenericStyles.row, GenericStyles.alignCenter, GenericStyles.mb8]}>
|
||||
<PressableChip
|
||||
onSelectionChange={() => setSelectedTab(EmiSelectedTab.UNPAID)}
|
||||
checked={selectedTab === EmiSelectedTab.UNPAID}
|
||||
label="Unpaid"
|
||||
key={1}
|
||||
containerStyles={pressableChipStyle}
|
||||
/>
|
||||
<PressableChip
|
||||
onSelectionChange={() => setSelectedTab(EmiSelectedTab.PAID)}
|
||||
checked={selectedTab === EmiSelectedTab.PAID}
|
||||
label="Paid"
|
||||
key={2}
|
||||
containerStyles={pressableChipStyle}
|
||||
/>
|
||||
<PressableChip
|
||||
onSelectionChange={() => setSelectedTab(EmiSelectedTab.ALL)}
|
||||
checked={selectedTab === EmiSelectedTab.ALL}
|
||||
label="All"
|
||||
key={3}
|
||||
containerStyles={pressableChipStyle}
|
||||
/>
|
||||
{/* @ts-ignore */}
|
||||
{_map(emiTypes, (tab: EmiSelectedTab) => (
|
||||
<PressableChip
|
||||
onSelectionChange={() => setSelectedTab(tab)}
|
||||
checked={selectedTab === tab}
|
||||
label={emiTypes[tab]}
|
||||
key={tab}
|
||||
containerStyles={pressableChipStyle}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
{filteredData?.length > 0 ? (
|
||||
<FlatList
|
||||
data={filteredData}
|
||||
renderItem={({ item, index }) => (
|
||||
<EmiScheduleItem
|
||||
emiItem={item}
|
||||
listNumber={item?.rank ?? filteredData.length - index}
|
||||
key={item.referenceId}
|
||||
selectedTab={selectedTab}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<View style={[GenericStyles.alignCenter, GenericStyles.mt16]}>
|
||||
{selectedTab === EmiSelectedTab.PAID ? (
|
||||
<>
|
||||
<EmptyPaidListIcon />
|
||||
<Text style={[GenericStyles.mt16]} light>
|
||||
Customer has not paid any EMI’s yet
|
||||
</Text>
|
||||
</>
|
||||
) : null}
|
||||
{selectedTab === EmiSelectedTab.UNPAID ? (
|
||||
<>
|
||||
<EmptyUnpaidListIcon />
|
||||
<Text style={[GenericStyles.mt16]} light>
|
||||
Customer has paid all due EMI’s
|
||||
</Text>
|
||||
</>
|
||||
) : null}
|
||||
</View>
|
||||
)}
|
||||
<SuspenseLoader
|
||||
loading={isLoading}
|
||||
fallBack={
|
||||
<>
|
||||
{[...Array(5).keys()].map(() => (
|
||||
<LineLoader
|
||||
width={'100%'}
|
||||
height={60}
|
||||
style={[GenericStyles.br6, { marginBottom: 20 }]}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
}
|
||||
>
|
||||
{filteredData?.length > 0 ? (
|
||||
<FlatList
|
||||
data={filteredData}
|
||||
renderItem={({ item, index }) => (
|
||||
<EmiScheduleItem
|
||||
emiItem={item}
|
||||
isLast={index === filteredData.length - 1}
|
||||
listNumber={item?.rank ?? filteredData.length - index}
|
||||
key={item.referenceId}
|
||||
selectedTab={selectedTab}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<View style={[GenericStyles.alignCenter, GenericStyles.mt16]}>
|
||||
{selectedTab === EmiSelectedTab.PAID ? (
|
||||
<>
|
||||
<EmptyPaidListIcon />
|
||||
<Text style={[GenericStyles.mt16]} light>
|
||||
Customer has not paid any EMI’s yet
|
||||
</Text>
|
||||
</>
|
||||
) : null}
|
||||
{selectedTab === EmiSelectedTab.UNPAID ? (
|
||||
<>
|
||||
<EmptyUnpaidListIcon />
|
||||
<Text style={[GenericStyles.mt16]} light>
|
||||
Customer has paid all due EMI’s
|
||||
</Text>
|
||||
</>
|
||||
) : null}
|
||||
{selectedTab === EmiSelectedTab.SCHEDULED ? (
|
||||
<>
|
||||
<EmptyScheduledListIcon />
|
||||
<Text style={[GenericStyles.mt16]} light>
|
||||
No scheduled EMI’s
|
||||
</Text>
|
||||
</>
|
||||
) : null}
|
||||
</View>
|
||||
)}
|
||||
</SuspenseLoader>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,48 +3,58 @@ import { StyleSheet, View } from 'react-native';
|
||||
import { GenericStyles } from '../../../../RN-UI-LIB/src/styles';
|
||||
import Text from '../../../../RN-UI-LIB/src/components/Text';
|
||||
import { COLORS } from '../../../../RN-UI-LIB/src/styles/colors';
|
||||
import RepaymentsUnpaidIcon from '../../../assets/icons/RepaymentsUnpaidIcon';
|
||||
import RepaymentsPaidIcon from '../../../assets/icons/RepaymentsPaidIcon';
|
||||
import Tag from '../../../../RN-UI-LIB/src/components/Tag';
|
||||
import { IRepaymentsRecord, REPAYMENT_STATUS } from '../../../types/repayments.types';
|
||||
import { row } from '../constants';
|
||||
import { BUSINESS_DATE_FORMAT, dateFormat } from '../../../../RN-UI-LIB/src/utlis/dates';
|
||||
import {
|
||||
IRepaymentsRecord,
|
||||
REPAYMENT_STATUS,
|
||||
RepaymentStatusTagging,
|
||||
} from '../../../types/repayments.types';
|
||||
import {
|
||||
BUSINESS_DATE_FORMAT,
|
||||
BUSINESS_TIME_FORMAT,
|
||||
dateFormat,
|
||||
} from '../../../../RN-UI-LIB/src/utlis/dates';
|
||||
import InfoIcon from '../../../../RN-UI-LIB/src/Icons/InfoIcon';
|
||||
|
||||
interface RepaymentsItemProps {
|
||||
repaymentRecord: IRepaymentsRecord;
|
||||
}
|
||||
|
||||
const StatusIcon: React.FC<{ status: REPAYMENT_STATUS }> = ({ status }) => {
|
||||
switch (status) {
|
||||
case REPAYMENT_STATUS.FAILED:
|
||||
return <RepaymentsUnpaidIcon />;
|
||||
case REPAYMENT_STATUS.SUCCESS:
|
||||
return <RepaymentsPaidIcon />;
|
||||
default:
|
||||
return <></>;
|
||||
}
|
||||
};
|
||||
|
||||
const RepaymentsItem: React.FC<RepaymentsItemProps> = ({ repaymentRecord }) => {
|
||||
const repaymentTag = RepaymentStatusTagging[repaymentRecord.status];
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.leftContainer}>
|
||||
<StatusIcon status={repaymentRecord.status} />
|
||||
{repaymentRecord.status === REPAYMENT_STATUS.SUCCESS ? (
|
||||
<RepaymentsPaidIcon />
|
||||
) : repaymentTag ? (
|
||||
<InfoIcon color={repaymentTag.iconColor} />
|
||||
) : null}
|
||||
</View>
|
||||
<View style={[GenericStyles.fill]}>
|
||||
<View style={[GenericStyles.row, GenericStyles.fill]}>
|
||||
<View style={[GenericStyles.row, GenericStyles.fill]}>
|
||||
<Text style={[styles.repaymentsText]}>₹ {repaymentRecord?.amount}</Text>
|
||||
{repaymentRecord.status === REPAYMENT_STATUS.FAILED && (
|
||||
<Tag text="Failed" variant="error" />
|
||||
)}
|
||||
{repaymentTag ? <Tag text={repaymentTag.label} variant={repaymentTag.variant} /> : null}
|
||||
</View>
|
||||
<Text>{dateFormat(new Date(repaymentRecord.valueDate ?? ''), BUSINESS_DATE_FORMAT)}</Text>
|
||||
<Text>
|
||||
{dateFormat(
|
||||
new Date(repaymentRecord.orderCompletionTimestamp ?? ''),
|
||||
BUSINESS_DATE_FORMAT
|
||||
)}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={[GenericStyles.row]}>
|
||||
<View style={[GenericStyles.row, GenericStyles.justifyContentSpaceBetween]}>
|
||||
<Text style={[GenericStyles.pt7]} light>
|
||||
{repaymentRecord.repaymentMode}
|
||||
</Text>
|
||||
<Text style={[GenericStyles.pt7, styles.timeStamp]}>
|
||||
{dateFormat(
|
||||
new Date(repaymentRecord.orderCompletionTimestamp ?? ''),
|
||||
BUSINESS_TIME_FORMAT
|
||||
)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
@@ -66,6 +76,9 @@ const styles = StyleSheet.create({
|
||||
flexBasis: '8%',
|
||||
},
|
||||
repaymentsText: { marginRight: 5, lineHeight: 19 },
|
||||
timeStamp: {
|
||||
color: '#C9C9C9',
|
||||
},
|
||||
});
|
||||
|
||||
export default RepaymentsItem;
|
||||
|
||||
@@ -20,13 +20,15 @@ const OtpText: React.FC<IOtpText> = ({ resetOtp }) => {
|
||||
const [countDownTimeLeft, setCountDownTimeLeft] = useState<number>(RESEND_OTP_TIME);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { phoneNumber, verifyOTPError } = useSelector((state: RootState) => state.loginInfo);
|
||||
const { phoneNumber, verifyOTPError, otpToken } = useSelector(
|
||||
(state: RootState) => state.loginInfo
|
||||
);
|
||||
|
||||
const handleResendOTP = () => {
|
||||
setCountDownComplete(false);
|
||||
setCountDownTimeLeft(RESEND_OTP_TIME);
|
||||
dispatch(resetVerifyOTPError());
|
||||
dispatch(generateOTP({ phoneNumber }, true));
|
||||
dispatch(generateOTP({ phoneNumber, otpToken }, true));
|
||||
resetOtp();
|
||||
};
|
||||
|
||||
|
||||
@@ -39,6 +39,8 @@ export enum GoogleSignInError {
|
||||
|
||||
GoogleSignin.configure({
|
||||
webClientId: GOOGLE_SSO_CLIENT_ID,
|
||||
forceCodeForRefreshToken: true,
|
||||
offlineAccess: true,
|
||||
});
|
||||
|
||||
function Login() {
|
||||
@@ -49,9 +51,9 @@ function Login() {
|
||||
formState: { isValid },
|
||||
} = useForm<ILoginForm>();
|
||||
const dispatch = useAppDispatch();
|
||||
const { phoneNumber, OTPError, isLoading, deviceId } = useSelector((state: RootState) => ({
|
||||
deviceId: state.user.deviceId,
|
||||
const { phoneNumber, OTPError, isLoading, otpToken } = useSelector((state: RootState) => ({
|
||||
phoneNumber: state.loginInfo.phoneNumber,
|
||||
otpToken: state.loginInfo.otpToken,
|
||||
OTPError: state.loginInfo.OTPError,
|
||||
isLoading: state.loginInfo.isLoading,
|
||||
}));
|
||||
@@ -68,20 +70,24 @@ function Login() {
|
||||
}, []);
|
||||
|
||||
const handleGenerateOTP = (data: GenerateOTPPayload) => {
|
||||
let payload = {
|
||||
...data,
|
||||
otpToken: data.phoneNumber === phoneNumber ? otpToken : undefined,
|
||||
};
|
||||
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.AV_LOGIN_SCREEN_SEND_OTP_CLICKED);
|
||||
dispatch(generateOTP(data));
|
||||
dispatch(generateOTP(payload));
|
||||
};
|
||||
|
||||
const onPressGoogle = async () => {
|
||||
try {
|
||||
await GoogleSignin.hasPlayServices();
|
||||
const userInfo: GoogleSigninUser = await GoogleSignin.signIn();
|
||||
if (userInfo?.idToken) {
|
||||
if (userInfo?.serverAuthCode) {
|
||||
toast({
|
||||
text1: ToastMessages.FETCHING_USER_DATA,
|
||||
type: 'success',
|
||||
});
|
||||
await dispatch(verifyGoogleSignIn(userInfo.idToken));
|
||||
await dispatch(verifyGoogleSignIn(userInfo.serverAuthCode));
|
||||
return;
|
||||
}
|
||||
throw userInfo;
|
||||
|
||||
@@ -64,13 +64,7 @@ const Notifications = () => {
|
||||
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_NOTIFICATIONS_LOADED, {
|
||||
count: data?.length,
|
||||
});
|
||||
if (data?.length) {
|
||||
return;
|
||||
}
|
||||
if (!isOnline) {
|
||||
return;
|
||||
}
|
||||
dispatch(getNotifications());
|
||||
onRefresh();
|
||||
}, []);
|
||||
|
||||
const notificationsList: INotification[] = useMemo(() => {
|
||||
|
||||
@@ -23,7 +23,7 @@ const DropdownItem: React.FC<IDropdownItem> = ({
|
||||
sourceText,
|
||||
}) => {
|
||||
const pressHandler = () => {
|
||||
handleSelection && handleSelection(label);
|
||||
handleSelection && handleSelection(id);
|
||||
close && close();
|
||||
};
|
||||
return (
|
||||
|
||||
@@ -14,6 +14,7 @@ import { toast } from '../../../RN-UI-LIB/src/components/toast';
|
||||
import {
|
||||
copyToClipboard,
|
||||
getDynamicBottomSheetHeightPercentageFn,
|
||||
getPhoneNumberString,
|
||||
} from '../../components/utlis/commonFunctions';
|
||||
import useIsOnline from '../../hooks/useIsOnline';
|
||||
import { RootState } from '../../store/store';
|
||||
@@ -21,12 +22,13 @@ import { generatePaymentLinkAction } from '../../action/paymentActions';
|
||||
import { useAppDispatch, useAppSelector } from '../../hooks';
|
||||
import DropdownItem from './DropdownItem';
|
||||
import { PhoneNumber } from '../caseDetails/interface';
|
||||
import { setPaymentLink } from '../../reducer/paymentSlice';
|
||||
import { setLoading, setPaymentLink } from '../../reducer/paymentSlice';
|
||||
import { ToastMessages } from '../allCases/constants';
|
||||
import { addClickstreamEvent } from '../../services/clickstreamEventService';
|
||||
import { CLICKSTREAM_EVENT_NAMES } from '../../common/Constants';
|
||||
import QrCodeModal from './QrCodeModal';
|
||||
import ModalWrapper from '../../../RN-UI-LIB/src/components/modalWrapper/ModalWrapper';
|
||||
import OfflineScreen from '../../common/OfflineScreen';
|
||||
|
||||
interface IRegisterForm {
|
||||
selectedPhoneNumber: string;
|
||||
@@ -46,14 +48,12 @@ interface IRegisterPayments {
|
||||
|
||||
const HEADER_HEIGHT = 100;
|
||||
const ROW_HEIGHT = 40;
|
||||
|
||||
const PAGE_TITLE = 'Collect Money';
|
||||
const RegisterPayments: React.FC<IRegisterPayments> = ({ route }) => {
|
||||
let {
|
||||
params: { caseId, numbers, amount, pos },
|
||||
} = route;
|
||||
const { isLoading, paymentLink, loanIdToValue } = useAppSelector(
|
||||
(state: RootState) => state.payment
|
||||
);
|
||||
const { isLoading, paymentLink } = useAppSelector((state: RootState) => state.payment);
|
||||
const dispatch = useAppDispatch();
|
||||
const isOnline = useIsOnline();
|
||||
const caseDetail = useAppSelector((state) => state.allCases.caseDetails[caseId]);
|
||||
@@ -79,9 +79,15 @@ const RegisterPayments: React.FC<IRegisterPayments> = ({ route }) => {
|
||||
}
|
||||
}, [paymentLink, generateClicked]);
|
||||
|
||||
const ChildComponents = numbers?.map(({ number, createdAt, sourceText }) => {
|
||||
const ChildComponents = numbers?.map((phoneNumber) => {
|
||||
const { number, createdAt, sourceText } = phoneNumber;
|
||||
return (
|
||||
<DropdownItem createdAt={createdAt} id={number} label={number} sourceText={sourceText} />
|
||||
<DropdownItem
|
||||
createdAt={createdAt}
|
||||
id={number}
|
||||
label={getPhoneNumberString(phoneNumber)}
|
||||
sourceText={sourceText}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -128,20 +134,17 @@ const RegisterPayments: React.FC<IRegisterPayments> = ({ route }) => {
|
||||
setShowQrCodeModal(true);
|
||||
} else {
|
||||
dispatch(
|
||||
generatePaymentLinkAction(
|
||||
{
|
||||
alternateContactNumber: getValues('selectedPhoneNumber'),
|
||||
customAmount: {
|
||||
currency: 'INR',
|
||||
amount: Number(getValues('amount')),
|
||||
},
|
||||
customAmountProvided: Number(getValues('amount')) > -1,
|
||||
loanAccountNumber: caseDetail.loanAccountNumber!!,
|
||||
notifyToAlternateContact: true,
|
||||
customerReferenceId: caseDetail.customerReferenceId,
|
||||
generatePaymentLinkAction({
|
||||
alternateContactNumber: getValues('selectedPhoneNumber'),
|
||||
customAmount: {
|
||||
currency: 'INR',
|
||||
amount: Number(getValues('amount')),
|
||||
},
|
||||
loanIdToValue
|
||||
)
|
||||
customAmountProvided: Number(getValues('amount')) > -1,
|
||||
loanAccountNumber: caseDetail.loanAccountNumber!!,
|
||||
notifyToAlternateContact: true,
|
||||
customerReferenceId: caseDetail.customerReferenceId,
|
||||
})
|
||||
);
|
||||
setGenerateClicked(true);
|
||||
}
|
||||
@@ -151,14 +154,19 @@ const RegisterPayments: React.FC<IRegisterPayments> = ({ route }) => {
|
||||
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_COLLECT_MONEY_LOADED, { caseId: caseId });
|
||||
return () => {
|
||||
dispatch(setPaymentLink(''));
|
||||
dispatch(setLoading(false));
|
||||
};
|
||||
}, []);
|
||||
|
||||
const getBottomSheetHeight = getDynamicBottomSheetHeightPercentageFn(HEADER_HEIGHT, ROW_HEIGHT);
|
||||
|
||||
if (!isOnline) {
|
||||
return <OfflineScreen goBack={goBack} pageTitle={PAGE_TITLE} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<NavigationHeader title={'Collect Money'} onBack={goBack} />
|
||||
<NavigationHeader title={PAGE_TITLE} onBack={goBack} />
|
||||
<View style={[GenericStyles.fill, GenericStyles.p16, GenericStyles.whiteBackground]}>
|
||||
<Text light>Share a payment link on the selected number</Text>
|
||||
<Text style={[GenericStyles.mb8, GenericStyles.mt16]}>Select number</Text>
|
||||
|
||||
@@ -90,6 +90,7 @@ export interface IGetTransformedCaseItem extends CaseDetail {
|
||||
answer: any;
|
||||
caseId: string;
|
||||
coords: Geolocation.GeoCoordinates;
|
||||
templateId: string;
|
||||
}
|
||||
export const getTransformedCollectionCaseItem = async (caseItem: IGetTransformedCaseItem) => {
|
||||
let cloneCaseItem = { ...caseItem };
|
||||
@@ -100,7 +101,7 @@ export const getTransformedCollectionCaseItem = async (caseItem: IGetTransformed
|
||||
location: cloneCaseItem?.coords,
|
||||
offlineCaseKey: cloneCaseItem?.offlineCaseKey,
|
||||
};
|
||||
return { caseType: caseItem.caseType, data };
|
||||
return { caseType: caseItem.caseType, data, templateId: caseItem.templateId };
|
||||
};
|
||||
|
||||
export const getTransformedAvCase = async (
|
||||
|
||||
@@ -120,6 +120,8 @@ export const dataSyncService = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
if (!syncUploadStatusPayload.length) return;
|
||||
|
||||
const uploadCompletedApiPayload: IUploadCompletedApiPayload = {
|
||||
deviceId: GLOBAL.DEVICE_ID,
|
||||
syncUploadStatus: syncUploadStatusPayload,
|
||||
|
||||
@@ -1,9 +1,39 @@
|
||||
import { COLORS } from '../../RN-UI-LIB/src/styles/colors';
|
||||
|
||||
export enum REPAYMENT_STATUS {
|
||||
SUCCESS = 'SUCCESS',
|
||||
FAILED = 'FAILED',
|
||||
FAILURE = 'FAILURE',
|
||||
SCHEDULED = 'SCHEDULED',
|
||||
UNKNOWN = 'UNKNOWN',
|
||||
FAILED = 'FAILED',
|
||||
}
|
||||
|
||||
interface IRepaymentStatusTagging {
|
||||
[key: string]: {
|
||||
label: string;
|
||||
iconColor: string;
|
||||
variant: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const RepaymentStatusTagging: IRepaymentStatusTagging = {
|
||||
[REPAYMENT_STATUS.FAILURE]: {
|
||||
label: 'Failed',
|
||||
iconColor: COLORS.TEXT.RED,
|
||||
variant: 'error',
|
||||
},
|
||||
[REPAYMENT_STATUS.FAILED]: {
|
||||
label: 'Failed',
|
||||
iconColor: COLORS.TEXT.RED,
|
||||
variant: 'error',
|
||||
},
|
||||
[REPAYMENT_STATUS.UNKNOWN]: {
|
||||
label: 'Unknown',
|
||||
iconColor: COLORS.TEXT.LIGHT,
|
||||
variant: 'gray',
|
||||
},
|
||||
};
|
||||
|
||||
export interface IRepaymentsRecord {
|
||||
loanReferenceId: string;
|
||||
customerReferenceId: string;
|
||||
@@ -15,4 +45,5 @@ export interface IRepaymentsRecord {
|
||||
amount: number;
|
||||
failed: boolean;
|
||||
success: boolean;
|
||||
orderCompletionTimestamp: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user