From 1df3d44f274c85d2a07ba83bfa3757f151e00ba1 Mon Sep 17 00:00:00 2001 From: olewandowski1 Date: Tue, 15 Oct 2024 09:24:50 +0200 Subject: [PATCH 1/5] OM-316: upload init --- src/components/UploadWorkerModal.js | 271 ++++++++++++++++++++++++++++ src/constants.js | 6 + src/context/UploadWorkerContext.js | 121 +++++++++++++ src/pages/WorkersPage.js | 72 ++++++-- src/translations/en.json | 17 ++ 5 files changed, 469 insertions(+), 18 deletions(-) create mode 100644 src/components/UploadWorkerModal.js create mode 100644 src/context/UploadWorkerContext.js diff --git a/src/components/UploadWorkerModal.js b/src/components/UploadWorkerModal.js new file mode 100644 index 0000000..c842f69 --- /dev/null +++ b/src/components/UploadWorkerModal.js @@ -0,0 +1,271 @@ +import React, { useRef, useState } from 'react'; + +import { + Box, + Button, + ButtonBase, + CircularProgress, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Grid, + TextField, + Typography, +} from '@material-ui/core'; +import { alpha } from '@material-ui/core/styles/colorManipulator'; +import CheckCircleIcon from '@material-ui/icons/CheckCircle'; +import CloudUploadIcon from '@material-ui/icons/CloudUpload'; +import DescriptionIcon from '@material-ui/icons/Description'; +import ErrorIcon from '@material-ui/icons/Error'; +import WarningIcon from '@material-ui/icons/Warning'; +import { makeStyles } from '@material-ui/styles'; + +import { useModulesManager, useTranslations } from '@openimis/fe-core'; +import { MODULE_NAME, UPLOAD_STAGE } from '../constants'; +import { useUploadWorkerContext } from '../context/UploadWorkerContext'; + +const useStyles = makeStyles((theme) => ({ + primaryButton: theme.dialog.primaryButton, + secondaryButton: theme.dialog.secondaryButton, + errorButton: { + ...theme.dialog.primaryButton, + backgroundColor: theme.palette.error.main, + '&:hover': { + backgroundColor: alpha(theme.palette.error.main, 0.5), + color: theme.palette.error.main, + }, + }, + wrapper: { + width: '400px', + display: 'flex', + flexDirection: 'column', + }, + uploadBox: { + width: '100%', + minHeight: '140px', + backgroundColor: theme.palette.background.default, + border: `1px solid ${theme.palette.divider}`, + borderRadius: theme.shape.borderRadius, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + gap: theme.spacing(0.5), + flexDirection: 'column', + padding: theme.spacing(2), + }, + input: { + display: 'none', + }, + statusBox: { + padding: theme.spacing(2), + border: `1px solid ${theme.palette.divider}`, + borderRadius: theme.shape.borderRadius, + display: 'flex', + justifyContent: 'start', + alignItems: 'center', + gap: theme.spacing(0.5), + flexDirection: 'row', + marginBottom: theme.spacing(1), + }, + successBox: { + backgroundColor: '#e0f7fa', + border: '1px solid #009688', + color: '#00796b', + }, + warningBox: { + backgroundColor: '#fff3e0', + border: '1px solid #ff9800', + color: '#e65100', + }, + errorBox: { + backgroundColor: '#ffebee', + border: '1px solid #f44336', + color: '#c62828', + }, + summaryTextField: { + width: '100%', + }, +})); + +function UploadWorkerModal({ open, onClose }) { + const classes = useStyles(); + const modulesManager = useModulesManager(); + const { formatMessage, formatMessageWithValues } = useTranslations(MODULE_NAME, modulesManager); + const fileInputRef = useRef(null); + const [fileUploadError, setFileUploadError] = useState(null); + const maxSizeInMB = 10; + + const { + file, + isUploading, + uploadStage, + validationError, + validationSuccess, + validationWarning, + onFileUpload, + isUploaded, + onWorkersUpload, + uploadSummary, + resetFile, + } = useUploadWorkerContext(); + + const isFileUploadStage = uploadStage === UPLOAD_STAGE.FILE_UPLOAD; + const isWorkerUploadStage = uploadStage === UPLOAD_STAGE.WORKER_UPLOAD; + + const handleFileChange = (e) => { + const uploadedFile = e.target.files[0]; + + if (!uploadedFile) { + setFileUploadError(formatMessage('UploadWorkerModal.fileRequired')); + return; + } + + if (uploadedFile.size > maxSizeInMB * 1024 * 1024) { + setFileUploadError( + formatMessageWithValues('UploadWorkerModal.fileSizeError', { + fileSize: maxSizeInMB, + }), + ); + return; + } + + onFileUpload(uploadedFile); + }; + + const handleDeleteFile = () => { + resetFile(); + fileInputRef.current.value = null; + }; + + return ( + + {formatMessage('UploadWorkerModal.dialogTitle')} + + {isFileUploadStage && ( + <> + + + fileInputRef.current.click()}> + {isUploading && } + + {!isUploading && fileUploadError && ( + + + {fileUploadError} + + )} + + {!isUploading && !fileUploadError && !isUploaded && ( + <> + + {formatMessage('UploadWorkerModal.uploadPrompt')} + {formatMessage('UploadWorkerModal.uploadCaption')} + + )} + + {!isUploading && !fileUploadError && isUploaded && file && ( + <> + + + {formatMessageWithValues('UploadWorkerModal.uploadSuccess', { + button: {formatMessage('UploadWorkerModal.upload')}, + })} + + + {formatMessageWithValues('UploadWorkerModal.uploadSuccessCaption', { + fileName: {file.name}, + fileSize: {(file.size / 1024 / 1024).toFixed(2)}, + })} + + + )} + + + )} + {isWorkerUploadStage && ( + <> + {!isUploading && ( + + {validationError && ( + + + {validationError} + + )} + {validationSuccess && ( + + + {validationSuccess} + + )} + {validationWarning && ( + + + {validationWarning} + + )} + + )} + +
+ {isUploading ? ( + + ) : ( + Object.entries(uploadSummary).map(([key, value]) => ( + + )) + )} +
+ + )} +
+ + {isFileUploadStage && ( + <> + + {isUploaded && !fileUploadError && file && ( + <> + + + + )} + + )} + {isWorkerUploadStage && ( + + )} + +
+ ); +} + +export default UploadWorkerModal; diff --git a/src/constants.js b/src/constants.js index 2341ac6..8f0f879 100644 --- a/src/constants.js +++ b/src/constants.js @@ -3,6 +3,7 @@ export const EMPLOYER_RIGHT_SEARCH = 203000; export const VOUCHER_PRICE_MANAGEMENT_RIGHT = 205001; export const INSPECTOR_RIGHT = 204005; export const ADMIN_RIGHT = 204004; +export const RIGHT_WORKER_UPLOAD = 101101; export const RIGHT_WORKER_SEARCH = 101101; export const RIGHT_WORKER_ADD = 101102; export const RIGHT_WORKER_EDIT = 101103; @@ -93,3 +94,8 @@ export const WORKER_IMPORT_PLANS = [ labelKey: 'workerVoucher.workerImport.previousDay', }, ]; + +export const UPLOAD_STAGE = { + FILE_UPLOAD: 'FILE_UPLOAD', + WORKER_UPLOAD: 'WORKER_UPLOAD', +}; diff --git a/src/context/UploadWorkerContext.js b/src/context/UploadWorkerContext.js new file mode 100644 index 0000000..00fce8b --- /dev/null +++ b/src/context/UploadWorkerContext.js @@ -0,0 +1,121 @@ +import React, { + createContext, useContext, useEffect, useMemo, useState, +} from 'react'; +import { useSelector } from 'react-redux'; + +import { baseApiUrl, useTranslations } from '@openimis/fe-core'; +import { EMPTY_STRING, MODULE_NAME, UPLOAD_STAGE } from '../constants'; + +const UploadWorkerContext = createContext(); + +export function UploadWorkerProvider({ children }) { + const { economicUnit } = useSelector((state) => state.policyHolder); + const { formatMessage } = useTranslations(MODULE_NAME); + const [uploadStage, setUploadStage] = useState(UPLOAD_STAGE.FILE_UPLOAD); + const [file, setFile] = useState(null); + const [isUploading, setIsUploading] = useState(false); + const [isUploaded, setIsUploaded] = useState(false); + const [validationError, setValidationError] = useState(EMPTY_STRING); + const [validationSuccess, setValidationSuccess] = useState(EMPTY_STRING); + const [validationWarning, setValidationWarning] = useState(EMPTY_STRING); + const [uploadSummary, setUploadSummary] = useState({ + affectedRows: 0, + totalNumberOfRecordsInFile: 0, + skippedRows: 0, + }); + + const onFileUpload = (uploadedFile) => { + try { + setFile(uploadedFile); + } catch (error) { + // eslint-disable-next-line no-console + console.log('[UPLOAD_WORKER_CONTEXT]: Error while uploading file', error); + } finally { + setIsUploading(false); + setIsUploaded(true); + } + }; + + const uploadWorkers = async () => { + setIsUploading(true); + + const uploadUrl = new URL(`${window.location.origin}${baseApiUrl}/worker_voucher/worker_upload/`); + uploadUrl.searchParams.append('economic_unit_code', economicUnit.code); + + const formData = new FormData(); + formData.append('file', file); + + try { + const response = await fetch(uploadUrl, { + method: 'POST', + body: formData, + }); + + if (!response.ok) { + setValidationError(formatMessage('UploadWorkerModal.workerUploadError')); + return; + } + + const data = await response.json(); + + console.log(response); + console.log(data); + } catch (error) { + setValidationError(formatMessage('UploadWorkerModal.workerUploadError')); + } finally { + setIsUploading(false); + setIsUploaded(true); + } + }; + + const onWorkersUpload = async () => { + setUploadStage(UPLOAD_STAGE.WORKER_UPLOAD); + + await uploadWorkers(); + }; + + const resetFile = () => { + setFile(null); + setIsUploaded(false); + setValidationError(EMPTY_STRING); + setValidationSuccess(EMPTY_STRING); + setValidationWarning(EMPTY_STRING); + setUploadStage(UPLOAD_STAGE.FILE_UPLOAD); + setUploadSummary({ + affectedRows: 0, + totalNumberOfRecordsInFile: 0, + skippedRows: 0, + }); + }; + + useEffect(() => { + resetFile(); + }, [economicUnit.code]); + + const memoizedContextValue = useMemo( + () => ({ + file, + isUploading, + isUploaded, + validationError, + validationSuccess, + validationWarning, + uploadSummary, + uploadStage, + onWorkersUpload, + setFile, + setIsUploading, + setValidationError, + setValidationSuccess, + setValidationWarning, + onFileUpload, + setUploadSummary, + resetFile, + }), + [file, isUploading, uploadStage, uploadSummary, isUploaded, validationError, validationSuccess, validationWarning], + ); + + return {children}; +} + +export const useUploadWorkerContext = () => useContext(UploadWorkerContext); diff --git a/src/pages/WorkersPage.js b/src/pages/WorkersPage.js index 5a62a0b..652cad4 100644 --- a/src/pages/WorkersPage.js +++ b/src/pages/WorkersPage.js @@ -1,47 +1,83 @@ -import React from 'react'; +import React, { useState } from 'react'; +import clsx from 'clsx'; import { useSelector } from 'react-redux'; import { Fab } from '@material-ui/core'; -import { makeStyles } from '@material-ui/styles'; import AddIcon from '@material-ui/icons/Add'; +import CloudUploadIcon from '@material-ui/icons/CloudUpload'; +import { makeStyles } from '@material-ui/styles'; import { - historyPush, Helmet, useModulesManager, useTranslations, withTooltip, useHistory, + Helmet, + historyPush, + useHistory, + useModulesManager, useTranslations, withTooltip, } from '@openimis/fe-core'; -import { MODULE_NAME, RIGHT_WORKER_ADD, RIGHT_WORKER_SEARCH } from '../constants'; import WorkerSearcher from '../components/WorkerSearcher'; +import { + MODULE_NAME, RIGHT_WORKER_ADD, RIGHT_WORKER_SEARCH, RIGHT_WORKER_UPLOAD, +} from '../constants'; +import { UploadWorkerProvider } from '../context/UploadWorkerContext'; +import UploadWorkerModal from '../components/UploadWorkerModal'; export const useStyles = makeStyles((theme) => ({ page: theme.page, fab: theme.fab, + wrapper: { + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(1), + }, })); function WorkersPage() { + const [uploadOpen, setUploadOpen] = useState(false); const modulesManager = useModulesManager(); const history = useHistory(); const classes = useStyles(); const { formatMessage } = useTranslations(MODULE_NAME, modulesManager); - const rights = useSelector((state) => (state.core?.user?.i_user?.rights ?? [])); + const rights = useSelector((state) => state.core?.user?.i_user?.rights ?? []); + const leftPlacement = 'left'; const onAddRedirect = () => { historyPush(modulesManager, history, 'workerVoucher.route.worker'); }; + const onUploadOpen = () => { + setUploadOpen(true); + }; + + const onUploadClose = () => { + setUploadOpen(false); + }; + return ( rights.includes(RIGHT_WORKER_SEARCH) && ( -
- - - {rights.includes(RIGHT_WORKER_ADD) - && withTooltip( -
- - - -
, - formatMessage('workerVoucher.WorkersPage.addTooltip'), - )} -
+ + +
+ + +
+ {rights.includes(RIGHT_WORKER_UPLOAD) + && withTooltip( + + + , + formatMessage('workerVoucher.WorkersPage.uploadTooltip'), + leftPlacement, + )} + {rights.includes(RIGHT_WORKER_ADD) + && withTooltip( + + + , + formatMessage('workerVoucher.WorkersPage.addTooltip'), + leftPlacement, + )} +
+
+
) ); } diff --git a/src/translations/en.json b/src/translations/en.json index c0c8da2..8abf21c 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -43,6 +43,7 @@ "workerVoucher.status.CANCELED": "CANCELED", "workerVoucher.status.CLOSED": "CLOSED", "workerVoucher.WorkersPage.addTooltip": "Add Worker", + "workerVoucher.WorkersPage.uploadTooltip": "Upload Workers", "workerVoucher.WorkerSearcher.exportClose": "Close", "workerVoucher.VoucherDetailsPage.title": "Worker Voucher - {code}", "workerVoucher.WorkerDetailsPage.title": "{chfId} Worker Details Page", @@ -140,6 +141,22 @@ "workerVoucher.WorkerMConnectAddForm.notFound": "Worker not found. Ensure that the National ID is correct and try again.", "workerVoucher.WorkerMConnectAddForm.tip": "As an Employer, you can add a worker to the system by searching for the worker's National ID. Once the worker is found, you can save the form and add the worker to your list of workers.", "workerVoucher.WorkerMConnectAddForm.detail": "Type the worker's National ID and get the worker's details. Ensure that the National ID is correct.", + "workerVoucher.UploadWorkerModal.dialogTitle": "Upload Workers", + "workerVoucher.UploadWorkerModal.close": "Close", + "workerVoucher.UploadWorkerModal.upload": "Upload", + "workerVoucher.UploadWorkerModal.clearFile": "Clear File", + "workerVoucher.UploadWorkerModal.uploadPrompt": "Click here to upload a file", + "workerVoucher.UploadWorkerModal.uploadCaption": "Supports: CSV, XLSX.", + "workerVoucher.UploadWorkerModal.uploadSuccess": "We have your file. Click {button} to proceed.", + "workerVoucher.UploadWorkerModal.uploadSuccessCaption": "File name: {fileName}. File size: ~{fileSize} MB.", + "workerVoucher.UploadWorkerModal.affectedRows": "Affected Rows", + "workerVoucher.UploadWorkerModal.totalNumberOfRecordsInFile": "Total Number of Records in File", + "workerVoucher.UploadWorkerModal.skippedRows": "Skipped Rows", + "workerVoucher.UploadWorkerModal.fileRequired": "File is required", + "workerVoucher.UploadWorkerModal.fileSizeError": "File size should be less than {fileSize} MB", + "workerVoucher.UploadWorkerModal.workerUploadSuccess": "Workers have been successfully uploaded. You can close this dialog.", + "workerVoucher.UploadWorkerModal.workerUploadError": "Failed to upload workers. Please ensure that the file is in the correct format and try again.", + "workerVoucher.UploadWorkerModal.workerUploadWarning": "Some workers have been skipped. You can download the list of skipped workers by clicking the button below.", "workerVoucher.WorkerSearcher.selection": "Selected {count} workers", "workerVoucher.WorkerSearcherSelectActions.delete": "Delete Selected Workers", "workerVoucher.WorkerSearcherSelectActions.dialog.title": "Are you sure you want to proceed?", From a1ce8b33ee73df9892ea59529ab306ddafb6add2 Mon Sep 17 00:00:00 2001 From: olewandowski1 Date: Tue, 15 Oct 2024 15:02:24 +0200 Subject: [PATCH 2/5] OM-316: add downloading file with errors, overall improvements --- src/components/UploadWorkerModal.js | 48 ++++++++++++++++++------ src/components/WorkerSearcher.js | 7 +++- src/constants.js | 5 ++- src/context/UploadWorkerContext.js | 57 ++++++++++++++++++++++++++--- src/translations/en.json | 7 ++-- 5 files changed, 102 insertions(+), 22 deletions(-) diff --git a/src/components/UploadWorkerModal.js b/src/components/UploadWorkerModal.js index c842f69..6b5603c 100644 --- a/src/components/UploadWorkerModal.js +++ b/src/components/UploadWorkerModal.js @@ -1,4 +1,5 @@ import React, { useRef, useState } from 'react'; +import { useSelector } from 'react-redux'; import { Box, @@ -22,7 +23,7 @@ import WarningIcon from '@material-ui/icons/Warning'; import { makeStyles } from '@material-ui/styles'; import { useModulesManager, useTranslations } from '@openimis/fe-core'; -import { MODULE_NAME, UPLOAD_STAGE } from '../constants'; +import { MODULE_NAME, RIGHT_WORKER_UPLOAD, UPLOAD_STAGE } from '../constants'; import { useUploadWorkerContext } from '../context/UploadWorkerContext'; const useStyles = makeStyles((theme) => ({ @@ -92,6 +93,7 @@ function UploadWorkerModal({ open, onClose }) { const classes = useStyles(); const modulesManager = useModulesManager(); const { formatMessage, formatMessageWithValues } = useTranslations(MODULE_NAME, modulesManager); + const rights = useSelector((state) => state.core?.user?.i_user?.rights ?? []); const fileInputRef = useRef(null); const [fileUploadError, setFileUploadError] = useState(null); const maxSizeInMB = 10; @@ -108,12 +110,17 @@ function UploadWorkerModal({ open, onClose }) { onWorkersUpload, uploadSummary, resetFile, + workersWithError, + downloadWorkersWithError, } = useUploadWorkerContext(); const isFileUploadStage = uploadStage === UPLOAD_STAGE.FILE_UPLOAD; const isWorkerUploadStage = uploadStage === UPLOAD_STAGE.WORKER_UPLOAD; + const isWorkerWithErrorAvailable = Object.keys(workersWithError).length > 0; const handleFileChange = (e) => { + setFileUploadError(null); + const uploadedFile = e.target.files[0]; if (!uploadedFile) { @@ -138,6 +145,10 @@ function UploadWorkerModal({ open, onClose }) { fileInputRef.current.value = null; }; + if (!rights.includes(RIGHT_WORKER_UPLOAD)) { + return null; + } + return ( {formatMessage('UploadWorkerModal.dialogTitle')} @@ -252,16 +263,31 @@ function UploadWorkerModal({ open, onClose }) { )} {isWorkerUploadStage && ( - + <> + + {isWorkerWithErrorAvailable && ( + + )} + )} diff --git a/src/components/WorkerSearcher.js b/src/components/WorkerSearcher.js index 8441cd6..3a8cc40 100644 --- a/src/components/WorkerSearcher.js +++ b/src/components/WorkerSearcher.js @@ -36,6 +36,7 @@ import { } from '../constants'; import WorkerFilter from './WorkerFilter'; import { ACTION_TYPE } from '../reducer'; +import { useUploadWorkerContext } from '../context/UploadWorkerContext'; const WORKER_SEARCHER_ACTION_CONTRIBUTION_KEY = 'workerVoucher.WorkerSearcherAction.select'; @@ -72,6 +73,8 @@ function WorkerSearcher({ downloadWorkers, fetchWorkers: fetchWorkersAction, cle const [workerToDelete, setWorkerToDelete] = useState(null); const [exportFileFormat, setExportFileFormat] = useState(EXPORT_FILE_FORMATS.csv); + const { validationSuccess, validationWarning } = useUploadWorkerContext(); + const exportConfiguration = { exportFields: ['chf_id', 'last_name', 'other_names'], additionalExportFields: { @@ -225,10 +228,10 @@ function WorkerSearcher({ downloadWorkers, fetchWorkers: fetchWorkersAction, cle }; useEffect(() => { - if (queryParams.length) { + if (queryParams.length && (economicUnit || validationSuccess || validationWarning)) { fetchWorkers(queryParams); } - }, [economicUnit, queryParams]); + }, [economicUnit, queryParams, validationSuccess, validationWarning]); useEffect(() => { if (prevSubmittingMutationRef.current && !submittingMutation) { diff --git a/src/constants.js b/src/constants.js index d3f5620..f268cac 100644 --- a/src/constants.js +++ b/src/constants.js @@ -3,7 +3,7 @@ export const EMPLOYER_RIGHT_SEARCH = 203000; export const VOUCHER_PRICE_MANAGEMENT_RIGHT = 205001; export const INSPECTOR_RIGHT = 204005; export const ADMIN_RIGHT = 204004; -export const RIGHT_WORKER_UPLOAD = 101101; +export const RIGHT_WORKER_UPLOAD = 101102; export const RIGHT_WORKER_SEARCH = 101101; export const RIGHT_WORKER_ADD = 101102; export const RIGHT_WORKER_EDIT = 101103; @@ -99,6 +99,9 @@ export const WORKER_IMPORT_PLANS = [ }, ]; +// There are 2 worker upload stages. Depending on the stage, the UI will show different fields/buttons. +// 1. FILE_UPLOAD: When the user uploads a file +// 2. WORKER_UPLOAD: When the user confirms the upload (after validation workers are uploaded) export const UPLOAD_STAGE = { FILE_UPLOAD: 'FILE_UPLOAD', WORKER_UPLOAD: 'WORKER_UPLOAD', diff --git a/src/context/UploadWorkerContext.js b/src/context/UploadWorkerContext.js index 00fce8b..2a8eecf 100644 --- a/src/context/UploadWorkerContext.js +++ b/src/context/UploadWorkerContext.js @@ -3,8 +3,10 @@ import React, { } from 'react'; import { useSelector } from 'react-redux'; -import { baseApiUrl, useTranslations } from '@openimis/fe-core'; -import { EMPTY_STRING, MODULE_NAME, UPLOAD_STAGE } from '../constants'; +import { baseApiUrl, useTranslations, openBlob } from '@openimis/fe-core'; +import { + EMPTY_OBJECT, EMPTY_STRING, MODULE_NAME, UPLOAD_STAGE, +} from '../constants'; const UploadWorkerContext = createContext(); @@ -18,6 +20,7 @@ export function UploadWorkerProvider({ children }) { const [validationError, setValidationError] = useState(EMPTY_STRING); const [validationSuccess, setValidationSuccess] = useState(EMPTY_STRING); const [validationWarning, setValidationWarning] = useState(EMPTY_STRING); + const [workersWithError, setWorkersWithError] = useState(EMPTY_OBJECT); const [uploadSummary, setUploadSummary] = useState({ affectedRows: 0, totalNumberOfRecordsInFile: 0, @@ -58,8 +61,24 @@ export function UploadWorkerProvider({ children }) { const data = await response.json(); - console.log(response); - console.log(data); + setUploadSummary({ + affectedRows: data.summary?.affected_rows || 0, + totalNumberOfRecordsInFile: data.summary?.total_number_of_records_in_file || 0, + skippedRows: data.summary?.skipped_items || 0, + }); + + if (!data.success) { + setValidationError(formatMessage('UploadWorkerModal.workerUploadError')); + return; + } + + if (!!data?.summary?.skipped_items && !!Object.keys(data?.error).length) { + setWorkersWithError(data.error); + setValidationWarning(formatMessage('UploadWorkerModal.workerUploadWarning')); + return; + } + + setValidationSuccess(formatMessage('UploadWorkerModal.workerUploadSuccess')); } catch (error) { setValidationError(formatMessage('UploadWorkerModal.workerUploadError')); } finally { @@ -68,6 +87,21 @@ export function UploadWorkerProvider({ children }) { } }; + const downloadWorkersWithError = async () => { + const baseUrl = new URL(`${window.location.origin}${baseApiUrl}/worker_voucher/download_worker_upload_file/`); + baseUrl.searchParams.append('economic_unit_code', economicUnit.code); + baseUrl.searchParams.append('filename', file.name); + + try { + const response = await fetch(baseUrl); + const blob = await response.blob(); + return openBlob(blob, `errors_${file.name}`, file.type); + } catch (error) { + // eslint-disable-next-line no-console + throw new Error(`[UPLOAD_WORKER_CONTEXT]: Upload failed. ${error}`); + } + }; + const onWorkersUpload = async () => { setUploadStage(UPLOAD_STAGE.WORKER_UPLOAD); @@ -81,6 +115,7 @@ export function UploadWorkerProvider({ children }) { setValidationSuccess(EMPTY_STRING); setValidationWarning(EMPTY_STRING); setUploadStage(UPLOAD_STAGE.FILE_UPLOAD); + setWorkersWithError(EMPTY_OBJECT); setUploadSummary({ affectedRows: 0, totalNumberOfRecordsInFile: 0, @@ -102,6 +137,7 @@ export function UploadWorkerProvider({ children }) { validationWarning, uploadSummary, uploadStage, + workersWithError, onWorkersUpload, setFile, setIsUploading, @@ -111,8 +147,19 @@ export function UploadWorkerProvider({ children }) { onFileUpload, setUploadSummary, resetFile, + downloadWorkersWithError, }), - [file, isUploading, uploadStage, uploadSummary, isUploaded, validationError, validationSuccess, validationWarning], + [ + file, + isUploading, + workersWithError, + uploadStage, + uploadSummary, + isUploaded, + validationError, + validationSuccess, + validationWarning, + ], ); return {children}; diff --git a/src/translations/en.json b/src/translations/en.json index 6ca0654..e7deb5f 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -156,11 +156,12 @@ "workerVoucher.UploadWorkerModal.affectedRows": "Affected Rows", "workerVoucher.UploadWorkerModal.totalNumberOfRecordsInFile": "Total Number of Records in File", "workerVoucher.UploadWorkerModal.skippedRows": "Skipped Rows", - "workerVoucher.UploadWorkerModal.fileRequired": "File is required", - "workerVoucher.UploadWorkerModal.fileSizeError": "File size should be less than {fileSize} MB", + "workerVoucher.UploadWorkerModal.fileRequired": "File is required. Please upload a file.", + "workerVoucher.UploadWorkerModal.fileSizeError": "File size should be less than {fileSize} MB. Please upload a smaller file.", "workerVoucher.UploadWorkerModal.workerUploadSuccess": "Workers have been successfully uploaded. You can close this dialog.", - "workerVoucher.UploadWorkerModal.workerUploadError": "Failed to upload workers. Please ensure that the file is in the correct format and try again.", + "workerVoucher.UploadWorkerModal.workerUploadError": "Failed to upload workers. Please ensure that the content of the file is correct and try again.", "workerVoucher.UploadWorkerModal.workerUploadWarning": "Some workers have been skipped. You can download the list of skipped workers by clicking the button below.", + "workerVoucher.UploadWorkerModal.downloadWorkersWithError": "Download Errors and close", "workerVoucher.WorkerSearcher.selection": "Selected {count} workers", "workerVoucher.WorkerSearcherSelectActions.delete": "Delete Selected Workers", "workerVoucher.WorkerSearcherSelectActions.dialog.title": "Are you sure you want to proceed?", From 060708c14ef0547b9d97da24cebef5a9d88eafdc Mon Sep 17 00:00:00 2001 From: olewandowski1 Date: Tue, 15 Oct 2024 15:04:22 +0200 Subject: [PATCH 3/5] OM-316: condition fix --- src/components/WorkerSearcher.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/WorkerSearcher.js b/src/components/WorkerSearcher.js index 3a8cc40..f288b44 100644 --- a/src/components/WorkerSearcher.js +++ b/src/components/WorkerSearcher.js @@ -228,7 +228,7 @@ function WorkerSearcher({ downloadWorkers, fetchWorkers: fetchWorkersAction, cle }; useEffect(() => { - if (queryParams.length && (economicUnit || validationSuccess || validationWarning)) { + if (queryParams.length) { fetchWorkers(queryParams); } }, [economicUnit, queryParams, validationSuccess, validationWarning]); From 003dd6b2b1a404944e35b419aae10278e8af7061 Mon Sep 17 00:00:00 2001 From: olewandowski1 Date: Tue, 15 Oct 2024 15:11:06 +0200 Subject: [PATCH 4/5] OM-316: add memo for urls --- src/context/UploadWorkerContext.js | 34 +++++++++++++++--------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/context/UploadWorkerContext.js b/src/context/UploadWorkerContext.js index 2a8eecf..b884af2 100644 --- a/src/context/UploadWorkerContext.js +++ b/src/context/UploadWorkerContext.js @@ -27,24 +27,28 @@ export function UploadWorkerProvider({ children }) { skippedRows: 0, }); + const uploadUrl = useMemo(() => { + const url = new URL(`${window.location.origin}${baseApiUrl}/worker_voucher/worker_upload/`); + url.searchParams.append('economic_unit_code', economicUnit.code); + return url; + }, [economicUnit.code]); + + const uploadErrorUrl = useMemo(() => { + const url = new URL(`${window.location.origin}${baseApiUrl}/worker_voucher/download_worker_upload_file/`); + url.searchParams.append('economic_unit_code', economicUnit.code); + url.searchParams.append('filename', file?.name); + return url; + }, [economicUnit.code, file]); + const onFileUpload = (uploadedFile) => { - try { - setFile(uploadedFile); - } catch (error) { - // eslint-disable-next-line no-console - console.log('[UPLOAD_WORKER_CONTEXT]: Error while uploading file', error); - } finally { - setIsUploading(false); - setIsUploaded(true); - } + setFile(uploadedFile); + setIsUploading(false); + setIsUploaded(true); }; const uploadWorkers = async () => { setIsUploading(true); - const uploadUrl = new URL(`${window.location.origin}${baseApiUrl}/worker_voucher/worker_upload/`); - uploadUrl.searchParams.append('economic_unit_code', economicUnit.code); - const formData = new FormData(); formData.append('file', file); @@ -88,12 +92,8 @@ export function UploadWorkerProvider({ children }) { }; const downloadWorkersWithError = async () => { - const baseUrl = new URL(`${window.location.origin}${baseApiUrl}/worker_voucher/download_worker_upload_file/`); - baseUrl.searchParams.append('economic_unit_code', economicUnit.code); - baseUrl.searchParams.append('filename', file.name); - try { - const response = await fetch(baseUrl); + const response = await fetch(uploadErrorUrl); const blob = await response.blob(); return openBlob(blob, `errors_${file.name}`, file.type); } catch (error) { From 0492df61921362da2758debd816dfea8733e7fd6 Mon Sep 17 00:00:00 2001 From: olewandowski1 Date: Tue, 15 Oct 2024 15:22:32 +0200 Subject: [PATCH 5/5] OM-316: get rid of useless refetch --- src/components/WorkerSearcher.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/WorkerSearcher.js b/src/components/WorkerSearcher.js index f288b44..18bf8d3 100644 --- a/src/components/WorkerSearcher.js +++ b/src/components/WorkerSearcher.js @@ -231,7 +231,13 @@ function WorkerSearcher({ downloadWorkers, fetchWorkers: fetchWorkersAction, cle if (queryParams.length) { fetchWorkers(queryParams); } - }, [economicUnit, queryParams, validationSuccess, validationWarning]); + }, [economicUnit, queryParams]); + + useEffect(() => { + if (validationWarning || validationSuccess) { + fetchWorkers(queryParams); + } + }, [validationSuccess, validationWarning]); useEffect(() => { if (prevSubmittingMutationRef.current && !submittingMutation) {