Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OM-316: add worker upload #73

Merged
merged 6 commits into from
Oct 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading