/* eslint-disable no-restricted-syntax */ /* eslint-disable array-callback-return */ /* eslint-disable @typescript-eslint/no-shadow */ /* eslint-disable react/no-array-index-key */ /* eslint-disable @typescript-eslint/no-use-before-define */ /* eslint-disable no-plusplus */ /* eslint-disable import/no-extraneous-dependencies */ /* eslint-disable no-alert */ /* eslint-disable jsx-a11y/click-events-have-key-events */ /* eslint-disable jsx-a11y/no-static-element-interactions */ /* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable jsx-a11y/control-has-associated-label */ /* eslint-disable react/button-has-type */ /* eslint-disable max-len */ import React, { useState, useEffect, useCallback } from 'react'; import { useDropzone } from 'react-dropzone'; import { AiOutlineClose } from 'react-icons/ai'; import FileUploadIcon from '@assets/FileUploadIcon.svg'; import FileListIcon from '@assets/FileListIcon.svg'; import FileUploadIconDark from '@assets/FileUploadIconDark.svg'; import TickMarkIconGreen from '@assets/TickMarkIconGreen.svg'; import TickMarkIconGrey from '@assets/TickMarkIconGrey.svg'; import FileArrowIcon from '@assets/FileArrowIcon.svg'; import './style.scss'; import Box from '@mui/material/Box/Box'; import Button from '@mui/material/Button/Button'; import { useForm } from 'react-hook-form'; import { fetchDocumentTypeList, fetchDocumentsList, deleteDocument, willDocumentUploadSelector, saveDocument, } from '@redux/slices/will-documents-upload'; import { useSelector, useDispatch } from 'react-redux'; import { DocumentTypeList } from '@api/models'; import axios from 'axios'; import { willPersonalInformationSelector } from '@redux/slices/will-personal-information'; import { willsValidatorSelector } from '@redux/slices/willsValidator'; import MirrorWillSwitcher from '@components/MirrorWillSwitcher'; // import custom hook useDocumentsList import useDocumentsList from '@utils/hooks/useDocumentsList'; import DeleteConfirmationModal from '@components/DeleteConfirmationModal'; import { useLocation } from 'react-router'; import DocumentManagerModal from '@components/DocumentManagerModal'; import { fetchAppointmentDetail } from '@redux/slices/wills-book-appointment'; import { Alert, AlertColor, Snackbar } from '@mui/material'; import MuiAlert from '@mui/material/Alert'; import { trackPromise } from 'react-promise-tracker'; import { getAppointmentDetail } from '../../redux/slices/wills-book-appointment'; import api from '../../api'; interface Props { handleNext: (step: number, stepName?: string) => void; currentBtnStep: number; handleBack: () => void; willTypeID: number; } function DocumentManager({ handleNext, currentBtnStep, handleBack, willTypeID, }: Props) { // const [documentTypeID, setDocumentTypeID] = useState<string>(''); const [documentTypeArray, setDocumentTypeArray] = useState([]); const [documentTypeID, setDocumentTypeID] = useState(); const [uniqueDocumentId, setUniqueDocumentId] = useState(); const [witnessProfileGUID, setWitnessProfileGUID] = useState(); const [willDocGuid, setWillDocGuid] = useState<string>(''); // const bookedForProfileGUID = 'ba125f2d-8c78-41ce-b576-6aaef9b57c2a'; const [uploadedFiles, setUploadedFiles] = useState([]); const [showModal, setShowModal] = useState(false); const { state } = useLocation(); const [showBookAppointnentModal, setShowBookAppointnentModal] = useState(false); // State for managing snackbar const [snackbarOpen, setSnackbarOpen] = useState(false); const [snackbarSeverity, setSnackbarSeverity] = useState<AlertColor>('error'); // You can change 'error' to 'warning' or other values if needed // Snackbar message to display const snackbarMessage = 'Please upload all required documents before proceeding to the next step.'; const [documentIsRequired, setDocumentIsRequired] = useState<boolean>(false); // check document is required or not and useEffect(() => { const isRequiredDocumentsExist = documentTypeArray.some((d) => d.isRequired); setDocumentIsRequired(isRequiredDocumentsExist); }, [documentTypeArray]); // profile guid const { isSpouseSelected, spouseGuid, profileGuid } = useSelector(willPersonalInformationSelector); const dispatch = useDispatch(); const handleOpenModal = () => { setShowModal(true); }; const handleCloseModal = () => { setShowModal(false); }; const handleBookAppointmentModalOpen = () => { setShowBookAppointnentModal(true); }; const handleBookAppointmentModalClose = () => { setShowBookAppointnentModal(false); }; const handleBookAppointmentModalContinue = () => { setShowBookAppointnentModal(false); // handleNext(currentBtnStep, 'upload'); }; useEffect(() => { const fetchData = async () => { try { const response = await trackPromise(api.getAppointmentDetail(profileGuid)); console.log('fetch AppointmentDetail Result', response?.data?.Output); // log the timeSlotID console.log('timeSlotID:', response?.data?.Output?.timeSlotID); if (response?.data?.Output?.timeSlotID === 0) { console.log('No appointment details found! Opening modal'); handleBookAppointmentModalOpen(); } else { const appointmentDetails = response.data.Output; console.log('Appointment details:', appointmentDetails); } } catch (error) { console.error('Error fetching appointment detail:', error); } }; fetchData(); }, []); // API results from Redux const { bookedforprofileguid, docType, documentTypeList, documentsList, docTypeID, isDocumentsChanged, } = useSelector(willDocumentUploadSelector); const { mirrorWillCheck } = useSelector(willsValidatorSelector); // custom hook for document list const uploadedDocumentsList = useDocumentsList(documentsList); /** * Retrieves the list of document types and sets it to the document type array. * * @return {void} No return value. */ const getDocumentTypeList = () => { const documentTypes: any = []; documentTypeList.forEach((d: DocumentTypeList) => { documentTypes.push({ documentTypeID: d.docTypeID, documentTypeName: d.fileName, witnessProfileGUID: d.witnessProfileGUID, isRequired: d.isRequired, }); }); setDocumentTypeArray(documentTypes); }; // API calls inside useEffects ------- useEffect(() => { // dispatch<any>(fetchDocumentTypeList(bookedForProfileGUID)); dispatch<any>(fetchDocumentTypeList(isSpouseSelected ? spouseGuid : profileGuid)); }, [dispatch, isSpouseSelected]); useEffect(() => { // Dispatch the fetchDocumentsList action when documentTypeID changes dispatch<any>(fetchDocumentsList(isSpouseSelected ? spouseGuid : profileGuid, documentTypeID, witnessProfileGUID)); }, [uniqueDocumentId, documentTypeID, isDocumentsChanged, isSpouseSelected]); // Result transformation for UI useEffect(() => { getDocumentTypeList(); }, [documentTypeList]); const { handleSubmit } = useForm(); /** * Sets the active element to the given element string. * * @param {string} element - The string representing the active element to set. */ const handleElementClick = (docTypeId: any, witnessProfileGUID: any) => { const uniqueDocumentId: any = `${docTypeId}-${witnessProfileGUID}`; setUniqueDocumentId(uniqueDocumentId); setDocumentTypeID(docTypeId); setWitnessProfileGUID(witnessProfileGUID); }; /** * Returns the background color style object for a given element. * * @param {string} element - The element to get background style for. * @return {Object} The background color style object for the given element. */ const getBackgroundStyle = (element: any) => ({ backgroundColor: uniqueDocumentId === element ? '#023979' : '#F4F4F4', }); /** * Returns an object representing the style to be applied to the title element. * * @param {string} element - The element to apply the style to. * @return {Object} An object containing the color property to be applied to the title element. */ const getTitleStyle = (element: any) => ({ color: uniqueDocumentId === element ? '#FFFFFF' : '#1B202D', }); const getFileUploadIcon = (element: any) => (uniqueDocumentId === element ? FileUploadIcon : FileUploadIconDark); const getTickMarkicon = (element: any) => (uniqueDocumentId === element ? TickMarkIconGreen : TickMarkIconGrey); /** * Handles click event on "Add File" button. * * @param {any} e - The event object. * @return {void} Nothing is returned by this function. */ const handleAddFileClick = (e: any) => { e.preventDefault(); if (e.target !== e.currentTarget) { return; } document.getElementById('file-upload-input')?.click(); }; /** * Handles the file input change event. * * @param {any} e - the event object * @return {void} */ const handleFileInputChange = (e: any) => { const newFiles = Array.from(e.target.files); checkFileValidity(newFiles); }; /** * Updates the document type array by setting the 'isRequired' property to false for the document type * with the specified 'documentTypeID'. Returns the updated document type array. * * @param {any} documentTypeID - The ID of the document type to update * @return {void} */ const updateDocumentTypeArray = (documentTypeID: any): void => { const updatedDocumentTypeArray = documentTypeArray.map((doc) => (doc.documentTypeID === documentTypeID ? { ...doc, isRequired: false } : doc)); setDocumentTypeArray(updatedDocumentTypeArray); }; /** * Handles the file drop event by checking the validity of the accepted files. * * @param {any} acceptedFiles - the files that have been accepted for upload */ const handleFileDrop = (acceptedFiles: any) => { checkFileValidity(acceptedFiles); }; /** * Prevents the click event from propagating and executing other click events. * * @param {any} e - the click event to be stopped from propagating */ const handleRowItemClick = (e: any) => { e.stopPropagation(); }; /** * Filters files by their extension and size, and adds the valid files to the uploadedFiles state. * * @param {Array} files - The array of files to be checked. * @return {void} Returns nothing. */ // Check the validity of uploaded files const checkFileValidity = async (files: any[]) => { const validExtensions = ['.pdf', '.jpeg', '.jpg', '.bmp', '.doc', '.docx']; const maxFileSize = 20 * 1024 * 1024; // Filter valid files based on extension and file size const validFiles = files.filter((file: { name: string; size: number; }) => { // Check if the file extension is valid const isValidExtension = validExtensions.some((ext) => file.name.toLowerCase().endsWith(ext)); // Check if the file size is within the allowed limit const isWithinMaxSize = file.size <= maxFileSize; return isValidExtension && isWithinMaxSize; }); // Filter invalid files const invalidFiles = files.filter( (file: any) => !validFiles.includes(file), ); if (invalidFiles.length > 0) { // Display an alert message for invalid files const invalidFileNames = invalidFiles .map((file: { name: any; }) => file.name) .join(', '); alert( `Invalid files: ${invalidFileNames}. Please use A4-size PDF, JPEG, BMP, DOC, or DOCX files that are within 20MB.`, ); } else { // Add valid files to the uploaded files list const updatedUploadedFiles = [...uploadedFiles, ...validFiles]; setUploadedFiles(updatedUploadedFiles); // Update the document type array with the document type ID. updateDocumentTypeArray(documentTypeID); const formData = new FormData(); for (let i = 0; i < validFiles.length; i++) { const file = validFiles[i]; formData.append('FileDoc', file, file.name); } dispatch<any>( saveDocument( isSpouseSelected ? spouseGuid : profileGuid, documentTypeID, witnessProfileGUID, formData, ), ); } }; const handleNextStepClick = () => { if (!documentIsRequired) { // Handle Next Step click logic here handleNext(state === 'Guardianship Will' ? 6 : currentBtnStep, 'upload'); } else { // Show the snackbar with an error message setSnackbarSeverity('error'); // You can change 'error' to 'warning' or other values if needed setSnackbarOpen(true); } }; /** * Removes a file from the system. * * @param {string} willDocGuid - the unique identifier of the file to be removed * @return {any} the result of the delete operation */ const removeFile = (willDocGuid: string) => { setWillDocGuid(willDocGuid); handleOpenModal(); }; const deleteFile = () => { console.log(`Delete the doc with GUID: ${willDocGuid}`); dispatch<any>(deleteDocument(willDocGuid)); // log the result of the delete operation console.log('File removed successfully!'); setShowModal(false); }; const handleUploadDocument = (data: any) => { console.log(data); // handle document upload handleNext(currentBtnStep, 'upload'); }; const { getRootProps, // Props for the file drop zone element getInputProps, // Props for the file input element } = useDropzone({ onDrop: handleFileDrop, // Callback function for handling dropped or selected files }); return ( <main> <section> {mirrorWillCheck && <MirrorWillSwitcher />} <header className="header mt-4">Upload Documents</header> <p className="description"> Upload the documents in PDF or JPEG format. Click on Next Step to save the files once all the documents have been uploaded </p> </section> <div className="row document-upload-container"> <div className="col-lg-6 content-wrapper"> {documentTypeArray?.map((type) => ( <div className={`top${ uniqueDocumentId === `${type?.documentTypeID}-${type?.witnessProfileGUID}` ? ' active' : '' }`} style={getBackgroundStyle(`${type?.documentTypeID}-${type?.witnessProfileGUID}`)} onClick={() => handleElementClick(type?.documentTypeID, type?.witnessProfileGUID)} > <div className="left-container"> <div className="file-upload-icon"> <img src={getFileUploadIcon(`${type?.documentTypeID}-${type?.witnessProfileGUID}`)} alt="File Uploader Icon" /> </div> <div className="document-title" style={getTitleStyle(`${type?.documentTypeID}-${type?.witnessProfileGUID}`)} > {type.documentTypeName} </div> </div> <div className="tick-icon"> <img src={getTickMarkicon(`${type?.documentTypeID}-${type?.witnessProfileGUID}`)} alt="Tick Mark" /> </div> </div> ))} </div> <div className="col-lg-6 row-item" {...getRootProps()} onClick={handleRowItemClick} > <div className="file-upload-arrow"> <img src={FileArrowIcon} alt="File Upload Arrow Icon" /> </div> <div className="file-upload-text"> Drag and drop document here to upload </div> <div className="file-attach-instructions"> Please attach the file(s) below (use the Add button). We recommend using A4-size PDF, BMP, PNG, DOC, DOCX, JPG and JPEG files. File size cannot be more than 20 megabytes (MB). Your files will be uploaded when you submit your form. </div> <div className="file-add-button"> <button className="add-file-btn" onClick={handleAddFileClick}> Add File </button> <input type="file" id="file-upload-input" name="file-upload-input" accept=".pdf, .bmp, .png, .doc, .docx, .jpg, .jpeg" multiple onChange={handleFileInputChange} style={{ display: 'none' }} {...getInputProps()} /> </div> {uploadedDocumentsList.length > 0 && uploadedDocumentsList.map((file: any, index) => ( <div className="file-list-item" key={index}> <div className="file-info"> <div className="file-icon"> <img src={FileListIcon} alt="File List Icon" /> </div> <div className="file-name">{file.fileName}</div> </div> <div className="close-icon" onClick={() => removeFile(file.willDocGuid)}> <span className="close-icon-text">Remove</span> </div> </div> ))} </div> </div> {/* log documentIsRequired on template */} {/* {documentIsRequired && <div>Document is required</div>} */} <Box sx={{ display: 'flex', flexDirection: 'row', pt: 2 }}> <button type="button" className="next-btn" onClick={handleNextStepClick} > Next Step </button> </Box> {/* Snackbar */} <Snackbar open={snackbarOpen} autoHideDuration={3000} onClose={() => setSnackbarOpen(false)}> <MuiAlert elevation={6} variant="filled" severity={snackbarSeverity} onClose={() => setSnackbarOpen(false)} > {snackbarMessage} </MuiAlert> </Snackbar> <span className="next-btn-text mt-4"> *Before clicking next, please make sure the details provided here are correct. </span> <DeleteConfirmationModal show={showModal} handleClose={handleCloseModal} handleContinue={deleteFile} type="Document" /> { (willTypeID === 1 || willTypeID === 2) && ( <DocumentManagerModal showBookAppointnentModal={showBookAppointnentModal} handleBookAppointmentModalClose={handleBookAppointmentModalClose} handleBookAppointmentModalContinue={handleBookAppointmentModalContinue} handleNext={handleNext} currentBtnStep={currentBtnStep} handleBack={handleBack} /> ) } </main> ); } export default DocumentManager;
Preview:
downloadDownload PNG
downloadDownload JPEG
downloadDownload SVG
Tip: You can change the style, width & colours of the snippet with the inspect tool before clicking Download!
Click to optimize width for Twitter