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:
committed by
GitHub
parent
a6fdd0537d
commit
3ea8e88969
@@ -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);
|
||||
});
|
||||
};
|
||||
|
||||
168
src/pages/CaseDetails/components/Addresses/AddNewAddressForm.tsx
Normal file
168
src/pages/CaseDetails/components/Addresses/AddNewAddressForm.tsx
Normal 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;
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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: ''
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user