NTP-30201 | Upgrading Longhorn for CIU Team (#1379)

* NTP-30201 | Upgrading Longhorn for CIU Team

* NTP-30201 | Upgrading Longhorn for CIU Team

* NTP-30201 | Upgrading Longhorn for CIU Team

* NTP-30201 | Upgrading Longhorn for CIU Team

* NTP-30201 | Upgrading Longhorn for CIU Team | PR comments resolved

* NTP-30201 | Upgrading Longhorn for CIU Team | web-ui update

* NTP-30201 | Upgrading Longhorn for CIU Team | pr fix
This commit is contained in:
Ashish Deo
2025-02-12 19:28:39 +05:30
committed by GitHub
parent 00f96b0b16
commit aaf7220d4b
12 changed files with 193 additions and 51 deletions

View File

@@ -1,9 +1,10 @@
import { Dispatch } from '@reduxjs/toolkit';
import axiosInstance, { ApiKeys, getApiUrl } from '../../../utils/ApiHelper';
import axiosInstance, { ApiKeys, getApiUrl, logError } from '../../../utils/ApiHelper';
import {
setCaseDetail,
setCaseDetailLoading,
setContacts,
setCustomerConfidence,
setCustomerInfo,
setEmiBreakup,
setEmiBreakupError,
@@ -258,3 +259,14 @@ export const removeContact = (
toast('Unable to remove contact', { type: 'error' });
});
};
export const getCustomerConfidence = () => (dispatch: Dispatch) => {
axiosInstance
.get(getApiUrl(ApiKeys.CUSTOMER_CONFIDENCE))
.then(res => {
dispatch(setCustomerConfidence(res.data));
})
.catch(() => {
logError('Error fetching customer confidence');
});
};

View File

