Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 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
26 changes: 26 additions & 0 deletions client/src/components/ButtonWithIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { SxProps, Theme } from '@mui/material/styles';

import Button from '@mui/material/Button';
import React from 'react';

interface ButtonWithIconProps {
text: string;
startIcon?: React.ReactNode;
onClick?: React.MouseEventHandler<HTMLButtonElement>;
isLoading?: boolean;
sx?: SxProps<Theme>;
type?: 'button' | 'submit' | 'reset';
}

export const ButtonWithIcon: React.FC<ButtonWithIconProps> = ({
text,
startIcon,
onClick,
sx,
type = 'button',
isLoading = false,
}) => (
<Button variant="contained" startIcon={startIcon} onClick={onClick} sx={sx} type={type} loading={isLoading}>
{text}
</Button>
);
13 changes: 13 additions & 0 deletions client/src/components/StyledErrorBox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Box } from '@mui/material';
import { ErrorBox } from './ErrorBox';

type StyledErrorBoxProps = {
message: string;
};
export const StyledErrorBox: React.FC<StyledErrorBoxProps> = ({ message }) => {
return (
<Box width="50%" margin="auto" marginTop="2rem" marginBottom="2rem">
<ErrorBox errorMessage={message} />
</Box>
);
};
1 change: 1 addition & 0 deletions client/src/data/types/Trainee.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ export interface Trainee {
readonly createdAt: Date;
readonly updatedAt: Date;
displayName: string;
profilePath: string;
imageURL?: string;
thumbnailURL?: string;
personalInfo: TraineePersonalInfo;
Expand Down
48 changes: 23 additions & 25 deletions client/src/features/cohorts/CohortsPage.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { ErrorBox, Loader } from '../../components';

import { ActionsCard } from './components/ActionsCard';
import Box from '@mui/material/Box';
import { Cohort } from '../cohorts/Cohorts';
import CohortAccordion from './components/CohortAccordion';
import Container from '@mui/material/Container';
import { Loader } from '../../components';
import Stack from '@mui/material/Stack';
import { StyledErrorBox } from '../../components/StyledErrorBox';
import Typography from '@mui/material/Typography';
import { useCohortsData } from './data/useCohortsData';
import { useEffect } from 'react';
Expand All @@ -17,34 +18,31 @@ const CohortsPage = () => {
document.title = 'Cohorts | Dojo';
}, []);

const { isLoading, isError, data, error, isFetching } = useCohortsData();

if (isLoading || isFetching) {
return <Loader />;
}

if (isError && error instanceof Error) {
return (
//fixme: make this reusable
<Box width="50%" margin="auto" marginTop="2rem">
return <ErrorBox errorMessage={error.message} />;
</Box>
);
}
const { isError, data, error, isPending } = useCohortsData();

return (
<Container fixed>
<Box p={2}>
<Typography variant="h4">Cohorts Overview</Typography>
<Box display="flex" alignItems="start" justifyContent="start" p={2}>
<Stack direction="column" spacing={2}>
{data?.sort(compareCohort).map((cohort: Cohort, index: number) => (
<div key={index}>
<CohortAccordion cohortInfo={cohort}></CohortAccordion>
</div>
))}
</Stack>
</Box>
<ActionsCard />
{isPending && (
<Box display="flex" justifyContent="center" alignItems="center" minHeight="200px">
<Loader />
</Box>
)}
{isError && (
<StyledErrorBox
message={error instanceof Error ? error.message : 'An unknown error occurred while fetching cohorts data.'}
/>
)}

<Stack direction="column" spacing={2}>
{data?.sort(compareCohort).map((cohort: Cohort, index: number) => (
<Box key={index}>
<CohortAccordion cohortInfo={cohort}></CohortAccordion>
</Box>
))}
</Stack>
</Box>
</Container>
);
Expand Down
23 changes: 23 additions & 0 deletions client/src/features/cohorts/components/ActionsCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Add } from '@mui/icons-material';
import { AddTraineeDialog } from '../../trainee-profile/create/AddTraineeDialog';
import { Box } from '@mui/material';
import { ButtonWithIcon } from '../../../components/ButtonWithIcon';
import { useState } from 'react';

