Skip to content

Commit

Permalink
OM-316: add worker upload (#73)
Browse files Browse the repository at this point in the history
* OM-316: upload init

* OM-316: add downloading file with errors, overall improvements

* OM-316: condition fix

* OM-316: add memo for urls

* OM-316: get rid of useless refetch
  • Loading branch information
olewandowski1 authored Oct 15, 2024
1 parent 1a4de3c commit 1d82ffa
Show file tree
Hide file tree
Showing 6 changed files with 555 additions and 18 deletions.
297 changes: 297 additions & 0 deletions src/components/UploadWorkerModal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,297 @@
import React, { useRef, useState } from 'react';
import { useSelector } from 'react-redux';

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, RIGHT_WORKER_UPLOAD, 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 rights = useSelector((state) => state.core?.user?.i_user?.rights ?? []);
const fileInputRef = useRef(null);
const [fileUploadError, setFileUploadError] = useState(null);
const maxSizeInMB = 10;

const {
file,
isUploading,
uploadStage,
validationError,
validationSuccess,
validationWarning,
onFileUpload,
isUploaded,
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) {
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;
};

if (!rights.includes(RIGHT_WORKER_UPLOAD)) {
return null;
}

return (
<Dialog open={open} onClose={onClose} disableBackdropClick>
<DialogTitle>{formatMessage('UploadWorkerModal.dialogTitle')}</DialogTitle>
<DialogContent className={classes.wrapper}>
{isFileUploadStage && (
<>
<input
type="file"
ref={fileInputRef}
className={classes.input}
disabled={isUploading}
onChange={handleFileChange}
accept=".csv, .xlsx"
/>

<ButtonBase className={classes.uploadBox} onClick={() => fileInputRef.current.click()}>
{isUploading && <CircularProgress />}

{!isUploading && fileUploadError && (
<Box className={`${classes.statusBox} ${classes.errorBox}`}>
<ErrorIcon />
<Typography variant="subtitle1">{fileUploadError}</Typography>
</Box>
)}

{!isUploading && !fileUploadError && !isUploaded && (
<>
<CloudUploadIcon fontSize="large" />
<Typography variant="subtitle1">{formatMessage('UploadWorkerModal.uploadPrompt')}</Typography>
<Typography variant="caption">{formatMessage('UploadWorkerModal.uploadCaption')}</Typography>
</>
)}

{!isUploading && !fileUploadError && isUploaded && file && (
<>
<DescriptionIcon fontSize="large" />
<Typography variant="subtitle1">
{formatMessageWithValues('UploadWorkerModal.uploadSuccess', {
button: <strong>{formatMessage('UploadWorkerModal.upload')}</strong>,
})}
</Typography>
<Typography variant="caption">
{formatMessageWithValues('UploadWorkerModal.uploadSuccessCaption', {
fileName: <strong>{file.name}</strong>,
fileSize: <strong>{(file.size / 1024 / 1024).toFixed(2)}</strong>,
})}
</Typography>
</>
)}
</ButtonBase>
</>
)}
{isWorkerUploadStage && (
<>
{!isUploading && (
<Grid item xs={12}>
{validationError && (
<Box className={`${classes.statusBox} ${classes.errorBox}`}>
<ErrorIcon />
<Typography variant="subtitle1">{validationError}</Typography>
</Box>
)}
{validationSuccess && (
<Box className={`${classes.statusBox} ${classes.successBox}`}>
<CheckCircleIcon />
<Typography variant="subtitle1">{validationSuccess}</Typography>
</Box>
)}
{validationWarning && (
<Box className={`${classes.statusBox} ${classes.warningBox}`}>
<WarningIcon />
<Typography variant="subtitle1">{validationWarning}</Typography>
</Box>
)}
</Grid>
)}

<div className={classes.uploadBox} style={{ gap: '16px' }}>
{isUploading ? (
<CircularProgress />
) : (
Object.entries(uploadSummary).map(([key, value]) => (
<TextField
key={key}
className={classes.summaryTextField}
label={formatMessage(`UploadWorkerModal.${key}`)}
value={value}
disabled
/>
))
)}
</div>
</>
)}
</DialogContent>
<DialogActions>
{isFileUploadStage && (
<>
<Button disabled={isUploading} onClick={onClose} className={classes.secondaryButton}>
{formatMessage('UploadWorkerModal.close')}
</Button>
{isUploaded && !fileUploadError && file && (
<>
<Button disabled={isUploading} onClick={handleDeleteFile} className={classes.errorButton}>
{formatMessage('UploadWorkerModal.clearFile')}
</Button>
<Button disabled={isUploading} onClick={onWorkersUpload} className={classes.primaryButton}>
{formatMessage('UploadWorkerModal.upload')}
</Button>
</>
)}
</>
)}
{isWorkerUploadStage && (
<>
<Button
onClick={() => {
resetFile();
onClose();
}}
disabled={isUploading}
className={classes.secondaryButton}
>
{formatMessage('UploadWorkerModal.close')}
</Button>
{isWorkerWithErrorAvailable && (
<Button
onClick={() => {
downloadWorkersWithError();
resetFile();
onClose();
}}
disabled={isUploading}
className={classes.errorButton}
>
{formatMessage('UploadWorkerModal.downloadWorkersWithError')}
</Button>
)}
</>
)}
</DialogActions>
</Dialog>
);
}

export default UploadWorkerModal;
9 changes: 9 additions & 0 deletions src/components/WorkerSearcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -230,6 +233,12 @@ function WorkerSearcher({ downloadWorkers, fetchWorkers: fetchWorkersAction, cle
}
}, [economicUnit, queryParams]);

useEffect(() => {
if (validationWarning || validationSuccess) {
fetchWorkers(queryParams);
}
}, [validationSuccess, validationWarning]);

useEffect(() => {
if (prevSubmittingMutationRef.current && !submittingMutation) {
dispatch(journalize(mutation));
Expand Down
9 changes: 9 additions & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = 101102;
export const RIGHT_WORKER_SEARCH = 101101;
export const RIGHT_WORKER_ADD = 101102;
export const RIGHT_WORKER_EDIT = 101103;
Expand Down Expand Up @@ -97,3 +98,11 @@ export const WORKER_IMPORT_PLANS = [
labelKey: 'workerVoucher.workerImport.previousDay',
},
];

// 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',
};
Loading

0 comments on commit 1d82ffa

Please sign in to comment.