diff --git a/src/App.tsx b/src/App.tsx index 2216976d..bf7fe24d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -211,12 +211,12 @@ const App = () => { registerNavigateAndDispatch(navigate, dispatch); const { config } = window; - initApm({ - serviceName: config.APM_APP_NAME, - serverUrl: config.APM_BASE_URL, - serviceVersion: '1.0.0', - environment: config.ENV - }); + // initApm({ + // serviceName: config.APM_APP_NAME, + // serverUrl: config.APM_BASE_URL, + // serviceVersion: '1.0.0', + // environment: config.ENV + // }); const setFCMTokenRedux = (fcmToken: string) => { dispatch(setFcmToken(fcmToken)); }; diff --git a/src/assets/icons/ErrorIconOutline.tsx b/src/assets/icons/ErrorIconOutline.tsx index 04775044..a8950824 100644 --- a/src/assets/icons/ErrorIconOutline.tsx +++ b/src/assets/icons/ErrorIconOutline.tsx @@ -5,19 +5,17 @@ const ErrorIconOutline: React.FC = props => { const { className, size = 16, fillColor = '#E92C2C', onClick } = props; return ( ); diff --git a/src/assets/images/icons/DownloadIcon.tsx b/src/assets/images/icons/DownloadIcon.tsx new file mode 100644 index 00000000..a3c36b6a --- /dev/null +++ b/src/assets/images/icons/DownloadIcon.tsx @@ -0,0 +1,29 @@ +import React from 'react'; + +type DownloadIconProps = { + width?: number; + height?: number; +}; +function DownloadIcon({ width = 20, height = 20 }: DownloadIconProps) { + return ( + + + + + + + + + ); +} + +export default DownloadIcon; diff --git a/src/assets/images/icons/FileIcon.tsx b/src/assets/images/icons/FileIcon.tsx new file mode 100644 index 00000000..d2d9f4a5 --- /dev/null +++ b/src/assets/images/icons/FileIcon.tsx @@ -0,0 +1,28 @@ +import React from 'react'; + +type FileIconProps = { + width?: number; + height?: number; +}; +function FileIcon({ width = 20, height = 20 }: FileIconProps) { + return ( + + + + + + + + + ); +} +export default FileIcon; diff --git a/src/components/UploadComponent/UploadComponent.tsx b/src/components/UploadComponent/UploadComponent.tsx new file mode 100644 index 00000000..cdf22b42 --- /dev/null +++ b/src/components/UploadComponent/UploadComponent.tsx @@ -0,0 +1,185 @@ +import React, { useRef, useState } from 'react'; +import cx from 'classnames'; +import UploadFileIcon from '@cp/assets/icons/UploadFileIcon'; +import Button from '@primitives/Button'; +import Typography from '@primitives/Typography'; +import { noop } from '@utils/common'; +import UploadSuccessComponent from '@cp/components/UploadComponent/UploadSuccessComponent'; +import { RegisterOptions } from 'react-hook-form'; +import { useMergeRefs } from '@floating-ui/react'; + +export type ErrorMessage = { + fileName: string | null; + message: string | boolean; +}; + +type UploadComponentProps = { + isSampleFileAvailable?: boolean; + supportedFile?: string; + MAX_FILE_SIZE_IN_MB?: number; + validationOfFile?: (file: File | undefined) => boolean; + file?: File | null; + setFile: (file: File | null) => void; + setHasError: (errorMessage: ErrorMessage) => void; + isValidationInfoVisible?: boolean; + uploadContentContainer?: ( + handleUploadClick: (e: React.ChangeEvent) => void + ) => React.ReactElement; + containerClassName?: string; + register?: RegisterOptions; + refProp?: React.Ref; +}; + +function UploadComponent({ + isSampleFileAvailable = false, + MAX_FILE_SIZE_IN_MB = 5, + supportedFile = '.csv', + validationOfFile, + file: uploadedFile = null, + setFile: setUploadedFile, + setHasError = (errorMessage: ErrorMessage) => noop, + isValidationInfoVisible = false, + uploadContentContainer, + containerClassName = '', + refProp, + ...rest +}: UploadComponentProps) { + const hiddenFileInput = useRef(null); + const [isDragging, setIsDragging] = useState(false); + const mergedRef = useMergeRefs([hiddenFileInput, refProp]); + + const defaultValidationOfFile = (file: File | undefined) => { + if (file) { + if (file?.size > MAX_FILE_SIZE) { + return `File size is greater than ${MAX_FILE_SIZE_IN_MB} MB`; + } + } + return false; + }; + + const handleFile = (file: File | undefined) => { + if (file) { + setUploadedFile(file); + const validationFunc = validationOfFile || defaultValidationOfFile; + const fileValidationMessage = validationFunc(file); + if (!fileValidationMessage) { + setUploadedFile(file); + } else { + setHasError({ + fileName: file.name, + message: fileValidationMessage + }); + setUploadedFile(null); + } + } + }; + const handleChange = (event: React.ChangeEvent) => { + const selectedFile = event.target.files?.[0]; + handleFile(selectedFile); + }; + + const handleDragOver = (event: React.DragEvent) => { + event.preventDefault(); + event.stopPropagation(); + setIsDragging(true); + }; + const handleDragEnter = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragging(true); + }; + + const handleDragLeave = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragging(false); + }; + + const handleDrop = (event: React.DragEvent) => { + event.preventDefault(); + event.stopPropagation(); + setIsDragging(false); + const selectedFile = event.dataTransfer.files?.[0]; + handleFile(selectedFile); + }; + const MAX_FILE_SIZE = 1e6 * MAX_FILE_SIZE_IN_MB; + + const handleUploadClick = (e: React.ChangeEvent) => { + e.preventDefault(); + hiddenFileInput.current?.focus(); + hiddenFileInput.current?.click(); + }; + + const handleDownloadClick = (e: React.ChangeEvent) => { + e.preventDefault(); + }; + return ( +
+
+
+ ) => { + e.target.value = ''; + }} + ref={hiddenFileInput} + style={{ display: 'none' }} + /> + {uploadContentContainer ? ( + uploadContentContainer(handleUploadClick) + ) : ( +
+ +
+ {' '} + or drag and drop here +
+
+ )} + {isValidationInfoVisible ? ( +
+ + Upload only .csv + +
+ + Max 5 MB size allowed + +
+ ) : null} + {isSampleFileAvailable ? ( + + ) : null} +
+
+
+ ); +} + +export default UploadComponent; diff --git a/src/components/UploadComponent/UploadErrorComponent.tsx b/src/components/UploadComponent/UploadErrorComponent.tsx new file mode 100644 index 00000000..1a99870a --- /dev/null +++ b/src/components/UploadComponent/UploadErrorComponent.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import styles from '@cp/pages/AmeyoUtility/components/UploadNumbers/index.module.scss'; +import { CheckIcon, CloseIcon } from '@icons'; +import Button from '@primitives/Button'; +import uploadComponent from '@cp/components/UploadComponent/UploadComponent'; +import ErrorIconOutline from '@cp/assets/icons/ErrorIconOutline'; +import cx from 'classnames'; + +type UploadErrorComponent = { + message: string | boolean; + onCancel: () => void; + containerClassName?: string; + fileName: string; +}; + +function UploadErrorComponent({ + message, + onCancel, + containerClassName, + fileName +}: UploadErrorComponent) { + return ( + <> +
+
+ +
+ {fileName} +
+
+
+ +
+
+
{message}
+ + ); +} + +export default UploadErrorComponent; diff --git a/src/components/UploadComponent/UploadSuccessComponent.tsx b/src/components/UploadComponent/UploadSuccessComponent.tsx new file mode 100644 index 00000000..0f9ba0c9 --- /dev/null +++ b/src/components/UploadComponent/UploadSuccessComponent.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import styles from '@cp/pages/AmeyoUtility/components/UploadNumbers/index.module.scss'; +import { CheckIcon, CloseIcon } from '@icons'; +import Button from '@primitives/Button'; +import cx from 'classnames'; +import CircularLoader from '@navi/web-ui/lib/icons/CircularLoaderIcon'; + +type UploadSuccessComponent = { + name: string; + onCancel: () => void; + containerClassName?: string; + isInProgress?: boolean; +}; + +function UploadSuccessComponent({ + name, + onCancel, + containerClassName, + isInProgress +}: UploadSuccessComponent) { + return ( +
+
+
+ {isInProgress ? ( + + ) : ( + + )} +
+
{name}
+
+ +
+ ); +} + +export default UploadSuccessComponent; diff --git a/src/components/UploadComponent/index.tsx b/src/components/UploadComponent/index.tsx new file mode 100644 index 00000000..e7559423 --- /dev/null +++ b/src/components/UploadComponent/index.tsx @@ -0,0 +1,4 @@ +import uploadComponent from '@cp/components/UploadComponent/UploadComponent'; +import UseUpload from '@cp/components/UploadComponent/useUpload'; + +export { uploadComponent, UseUpload }; diff --git a/src/components/UploadComponent/useUpload.ts b/src/components/UploadComponent/useUpload.ts new file mode 100644 index 00000000..d7f5efa8 --- /dev/null +++ b/src/components/UploadComponent/useUpload.ts @@ -0,0 +1,47 @@ +import React, { useCallback, useState } from 'react'; +import axiosInstance from '@cp/utils/ApiHelper'; +import { ErrorMessage } from '@cp/components/UploadComponent/UploadComponent'; + +export enum UPLOAD_STATUS { + IDLE = 'IDLE', + IN_PROGRESS = 'IN_PROGRESS', + SUCCESS = 'SUCCESS', + ERROR = 'ERROR' +} + +interface UseUploadParams { + onFileUploadedSuccess?: () => void; + onFileUploadedError?: () => void; +} + +function UseUpload({ onFileUploadedSuccess, onFileUploadedError }: UseUploadParams) { + const [fileStatus, setFileStatus] = useState(UPLOAD_STATUS.IDLE); + const [file, setFile] = useState(null as File | null); + const [error, setError] = useState({ message: '', fileName: null }); + const upload = useCallback((file: File, uploadUrl: string) => { + setFileStatus(UPLOAD_STATUS.IN_PROGRESS); + if (!file) { + return; + } + const formData = new FormData(); + formData.append('file', file); + return axiosInstance + .put(uploadUrl, formData) + .then(_ => { + setFileStatus(UPLOAD_STATUS.SUCCESS); + onFileUploadedSuccess && onFileUploadedSuccess(); + }) + .catch(_ => { + setFileStatus(UPLOAD_STATUS.ERROR); + onFileUploadedError && onFileUploadedError(); + }); + }, []); + + const resetFileStatus = useCallback(() => { + setFileStatus(UPLOAD_STATUS.IDLE); + }, []); + + return { fileStatus, upload, resetFileStatus, setFile, file, error, setError }; +} + +export default UseUpload; diff --git a/src/pages/AllAgents/AgentForm/AgentForm.tsx b/src/pages/AllAgents/AgentForm/AgentForm.tsx index b07cff33..8159d8ef 100644 --- a/src/pages/AllAgents/AgentForm/AgentForm.tsx +++ b/src/pages/AllAgents/AgentForm/AgentForm.tsx @@ -46,6 +46,9 @@ 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'; export const AgentForm = forwardRef((props: AgentFormProps, ref) => { const { @@ -86,7 +89,8 @@ export const AgentForm = forwardRef((props: AgentFormProps, ref) => { getValues, setValue, watch, - setFocus + setFocus, + register } = useForm({ defaultValues }); @@ -132,6 +136,10 @@ export const AgentForm = forwardRef((props: AgentFormProps, ref) => { const enableStateEdit = rolesContainAnyOfV1(agentStateAndLanguageEditRoles); const disableStateEdit = !!(!enableStateEdit && agentData?.referenceId); const isUserManagementRole = user?.roles?.includes(Roles.ROLE_USER_MANAGEMENT_ADMIN); + const isDraUploadAccessFeatureFlagEnabled = useSelector( + (store: RootState) => store.common.featureFlags?.draUploadAccess + ); + const getAgencyCode = () => { if (isEditable || isUserManagementRole) { return watch('agencyCodeRequested'); @@ -179,7 +187,9 @@ export const AgentForm = forwardRef((props: AgentFormProps, ref) => { tertiaryRegion: agentData?.tertiaryRegion || '', bucketGroup: agentData?.bucketGroup, agencyCodeRequested: agentData?.agencyCodeRequested, - agentCenter: agentData?.agentCenter + agentCenter: agentData?.agentCenter, + draCertificateView: agentData?.draCertificateView, + draCertificate: {} }, { keepDirty: !!isDrawerOpen @@ -262,7 +272,8 @@ export const AgentForm = forwardRef((props: AgentFormProps, ref) => { tertiaryLanguage, bucketGroup, agencyCodeRequested, - agentCenter + agentCenter, + draCertificate } = data; const createAgentRequest = { @@ -287,7 +298,13 @@ export const AgentForm = forwardRef((props: AgentFormProps, ref) => { reportingManagerReferenceId: managerList.find( manager => manager?.additionalData?.emailAddress === reportingManagerEmailAddress - )?.additionalData?.referenceId || '' + )?.additionalData?.referenceId || '', + draCertificateView: { + draCertificateUpdateRequest: { + mode: 'ADD', + uploadReferenceId: draCertificate?.referenceId || null + } + } }; createAgent(createAgentRequest, setIsSubmitButtonDisabled, setShowLoader, onClose); }; @@ -309,7 +326,8 @@ export const AgentForm = forwardRef((props: AgentFormProps, ref) => { tertiaryLanguage, bucketGroup, agencyCodeRequested, - agentCenter + agentCenter, + draCertificate } = data; const updateAgentRequest = { @@ -334,7 +352,17 @@ export const AgentForm = forwardRef((props: AgentFormProps, ref) => { reportingManagerReferenceId: managerList.find( manager => manager?.additionalData?.emailAddress === reportingManagerEmailAddress - )?.additionalData?.referenceId || '' + )?.additionalData?.referenceId || '', + draCertificateView: { + draCertificateUpdateRequest: draCertificate + ? { + mode: 'EDIT', + uploadReferenceId: draCertificate?.referenceId + } + : { + mode: 'DELETE' + } + } }; updateAgent( @@ -706,6 +734,30 @@ export const AgentForm = forwardRef((props: AgentFormProps, ref) => { showRequiredIcon={false} />
+ {watch('draCertificateView') ? ( +
+ +
+ ) : ( +
+ {isDraUploadAccessFeatureFlagEnabled ? ( + + ) : null} +
+ )} diff --git a/src/pages/AllAgents/AgentForm/components/form/AgentFormDownloadDraCertificate.tsx b/src/pages/AllAgents/AgentForm/components/form/AgentFormDownloadDraCertificate.tsx new file mode 100644 index 00000000..248e24cb --- /dev/null +++ b/src/pages/AllAgents/AgentForm/components/form/AgentFormDownloadDraCertificate.tsx @@ -0,0 +1,79 @@ +import React from 'react'; +import Button from '@primitives/Button'; +import cx from 'classnames'; +import FileIcon from '@cp/assets/images/icons/FileIcon'; +import DownloadIcon from '@cp/assets/images/icons/DownloadIcon'; +import { Control, useController } from 'react-hook-form'; +import { AgentFormValues } from '@cp/pages/AllAgents/AgentForm/interfaces/types'; +import { GenericObject } from '@cp/src/types/CommonConstans'; +import { getFileDownload } from '@cp/utils/commonUtils'; +import { EditIcon } from '@icons'; +import CrossIcon from '@cp/assets/icons/CrossIcon'; + +type AgentFormDownloadDraCertificateProps = { + name: keyof AgentFormValues; + containerClassName?: string; + control: Control; + rules?: GenericObject; + isEditDisable: boolean; +}; + +function AgentFormDownloadDraCertificate({ + name, + containerClassName, + control, + rules, + isEditDisable +}: AgentFormDownloadDraCertificateProps) { + const { field } = useController({ + name, + control, + rules + }); + return ( + <> +
DRA Certificate
+
+
+
+ +
+
+ {'Agent DRA Certificate.pdf'} +
+
+
+ {!isEditDisable ? ( + + ) : ( + + )} +
+
+ + ); +} + +export default AgentFormDownloadDraCertificate; diff --git a/src/pages/AllAgents/AgentForm/components/form/AgentFormUploadControl.tsx b/src/pages/AllAgents/AgentForm/components/form/AgentFormUploadControl.tsx new file mode 100644 index 00000000..b35654c6 --- /dev/null +++ b/src/pages/AllAgents/AgentForm/components/form/AgentFormUploadControl.tsx @@ -0,0 +1,137 @@ +import React, { useEffect } from 'react'; +import { Control, FieldErrorsImpl, RegisterOptions, useController } from 'react-hook-form'; +import { AgentFormValues } from '../../interfaces/types'; +import useUpload, { UPLOAD_STATUS } from '@cp/components/UploadComponent/useUpload'; +import UploadComponent from '@cp/components/UploadComponent/UploadComponent'; +import UploadErrorComponent from '@cp/components/UploadComponent/UploadErrorComponent'; +import UploadIcon from '@navi/web-ui/lib/icons/UploadIcon'; +import UploadSuccessComponent from '@cp/components/UploadComponent/UploadSuccessComponent'; +import axiosInstance, { ApiKeys, getApiUrl } from '@cp/utils/ApiHelper'; + +type AgentFormUploadControlProps = { + control: Control; + name: keyof AgentFormValues; + rules: RegisterOptions; + errors: Partial>; + isEditDisabled: boolean | undefined; + styles: CSSModuleClasses; + inputLabel?: string; + isRequired?: boolean; + setSubmitButtonDisabled: (isDisabled: boolean) => void; +}; + +const customAgentFormUploadComponent = (handleUploadClick: (e: React.ChangeEvent) => void) => { + return ( +
+
+ +
{ + handleUploadClick(e); + }} + > + {' '} + Click to Upload{' '} + or drag and drop here +
+
+
+ ); +}; +const AgentFormUploadControl: React.FC = props => { + const { control, name, rules, errors, styles, inputLabel = '', setSubmitButtonDisabled } = props; + + const { field } = useController({ + name, + control, + rules + }); + + const { file, setFile, upload, fileStatus, error, setError } = useUpload({}); + useEffect(() => { + if (file) { + const getUploadUrl = getApiUrl( + ApiKeys.GET_DOCUMENT_UPLOAD_URL, + {}, + { + documentType: 'DRA_CERTIFICATE', + documentContentType: 'PDF' + } + ); + axiosInstance.get(getUploadUrl).then(async res => { + //presigned url fetch and pass it to upload function + setSubmitButtonDisabled(true); + upload(file, res.data?.preSignedPutUrl) + ?.then(() => { + //on success + const ackUrl = getApiUrl(ApiKeys.DOCUMENT_UPLOAD_ACK_URL); + + axiosInstance + .post(ackUrl, { + referenceId: res?.data?.referenceId, + status: 'UPLOAD_SUCCESS', + message: '' + }) + .then(res => { + field.onChange(res?.data); + }); + }) + .catch(err => { + setError({ message: err?.message || '', fileName: file?.name || '' }); + }) + .finally(() => { + setSubmitButtonDisabled(false); + }); + }); + } + }, [file]); + + return ( + <> +
+ Upload DRA Certificate +
+ {error?.message ? ( +
+ { + setError({ message: '', fileName: '' }); + }} + /> +
+ ) : null} + {error?.message ? null : ( +
+ {file ? ( + { + setFile(null); + field.onChange(null); + }} + /> + ) : ( +
+ +
+ )} +
+ )} + + ); +}; + +export default AgentFormUploadControl; diff --git a/src/pages/AllAgents/AgentForm/interfaces/types.ts b/src/pages/AllAgents/AgentForm/interfaces/types.ts index db0ac78e..c7c2065f 100644 --- a/src/pages/AllAgents/AgentForm/interfaces/types.ts +++ b/src/pages/AllAgents/AgentForm/interfaces/types.ts @@ -1,6 +1,7 @@ import { PageRequest } from '@cp/src/components/interfaces'; import { Roles } from '@cp/src/pages/auth/constants/AuthConstants'; import { Dispatch, SetStateAction } from 'react'; +import { GenericObject } from '@cp/src/types/CommonConstans'; export interface AgentFormProps { ref?: any; @@ -33,6 +34,12 @@ export interface AgentFormValues { bucketGroup: string; reportingManagerReferenceId?: string; agentCenter?: string; + draCertificate?: GenericObject; + draCertificateView?: { + draCertificate: { + uri: string; + }; + }; } export interface Agent { @@ -62,6 +69,11 @@ export interface Agent { agencyCodeRequested?: string; reportingManagerReferenceId?: string; agentCenter?: string; + draCertificateView?: { + draCertificate: { + uri: string; + }; + }; } export interface AgentPageRequest extends PageRequest { diff --git a/src/pages/CaseDetails/constants/EmiSchedule.component.tsx b/src/pages/CaseDetails/constants/EmiSchedule.component.tsx index 461bfabd..cbe7d31e 100644 --- a/src/pages/CaseDetails/constants/EmiSchedule.component.tsx +++ b/src/pages/CaseDetails/constants/EmiSchedule.component.tsx @@ -149,6 +149,7 @@ export const FeeWaiverHistoryColumnDefs: ColDefsType[] = [ { headerName: 'Amount(incl. GST)', field: 'feeAmount', + width: 150, cellRenderer: (params: ICellRendererParams) => { const value = formatAmount(params.data?.deferredAmount, false) || '--'; diff --git a/src/utils/ApiHelper.ts b/src/utils/ApiHelper.ts index 609e05f3..8282e779 100644 --- a/src/utils/ApiHelper.ts +++ b/src/utils/ApiHelper.ts @@ -253,7 +253,9 @@ export enum ApiKeys { HUMAN_REMNINDER_DISPOSE_CALL_V2, PHONENUMBER_SECONDARY_SOURCES, VALIDATE_PHONE_NUMBER, - REMOVE_CONTACT + REMOVE_CONTACT, + GET_DOCUMENT_UPLOAD_URL, + DOCUMENT_UPLOAD_ACK_URL } // TODO: try to get rid of `as` @@ -510,6 +512,8 @@ API_URLS[ApiKeys.HUMAN_REMINDER_AVAILABLITY_V2] = 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'; +API_URLS[ApiKeys.GET_DOCUMENT_UPLOAD_URL] = 'v2/file-uploads/upload-url'; +API_URLS[ApiKeys.DOCUMENT_UPLOAD_ACK_URL] = '/v2/file-uploads/upload-ack'; // TODO: try to get rid of `as` const MOCK_API_URLS: Record = {} as Record;