NTP-39419 | Add address flow on LH for field CIU (#1385)

* NTP-34968| Add address flow for field CIU

* NTP-34968| Add address flow for field CIU

* NTP-39419| added role check

* NTP-39419| Code refactor

* NTP-39419| Code refactor

* NTP-39419| Added enum for validation error

* NTP-39419| Code refactor

* NTP-39419| Added address validations

* NTP-39419| removed unused code

* NTP-39419| type changes

* NTP-39419| add address flow for field ciu

* NTP-39419| removed magic numbers

* NTP-39419| PR comments resolved

* NTP-24405| removed unused code
This commit is contained in:
Aishwarya Srivastava
2025-02-21 19:57:53 +05:30
committed by GitHub
parent a6fdd0537d
commit 3ea8e88969
10 changed files with 402 additions and 7 deletions

View File

@@ -1,5 +1,10 @@
import { Dispatch } from '@reduxjs/toolkit';
import axiosInstance, { ApiKeys, getApiUrl } from '../../../utils/ApiHelper';
import axiosInstance, {
API_STATUS_CODE,
ApiKeys,
getApiUrl,
logError
} from '../../../utils/ApiHelper';
import { STORE_KEYS } from '../constants';
import {
setCaseDetail,
@@ -7,6 +12,8 @@ import {
setCaseDetailLoading
} from '../reducers/caseDetailSlice';
import { ISimilarGeoLocationsPayload } from '@cp/pages/CaseDetails/interfaces/MoreDetails.type';
import { toast } from '@navi/web-ui/lib/primitives/Toast';
import { AddAddressFormValues } from '../components/Addresses/contants';
export const fetchAddresses =
(
@@ -155,3 +162,39 @@ export const fetchSimilarGeoLocations =
);
});
};
export const getPinCodeDetails = async (pinCode: string) => {
const url = getApiUrl(ApiKeys.GET_PIN_CODES_DETAILS, { pinCode });
return await axiosInstance.get(url, {
headers: {
donotHandleError: true
}
});
};
export const addAddress =
(
payload: AddAddressFormValues,
commonPayload: {
loanAccountNumber: string;
},
setIsSubmitButtonDisabled: React.Dispatch<React.SetStateAction<boolean>>,
successCb: () => void
) =>
() => {
const url = getApiUrl(ApiKeys.ADD_NEW_ADDRESS);
setIsSubmitButtonDisabled(true);
return axiosInstance
.post(url, { ...payload, ...commonPayload })
.then(response => {
if (response?.status === API_STATUS_CODE.CREATED) {
successCb();
toast('Address added successfully', { type: 'success' });
}
})
.catch(err => {
logError(err);
toast('Address update failed', { type: 'error' });
setIsSubmitButtonDisabled(false);
});
};

View File

