Skip to content

Commit 968b398

Browse files
authored
Implement Employment History (#304)
* started working on the feature, not much added, still figuring out MUI API/used components * started working on queries file and integration of Modal into the page * 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 * finished implementing the new feature, cleaned up codebase a little and adjusted styling * finished implementing the new feature, cleaned up codebase a little and adjusted styling * updated dependencies to not include extensions, moved fetching logic to api/api.ts, adjusted styling to fit criteria, fixed typo isEditing=>!isEditing, applied copilot feedback in most places * applied given feedback * moved employment history related components into a separate folder and di `npm audit fix` * switched to using key factory * ran `npm audit fix` on both server and client, fixed an issue with key invalidation, small UX/UI changes * when !feeCollected feeAmount falls back to "" * switched from "" to undefined for fallback for feeAmount since feeAmount can be `number | undefined` * found the reason of the bug, added todo comment * bug with feeAmount fixed
1 parent d5be830 commit 968b398

File tree

11 files changed

+1040
-452
lines changed

11 files changed

+1040
-452
lines changed

client/package-lock.json

Lines changed: 80 additions & 99 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

client/src/features/trainee-profile/education/strikes/StrikeDetailsModal.tsx

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -106,10 +106,8 @@ export const StrikeDetailsModal = ({
106106
<Modal
107107
open={isOpen}
108108
closeAfterTransition
109-
BackdropComponent={Backdrop}
110-
BackdropProps={{
111-
timeout: 500,
112-
}}
109+
slots={{ backdrop: Backdrop }}
110+
slotProps={{ backdrop: { timeout: 500 } }}
113111
>
114112
<Fade in={isOpen}>
115113
<Box
@@ -123,7 +121,7 @@ export const StrikeDetailsModal = ({
123121
top: '50%',
124122
left: '50%',
125123
transform: 'translate(-50%, -50%)',
126-
bgcolor: 'background.paper',
124+
backgroundColor: 'background.paper',
127125
boxShadow: 24,
128126
p: 4,
129127
}}
@@ -140,7 +138,7 @@ export const StrikeDetailsModal = ({
140138
label="Date"
141139
type="date"
142140
value={formatDate(strikeFields.date)}
143-
InputLabelProps={{ shrink: true }}
141+
slotProps={{ inputLabel: { shrink: true } }}
144142
onChange={handleStrikeChange}
145143
fullWidth
146144
/>
@@ -187,7 +185,7 @@ export const StrikeDetailsModal = ({
187185
error={commentsRequiredError}
188186
helperText={commentsRequiredError ? 'Comments are required' : ''}
189187
value={strikeFields.comments}
190-
InputLabelProps={{ shrink: true }}
188+
slotProps={{ inputLabel: { shrink: true} }}
191189
onChange={onChangeComments}
192190
fullWidth
193191
/>

client/src/features/trainee-profile/education/strikes/StrikesList.tsx

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import EditIcon from '@mui/icons-material/Edit';
66
import MarkdownText from '../../components/MarkdownText';
77
import { Strike } from '../../../../data/types/Trainee';
88
import { formatDateForDisplay } from '../../utils/dateHelper';
9+
import React from 'react';
910

1011
interface StrikesListProps {
1112
strikes: Strike[];
@@ -33,7 +34,7 @@ export const StrikesList: React.FC<StrikesListProps> = ({ strikes, onClickEdit,
3334

3435
const renderActions = (id: string) => {
3536
return (
36-
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', paddingRight: 1 }}>
37+
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', pr: 1 }}>
3738
<IconButton aria-label="edit" onClick={() => handleEdit(id)}>
3839
<EditIcon />
3940
</IconButton>
@@ -48,7 +49,7 @@ export const StrikesList: React.FC<StrikesListProps> = ({ strikes, onClickEdit,
4849
<List
4950
sx={{
5051
width: '100%',
51-
bgcolor: 'background.paper',
52+
backgroundColor: 'background.paper',
5253
maxHeight: 300,
5354
overflow: 'auto',
5455
scrollbarWidth: 'thin',
@@ -66,16 +67,15 @@ export const StrikesList: React.FC<StrikesListProps> = ({ strikes, onClickEdit,
6667
alignItems="flex-start"
6768
disablePadding
6869
sx={{
69-
bgcolor: index % 2 === 0 ? 'background.paperAlt' : 'background.paper',
70+
backgroundColor: index % 2 === 0 ? 'background.paperAlt' : 'background.paper',
7071
}}
7172
>
7273
<ListItemAvatar
7374
sx={{
7475
display: 'flex',
75-
alignIntems: 'center',
76-
paddingLeft: 2,
77-
paddingRight: 2,
78-
paddingTop: 1,
76+
alignItems: 'center',
77+
px: 2,
78+
pt: 1,
7979
}}
8080
>
8181
<AvatarWithTooltip imageUrl={strike.reporter.imageUrl} name={strike.reporter.name} />
@@ -87,11 +87,10 @@ export const StrikesList: React.FC<StrikesListProps> = ({ strikes, onClickEdit,
8787
flexDirection="row"
8888
justifyContent="space-between"
8989
width="100%"
90-
paddingTop={1}
91-
paddingBottom={1}
90+
py={1}
9291
>
9392
{formatStrikeReason(strike.reason || '')}
94-
<Typography sx={{ paddingRight: 2 }}>{formatDateForDisplay(strike.date)}</Typography>
93+
<Typography sx={{ pr: 2 }}>{formatDateForDisplay(strike.date)}</Typography>
9594
</Box>
9695
}
9796
secondary={

client/src/features/trainee-profile/employment/EmploymentInfo.tsx

Lines changed: 30 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,19 @@
11
import {
22
Box,
3-
Divider,
43
FormControl,
54
InputAdornment,
65
InputLabel,
76
Link,
8-
List,
9-
ListItem,
10-
ListItemText,
117
MenuItem,
128
Select,
139
TextField,
14-
Typography,
1510
} from '@mui/material';
1611
import { createSelectChangeHandler, createTextChangeHandler } from '../utils/formHelper';
17-
1812
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
1913
import { JobPathSelect } from '../profile/components/JobPathSelect';
2014
import LinkIcon from '@mui/icons-material/Link';
21-
import React from 'react';
22-
import { formatDate } from '../utils/dateHelper';
2315
import { useTraineeProfileContext } from '../context/useTraineeProfileContext';
16+
import { EmploymentHistoryGroup } from './history/EmploymentHistoryGroup';
2417

2518
const NoIcon = () => null;
2619

@@ -32,9 +25,11 @@ const NoIcon = () => null;
3225
export const EmploymentInfo = () => {
3326
const { trainee, setTrainee, isEditMode: isEditing } = useTraineeProfileContext();
3427
const { employmentInfo: editedFields } = trainee;
28+
3529
const handleTextChange = createTextChangeHandler(setTrainee, 'employmentInfo');
3630
const handleSelectChange = createSelectChangeHandler(setTrainee, 'employmentInfo');
3731

32+
3833
return (
3934
<Box display="flex" flexDirection="row" flexWrap="wrap" gap={4} padding="24px">
4035
<div style={{ width: '100%' }}>
@@ -48,21 +43,22 @@ export const EmploymentInfo = () => {
4843
name="cvURL"
4944
label="CV"
5045
type="url"
46+
placeholder="https://cv.example.com"
5147
value={editedFields?.cvURL || ''}
52-
InputProps={{
53-
readOnly: isEditing ? false : true,
54-
endAdornment: (
55-
<InputAdornment position="start">
56-
{!isEditing && editedFields?.cvURL && (
57-
<Link href={editedFields?.cvURL} target="_blank">
58-
<LinkIcon sx={{ color: 'action.active' }} />
59-
</Link>
60-
)}
61-
</InputAdornment>
62-
),
63-
}}
64-
InputLabelProps={{
65-
shrink: true,
48+
slotProps={{
49+
input: {
50+
readOnly: !isEditing,
51+
endAdornment: (
52+
<InputAdornment position="start">
53+
{!isEditing && editedFields?.cvURL && (
54+
<Link href={editedFields?.cvURL} target="_blank">
55+
<LinkIcon sx={{ color: 'action.active' }} />
56+
</Link>
57+
)}
58+
</InputAdornment>
59+
),
60+
},
61+
inputLabel: { shrink: true },
6662
}}
6763
variant={isEditing ? 'outlined' : 'standard'}
6864
onChange={handleTextChange}
@@ -78,9 +74,9 @@ export const EmploymentInfo = () => {
7874
name="availability"
7975
label="Availability"
8076
type="text"
77+
placeholder="From next month, fulltime"
8178
value={editedFields?.availability || ''}
82-
InputProps={{ readOnly: isEditing ? false : true }}
83-
InputLabelProps={{ shrink: true }}
79+
slotProps={{ input: { readOnly: !isEditing }, inputLabel: { shrink: true } }}
8480
variant={isEditing ? 'outlined' : 'standard'}
8581
onChange={handleTextChange}
8682
/>
@@ -95,9 +91,9 @@ export const EmploymentInfo = () => {
9591
name="preferredRole"
9692
label="Preferred role"
9793
type="text"
94+
placeholder="Backend"
9895
value={editedFields?.preferredRole || ''}
99-
InputProps={{ readOnly: isEditing ? false : true }}
100-
InputLabelProps={{ shrink: true }}
96+
slotProps={{ input: { readOnly: !isEditing }, inputLabel: { shrink: true } }}
10197
variant={isEditing ? 'outlined' : 'standard'}
10298
onChange={handleTextChange}
10399
/>
@@ -110,9 +106,9 @@ export const EmploymentInfo = () => {
110106
name="preferredLocation"
111107
label="Preferred location"
112108
type="text"
109+
placeholder="Randstad, Utrecht"
113110
value={editedFields?.preferredLocation || ''}
114-
InputProps={{ readOnly: isEditing ? false : true }}
115-
InputLabelProps={{ shrink: true }}
111+
slotProps={{ input: { readOnly: !isEditing }, inputLabel: { shrink: true } }}
116112
variant={isEditing ? 'outlined' : 'standard'}
117113
onChange={handleTextChange}
118114
/>
@@ -126,7 +122,7 @@ export const EmploymentInfo = () => {
126122
id="drivingLicense"
127123
label="Driving license"
128124
value={editedFields?.drivingLicense == null ? '' : editedFields?.drivingLicense}
129-
inputProps={{ readOnly: isEditing ? false : true }}
125+
slotProps={{ input: { readOnly: !isEditing } }}
130126
IconComponent={isEditing ? ArrowDropDownIcon : NoIcon}
131127
startAdornment=" "
132128
onChange={handleSelectChange}
@@ -145,49 +141,17 @@ export const EmploymentInfo = () => {
145141
name="extraTechnologies"
146142
label="Extra technologies"
147143
type="text"
144+
placeholder="C#, C++, Vue.js"
148145
value={editedFields?.extraTechnologies || ''}
149-
InputProps={{ readOnly: isEditing ? false : true }}
150-
InputLabelProps={{ shrink: true }}
146+
slotProps={{ input: { readOnly: !isEditing }, inputLabel: { shrink: true } }}
151147
variant={isEditing ? 'outlined' : 'standard'}
152148
onChange={handleTextChange}
153149
/>
154150
</FormControl>
155151
</div>
156152

157-
<div style={{ width: '100%' }}>
158-
{/* Employment history */}
159-
<Box display="flex" flexDirection="row" alignItems="center" justifyContent="space-between">
160-
<Typography variant="h6" padding="16px">
161-
Employment history ({editedFields?.employmentHistory.length || 0})
162-
</Typography>
163-
</Box>
164-
165-
<List
166-
sx={{
167-
width: '100%',
168-
bgcolor: 'background.paper',
169-
}}
170-
>
171-
{editedFields?.employmentHistory.map((employmentHistory, index) => (
172-
<React.Fragment key={employmentHistory.id}>
173-
<ListItem
174-
alignItems="flex-start"
175-
secondaryAction={formatDate(employmentHistory.startDate)}
176-
disablePadding
177-
sx={{
178-
paddingBottom: '16px',
179-
}}
180-
>
181-
<ListItemText
182-
primary={`${employmentHistory.role} at ${employmentHistory.companyName} (${employmentHistory.type})`}
183-
secondary={employmentHistory.comments}
184-
/>
185-
</ListItem>
186-
{index < editedFields?.employmentHistory.length - 1 && <Divider sx={{ color: 'black' }} component="li" />}
187-
</React.Fragment>
188-
))}
189-
</List>
190-
</div>
153+
{/* Employment history */}
154+
<EmploymentHistoryGroup />
191155

192156
<div style={{ width: '100%' }}>
193157
{/* Comments */}
@@ -199,8 +163,7 @@ export const EmploymentInfo = () => {
199163
type="text"
200164
multiline
201165
value={editedFields?.comments || ''}
202-
InputProps={{ readOnly: isEditing ? false : true }}
203-
InputLabelProps={{ shrink: true }}
166+
slotProps={{ input: { readOnly: !isEditing }, inputLabel: { shrink: true } }}
204167
variant={isEditing ? 'outlined' : 'standard'}
205168
onChange={handleTextChange}
206169
/>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import axios from 'axios';
2+
import { EmploymentHistory } from '../../../../data/types/Trainee';
3+
4+
export const getEmployments = async (traineeId: string) => {
5+
const { data } = await axios.get<EmploymentHistory[]>(`/api/trainees/${traineeId}/employment-history`);
6+
return data;
7+
};
8+
9+
export const addEmployment = async (traineeId: string, employment: EmploymentHistory) => {
10+
await axios.post(`/api/trainees/${traineeId}/employment-history`, employment);
11+
};
12+
13+
export const deleteEmployment = async (traineeId: string, employmentId: string) => {
14+
await axios.delete(`/api/trainees/${traineeId}/employment-history/${employmentId}`);
15+
};
16+
17+
export const editEmployment = async (traineeId: string, employment: EmploymentHistory) => {
18+
await axios.put(`/api/trainees/${traineeId}/employment-history/${employment.id}`, employment);
19+
};
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { useQuery } from '@tanstack/react-query';
2+
import { EmploymentHistory } from '../../../../data/types/Trainee';
3+
import { getEmployments } from '../api/api';
4+
5+
export const employmentHistoryKeys = {
6+
all: ['employmentHistory'] as const, // for broad invalidation
7+
byQuery: (traineeId: string) => ['employmentHistory', traineeId] as const,
8+
// per-term cache
9+
};
10+
11+
/**
12+
* Hook to get employments of a trainee.
13+
* @param {string} traineeId the id of the trainee to get the employments from.
14+
* @returns {UseQueryResult<EmploymentHistory[], Error>} the employments of the trainee.
15+
*/
16+
export const useGetEmploymentHistory = (traineeId: string) => {
17+
return useQuery({
18+
queryKey: employmentHistoryKeys.byQuery(traineeId),
19+
queryFn: async () => {
20+
const data = await getEmployments(traineeId);
21+
return orderEmploymentHistoryByDateDesc(data);
22+
},
23+
enabled: !!traineeId,
24+
refetchOnWindowFocus: false,
25+
});
26+
};
27+
28+
const orderEmploymentHistoryByDateDesc = (data: EmploymentHistory[]): EmploymentHistory[] => {
29+
return data.sort((a, b) => new Date(b.startDate).getTime() - new Date(a.startDate).getTime());
30+
};
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { useMutation } from '@tanstack/react-query';
2+
import { EmploymentHistory } from '../../../../data/types/Trainee'
3+
import { addEmployment, deleteEmployment, editEmployment } from '../api/api';
4+
5+
/**
6+
* Hook to add employment to a trainee.
7+
* @param {string} traineeId the id of the trainee to add the employment to.
8+
* @param {EmploymentHistory} employment the employment to add.
9+
*/
10+
export const useAddEmploymentHistory = (traineeId: string) => {
11+
return useMutation({
12+
mutationFn: (employment: EmploymentHistory) => addEmployment(traineeId, employment),
13+
});
14+
};
15+
16+
/**
17+
* Hook to delete employment from a trainee.
18+
* @param {string} traineeId the id of the trainee to delete the employment from.
19+
* @param {string} employmentId the id of the employment to delete.
20+
* */
21+
22+
export const useDeleteEmploymentHistory = (traineeId: string) => {
23+
return useMutation({
24+
mutationFn: (employmentId: string) => deleteEmployment(traineeId, employmentId),
25+
});
26+
};
27+
28+
/**
29+
* Hook to edit employment of a trainee.
30+
* @param {string} traineeId the id of the trainee to edit the employment of.
31+
*/
32+
export const useEditEmploymentHistory = (traineeId: string) => {
33+
return useMutation({
34+
mutationFn: (employment: EmploymentHistory) => editEmployment(traineeId, employment),
35+
});
36+
};

0 commit comments

Comments
 (0)