From 3ea8e889694794dddb6bd2adaf8a6fc788f1f552 Mon Sep 17 00:00:00 2001 From: Aishwarya Srivastava Date: Fri, 21 Feb 2025 19:57:53 +0530 Subject: [PATCH] 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 --- .../CaseDetails/actions/addressesActions.ts | 45 ++++- .../Addresses/AddNewAddressForm.tsx | 168 ++++++++++++++++++ .../Addresses/AddressFormTextInput.tsx | 72 ++++++++ .../components/Addresses/AddressesTab.tsx | 32 +++- .../components/Addresses/contants.ts | 14 ++ .../components/Addresses/index.module.scss | 12 ++ .../components/Addresses/interfaces.ts | 29 +++ src/pages/CaseDetails/constants/index.tsx | 6 + src/pages/CaseDetails/constants/utils.tsx | 24 ++- src/utils/ApiHelper.ts | 7 +- 10 files changed, 402 insertions(+), 7 deletions(-) create mode 100644 src/pages/CaseDetails/components/Addresses/AddNewAddressForm.tsx create mode 100644 src/pages/CaseDetails/components/Addresses/AddressFormTextInput.tsx diff --git a/src/pages/CaseDetails/actions/addressesActions.ts b/src/pages/CaseDetails/actions/addressesActions.ts index ecf9de85..8099c4f2 100644 --- a/src/pages/CaseDetails/actions/addressesActions.ts +++ b/src/pages/CaseDetails/actions/addressesActions.ts @@ -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>, + 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); + }); + }; diff --git a/src/pages/CaseDetails/components/Addresses/AddNewAddressForm.tsx b/src/pages/CaseDetails/components/Addresses/AddNewAddressForm.tsx new file mode 100644 index 00000000..a42cf373 --- /dev/null +++ b/src/pages/CaseDetails/components/Addresses/AddNewAddressForm.tsx @@ -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({ + 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 ( +
+
+
+ + + { + 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} + /> + +
+
+
+ +
+
+ ); +}; + +export default AddNewAddressForm; diff --git a/src/pages/CaseDetails/components/Addresses/AddressFormTextInput.tsx b/src/pages/CaseDetails/components/Addresses/AddressFormTextInput.tsx new file mode 100644 index 00000000..94e7d7ca --- /dev/null +++ b/src/pages/CaseDetails/components/Addresses/AddressFormTextInput.tsx @@ -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, + onChange: (...event: any[]) => void + ) => { + const value = e.target.value; + if (name === 'pinCode' && isFunction(handlePincodeChange)) { + handlePincodeChange(value, onChange); + } else { + onChange(value); + } + }; + return ( + <> +
+ + {inputLabel} + {showRequiredIcon ? : null} + +
+ ( + handleValueChange(e, onChange)} + value={value} + disabled={isEditDisabled} + RightInputAdornment={isPincodeLoading ? : null} + onBlur={onBlur} + /> + )} + /> + {errors?.[name]?.message && ( +
+ + + {errors?.[name]?.message} + +
+ )} + + ); +}; + +export default AddressFormTextInput; diff --git a/src/pages/CaseDetails/components/Addresses/AddressesTab.tsx b/src/pages/CaseDetails/components/Addresses/AddressesTab.tsx index 94c39053..6ec7ee9a 100644 --- a/src/pages/CaseDetails/components/Addresses/AddressesTab.tsx +++ b/src/pages/CaseDetails/components/Addresses/AddressesTab.tsx @@ -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 (
@@ -86,6 +93,17 @@ const AddressesTab = () => {
} > + {showAddNewAddress ? ( +
+ +
+ ) : null} {addresses.length === 0 ? (
No addresses found @@ -130,6 +148,14 @@ const AddressesTab = () => { ) : null} )} + + +
); diff --git a/src/pages/CaseDetails/components/Addresses/contants.ts b/src/pages/CaseDetails/components/Addresses/contants.ts index b25ef474..a6e24838 100644 --- a/src/pages/CaseDetails/components/Addresses/contants.ts +++ b/src/pages/CaseDetails/components/Addresses/contants.ts @@ -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: '' +}; diff --git a/src/pages/CaseDetails/components/Addresses/index.module.scss b/src/pages/CaseDetails/components/Addresses/index.module.scss index edd08816..8866c78f 100644 --- a/src/pages/CaseDetails/components/Addresses/index.module.scss +++ b/src/pages/CaseDetails/components/Addresses/index.module.scss @@ -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; + } +} diff --git a/src/pages/CaseDetails/components/Addresses/interfaces.ts b/src/pages/CaseDetails/components/Addresses/interfaces.ts index 2377ef02..36dd1b7d 100644 --- a/src/pages/CaseDetails/components/Addresses/interfaces.ts +++ b/src/pages/CaseDetails/components/Addresses/interfaces.ts @@ -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; + name: keyof AddAddressFormValues; + rules?: RegisterOptions; + errors: Partial>; + 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; +} diff --git a/src/pages/CaseDetails/constants/index.tsx b/src/pages/CaseDetails/constants/index.tsx index 0ccfb52a..ab606907 100644 --- a/src/pages/CaseDetails/constants/index.tsx +++ b/src/pages/CaseDetails/constants/index.tsx @@ -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; diff --git a/src/pages/CaseDetails/constants/utils.tsx b/src/pages/CaseDetails/constants/utils.tsx index 0aa4f2d5..abeea562 100644 --- a/src/pages/CaseDetails/constants/utils.tsx +++ b/src/pages/CaseDetails/constants/utils.tsx @@ -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; +}; diff --git a/src/utils/ApiHelper.ts b/src/utils/ApiHelper.ts index a575f2fa..b874ae90 100644 --- a/src/utils/ApiHelper.ts +++ b/src/utils/ApiHelper.ts @@ -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 = {} as Record; MOCK_API_URLS[ApiKeys.PEOPLE] = 'people.json';