@@ -0,0 +1,168 @@
import { Button } from '@navi/web-ui/lib/primitives';
import { useForm } from 'react-hook-form';
import { AddAddressFormValues, defaultValues } from './contants';
import { useDispatch } from 'react-redux';
import { useState } from 'react';
import AddressFormTextInput from './AddressFormTextInput';
import { checkValidPinCode, validatePincode } from '../../constants/utils';
import { addAddress, fetchAddresses, getPinCodeDetails } from '../../actions/addressesActions';
import { VALIDATION_MESSAGE } from '../../constants';
import { useParams } from 'react-router-dom';
import { IAddNewAddressFormProps } from './interfaces';
const AddNewAddressForm = (props: IAddNewAddressFormProps) => {
const { loanId, handleAddAddressModal } = props;
const {
watch,
handleSubmit,
control,
getValues,
setValue,
setError,
formState: { errors, isValid }
} = useForm<AddAddressFormValues>({
defaultValues,
mode: 'onTouched',
reValidateMode: 'onChange'
});
const dispatch = useDispatch();
const [isPincodeLoading, setIsPincodeLoading] = useState(false);
const handlePinCodeChange = (pinCode: string, onChange: (...event: any[]) => void) => {
if (checkValidPinCode(pinCode)) {
setIsPincodeLoading(true);
getPinCodeDetails(pinCode)
.then(res => {
if (res?.data?.city) {
setValue('city', res?.data?.city);
} else {
setValue('city', '');
}
})
.finally(() => {
setIsPincodeLoading(false);
});
} else {
setValue('city', '');
}
onChange(pinCode);
};
const { customerId = '', caseBusinessVertical = '', caseReferenceId = '' } = useParams();
const handleAddressFormSubmission = () => {
handleAddAddressModal();
dispatch(fetchAddresses(loanId, customerId, caseReferenceId, caseBusinessVertical));
};
const [isSubmitButtonDisabled, setIsSubmitButtonDisabled] = useState(false);
const addNewAddress = () => {
const commonPayload = {
loanAccountNumber: loanId
};
const payload = {
city: getValues('city'),
lineOne: getValues('lineOne'),
lineTwo: getValues('lineTwo'),
pinCode: getValues('pinCode')
};
dispatch(
addAddress(payload, commonPayload, setIsSubmitButtonDisabled, handleAddressFormSubmission)
);
};
return (
<div className="flex flex-col">
<form>
<div className="flex flex-col">
<AddressFormTextInput
control={control}
name="lineOne"
rules={{
required: true,
minLength: {
value: 5,
message: VALIDATION_MESSAGE.ADDRESS_LENGTH_VALIDATION_MESSAGE
},
pattern: {
value: /^(?!\s*$)[a-zA-Z0-9\s,.-]+$/,
message: VALIDATION_MESSAGE.ADDRESS_VALIDATION_MESSAGE
}
}}
errors={errors}
inputLabel="House, flat, building"
placeholder="Eg. A-101"
validationMessage={VALIDATION_MESSAGE.ADDRESS_VALIDATION_MESSAGE}
showRequiredIcon
/>
<AddressFormTextInput
control={control}
name="lineTwo"
rules={{
required: true,
minLength: {
value: 5,
message: VALIDATION_MESSAGE.ADDRESS_LENGTH_VALIDATION_MESSAGE
},
pattern: {
value: /^(?!\s*$)[a-zA-Z0-9\s,.-]+$/,
message: VALIDATION_MESSAGE.ADDRESS_VALIDATION_MESSAGE
}
}}
errors={errors}
inputLabel="Street, locality, colony"
placeholder="Eg. Chandni Chowk"
validationMessage={VALIDATION_MESSAGE.ADDRESS_VALIDATION_MESSAGE}
showRequiredIcon
containerClass="mt-5"
/>
<AddressFormTextInput
control={control}
name="pinCode"
rules={{
required: true,
minLength: { value: 6, message: VALIDATION_MESSAGE.PINCODE_VALIDATION_MESSAGE },
maxLength: { value: 6, message: VALIDATION_MESSAGE.PINCODE_VALIDATION_MESSAGE },
pattern: {
value: /^[0-9]{6}$/,
message: VALIDATION_MESSAGE.PINCODE_VALIDATION_MESSAGE
},
validate: (value: string) => {
if (watch('pinCode')) {
return validatePincode(value);
}
}
}}
errors={errors}
inputLabel="Pincode"
placeholder="Eg. 110006"
validationMessage={VALIDATION_MESSAGE.PINCODE_VALIDATION_MESSAGE}
showRequiredIcon
containerClass="mt-5"
handlePincodeChange={handlePinCodeChange}
isPincodeLoading={isPincodeLoading}
/>
<AddressFormTextInput
control={control}
name="city"
errors={errors}
inputLabel="City"
placeholder="Enter pincode to update city"
validationMessage=""
containerClass="mt-5"
isEditDisabled={true}
/>
</div>
</form>
<div className="flex justify-end pt-2">
<Button
onClick={handleSubmit(addNewAddress)}
className="!w-[124px]"
disabled={!isValid || isSubmitButtonDisabled}
>
Add
</Button>
</div>
</div>
);
};
export default AddNewAddressForm;

View File

@@ -0,0 +1,72 @@
import { BorderedInput, Typography } from '@navi/web-ui/lib/primitives';
import { Controller } from 'react-hook-form';
import RequiredIcon from '@navi/web-ui/lib/icons/RequiredIcon';
import ErrorIconOutline from '@cp/src/assets/icons/ErrorIconOutline';
import cx from 'classnames';
import styles from './index.module.scss';
import { LoadingIcon } from '@navi/web-ui/lib/icons';
import { IAddressFormTextInput } from './interfaces';
import { isFunction } from '@cp/src/utils/commonUtils';
const AddressFormTextInput = (props: IAddressFormTextInput) => {
const {
control,
name,
rules,
errors,
isEditDisabled = false,
inputLabel,
placeholder,
showRequiredIcon = false,
containerClass,
handlePincodeChange,
isPincodeLoading = false
} = props;
const handleValueChange = (
e: React.ChangeEvent<HTMLInputElement>,
onChange: (...event: any[]) => void
) => {
const value = e.target.value;
if (name === 'pinCode' && isFunction(handlePincodeChange)) {
handlePincodeChange(value, onChange);
} else {
onChange(value);
}
};
return (
<>
<div className={cx('flex items-center', containerClass)}>
<Typography color="var(--grayscale-2)" variant={'p4'}>
{inputLabel}
{showRequiredIcon ? <RequiredIcon className="mb-1 ml-1" /> : null}
</Typography>
</div>
<Controller
control={control}
name={name}
rules={rules}
render={({ field: { onChange, value, onBlur } }) => (
<BorderedInput
placeholder={placeholder}
fullWidth
onChange={e => handleValueChange(e, onChange)}
value={value}
disabled={isEditDisabled}
RightInputAdornment={isPincodeLoading ? <LoadingIcon /> : null}
onBlur={onBlur}
/>
)}
/>
{errors?.[name]?.message && (
<div className={styles.errorContainer}>
<ErrorIconOutline />
<Typography variant="p4" className={styles.errorText}>
{errors?.[name]?.message}
</Typography>
</div>
)}
</>
);
};
export default AddressFormTextInput;

