TP-74739|sourceAddition (#1062)
* TP-74739|basic layout,popper|varshitha * TP-74739|cobntroller added|varshitha * TP-74739|validations added|varshitha * TP-74739|custom copy dropdown|varshitha * TP-74739|others fix|varshitha * TP-74739|validate api added|varshitha * TP-74739|globalAccess|varshitha * TP-74739|contract changes|varshitha * TP-74739|slice changes|varshitha * TP-74739|error msg changes|varshitha * TP-74739|fixes|varshitha * TP-74739|integration fixes|varshitha * TP-74739|cross option requirement,dropdown fix|varshitha * TP-74739|more validations added,retain dropdown state|varshitha * TP-74739|code cleanup|varshitha * TP-74739|webui update|varshitha
This commit is contained in:
34
src/assets/icons/PersonWithSeprator.tsx
Normal file
34
src/assets/icons/PersonWithSeprator.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { IconProps } from './types';
|
||||
|
||||
const PersonWithSeperatorIcon = (props: IconProps) => {
|
||||
const { width = '16', height = '16', fillColor = '#0276FE' } = props;
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
>
|
||||
<mask
|
||||
id="mask0_353_7506"
|
||||
style={{ maskType: 'alpha' }}
|
||||
maskUnits="userSpaceOnUse"
|
||||
x="0"
|
||||
y="0"
|
||||
width={width}
|
||||
height={height}
|
||||
>
|
||||
<rect width={width} height={height} fill="#D9D9D9" />
|
||||
</mask>
|
||||
<g mask="url(#mask0_353_7506)">
|
||||
<path
|
||||
d="M8.00033 8.00008C7.26699 8.00008 6.63921 7.73897 6.11699 7.21675C5.59477 6.69453 5.33366 6.06675 5.33366 5.33341C5.33366 4.60008 5.59477 3.9723 6.11699 3.45008C6.63921 2.92786 7.26699 2.66675 8.00033 2.66675C8.73366 2.66675 9.36144 2.92786 9.88366 3.45008C10.4059 3.9723 10.667 4.60008 10.667 5.33341C10.667 6.06675 10.4059 6.69453 9.88366 7.21675C9.36144 7.73897 8.73366 8.00008 8.00033 8.00008ZM2.66699 12.0001V11.4667C2.66699 11.089 2.76421 10.7417 2.95866 10.4251C3.1531 10.1084 3.41144 9.86675 3.73366 9.70008C4.42255 9.35564 5.12255 9.0973 5.83366 8.92508C6.54477 8.75286 7.26699 8.66675 8.00033 8.66675C8.73366 8.66675 9.45588 8.75286 10.167 8.92508C10.8781 9.0973 11.5781 9.35564 12.267 9.70008C12.5892 9.86675 12.8475 10.1084 13.042 10.4251C13.2364 10.7417 13.3337 11.089 13.3337 11.4667V12.0001C13.3337 12.3667 13.2031 12.6806 12.942 12.9417C12.6809 13.2029 12.367 13.3334 12.0003 13.3334H4.00033C3.63366 13.3334 3.31977 13.2029 3.05866 12.9417C2.79755 12.6806 2.66699 12.3667 2.66699 12.0001Z"
|
||||
fill={fillColor}
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default PersonWithSeperatorIcon;
|
||||
14
src/assets/icons/ResizeIcon.tsx
Normal file
14
src/assets/icons/ResizeIcon.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
const ResizeIcon = () => {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="8" height="16" viewBox="0 0 8 16" fill="none">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M1.42169 16L6.5925 16C7.37112 16 8 15.3705 8 14.5912L8 9.41548C8 8.15653 6.47271 7.52706 5.58429 8.41632L0.413478 13.592C-0.474944 14.4813 0.163921 16 1.42169 16Z"
|
||||
fill="#E8E8E8"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResizeIcon;
|
||||
@@ -11,7 +11,9 @@ const OptionsList: React.FC<IOptionsList> = ({
|
||||
isMultiSelect,
|
||||
optionTemplate,
|
||||
onOptionClick,
|
||||
isOptionSelected
|
||||
isOptionSelected,
|
||||
notFoundCopy,
|
||||
customOptionsContainer
|
||||
}) => {
|
||||
if (showLoader) {
|
||||
return (
|
||||
@@ -27,7 +29,7 @@ const OptionsList: React.FC<IOptionsList> = ({
|
||||
return (
|
||||
<Card className={styles.options}>
|
||||
<Typography variant="h4" className={styles.noResult}>
|
||||
No results found !
|
||||
{notFoundCopy ? notFoundCopy : 'No results found !'}
|
||||
</Typography>
|
||||
</Card>
|
||||
);
|
||||
@@ -38,7 +40,7 @@ const OptionsList: React.FC<IOptionsList> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className={styles.options}>
|
||||
<Card className={cx(styles.options, customOptionsContainer)}>
|
||||
{options.map(option => (
|
||||
<div key={option.value} onClick={() => onOptionClick(option)}>
|
||||
{isMultiSelect ? (
|
||||
|
||||
@@ -31,6 +31,8 @@ const SingleAutocompleteDropdown = forwardRef<HTMLInputElement, ISingleAutocompl
|
||||
customOptionTemplate,
|
||||
onSearch,
|
||||
onSelectionChange,
|
||||
notFoundCopy,
|
||||
customOptionsContainer,
|
||||
...restInputProps
|
||||
},
|
||||
ref
|
||||
@@ -239,7 +241,7 @@ const SingleAutocompleteDropdown = forwardRef<HTMLInputElement, ISingleAutocompl
|
||||
ref={inputRef}
|
||||
placeholder={placeholder}
|
||||
type="text"
|
||||
value={searchText}
|
||||
value={selectedOption ? selectedOption : searchText}
|
||||
onChange={handleInputChange}
|
||||
disabled={disabled}
|
||||
{...restInputProps}
|
||||
@@ -259,7 +261,9 @@ const SingleAutocompleteDropdown = forwardRef<HTMLInputElement, ISingleAutocompl
|
||||
) : null}
|
||||
{showPicker ? (
|
||||
<OptionsList
|
||||
notFoundCopy={notFoundCopy}
|
||||
options={sortedOptions}
|
||||
customOptionsContainer={customOptionsContainer}
|
||||
isOptionSelected={isOptionSelected}
|
||||
onOptionClick={handleOptionClick}
|
||||
optionTemplate={customOptionTemplate}
|
||||
|
||||
@@ -35,7 +35,9 @@ export interface ISingleAutocompleteDropdown extends ComponentPropsWithRef<'inpu
|
||||
hideCloseOption?: boolean;
|
||||
showSearchIcon?: boolean;
|
||||
enableFuzzySearch?: boolean;
|
||||
notFoundCopy?: string;
|
||||
fuzzySearchAdditionalKeys?: string[];
|
||||
customOptionsContainer?: string;
|
||||
onSearch?: (searchTerm: string) => void;
|
||||
onSelectionChange: (selectedOptions: IOption | null) => void;
|
||||
customOptionTemplate?: (option: IOption) => React.ReactNode;
|
||||
@@ -49,6 +51,8 @@ export interface IOptionsList {
|
||||
onOptionClick: (option: IOption) => void;
|
||||
isOptionSelected: (option: IOption) => boolean;
|
||||
optionTemplate?: (option: IOption) => React.ReactNode;
|
||||
notFoundCopy?: string;
|
||||
customOptionsContainer?: string;
|
||||
}
|
||||
|
||||
export interface ISelectedOptions {
|
||||
|
||||
@@ -34,6 +34,7 @@ interface PopperOptions {
|
||||
crossAxisOffset?: number;
|
||||
disableCloseOnOutsideClick?: boolean;
|
||||
transform?: boolean;
|
||||
hideStrategy?: 'escaped' | 'referenceHidden' | undefined;
|
||||
}
|
||||
|
||||
interface PopperContentProps {
|
||||
@@ -67,7 +68,8 @@ export function usePopper({
|
||||
mainAxisOffset = 9,
|
||||
crossAxisOffset = 9,
|
||||
disableCloseOnOutsideClick = false,
|
||||
transform = true
|
||||
transform = true,
|
||||
hideStrategy = 'escaped'
|
||||
}: PopperOptions = {}) {
|
||||
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(initialOpen);
|
||||
|
||||
@@ -94,7 +96,7 @@ export function usePopper({
|
||||
arrow({ element: arrowRef?.current }),
|
||||
hide({
|
||||
padding: hiddenPadding,
|
||||
strategy: 'escaped'
|
||||
strategy: hideStrategy
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
@@ -4,8 +4,10 @@ import { STORE_KEYS, ToastMessage } from '../constants';
|
||||
import {
|
||||
setCaseDetail,
|
||||
setCaseDetailError,
|
||||
setCaseDetailLoading
|
||||
setCaseDetailLoading,
|
||||
setSecondarySources
|
||||
} from '../reducers/caseDetailSlice';
|
||||
import { Dispatch } from '@reduxjs/toolkit';
|
||||
|
||||
interface dispatchProps {
|
||||
payload: any;
|
||||
@@ -51,9 +53,11 @@ export const postAlternateNumber =
|
||||
(
|
||||
loanaccountnumber: string,
|
||||
customerreferenceid: string,
|
||||
alternateNumber: string,
|
||||
successCallback: () => void,
|
||||
failureCallback: () => void
|
||||
cb: (isModalOpen: boolean) => void,
|
||||
alternateNumber: string,
|
||||
source: string,
|
||||
otherSource?: string
|
||||
) =>
|
||||
() => {
|
||||
axiosInstance
|
||||
@@ -63,6 +67,8 @@ export const postAlternateNumber =
|
||||
{
|
||||
number: alternateNumber,
|
||||
source: 'LONGHORN',
|
||||
secondarySource: source,
|
||||
remarkForSecondarySource: otherSource,
|
||||
type: 'Alternate Contact'
|
||||
}
|
||||
],
|
||||
@@ -70,16 +76,36 @@ export const postAlternateNumber =
|
||||
headers: { customerreferenceid, loanaccountnumber }
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
successCallback();
|
||||
.then(response => {
|
||||
toast(ToastMessage.NUMBER_ADDED_SUCCESSFULLY, { type: 'success' });
|
||||
cb(false);
|
||||
successCallback();
|
||||
})
|
||||
.catch(() => {
|
||||
failureCallback();
|
||||
.catch(err => {
|
||||
toast(ToastMessage.NUMBER_ADDITION_FAILED, { type: 'error' });
|
||||
});
|
||||
};
|
||||
|
||||
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 {
|
||||
const response = await axiosInstance.get(url, {
|
||||
headers: { customerReferenceId: customerRefId }
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
return 'Error';
|
||||
}
|
||||
};
|
||||
|
||||
export const postAlternateEmail = (
|
||||
loanaccountnumber: string,
|
||||
customerreferenceid: string,
|
||||
|
||||
@@ -4,60 +4,38 @@ 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, useReducer } from 'react';
|
||||
import { IAddNewNumber, IAddNewNumberAction } from '../../interfaces/MoreDetails.type';
|
||||
import { AddNewNumberKind } from '../../constants/MoreDetails.constant';
|
||||
import { useCallback } from 'react';
|
||||
import Typography from '@navi/web-ui/lib/primitives/Typography/Typography';
|
||||
import { postAlternateNumber } from '../../actions/moreDetailsActions';
|
||||
import { getTelephonesV2 } from '../../actions/casesDetailsActions';
|
||||
import { createKey, isNumericLength } from 'src/utils/CaseDetail.utils';
|
||||
import {
|
||||
getSecondarySources,
|
||||
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';
|
||||
import Modal from '@cp/src/components/Modal/Modal';
|
||||
import { isValidPhoneNumber } from '@cp/src/utils/commonUtils';
|
||||
import { RootState } from '@cp/src/store';
|
||||
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 SingleAutoDropdown from './SingleAutoDropdown';
|
||||
import {
|
||||
OTHER_SOURCE,
|
||||
OtherSourceValidations,
|
||||
PHONE_NUMBER,
|
||||
PhoneNumberValidations,
|
||||
SOURCE
|
||||
} from '../../constants/MoreDetails.constant';
|
||||
|
||||
const addNewNumberReducer = (state: IAddNewNumber, action: IAddNewNumberAction) => {
|
||||
const { type, payload } = action;
|
||||
switch (type) {
|
||||
case AddNewNumberKind.INITIAL_STATE:
|
||||
return {
|
||||
...state,
|
||||
open: true
|
||||
};
|
||||
case AddNewNumberKind.CLOSE:
|
||||
return {
|
||||
...state,
|
||||
open: false
|
||||
};
|
||||
case AddNewNumberKind.BEFORE_API_CALL:
|
||||
return {
|
||||
...state,
|
||||
loading: true
|
||||
};
|
||||
case AddNewNumberKind.API_CALL_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
loading: false,
|
||||
open: false,
|
||||
newNumber: ''
|
||||
};
|
||||
case AddNewNumberKind.API_ERROR:
|
||||
return {
|
||||
...state,
|
||||
loading: false
|
||||
};
|
||||
case AddNewNumberKind.SET_NUMBER: {
|
||||
return {
|
||||
...state,
|
||||
newNumber: payload?.newNumber || ''
|
||||
};
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
export interface AddNumberProps {
|
||||
phoneNumber: string;
|
||||
source: string;
|
||||
otherSource: string;
|
||||
}
|
||||
|
||||
const AddNewNumberOverlay: React.FC<React.PropsWithChildren> = () => {
|
||||
const { loanId = '', customerId = '' } = useParams();
|
||||
@@ -68,64 +46,98 @@ const AddNewNumberOverlay: React.FC<React.PropsWithChildren> = () => {
|
||||
}));
|
||||
const isGlobalAccessRoleGiven = user?.roles?.includes(Roles.ROLE_GLOBAL_ACCESS);
|
||||
const disableCTAs = isGlobalAccessRoleGiven ? !editAccessFlag : false;
|
||||
const { telephones } = useSelector((state: RootState) => ({
|
||||
telephones: state.caseDetail.pageData?.[createKey(loanId, customerId)]?.telephonesv2?.data
|
||||
}));
|
||||
const secondarySources = useSelector((state: RootState) => state.caseDetail.secondarySources);
|
||||
|
||||
const optionsList =
|
||||
secondarySources?.map(source => ({
|
||||
label: source.label,
|
||||
value: source.value
|
||||
})) || [];
|
||||
|
||||
const [isModalOpen, setIsModalOpen] = React.useState(false);
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
watch,
|
||||
setValue,
|
||||
formState: { errors, isValid, isDirty },
|
||||
register,
|
||||
control,
|
||||
getValues,
|
||||
reset
|
||||
} = useForm<AddNumberProps>({ mode: 'onChange' });
|
||||
const dispatch = useDispatch();
|
||||
const [addNewNumber, dispatchAddNewNumber] = useReducer(addNewNumberReducer, {
|
||||
open: false,
|
||||
loading: false,
|
||||
newNumber: ''
|
||||
});
|
||||
|
||||
const openModal = () => {
|
||||
dispatchAddNewNumber({
|
||||
type: AddNewNumberKind.INITIAL_STATE
|
||||
});
|
||||
setIsModalOpen(true);
|
||||
dispatch(getSecondarySources());
|
||||
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.LH_Phone_AddNewNumberCTA);
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
dispatchAddNewNumber({
|
||||
type: AddNewNumberKind.CLOSE
|
||||
});
|
||||
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.LH_Phone_AddNewNumberCross);
|
||||
setIsModalOpen(false);
|
||||
};
|
||||
|
||||
const handleAddNumber = () => {
|
||||
dispatchAddNewNumber({ type: AddNewNumberKind.BEFORE_API_CALL });
|
||||
dispatch(
|
||||
postAlternateNumber(
|
||||
loanId,
|
||||
customerId,
|
||||
addNewNumber.newNumber,
|
||||
successCallback,
|
||||
failureCallback
|
||||
)
|
||||
);
|
||||
const handleFormSubmit = data => {};
|
||||
const handleAddNumber = async () => {
|
||||
if (!(!isDirty || !isValid)) {
|
||||
dispatch(
|
||||
postAlternateNumber(
|
||||
loanId,
|
||||
customerId,
|
||||
successCallback,
|
||||
setIsModalOpen,
|
||||
getValues(PHONE_NUMBER),
|
||||
getValues(SOURCE).value,
|
||||
getValues(OTHER_SOURCE)
|
||||
)
|
||||
);
|
||||
}
|
||||
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.LH_Phone_AddNewNumberSubmit, {
|
||||
newPhoneNumber: addNewNumber.newNumber
|
||||
newPhoneNumber: getValues(PHONE_NUMBER)
|
||||
});
|
||||
};
|
||||
|
||||
const successCallback = useCallback(() => {
|
||||
dispatchAddNewNumber({ type: AddNewNumberKind.API_CALL_SUCCESS });
|
||||
dispatch(getTelephonesV2(loanId, customerId));
|
||||
}, [addNewNumber.newNumber]);
|
||||
|
||||
const failureCallback = useCallback(() => {
|
||||
dispatchAddNewNumber({ type: AddNewNumberKind.API_ERROR });
|
||||
}, [addNewNumber.newNumber]);
|
||||
reset();
|
||||
}, []);
|
||||
|
||||
const handleNumberChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
event.preventDefault();
|
||||
dispatchAddNewNumber({
|
||||
type: AddNewNumberKind.SET_NUMBER,
|
||||
payload: { newNumber: event.target.value }
|
||||
});
|
||||
}, []);
|
||||
const handleKeyPress = event => {
|
||||
if (event.key === 'Enter' && isValidPhoneNumber(addNewNumber.newNumber)) {
|
||||
handleAddNumber();
|
||||
|
||||
const selectedSource = watch(SOURCE, '');
|
||||
const enteredOtherSource = watch(OTHER_SOURCE, '');
|
||||
|
||||
const validateNumber = async (value: string) => {
|
||||
if (value.length == 10) {
|
||||
if (!isValidPhoneNumber(value)) return PhoneNumberValidations.VALID_PHONE_NUMBER;
|
||||
const errorMessage = await getValidateNumber(value, customerId);
|
||||
if (errorMessage === 'Error') {
|
||||
return PhoneNumberValidations.ERROR;
|
||||
}
|
||||
if (errorMessage?.isPresent) {
|
||||
if (errorMessage?.isValid) {
|
||||
return PhoneNumberValidations.PHONE_NUMBER_EXISTS;
|
||||
} else return PhoneNumberValidations.NUMBER_MARKED_INVALID;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return PhoneNumberValidations.VALID_PHONE_NUMBER;
|
||||
};
|
||||
const validateOtherSource = (value: string) => {
|
||||
if (value?.length < 3) return OtherSourceValidations.MIN_SOURCE_LENGTH;
|
||||
const areAllCharactersSame = value.split('').every(char => char === value[0]);
|
||||
if (areAllCharactersSame) return OtherSourceValidations.SAME_CHARACTERS;
|
||||
return /^(?=.*[a-zA-Z0-9]).+$/i.test(value) || OtherSourceValidations.ONLY_SPECIAL_CHARACTERS;
|
||||
};
|
||||
|
||||
const handleSelectionChange = selectedOption => {
|
||||
setValue(SOURCE, selectedOption.value);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -141,28 +153,87 @@ const AddNewNumberOverlay: React.FC<React.PropsWithChildren> = () => {
|
||||
>
|
||||
Add new number
|
||||
</Button>
|
||||
<Modal visible={addNewNumber.open} onClose={closeModal} header="Add new number">
|
||||
<div className={styles.addNewNumberCard}>
|
||||
<Typography className={styles.enterPhoneNumber} variant={'p4'}>
|
||||
Mobile number
|
||||
</Typography>
|
||||
<input
|
||||
type={'tel'}
|
||||
placeholder="Enter mobile number"
|
||||
minLength={10}
|
||||
maxLength={10}
|
||||
onChange={handleNumberChange}
|
||||
onKeyPress={handleKeyPress}
|
||||
value={addNewNumber.newNumber}
|
||||
autoFocus
|
||||
/>
|
||||
<Button
|
||||
disabled={!isValidPhoneNumber(addNewNumber.newNumber) || addNewNumber.loading}
|
||||
onClick={handleAddNumber}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
<Modal visible={isModalOpen} onClose={closeModal} header="Add new number">
|
||||
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||
<div className={styles.addNewNumberCard}>
|
||||
<Typography className={styles.enterPhoneNumber} variant={'p4'}>
|
||||
Mobile number
|
||||
</Typography>
|
||||
|
||||
<BorderedInputTextBox
|
||||
control={control}
|
||||
maxLength={10}
|
||||
name="phoneNumber"
|
||||
rules={{
|
||||
validate: (value: string) => {
|
||||
if (watch(PHONE_NUMBER)) {
|
||||
return validateNumber(value);
|
||||
}
|
||||
}
|
||||
}}
|
||||
onChange={handleNumberChange}
|
||||
placeholder="Enter phone number"
|
||||
type="string"
|
||||
errors={errors}
|
||||
containerClassName="w-[100%]"
|
||||
isRequired={true}
|
||||
/>
|
||||
<Typography className={cx(styles.enterPhoneNumber, 'my-[8px]')} variant={'p4'}>
|
||||
Source of number
|
||||
</Typography>
|
||||
<SingleAutoDropdown
|
||||
optionsList={optionsList}
|
||||
selectedSource={selectedSource}
|
||||
handleSelectionChange={handleSelectionChange}
|
||||
placeholder="Enter or select source"
|
||||
errors={errors}
|
||||
control={control}
|
||||
name="source"
|
||||
rules={{
|
||||
required: true
|
||||
}}
|
||||
/>
|
||||
{selectedSource?.value === 'OTHER_SOURCES' && (
|
||||
<>
|
||||
<Typography className={cx(styles.enterPhoneNumber, 'mt-[24px]')} variant={'p4'}>
|
||||
Enter other source
|
||||
</Typography>
|
||||
|
||||
<BorderedInputTextBox
|
||||
placeholder="Enter a valid source"
|
||||
type="string"
|
||||
errors={errors}
|
||||
maxLength={30}
|
||||
containerClassName="w-[100%]"
|
||||
RightInputAdornment={
|
||||
<div className="mt-[25px] flex row mb-[6px]">
|
||||
<Typography variant={'p4'} className="text-grayscale-3 text-sm">
|
||||
{enteredOtherSource?.length}/30
|
||||
</Typography>
|
||||
</div>
|
||||
}
|
||||
control={control}
|
||||
name="otherSource"
|
||||
rules={{
|
||||
required: true,
|
||||
validate: (value: string) => {
|
||||
return validateOtherSource(value);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<div className="flex justify-end mt-[24px]">
|
||||
<Button
|
||||
className=""
|
||||
disabled={!isDirty || !isValid || !isValidPhoneNumber(getValues(PHONE_NUMBER))}
|
||||
onClick={handleAddNumber}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { BorderedInput } from '@navi/web-ui/lib/primitives';
|
||||
import { AddNumberProps } from './AddNewNumberOverlay';
|
||||
import {
|
||||
Control,
|
||||
Controller,
|
||||
FieldErrorsImpl,
|
||||
RegisterOptions,
|
||||
UseFormGetValues
|
||||
} from 'react-hook-form';
|
||||
|
||||
type IBorderedInputTextBoxProps = {
|
||||
placeholder: string;
|
||||
type: string;
|
||||
errors: Partial<FieldErrorsImpl<AddNumberProps>>;
|
||||
containerClassName?: string;
|
||||
RightInputAdornment?: React.ReactNode;
|
||||
control: Control<AddNumberProps, any>;
|
||||
name: keyof AddNumberProps;
|
||||
rules: RegisterOptions;
|
||||
isRequired?: boolean;
|
||||
maxLength?: number;
|
||||
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
};
|
||||
const BorderedInputTextBox = (props: IBorderedInputTextBoxProps) => {
|
||||
const {
|
||||
placeholder,
|
||||
type,
|
||||
errors,
|
||||
containerClassName,
|
||||
RightInputAdornment,
|
||||
control,
|
||||
name,
|
||||
rules,
|
||||
maxLength,
|
||||
isRequired = true
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Controller
|
||||
control={control}
|
||||
name={name}
|
||||
rules={rules}
|
||||
render={({ field: { onChange, value, ref } }) => (
|
||||
<BorderedInput
|
||||
placeholder={placeholder}
|
||||
ref={ref}
|
||||
type={type}
|
||||
fullWidth={true}
|
||||
required={isRequired}
|
||||
containerClassName={containerClassName}
|
||||
value={value}
|
||||
onChange={e => {
|
||||
onChange(e.target.value);
|
||||
}}
|
||||
maxLength={maxLength}
|
||||
error={errors && errors[name] && errors[name].message}
|
||||
RightInputAdornment={RightInputAdornment}
|
||||
onFocus={e => e}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default BorderedInputTextBox;
|
||||
@@ -0,0 +1,59 @@
|
||||
import { AddNumberProps } from './AddNewNumberOverlay';
|
||||
import { Control, Controller, FieldErrorsImpl, RegisterOptions } from 'react-hook-form';
|
||||
import SingleAutocompleteDropdown from '@cp/src/components/AutocompleteDropdown/SingleAutocompleteDropdown';
|
||||
|
||||
type ISingleAutoDropdownProps = {
|
||||
placeholder: string;
|
||||
errors: Partial<FieldErrorsImpl<AddNumberProps>>;
|
||||
containerClassName?: string;
|
||||
control: Control<AddNumberProps, any>;
|
||||
name: keyof AddNumberProps;
|
||||
rules: RegisterOptions;
|
||||
optionsList: {
|
||||
label: string;
|
||||
value: string;
|
||||
}[];
|
||||
selectedSource: string;
|
||||
handleSelectionChange: (selectedOption: any) => void;
|
||||
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
};
|
||||
const SingleAutoDropdown = (props: ISingleAutoDropdownProps) => {
|
||||
const {
|
||||
placeholder,
|
||||
errors,
|
||||
control,
|
||||
name,
|
||||
rules,
|
||||
selectedSource,
|
||||
handleSelectionChange,
|
||||
optionsList
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Controller
|
||||
control={control}
|
||||
name={name}
|
||||
rules={rules}
|
||||
render={({ field: { onChange, value, ref } }) => (
|
||||
<SingleAutocompleteDropdown
|
||||
ref={ref}
|
||||
options={optionsList}
|
||||
disabled={false}
|
||||
notFoundCopy={
|
||||
optionsList.length > 0
|
||||
? 'No results found! Try adding other source.'
|
||||
: 'No sources found'
|
||||
}
|
||||
customOptionsContainer="!max-h-[200px]"
|
||||
onSelectionChange={onChange}
|
||||
selectedOption={value?.label}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SingleAutoDropdown;
|
||||
@@ -87,7 +87,8 @@
|
||||
.addNewNumberCard {
|
||||
box-sizing: border-box;
|
||||
width: 302px;
|
||||
height: 138px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 8px;
|
||||
background-color: var(--bg-primary);
|
||||
transition: height 200ms;
|
||||
@@ -102,18 +103,7 @@
|
||||
.enterPhoneNumber {
|
||||
color: var(--grayscale-2);
|
||||
}
|
||||
input {
|
||||
margin-top: 12px;
|
||||
width: 100%;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
height: 36px;
|
||||
padding: 12px;
|
||||
}
|
||||
button {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,12 +13,16 @@ import { ITelePhoneData, LIMIT_TYPE } from '@cp/pages/CaseDetails/interfaces/Cas
|
||||
import PocNumbersTag from '../PocTag/Index';
|
||||
import dayjs from 'dayjs';
|
||||
import InfoIconOutlined from '@cp/assets/images/icons/InfoIconOutlined';
|
||||
import PersonWithSeperatorIcon from '@cp/src/assets/icons/PersonWithSeprator';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { RootState } from '@cp/src/store';
|
||||
import { Roles } from '@cp/src/pages/auth/constants/AuthConstants';
|
||||
import { Popper, PopperContent, PopperTrigger } from '@cp/src/components/Popper/Popper';
|
||||
import CopyToClipboardIcon from '@cp/src/assets/icons/CopyToClipboardIcon';
|
||||
import Button from '@navi/web-ui/lib/primitives/Button/Button';
|
||||
import { copyToClipboard } from '@cp/src/utils/commonUtils';
|
||||
import VerticalSeperator from '@cp/src/components/VerticalSeperator';
|
||||
import DeleteIcon from '@cp/src/assets/icons/DeleteIcon';
|
||||
import Button from '@navi/web-ui/lib/primitives/Button/Button';
|
||||
import RemoveNumberModal from './RemoveNumberModal';
|
||||
|
||||
const NO_OF_RECORDS = 5;
|
||||
@@ -27,6 +31,7 @@ const PhoneNumberContactCard = ({ callData }: { callData: ITelePhoneData }) => {
|
||||
const openTrueCallerTab = (number: string) => {
|
||||
window.open(`tel:${number}`, '_blank');
|
||||
};
|
||||
const userRole = useSelector((state: RootState) => state?.common?.userData?.roles);
|
||||
const { dailyLimit, hourlyLimit, callRemainingPercentage } = useMemo(() => {
|
||||
const dailyLimit = callData?.limit?.find(limitRecord => limitRecord.type === LIMIT_TYPE.DAILY);
|
||||
const hourlyLimit = callData?.limit?.find(
|
||||
@@ -41,7 +46,8 @@ const PhoneNumberContactCard = ({ callData }: { callData: ITelePhoneData }) => {
|
||||
};
|
||||
}, [callData]);
|
||||
|
||||
const userRole = useSelector((state: RootState) => state.common.userData?.roles);
|
||||
const isAddedFromLonghorn = callData?.showCreatedBy || false;
|
||||
|
||||
const [isModalOpen, setIsModalOpen] = React.useState(false);
|
||||
|
||||
const isPrimary = callData?.sources.includes('Primary');
|
||||
@@ -167,6 +173,55 @@ const PhoneNumberContactCard = ({ callData }: { callData: ITelePhoneData }) => {
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.sourcesWrapper}>
|
||||
{userRole?.includes(Roles.ROLE_GLOBAL_ACCESS) && isAddedFromLonghorn ? (
|
||||
<Popper placement="bottom-start" hideStrategy="referenceHidden">
|
||||
<PopperTrigger>
|
||||
<PersonWithSeperatorIcon />
|
||||
</PopperTrigger>
|
||||
<PopperContent
|
||||
headerText="Added by"
|
||||
className="popperWrapper"
|
||||
arrowColor="var(--orange-oldlase)"
|
||||
>
|
||||
<div className="flex flex-col px-[12px] py-[8px] w-[286px] gap-[8px]">
|
||||
<div className="flex flex-row justify-between">
|
||||
<Typography variant="p3" className="text-sm" color="var(--grayscale-2)">
|
||||
Name
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="p3"
|
||||
className="text-sm font-medium"
|
||||
color="var(--grayscale-2)"
|
||||
>
|
||||
{callData?.createdByName || 'NA'}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="flex flex-row justify-between">
|
||||
<Typography variant="p3" className="text-sm " color="var(--grayscale-2)">
|
||||
Email
|
||||
</Typography>
|
||||
<div className="flex flex-row gap-[2px]">
|
||||
<Typography
|
||||
variant="p3"
|
||||
className="text-sm font-medium"
|
||||
color="var(--grayscale-2)"
|
||||
>
|
||||
{callData?.createdByEmail || 'NA'}
|
||||
</Typography>
|
||||
<Button
|
||||
variant="text"
|
||||
onClick={() => copyToClipboard(callData?.createdByEmail || '')}
|
||||
>
|
||||
<CopyToClipboardIcon width={16} height={16} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PopperContent>
|
||||
</Popper>
|
||||
) : (
|
||||
isAddedFromLonghorn && <PersonWithSeperatorIcon />
|
||||
)}
|
||||
<span className={styles.sourceTextNames}>
|
||||
{callData?.sources.slice(0, 2).map((source, index) => (
|
||||
<>
|
||||
|
||||
@@ -68,6 +68,8 @@
|
||||
}
|
||||
|
||||
.sourcesWrapper {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
background: transparent;
|
||||
padding: 9px 16px;
|
||||
font-weight: 500;
|
||||
|
||||
@@ -157,3 +157,22 @@ export const tooltipArrowColor = 'var(--tooltip-background-color)';
|
||||
export const gmailComposeLink = 'https://mail.google.com/mail/?view=cm&fs=1&to=';
|
||||
|
||||
export const gmeetCallLink = 'https://meet.google.com/calling/';
|
||||
|
||||
export const PHONE_NUMBER = 'phoneNumber';
|
||||
|
||||
export const SOURCE = 'source';
|
||||
|
||||
export const OTHER_SOURCE = 'otherSource';
|
||||
|
||||
export enum PhoneNumberValidations {
|
||||
VALID_PHONE_NUMBER = 'Enter a valid 10 digit mobile number',
|
||||
PHONE_NUMBER_EXISTS = 'Phone number already exists',
|
||||
NUMBER_MARKED_INVALID = 'Number marked invalid previously',
|
||||
ERROR = 'Error in validating number'
|
||||
}
|
||||
|
||||
export enum OtherSourceValidations {
|
||||
MIN_SOURCE_LENGTH = 'Enter at least 3 characters',
|
||||
SAME_CHARACTERS = 'All characters cannot be same',
|
||||
ONLY_SPECIAL_CHARACTERS = 'Only special characters are not allowed'
|
||||
}
|
||||
|
||||
@@ -200,7 +200,8 @@ export enum STORE_KEYS {
|
||||
SHERLOCK = 'sherlock',
|
||||
EMAILS = 'emails',
|
||||
GEOLOCATIONS_ADDRESSES = 'geolocationsAndAddresses',
|
||||
SIMILAR_GEOLOCATIONS = 'similarGeolocations'
|
||||
SIMILAR_GEOLOCATIONS = 'similarGeolocations',
|
||||
SECONDARY_SOURCES = 'secondarySources'
|
||||
}
|
||||
|
||||
export const ExcludedRedirectionEndPointsList = [COSMOS_PATH];
|
||||
|
||||
@@ -372,6 +372,9 @@ export interface ITelePhoneData {
|
||||
createdAt: string;
|
||||
isPoc: boolean;
|
||||
limit: IPhoneNumberLimit[];
|
||||
showCreatedBy: boolean;
|
||||
createdByName?: string;
|
||||
createdByEmail?: string;
|
||||
}
|
||||
|
||||
interface IAddresses extends IApiData {
|
||||
@@ -529,8 +532,13 @@ export interface ReduxCaseDetail {
|
||||
detailsApiLoading: boolean;
|
||||
pageData: PageData;
|
||||
caseDetailTabsFeatureFlag: Record<caseDetailsTabsValue, boolean>;
|
||||
secondarySources: ISecondarySources[];
|
||||
}
|
||||
|
||||
export interface ISecondarySources {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
export interface ISherlock {
|
||||
type: string;
|
||||
date: string;
|
||||
|
||||
@@ -23,12 +23,16 @@ export interface IAddNewNumber {
|
||||
open: boolean;
|
||||
loading: boolean;
|
||||
newNumber: string;
|
||||
source: string;
|
||||
otherSource?: string;
|
||||
}
|
||||
|
||||
export interface IAddNewNumberAction {
|
||||
type: AddNewNumberKind;
|
||||
payload?: {
|
||||
newNumber?: string;
|
||||
source: string;
|
||||
otherSource?: string;
|
||||
};
|
||||
}
|
||||
export interface IAddNewEmail {
|
||||
|
||||
@@ -15,7 +15,8 @@ import { feeWaiveTransformedEmiData } from '../components/EmiSchedule/utils';
|
||||
const initialState: ReduxCaseDetail = {
|
||||
detailsApiLoading: false,
|
||||
pageData: {},
|
||||
caseDetailTabsFeatureFlag: {}
|
||||
caseDetailTabsFeatureFlag: {},
|
||||
secondarySources: []
|
||||
};
|
||||
|
||||
const caseDetailsSlice = createSlice({
|
||||
@@ -67,6 +68,9 @@ const caseDetailsSlice = createSlice({
|
||||
}
|
||||
};
|
||||
},
|
||||
setSecondarySources: (state, action) => {
|
||||
state.secondarySources = action.payload;
|
||||
},
|
||||
setCustomerInfo: (state, action) => {
|
||||
const { lan, customerRefrenceId, data } = action.payload;
|
||||
const pageKey = createKey(lan, customerRefrenceId);
|
||||
@@ -471,6 +475,7 @@ export const {
|
||||
setTelephones,
|
||||
setMonthlyEnach,
|
||||
setContacts,
|
||||
setSecondarySources,
|
||||
setCustomerInfo,
|
||||
setRepaymentHistory,
|
||||
setTelephonesV2,
|
||||
|
||||
@@ -251,6 +251,8 @@ 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
|
||||
}
|
||||
|
||||
@@ -505,6 +507,8 @@ 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';
|
||||
|
||||
// TODO: try to get rid of `as`
|
||||
|
||||
@@ -445,7 +445,7 @@ export const isValidEmail = (email: string) => {
|
||||
|
||||
export const isValidPhoneNumber = (phoneNumber: string) => {
|
||||
// Regular expression for validating a phone number
|
||||
const phoneNumberRegex = /^[1-9][0-9]{9}$/;
|
||||
const phoneNumberRegex = /^[6-9][0-9]{9}$/;
|
||||
return phoneNumberRegex.test(phoneNumber);
|
||||
};
|
||||
|
||||
|
||||
Submodule web-ui-library updated: ceb04ad98f...a5d0e0155a
Reference in New Issue
Block a user