export const ActionsCard = () => {
const [isAddTraineeDialogOpen, setIsAddTraineeDialogOpen] = useState(false);

const handleOpenAddTraineeDialog = () => {
setIsAddTraineeDialogOpen(true);
};

const handleCloseAddTraineeDialog = () => {
setIsAddTraineeDialogOpen(false);
};
return (
<Box sx={{ my: 2, paddingTop: 2, paddingBottom: 2, display: 'flex', justifyContent: 'flex-end', paddingRight: 2 }}>
<AddTraineeDialog isOpen={isAddTraineeDialogOpen} handleClose={handleCloseAddTraineeDialog} />
<ButtonWithIcon text="Add Trainee" startIcon={<Add />} onClick={handleOpenAddTraineeDialog} />
</Box>
);
};
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { createContext, useContext } from 'react';

import { SaveTraineeRequestData } from '../personal-info/data/useTraineeInfoData';
import { Trainee } from '../../../data/types/Trainee';
import { UpdateTraineeRequestData } from '../personal-info/data/useTraineeInfoData';

export type TraineeProfileContextType = {
traineeId: string;
Expand All @@ -12,7 +12,7 @@ export type TraineeProfileContextType = {
setIsEditMode: (isEditMode: boolean) => void;
isSavingProfile: boolean;
setIsSavingProfile: React.Dispatch<React.SetStateAction<boolean>>;
getTraineeInfoChanges: (trainee: Trainee) => SaveTraineeRequestData;
getTraineeInfoChanges: (trainee: Trainee) => UpdateTraineeRequestData;
};

export const TraineeProfileContext = createContext<TraineeProfileContextType>({
Expand All @@ -24,7 +24,7 @@ export const TraineeProfileContext = createContext<TraineeProfileContextType>({
setIsEditMode: () => {},
isSavingProfile: false,
setIsSavingProfile: () => {},
getTraineeInfoChanges: () => ({}) as SaveTraineeRequestData,
getTraineeInfoChanges: () => ({}) as UpdateTraineeRequestData,
});

export const useTraineeProfileContext = () => useContext(TraineeProfileContext);
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import {
TraineePersonalInfo,
} from '../../../data/types/Trainee';

import { SaveTraineeRequestData } from '../personal-info/data/useTraineeInfoData';
import { TraineeProfileContext } from './useTraineeProfileContext';
import { UpdateTraineeRequestData } from '../personal-info/data/useTraineeInfoData';

type TraineeInfoType = TraineePersonalInfo | TraineeContactInfo | TraineeEmploymentInfo | TraineeEducationInfo;

Expand All @@ -32,10 +32,10 @@ export const TraineeProfileProvider = ({

/**
* Function to get the changes made to the trainee's profile (every tab)
* @returns {SaveTraineeRequestData} - Object with the changes made to the trainee's profile.
* @returns {UpdateTraineeRequestData} - Object with the changes made to the trainee's profile.
* The object is structured as follows: { personalInfo, contactInfo, educationInfo, employmentInfo }
*/
const getTraineeInfoChanges = (): SaveTraineeRequestData => {
const getTraineeInfoChanges = (): UpdateTraineeRequestData => {
const personalInfo: Partial<TraineePersonalInfo> | null = getChangedFields(
originalTrainee.personalInfo,
trainee.personalInfo
Expand All @@ -53,7 +53,7 @@ export const TraineeProfileProvider = ({
trainee.educationInfo
);

const dataToSave: SaveTraineeRequestData = {};
const dataToSave: UpdateTraineeRequestData = {};

// add the changed fields to the dataToSave object if not null
if (personalInfo) dataToSave.personalInfo = personalInfo;
Expand Down
104 changes: 104 additions & 0 deletions client/src/features/trainee-profile/create/AddTraineeDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { Alert, Box, Dialog, SelectChangeEvent, Typography } from '@mui/material';
import { FormErrors, FormState, NewTraineeForm } from './components/NewTraineeForm';
import { JobPath, LearningStatus } from '../../../data/types/Trainee';

import { useCreateTraineeProfile } from './data/mutations';
import { useNavigate } from 'react-router-dom';
import { useState } from 'react';
import { validateForm } from './lib/formHelper';

interface AddTraineeDialogProps {
isOpen: boolean;
handleClose: () => void;
}
export const AddTraineeDialog: React.FC<AddTraineeDialogProps> = ({ isOpen, handleClose }) => {
const initialState = {
firstName: '',
lastName: '',
gender: null,
email: '',
cohort: 0,
learningStatus: LearningStatus.Studying,
jobPath: JobPath.NotGraduated,
};
const navigate = useNavigate();
const {
mutate: createTrainee,
isPending,
error: submitError,
} = useCreateTraineeProfile({
onSuccess: (profilePath: string) => onSuccess(profilePath),
});

const [errors, setErrors] = useState<FormErrors>({});

const [formState, setFormState] = useState<FormState>(initialState);
const onClose = () => {
setFormState(initialState);
setErrors({});
handleClose();
};

const onSuccess = (path: string) => {
onClose();
navigate(path);
};
const handleTextChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormState((prevState: FormState) => ({
...prevState,
[name]: value,
}));
};

const handleSelectChange = (event: SelectChangeEvent<string | number>) => {
const { name, value } = event.target;

setFormState((prevState: FormState) => ({
...prevState,
[name]: value,
}));
};

const handleSubmit: React.ComponentProps<'form'>['onSubmit'] = (event) => {
event.preventDefault();
setErrors({});
const errors = validateForm(formState);
setErrors(errors);

const hasErrors = Object.values(errors).some((error) => error != null);

if (hasErrors) {
return;
}

createTrainee(formState);
};

return (
<Dialog open={isOpen} onClose={handleClose} fullWidth maxWidth="sm">
<Box padding={5} sx={{ backgroundColor: 'background.paper' }}>
<Typography variant="h4" gutterBottom>
New trainee profile
</Typography>
<NewTraineeForm
isLoading={isPending}
formState={formState}
errors={errors}
handleChange={handleTextChange}
handleSelect={handleSelectChange}
handleSubmit={handleSubmit}
handleClose={onClose}
/>

{submitError && (
<Box paddingTop={2}>
<Alert severity="error">
An error occurred while creating the trainee profile: {submitError?.message && 'unknown'}
</Alert>
</Box>
)}
</Box>
</Dialog>
);
};
8 changes: 8 additions & 0 deletions client/src/features/trainee-profile/create/api/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { CreateTraineeRequestData } from './types';
import { Trainee } from '../../../../data/types/Trainee';
import axios from 'axios';

export const createTrainee = async (request: CreateTraineeRequestData): Promise<Trainee> => {
const { data } = await axios.post<Trainee>(`/api/trainees`, request);
return data;
};
13 changes: 13 additions & 0 deletions client/src/features/trainee-profile/create/api/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {
TraineeContactInfo,
TraineeEducationInfo,
TraineeEmploymentInfo,
TraineePersonalInfo,
} from '../../../../data/types/Trainee';

export type CreateTraineeRequestData = {
personalInfo: Pick<TraineePersonalInfo, 'firstName' | 'lastName' | 'gender'>;
contactInfo: Pick<TraineeContactInfo, 'email'>;
educationInfo: Pick<TraineeEducationInfo, 'startCohort' | 'currentCohort' | 'learningStatus'>;
employmentInfo: Pick<TraineeEmploymentInfo, 'jobPath'>;
};
Loading