View File

@@ -1,4 +1,4 @@
import { Button, Typography } from '@navi/web-ui/lib/primitives';
import { Typography } from '@navi/web-ui/lib/primitives';
import { useEffect, useMemo, useState } from 'react';
import styles from './index.module.scss';
import AddressCard from './AddressCard';
@@ -8,12 +8,15 @@ import { RootState } from 'src/store';
import { fetchAddresses } from '../../actions/addressesActions';
import { createKey } from 'src/utils/CaseDetail.utils';
import { IAddress } from './interfaces';
import { ArrowDownIcon, ArrowUpIcon } from '@navi/web-ui/lib/icons';
import { AddIcon, ArrowDownIcon, ArrowUpIcon } from '@navi/web-ui/lib/icons';
import SuspenseLoader from 'src/components/SkeletonLoader/SuspenseLoader';
import LineLoader from 'src/components/SkeletonLoader/LineLoader';
import cx from 'classnames';
import { addClickstreamEvent } from '../../../../service/clickStreamEventService';
import { CLICKSTREAM_EVENT_NAMES } from '../../../../service/clickStream.constant';
import Button from '@primitives/Button';
import Modal from '@cp/src/components/Modal/Modal';
import AddNewAddressForm from './AddNewAddressForm';
const AddressesTab = () => {
const {
@@ -28,7 +31,8 @@ const AddressesTab = () => {
);
const { data: addresses = [], loading: addressesLoading } = pageData?.addresses || {};
const [isOtherExpanded, setIsOtherExpanded] = useState(false);
const [isModalOpen, setIsModalOpen] = useState(false);
const showAddNewAddress = useSelector((state: RootState) => state.common.isCiuAgent);
useEffect(() => {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.LH_Addresses_PageLand);
}, []);
@@ -66,6 +70,9 @@ const AddressesTab = () => {
setIsOtherExpanded(!isOtherExpanded);
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.LH_Addresses_ViewOtherCTA);
};
const handleAddAddressModal = () => {
setIsModalOpen(!isModalOpen);
};
return (
<div className={cx(styles.addresses, styles.tabContentScrollAddress)}>
@@ -86,6 +93,17 @@ const AddressesTab = () => {
</div>
}
>
{showAddNewAddress ? (
<div className="flex justify-end items-center pt-3 px-6">
<Button
variant="text"
onClick={handleAddAddressModal}
startAdornment={<AddIcon color="var(--blue-base)" />}
>
Add new address
</Button>
</div>
) : null}
{addresses.length === 0 ? (
<div className={'flex justify-center items-center mt-[25%]'}>
<Typography variant="h4">No addresses found</Typography>
@@ -130,6 +148,14 @@ const AddressesTab = () => {
) : null}
</>
)}
<Modal
onClose={handleAddAddressModal}
visible={isModalOpen}
header={'Add new address'}
headerClassName="w-[380px] !py-6 !pr-3"
>
<AddNewAddressForm loanId={loanId} handleAddAddressModal={handleAddAddressModal} />
</Modal>
</SuspenseLoader>
</div>
);

View File

@@ -35,3 +35,17 @@ export const MAX_FEEDBACK_STATUS_CHAR_LENGTH = 28;
export enum AddressSource {
SKIP_TRACING = 'SKIP_TRACING'
}
export interface AddAddressFormValues {
lineOne: string;
lineTwo: string;
pinCode: string;
city: string;
}
export const defaultValues = {
lineOne: '',
lineTwo: '',
pinCode: '',
city: ''
};

View File

@@ -114,3 +114,15 @@
height: calc(100vh - 225px);
overflow: hidden;
}
.errorContainer {
display: flex;
align-items: center;
padding-top: 1px;
.errorText {
margin-left: 5px;
color: var(--navi-color-red-base);
margin-top: 1px;
}
}

