Skip to content

Commit

Permalink
chore: manager can import templates with choices
Browse files Browse the repository at this point in the history
* chore: manager can import students from a new template

* chore: change the import Button into an import input file

* chore: manager has choices when importing students

* test: update tests

* style: run prettier
  • Loading branch information
NyAndoMayah authored Nov 24, 2023
1 parent a1043f5 commit 3f99ad3
Show file tree
Hide file tree
Showing 5 changed files with 238 additions and 60 deletions.
5 changes: 3 additions & 2 deletions src/__tests__/Manager.Multiple.Student.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import {
const importFile = (file, message) => {
const _path = 'cypress/fixtures'
const _mockFile = `${_path}/${file}`
cy.get('[data-testid="inputFile"]').selectFile(_mockFile, { force: true })
cy.get('#import-button').trigger('mousemove')
cy.get("[data-testid='inputFile']").selectFile(_mockFile, { force: true })

cy.contains('Confirmer').click()

Expand All @@ -40,7 +41,7 @@ describe(specTitle('Manager create multiple students'), () => {
cy.intercept('GET', `/students?page=1&page_size=10&last_name=${studentNameToBeCheckedMock}`, [student1Mock]).as('getStudentsByName')
cy.intercept('GET', `/students/${student1Mock.id}`, student1Mock)

cy.wait('@getWhoami')
//cy.wait('@getWhoami')
cy.get('[data-testid="students-menu"]').click()
cy.get('[href="#/students"]').click()
})
Expand Down
148 changes: 148 additions & 0 deletions src/operations/students/ImportNewTemplate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { useRef } from 'react'
import { Button, Checkbox, Dialog, DialogContent, DialogTitle, FormControlLabel, FormGroup, Grid, TextField, Typography } from '@mui/material'
import { ArrowBackIos, ArrowForwardIos } from '@material-ui/icons'
import { useWizardFormContext, WizardForm, WizardFormStep } from '@react-admin/ra-form-layout'
import { useNotify } from 'react-admin'
import { exporter, importHeaders, validateData } from '../utils'
import { Download, Upload } from '@mui/icons-material'
import { useForm } from 'react-hook-form'
import { EnableStatus } from '../../gen/haClient'
import studentProvider from '../../providers/studentProvider'
import ImportInputFile from '../utils/ImportInputFile'

const WizardToolbar = () => {
const { hasNextStep, hasPreviousStep, goToNextStep, goToPreviousStep } = useWizardFormContext()
return (
<div style={{ display: 'flex', justifyContent: 'space-around' }}>
{hasPreviousStep && (
<Button
onClick={() => goToPreviousStep()}
startIcon={<ArrowBackIos />}
style={{ display: 'flex', justifySelf: 'start' }}
variant='contained'
size='small'
>
Retour
</Button>
)}
{hasNextStep && (
<Button onClick={() => goToNextStep()} endIcon={<ArrowForwardIos />} style={{ display: 'flex', justifySelf: 'end' }} variant='contained' size='small'>
Suivant
</Button>
)}
</div>
)
}

const defaultHeaders = [
{ id: 1, label: 'Référence (ref)', value: 'ref', disabled: true },
{ id: 2, label: 'Prénoms (first_name)', value: 'first_name', disabled: true },
{ id: 3, label: 'Nom (last_name)', value: 'last_name', disabled: true },
{ id: 4, label: 'Mail (email)', value: 'email', disabled: true },
{ id: 5, label: "Date d'entrée à HEI (entrance_datetime)", value: 'entrance_datetime', disabled: true }
]
const optionalHeaders = [
{ id: 5, label: 'Sexe (sex)', value: 'sex', disabled: false },
{ id: 6, label: 'Date de naissance (birth_date)', value: 'birth_date', disabled: false },
{ id: 8, label: 'Adresse (address)', value: 'address', disabled: false },
{ id: 9, label: 'Numéro de téléphone (phone)', value: 'phone', disabled: false }
]
export const ImportNewTemplate = ({ isOpen, toggle }) => {
const notify = useNotify()
const inputRef = useRef(null)
const headers = defaultHeaders.map(header => header.value)
const { register, handleSubmit } = useForm({
fileName: 'template',
importHeaders: headers
})

const downloadFile = data => {
exporter([], [...headers, ...data?.importHeaders], data.fileName)
}

const addStudents = async (data, setData) => {
const importValidate = validateData(data)
if (importValidate.isValid) {
const modifiedData = data.map(element => {
element.entrance_datetime = new Date(element.entrance_datetime).toISOString()
element['status'] = EnableStatus.Enabled
})

setData(modifiedData)

await studentProvider
.saveOrUpdate(data)
.then(() => notify(`Importation effectuée avec succès`, { type: 'success', autoHideDuration: 1000 }))
.catch(() => notify(`L'importation n'a pas pu être effectuée`, { type: 'error', autoHideDuration: 1000 }))
} else {
notify(importValidate.message, { type: 'error', autoHideDuration: 1000 })
}
}

return (
<Dialog open={isOpen} onClose={toggle}>
<DialogTitle>Importer des étudiants en 3 étapes</DialogTitle>
<DialogContent>
<WizardForm toolbar={<WizardToolbar />}>
<WizardFormStep label='Fichier'>
<Grid container padding={3} rowSpacing={5}>
<Typography variant='body2'>
Vous pouvez choisir le nom de votre template. Par défaut, ce sera <strong>template.xlsx</strong>
</Typography>
<TextField label='Nom de votre template' variant='filled' defaultValue='template' fullWidth {...register('fileName', {})} />
</Grid>
</WizardFormStep>
<WizardFormStep label='En-têtes'>
<Grid container padding={3} rowSpacing={2}>
<Grid item>
<Typography variant='body2'>
Vous pouvez aussi personnaliser les en-têtes. Les en-têtes déjà cochées sont obligatoires.
<br />
Il ne vous restera plus qu'à appuyez sur le bouton <strong>TÉLÉCHARGER</strong> et modifier votre template.
</Typography>
</Grid>
<Grid item>
<FormGroup>
{defaultHeaders.map(head => (
<FormControlLabel
key={head.id}
control={<Checkbox size='small' disabled={head.disabled} checked {...register('importHeaders', {})} />}
value={head.value}
label={<Typography variant='body2'>{head.label}</Typography>}
/>
))}
{optionalHeaders.map(head => (
<FormControlLabel
key={head.id}
control={<Checkbox size='small' {...register('importHeaders', {})} />}
value={head.value}
label={<Typography variant='body2'>{head.label}</Typography>}
/>
))}
</FormGroup>
<Button onClick={handleSubmit(downloadFile)} startIcon={<Download />} size='medium' variant='outlined'>
Télécharger
</Button>
</Grid>
</Grid>
</WizardFormStep>
<WizardFormStep label='Importation'>
<Grid container padding={3} rowSpacing={5}>
<Grid item>
<Typography variant='body2'>
Ça y est? Après avoir vérifié votre fichier, appuyez sur le bouton <strong>IMPORTER</strong> et voilà ! 😁
</Typography>
</Grid>
<Grid item>
<Button size='medium' variant='outlined' onClick={() => inputRef.current.click()} startIcon={<Upload />}>
<ImportInputFile ref={inputRef} mutationRequest={addStudents} />
<span>Importer</span>
</Button>
</Grid>
</Grid>
</WizardFormStep>
</WizardForm>
</DialogContent>
</Dialog>
)
}
100 changes: 69 additions & 31 deletions src/operations/students/StudentList.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,34 @@
import { useRef, useState } from 'react'
import { CreateButton, Datagrid, EditButton, ExportButton, FilterButton, List, ShowButton, TextField, TopToolbar, useNotify } from 'react-admin'
import authProvider from '../../providers/authProvider'

import { EnableStatus, WhoamiRoleEnum } from '../../gen/haClient'

import { profileFilters } from '../profile'
import { exporter, exportHeaders, importHeaders, pageSize, PrevNextPagination, validateData } from '../utils'
import { UploadFile } from '@mui/icons-material'
import ImportListButton from '../utils/ImportListButton'
import { exporter, exportHeaders, pageSize, PrevNextPagination, validateData } from '../utils'
import { Upload } from '@mui/icons-material'
import { Button, MenuItem, Popover } from '@mui/material'
import { ImportNewTemplate } from './ImportNewTemplate'
import { useToggle } from '../../hooks/useToggle'
import ImportInputFile from '../utils/ImportInputFile'
import studentProvider from '../../providers/studentProvider'
import authProvider from '../../providers/authProvider'

const ListActions = () => {
const role = authProvider.getCachedRole()
const notify = useNotify()
const [isOpen, _setIsOpen, toggle] = useToggle()
const [isShown, _setIsShown, toggleMenu] = useToggle()
const [anchorEl, setAnchorEl] = useState(null)
const buttonRef = useRef(null)

const role = authProvider.getCachedRole()
const isManager = role === WhoamiRoleEnum.Manager

const openMenu = e => {
toggleMenu()
setAnchorEl(e.currentTarget)
}
const closeMenu = e => {
toggleMenu()
setAnchorEl(null)
}
const addStudents = async (data, setData) => {
const importValidate = validateData(data)

Expand All @@ -25,24 +40,45 @@ const ListActions = () => {

setData(modifiedData)

await studentProvider
.saveOrUpdate(data)
.then(() => notify(`Importation effectuée avec succès`, { type: 'success', autoHideDuration: 1000 }))
.catch(() => notify(`L'importation n'a pas pu être effectuée`, { type: 'error', autoHideDuration: 1000 }))
await studentProvider.saveOrUpdate(data).then(() => notify(`Importation effectuée avec succès`, { type: 'success', autoHideDuration: 1000 }))
} else {
notify(importValidate.message, { type: 'error', autoHideDuration: 1000 })
}
}

return (
<TopToolbar>
<FilterButton />
<CreateButton />
<ExportButton />
{isManager && (
<>
<ImportListButton mutationRequest={addStudents} />
<ExportButton exporter={() => exporter([], importHeaders, 'template_students')} label='TEMPLATE' startIcon={<UploadFile />} />
<Button id='import-button' startIcon={<Upload />} onMouseMove={openMenu} size='small'>
Importer
</Button>
<Popover
open={isShown}
onClose={closeMenu}
anchorEl={anchorEl}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left'
}}
transformOrigin={{
vertical: 'top',
horizontal: 'left'
}}
>
<MenuItem
data-testid='existantTemplate'
onClick={() => {
buttonRef.current.click()
}}
>
A partir d'un template existant <ImportInputFile mutationRequest={addStudents} ref={buttonRef} />
</MenuItem>
<MenuItem onClick={toggle}>A partir d'un nouveau template</MenuItem>
</Popover>
<ImportNewTemplate toggle={toggle} isOpen={isOpen} />
</>
)}
</TopToolbar>
Expand All @@ -51,26 +87,28 @@ const ListActions = () => {

const StudentList = () => {
const role = authProvider.getCachedRole()

const isManager = role === WhoamiRoleEnum.Manager

return (
<List
label='Étudiants'
hasCreate={isManager}
actions={<ListActions />}
filters={profileFilters}
exporter={list => exporter(list, exportHeaders, 'students')}
perPage={pageSize}
pagination={<PrevNextPagination />}
>
<Datagrid bulkActionButtons={false} rowClick='show'>
<TextField source='ref' label='Référence' />
<TextField source='first_name' label='Prénom·s' />
<TextField source='last_name' label='Nom·s' />
{isManager ? <EditButton /> : <ShowButton />}
</Datagrid>
</List>
<>
<List
label='Étudiants'
hasCreate={isManager}
actions={<ListActions />}
filters={profileFilters}
exporter={list => exporter(list, exportHeaders, 'students')}
perPage={pageSize}
pagination={<PrevNextPagination />}
>
<Datagrid bulkActionButtons={false} rowClick='show'>
<TextField source='ref' label='Référence' />
<TextField source='first_name' label='Prénom·s' />
<TextField source='last_name' label='Nom·s' />
{isManager ? <EditButton /> : <ShowButton />}
</Datagrid>
</List>
<ImportNewTemplate />
</>
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,36 +35,19 @@ const FileInput = forwardRef(function Input({ setIsSubmitted, setData }, ref) {
return <input data-testid='inputFile' type='file' ref={ref} style={{ display: 'none' }} onChange={processFile} accept={excelType} />
})

const ImportListButton = ({ mutationRequest }) => {
const ImportInputFile = forwardRef(function ImportInput({ mutationRequest, ...props }, ref) {
const [data, setData] = useState([])
const [open, setOpen, _toggle] = useToggle()

const isSmall = useMediaQuery('(max-width: 625px)')
const inputRef = useRef(null)

const handleClick = e => {
inputRef.current.click()
}
const notify = useNotify()

const makeRequest = () => {
setOpen(false)
mutationRequest(data, setData)
mutationRequest(data, setData).catch(() => notify(`L'importation n'a pas pu être effectuée`, { type: 'error', autoHideDuration: 1000 }))
}

const InputFile = () => <FileInput ref={inputRef} setData={setData} setIsSubmitted={setOpen} />
return (
<>
{isSmall ? (
<IconButton onClick={handleClick} color='primary'>
<Upload />
<InputFile />
</IconButton>
) : (
<Button size='small' onClick={handleClick} startIcon={<Upload />} sx={{ padding: 0.3 }}>
<InputFile />
<span>Importer</span>
</Button>
)}
<FileInput ref={ref} setData={setData} setIsSubmitted={setOpen} />
<Confirm
isOpen={open}
title={`Importer`}
Expand All @@ -74,5 +57,5 @@ const ImportListButton = ({ mutationRequest }) => {
/>
</>
)
}
export default ImportListButton
})
export default ImportInputFile
16 changes: 12 additions & 4 deletions src/operations/utils/importer.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,20 @@ export const minimalImportHeaders = ['ref', 'first_name', 'last_name', 'email',
export const validateData = data => {
let isValid = false
let message = ''

const isEqual = (data1, headers) => {
let isEq = true
data1.forEach(element => {
if (!headers.includes(element)) {
isEq = false
}
})
return isEq
}
if (data.length === 0) {
message = "Il n'y a pas d'élément à insérer"
} else if (Object.keys(data[0]).toString() === minimalImportHeaders.toString()) {
data.length <= 10 ? (isValid = true) : (message = 'Vous ne pouvez importer que 10 éléments à la fois.')
} else if (Object.keys(data[0]).toString() !== importHeaders.toString()) {
} else if (!isEqual(Object.keys(data[0]), minimalImportHeaders)) {
message = 'Veuillez re-vérifier les en-têtes de votre fichier'
} else if (!isEqual(Object.keys(data[0]), minimalImportHeaders)) {
message = 'Veuillez re-vérifier les en-têtes de votre fichier'
} else if (data.length >= 10) {
message = 'Vous ne pouvez importer que 10 éléments à la fois.'
Expand Down

0 comments on commit 3f99ad3

Please sign in to comment.