From c78719cdf4b4ca566f808b26d566be7b2917c1d9 Mon Sep 17 00:00:00 2001 From: zalexa19 Date: Sun, 8 Feb 2026 21:46:02 +0100 Subject: [PATCH 01/17] created GenderSelect --- .../create/CreateTraineeDialog.tsx | 29 ++++++++++++++ .../personal-info/PersonalInfo.tsx | 20 +--------- .../profile/components/GenderSelect.tsx | 39 +++++++++++++++++++ .../trainee-profile/utils/stringHelper.ts | 5 +++ 4 files changed, 75 insertions(+), 18 deletions(-) create mode 100644 client/src/features/trainee-profile/create/CreateTraineeDialog.tsx create mode 100644 client/src/features/trainee-profile/profile/components/GenderSelect.tsx create mode 100644 client/src/features/trainee-profile/utils/stringHelper.ts diff --git a/client/src/features/trainee-profile/create/CreateTraineeDialog.tsx b/client/src/features/trainee-profile/create/CreateTraineeDialog.tsx new file mode 100644 index 00000000..eca14ae6 --- /dev/null +++ b/client/src/features/trainee-profile/create/CreateTraineeDialog.tsx @@ -0,0 +1,29 @@ +import { Box, Dialog, SelectChangeEvent, Typography } from '@mui/material'; +import { ChangeEventHandler, useState } from 'react'; + +import { Gender } from '../../../data/types/Trainee'; +import { GenderSelect } from '../profile/components/GenderSelect'; +import TextFieldWrapper from './components/TextFieldWrapper'; + +export const CreateTraineeDialog: React.FC = () => { + const [gender, setGender] = useState(null); + const onSelectGender = (event: SelectChangeEvent) => { + setGender(event.target.value as Gender); + }; + return ( + {}} fullWidth maxWidth="sm"> + + + Create Trainee Profile + + {/* Form fields for creating a trainee profile would go here */} + + + + + + + + + ); +}; diff --git a/client/src/features/trainee-profile/personal-info/PersonalInfo.tsx b/client/src/features/trainee-profile/personal-info/PersonalInfo.tsx index df8dc04d..62947761 100644 --- a/client/src/features/trainee-profile/personal-info/PersonalInfo.tsx +++ b/client/src/features/trainee-profile/personal-info/PersonalInfo.tsx @@ -10,6 +10,7 @@ import { Box, FormControl, InputLabel, MenuItem, Select, TextField } from '@mui/ import { createSelectChangeHandler, createTextChangeHandler } from '../utils/formHelper'; import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'; +import { GenderSelect } from '../profile/components/GenderSelect'; import { useTraineeProfileContext } from '../context/useTraineeProfileContext'; const NoIcon = () => null; @@ -88,24 +89,7 @@ const PersonalInfo = () => {
{/* Gender */} - - Gender - - + {/* Pronouns */} diff --git a/client/src/features/trainee-profile/profile/components/GenderSelect.tsx b/client/src/features/trainee-profile/profile/components/GenderSelect.tsx new file mode 100644 index 00000000..17a07da6 --- /dev/null +++ b/client/src/features/trainee-profile/profile/components/GenderSelect.tsx @@ -0,0 +1,39 @@ +import { FormControl, InputLabel, MenuItem, Select, SelectChangeEvent, TextFieldProps } from '@mui/material'; + +import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'; +import { Gender } from '../../../../data/types/Trainee'; +import { capitalize } from '../../utils/stringHelper'; + +type GenderSelectProps = { + isEditing: boolean; + gender?: Gender | null; + onChange: (event: SelectChangeEvent) => void; +}; + +const genderOptions: Gender[] = Object.values(Gender); + +export const GenderSelect: React.FC = ({ isEditing, gender = '', onChange = () => {} }) => { + const NoIcon = () => null; + + return ( + + Gender + + + ); +}; diff --git a/client/src/features/trainee-profile/utils/stringHelper.ts b/client/src/features/trainee-profile/utils/stringHelper.ts new file mode 100644 index 00000000..7abbb5ac --- /dev/null +++ b/client/src/features/trainee-profile/utils/stringHelper.ts @@ -0,0 +1,5 @@ +// String helper for capitalizing the first letter +export const capitalize = (str: string): string => { + if (!str) return ''; + return str.charAt(0).toUpperCase() + str.slice(1); +}; From 1036c2f88037968c3a2014b584cfcc5d764f3b64 Mon Sep 17 00:00:00 2001 From: zalexa19 Date: Mon, 9 Feb 2026 22:13:12 +0100 Subject: [PATCH 02/17] added a hook tot save the form state --- .../create/CreateTraineeDialog.tsx | 91 ++++++++++++++++--- .../create/components/TextFieldWrapper.tsx | 19 ++++ .../create/hooks/useCreateTraineeForm.ts | 80 ++++++++++++++++ .../profile/components/GenderSelect.tsx | 7 +- 4 files changed, 180 insertions(+), 17 deletions(-) create mode 100644 client/src/features/trainee-profile/create/components/TextFieldWrapper.tsx create mode 100644 client/src/features/trainee-profile/create/hooks/useCreateTraineeForm.ts diff --git a/client/src/features/trainee-profile/create/CreateTraineeDialog.tsx b/client/src/features/trainee-profile/create/CreateTraineeDialog.tsx index eca14ae6..89bfba83 100644 --- a/client/src/features/trainee-profile/create/CreateTraineeDialog.tsx +++ b/client/src/features/trainee-profile/create/CreateTraineeDialog.tsx @@ -1,28 +1,89 @@ -import { Box, Dialog, SelectChangeEvent, Typography } from '@mui/material'; -import { ChangeEventHandler, useState } from 'react'; +import { Background, Gender, JobPath, LearningStatus } from '../../../data/types/Trainee'; +import { Box, Button, Dialog, SelectChangeEvent, Stack, Typography } from '@mui/material'; -import { Gender } from '../../../data/types/Trainee'; import { GenderSelect } from '../profile/components/GenderSelect'; import TextFieldWrapper from './components/TextFieldWrapper'; +import { useCreateTraineeForm } from './hooks/useCreateTraineeForm'; + +let counter = 0; export const CreateTraineeDialog: React.FC = () => { - const [gender, setGender] = useState(null); - const onSelectGender = (event: SelectChangeEvent) => { - setGender(event.target.value as Gender); - }; + const { formState, setFormState, onSelectGender, handleChange, handleSubmit, errors } = useCreateTraineeForm(); + + console.log('reloading', counter++); return ( - {}} fullWidth maxWidth="sm"> - + {}} fullWidth maxWidth="sm"> + - Create Trainee Profile + New trainee profile {/* Form fields for creating a trainee profile would go here */} - - - - - +
+ + + + + + + + + + + + + +
); diff --git a/client/src/features/trainee-profile/create/components/TextFieldWrapper.tsx b/client/src/features/trainee-profile/create/components/TextFieldWrapper.tsx new file mode 100644 index 00000000..63c6dfcb --- /dev/null +++ b/client/src/features/trainee-profile/create/components/TextFieldWrapper.tsx @@ -0,0 +1,19 @@ +import { FormHelperText, Stack } from '@mui/material'; +import TextField, { TextFieldProps } from '@mui/material/TextField'; + +import React from 'react'; + +type TextFieldWrapperProps = TextFieldProps & { + // Add custom props here if needed +}; + +const TextFieldWrapper: React.FC = (props) => { + const { id, error, helperText } = props; + return ( + + + + ); +}; + +export default TextFieldWrapper; diff --git a/client/src/features/trainee-profile/create/hooks/useCreateTraineeForm.ts b/client/src/features/trainee-profile/create/hooks/useCreateTraineeForm.ts new file mode 100644 index 00000000..f824e89c --- /dev/null +++ b/client/src/features/trainee-profile/create/hooks/useCreateTraineeForm.ts @@ -0,0 +1,80 @@ +import { Gender, JobPath, LearningStatus } from '../../../../data/types/Trainee'; + +import { SelectChangeEvent } from '@mui/material'; +import { useState } from 'react'; + +type FormState = { + firstName: string; + lastName: string; + gender: Gender | null; + email: string; + startCohort?: number; + learningStatus: LearningStatus; + jobPath: JobPath; +}; + +type FormErrors = { + [K in keyof FormState]?: string; +}; +export const useCreateTraineeForm = () => { + // fixme: rename the formState to something more specific like createTraineeFormState + const [formState, setFormState] = useState({ + firstName: '', + lastName: '', + gender: null, + email: '', + startCohort: undefined, + learningStatus: LearningStatus.Studying, + jobPath: JobPath.NotGraduated, + }); + + const [errors, setErrors] = useState({}); + + const validate = () => { + const newErrors: FormErrors = {}; + if (!formState.firstName) newErrors.firstName = 'First name is required'; + if (!formState.lastName) newErrors.lastName = 'Last name is required'; + if (!formState.email) newErrors.email = 'Email is required'; + if (!formState.gender) newErrors.gender = 'Gender is required'; + if (!formState.startCohort) newErrors.startCohort = 'Start cohort is required'; + return newErrors; + }; + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormState((prevState) => ({ + ...prevState, + [name]: value, + })); + }; + + const onSelectGender = (event: SelectChangeEvent) => { + const genderValue = event.target.value as Gender; + setFormState((prevState) => ({ + ...prevState, + gender: genderValue, + })); + }; + const handleSubmit = (event: React.SubmitEventHandler) => { + // event.preventDefault(); + + setErrors({}); + const errors = validate(); + console.log(errors); + setErrors(errors); + if (Object.keys(errors).length > 0) { + // Handle validation errors, e.g., set error state or display messages + console.log('Validation errors:', errors); + return; + } + console.log(' no errors, submit, yay'); + }; + return { + formState, + onSelectGender, + handleChange, + setFormState, + handleSubmit, + errors, + }; +}; diff --git a/client/src/features/trainee-profile/profile/components/GenderSelect.tsx b/client/src/features/trainee-profile/profile/components/GenderSelect.tsx index 17a07da6..ab537e43 100644 --- a/client/src/features/trainee-profile/profile/components/GenderSelect.tsx +++ b/client/src/features/trainee-profile/profile/components/GenderSelect.tsx @@ -1,4 +1,4 @@ -import { FormControl, InputLabel, MenuItem, Select, SelectChangeEvent, TextFieldProps } from '@mui/material'; +import { FormControl, FormHelperText, InputLabel, MenuItem, Select, SelectChangeEvent } from '@mui/material'; import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'; import { Gender } from '../../../../data/types/Trainee'; @@ -7,18 +7,20 @@ import { capitalize } from '../../utils/stringHelper'; type GenderSelectProps = { isEditing: boolean; gender?: Gender | null; + error?: string; onChange: (event: SelectChangeEvent) => void; }; const genderOptions: Gender[] = Object.values(Gender); -export const GenderSelect: React.FC = ({ isEditing, gender = '', onChange = () => {} }) => { +export const GenderSelect: React.FC = ({ isEditing, gender = '', error, onChange = () => {} }) => { const NoIcon = () => null; return ( Gender ); From 391a97a79971f03e4afa6c27d35f6e20dc6217de Mon Sep 17 00:00:00 2001 From: zalexa19 Date: Tue, 10 Feb 2026 22:59:50 +0100 Subject: [PATCH 03/17] wip: saving data to the db --- .../create/CreateTraineeDialog.tsx | 61 ++++++++++++------- .../trainee-profile/create/api/api.ts | 8 +++ .../trainee-profile/create/api/types.ts | 13 ++++ .../create/components/TextFieldWrapper.tsx | 22 +++++-- .../trainee-profile/create/data/mutations.ts | 37 +++++++++++ .../create/hooks/useCreateTraineeForm.ts | 59 +++++++++--------- .../trainee-profile/create/lib/formHelper.ts | 35 +++++++++++ .../trainee-profile/personal-info/api/api.ts | 8 +-- .../personal-info/api/types.ts | 2 +- .../personal-info/data/useTraineeInfoData.tsx | 13 ++-- .../profile/components/DropdownSelect.tsx | 57 +++++++++++++++++ .../profile/components/GenderSelect.tsx | 11 +++- 12 files changed, 261 insertions(+), 65 deletions(-) create mode 100644 client/src/features/trainee-profile/create/api/api.ts create mode 100644 client/src/features/trainee-profile/create/api/types.ts create mode 100644 client/src/features/trainee-profile/create/data/mutations.ts create mode 100644 client/src/features/trainee-profile/create/lib/formHelper.ts create mode 100644 client/src/features/trainee-profile/profile/components/DropdownSelect.tsx diff --git a/client/src/features/trainee-profile/create/CreateTraineeDialog.tsx b/client/src/features/trainee-profile/create/CreateTraineeDialog.tsx index 89bfba83..4d617faf 100644 --- a/client/src/features/trainee-profile/create/CreateTraineeDialog.tsx +++ b/client/src/features/trainee-profile/create/CreateTraineeDialog.tsx @@ -1,19 +1,18 @@ import { Background, Gender, JobPath, LearningStatus } from '../../../data/types/Trainee'; import { Box, Button, Dialog, SelectChangeEvent, Stack, Typography } from '@mui/material'; +import { DropdownSelect } from '../profile/components/DropdownSelect'; import { GenderSelect } from '../profile/components/GenderSelect'; import TextFieldWrapper from './components/TextFieldWrapper'; import { useCreateTraineeForm } from './hooks/useCreateTraineeForm'; -let counter = 0; - export const CreateTraineeDialog: React.FC = () => { - const { formState, setFormState, onSelectGender, handleChange, handleSubmit, errors } = useCreateTraineeForm(); + const { formState, onSelectGender, handleChange, handleSelect, handleSubmit, errors, isPending } = + useCreateTraineeForm(); - console.log('reloading', counter++); return ( {}} fullWidth maxWidth="sm"> - + New trainee profile @@ -22,6 +21,7 @@ export const CreateTraineeDialog: React.FC = () => {
{ onChange={handleChange} /> { value={formState.lastName} onChange={handleChange} /> - + { onChange={handleChange} /> - - - - +
diff --git a/client/src/features/trainee-profile/create/api/api.ts b/client/src/features/trainee-profile/create/api/api.ts new file mode 100644 index 00000000..9f562818 --- /dev/null +++ b/client/src/features/trainee-profile/create/api/api.ts @@ -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 => { + const { data } = await axios.post(`/api/trainees`, request); + return data; +}; diff --git a/client/src/features/trainee-profile/create/api/types.ts b/client/src/features/trainee-profile/create/api/types.ts new file mode 100644 index 00000000..10923508 --- /dev/null +++ b/client/src/features/trainee-profile/create/api/types.ts @@ -0,0 +1,13 @@ +import { + TraineeContactInfo, + TraineeEducationInfo, + TraineeEmploymentInfo, + TraineePersonalInfo, +} from '../../../../data/types/Trainee'; + +export type CreateTraineeRequestData = { + personalInfo: Pick; + contactInfo: Pick; + educationInfo: Pick; + employmentInfo: Pick; +}; diff --git a/client/src/features/trainee-profile/create/components/TextFieldWrapper.tsx b/client/src/features/trainee-profile/create/components/TextFieldWrapper.tsx index 63c6dfcb..d85b6b6f 100644 --- a/client/src/features/trainee-profile/create/components/TextFieldWrapper.tsx +++ b/client/src/features/trainee-profile/create/components/TextFieldWrapper.tsx @@ -1,17 +1,31 @@ -import { FormHelperText, Stack } from '@mui/material'; import TextField, { TextFieldProps } from '@mui/material/TextField'; import React from 'react'; +import { Stack } from '@mui/material'; type TextFieldWrapperProps = TextFieldProps & { - // Add custom props here if needed + maxLength?: number; }; const TextFieldWrapper: React.FC = (props) => { - const { id, error, helperText } = props; + const handleChange = (e: React.ChangeEvent) => { + const { value } = e.target; + // maxLength is ignored for number type, so we need to check the length of the value manually + if (props.maxLength && props.type === 'number' && value.length > props.maxLength) { + return; + } + + props.onChange?.(e); + }; return ( - + ); }; diff --git a/client/src/features/trainee-profile/create/data/mutations.ts b/client/src/features/trainee-profile/create/data/mutations.ts new file mode 100644 index 00000000..a63e162b --- /dev/null +++ b/client/src/features/trainee-profile/create/data/mutations.ts @@ -0,0 +1,37 @@ +import { CreateTraineeRequestData } from '../api/types'; +import { FormState } from '../hooks/useCreateTraineeForm'; +import { Trainee } from '../../../../data/types/Trainee'; +import { createTrainee } from '../api/api'; +import { useMutation } from '@tanstack/react-query'; + +const mapFormStateToRequestData = (formState: FormState): CreateTraineeRequestData => { + return { + personalInfo: { + firstName: formState.firstName, + lastName: formState.lastName, + gender: formState.gender!, + }, + contactInfo: { + email: formState.email, + }, + educationInfo: { + startCohort: Number(formState.cohort), + currentCohort: Number(formState.cohort), + learningStatus: formState.learningStatus, + }, + employmentInfo: { + jobPath: formState.jobPath, + }, + }; +}; + +export const useCreateTraineeProfile = () => { + return useMutation({ + mutationFn: async (form: FormState) => createTrainee(mapFormStateToRequestData(form)), + onError: (error) => { + // TODO: global error handling? + console.error('Error creating trainee profile:', error); + }, + onSuccess: (data: Trainee) => data.id, + }); +}; diff --git a/client/src/features/trainee-profile/create/hooks/useCreateTraineeForm.ts b/client/src/features/trainee-profile/create/hooks/useCreateTraineeForm.ts index f824e89c..3c8ee3ce 100644 --- a/client/src/features/trainee-profile/create/hooks/useCreateTraineeForm.ts +++ b/client/src/features/trainee-profile/create/hooks/useCreateTraineeForm.ts @@ -1,45 +1,38 @@ import { Gender, JobPath, LearningStatus } from '../../../../data/types/Trainee'; +import { SubmitEventHandler, useState } from 'react'; import { SelectChangeEvent } from '@mui/material'; -import { useState } from 'react'; +import { useCreateTraineeProfile } from '../data/mutations'; +import { validateForm } from '../lib/formHelper'; -type FormState = { +export type FormState = { firstName: string; lastName: string; gender: Gender | null; email: string; - startCohort?: number; + cohort: number; learningStatus: LearningStatus; jobPath: JobPath; }; -type FormErrors = { - [K in keyof FormState]?: string; +export type FormErrors = { + [K in keyof FormState]?: string | null; }; export const useCreateTraineeForm = () => { + const { mutate: createTrainee, isPending, data } = useCreateTraineeProfile(); // fixme: rename the formState to something more specific like createTraineeFormState const [formState, setFormState] = useState({ firstName: '', lastName: '', gender: null, email: '', - startCohort: undefined, + cohort: 0, learningStatus: LearningStatus.Studying, jobPath: JobPath.NotGraduated, }); const [errors, setErrors] = useState({}); - const validate = () => { - const newErrors: FormErrors = {}; - if (!formState.firstName) newErrors.firstName = 'First name is required'; - if (!formState.lastName) newErrors.lastName = 'Last name is required'; - if (!formState.email) newErrors.email = 'Email is required'; - if (!formState.gender) newErrors.gender = 'Gender is required'; - if (!formState.startCohort) newErrors.startCohort = 'Start cohort is required'; - return newErrors; - }; - const handleChange = (e: React.ChangeEvent) => { const { name, value } = e.target; setFormState((prevState) => ({ @@ -48,33 +41,43 @@ export const useCreateTraineeForm = () => { })); }; - const onSelectGender = (event: SelectChangeEvent) => { - const genderValue = event.target.value as Gender; + const handleSelect = (event: SelectChangeEvent) => { + const { name, value } = event.target; + setFormState((prevState) => ({ ...prevState, - gender: genderValue, + [name]: value, })); }; - const handleSubmit = (event: React.SubmitEventHandler) => { - // event.preventDefault(); + // TODO: move this to the component + const handleSubmit: SubmitEventHandler = (event) => { + event.preventDefault(); setErrors({}); - const errors = validate(); - console.log(errors); + const errors = validateForm(formState); setErrors(errors); - if (Object.keys(errors).length > 0) { - // Handle validation errors, e.g., set error state or display messages - console.log('Validation errors:', errors); + + const hasErrors = Object.values(errors).some((error) => error != null); + + if (hasErrors) { + console.log('form has errors, not submitting', errors); return; } - console.log(' no errors, submit, yay'); + + createTrainee(formState); + // TODO: Navigate to the profile of the new trainee + //TODO: handle modal opening and closing + // TODO: where to put the button?? + // TODO: test updating existing profile }; return { formState, - onSelectGender, + onSelectGender: handleSelect, handleChange, + handleSelect, setFormState, handleSubmit, errors, + isPending, }; }; diff --git a/client/src/features/trainee-profile/create/lib/formHelper.ts b/client/src/features/trainee-profile/create/lib/formHelper.ts new file mode 100644 index 00000000..1013f2a3 --- /dev/null +++ b/client/src/features/trainee-profile/create/lib/formHelper.ts @@ -0,0 +1,35 @@ +import { FormErrors, FormState } from '../hooks/useCreateTraineeForm'; + +const FIELD_REQUIRED_ERROR = 'This field is required'; + +const validateName = (name: string) => { + if (!name) return FIELD_REQUIRED_ERROR; + if (name.length < 2) return 'Name must be at least 2 characters'; + return null; +}; + +const validateEmail = (email: string) => { + if (!email) return FIELD_REQUIRED_ERROR; + + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(email)) return 'Email must be of format name@domain.com'; + return null; +}; + +const validateCohort = (cohort: number | undefined) => { + if (cohort === undefined) return FIELD_REQUIRED_ERROR; + if (cohort < 0) return 'Cohort must be a positive number'; + return null; +}; + +export const validateForm = (formState: FormState): FormErrors => { + const errors: FormErrors = { + firstName: validateName(formState.firstName), + lastName: validateName(formState.lastName), + gender: formState.gender ? null : FIELD_REQUIRED_ERROR, + email: validateEmail(formState.email), + cohort: validateCohort(formState.cohort), + }; + + return errors; +}; diff --git a/client/src/features/trainee-profile/personal-info/api/api.ts b/client/src/features/trainee-profile/personal-info/api/api.ts index 801dbb8e..254c25f4 100644 --- a/client/src/features/trainee-profile/personal-info/api/api.ts +++ b/client/src/features/trainee-profile/personal-info/api/api.ts @@ -1,13 +1,13 @@ -import axios from 'axios'; import { Trainee } from '../../../../data/types/Trainee'; -import { SaveTraineeRequestData } from './types'; +import { UpdateTraineeRequestData } from './types'; +import axios from 'axios'; -export const getTraineeInfo = async (traineeId: string) => { +export const getTrainee = async (traineeId: string) => { const { data } = await axios.get(`/api/trainees/${traineeId}`); return data; }; -export const saveTraineeInfo = async (traineeId: string, dataToSave: SaveTraineeRequestData) => { +export const updateTrainee = async (traineeId: string, dataToSave: UpdateTraineeRequestData) => { const { data } = await axios.patch(`/api/trainees/${traineeId}`, dataToSave); return data; }; diff --git a/client/src/features/trainee-profile/personal-info/api/types.ts b/client/src/features/trainee-profile/personal-info/api/types.ts index d568677a..b2271ab7 100644 --- a/client/src/features/trainee-profile/personal-info/api/types.ts +++ b/client/src/features/trainee-profile/personal-info/api/types.ts @@ -5,7 +5,7 @@ import { TraineePersonalInfo, } from '../../../../data/types/Trainee'; -export interface SaveTraineeRequestData { +export interface UpdateTraineeRequestData { personalInfo?: Partial; contactInfo?: Partial; educationInfo?: Partial; diff --git a/client/src/features/trainee-profile/personal-info/data/useTraineeInfoData.tsx b/client/src/features/trainee-profile/personal-info/data/useTraineeInfoData.tsx index dfcba073..db6cdde8 100644 --- a/client/src/features/trainee-profile/personal-info/data/useTraineeInfoData.tsx +++ b/client/src/features/trainee-profile/personal-info/data/useTraineeInfoData.tsx @@ -1,7 +1,8 @@ -import { Trainee } from '../../../../data/types/Trainee'; +import { getTrainee, updateTrainee } from '../api/api'; import { useMutation, useQuery } from '@tanstack/react-query'; -import { getTraineeInfo, saveTraineeInfo } from '../api/api'; -import { SaveTraineeRequestData } from '../api/types'; + +import { Trainee } from '../../../../data/types/Trainee'; +import { UpdateTraineeRequestData } from '../api/types'; /** * A React Query hook that fetches trainee information data form api. @@ -11,7 +12,7 @@ import { SaveTraineeRequestData } from '../api/types'; export const useTraineeInfoData = (traineeId: string) => { return useQuery({ queryKey: ['traineeInfo', traineeId], - queryFn: () => getTraineeInfo(traineeId), + queryFn: () => getTrainee(traineeId), enabled: !!traineeId, //Added because it keeps rendering refetchOnMount: false, // Prevent refetching on component mount @@ -27,8 +28,8 @@ export const useTraineeInfoData = (traineeId: string) => { */ export const useSaveTraineeInfo = (traineeId: string) => { return useMutation({ - mutationFn: (dataToSave: SaveTraineeRequestData) => saveTraineeInfo(traineeId, dataToSave), + mutationFn: (dataToSave: UpdateTraineeRequestData) => updateTrainee(traineeId, dataToSave), }); }; -export type { SaveTraineeRequestData }; +export type { UpdateTraineeRequestData }; diff --git a/client/src/features/trainee-profile/profile/components/DropdownSelect.tsx b/client/src/features/trainee-profile/profile/components/DropdownSelect.tsx new file mode 100644 index 00000000..65769e4a --- /dev/null +++ b/client/src/features/trainee-profile/profile/components/DropdownSelect.tsx @@ -0,0 +1,57 @@ +import { FormControl, FormHelperText, InputLabel, MenuItem, Select, SelectChangeEvent } from '@mui/material'; + +import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'; +import { capitalize } from '../../utils/stringHelper'; + +type CustomSelectProps = { + inputLabel: string; + disabled?: boolean; + id: string; + label: string; + name: string; + value?: string | number; //currently selected value + options: string[]; // array of values to show in the dropdown + isEditing?: boolean; + error?: string; + onChange: (event: SelectChangeEvent) => void; +}; + +export const DropdownSelect = ({ + disabled = false, + inputLabel, + id, + label, + name, + value = '', + options, + isEditing = false, + error, + onChange = () => {}, +}: CustomSelectProps) => { + const NoIcon = () => null; + + return ( + + {inputLabel} + + + ); +}; diff --git a/client/src/features/trainee-profile/profile/components/GenderSelect.tsx b/client/src/features/trainee-profile/profile/components/GenderSelect.tsx index ab537e43..4a6e6e8d 100644 --- a/client/src/features/trainee-profile/profile/components/GenderSelect.tsx +++ b/client/src/features/trainee-profile/profile/components/GenderSelect.tsx @@ -5,6 +5,7 @@ import { Gender } from '../../../../data/types/Trainee'; import { capitalize } from '../../utils/stringHelper'; type GenderSelectProps = { + disabled?: boolean; isEditing: boolean; gender?: Gender | null; error?: string; @@ -13,13 +14,21 @@ type GenderSelectProps = { const genderOptions: Gender[] = Object.values(Gender); -export const GenderSelect: React.FC = ({ isEditing, gender = '', error, onChange = () => {} }) => { +// TODO: User the generic select +export const GenderSelect: React.FC = ({ + disabled = false, + isEditing, + gender = '', + error, + onChange = () => {}, +}) => { const NoIcon = () => null; return ( Gender - {genderOptions.map((option) => ( - - {capitalize(option)} - - ))} - {error} - - + ); }; diff --git a/client/src/features/trainee-profile/utils/stringHelper.ts b/client/src/features/trainee-profile/utils/stringHelper.ts deleted file mode 100644 index 7abbb5ac..00000000 --- a/client/src/features/trainee-profile/utils/stringHelper.ts +++ /dev/null @@ -1,5 +0,0 @@ -// String helper for capitalizing the first letter -export const capitalize = (str: string): string => { - if (!str) return ''; - return str.charAt(0).toUpperCase() + str.slice(1); -}; From 733fb8ce7d849630f7103507a7a7e2cc2f129dd5 Mon Sep 17 00:00:00 2001 From: zalexa19 Date: Wed, 11 Feb 2026 15:16:30 +0100 Subject: [PATCH 07/17] reusing the new selectors --- .../create/CreateTraineeDialog.tsx | 2 +- .../create/components/NewTraineeForm.tsx | 18 +++-------- .../education/EducationInfo.tsx | 3 ++ .../employment/EmploymentInfo.tsx | 25 ++------------- .../profile/components/DropdownSelect.tsx | 12 ++++--- .../profile/components/GenderSelect.tsx | 7 +++-- .../profile/components/JobPathSelect.tsx | 24 ++++++++++++++ .../components/LearningStatusSelect.tsx | 31 +++++++++++++++++++ 8 files changed, 79 insertions(+), 43 deletions(-) create mode 100644 client/src/features/trainee-profile/profile/components/JobPathSelect.tsx create mode 100644 client/src/features/trainee-profile/profile/components/LearningStatusSelect.tsx diff --git a/client/src/features/trainee-profile/create/CreateTraineeDialog.tsx b/client/src/features/trainee-profile/create/CreateTraineeDialog.tsx index 5d3ce4c5..0cbceb04 100644 --- a/client/src/features/trainee-profile/create/CreateTraineeDialog.tsx +++ b/client/src/features/trainee-profile/create/CreateTraineeDialog.tsx @@ -1,4 +1,4 @@ -import { Alert, Box, Button, Dialog, SelectChangeEvent, Stack, Typography } from '@mui/material'; +import { Alert, Box, Dialog, SelectChangeEvent, Typography } from '@mui/material'; import { FormErrors, FormState, NewTraineeForm } from './components/NewTraineeForm'; import { JobPath, LearningStatus } from '../../../data/types/Trainee'; import { SubmitEventHandler, useState } from 'react'; diff --git a/client/src/features/trainee-profile/create/components/NewTraineeForm.tsx b/client/src/features/trainee-profile/create/components/NewTraineeForm.tsx index a1d9f2e5..ef1516b0 100644 --- a/client/src/features/trainee-profile/create/components/NewTraineeForm.tsx +++ b/client/src/features/trainee-profile/create/components/NewTraineeForm.tsx @@ -1,8 +1,9 @@ import { Button, SelectChangeEvent, Stack } from '@mui/material'; import { Gender, JobPath, LearningStatus } from '../../../../data/types/Trainee'; -import { DropdownSelect } from '../../profile/components/DropdownSelect'; import { GenderSelect } from '../../profile/components/GenderSelect'; +import { JobPathSelect } from '../../profile/components/JobPathSelect'; +import { LearningStatusSelect } from '../../profile/components/LearningStatusSelect'; import { SubmitEventHandler } from 'react'; import TextFieldWrapper from './TextFieldWrapper'; @@ -81,25 +82,16 @@ export const NewTraineeForm: React.FC<{ onChange={handleChange} maxLength={3} /> - - {
{/* Learning status */} + + Learning status - Not graduated - Searching - Internship - Tech job - Non-tech job - Not searching - Other studies - No longer helping - - + {/* CV */} diff --git a/client/src/features/trainee-profile/profile/components/DropdownSelect.tsx b/client/src/features/trainee-profile/profile/components/DropdownSelect.tsx index 39bf4071..dd416c91 100644 --- a/client/src/features/trainee-profile/profile/components/DropdownSelect.tsx +++ b/client/src/features/trainee-profile/profile/components/DropdownSelect.tsx @@ -9,12 +9,16 @@ type CustomSelectProps = { label: string; name: string; value?: string; //currently selected value - options: string[]; // array of values to show in the dropdown + options: MenuItemType[]; // array of values to show in the dropdown isEditing?: boolean; error?: string; onChange: (event: SelectChangeEvent) => void; }; +export type MenuItemType = { + label: string; //To be displayed to the user + value: string; +}; export const DropdownSelect = ({ disabled = false, inputLabel, @@ -44,9 +48,9 @@ export const DropdownSelect = ({ startAdornment=" " onChange={onChange} > - {options.map((option) => ( - - {option} + {options.map((option: MenuItemType) => ( + + {option.label} ))} {error} diff --git a/client/src/features/trainee-profile/profile/components/GenderSelect.tsx b/client/src/features/trainee-profile/profile/components/GenderSelect.tsx index 2aa8c2ad..d5b96466 100644 --- a/client/src/features/trainee-profile/profile/components/GenderSelect.tsx +++ b/client/src/features/trainee-profile/profile/components/GenderSelect.tsx @@ -3,6 +3,11 @@ import { Gender } from '../../../../data/types/Trainee'; import { SelectChangeEvent } from '@mui/material'; import { formatTextToFriendly } from '../../utils/formHelper'; +const genderOptions = Object.values(Gender).map((gender) => ({ + label: formatTextToFriendly(gender), + value: gender, +})); + type GenderSelectProps = { disabled?: boolean; isEditing: boolean; @@ -11,8 +16,6 @@ type GenderSelectProps = { onChange: (event: SelectChangeEvent) => void; }; -const genderOptions = Object.values(Gender).map((gender) => formatTextToFriendly(gender)); - export const GenderSelect: React.FC = (props) => { return ( diff --git a/client/src/features/trainee-profile/profile/components/JobPathSelect.tsx b/client/src/features/trainee-profile/profile/components/JobPathSelect.tsx new file mode 100644 index 00000000..ebf675f5 --- /dev/null +++ b/client/src/features/trainee-profile/profile/components/JobPathSelect.tsx @@ -0,0 +1,24 @@ +import { DropdownSelect } from './DropdownSelect'; +import { JobPath } from '../../../../data/types/Trainee'; +import { SelectChangeEvent } from '@mui/material'; +import { formatTextToFriendly } from '../../utils/formHelper'; + +const options = Object.values(JobPath).map((status) => ({ + label: formatTextToFriendly(status), + value: status, +})); + +type JobPathSelectProps = { + initialValue?: string; + disabled?: boolean; + isEditing: boolean; + value?: string; + error?: string; + onChange: (event: SelectChangeEvent) => void; +}; + +export const JobPathSelect: React.FC = (props) => { + return ( + + ); +}; diff --git a/client/src/features/trainee-profile/profile/components/LearningStatusSelect.tsx b/client/src/features/trainee-profile/profile/components/LearningStatusSelect.tsx new file mode 100644 index 00000000..8692090b --- /dev/null +++ b/client/src/features/trainee-profile/profile/components/LearningStatusSelect.tsx @@ -0,0 +1,31 @@ +import { DropdownSelect } from './DropdownSelect'; +import { LearningStatus } from '../../../../data/types/Trainee'; +import { SelectChangeEvent } from '@mui/material'; +import { formatTextToFriendly } from '../../utils/formHelper'; + +const options = Object.values(LearningStatus).map((status) => ({ + label: formatTextToFriendly(status), + value: status, +})); + +type LearningStatusSelectProps = { + initialValue?: string; + disabled?: boolean; + isEditing: boolean; + value?: string; + error?: string; + onChange: (event: SelectChangeEvent) => void; +}; + +export const LearningStatusSelect: React.FC = (props) => { + return ( + + ); +}; From 5da1941369a68decb8ac55f5f561939f012e8d50 Mon Sep 17 00:00:00 2001 From: zalexa19 Date: Wed, 11 Feb 2026 15:19:12 +0100 Subject: [PATCH 08/17] renamed the new dialog --- client/src/features/cohorts/components/ActionsCard.tsx | 4 ++-- .../{CreateTraineeDialog.tsx => AddTraineeDialog.tsx} | 6 ++---- 2 files changed, 4 insertions(+), 6 deletions(-) rename client/src/features/trainee-profile/create/{CreateTraineeDialog.tsx => AddTraineeDialog.tsx} (94%) diff --git a/client/src/features/cohorts/components/ActionsCard.tsx b/client/src/features/cohorts/components/ActionsCard.tsx index 5ee10e60..de6cc9ef 100644 --- a/client/src/features/cohorts/components/ActionsCard.tsx +++ b/client/src/features/cohorts/components/ActionsCard.tsx @@ -1,7 +1,7 @@ import { Add } from '@mui/icons-material'; +import { AddTraineeDialog } from '../../trainee-profile/create/CreateTraineeDialog'; import { ButtonWithIcon } from '../../../components/ButtonWithIcon'; import { Card } from '@mui/material'; -import { CreateTraineeDialog } from '../../trainee-profile/create/CreateTraineeDialog'; import { useState } from 'react'; export const ActionsCard = () => { @@ -19,7 +19,7 @@ export const ActionsCard = () => { variant="outlined" sx={{ my: 2, paddingTop: 2, paddingBottom: 2, display: 'flex', justifyContent: 'flex-end', paddingRight: 2 }} > - void; } -export const CreateTraineeDialog: React.FC = ({ isOpen, handleClose }) => { +export const AddTraineeDialog: React.FC = ({ isOpen, handleClose }) => { const navigate = useNavigate(); const { mutate: createTrainee, From 79d368c5f525cb6514639923b49e3c3559369bf0 Mon Sep 17 00:00:00 2001 From: zalexa19 Date: Wed, 11 Feb 2026 15:19:28 +0100 Subject: [PATCH 09/17] wip: forgot to commit file --- client/src/features/cohorts/components/ActionsCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/features/cohorts/components/ActionsCard.tsx b/client/src/features/cohorts/components/ActionsCard.tsx index de6cc9ef..87f57ab3 100644 --- a/client/src/features/cohorts/components/ActionsCard.tsx +++ b/client/src/features/cohorts/components/ActionsCard.tsx @@ -1,5 +1,5 @@ import { Add } from '@mui/icons-material'; -import { AddTraineeDialog } from '../../trainee-profile/create/CreateTraineeDialog'; +import { AddTraineeDialog } from '../../trainee-profile/create/AddTraineeDialog'; import { ButtonWithIcon } from '../../../components/ButtonWithIcon'; import { Card } from '@mui/material'; import { useState } from 'react'; From b058b7c2660ce5d6b395c00ff9ff8a3ba5cb6d87 Mon Sep 17 00:00:00 2001 From: zalexa19 Date: Wed, 11 Feb 2026 15:36:03 +0100 Subject: [PATCH 10/17] fixed linting --- .../trainee-profile/context/useTraineeProfileContext.tsx | 6 +++--- .../trainee-profile/context/useTraineeProfileProvider.tsx | 8 ++++---- .../features/trainee-profile/create/AddTraineeDialog.tsx | 2 +- .../trainee-profile/create/components/NewTraineeForm.tsx | 2 +- .../src/features/trainee-profile/create/lib/formHelper.ts | 2 +- .../trainee-profile/personal-info/PersonalInfo.tsx | 2 +- .../trainee-profile/profile/components/TraineeProfile.tsx | 6 +++--- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/client/src/features/trainee-profile/context/useTraineeProfileContext.tsx b/client/src/features/trainee-profile/context/useTraineeProfileContext.tsx index bda0b19c..ff2f7835 100644 --- a/client/src/features/trainee-profile/context/useTraineeProfileContext.tsx +++ b/client/src/features/trainee-profile/context/useTraineeProfileContext.tsx @@ -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; @@ -12,7 +12,7 @@ export type TraineeProfileContextType = { setIsEditMode: (isEditMode: boolean) => void; isSavingProfile: boolean; setIsSavingProfile: React.Dispatch>; - getTraineeInfoChanges: (trainee: Trainee) => SaveTraineeRequestData; + getTraineeInfoChanges: (trainee: Trainee) => UpdateTraineeRequestData; }; export const TraineeProfileContext = createContext({ @@ -24,7 +24,7 @@ export const TraineeProfileContext = createContext({ setIsEditMode: () => {}, isSavingProfile: false, setIsSavingProfile: () => {}, - getTraineeInfoChanges: () => ({}) as SaveTraineeRequestData, + getTraineeInfoChanges: () => ({}) as UpdateTraineeRequestData, }); export const useTraineeProfileContext = () => useContext(TraineeProfileContext); diff --git a/client/src/features/trainee-profile/context/useTraineeProfileProvider.tsx b/client/src/features/trainee-profile/context/useTraineeProfileProvider.tsx index 6a36276a..8b9695f1 100644 --- a/client/src/features/trainee-profile/context/useTraineeProfileProvider.tsx +++ b/client/src/features/trainee-profile/context/useTraineeProfileProvider.tsx @@ -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; @@ -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 | null = getChangedFields( originalTrainee.personalInfo, trainee.personalInfo @@ -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; diff --git a/client/src/features/trainee-profile/create/AddTraineeDialog.tsx b/client/src/features/trainee-profile/create/AddTraineeDialog.tsx index 3a6db9bf..9eea1ba5 100644 --- a/client/src/features/trainee-profile/create/AddTraineeDialog.tsx +++ b/client/src/features/trainee-profile/create/AddTraineeDialog.tsx @@ -45,7 +45,7 @@ export const AddTraineeDialog: React.FC = ({ isOpen, hand })); }; - const handleSelectChange = (event: SelectChangeEvent) => { + const handleSelectChange = (event: SelectChangeEvent) => { const { name, value } = event.target; setFormState((prevState: FormState) => ({ diff --git a/client/src/features/trainee-profile/create/components/NewTraineeForm.tsx b/client/src/features/trainee-profile/create/components/NewTraineeForm.tsx index ef1516b0..808e5206 100644 --- a/client/src/features/trainee-profile/create/components/NewTraineeForm.tsx +++ b/client/src/features/trainee-profile/create/components/NewTraineeForm.tsx @@ -27,7 +27,7 @@ export const NewTraineeForm: React.FC<{ errors: FormErrors; handleChange: (e: React.ChangeEvent) => void; handleClose: () => void; - handleSelect: (event: SelectChangeEvent) => void; + handleSelect: (event: SelectChangeEvent) => void; handleSubmit: SubmitEventHandler; }> = ({ isLoading, formState, errors, handleChange, handleSelect, handleSubmit, handleClose }) => { return ( diff --git a/client/src/features/trainee-profile/create/lib/formHelper.ts b/client/src/features/trainee-profile/create/lib/formHelper.ts index 1013f2a3..27884de6 100644 --- a/client/src/features/trainee-profile/create/lib/formHelper.ts +++ b/client/src/features/trainee-profile/create/lib/formHelper.ts @@ -1,4 +1,4 @@ -import { FormErrors, FormState } from '../hooks/useCreateTraineeForm'; +import { FormErrors, FormState } from './../components/NewTraineeForm'; const FIELD_REQUIRED_ERROR = 'This field is required'; diff --git a/client/src/features/trainee-profile/personal-info/PersonalInfo.tsx b/client/src/features/trainee-profile/personal-info/PersonalInfo.tsx index 73d33230..90e52265 100644 --- a/client/src/features/trainee-profile/personal-info/PersonalInfo.tsx +++ b/client/src/features/trainee-profile/personal-info/PersonalInfo.tsx @@ -1,6 +1,6 @@ import { Background, EducationLevel, EnglishLevel, Pronouns, ResidencyStatus } from '../../../data/types/Trainee'; import { Box, FormControl, InputLabel, MenuItem, Select, TextField } from '@mui/material'; -import { createSelectChangeHandler, createTextChangeHandler, formatTextToFriendly } from '../utils/formHelper'; +import { createSelectChangeHandler, createTextChangeHandler } from '../utils/formHelper'; import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'; import { GenderSelect } from '../profile/components/GenderSelect'; diff --git a/client/src/features/trainee-profile/profile/components/TraineeProfile.tsx b/client/src/features/trainee-profile/profile/components/TraineeProfile.tsx index d85bd930..167c885f 100644 --- a/client/src/features/trainee-profile/profile/components/TraineeProfile.tsx +++ b/client/src/features/trainee-profile/profile/components/TraineeProfile.tsx @@ -1,6 +1,6 @@ import { Box, Snackbar } from '@mui/material'; import { - SaveTraineeRequestData, + UpdateTraineeRequestData, useSaveTraineeInfo, useTraineeInfoData, } from '../../personal-info/data/useTraineeInfoData'; @@ -62,7 +62,7 @@ const TraineeProfile = ({ id }: TraineeProfileProps) => { * Shows a snackbar with the result of the save operation and refreshes the trainee data. * @param editedFields */ - const saveTraineeData = async (editedFields: SaveTraineeRequestData) => { + const saveTraineeData = async (editedFields: UpdateTraineeRequestData) => { mutate(editedFields, { onSuccess: (data: Trainee) => { setSnackbarSeverity('success'); @@ -91,7 +91,7 @@ const TraineeProfile = ({ id }: TraineeProfileProps) => { return; } - const changedFields: SaveTraineeRequestData = getTraineeInfoChanges(traineeData!); + const changedFields: UpdateTraineeRequestData = getTraineeInfoChanges(traineeData!); saveTraineeData(changedFields); }; From aac22fc30534eebd80be53f6b32913f5b17d56b6 Mon Sep 17 00:00:00 2001 From: zalexa19 Date: Wed, 11 Feb 2026 15:57:17 +0100 Subject: [PATCH 11/17] changed the card bg to a box --- client/src/features/cohorts/components/ActionsCard.tsx | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/client/src/features/cohorts/components/ActionsCard.tsx b/client/src/features/cohorts/components/ActionsCard.tsx index 87f57ab3..4967c239 100644 --- a/client/src/features/cohorts/components/ActionsCard.tsx +++ b/client/src/features/cohorts/components/ActionsCard.tsx @@ -1,7 +1,7 @@ import { Add } from '@mui/icons-material'; import { AddTraineeDialog } from '../../trainee-profile/create/AddTraineeDialog'; +import { Box } from '@mui/material'; import { ButtonWithIcon } from '../../../components/ButtonWithIcon'; -import { Card } from '@mui/material'; import { useState } from 'react'; export const ActionsCard = () => { @@ -15,16 +15,13 @@ export const ActionsCard = () => { setIsAddTraineeDialogOpen(false); }; return ( - + } onClick={handleOpenAddTraineeDialog} /> - + ); }; From 92926c14e99e843665886a3c53a737045d3f5676 Mon Sep 17 00:00:00 2001 From: zalexa19 Date: Wed, 11 Feb 2026 18:21:44 +0100 Subject: [PATCH 12/17] fixed according to copilot --- client/src/components/ButtonWithIcon.tsx | 12 +------ .../cohorts/components/ActionsCard.tsx | 6 +--- .../create/AddTraineeDialog.tsx | 36 +++++++++++-------- .../create/components/NewTraineeForm.tsx | 3 +- .../create/components/TextFieldWrapper.tsx | 2 +- .../trainee-profile/create/lib/formHelper.ts | 2 +- .../profile/components/DropdownSelect.tsx | 4 +-- .../profile/components/GenderSelect.tsx | 3 +- 8 files changed, 31 insertions(+), 37 deletions(-) diff --git a/client/src/components/ButtonWithIcon.tsx b/client/src/components/ButtonWithIcon.tsx index 95c6dc69..e413f790 100644 --- a/client/src/components/ButtonWithIcon.tsx +++ b/client/src/components/ButtonWithIcon.tsx @@ -7,7 +7,6 @@ interface ButtonWithIconProps { text: string; startIcon?: React.ReactNode; onClick?: React.MouseEventHandler; - disabled?: boolean; isLoading?: boolean; sx?: SxProps; type?: 'button' | 'submit' | 'reset'; @@ -17,20 +16,11 @@ export const ButtonWithIcon: React.FC = ({ text, startIcon, onClick, - disabled = false, sx, type = 'button', isLoading = false, }) => ( - ); diff --git a/client/src/features/cohorts/components/ActionsCard.tsx b/client/src/features/cohorts/components/ActionsCard.tsx index 4967c239..46928a1c 100644 --- a/client/src/features/cohorts/components/ActionsCard.tsx +++ b/client/src/features/cohorts/components/ActionsCard.tsx @@ -16,11 +16,7 @@ export const ActionsCard = () => { }; return ( - + } onClick={handleOpenAddTraineeDialog} /> ); diff --git a/client/src/features/trainee-profile/create/AddTraineeDialog.tsx b/client/src/features/trainee-profile/create/AddTraineeDialog.tsx index 9eea1ba5..0398f376 100644 --- a/client/src/features/trainee-profile/create/AddTraineeDialog.tsx +++ b/client/src/features/trainee-profile/create/AddTraineeDialog.tsx @@ -1,10 +1,10 @@ import { Alert, Box, Dialog, SelectChangeEvent, Typography } from '@mui/material'; import { FormErrors, FormState, NewTraineeForm } from './components/NewTraineeForm'; import { JobPath, LearningStatus } from '../../../data/types/Trainee'; -import { SubmitEventHandler, useState } from 'react'; import { useCreateTraineeProfile } from './data/mutations'; import { useNavigate } from 'react-router-dom'; +import { useState } from 'react'; import { validateForm } from './lib/formHelper'; interface AddTraineeDialogProps { @@ -12,6 +12,15 @@ interface AddTraineeDialogProps { handleClose: () => void; } export const AddTraineeDialog: React.FC = ({ isOpen, handleClose }) => { + const initialState = { + firstName: '', + lastName: '', + gender: null, + email: '', + cohort: 0, + learningStatus: LearningStatus.Studying, + jobPath: JobPath.NotGraduated, + }; const navigate = useNavigate(); const { mutate: createTrainee, @@ -23,18 +32,15 @@ export const AddTraineeDialog: React.FC = ({ isOpen, hand const [errors, setErrors] = useState({}); - const [formState, setFormState] = useState({ - firstName: '', - lastName: '', - gender: null, - email: '', - cohort: 0, - learningStatus: LearningStatus.Studying, - jobPath: JobPath.NotGraduated, - }); + const [formState, setFormState] = useState(initialState); + const onClose = () => { + setFormState(initialState); + setErrors({}); + handleClose(); + }; const onSuccess = (path: string) => { - handleClose(); + onClose(); navigate(path); }; const handleTextChange = (e: React.ChangeEvent) => { @@ -54,7 +60,7 @@ export const AddTraineeDialog: React.FC = ({ isOpen, hand })); }; - const handleSubmit: SubmitEventHandler = (event) => { + const handleSubmit: React.ComponentProps<'form'>['onSubmit'] = (event) => { event.preventDefault(); setErrors({}); const errors = validateForm(formState); @@ -82,12 +88,14 @@ export const AddTraineeDialog: React.FC = ({ isOpen, hand handleChange={handleTextChange} handleSelect={handleSelectChange} handleSubmit={handleSubmit} - handleClose={handleClose} + handleClose={onClose} /> {submitError && ( - An error occurred while creating the trainee profile: {submitError.message} + + An error occurred while creating the trainee profile: {submitError?.message && 'unknown'} + )} diff --git a/client/src/features/trainee-profile/create/components/NewTraineeForm.tsx b/client/src/features/trainee-profile/create/components/NewTraineeForm.tsx index 808e5206..4d22aa1a 100644 --- a/client/src/features/trainee-profile/create/components/NewTraineeForm.tsx +++ b/client/src/features/trainee-profile/create/components/NewTraineeForm.tsx @@ -4,7 +4,6 @@ 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 { SubmitEventHandler } from 'react'; import TextFieldWrapper from './TextFieldWrapper'; export type FormState = { @@ -28,7 +27,7 @@ export const NewTraineeForm: React.FC<{ handleChange: (e: React.ChangeEvent) => void; handleClose: () => void; handleSelect: (event: SelectChangeEvent) => void; - handleSubmit: SubmitEventHandler; + handleSubmit: React.ComponentProps<'form'>['onSubmit']; }> = ({ isLoading, formState, errors, handleChange, handleSelect, handleSubmit, handleClose }) => { return (
diff --git a/client/src/features/trainee-profile/create/components/TextFieldWrapper.tsx b/client/src/features/trainee-profile/create/components/TextFieldWrapper.tsx index d85b6b6f..004fa281 100644 --- a/client/src/features/trainee-profile/create/components/TextFieldWrapper.tsx +++ b/client/src/features/trainee-profile/create/components/TextFieldWrapper.tsx @@ -20,7 +20,7 @@ const TextFieldWrapper: React.FC = (props) => { return ( { const validateCohort = (cohort: number | undefined) => { if (cohort === undefined) return FIELD_REQUIRED_ERROR; - if (cohort < 0) return 'Cohort must be a positive number'; + if (cohort <= 0) return 'Cohort must be a positive number'; return null; }; diff --git a/client/src/features/trainee-profile/profile/components/DropdownSelect.tsx b/client/src/features/trainee-profile/profile/components/DropdownSelect.tsx index dd416c91..2a7d0ed3 100644 --- a/client/src/features/trainee-profile/profile/components/DropdownSelect.tsx +++ b/client/src/features/trainee-profile/profile/components/DropdownSelect.tsx @@ -34,7 +34,7 @@ export const DropdownSelect = ({ const NoIcon = () => null; return ( - + {inputLabel} + {!!error && {error}} ); }; diff --git a/client/src/features/trainee-profile/profile/components/GenderSelect.tsx b/client/src/features/trainee-profile/profile/components/GenderSelect.tsx index d5b96466..2bfaa7fe 100644 --- a/client/src/features/trainee-profile/profile/components/GenderSelect.tsx +++ b/client/src/features/trainee-profile/profile/components/GenderSelect.tsx @@ -9,6 +9,7 @@ const genderOptions = Object.values(Gender).map((gender) => ({ })); type GenderSelectProps = { + initialValue?: string; disabled?: boolean; isEditing: boolean; value?: string; @@ -18,6 +19,6 @@ type GenderSelectProps = { export const GenderSelect: React.FC = (props) => { return ( - + ); }; From 6ebc02e20276afa2f65c08bbbafc7b99ae123c39 Mon Sep 17 00:00:00 2001 From: zalexa19 Date: Wed, 11 Feb 2026 22:00:45 +0100 Subject: [PATCH 13/17] fixed according to pr comments --- client/src/components/ButtonWithIcon.tsx | 26 ------------ client/src/components/StyledErrorBox.tsx | 13 ------ client/src/features/cohorts/CohortsPage.tsx | 14 ++++--- .../cohorts/components/ActionsCard.tsx | 8 ++-- .../create/AddTraineeDialog.tsx | 17 ++++---- .../create/components/NewTraineeForm.tsx | 42 ++++++++----------- .../trainee-profile/create/lib/formHelper.ts | 35 ++++++++++------ 7 files changed, 60 insertions(+), 95 deletions(-) delete mode 100644 client/src/components/ButtonWithIcon.tsx delete mode 100644 client/src/components/StyledErrorBox.tsx diff --git a/client/src/components/ButtonWithIcon.tsx b/client/src/components/ButtonWithIcon.tsx deleted file mode 100644 index e413f790..00000000 --- a/client/src/components/ButtonWithIcon.tsx +++ /dev/null @@ -1,26 +0,0 @@ -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; - isLoading?: boolean; - sx?: SxProps; - type?: 'button' | 'submit' | 'reset'; -} - -export const ButtonWithIcon: React.FC = ({ - text, - startIcon, - onClick, - sx, - type = 'button', - isLoading = false, -}) => ( - -); diff --git a/client/src/components/StyledErrorBox.tsx b/client/src/components/StyledErrorBox.tsx deleted file mode 100644 index b1d1c1c7..00000000 --- a/client/src/components/StyledErrorBox.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { Box } from '@mui/material'; -import { ErrorBox } from './ErrorBox'; - -type StyledErrorBoxProps = { - message: string; -}; -export const StyledErrorBox: React.FC = ({ message }) => { - return ( - - - - ); -}; diff --git a/client/src/features/cohorts/CohortsPage.tsx b/client/src/features/cohorts/CohortsPage.tsx index 2090703b..38a0889c 100644 --- a/client/src/features/cohorts/CohortsPage.tsx +++ b/client/src/features/cohorts/CohortsPage.tsx @@ -1,11 +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'; @@ -31,9 +31,13 @@ const CohortsPage = () => { )} {isError && ( - + + + )} diff --git a/client/src/features/cohorts/components/ActionsCard.tsx b/client/src/features/cohorts/components/ActionsCard.tsx index 46928a1c..89144689 100644 --- a/client/src/features/cohorts/components/ActionsCard.tsx +++ b/client/src/features/cohorts/components/ActionsCard.tsx @@ -1,7 +1,7 @@ +import { Box, Button } from '@mui/material'; + 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 = () => { @@ -17,7 +17,9 @@ export const ActionsCard = () => { return ( - } onClick={handleOpenAddTraineeDialog} /> + ); }; diff --git a/client/src/features/trainee-profile/create/AddTraineeDialog.tsx b/client/src/features/trainee-profile/create/AddTraineeDialog.tsx index 0398f376..b06869e8 100644 --- a/client/src/features/trainee-profile/create/AddTraineeDialog.tsx +++ b/client/src/features/trainee-profile/create/AddTraineeDialog.tsx @@ -5,7 +5,7 @@ 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'; +import { validateAndCollectFormErrors } from './lib/formHelper'; interface AddTraineeDialogProps { isOpen: boolean; @@ -21,6 +21,7 @@ export const AddTraineeDialog: React.FC = ({ isOpen, hand learningStatus: LearningStatus.Studying, jobPath: JobPath.NotGraduated, }; + const navigate = useNavigate(); const { mutate: createTrainee, @@ -30,12 +31,12 @@ export const AddTraineeDialog: React.FC = ({ isOpen, hand onSuccess: (profilePath: string) => onSuccess(profilePath), }); - const [errors, setErrors] = useState({}); + const [errors, setErrors] = useState(null); const [formState, setFormState] = useState(initialState); const onClose = () => { setFormState(initialState); - setErrors({}); + setErrors(null); handleClose(); }; @@ -62,16 +63,12 @@ export const AddTraineeDialog: React.FC = ({ isOpen, hand 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); + const errors = validateAndCollectFormErrors(formState); - if (hasErrors) { + if (errors) { + setErrors(errors); return; } - createTrainee(formState); }; diff --git a/client/src/features/trainee-profile/create/components/NewTraineeForm.tsx b/client/src/features/trainee-profile/create/components/NewTraineeForm.tsx index 4d22aa1a..a53f0849 100644 --- a/client/src/features/trainee-profile/create/components/NewTraineeForm.tsx +++ b/client/src/features/trainee-profile/create/components/NewTraineeForm.tsx @@ -17,13 +17,17 @@ export type FormState = { }; export type FormErrors = { - [K in keyof FormState]?: string | null; + firstName?: string; + lastName?: string; + gender?: string; + email?: string; + cohort?: string; }; export const NewTraineeForm: React.FC<{ isLoading: boolean; formState: FormState; - errors: FormErrors; + errors: FormErrors | null; handleChange: (e: React.ChangeEvent) => void; handleClose: () => void; handleSelect: (event: SelectChangeEvent) => void; @@ -36,8 +40,8 @@ export const NewTraineeForm: React.FC<{ disabled={isLoading} id="firstName" name="firstName" - error={!!errors.firstName} - helperText={errors.firstName} + error={!!errors?.firstName} + helperText={errors?.firstName} label="First name" value={formState.firstName} onChange={handleChange} @@ -46,8 +50,8 @@ export const NewTraineeForm: React.FC<{ disabled={isLoading} id="lastName" name="lastName" - error={!!errors.lastName} - helperText={errors.lastName} + error={!!errors?.lastName} + helperText={errors?.lastName} label="Last name" value={formState.lastName} onChange={handleChange} @@ -56,15 +60,15 @@ export const NewTraineeForm: React.FC<{ disabled={isLoading} isEditing value={formState.gender || undefined} - error={errors.gender || ''} + error={errors?.gender || ''} onChange={handleSelect} /> - - + +