@@ -4,8 +4,7 @@ import { STORE_KEYS, ToastMessage } from '../constants';
import {
setCaseDetail,
setCaseDetailError,
setCaseDetailLoading,
setSecondarySources
setCaseDetailLoading
} from '../reducers/caseDetailSlice';
import { Dispatch } from '@reduxjs/toolkit';
@@ -68,6 +67,7 @@ export const postAlternateNumber =
caseReferenceId,
caseBusinessVertical,
alternateNumber,
customerConfidence,
source,
successCallback,
cb,
@@ -80,6 +80,7 @@ export const postAlternateNumber =
caseReferenceId: string;
caseBusinessVertical: string;
alternateNumber: string;
customerConfidence?: string;
source: string;
successCallback?: () => void;
cb?: (isModalOpen: boolean) => void;
@@ -98,7 +99,8 @@ export const postAlternateNumber =
secondarySource: source,
remarkForSecondarySource: otherSource,
type: 'Alternate Contact',
numberCustomId: identifier
numberCustomId: identifier,
telephoneCustomerConfidence: customerConfidence
}
],
{
@@ -119,14 +121,6 @@ export const postAlternateNumber =
});
};
export const getSecondarySources = () => (dispatch: Dispatch) => {
const url = getApiUrl(ApiKeys.PHONENUMBER_SECONDARY_SOURCES);
axiosInstance.get(url).then(response => {
dispatch(setSecondarySources(response.data));
});
};
export const getValidateNumber = async (number: string, customerRefId: string) => {
const url = getApiUrl(ApiKeys.VALIDATE_PHONE_NUMBER, { number });
try {

View File

@@ -40,6 +40,7 @@ const AddNumberPopper = (props: IAddNumberPopperProps) => {
const [isExisting, setIsExisting] = useState<boolean>(false);
const [numberInvalid, setNumberInvalid] = useState<boolean>(false);
const slashCallStatus = useSelector((state: RootState) => state.common.slashCallStatus);
const isCiuAgent = useSelector((state: RootState) => state.common.isCiuAgent);
useEffect(() => {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.LH_DOCUMENTS_BANK_STATEMENT_HYPERLINKING_POPPER);
@@ -126,7 +127,11 @@ const AddNumberPopper = (props: IAddNumberPopperProps) => {
}
};
return (
return isCiuAgent ? (
<>
<span>{children}</span>
</>
) : (
<span onClick={handleFetchStatus}>
<Popper placement="top" hideStrategy="referenceHidden">
<PopperTrigger>

View File

@@ -4,13 +4,9 @@ import AddIcon from '@icons/AddIcon';
import styles from './index.module.scss';
import { useParams } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
import { useCallback } from 'react';
import { useCallback, useEffect, useState } from 'react';
import Typography from '@navi/web-ui/lib/primitives/Typography/Typography';
import {
getSecondarySources,
getValidateNumber,
postAlternateNumber
} from '../../actions/moreDetailsActions';
import { getValidateNumber, postAlternateNumber } from '../../actions/moreDetailsActions';
import { createKey } from 'src/utils/CaseDetail.utils';
import { addClickstreamEvent } from '../../../../service/clickStreamEventService';
import { CLICKSTREAM_EVENT_NAMES } from '../../../../service/clickStream.constant';
@@ -21,20 +17,25 @@ import { Roles } from '@cp/pages/auth/constants/AuthConstants';
import { useForm } from 'react-hook-form';
import cx from 'classnames';
import BorderedInputTextBox from './BorderedInputTextBox';
import { getTelephonesV2 } from '../../actions/casesDetailsActions';
import { getCustomerConfidence, getTelephonesV2 } from '../../actions/casesDetailsActions';
import SingleAutoDropdown from './SingleAutoDropdown';
import {
CUSTOMER_CONFIDENCE,
OTHER_SOURCE,
OtherSourceValidations,
PHONE_NUMBER,
PhoneNumberValidations,
SOURCE
} from '../../constants/MoreDetails.constant';
import { ICustomerConfidenceLevel } from '../../interfaces/CaseDetail.type';
import { Chip } from '@navi/web-ui/lib/primitives';
import { convertToRadioOptions } from './utils/utils';
export interface AddNumberProps {
phoneNumber: string;
source: string;
otherSource: string;
customerConfidence: string;
}
const AddNewNumberOverlay: React.FC<React.PropsWithChildren> = () => {
@@ -50,12 +51,15 @@ const AddNewNumberOverlay: React.FC<React.PropsWithChildren> = () => {
);
const user = useSelector((state: RootState) => state.common.userData);
const isGlobalAccessRoleGiven = user?.roles?.includes(Roles.ROLE_GLOBAL_ACCESS);
const isCiuAgent = useSelector((state: RootState) => state.common.isCiuAgent);
const disableCTAs = isGlobalAccessRoleGiven ? !editAccessFlag : false;
const telephones = useSelector(
(state: RootState) =>
state.caseDetail.pageData?.[createKey(loanId, customerId)]?.telephonesv2?.data
);
const secondarySources = useSelector((state: RootState) => state.caseDetail.secondarySources);
const [isDisabledSubmitButton, setIsDisabledSubmitButton] = useState(false);
const customerConfidence = useSelector((state: RootState) => state.caseDetail.customerConfidence);
const telephoneCustomerConfidence = customerConfidence?.telephoneCustomerConfidence;
const secondarySources = customerConfidence?.longhornSecondarySources;
const radioOptionsList = convertToRadioOptions(telephoneCustomerConfidence);
const optionsList =
secondarySources?.map(source => ({
@@ -63,7 +67,7 @@ const AddNewNumberOverlay: React.FC<React.PropsWithChildren> = () => {
value: source.value
})) || [];
const [isModalOpen, setIsModalOpen] = React.useState(false);
const [isModalOpen, setIsModalOpen] = useState(false);
const {
handleSubmit,
@@ -79,7 +83,6 @@ const AddNewNumberOverlay: React.FC<React.PropsWithChildren> = () => {
const openModal = () => {
setIsModalOpen(true);
dispatch(getSecondarySources());
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.LH_Phone_AddNewNumberCTA);
};
@@ -90,6 +93,7 @@ const AddNewNumberOverlay: React.FC<React.PropsWithChildren> = () => {
const handleFormSubmit = data => {};
const handleAddNumber = async () => {
if (!(!isDirty || !isValid)) {
setIsDisabledSubmitButton(true);
dispatch(
postAlternateNumber({
loanaccountnumber: loanId,
@@ -100,7 +104,8 @@ const AddNewNumberOverlay: React.FC<React.PropsWithChildren> = () => {
source: getValues(SOURCE).value,
successCallback: successCallback,
cb: setIsModalOpen,
otherSource: getValues(OTHER_SOURCE)
otherSource: getValues(OTHER_SOURCE),
customerConfidence: watch('customerConfidence')
})
);
}
@@ -111,6 +116,7 @@ const AddNewNumberOverlay: React.FC<React.PropsWithChildren> = () => {
const successCallback = useCallback(() => {
dispatch(getTelephonesV2(loanId, customerId, caseReferenceId, caseBusinessVertical));
setIsDisabledSubmitButton(false);
reset();
}, []);
@@ -120,6 +126,7 @@ const AddNewNumberOverlay: React.FC<React.PropsWithChildren> = () => {
const selectedSource = watch(SOURCE, '');
const enteredOtherSource = watch(OTHER_SOURCE, '');
const selectedValue = watch(CUSTOMER_CONFIDENCE, '');
const validateNumber = async (value: string) => {
if (value.length == 10) {
@@ -148,12 +155,20 @@ const AddNewNumberOverlay: React.FC<React.PropsWithChildren> = () => {
setValue(SOURCE, selectedOption.value);
};
const isButtonDisabled =
!isDirty ||
!isValid ||
!isValidPhoneNumber(getValues(PHONE_NUMBER)) ||
(isCiuAgent && !selectedValue) ||
isDisabledSubmitButton;
return (
<div className={styles.addNewNumberOverlay}>
<Button
variant={'text'}
onClick={() => {
if (disableCTAs) return;
dispatch(getCustomerConfidence());
openModal();
}}
disabled={disableCTAs}
@@ -161,7 +176,12 @@ const AddNewNumberOverlay: React.FC<React.PropsWithChildren> = () => {
>
Add new number
</Button>
<Modal visible={isModalOpen} onClose={closeModal} header="Add new number">
<Modal
visible={isModalOpen}
contentWrapperClass="w-[490px]"
onClose={closeModal}
header="Add new number"
>
<form onSubmit={handleSubmit(handleFormSubmit)}>
<div className={styles.addNewNumberCard}>
<Typography className={styles.enterPhoneNumber} variant={'p4'}>
@@ -231,13 +251,34 @@ const AddNewNumberOverlay: React.FC<React.PropsWithChildren> = () => {
/>
</>
)}
<div className="flex justify-end mt-[24px]">
<Button
className=""
disabled={!isDirty || !isValid || !isValidPhoneNumber(getValues(PHONE_NUMBER))}
onClick={handleAddNumber}
>
Add
{isCiuAgent ? (
<>
<Typography className={cx(styles.enterPhoneNumber, 'mt-4 mb-2')} variant={'p4'}>
Confidence on Number
</Typography>
<div className="flex flex-wrap gap-2">
{radioOptionsList.map(optionType => (
<Chip
key={optionType.value}
label={optionType.label}
wrapperClasses={cx([
styles.chip,
{ [styles.chipActiveColor]: optionType.value === selectedValue }
])}
labelClasses={styles.text}
onChipClick={() => {
setValue('customerConfidence', optionType.value);
}}
/>
))}
</div>
</>
) : null}
<div className="flex justify-end mt-[30px]">
<Button className="!w-[173px]" disabled={isButtonDisabled} onClick={handleAddNumber}>
Add new number
</Button>
</div>
</div>

View File

@@ -86,13 +86,28 @@
.addNewNumberOverlay {
.addNewNumberCard {
box-sizing: border-box;
width: 302px;
display: flex;
flex-direction: column;
border-radius: 8px;
background-color: var(--bg-primary);
transition: height 200ms;
position: relative;
.chip {
padding: 4px 12px;
font-size: 16px;
width: max-content;
}
.chipActiveColor {
background-color: var(--blue-bg);
border: 1px solid var(--blue-border);
.text {
color: var(--blue-base);
}
}
header {
display: flex;
justify-content: space-between;

View File

@@ -0,0 +1,14 @@
import { ICustomerConfidenceLevel } from '../../../interfaces/CaseDetail.type';
export const convertToRadioOptions = (telephoneCustomerConfidence: ICustomerConfidenceLevel[]) => {
return telephoneCustomerConfidence.map((item, index) => ({
id: (index + 1).toString(),
value: item.name,
label: `${item.label} (${item.level})`,
otherProps: {
label: item.label,
level: item.level,
name: item.name
}
}));
};

View File

@@ -223,7 +223,7 @@ const PhoneNumberContactCard = ({ callData }: { callData: ITelePhoneData }) => {
</div>
<div className={styles.sourcesWrapper}>
{isGlobalRoleAndNnumberformLonghorn ? (
<Popper placement="bottom-end" hideStrategy="referenceHidden">
<Popper placement="left-end" hideStrategy="referenceHidden">
<PopperTrigger>
<PersonWithSeperatorIcon />
</PopperTrigger>
@@ -265,11 +265,49 @@ const PhoneNumberContactCard = ({ callData }: { callData: ITelePhoneData }) => {
</Button>
</div>
</div>
{callData?.telephoneCustomerConfidence?.label ? (
<>
<div className="flex flex-row justify-between">
<Typography variant="p3" className="text-sm " color="var(--grayscale-2)">
Confidence
</Typography>
<div className="flex flex-row gap-[2px]">
<Typography
variant="p3"
className="text-sm font-medium"
color="var(--grayscale-2)"
>
{callData?.telephoneCustomerConfidence?.label || 'NA'}
</Typography>
<Button
variant="text"
onClick={() =>
copyToClipboard(callData?.telephoneCustomerConfidence?.label || '')
}
>
<CopyToClipboardIcon width={16} height={16} />
</Button>
</div>
</div>
</>
) : null}
</div>
</PopperContent>
</Popper>
) : (
isAddedFromLonghorn && <PersonWithSeperatorIcon />
isAddedFromLonghorn &&
(callData?.telephoneCustomerConfidence?.label ? (
<Tooltip>
<TooltipTrigger>
<PersonWithSeperatorIcon />
</TooltipTrigger>
<TooltipContent className={styles.sourceTooltipContent}>
{callData.telephoneCustomerConfidence.label}
</TooltipContent>
</Tooltip>
) : (
<PersonWithSeperatorIcon />
))
)}
<span className={styles.sourceTextNames}>
{callData?.sources.slice(0, 2).map((source, index) => (

View File

@@ -4,7 +4,7 @@ import { useParams } from 'react-router-dom';
import { createKey } from 'src/utils/CaseDetail.utils';
import { RootState } from 'src/store';
import AddNewNumberOverlay from '../MoreDetails/AddNewNumberOverlay';
import { getTelephonesV2 } from '../../actions/casesDetailsActions';
import { getCustomerConfidence, getTelephonesV2 } from '../../actions/casesDetailsActions';
import CustomerDeviceData from './CustomerDeviceData';
import { addClickstreamEvent } from '../../../../service/clickStreamEventService';
import { CLICKSTREAM_EVENT_NAMES } from '../../../../service/clickStream.constant';

View File

@@ -164,6 +164,8 @@ export const SOURCE = 'source';
export const OTHER_SOURCE = 'otherSource';
export const CUSTOMER_CONFIDENCE = 'customerConfidence';
export enum PhoneNumberValidations {
VALID_PHONE_NUMBER = 'Enter a valid 10 digit mobile number',
PHONE_NUMBER_EXISTS = 'Phone number already exists',

View File

@@ -409,6 +409,7 @@ export interface ITelePhoneData {
showCreatedBy: boolean;
createdByName?: string;
createdByEmail?: string;
telephoneCustomerConfidence?: ICustomerConfidenceLevel;
}
interface IAddresses extends IApiData {
@@ -570,6 +571,18 @@ export interface ReduxCaseDetail {
pageData: PageData;
caseDetailTabsFeatureFlag: Record<caseDetailsTabsValue, boolean>;
secondarySources: ISecondarySources[];
customerConfidence: ICustomerConfidence;
}
export interface ICustomerConfidenceLevel {
label: string;
level: string;
name: string;
}
export interface ICustomerConfidence {
telephoneCustomerConfidence: ICustomerConfidenceLevel[];
longhornSecondarySources: ISecondarySources[];
}
export interface ISecondarySources {

View File

@@ -16,7 +16,10 @@ const initialState: ReduxCaseDetail = {
detailsApiLoading: false,
pageData: {},
caseDetailTabsFeatureFlag: {},
secondarySources: []
customerConfidence: {
telephoneCustomerConfidence: [],
longhornSecondarySources: []
}
};
const caseDetailsSlice = createSlice({
@@ -68,8 +71,8 @@ const caseDetailsSlice = createSlice({
}
};
},
setSecondarySources: (state, action) => {
state.secondarySources = action.payload;
setCustomerConfidence: (state, action) => {
state.customerConfidence = action.payload;
},
setCustomerInfo: (state, action) => {
const { lan, customerRefrenceId, data } = action.payload;
@@ -482,7 +485,7 @@ export const {
setTelephones,
setMonthlyEnach,
setContacts,
setSecondarySources,
setCustomerConfidence,
setCustomerInfo,
setRepaymentHistory,
setTelephonesV2,

View File

@@ -28,9 +28,14 @@ import getPathParams from '@cp/utils/getPathParams';
import logger from './logger';
import { v4 as uuidv4 } from 'uuid';
//Will add the error to sentry later
export const logError = (error?: Error | unknown, message?: string) => {};
// Sentry.captureException({ error, message });
export const logError = (error?: Error | unknown, message?: string) => {
logger({
msg: message ?? 'Error',
extras: {
error: String(error) ?? 'Error not found'
}
});
};
const MOCK_DIR = '../../__mocks__';
@@ -261,7 +266,6 @@ export enum ApiKeys {
HUMAN_REMINDER_DISCONNECT_V2,
HUMAN_REMINDER_GET_CASE_DETAILS_V2,
HUMAN_REMNINDER_DISPOSE_CALL_V2,
PHONENUMBER_SECONDARY_SOURCES,
VALIDATE_PHONE_NUMBER,
REMOVE_CONTACT,
USER_ENACH_UPLOAD_HISTORY,
@@ -307,7 +311,8 @@ export enum ApiKeys {
UPLOAD_NDRA,
DOWNLOAD_NDRA_FAILURE_FILE,
PAUSED_CASES,
UPLOAD_MEDIA
UPLOAD_MEDIA,
CUSTOMER_CONFIDENCE
}
// TODO: try to get rid of `as`
@@ -572,7 +577,6 @@ API_URLS[ApiKeys.HUMAN_REMINDER_GET_CASE_DETAILS_V2] = '/hrc/case-details';
API_URLS[ApiKeys.HUMAN_REMNINDER_DISPOSE_CALL_V2] = '/hrc/dispose';
API_URLS[ApiKeys.HUMAN_REMINDER_AVAILABLITY_V2] =
'/hrc/agent-activity/{agentAvailabilityStatus}/{callId}';
API_URLS[ApiKeys.PHONENUMBER_SECONDARY_SOURCES] = '/list-longhorn-secondary-sources';
API_URLS[ApiKeys.VALIDATE_PHONE_NUMBER] = '/validate-existing-telephone/{number}';
API_URLS[ApiKeys.REMOVE_CONTACT] = '/global-access/telephone/remove';
API_URLS[ApiKeys.USER_ENACH_UPLOAD_HISTORY] = 'v2/file-uploads/history';
@@ -615,6 +619,7 @@ API_URLS[ApiKeys.UPLOAD_NDRA] = '/agent/bulk/ndra';
API_URLS[ApiKeys.DOWNLOAD_NDRA_FAILURE_FILE] = '/uploads/v2/{referenceId}/failure-report';
API_URLS[ApiKeys.PAUSED_CASES] = '/allocated-cases/paused';
API_URLS[ApiKeys.UPLOAD_MEDIA] = 'v2/interactions/upload/media';
API_URLS[ApiKeys.CUSTOMER_CONFIDENCE] = '/telephone-configurations';
// TODO: try to get rid of `as`
const MOCK_API_URLS: Record<ApiKeys, string> = {} as Record<ApiKeys, string>;
@@ -1036,8 +1041,8 @@ axiosInstance.interceptors.response.use(
axiosInstance.defaults.headers.common['Content-Type'] = 'application/json';
axiosInstance.defaults.baseURL = `${window?.config?.BFF_SERVICE_BASE_URL || '/api'}`;
axiosInstance.defaults.headers.common['routing_key'] = '';
if (USE_MOCK && !import.meta.env.PROD) {
axiosInstance.defaults.baseURL = '/';
}