From 38a54cedaa838fe0bb83fe72b5cd3d58113093c5 Mon Sep 17 00:00:00 2001 From: Ilya Bubnov Date: Mon, 9 Feb 2026 17:48:07 +0100 Subject: [PATCH 01/14] started working on the feature, not much added, still figuring out MUI API/used components --- client/package-lock.json | 21 +- .../employment/EmploymentDetailsModal.tsx | 280 ++++++++++++++++++ .../employment/EmploymentInfo.tsx | 56 ++-- 3 files changed, 320 insertions(+), 37 deletions(-) create mode 100644 client/src/features/trainee-profile/employment/EmploymentDetailsModal.tsx diff --git a/client/package-lock.json b/client/package-lock.json index ad9e3f9c..a724e4d2 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -75,7 +75,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -412,7 +411,6 @@ "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -456,7 +454,6 @@ "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -1319,7 +1316,6 @@ "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.7.tgz", "integrity": "sha512-6bdIxqzeOtBAj2wAsfhWCYyMKPLkRO9u/2o5yexcL0C3APqyy91iGSWgT3H7hg+zR2XgE61+WAu12wXPON8b6A==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.28.4", "@mui/core-downloads-tracker": "^7.3.7", @@ -1430,7 +1426,6 @@ "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.3.7.tgz", "integrity": "sha512-DovL3k+FBRKnhmatzUMyO5bKkhMLlQ9L7Qw5qHrre3m8zCZmE+31NDVBFfqrbrA7sq681qaEIHdkWD5nmiAjyQ==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.28.4", "@mui/private-theming": "^7.3.7", @@ -2112,7 +2107,6 @@ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.20.tgz", "integrity": "sha512-vXBxa+qeyveVO7OA0jX1z+DeyCA4JKnThKv411jd5SORpBKgkcVnYKCiBgECvADvniBX7tobwBmg01qq9JmMJw==", "license": "MIT", - "peer": true, "dependencies": { "@tanstack/query-core": "5.90.20" }, @@ -2316,7 +2310,6 @@ "integrity": "sha512-CPrnr8voK8vC6eEtyRzvMpgp3VyVRhgclonE7qYi6P9sXwYb59ucfrnmFBTaP0yUi8Gk4yZg/LlTJULGxvTNsg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -2338,7 +2331,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.13.tgz", "integrity": "sha512-KkiJeU6VbYbUOp5ITMIc7kBfqlYkKA5KhEHVrGMmUUMt7NeaZg65ojdPk+FtNrBAOXNVM5QM72jnADjM+XVRAQ==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -2413,7 +2405,6 @@ "integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/types": "8.54.0", @@ -2645,7 +2636,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2798,7 +2788,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -3149,8 +3138,7 @@ "version": "1.11.19", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/debug": { "version": "4.4.3", @@ -3375,7 +3363,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -5427,7 +5414,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -5537,7 +5523,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -5547,7 +5532,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -6032,7 +6016,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6243,7 +6226,6 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -6365,7 +6347,6 @@ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/client/src/features/trainee-profile/employment/EmploymentDetailsModal.tsx b/client/src/features/trainee-profile/employment/EmploymentDetailsModal.tsx new file mode 100644 index 00000000..0bafd406 --- /dev/null +++ b/client/src/features/trainee-profile/employment/EmploymentDetailsModal.tsx @@ -0,0 +1,280 @@ +import { EmploymentHistory, Strike, StrikeReason } from '../../../data/types/Trainee.ts'; +import { useState } from 'react'; +import { + Alert, + Backdrop, + Box, + Button, Checkbox, + Fade, + FormControl, FormControlLabel, + FormHelperText, + InputLabel, + MenuItem, + Modal, + Select, + SelectChangeEvent, + TextField, + Typography, +} from '@mui/material'; +import { formatDate } from '../utils/dateHelper.ts'; +import { LoadingButton } from '@mui/lab'; + +interface EmploymentDetailsModalProps { + isOpen: boolean; + error: string; + isLoading: boolean; + onClose: () => void; + onConfirmAdd: (employment: EmploymentHistory) => void; + onConfirmEdit: (employment: EmploymentHistory) => void; + initialEmployment: EmploymentHistory | null; +} + +export const EmploymentDetailsModal = ({ + isOpen, + isLoading, + error, + onClose, + onConfirmAdd, + onConfirmEdit, + initialEmployment, +}: EmploymentDetailsModalProps) => { + const [employmentFields, setEmploymentFields] = useState({ + id: initialEmployment?.id || '', + type: initialEmployment?.type || '', + companyName: initialEmployment?.companyName || '', + role: initialEmployment?.role || '', + startDate: initialEmployment?.startDate || new Date(), + endDate: initialEmployment?.endDate || new Date(), + feeCollected: initialEmployment?.feeCollected || false, + feeAmount: initialEmployment?.feeAmount || 0, + comments: initialEmployment?.comments || '', + } as EmploymentHistory); + + // TODO: add logic to error handling + const [requiredFieldError, setRequiredFieldError] = useState({ + type: false, + companyName: false, + role: false, + startDate: false, + feeCollected: false, + }); + const isEditMode = Boolean(initialEmployment); + + const handleClose = () => { + onClose(); + }; + const handleEmploymentChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + + setEmploymentFields((prevEmployment: EmploymentHistory) => ({ + ...prevEmployment, + [name]: value, + })); + }; + + const handleEmploymentSelectChange = (e: SelectChangeEvent) => { + const { name, value } = e.target; + + setEmploymentFields((prevEmployment: EmploymentHistory) => ({ + ...prevEmployment, + [name]: value, + })); + }; + + /* + const onConfirm = async () => { + if (employmentFields.reason === null) { + setReasonRequiredError(true); + return; + } + if (!employmentFields.comments) { + setCommentsRequiredError(true); + return; + } + + if (initialStrike) { + onConfirmEdit(employmentFields); + } else { + onConfirmAdd(employmentFields); + } + }; + + const onChangeComments = (e: React.ChangeEvent) => { + setCommentsRequiredError(false); + const { name, value } = e.target; + + setStrikeFields((prevStrike) => ({ + ...prevStrike, + [name]: value, + })); + }; + */ + + return ( + + + + + {isEditMode ? 'Edit employment' : 'Add a new employment'} + + + + + {requiredFieldError.companyName && Company name is required} + + + + Type* + + + {requiredFieldError.type && Type is required} + + + + + {requiredFieldError.role && Role is required} + + + + + {requiredFieldError.startDate && Start date is required} + + + + + + + + + } + label="Fee collected" + /> + + + + + + + + + {error && {error}} + + + + + Save + + + + + + ); +}; \ No newline at end of file diff --git a/client/src/features/trainee-profile/employment/EmploymentInfo.tsx b/client/src/features/trainee-profile/employment/EmploymentInfo.tsx index aeb34e61..a5a1f1c8 100644 --- a/client/src/features/trainee-profile/employment/EmploymentInfo.tsx +++ b/client/src/features/trainee-profile/employment/EmploymentInfo.tsx @@ -1,5 +1,6 @@ import { Box, + Button, Divider, FormControl, InputAdornment, @@ -10,17 +11,19 @@ import { ListItemText, MenuItem, Select, + Stack, TextField, Typography, } from '@mui/material'; import { createSelectChangeHandler, createTextChangeHandler } from '../utils/formHelper'; import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'; -import { JobPath } from '../../../data/types/Trainee'; +import { EmploymentHistory, JobPath } from '../../../data/types/Trainee'; import LinkIcon from '@mui/icons-material/Link'; -import React from 'react'; +import React, { useState } from 'react'; import { formatDate } from '../utils/dateHelper'; import { useTraineeProfileContext } from '../context/useTraineeProfileContext'; +import AddIcon from '@mui/icons-material/Add'; const NoIcon = () => null; @@ -30,11 +33,21 @@ const NoIcon = () => null; * @returns {ReactNode} A React element that renders trainee employment information with view, add, and edit logic. */ export const EmploymentInfo = () => { + const [isModalOpen, setIsModalOpen] = useState(false); + const [employmentToEdit, setEmploymentToEdit] = useState(null); const { trainee, setTrainee, isEditMode: isEditing } = useTraineeProfileContext(); const { employmentInfo: editedFields } = trainee; const handleTextChange = createTextChangeHandler(setTrainee, 'employmentInfo'); const handleSelectChange = createSelectChangeHandler(setTrainee, 'employmentInfo'); + /** + * Function to enable adding entries. + */ + const onClickAdd = () => { + setEmploymentToEdit(null); + setIsModalOpen(true); + }; + return (
@@ -69,21 +82,23 @@ export const EmploymentInfo = () => { name="cvURL" label="CV" type="url" + placeholder="https://cv.example.com" value={editedFields?.cvURL || ''} - InputProps={{ - readOnly: isEditing ? false : true, - endAdornment: ( - - {!isEditing && editedFields?.cvURL && ( - - - - )} - - ), - }} - InputLabelProps={{ - shrink: true, + // TODO: get back to this when get response from Stas + slotProps={{ + input: { + readOnly: !isEditing, + endAdornment: ( + + {!isEditing && editedFields?.cvURL && ( + + + + )} + + ), + }, + inputLabel: { shrink: true }, }} variant={isEditing ? 'outlined' : 'standard'} onChange={handleTextChange} @@ -99,6 +114,7 @@ export const EmploymentInfo = () => { name="availability" label="Availability" type="text" + placeholder="From next month, fulltime" value={editedFields?.availability || ''} InputProps={{ readOnly: isEditing ? false : true }} InputLabelProps={{ shrink: true }} @@ -116,6 +132,7 @@ export const EmploymentInfo = () => { name="preferredRole" label="Preferred role" type="text" + placeholder="Backend" value={editedFields?.preferredRole || ''} InputProps={{ readOnly: isEditing ? false : true }} InputLabelProps={{ shrink: true }} @@ -131,6 +148,7 @@ export const EmploymentInfo = () => { name="preferredLocation" label="Preferred location" type="text" + placeholder="Randstad, Utrecht" value={editedFields?.preferredLocation || ''} InputProps={{ readOnly: isEditing ? false : true }} InputLabelProps={{ shrink: true }} @@ -166,6 +184,7 @@ export const EmploymentInfo = () => { name="extraTechnologies" label="Extra technologies" type="text" + placeholder="C#, C++, Vue.js" value={editedFields?.extraTechnologies || ''} InputProps={{ readOnly: isEditing ? false : true }} InputLabelProps={{ shrink: true }} @@ -181,6 +200,9 @@ export const EmploymentInfo = () => { Employment history ({editedFields?.employmentHistory.length || 0}) + + + {
); -}; +};; export default EmploymentInfo; From f768ba9a2f663185e11c2148220f8ff42b6ae94f Mon Sep 17 00:00:00 2001 From: Ilya Bubnov Date: Tue, 10 Feb 2026 11:47:04 +0100 Subject: [PATCH 02/14] started working on queries file and integration of Modal into the page --- .../employment/EmploymentDetailsModal.tsx | 67 ++++++------- .../employment/EmploymentInfo.tsx | 96 ++++++++++++++++++- .../employment/data/employment-queries.ts | 68 +++++++++++++ 3 files changed, 190 insertions(+), 41 deletions(-) create mode 100644 client/src/features/trainee-profile/employment/data/employment-queries.ts diff --git a/client/src/features/trainee-profile/employment/EmploymentDetailsModal.tsx b/client/src/features/trainee-profile/employment/EmploymentDetailsModal.tsx index 0bafd406..6fe5961c 100644 --- a/client/src/features/trainee-profile/employment/EmploymentDetailsModal.tsx +++ b/client/src/features/trainee-profile/employment/EmploymentDetailsModal.tsx @@ -1,13 +1,16 @@ -import { EmploymentHistory, Strike, StrikeReason } from '../../../data/types/Trainee.ts'; -import { useState } from 'react'; +import { EmploymentHistory, EmploymentType } from '../../../data/types/Trainee.ts'; +import React, { useState } from 'react'; import { Alert, Backdrop, Box, - Button, Checkbox, + Button, + Checkbox, Fade, - FormControl, FormControlLabel, + FormControl, + FormControlLabel, FormHelperText, + InputAdornment, InputLabel, MenuItem, Modal, @@ -56,7 +59,6 @@ export const EmploymentDetailsModal = ({ companyName: false, role: false, startDate: false, - feeCollected: false, }); const isEditMode = Boolean(initialEmployment); @@ -81,35 +83,19 @@ export const EmploymentDetailsModal = ({ })); }; - /* const onConfirm = async () => { - if (employmentFields.reason === null) { - setReasonRequiredError(true); - return; - } - if (!employmentFields.comments) { - setCommentsRequiredError(true); - return; - } + if (!employmentFields.companyName) setRequiredFieldError({ ...requiredFieldError, companyName: true }); + if (!employmentFields.type) setRequiredFieldError({ ...requiredFieldError, type: true }); + if (!employmentFields.role) setRequiredFieldError({ ...requiredFieldError, role: true }); + if (!employmentFields.startDate) setRequiredFieldError({ ...requiredFieldError, startDate: true }); - if (initialStrike) { + if (initialEmployment) { onConfirmEdit(employmentFields); } else { onConfirmAdd(employmentFields); } }; - const onChangeComments = (e: React.ChangeEvent) => { - setCommentsRequiredError(false); - const { name, value } = e.target; - - setStrikeFields((prevStrike) => ({ - ...prevStrike, - [name]: value, - })); - }; - */ - return ( @@ -135,6 +121,7 @@ export const EmploymentDetailsModal = ({ @@ -153,6 +141,7 @@ export const EmploymentDetailsModal = ({ Type* {requiredFieldError.type && Type is required} @@ -185,6 +176,7 @@ export const EmploymentDetailsModal = ({ @@ -228,14 +221,19 @@ export const EmploymentDetailsModal = ({ € }, + }} + //TODO: error logic, right now it doesn't take into account if feeAmount is set onChange={handleEmploymentChange} fullWidth /> @@ -243,7 +241,6 @@ export const EmploymentDetailsModal = ({ diff --git a/client/src/features/trainee-profile/employment/EmploymentInfo.tsx b/client/src/features/trainee-profile/employment/EmploymentInfo.tsx index a5a1f1c8..a089208f 100644 --- a/client/src/features/trainee-profile/employment/EmploymentInfo.tsx +++ b/client/src/features/trainee-profile/employment/EmploymentInfo.tsx @@ -18,12 +18,14 @@ import { import { createSelectChangeHandler, createTextChangeHandler } from '../utils/formHelper'; import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'; -import { EmploymentHistory, JobPath } from '../../../data/types/Trainee'; +import { EmploymentHistory, JobPath, Strike } from '../../../data/types/Trainee'; import LinkIcon from '@mui/icons-material/Link'; import React, { useState } from 'react'; import { formatDate } from '../utils/dateHelper'; import { useTraineeProfileContext } from '../context/useTraineeProfileContext'; import AddIcon from '@mui/icons-material/Add'; +import { EmploymentDetailsModal } from './EmploymentDetailsModal.tsx'; +import { useQueryClient } from '@tanstack/react-query'; const NoIcon = () => null; @@ -34,12 +36,63 @@ const NoIcon = () => null; */ export const EmploymentInfo = () => { const [isModalOpen, setIsModalOpen] = useState(false); + + const [modalError, setModalError] = useState(''); const [employmentToEdit, setEmploymentToEdit] = useState(null); - const { trainee, setTrainee, isEditMode: isEditing } = useTraineeProfileContext(); + const { traineeId, trainee, setTrainee, isEditMode: isEditing } = useTraineeProfileContext(); const { employmentInfo: editedFields } = trainee; + const { mutate: addEmployment, isPending: addEmploymentLoading } = useAddEmployment(traineeId); + const { mutate: deleteEmployment, isPending: deleteEmploymentLoading, error: deleteEmploymentError } = useDeleteEmployment(traineeId); + const { mutate: editEmployment, isPending: editEmploymentLoading } = useEditEmployment(traineeId); + const { data: employments, isPending: employmentsLoading, error: employmentsError } = useGetEmployment(traineeId); + const [isConfirmationDialogOpen, setIsConfirmationDialogOpen] = useState(false); + const handleTextChange = createTextChangeHandler(setTrainee, 'employmentInfo'); const handleSelectChange = createSelectChangeHandler(setTrainee, 'employmentInfo'); + const [idToDelete, setIdToDelete] = useState(''); + const queryClient = useQueryClient(); + const handleSuccess = () => { + queryClient.invalidateQueries({ queryKey: ['employmentHistory', traineeId] }); + setIsModalOpen(false); + }; + + const getErrorMessage = (error: Error | unknown) => { + return (error as Error).message || 'Unknown error'; + }; + + const onClickEdit = (id: string) => { + const employment = employments?.find((employment: EmploymentHistory) => employment.id === id) || null; + + setEmploymentToEdit(employment); + setIsModalOpen(true); + }; + + const onConfirmAdd = async (strike: Strike) => { + if (modalError) setModalError(''); + addStrike(strike, { + onSuccess: handleSuccess, + onError: (e) => { + setModalError((e as Error).message); + }, + }); + }; + + const onConfirmEdit = (strike: Strike) => { + if (modalError) setModalError(''); + editStrike(strike, { + onSuccess: handleSuccess, + onError: (e) => { + setModalError((e as Error).message); + }, + }); + }; + + const onClickDelete = (id: string) => { + setIdToDelete(id); + setIsConfirmationDialogOpen(true); + }; + /** * Function to enable adding entries. */ @@ -48,6 +101,28 @@ export const EmploymentInfo = () => { setIsModalOpen(true); }; + /** + * Function to cancel adding strikes. + */ + const closeModal = () => { + setIsModalOpen(false); + setStrikeToEdit(null); + setModalError(''); + }; + + const onCancelDelete = () => { + setIsConfirmationDialogOpen(false); + }; + + const onConfirmDelete = () => { + deleteStrike(idToDelete, { + onSuccess: () => { + setIsConfirmationDialogOpen(false); + queryClient.invalidateQueries({ queryKey: ['strikes', traineeId] }); + }, + }); + }; + return (
@@ -59,7 +134,7 @@ export const EmploymentInfo = () => { id="jobPath" label="Job path" value={editedFields?.jobPath || ''} - inputProps={{ readOnly: isEditing ? false : true }} + slotProps={{ input: { readOnly: isEditing } }} IconComponent={isEditing ? ArrowDropDownIcon : NoIcon} startAdornment=" " onChange={handleSelectChange} @@ -201,7 +276,9 @@ export const EmploymentInfo = () => { Employment history ({editedFields?.employmentHistory.length || 0}) - + @@ -230,6 +307,15 @@ export const EmploymentInfo = () => { ))} +
@@ -251,6 +337,6 @@ export const EmploymentInfo = () => {
); -};; +};;; export default EmploymentInfo; diff --git a/client/src/features/trainee-profile/employment/data/employment-queries.ts b/client/src/features/trainee-profile/employment/data/employment-queries.ts new file mode 100644 index 00000000..9b68291e --- /dev/null +++ b/client/src/features/trainee-profile/employment/data/employment-queries.ts @@ -0,0 +1,68 @@ +import { useMutation, useQuery } from '@tanstack/react-query'; + +import {EmploymentHistory} from '../../../../data/types/Trainee'; +import axios from 'axios'; + +/** + * Hook to add employment to a trainee. + * @param {string} traineeId the id of the trainee to add the employment to. + * @param {EmploymentHistory} employment the employment to add. + */ +export const useAddStrike = (traineeId: string) => { + return useMutation({ + mutationFn: async (strike: Strike) => { + return axios.post(`/api/trainees/${traineeId}/strikes`, strike).catch((error) => { + throw new Error(error.response?.data?.error || 'Failed to add strike'); + }); + }, + }); +}; + +/** + * Hook to get the strikes of a trainee. + * @param {string} traineeId the id of the trainee to get the strikes from. + * @returns {UseQueryResult} the strikes of the trainee. + */ +export const useGetStrikes = (traineeId: string) => { + return useQuery({ + queryKey: ['strikes', traineeId], + queryFn: async () => { + const { data } = await axios.get(`/api/trainees/${traineeId}/strikes`); + return orderStrikesByDateDesc(data as Strike[]); + }, + enabled: !!traineeId, + refetchOnWindowFocus: false, + }); +}; + +/** + * Hook to delete a strike from a trainee. + * @param {string} traineeId the id of the trainee to delete the strike from. + * @param {string} strikeId the id of the strike to delete. + * */ + +export const useDeleteStrike = (traineeId: string) => { + return useMutation({ + mutationFn: async (strikeId: string) => { + return axios.delete(`/api/trainees/${traineeId}/strikes/${strikeId}`); + }, + }); +}; + +/** + * Hook to edit a strike of a trainee. + * @param {string} traineeId the id of the trainee to edit the strike of. + */ +export const useEditStrike = (traineeId: string) => { + return useMutation({ + mutationFn: async (strike: Strike) => { + return axios.put(`/api/trainees/${traineeId}/strikes/${strike.id}`, strike).catch((error) => { + throw new Error(error.response?.data?.error || 'Failed to edit strike'); + }); + }, + }); +}; + +const orderStrikesByDateDesc = (data: Strike[]): Strike[] => { + return data.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); +}; From 2b7bcbd80c778a33f7c84e815ff84ca81911480e Mon Sep 17 00:00:00 2001 From: Ilya Bubnov Date: Tue, 10 Feb 2026 13:55:41 +0100 Subject: [PATCH 03/14] list is displayed, but needs adjustments to look as required, editing doesn't work (need to find the cause) errors are displayed as required plus hook queries adjusted --- .../education/strikes/StrikesList.tsx | 2 +- .../employment/EmploymentDetailsModal.tsx | 43 +++++--- .../employment/EmploymentInfo.tsx | 95 ++++++++-------- .../employment/EmploymentsList.tsx | 101 ++++++++++++++++++ .../employment/data/employment-queries.ts | 50 ++++----- 5 files changed, 198 insertions(+), 93 deletions(-) create mode 100644 client/src/features/trainee-profile/employment/EmploymentsList.tsx diff --git a/client/src/features/trainee-profile/education/strikes/StrikesList.tsx b/client/src/features/trainee-profile/education/strikes/StrikesList.tsx index 278c109e..8f27ec3d 100644 --- a/client/src/features/trainee-profile/education/strikes/StrikesList.tsx +++ b/client/src/features/trainee-profile/education/strikes/StrikesList.tsx @@ -72,7 +72,7 @@ export const StrikesList: React.FC = ({ strikes, onClickEdit, ) => { + const { name, checked } = e.target; + setEmploymentFields((prevEmployment: EmploymentHistory) => ({ + ...prevEmployment, + [name]: checked, + })); + } const handleEmploymentSelectChange = (e: SelectChangeEvent) => { const { name, value } = e.target; @@ -84,10 +91,17 @@ export const EmploymentDetailsModal = ({ }; const onConfirm = async () => { - if (!employmentFields.companyName) setRequiredFieldError({ ...requiredFieldError, companyName: true }); - if (!employmentFields.type) setRequiredFieldError({ ...requiredFieldError, type: true }); - if (!employmentFields.role) setRequiredFieldError({ ...requiredFieldError, role: true }); - if (!employmentFields.startDate) setRequiredFieldError({ ...requiredFieldError, startDate: true }); + const newErrors = { + type: !employmentFields.type, + companyName: !employmentFields.companyName, + role: !employmentFields.role, + startDate: !employmentFields.startDate, + feeAmount: employmentFields.feeCollected && (!employmentFields.feeAmount || employmentFields.feeAmount <= 0), + } + + setRequiredFieldError(newErrors); + const errors = Object.values(newErrors).some(Boolean) + if (errors) return; if (initialEmployment) { onConfirmEdit(employmentFields); @@ -125,7 +139,7 @@ export const EmploymentDetailsModal = ({ disabled={isLoading} id="companyName" name="companyName" - label="Company name*" + label="Company name" type="text" placeholder="HackYourFuture" value={employmentFields.companyName} @@ -138,13 +152,13 @@ export const EmploymentDetailsModal = ({
- Type* + Type