From bf69705ee6e357ef28a4c96453be51f7ed024d56 Mon Sep 17 00:00:00 2001 From: Ashish Deo Date: Wed, 25 Sep 2024 17:30:15 +0530 Subject: [PATCH] TP-77180 | Self serve UI for pausing collections (#1119) * TP-77180 | Self serve UI for pausing collections * TP-77180 | Self serve UI for pausing collections * TP-77180 | Self serve UI for pausing collections * TP-77180 | Self serve UI for pausing collections * TP-77180 | Self serve UI for pausing collections * TP-77180 | Hover button color change fix * TP-77180 | Hover button color change fix * TP-77180 | Hover button color change fix * TP-77180 | UAT FIxes * TP-77180 | UAT FIxes 1 * TP-77180 | UAT FIxes 1 * TP-77180 | UAT FIxes 2 * TP-77180 | ready for PR review * TP-77180 | prettier * TP-77180 | Merge to master fix * TP-77180 | Merge to master fix * TP-77180 | lint fix * TP-77180 | self serve UI ready for PR review * TP-77180 | self serve UI ready for PR review * TP-77180 | pr review fix v1 * TP-77180 | pr review fix v2 * TP-77180 | added feature flag check * TP-77180 | added feature flag check * TP-77180 | PR review fixes * TP-77180 | submodule update --- src/assets/icons/PauseIcon.tsx | 23 ++ src/assets/images/icons/InfoNotification.tsx | 27 +++ .../DateTimePickerComponent.tsx | 20 +- src/components/DateTimePicker/Footer.tsx | 17 ++ src/components/DateTimePicker/Heading.tsx | 17 ++ .../DateTimePicker/index.module.scss | 5 + .../Notifications/NotificationList.tsx | 3 +- src/components/Notifications/constants.tsx | 16 +- src/pages/AllAgents/AgentForm/AgentForm.tsx | 1 - .../CaseDetails/components/Pause/action.ts | 46 ++++ .../CaseDetails/components/Pause/const.ts | 51 +++++ .../components/Pause/index.module.scss | 67 ++++++ .../CaseDetails/components/Pause/index.tsx | 210 ++++++++++++++++++ .../components/RightSidebar/index.tsx | 14 +- .../constants/RightSidebar.constant.ts | 10 +- .../communicationHistory.constant.tsx | 6 + .../feedbackForm/index.module.scss | 1 - src/pages/CaseDetails/feedbackForm/index.tsx | 2 - src/pages/auth/constants/AuthConstants.ts | 3 +- src/utils/ApiHelper.ts | 6 +- web-ui-library | 2 +- 21 files changed, 522 insertions(+), 25 deletions(-) create mode 100644 src/assets/icons/PauseIcon.tsx create mode 100644 src/assets/images/icons/InfoNotification.tsx create mode 100644 src/components/DateTimePicker/Footer.tsx create mode 100644 src/components/DateTimePicker/Heading.tsx create mode 100644 src/pages/CaseDetails/components/Pause/action.ts create mode 100644 src/pages/CaseDetails/components/Pause/const.ts create mode 100644 src/pages/CaseDetails/components/Pause/index.module.scss create mode 100644 src/pages/CaseDetails/components/Pause/index.tsx diff --git a/src/assets/icons/PauseIcon.tsx b/src/assets/icons/PauseIcon.tsx new file mode 100644 index 00000000..eb776d1c --- /dev/null +++ b/src/assets/icons/PauseIcon.tsx @@ -0,0 +1,23 @@ +import { IconProps } from './types'; + +const PauseIcon = (props: IconProps) => { + const { width = '21', height = '20' } = props; + return ( + + + + ); +}; + +export default PauseIcon; diff --git a/src/assets/images/icons/InfoNotification.tsx b/src/assets/images/icons/InfoNotification.tsx new file mode 100644 index 00000000..cd3ab008 --- /dev/null +++ b/src/assets/images/icons/InfoNotification.tsx @@ -0,0 +1,27 @@ +import { IconProps } from '../../icons/types'; + +const InfoNotification = (props: IconProps) => { + const { width = 36, height = 36 } = props; + return ( + + + + + + + + + + ); +}; + +export default InfoNotification; diff --git a/src/components/DateTimePicker/DateTimePickerComponent.tsx b/src/components/DateTimePicker/DateTimePickerComponent.tsx index fc2f124f..fda124fb 100644 --- a/src/components/DateTimePicker/DateTimePickerComponent.tsx +++ b/src/components/DateTimePicker/DateTimePickerComponent.tsx @@ -1,13 +1,14 @@ -import React, { useEffect, useMemo, useRef, useState } from 'react'; -import styles from './index.module.scss'; +import React, { useEffect, useRef, useState } from 'react'; import { CalendarIcon } from '@navi/web-ui/lib/icons'; import { Typography } from '@navi/web-ui/lib/primitives'; -import { DATE_TIME_TYPE, DateTimeFormat } from './constants'; -import cx from 'classnames'; -import dayjs from 'dayjs'; +import { DATE_TIME_TYPE } from './constants'; import useOutsideClick from '@hooks/useOutsideClick'; import ClockIcon from '@icons/ClockIcon'; import DateTimePickerLabel from '@cp/components/DateTimePicker/DateTimePickerLabel'; +import styles from './index.module.scss'; +import cx from 'classnames'; +import FooterContainer from './Footer'; +import HeadingContainer from './Heading'; interface DatePickerProps extends React.InputHTMLAttributes { heading?: string; @@ -18,6 +19,7 @@ interface DatePickerProps extends React.InputHTMLAttributes { inputClasses?: string; iconColor?: string; onChange: (event: React.ChangeEvent) => void; + footer?: string; } const DateTimePickerComponent: React.FC = ({ @@ -31,6 +33,7 @@ const DateTimePickerComponent: React.FC = ({ iconColor, onChange, disabled = false, + footer, ...restProps }) => { const [isOpen, setIsOpen] = useState(false); @@ -79,11 +82,7 @@ const DateTimePickerComponent: React.FC = ({ className={cx(styles.datePickerInput, className, { [styles.disabled]: disabled })} ref={datePickerContainerRef} > - {heading ? ( - - {heading} - - ) : null} + {heading ? : null}
= ({ )}
+ {footer ? : null} ); }; diff --git a/src/components/DateTimePicker/Footer.tsx b/src/components/DateTimePicker/Footer.tsx new file mode 100644 index 00000000..0d4931d7 --- /dev/null +++ b/src/components/DateTimePicker/Footer.tsx @@ -0,0 +1,17 @@ +import { Typography } from '@navi/web-ui/lib/primitives'; +import styles from './index.module.scss'; + +type FooterContainerProps = { + footer: string; +}; + +const FooterContainer = (props: FooterContainerProps) => { + const { footer } = props; + return ( + + {footer} + + ); +}; + +export default FooterContainer; diff --git a/src/components/DateTimePicker/Heading.tsx b/src/components/DateTimePicker/Heading.tsx new file mode 100644 index 00000000..9d06123d --- /dev/null +++ b/src/components/DateTimePicker/Heading.tsx @@ -0,0 +1,17 @@ +import { Typography } from '@navi/web-ui/lib/primitives'; +import styles from './index.module.scss'; + +type HeadingContainerProps = { + heading: string; +}; + +const HeadingContainer = (props: HeadingContainerProps) => { + const { heading } = props; + return ( + + {heading} + + ); +}; + +export default HeadingContainer; diff --git a/src/components/DateTimePicker/index.module.scss b/src/components/DateTimePicker/index.module.scss index c626047a..80c68565 100644 --- a/src/components/DateTimePicker/index.module.scss +++ b/src/components/DateTimePicker/index.module.scss @@ -49,4 +49,9 @@ } } } + + .footerText { + margin-top: 8px; + color: var(--navi-color-gray-c3); + } } diff --git a/src/components/Notifications/NotificationList.tsx b/src/components/Notifications/NotificationList.tsx index c9bc2d8d..6b5252f1 100644 --- a/src/components/Notifications/NotificationList.tsx +++ b/src/components/Notifications/NotificationList.tsx @@ -48,7 +48,8 @@ const NotificationList: React.FC = ({ if ( [ TemplateTypes.CASE_ALLOCATED_NOTIFICATION_TEMPLATE, - TemplateTypes.CASE_DE_ALLOCATED_NOTIFICATION_TEMPLATE + TemplateTypes.CASE_DE_ALLOCATED_NOTIFICATION_TEMPLATE, + TemplateTypes.COLLECTION_PAUSE_EFFORTS_CASE_DEALLOCATED ].includes(templateName) ) { navigate(APP_ROUTES.CASES.path); diff --git a/src/components/Notifications/constants.tsx b/src/components/Notifications/constants.tsx index e15c6960..3d4f3d61 100644 --- a/src/components/Notifications/constants.tsx +++ b/src/components/Notifications/constants.tsx @@ -13,6 +13,7 @@ import AmeyoCallNotPickedIcon from 'src/assets/images/icons/AmyoCallNotPickedIco import RaiseRequestIcon from '@cp/src/assets/icons/RaiseRequestIcon'; import ReceiveRequestIcon from '@cp/src/assets/icons/ReceiveRequestIcon'; import FeeReappliedIcon from '@cp/src/assets/icons/FeeReappliedIcon'; +import InfoNotification from '@cp/src/assets/images/icons/InfoNotification'; export enum NotificationTypes { LONGHORN_NOTIFICATION = 'LONGHORN_NOTIFICATION', @@ -59,7 +60,8 @@ export const TemplateTypes = { 'REVISIT_GEN_AI_BOT_SCHEDULED_NOTIFICATION_TEMPLATE', SUPPORT_REQUEST_RECEIVED: 'SUPPORT_REQUEST_RECEIVED', SUPPORT_REQUEST_RESOLVED: 'SUPPORT_REQUEST_RESOLVED', - MESSAGE_WAIVE_UNHOLD_NOTIFICATION_TEMPLATE: 'MESSAGE_WAIVE_UNHOLD_NOTIFICATION_TEMPLATE' + MESSAGE_WAIVE_UNHOLD_NOTIFICATION_TEMPLATE: 'MESSAGE_WAIVE_UNHOLD_NOTIFICATION_TEMPLATE', + COLLECTION_PAUSE_EFFORTS_CASE_DEALLOCATED: 'COLLECTION_PAUSE_EFFORTS_CASE_DEALLOCATED' }; export const PaymentSuccessTemplateTypes = [ @@ -108,6 +110,7 @@ export const NotificationTabTemplateMap = { TemplateTypes.LONGHORN_ALERTS_NEW_ENQUIRY, TemplateTypes.CASE_ALLOCATED_NOTIFICATION_TEMPLATE, TemplateTypes.CASE_DE_ALLOCATED_NOTIFICATION_TEMPLATE, + TemplateTypes.COLLECTION_PAUSE_EFFORTS_CASE_DEALLOCATED, TemplateTypes.BUSY_SCHEDULED_NOTIFICATION_TEMPLATE, TemplateTypes.REVISIT_SCHEDULED_NOTIFICATION_TEMPLATE, TemplateTypes.FIELD_BOT_REVISIT_SCHEDULED_NOTIFICATION_TEMPLATE, @@ -128,6 +131,7 @@ export const NotificationTabTemplateMap = { TemplateTypes.LONGHORN_ALERTS_NEW_PHONE_NUMBER_ADDED, TemplateTypes.CASE_ALLOCATED_NOTIFICATION_TEMPLATE, TemplateTypes.CASE_DE_ALLOCATED_NOTIFICATION_TEMPLATE, + TemplateTypes.COLLECTION_PAUSE_EFFORTS_CASE_DEALLOCATED, TemplateTypes.PAYMENT_FAILED_TEMPLATE_V2, TemplateTypes.PAYMENT_MADE_TEMPLATE_V2, TemplateTypes.CUSTOMER_CALLED_TEMPLATE_V2, @@ -215,7 +219,7 @@ export const TemplateInfoMap = { icon: }, [TemplateTypes.LONGHORN_ALERTS_CHANGE_IN_DELIQUENCY_ACCOUNTS]: { - label: 'Change in deliquency accounts', + label: 'Change in delinquency accounts', icon: }, [TemplateTypes.LONGHORN_ALERTS_NEW_PHONE_NUMBER_ADDED]: { @@ -231,7 +235,7 @@ export const TemplateInfoMap = { icon: }, [TemplateTypes.LONGHORN_ALERTS_CHANGE_IN_DELIQUENCY]: { - label: 'Change in deliquency', + label: 'Change in delinquency', icon: }, [TemplateTypes.CASE_ALLOCATED_NOTIFICATION_TEMPLATE]: { @@ -239,9 +243,13 @@ export const TemplateInfoMap = { icon: }, [TemplateTypes.CASE_DE_ALLOCATED_NOTIFICATION_TEMPLATE]: { - label: 'Case Deallocated', + label: 'Case De-allocated', icon: }, + [TemplateTypes.COLLECTION_PAUSE_EFFORTS_CASE_DEALLOCATED]: { + label: 'Case De-allocated', + icon: + }, [TemplateTypes.AMEYO_CALL_DROP]: { label: 'Ameyo Call Drop', icon: diff --git a/src/pages/AllAgents/AgentForm/AgentForm.tsx b/src/pages/AllAgents/AgentForm/AgentForm.tsx index 5e68e5e0..e6979ec4 100644 --- a/src/pages/AllAgents/AgentForm/AgentForm.tsx +++ b/src/pages/AllAgents/AgentForm/AgentForm.tsx @@ -46,7 +46,6 @@ import InactiveTextContainer from './components/form/InactiveTextContainer'; import styles from './AgentForm.module.scss'; import cx from 'classnames'; import { Typography } from '@navi/web-ui/lib/primitives'; -import UploadComponent from '@cp/components/UploadComponent/UploadComponent'; import AgentFormUploadControl from '@cp/pages/AllAgents/AgentForm/components/form/AgentFormUploadControl'; import AgentFormDownloadDraCertificate from '@cp/pages/AllAgents/AgentForm/components/form/AgentFormDownloadDraCertificate'; diff --git a/src/pages/CaseDetails/components/Pause/action.ts b/src/pages/CaseDetails/components/Pause/action.ts new file mode 100644 index 00000000..14f8f413 --- /dev/null +++ b/src/pages/CaseDetails/components/Pause/action.ts @@ -0,0 +1,46 @@ +import axiosInstance, { ApiKeys, getApiUrl, logError } from '@cp/src/utils/ApiHelper'; +import { toast } from '@navi/web-ui/lib/primitives/Toast'; +import { PAUSE_ACTION_MESSAGE } from './const'; +import { isFunction } from '@cp/src/utils/commonUtils'; + +export const getLanPauseStatus = async (caseId: string) => { + try { + const url = getApiUrl(ApiKeys.GET_PAUSE_STATUS, { caseId }); + const response = await axiosInstance.get(url); + return response?.data; + } catch (error) { + return logError(error); + } +}; + +interface PauseActionInput { + caseId: string; + payload: any; + setLoading: (loading: boolean) => void; + successCallback: () => void; +} + +export const submitPauseAction = ({ + caseId, + payload, + setLoading, + successCallback +}: PauseActionInput) => { + const url = getApiUrl(ApiKeys.PAUSE_ACTION, { caseId }); + + if (isFunction(setLoading)) setLoading(true); + + axiosInstance + .post(url, payload) + .then(() => { + toast.success(PAUSE_ACTION_MESSAGE.COLLECTIONS_PAUSED); + successCallback(); + }) + .catch(error => { + toast.error(PAUSE_ACTION_MESSAGE.FAILED_TO_PAUSE); + logError(error); + }) + .finally(() => { + isFunction(setLoading) && setLoading(false); + }); +}; diff --git a/src/pages/CaseDetails/components/Pause/const.ts b/src/pages/CaseDetails/components/Pause/const.ts new file mode 100644 index 00000000..a74090f5 --- /dev/null +++ b/src/pages/CaseDetails/components/Pause/const.ts @@ -0,0 +1,51 @@ +export enum PauseType { + RBI_ESCALATION = 'RBI_ESCALATION', + CUSTOMER_GOODWILL = 'CUSTOMER_GOODWILL', + OTHERS = 'OTHERS' +} + +export const PAUSE_REASON_TYPES = [ + { + value: PauseType.RBI_ESCALATION, + label: 'RBI Escalation' + }, + { + value: PauseType.CUSTOMER_GOODWILL, + label: 'Customer Goodwill' + }, + { + value: PauseType.OTHERS, + label: 'Others' + } +]; + +export interface PauseFormValues { + pauseDate: string; + reason: PauseType | null; + details?: string; +} + +export const defaultValues = { + pauseDate: '', + reason: null, + details: '' +}; + +export enum PAUSE_MESSAGE { + UPDATE_PAUSE = 'Update pause on this LAN', + PAUSE_COLLECTIONS = 'Pause collections for this LAN', + UPDATE_PAUSE_DAYS = 'Update pause days', + PAUSE_TILL = 'Pause till', + PAUSE_IMMEDIATE_TODAY = 'The pause will be immediate from today', + DATE_ERROR = 'Please enter some other date', + REASON_UPDATE = 'Reason for update', + REASON_PAUSE = 'Reason for pause', + OTHER_REASONS = 'Other reasons', + UPDATE_PAUSE_DATE = 'Update pause date', + PAUSE_COLLECTIONS_EFFORTS = 'Pause collections efforts' +} + +export enum PAUSE_ACTION_MESSAGE { + FAILED_TO_PAUSE = 'Failed to pause collections', + COLLECTIONS_PAUSED = 'Collections paused successfully' +} diff --git a/src/pages/CaseDetails/components/Pause/index.module.scss b/src/pages/CaseDetails/components/Pause/index.module.scss new file mode 100644 index 00000000..48dece03 --- /dev/null +++ b/src/pages/CaseDetails/components/Pause/index.module.scss @@ -0,0 +1,67 @@ +.container { + width: 100%; + width: 100%; + display: flex; + height: 100%; + flex-direction: column; + justify-content: space-between; + + .feedback { + overflow-y: scroll; + box-sizing: border-box; + width: 100%; + padding: 0 16px 16px 16px; + flex: 1; + + .pauseDatePicker { + margin-top: 8px; + width: 100% !important; + } + + .pauseDatePickerInput { + border-radius: 8px; + padding-left: 12px; + padding-right: 12px; + font-size: 16px; + color: var(--navi-bordered-input-text-color); + } + + .textAreaContainer { + min-width: auto; + overflow: hidden; + overflow-y: auto; + } + + .questions { + display: flex; + justify-content: flex-start; + flex-wrap: wrap; + align-self: flex-start; + margin-bottom: 12px; + + .chip { + padding: 4px 12px; + font-size: 16px; + margin-top: 5px; + margin-bottom: 5px; + margin-right: 10px; + } + + .chipActiveColor { + background-color: var(--blue-bg); + border: 1px solid var(--blue-border); + + .text { + color: var(--blue-base); + } + } + } + } + + .submitButtonSection { + width: -webkit-fill-available; + padding: 16px 16px; + border-top: 1px solid var(--navi-color-gray-border); + height: fit-content; + } +} diff --git a/src/pages/CaseDetails/components/Pause/index.tsx b/src/pages/CaseDetails/components/Pause/index.tsx new file mode 100644 index 00000000..edacf356 --- /dev/null +++ b/src/pages/CaseDetails/components/Pause/index.tsx @@ -0,0 +1,210 @@ +import React, { useEffect, useState } from 'react'; +import { Button, Chip, TextArea, Typography } from '@navi/web-ui/lib/primitives'; +import DateTimePickerComponent from '@cp/src/components/DateTimePicker/DateTimePickerComponent'; +import { DATE_TIME_TYPE } from '@cp/src/components/DateTimePicker/constants'; +import { DateFormat, getFormatDate, getFormattedDateWithFullYear } from '@cp/src/utils/DateHelper'; +import { defaultValues, PAUSE_MESSAGE, PAUSE_REASON_TYPES, PauseFormValues } from './const'; +import InfoFilledIcon from '@cp/src/assets/icons/InfoFilledIcon'; +import { getLanPauseStatus, submitPauseAction } from './action'; +import { Controller, useForm } from 'react-hook-form'; +import { useParams } from 'react-router-dom'; +import { useSelector } from 'react-redux'; +import { RootState } from '@cp/src/store'; +import { createKey } from '@cp/src/utils/CaseDetail.utils'; +import { logError } from '@cp/src/utils/ApiHelper'; +import PauseIcon from '@cp/src/assets/icons/PauseIcon'; +import styles from './index.module.scss'; +import cx from 'classnames'; + +const PauseAction = () => { + const todayDate = getFormatDate(new Date(), DateFormat.YYYY_MM_DD); + const threeMonthsLaterDate = getFormatDate( + new Date(new Date().setMonth(new Date().getMonth() + 3)), + DateFormat.YYYY_MM_DD + ); + + const [isSelectedActionUpdatePause, setIsSelectedActionUpdatePause] = useState(false); + const [pausedDate, setPausedDate] = useState(''); + const [isSubmitting, setIsSubmitting] = useState(false); + + const { loanId = '', customerId = '' } = useParams(); + const { pageDetail } = useSelector((state: RootState) => ({ + pageDetail: state.caseDetail.pageData?.[createKey(loanId, customerId)] + })); + const caseReferenceId = pageDetail?.details?.data?.caseReferenceId || ''; + + const handleSubmitState = (state: boolean) => { + setIsSubmitting(state); + }; + + const { + register, + handleSubmit, + reset, + watch, + formState: { errors }, + control + } = useForm({ + defaultValues + }); + + const fetchPauseStatus = async () => { + try { + const getPauseStatusData = await getLanPauseStatus(caseReferenceId); + setIsSelectedActionUpdatePause(getPauseStatusData?.isPaused); + setPausedDate(getPauseStatusData?.pauseTillDate); + } catch (error) { + logError(error, 'Error while fetching pause status'); + } + }; + + useEffect(() => { + if (!caseReferenceId) return; + fetchPauseStatus(); + }, [caseReferenceId]); + + const handleSubmitForm = (data: PauseFormValues) => { + const { pauseDate, reason } = data; + const payload = { + pauseEffortsTillDate: pauseDate, + pauseEffortsReason: reason === 'OTHERS' ? data.details : reason + }; + + submitPauseAction({ + caseId: caseReferenceId, + payload: payload, + setLoading: handleSubmitState, + successCallback: fetchPauseStatus + }); + reset(defaultValues); + }; + + const isFormDisabled = + !caseReferenceId.length || + isSubmitting || + !watch('pauseDate') || + !watch('reason') || + (watch('reason') === 'OTHERS' && !watch('details')); + + return ( +
+
+
+ + {isSelectedActionUpdatePause + ? PAUSE_MESSAGE.UPDATE_PAUSE + : PAUSE_MESSAGE.PAUSE_COLLECTIONS} + +
+ + {isSelectedActionUpdatePause ? ( +
+ + + + Collections efforts have been paused till{' '} + + {pausedDate ? getFormattedDateWithFullYear(new Date(pausedDate)) : ''} + + +
+ ) : null} + + + !(isSelectedActionUpdatePause && value === pausedDate) || PAUSE_MESSAGE.DATE_ERROR + }} + render={({ field: { onChange, value } }) => ( + + )} + /> + + {errors?.pauseDate && ( + + {errors?.pauseDate?.message} + + )} + +
+ + {isSelectedActionUpdatePause ? PAUSE_MESSAGE.REASON_UPDATE : PAUSE_MESSAGE.REASON_PAUSE} + +
+ + ( +
+ {PAUSE_REASON_TYPES.map(pauseType => ( + onChange(pauseType.value)} + /> + ))} +
+ )} + /> + + {watch('reason') === 'OTHERS' ? ( +