Skip to content
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
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: 25 additions & 23 deletions client/src/features/cohorts/CohortsPage.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
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';
Expand All @@ -17,34 +18,35 @@ 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 && (
<Box width="50%" margin="auto" marginTop="2rem" marginBottom="2rem">
<ErrorBox
errorMessage={
error instanceof Error ? error.message : 'An unknown error occurred while fetching cohorts data.'
}
/>
</Box>
)}

<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
25 changes: 25 additions & 0 deletions client/src/features/cohorts/components/ActionsCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Box, Button } from '@mui/material';

import { Add } from '@mui/icons-material';
import { AddTraineeDialog } from '../../trainee-profile/create/AddTraineeDialog';
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} />
<Button variant="contained" startIcon={<Add />} onClick={handleOpenAddTraineeDialog}>
Add Trainee
</Button>
</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
101 changes: 101 additions & 0 deletions client/src/features/trainee-profile/create/AddTraineeDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
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 { validateAndCollectFormErrors } from './lib/formValidation';

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 | null>(null);

const [formState, setFormState] = useState<FormState>(initialState);
const onClose = () => {
setFormState(initialState);
setErrors(null);
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();
const errors = validateAndCollectFormErrors(formState);

if (errors) {
setErrors(errors);
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'>;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { Button, SelectChangeEvent, Stack } from '@mui/material';
import { Gender, JobPath, LearningStatus } from '../../../../data/types/Trainee';

import { GenderSelect } from '../../profile/components/GenderSelect';
import { JobPathSelect } from '../../profile/components/JobPathSelect';
import { LearningStatusSelect } from '../../profile/components/LearningStatusSelect';
import TextFieldWrapper from './TextFieldWrapper';

export type FormState = {
firstName: string;
lastName: string;
gender: Gender | null;
email: string;
cohort: number;
learningStatus: LearningStatus;
jobPath: JobPath;
};

export type FormErrors = {
firstName?: string;
lastName?: string;
gender?: string;
email?: string;
cohort?: string;
};

export const NewTraineeForm: React.FC<{
isLoading: boolean;
formState: FormState;
errors: FormErrors | null;
handleChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
handleClose: () => void;
handleSelect: (event: SelectChangeEvent<string | number>) => void;
handleSubmit: React.ComponentProps<'form'>['onSubmit'];
}> = ({ isLoading, formState, errors, handleChange, handleSelect, handleSubmit, handleClose }) => {
return (
<form onSubmit={handleSubmit}>
<Stack spacing={2} pt={2}>
<TextFieldWrapper
disabled={isLoading}
id="firstName"
name="firstName"
error={!!errors?.firstName}
helperText={errors?.firstName}
label="First name"
value={formState.firstName}
onChange={handleChange}
/>
<TextFieldWrapper
disabled={isLoading}
id="lastName"
name="lastName"
error={!!errors?.lastName}
helperText={errors?.lastName}
label="Last name"
value={formState.lastName}
onChange={handleChange}
/>
<GenderSelect
disabled={isLoading}
isEditing
value={formState.gender || undefined}
error={errors?.gender || ''}
onChange={handleSelect}
/>
<TextFieldWrapper
disabled={isLoading}
id="email"
name="email"
error={!!errors?.email}
helperText={errors?.email}
label="Email"
value={formState.email}
onChange={handleChange}
/>
<Stack direction="row" spacing={2} pb={2}>
<TextFieldWrapper
disabled={isLoading}
id="cohort"
name="cohort"
type="number"
error={!!errors?.cohort}
helperText={errors?.cohort}
label="Start cohort"
value={formState.cohort}
onChange={handleChange}
maxLength={3}
/>

<LearningStatusSelect
disabled={isLoading}
isEditing
value={formState.learningStatus}
onChange={handleSelect}
/>
<JobPathSelect disabled={isLoading} isEditing value={formState.jobPath} onChange={handleSelect} />
</Stack>
</Stack>
<Stack direction="row" spacing={2} justifyContent="flex-end" mt={2}>
<Button variant="outlined" color="secondary" disabled={isLoading} onClick={handleClose}>
Cancel
</Button>
<Button type="submit" variant="contained" color="primary" loading={isLoading} disabled={isLoading}>
Create
</Button>
</Stack>
</form>
);
};
Loading