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:
Podili Varshitha
2024-08-08 22:45:04 +05:30
committed by GitHub
parent 900597963d
commit ee90f3dc6c
21 changed files with 507 additions and 136 deletions

View 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;

View 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;

View File

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

View File

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

View File

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

View File

@@ -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
})
]
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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) => (
<>

View File

@@ -68,6 +68,8 @@
}
.sourcesWrapper {
display: flex;
gap: 4px;
background: transparent;
padding: 9px 16px;
font-weight: 500;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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);
};