View File

@@ -1,3 +1,6 @@
import { Control, FieldErrorsImpl, RegisterOptions } from 'react-hook-form';
import { AddAddressFormValues } from './contants';
export interface IAddress {
referenceId: string;
addressReferenceId: string;
@@ -44,3 +47,29 @@ export interface ICoordinates {
latitude: number;
longitude: number;
}
export interface IAddAddressFormProps {
loanId: string;
caseBusinessVertical: string;
caseId: string;
}
export interface IAddressFormTextInput {
control: Control<AddAddressFormValues, any>;
name: keyof AddAddressFormValues;
rules?: RegisterOptions;
errors: Partial<FieldErrorsImpl<AddAddressFormValues>>;
isEditDisabled?: boolean;
inputLabel: string;
placeholder: string;
validationMessage: string;
showRequiredIcon?: boolean;
containerClass?: string;
handlePincodeChange?: (...event: any[]) => void;
isPincodeLoading?: boolean;
}
export interface IAddNewAddressFormProps {
loanId: string;
handleAddAddressModal: () => void;
}

View File

@@ -42,6 +42,12 @@ export const ToastMessage = {
EMAIL_FORMAT: 'Please enter valid email address'
};
export enum VALIDATION_MESSAGE {
ADDRESS_LENGTH_VALIDATION_MESSAGE = 'Please enter at least 5 characters (a-z & 0-9)',
ADDRESS_VALIDATION_MESSAGE = 'Please enter a valid address',
PINCODE_VALIDATION_MESSAGE = 'Please enter a valid pincode'
}
export const CASE_DETAILS_TABS_FEATURE_FLAG = {
SKIP_TRACING: 'skipTracing'
} as const;

View File

@@ -1,7 +1,11 @@
import { ICellRendererParams } from 'ag-grid-community/dist/lib/rendering/cellRenderers/iCellRenderer';
import { AnswerView } from '../interfaces/CommunicationHistory.type';
import { InteractionStatuses } from './communicationHistory.constant';
import { TERMINAL_INTERACTION_SORT_ORDER } from '@cp/pages/CaseDetails/constants/index';
import {
TERMINAL_INTERACTION_SORT_ORDER,
VALIDATION_MESSAGE
} from '@cp/pages/CaseDetails/constants/index';
import { getPinCodeDetails } from '../actions/addressesActions';
export const feedbackHavingInteractionStatus = (params: ICellRendererParams) => {
const interactionStatusAnswers = params.data?.answerViews?.filter((answer: AnswerView) =>
@@ -43,3 +47,21 @@ export const feedbackHavingInteractionStatus = (params: ICellRendererParams) =>
return undefined;
};
export const getValidPincodeLength = 6;
export const minPincodeValue = 100000;
export const checkValidPinCode = (pinCode: string) => {
return (
pinCode?.length === getValidPincodeLength &&
!isNaN(Number(pinCode)) &&
Number(pinCode) >= minPincodeValue
);
};
export const validatePincode = async (value: string) => {
if (checkValidPinCode(value)) {
const isValidPincode = await getPinCodeDetails(value);
if (isValidPincode) return true;
}
return VALIDATION_MESSAGE.PINCODE_VALIDATION_MESSAGE;
};

View File

@@ -312,7 +312,9 @@ export enum ApiKeys {
DOWNLOAD_NDRA_FAILURE_FILE,
PAUSED_CASES,
UPLOAD_MEDIA,
CUSTOMER_CONFIDENCE
CUSTOMER_CONFIDENCE,
GET_PIN_CODES_DETAILS,
ADD_NEW_ADDRESS
}
// TODO: try to get rid of `as`
@@ -620,7 +622,8 @@ API_URLS[ApiKeys.DOWNLOAD_NDRA_FAILURE_FILE] = '/uploads/v2/{referenceId}/failur
API_URLS[ApiKeys.PAUSED_CASES] = '/allocated-cases/paused';
API_URLS[ApiKeys.UPLOAD_MEDIA] = 'v2/interactions/upload/media';
API_URLS[ApiKeys.CUSTOMER_CONFIDENCE] = '/telephone-configurations';
API_URLS[ApiKeys.GET_PIN_CODES_DETAILS] = '/v1/pincodes/{pinCode}';
API_URLS[ApiKeys.ADD_NEW_ADDRESS] = '/address';
// TODO: try to get rid of `as`
const MOCK_API_URLS: Record<ApiKeys, string> = {} as Record<ApiKeys, string>;
MOCK_API_URLS[ApiKeys.PEOPLE] = 'people.json';