From 6f8d335a1151ec8f4e6756ef8fc134729c5435b6 Mon Sep 17 00:00:00 2001 From: zalexa19 Date: Sun, 15 Feb 2026 11:35:51 +0100 Subject: [PATCH 01/15] added global error handling for queries as well --- client/src/data/tanstack/tanstackClient.ts | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/client/src/data/tanstack/tanstackClient.ts b/client/src/data/tanstack/tanstackClient.ts index aa83838c..71e4393c 100644 --- a/client/src/data/tanstack/tanstackClient.ts +++ b/client/src/data/tanstack/tanstackClient.ts @@ -43,17 +43,18 @@ export const resetNavigateTo = () => { navigateTo = null; }; +const handleError = (error: unknown) => { + const axiosError = error as AxiosError | undefined; + // If the backend returned 401, redirect to login using the stored navigate. + // If `navigateTo` is not set (e.g. during tests), this is a no-op. + if (axiosError?.response?.status === 401) { + navigateTo?.('/login', { replace: true }); + } +}; + // Centralized QueryCache. Global error handling happens here. const queryCache = new QueryCache({ - onError(error: unknown) { - const axiosError = error as AxiosError | undefined; - - // If the backend returned 401, redirect to login using the stored navigate. - // If `navigateTo` is not set (e.g. during tests), this is a no-op. - if (axiosError?.response?.status === 401) { - navigateTo?.('/login', { replace: true }); - } - }, + onError: handleError, }); // App-wide QueryClient using the cache above. Import and pass this to @@ -62,6 +63,6 @@ export const queryClient = new QueryClient({ queryCache, defaultOptions: { queries: { retry: 1 }, - mutations: { retry: 1 }, + mutations: { retry: 1, onError: handleError }, }, }); From 2f516bb40485efc525056f76c3f7a1b33f19ab6c Mon Sep 17 00:00:00 2001 From: zalexa19 Date: Sun, 15 Feb 2026 11:40:26 +0100 Subject: [PATCH 02/15] added global error handling to queries as well --- .../education/strikes/api/api.ts | 24 ++++--------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/client/src/features/trainee-profile/education/strikes/api/api.ts b/client/src/features/trainee-profile/education/strikes/api/api.ts index 0f737daa..02cdeeaa 100644 --- a/client/src/features/trainee-profile/education/strikes/api/api.ts +++ b/client/src/features/trainee-profile/education/strikes/api/api.ts @@ -1,21 +1,13 @@ +import { Strike } from '../models/strike'; import axios from 'axios'; -import { Strike } from '../../../../../data/types/Trainee'; export const getStrikes = async (traineeId: string) => { - const { data } = await axios.get(`/api/trainees/${traineeId}/strikes`); + const { data } = await axios.get(`/api/trainees/${traineeId}/strikes`); return data; }; export const addStrike = async (traineeId: string, strike: Strike) => { - try { - await axios.post(`/api/trainees/${traineeId}/strikes`, strike); - } catch (error) { - if (axios.isAxiosError(error)) { - throw new Error(error.response?.data?.error || 'Failed to add strike'); - } - - throw new Error(error instanceof Error ? error.message : 'An unexpected error occurred'); - } + await axios.post(`/api/trainees/${traineeId}/strikes`, strike); }; export const deleteStrike = async (traineeId: string, strikeId: string) => { @@ -23,13 +15,5 @@ export const deleteStrike = async (traineeId: string, strikeId: string) => { }; export const editStrike = async (traineeId: string, strike: Strike) => { - try { - await axios.put(`/api/trainees/${traineeId}/strikes/${strike.id}`, strike); - } catch (error) { - if (axios.isAxiosError(error)) { - throw new Error(error.response?.data?.error || 'Failed to edit strike'); - } - - throw new Error(error instanceof Error ? error.message : 'An unexpected error occurred'); - } + await axios.put(`/api/trainees/${traineeId}/strikes/${strike.id}`, strike); }; From 7d4f79346d89a2aa1fcfb1d7e5d47f745d48fcfd Mon Sep 17 00:00:00 2001 From: zalexa19 Date: Sun, 15 Feb 2026 11:46:21 +0100 Subject: [PATCH 03/15] cerated new model types for strikes --- .../education/strikes/api/types.ts | 22 +++++++++++++++++++ .../education/strikes/models/strike.ts | 20 +++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 client/src/features/trainee-profile/education/strikes/api/types.ts create mode 100644 client/src/features/trainee-profile/education/strikes/models/strike.ts diff --git a/client/src/features/trainee-profile/education/strikes/api/types.ts b/client/src/features/trainee-profile/education/strikes/api/types.ts new file mode 100644 index 00000000..f97c7a3f --- /dev/null +++ b/client/src/features/trainee-profile/education/strikes/api/types.ts @@ -0,0 +1,22 @@ +import { StrikeReason } from '../models/strike'; + +export interface StrikeRequest { + reason: StrikeReason; + date: string; // ISO String for backend + comments: string; + reporterID?: string; // Optional because the backend can default to the session user +} + +interface ReporterDTO { + id: string; + name: string; + imageUrl?: string; +} + +export interface StrikeResponse { + id: string; + comments: string; + date: string; + reason: StrikeReason; + reporter: ReporterDTO; +} diff --git a/client/src/features/trainee-profile/education/strikes/models/strike.ts b/client/src/features/trainee-profile/education/strikes/models/strike.ts new file mode 100644 index 00000000..10fe321f --- /dev/null +++ b/client/src/features/trainee-profile/education/strikes/models/strike.ts @@ -0,0 +1,20 @@ +export type Strike = { + readonly id: string; + date: Date; + reason: StrikeReason; + comments: string; + reporterName?: string; + reporterImageUrl?: string; +}; + +export type StrikeInput = Omit; + +export enum StrikeReason { + LateSubmission = 'late-submission', + MissedSubmission = 'missed-submission', + IncompleteSubmission = 'incomplete-submission', + LateAttendance = 'late-attendance', + Absence = 'absence', + PendingFeedback = 'pending-feedback', + Other = 'other', +} From d7db06f23300f95c5c119a4a78c7ebffb7583e35 Mon Sep 17 00:00:00 2001 From: zalexa19 Date: Sun, 15 Feb 2026 11:47:01 +0100 Subject: [PATCH 04/15] added a key factory for strikes --- .../features/trainee-profile/education/strikes/data/keys.ts | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 client/src/features/trainee-profile/education/strikes/data/keys.ts diff --git a/client/src/features/trainee-profile/education/strikes/data/keys.ts b/client/src/features/trainee-profile/education/strikes/data/keys.ts new file mode 100644 index 00000000..bbfca12b --- /dev/null +++ b/client/src/features/trainee-profile/education/strikes/data/keys.ts @@ -0,0 +1,5 @@ +export const strikeKeys = { + all: ['strikes'] as const, + list: (traineeId: string) => [...strikeKeys.all, 'list', traineeId] as const, + detail: (strikeId: string) => [...strikeKeys.all, 'detail', strikeId] as const, +}; From 52551f85096058e2aa118d13bf9725d953ddab20 Mon Sep 17 00:00:00 2001 From: zalexa19 Date: Sun, 15 Feb 2026 11:47:27 +0100 Subject: [PATCH 05/15] added a mappers for domain types --- .../education/strikes/api/mapper.ts | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 client/src/features/trainee-profile/education/strikes/api/mapper.ts diff --git a/client/src/features/trainee-profile/education/strikes/api/mapper.ts b/client/src/features/trainee-profile/education/strikes/api/mapper.ts new file mode 100644 index 00000000..eaa1a174 --- /dev/null +++ b/client/src/features/trainee-profile/education/strikes/api/mapper.ts @@ -0,0 +1,41 @@ +import { Strike, StrikeReason } from '../models/strike'; +import { StrikeRequest, StrikeResponse } from './types'; + +export const mapStrikeToDomain = (dto: StrikeResponse): Strike => { + return { + id: dto.id, + comments: dto.comments, + date: new Date(dto.date), + reason: mapStringToStrikeReason(dto.reason), + reporterName: dto.reporter.name, + reporterImageUrl: dto.reporter.imageUrl, + }; +}; + +export const mapDomainToStrikeRequest = (strike: Strike): StrikeRequest => { + return { + reason: strike.reason, + date: strike.date.toISOString(), + comments: strike.comments, + }; +}; +const mapStringToStrikeReason = (reason: string): StrikeReason => { + switch (reason) { + case 'late-submission': + return StrikeReason.LateSubmission; + case 'missed-submission': + return StrikeReason.MissedSubmission; + case 'incomplete-submission': + return StrikeReason.IncompleteSubmission; + case 'late-attendance': + return StrikeReason.LateAttendance; + case 'absence': + return StrikeReason.Absence; + case 'pending-feedback': + return StrikeReason.PendingFeedback; + case 'other': + return StrikeReason.Other; + default: + throw new Error(`Unknown strike reason: ${reason}`); + } +}; From 5981dec03a66105a3e878c8abd899c50ed71d638 Mon Sep 17 00:00:00 2001 From: zalexa19 Date: Sun, 15 Feb 2026 12:10:45 +0100 Subject: [PATCH 06/15] created a seperation between ui model and api types --- client/src/data/types/Trainee.ts | 19 ------- .../education/strikes/StrikeDetailsModal.tsx | 23 ++++---- .../education/strikes/StrikesComponent.tsx | 11 ++-- .../education/strikes/api/api.ts | 11 ++-- .../education/strikes/api/mapper.ts | 7 ++- .../education/strikes/api/types.ts | 1 + .../education/strikes/data/mutations.ts | 55 +++++++++++++++++++ .../education/strikes/data/strike-queries.ts | 43 ++------------- .../education/strikes/models/strike.ts | 4 +- 9 files changed, 93 insertions(+), 81 deletions(-) create mode 100644 client/src/features/trainee-profile/education/strikes/data/mutations.ts diff --git a/client/src/data/types/Trainee.ts b/client/src/data/types/Trainee.ts index 70a9906d..dc9be402 100644 --- a/client/src/data/types/Trainee.ts +++ b/client/src/data/types/Trainee.ts @@ -53,16 +53,6 @@ export enum LearningStatus { Quit = 'quit', } -export enum StrikeReason { - LateSubmission = 'late-submission', - MissedSubmission = 'missed-submission', - IncompleteSubmission = 'incomplete-submission', - LateAttendance = 'late-attendance', - Absence = 'absence', - PendingFeedback = 'pending-feedback', - Other = 'other', -} - export enum QuitReason { Technical = 'technical', SocialSkills = 'social-skills', @@ -180,15 +170,6 @@ export interface TraineeEmploymentInfo { comments?: string; } -export interface Strike { - readonly id: string; - date: Date; - reporterID: string; - reason: StrikeReason | null; - comments: string; - reporter: ReporterWithId; -} - export interface Assignment { readonly id: string; createDate: Date; diff --git a/client/src/features/trainee-profile/education/strikes/StrikeDetailsModal.tsx b/client/src/features/trainee-profile/education/strikes/StrikeDetailsModal.tsx index 52df0748..42b4f3f8 100644 --- a/client/src/features/trainee-profile/education/strikes/StrikeDetailsModal.tsx +++ b/client/src/features/trainee-profile/education/strikes/StrikeDetailsModal.tsx @@ -14,7 +14,7 @@ import { TextField, Typography, } from '@mui/material'; -import { Strike, StrikeReason } from '../../../../data/types/Trainee'; +import { Strike, StrikeInput, StrikeReason } from './models/strike'; import { LoadingButton } from '@mui/lab'; import { formatDate } from '../../utils/dateHelper'; @@ -39,13 +39,12 @@ export const StrikeDetailsModal = ({ onConfirmEdit, initialStrike, }: StrikeDetailsModalProps) => { - const [strikeFields, setStrikeFields] = useState({ + const [strikeFields, setStrikeFields] = useState({ id: initialStrike?.id || '', - date: initialStrike?.date || new Date(), - reporterID: initialStrike?.reporterID || '', + date: initialStrike ? new Date(initialStrike?.date) : new Date(), comments: initialStrike?.comments || '', reason: initialStrike?.reason || null, - } as Strike); + }); const [commentsRequiredError, setCommentsRequiredError] = useState(false); const [reasonRequiredError, setReasonRequiredError] = useState(false); @@ -57,7 +56,7 @@ export const StrikeDetailsModal = ({ const handleStrikeChange = (e: React.ChangeEvent) => { const { name, value } = e.target; - setStrikeFields((prevStrike: Strike) => ({ + setStrikeFields((prevStrike: StrikeInput) => ({ ...prevStrike, [name]: name === 'date' ? new Date(value) : value, })); @@ -66,12 +65,10 @@ export const StrikeDetailsModal = ({ const handleStrikeSelectChange = (event: SelectChangeEvent) => { const { name, value } = event.target; setReasonRequiredError(false); - setStrikeFields( - (prevStrike: Strike): Strike => ({ - ...prevStrike, - [name]: value, - }) - ); + setStrikeFields((prevStrike: StrikeInput) => ({ + ...prevStrike, + [name]: value, + })); }; const onConfirm = async () => { @@ -96,7 +93,7 @@ export const StrikeDetailsModal = ({ setCommentsRequiredError(false); const { name, value } = e.target; - setStrikeFields((prevStrike) => ({ + setStrikeFields((prevStrike: StrikeInput) => ({ ...prevStrike, [name]: value, })); diff --git a/client/src/features/trainee-profile/education/strikes/StrikesComponent.tsx b/client/src/features/trainee-profile/education/strikes/StrikesComponent.tsx index 96506551..f15a424c 100644 --- a/client/src/features/trainee-profile/education/strikes/StrikesComponent.tsx +++ b/client/src/features/trainee-profile/education/strikes/StrikesComponent.tsx @@ -1,11 +1,12 @@ import { Alert, Box, Button, CircularProgress, Stack, Typography } from '@mui/material'; -import { useAddStrike, useDeleteStrike, useEditStrike, useGetStrikes } from './data/strike-queries'; +import { Strike, StrikeInput } from './models/strike'; +import { useAddStrike, useDeleteStrike, useEditStrike } from './data/mutations'; import AddIcon from '@mui/icons-material/Add'; import { ConfirmationDialog } from '../../../../components/ConfirmationDialog'; -import { Strike } from '../../../../data/types/Trainee'; import { StrikeDetailsModal } from './StrikeDetailsModal'; import { StrikesList } from './StrikesList'; +import { useGetStrikes } from './data/strike-queries'; import { useQueryClient } from '@tanstack/react-query'; import { useState } from 'react'; import { useTraineeProfileContext } from '../../context/useTraineeProfileContext'; @@ -34,13 +35,13 @@ export const StrikesComponent = () => { }; const onClickEdit = (id: string) => { - const strike = strikes?.find((strike) => strike.id === id) || null; + const strike = strikes?.find((strike: Strike) => strike.id === id) || null; setStrikeToEdit(strike); setIsModalOpen(true); }; - const onConfirmAdd = async (strike: Strike) => { + const onConfirmAdd = async (strike: StrikeInput) => { if (modalError) setModalError(''); addStrike(strike, { onSuccess: handleSuccess, @@ -90,7 +91,7 @@ export const StrikesComponent = () => { deleteStrike(idToDelete, { onSuccess: () => { setIsConfirmationDialogOpen(false); - queryClient.invalidateQueries({ queryKey: ['strikes', traineeId] }); + // queryClient.invalidateQueries({ queryKey: ['strikes', traineeId] }); }, }); }; diff --git a/client/src/features/trainee-profile/education/strikes/api/api.ts b/client/src/features/trainee-profile/education/strikes/api/api.ts index 02cdeeaa..a1ec2c1b 100644 --- a/client/src/features/trainee-profile/education/strikes/api/api.ts +++ b/client/src/features/trainee-profile/education/strikes/api/api.ts @@ -1,4 +1,5 @@ import { Strike } from '../models/strike'; +import { StrikeRequest } from './types'; import axios from 'axios'; export const getStrikes = async (traineeId: string) => { @@ -6,14 +7,14 @@ export const getStrikes = async (traineeId: string) => { return data; }; -export const addStrike = async (traineeId: string, strike: Strike) => { - await axios.post(`/api/trainees/${traineeId}/strikes`, strike); -}; - export const deleteStrike = async (traineeId: string, strikeId: string) => { await axios.delete(`/api/trainees/${traineeId}/strikes/${strikeId}`); }; -export const editStrike = async (traineeId: string, strike: Strike) => { +export const addStrike = async (traineeId: string, strike: StrikeRequest) => { + await axios.post(`/api/trainees/${traineeId}/strikes`, strike); +}; + +export const editStrike = async (traineeId: string, strike: StrikeRequest) => { await axios.put(`/api/trainees/${traineeId}/strikes/${strike.id}`, strike); }; diff --git a/client/src/features/trainee-profile/education/strikes/api/mapper.ts b/client/src/features/trainee-profile/education/strikes/api/mapper.ts index eaa1a174..e2688271 100644 --- a/client/src/features/trainee-profile/education/strikes/api/mapper.ts +++ b/client/src/features/trainee-profile/education/strikes/api/mapper.ts @@ -13,11 +13,16 @@ export const mapStrikeToDomain = (dto: StrikeResponse): Strike => { }; export const mapDomainToStrikeRequest = (strike: Strike): StrikeRequest => { - return { + console.log(strike); + const request: StrikeRequest = { reason: strike.reason, date: strike.date.toISOString(), comments: strike.comments, }; + if (strike.id) { + request.id = strike.id; + } + return request; }; const mapStringToStrikeReason = (reason: string): StrikeReason => { switch (reason) { diff --git a/client/src/features/trainee-profile/education/strikes/api/types.ts b/client/src/features/trainee-profile/education/strikes/api/types.ts index f97c7a3f..953a4b4f 100644 --- a/client/src/features/trainee-profile/education/strikes/api/types.ts +++ b/client/src/features/trainee-profile/education/strikes/api/types.ts @@ -1,6 +1,7 @@ import { StrikeReason } from '../models/strike'; export interface StrikeRequest { + id?: string; reason: StrikeReason; date: string; // ISO String for backend comments: string; diff --git a/client/src/features/trainee-profile/education/strikes/data/mutations.ts b/client/src/features/trainee-profile/education/strikes/data/mutations.ts new file mode 100644 index 00000000..5f907b4d --- /dev/null +++ b/client/src/features/trainee-profile/education/strikes/data/mutations.ts @@ -0,0 +1,55 @@ +import { QueryClient, useMutation, useQueryClient } from '@tanstack/react-query'; +import { addStrike, deleteStrike, editStrike } from '../api/api'; + +import { Strike } from '../models/strike'; +import { mapDomainToStrikeRequest } from '../api/mapper'; +import { strikeKeys } from './keys'; + +const invalidateStrikesQuery = (queryClient: QueryClient, traineeId: string) => { + return queryClient.invalidateQueries({ queryKey: strikeKeys.list(traineeId) }); +}; + +/** + * Hook to add a strike to a trainee. + * @param {string} traineeId the id of the trainee to add the strike to. + */ +export const useAddStrike = (traineeId: string) => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (strike: Strike) => { + const request = mapDomainToStrikeRequest(strike); + return addStrike(traineeId, request); + }, + onSuccess: async () => await invalidateStrikesQuery(queryClient, traineeId), + }); +}; + +/** + * Hook to delete a strike from a trainee. + * @param {string} traineeId the id of the trainee to delete the strike from. + * */ + +export const useDeleteStrike = (traineeId: string) => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (strikeId: string) => deleteStrike(traineeId, strikeId), + onSuccess: async () => await invalidateStrikesQuery(queryClient, traineeId), + }); +}; + +/** + * 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) => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (strike: Strike) => { + const request = mapDomainToStrikeRequest(strike); + return editStrike(traineeId, request); + }, + onSuccess: async () => await invalidateStrikesQuery(queryClient, traineeId), + }); +}; diff --git a/client/src/features/trainee-profile/education/strikes/data/strike-queries.ts b/client/src/features/trainee-profile/education/strikes/data/strike-queries.ts index d5b2937f..0457c263 100644 --- a/client/src/features/trainee-profile/education/strikes/data/strike-queries.ts +++ b/client/src/features/trainee-profile/education/strikes/data/strike-queries.ts @@ -1,17 +1,7 @@ -import { useMutation, useQuery } from '@tanstack/react-query'; -import { Strike } from '../../../../../data/types/Trainee'; -import { getStrikes, addStrike, deleteStrike, editStrike } from '../api/api'; - -/** - * Hook to add a strike to a trainee. - * @param {string} traineeId the id of the trainee to add the strike to. - * @param {Strike} strike the strike to add. - */ -export const useAddStrike = (traineeId: string) => { - return useMutation({ - mutationFn: (strike: Strike) => addStrike(traineeId, strike), - }); -}; +import { Strike } from '../models/strike'; +import { getStrikes } from '../api/api'; +import { strikeKeys } from './keys'; +import { useQuery } from '@tanstack/react-query'; /** * Hook to get the strikes of a trainee. @@ -20,9 +10,10 @@ export const useAddStrike = (traineeId: string) => { */ export const useGetStrikes = (traineeId: string) => { return useQuery({ - queryKey: ['strikes', traineeId], + queryKey: strikeKeys.list(traineeId), queryFn: async () => { const data = await getStrikes(traineeId); + return orderStrikesByDateDesc(data); }, enabled: !!traineeId, @@ -30,28 +21,6 @@ export const useGetStrikes = (traineeId: string) => { }); }; -/** - * 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: (strikeId: string) => deleteStrike(traineeId, 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: (strike: Strike) => editStrike(traineeId, strike), - }); -}; - const orderStrikesByDateDesc = (data: Strike[]): Strike[] => { return data.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); }; diff --git a/client/src/features/trainee-profile/education/strikes/models/strike.ts b/client/src/features/trainee-profile/education/strikes/models/strike.ts index 10fe321f..28cff5fe 100644 --- a/client/src/features/trainee-profile/education/strikes/models/strike.ts +++ b/client/src/features/trainee-profile/education/strikes/models/strike.ts @@ -7,7 +7,9 @@ export type Strike = { reporterImageUrl?: string; }; -export type StrikeInput = Omit; +export type StrikeInput = Omit & { + reason: StrikeReason | null; //optional for form handling +}; export enum StrikeReason { LateSubmission = 'late-submission', From f44afd552457e2659fd699679a4b7d79c6158ece Mon Sep 17 00:00:00 2001 From: zalexa19 Date: Sun, 15 Feb 2026 12:31:49 +0100 Subject: [PATCH 07/15] moved stripeInput field back inside --- .vscode/settings.json | 22 ++++++++++++++++++- .../education/strikes/StrikeDetailsModal.tsx | 17 +++++++++++--- .../education/strikes/StrikesComponent.tsx | 8 ++----- .../education/strikes/api/mapper.ts | 1 - .../education/strikes/models/strike.ts | 4 ---- 5 files changed, 37 insertions(+), 15 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index aed6c8d8..32e78d64 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,5 +3,25 @@ "cSpell.words": [ "Docxtemplater", "Gotenberg" - ] + ], + "workbench.colorCustomizations": { + "activityBar.activeBackground": "#5f459a", + "activityBar.background": "#5f459a", + "activityBar.foreground": "#e7e7e7", + "activityBar.inactiveForeground": "#e7e7e799", + "activityBarBadge.background": "#2c1b14", + "activityBarBadge.foreground": "#e7e7e7", + "commandCenter.border": "#e7e7e799", + "sash.hoverBorder": "#5f459a", + "statusBar.background": "#493577", + "statusBar.foreground": "#e7e7e7", + "statusBarItem.hoverBackground": "#5f459a", + "statusBarItem.remoteBackground": "#493577", + "statusBarItem.remoteForeground": "#e7e7e7", + "titleBar.activeBackground": "#493577", + "titleBar.activeForeground": "#e7e7e7", + "titleBar.inactiveBackground": "#49357799", + "titleBar.inactiveForeground": "#e7e7e799" + }, + "peacock.color": "#493577" } \ No newline at end of file diff --git a/client/src/features/trainee-profile/education/strikes/StrikeDetailsModal.tsx b/client/src/features/trainee-profile/education/strikes/StrikeDetailsModal.tsx index 42b4f3f8..54fed171 100644 --- a/client/src/features/trainee-profile/education/strikes/StrikeDetailsModal.tsx +++ b/client/src/features/trainee-profile/education/strikes/StrikeDetailsModal.tsx @@ -14,12 +14,16 @@ import { TextField, Typography, } from '@mui/material'; -import { Strike, StrikeInput, StrikeReason } from './models/strike'; +import { Strike, StrikeReason } from './models/strike'; import { LoadingButton } from '@mui/lab'; import { formatDate } from '../../utils/dateHelper'; import { useState } from 'react'; +type StrikeInput = Omit & { + reason: StrikeReason | null; //optional for form handling +}; + interface StrikeDetailsModalProps { isOpen: boolean; error: string; @@ -81,10 +85,17 @@ export const StrikeDetailsModal = ({ return; } + const strikeToSave: Strike = { + id: strikeFields.id, + date: strikeFields.date, + reason: strikeFields.reason!, + comments: strikeFields.comments, + }; + if (initialStrike) { - onConfirmEdit(strikeFields); + onConfirmEdit(strikeToSave); } else { - onConfirmAdd(strikeFields); + onConfirmAdd(strikeToSave); } }; diff --git a/client/src/features/trainee-profile/education/strikes/StrikesComponent.tsx b/client/src/features/trainee-profile/education/strikes/StrikesComponent.tsx index f15a424c..361b651b 100644 --- a/client/src/features/trainee-profile/education/strikes/StrikesComponent.tsx +++ b/client/src/features/trainee-profile/education/strikes/StrikesComponent.tsx @@ -1,13 +1,12 @@ import { Alert, Box, Button, CircularProgress, Stack, Typography } from '@mui/material'; -import { Strike, StrikeInput } from './models/strike'; import { useAddStrike, useDeleteStrike, useEditStrike } from './data/mutations'; import AddIcon from '@mui/icons-material/Add'; import { ConfirmationDialog } from '../../../../components/ConfirmationDialog'; +import { Strike } from './models/strike'; import { StrikeDetailsModal } from './StrikeDetailsModal'; import { StrikesList } from './StrikesList'; import { useGetStrikes } from './data/strike-queries'; -import { useQueryClient } from '@tanstack/react-query'; import { useState } from 'react'; import { useTraineeProfileContext } from '../../context/useTraineeProfileContext'; @@ -24,9 +23,7 @@ export const StrikesComponent = () => { const [isConfirmationDialogOpen, setIsConfirmationDialogOpen] = useState(false); const [idToDelete, setIdToDelete] = useState(''); - const queryClient = useQueryClient(); const handleSuccess = () => { - queryClient.invalidateQueries({ queryKey: ['strikes', traineeId] }); setIsModalOpen(false); }; @@ -41,7 +38,7 @@ export const StrikesComponent = () => { setIsModalOpen(true); }; - const onConfirmAdd = async (strike: StrikeInput) => { + const onConfirmAdd = async (strike: Strike) => { if (modalError) setModalError(''); addStrike(strike, { onSuccess: handleSuccess, @@ -91,7 +88,6 @@ export const StrikesComponent = () => { deleteStrike(idToDelete, { onSuccess: () => { setIsConfirmationDialogOpen(false); - // queryClient.invalidateQueries({ queryKey: ['strikes', traineeId] }); }, }); }; diff --git a/client/src/features/trainee-profile/education/strikes/api/mapper.ts b/client/src/features/trainee-profile/education/strikes/api/mapper.ts index e2688271..2ab95d72 100644 --- a/client/src/features/trainee-profile/education/strikes/api/mapper.ts +++ b/client/src/features/trainee-profile/education/strikes/api/mapper.ts @@ -13,7 +13,6 @@ export const mapStrikeToDomain = (dto: StrikeResponse): Strike => { }; export const mapDomainToStrikeRequest = (strike: Strike): StrikeRequest => { - console.log(strike); const request: StrikeRequest = { reason: strike.reason, date: strike.date.toISOString(), diff --git a/client/src/features/trainee-profile/education/strikes/models/strike.ts b/client/src/features/trainee-profile/education/strikes/models/strike.ts index 28cff5fe..9ab9b0d0 100644 --- a/client/src/features/trainee-profile/education/strikes/models/strike.ts +++ b/client/src/features/trainee-profile/education/strikes/models/strike.ts @@ -7,10 +7,6 @@ export type Strike = { reporterImageUrl?: string; }; -export type StrikeInput = Omit & { - reason: StrikeReason | null; //optional for form handling -}; - export enum StrikeReason { LateSubmission = 'late-submission', MissedSubmission = 'missed-submission', From 25c36ca0b724349bf943f6bfbea1d1671e88112f Mon Sep 17 00:00:00 2001 From: zalexa19 Date: Sun, 15 Feb 2026 16:05:22 +0100 Subject: [PATCH 08/15] fixed lint --- .vscode/settings.json | 20 ------------------- .../education/strikes/StrikesList.tsx | 4 ++-- .../education/strikes/api/api.ts | 1 - 3 files changed, 2 insertions(+), 23 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 32e78d64..ad6922c9 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,24 +4,4 @@ "Docxtemplater", "Gotenberg" ], - "workbench.colorCustomizations": { - "activityBar.activeBackground": "#5f459a", - "activityBar.background": "#5f459a", - "activityBar.foreground": "#e7e7e7", - "activityBar.inactiveForeground": "#e7e7e799", - "activityBarBadge.background": "#2c1b14", - "activityBarBadge.foreground": "#e7e7e7", - "commandCenter.border": "#e7e7e799", - "sash.hoverBorder": "#5f459a", - "statusBar.background": "#493577", - "statusBar.foreground": "#e7e7e7", - "statusBarItem.hoverBackground": "#5f459a", - "statusBarItem.remoteBackground": "#493577", - "statusBarItem.remoteForeground": "#e7e7e7", - "titleBar.activeBackground": "#493577", - "titleBar.activeForeground": "#e7e7e7", - "titleBar.inactiveBackground": "#49357799", - "titleBar.inactiveForeground": "#e7e7e799" - }, - "peacock.color": "#493577" } \ No newline at end of file diff --git a/client/src/features/trainee-profile/education/strikes/StrikesList.tsx b/client/src/features/trainee-profile/education/strikes/StrikesList.tsx index 6d8e022b..598e6843 100644 --- a/client/src/features/trainee-profile/education/strikes/StrikesList.tsx +++ b/client/src/features/trainee-profile/education/strikes/StrikesList.tsx @@ -4,7 +4,7 @@ import { AvatarWithTooltip } from '../components/AvatarWithTooltip'; import DeleteIcon from '@mui/icons-material/Delete'; import EditIcon from '@mui/icons-material/Edit'; import MarkdownText from '../../components/MarkdownText'; -import { Strike } from '../../../../data/types/Trainee'; +import { Strike } from './models/strike'; import { formatDateForDisplay } from '../../utils/dateHelper'; import React from 'react'; @@ -78,7 +78,7 @@ export const StrikesList: React.FC = ({ strikes, onClickEdit, pt: 1, }} > - + Date: Sun, 15 Feb 2026 16:57:27 +0100 Subject: [PATCH 09/15] fixed lint and pr comments --- .../education/strikes/StrikesList.tsx | 2 +- .../education/strikes/api/api.ts | 7 ++--- .../education/strikes/api/mapper.ts | 26 ++++++------------- .../education/strikes/data/keys.ts | 6 ++--- 4 files changed, 16 insertions(+), 25 deletions(-) diff --git a/client/src/features/trainee-profile/education/strikes/StrikesList.tsx b/client/src/features/trainee-profile/education/strikes/StrikesList.tsx index 598e6843..0f5b7e9a 100644 --- a/client/src/features/trainee-profile/education/strikes/StrikesList.tsx +++ b/client/src/features/trainee-profile/education/strikes/StrikesList.tsx @@ -78,7 +78,7 @@ export const StrikesList: React.FC = ({ strikes, onClickEdit, pt: 1, }} > - + { - const { data } = await axios.get(`/api/trainees/${traineeId}/strikes`); + const { data } = await axios.get(`/api/trainees/${traineeId}/strikes`); return data; }; @@ -15,5 +16,5 @@ export const addStrike = async (traineeId: string, strike: StrikeRequest) => { }; export const editStrike = async (traineeId: string, strike: StrikeRequest) => { - await axios.put(`/api/trainees/${traineeId}/strikes/${strike.id}`, strike); + await axios.put(`/api/trainees/${traineeId}/strikes/${strike.id!}`, strike); }; diff --git a/client/src/features/trainee-profile/education/strikes/api/mapper.ts b/client/src/features/trainee-profile/education/strikes/api/mapper.ts index 2ab95d72..71ff8aeb 100644 --- a/client/src/features/trainee-profile/education/strikes/api/mapper.ts +++ b/client/src/features/trainee-profile/education/strikes/api/mapper.ts @@ -23,23 +23,13 @@ export const mapDomainToStrikeRequest = (strike: Strike): StrikeRequest => { } return request; }; -const mapStringToStrikeReason = (reason: string): StrikeReason => { - switch (reason) { - case 'late-submission': - return StrikeReason.LateSubmission; - case 'missed-submission': - return StrikeReason.MissedSubmission; - case 'incomplete-submission': - return StrikeReason.IncompleteSubmission; - case 'late-attendance': - return StrikeReason.LateAttendance; - case 'absence': - return StrikeReason.Absence; - case 'pending-feedback': - return StrikeReason.PendingFeedback; - case 'other': - return StrikeReason.Other; - default: - throw new Error(`Unknown strike reason: ${reason}`); + +export const mapStringToStrikeReason = (reason: string): StrikeReason => { + // Check if the string is one of the valid values in the StrikeReason enum + const isValid = Object.values(StrikeReason).includes(reason as StrikeReason); + + if (isValid) { + return reason as StrikeReason; } + throw new Error(`Invalid strike reason: ${reason}`); }; diff --git a/client/src/features/trainee-profile/education/strikes/data/keys.ts b/client/src/features/trainee-profile/education/strikes/data/keys.ts index bbfca12b..ddc5c0fb 100644 --- a/client/src/features/trainee-profile/education/strikes/data/keys.ts +++ b/client/src/features/trainee-profile/education/strikes/data/keys.ts @@ -1,5 +1,5 @@ +const STRIKES_QUERY_KEY = 'strikes'; + export const strikeKeys = { - all: ['strikes'] as const, - list: (traineeId: string) => [...strikeKeys.all, 'list', traineeId] as const, - detail: (strikeId: string) => [...strikeKeys.all, 'detail', strikeId] as const, + list: (traineeId: string) => [STRIKES_QUERY_KEY, 'list', traineeId] as const, }; From 2c25bac70b0b886c8bde6ae49673d1dc1406c727 Mon Sep 17 00:00:00 2001 From: zalexa19 Date: Sun, 15 Feb 2026 20:34:24 +0100 Subject: [PATCH 10/15] forgot to map back to domain --- .../trainee-profile/education/strikes/data/strike-queries.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/src/features/trainee-profile/education/strikes/data/strike-queries.ts b/client/src/features/trainee-profile/education/strikes/data/strike-queries.ts index 0457c263..b5d5c639 100644 --- a/client/src/features/trainee-profile/education/strikes/data/strike-queries.ts +++ b/client/src/features/trainee-profile/education/strikes/data/strike-queries.ts @@ -1,5 +1,6 @@ import { Strike } from '../models/strike'; import { getStrikes } from '../api/api'; +import { mapStrikeToDomain } from '../api/mapper'; import { strikeKeys } from './keys'; import { useQuery } from '@tanstack/react-query'; @@ -14,7 +15,8 @@ export const useGetStrikes = (traineeId: string) => { queryFn: async () => { const data = await getStrikes(traineeId); - return orderStrikesByDateDesc(data); + const strikes = data.map((strike) => mapStrikeToDomain(strike)); + return orderStrikesByDateDesc(strikes); }, enabled: !!traineeId, refetchOnWindowFocus: false, From 97dc77a0ef6e6e891f5bfecd77f4f0de71e7b12f Mon Sep 17 00:00:00 2001 From: zalexa19 Date: Wed, 18 Feb 2026 10:22:31 +0100 Subject: [PATCH 11/15] mapping in the api level --- .../features/trainee-profile/education/strikes/api/api.ts | 4 +++- .../trainee-profile/education/strikes/data/strike-queries.ts | 5 +---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/client/src/features/trainee-profile/education/strikes/api/api.ts b/client/src/features/trainee-profile/education/strikes/api/api.ts index 83b25bc4..f7b1e7c3 100644 --- a/client/src/features/trainee-profile/education/strikes/api/api.ts +++ b/client/src/features/trainee-profile/education/strikes/api/api.ts @@ -1,12 +1,14 @@ import { StrikeRequest, StrikeResponse } from './types'; import axios from 'axios'; +import { mapStrikeToDomain } from './mapper'; export const getStrikes = async (traineeId: string) => { const { data } = await axios.get(`/api/trainees/${traineeId}/strikes`); - return data; + return data.map((strike) => mapStrikeToDomain(strike)); }; +// TODO: Move these to mutation file export const deleteStrike = async (traineeId: string, strikeId: string) => { await axios.delete(`/api/trainees/${traineeId}/strikes/${strikeId}`); }; diff --git a/client/src/features/trainee-profile/education/strikes/data/strike-queries.ts b/client/src/features/trainee-profile/education/strikes/data/strike-queries.ts index b5d5c639..5d2772f2 100644 --- a/client/src/features/trainee-profile/education/strikes/data/strike-queries.ts +++ b/client/src/features/trainee-profile/education/strikes/data/strike-queries.ts @@ -1,6 +1,5 @@ import { Strike } from '../models/strike'; import { getStrikes } from '../api/api'; -import { mapStrikeToDomain } from '../api/mapper'; import { strikeKeys } from './keys'; import { useQuery } from '@tanstack/react-query'; @@ -13,9 +12,7 @@ export const useGetStrikes = (traineeId: string) => { return useQuery({ queryKey: strikeKeys.list(traineeId), queryFn: async () => { - const data = await getStrikes(traineeId); - - const strikes = data.map((strike) => mapStrikeToDomain(strike)); + const strikes = await getStrikes(traineeId); return orderStrikesByDateDesc(strikes); }, enabled: !!traineeId, From d30ed7fa5f907c8ba2dcc8dbd47269302b31e992 Mon Sep 17 00:00:00 2001 From: zalexa19 Date: Wed, 18 Feb 2026 11:03:25 +0100 Subject: [PATCH 12/15] mapping to request in the api + doc --- client/README.md | 168 +++++++++++++++++- .../education/strikes/api/api.ts | 18 +- .../education/strikes/data/mutations.ts | 6 +- 3 files changed, 176 insertions(+), 16 deletions(-) diff --git a/client/README.md b/client/README.md index a2fc36eb..df79c5b5 100644 --- a/client/README.md +++ b/client/README.md @@ -7,8 +7,8 @@ Frontend application for HackYourFuture's trainee management system ![Vite](https://img.shields.io/badge/vite-%23646CFF.svg?style=for-the-badge&logo=vite&logoColor=white) ![MUI](https://img.shields.io/badge/MUI-%23007FFF.svg?style=for-the-badge&logo=mui&logoColor=white) - ## Technology Stack + - **React** - Modern React with hooks and concurrent features - **TypeScript** - **Vite** - For development server @@ -26,16 +26,18 @@ Before you begin, ensure you have the following installed: ## Setup 1. **Install dependencies**: + ```bash npm run setup ``` 2. **Set up environment variables**: Create a `.env` file in the client directory: + ```bash # Backend API URL (optional - defaults to http://localhost:7777) VITE_BACKEND_PROXY_TARGET=http://localhost:7777 - + # Google OAuth Client ID (required for authentication) VITE_GOOGLE_CLIENT_ID=your_google_client_id_here ``` @@ -62,20 +64,23 @@ The build files will be generated in the `dist/` directory. ### Environment Variables -| Variable | Description | Default | Required | -|----------|-------------|---------|----------| -| `VITE_BACKEND_PROXY_TARGET` | Backend API URL | `http://localhost:7777` | No | -| `VITE_GOOGLE_CLIENT_ID` | Google OAuth Client ID | - | Yes | +| Variable | Description | Default | Required | +| --------------------------- | ---------------------- | ----------------------- | -------- | +| `VITE_BACKEND_PROXY_TARGET` | Backend API URL | `http://localhost:7777` | No | +| `VITE_GOOGLE_CLIENT_ID` | Google OAuth Client ID | - | Yes | ### API Proxy Configuration + You need to set up `VITE_BACKEND_PROXY_TARGET` variable to point to the correct backend URL. If you use the default http://localhost:7777, you need to run the local server first. Read more about local backend development in the server's [README.md](../server/) The development server automatically proxies API requests: + - `/api/*` β†’ Backend server - `/api-docs/*` β†’ Backend API documentation This eliminates CORS issues during development. ## πŸ—‚οΈ Client Structure + - `src/`: Contains all React components and application logic. - `assets/`: Contains all the assets and images used. - `components/`: Reusable UI components. @@ -98,7 +103,158 @@ Make sure you have `VITE_GOOGLE_CLIENT_ID` set up correctly. Check out the serve ## API Integration The client communicates with the backend API through: + - **Axios** for HTTP requests - **React Query** for caching and state management - **Automatic retry** for failed requests - **Optimistic updates** for better UX + +--- + +## Using TanStack: Example with Strikes Feature (wip) + +This project uses React Query's `useQuery` and `useMutation` hooks for data fetching and updates. Here’s how to organize your files and use these hooks, using the `strikes` feature as an example: + +### File Structure for API and Data Layers + +``` +strikes/ +β”œβ”€β”€ api/ +β”‚ β”œβ”€β”€ api.ts # API calls (fetch, add, edit, delete) +β”‚ β”œβ”€β”€ mapper.ts # Maps API types to domain models +β”‚ β”œβ”€β”€ types.ts # API request/response types +β”œβ”€β”€ data/ +β”‚ β”œβ”€β”€ keys.ts # Query key factory for React Query +β”‚ β”œβ”€β”€ mutations.ts # React Query mutation hooks +β”‚ β”œβ”€β”€ strike-queries.ts # React Query query hooks +``` + +- `api.ts`: clean calls using axios +- `mapper.ts`: Sometimes we get from the backend more information than we are using in our UI, or the information is arranged differently. Because of this, it's good to separate the business logic that transforms API responses into the shape your UI needs. + This file contains functions that map API types (often matching backend structure) to domain models (used in your frontend), ensuring consistency and making it easier to adapt if the backend changes or if you want to refactor your UI. + For example, you might convert snake_case fields to camelCase, filter out unused properties, or maybe flatten nested data/ +- `types.ts`: specifies the request and response type. This way it's very clear to see what data is sent to the backend and what we expect to get back. + +-`keys.ts`: In this file we define the key factory for the queries used in the feature. Query keys are unique identifiers for each query in React Query. They help React Query know which data to cache, refetch, or update. +A key factory is a function or object that generates consistent, structured keys for your queries. This makes it easy to manage cache and invalidation, especially as your feature grows. + +- `mutations.ts`: This file contains React Query mutation hooks for creating, updating, or deleting data. Mutations trigger changes on the server and, on success, typically invalidate relevant queries to keep the UI in sync. +- `queries.ts`: This file contains React Query query hooks for fetching data from the server. Queries use structured keys to manage caching, loading states, and automatic refetching, making data fetching reliable and efficient. + +### Example: Fetching Strikes with useQuery + +`data/strike-queries.ts`: + +```typescript +import { useQuery } from '@tanstack/react-query'; +import { getStrikes } from '../api/api'; +import { strikeKeys } from './keys'; + +export const useGetStrikes = (traineeId: string) => + useQuery({ + queryKey: strikeKeys.list(traineeId), + queryFn: () => getStrikes(traineeId), + }); +``` + +`data/keys.ts`: + +```typescript +export const strikeKeys = { + all: ['strikes'], + list: (traineeId: string) => [...strikeKeys.all, 'list', traineeId], +}; +``` + +> πŸ’‘ **Note:** This function creates two query keys for us. +> +> 1. all: the key looks like this: ['strikes'] +> 2. list: the key looks like this: ['stikes', 'list', `traineeId`]. +> And when invalidating the cache, if you do `queryKey: strikeKeys.list(traineeId)`, it invalidates the cache for this specific traineeId. But if you call `queryKey: strikeKeys.all()`, it will invalidate all the cache queries that start with 'strikes'. Which is pretty cool :) + +`api/api.ts`: + +```typescript +import axios from 'axios'; +import { StrikeResponse } from './types'; +import { mapStrikeToDomain } from './mapper'; + +export const getStrikes = async (traineeId: string) => { + const { data } = await axios.get(`/api/trainees/${traineeId}/strikes`); + return data.map((strike) => mapStrikeToDomain(strike)); +}; +``` + +As you can see, the reporter details we get from the backend are nested. + +```typescript +export interface StrikeResponse { + id: string; + comments: string; + date: string; // ISO string from backend + reason: StrikeReason; + reporter: ReporterDTO; +} + +interface ReporterDTO { + id: string; + name: string; + imageUrl?: string; +} +``` + +And the strikes model that is used in the frontend is flattend. We are also ignoring the reported id because it is not used in the Strikes component. + +```typescript +// models/strike.ts +export interface Strike { + id: string; + comments: string; + date: Date; + reason: StrikeReason; + reporterName?: string; + reporterImageUrl?: string; +} +``` + +### Using the Hook in a Component + +```typescript +const { data: strikes, isPending } = useGetStrikes(traineeId); +``` + +### Example: Mutating Strikes Data + +To add, edit, or delete a strike, use a mutation hook from `data/mutations.ts`: + +```typescript +export const useAddStrike = (traineeId: string) => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (strike: Strike) => { + return addStrike(traineeId, strike); + }, + onSuccess: async () => await invalidateStrikesQuery(queryClient, traineeId), + }); +}; +``` + +```typescript +import { useAddStrike } from './data/mutations'; + +const { mutate: addStrike, isPending } = useAddStrike(traineeId); + +// Add a new strike +addStrike(newStrike, { + onSuccess: () => { + // Optionally update UI or show a success message + }, + onError: (error) => { + // Handle error + }, +}); +``` + +> **Note:** On success, the mutation will invalidate the relevant query so the UI stays in sync with the server. +> This structure keeps your API logic, data fetching, and UI code clean and maintainable. Use this pattern for new features! diff --git a/client/src/features/trainee-profile/education/strikes/api/api.ts b/client/src/features/trainee-profile/education/strikes/api/api.ts index f7b1e7c3..bba9210f 100644 --- a/client/src/features/trainee-profile/education/strikes/api/api.ts +++ b/client/src/features/trainee-profile/education/strikes/api/api.ts @@ -1,7 +1,8 @@ -import { StrikeRequest, StrikeResponse } from './types'; +import { mapDomainToStrikeRequest, mapStrikeToDomain } from './mapper'; +import { Strike } from '../models/strike'; +import { StrikeResponse } from './types'; import axios from 'axios'; -import { mapStrikeToDomain } from './mapper'; export const getStrikes = async (traineeId: string) => { const { data } = await axios.get(`/api/trainees/${traineeId}/strikes`); @@ -13,10 +14,15 @@ export const deleteStrike = async (traineeId: string, strikeId: string) => { await axios.delete(`/api/trainees/${traineeId}/strikes/${strikeId}`); }; -export const addStrike = async (traineeId: string, strike: StrikeRequest) => { - await axios.post(`/api/trainees/${traineeId}/strikes`, strike); +export const addStrike = async (traineeId: string, strike: Strike) => { + const strikeRequest = mapDomainToStrikeRequest(strike); + const { data } = await axios.post(`/api/trainees/${traineeId}/strikes`, strikeRequest); + return mapStrikeToDomain(data); }; -export const editStrike = async (traineeId: string, strike: StrikeRequest) => { - await axios.put(`/api/trainees/${traineeId}/strikes/${strike.id!}`, strike); +export const editStrike = async (traineeId: string, strike: Strike) => { + const strikeRequest = mapDomainToStrikeRequest(strike); + + const { data } = await axios.put(`/api/trainees/${traineeId}/strikes/${strike.id!}`, strikeRequest); + return mapStrikeToDomain(data); }; diff --git a/client/src/features/trainee-profile/education/strikes/data/mutations.ts b/client/src/features/trainee-profile/education/strikes/data/mutations.ts index 5f907b4d..9d4b77de 100644 --- a/client/src/features/trainee-profile/education/strikes/data/mutations.ts +++ b/client/src/features/trainee-profile/education/strikes/data/mutations.ts @@ -18,8 +18,7 @@ export const useAddStrike = (traineeId: string) => { return useMutation({ mutationFn: (strike: Strike) => { - const request = mapDomainToStrikeRequest(strike); - return addStrike(traineeId, request); + return addStrike(traineeId, strike); }, onSuccess: async () => await invalidateStrikesQuery(queryClient, traineeId), }); @@ -47,8 +46,7 @@ export const useEditStrike = (traineeId: string) => { const queryClient = useQueryClient(); return useMutation({ mutationFn: (strike: Strike) => { - const request = mapDomainToStrikeRequest(strike); - return editStrike(traineeId, request); + return editStrike(traineeId, strike); }, onSuccess: async () => await invalidateStrikesQuery(queryClient, traineeId), }); From 24a2e7917ae3d3fa712e63d23c8e5aa07363e73c Mon Sep 17 00:00:00 2001 From: zalexa19 Date: Wed, 18 Feb 2026 11:03:55 +0100 Subject: [PATCH 13/15] changed the note --- client/README.md | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/client/README.md b/client/README.md index df79c5b5..41ebd8c8 100644 --- a/client/README.md +++ b/client/README.md @@ -166,11 +166,11 @@ export const strikeKeys = { }; ``` -> πŸ’‘ **Note:** This function creates two query keys for us. -> -> 1. all: the key looks like this: ['strikes'] -> 2. list: the key looks like this: ['stikes', 'list', `traineeId`]. -> And when invalidating the cache, if you do `queryKey: strikeKeys.list(traineeId)`, it invalidates the cache for this specific traineeId. But if you call `queryKey: strikeKeys.all()`, it will invalidate all the cache queries that start with 'strikes'. Which is pretty cool :) +πŸ’‘ **Note:** This function creates two query keys for us. + +1. all: the key looks like this: ['strikes'] +2. list: the key looks like this: ['stikes', 'list', `traineeId`]. + And when invalidating the cache, if you do `queryKey: strikeKeys.list(traineeId)`, it invalidates the cache for this specific traineeId. But if you call `queryKey: strikeKeys.all()`, it will invalidate all the cache queries that start with 'strikes'. Which is pretty cool :) `api/api.ts`: @@ -240,6 +240,19 @@ export const useAddStrike = (traineeId: string) => { }; ``` +It’s important to invalidate the relevant query after a mutation completes. While the mutation is still pending, React Query will keep the loading state active until the operation finishes and the cache is refreshed. + +```typescript +// api.ts +export const addStrike = async (traineeId: string, strike: Strike) => { + const strikeRequest = mapDomainToStrikeRequest(strike); + const { data } = await axios.post(`/api/trainees/${traineeId}/strikes`, strikeRequest); + return mapStrikeToDomain(data); +}; +``` + +Adn to use it in the component: + ```typescript import { useAddStrike } from './data/mutations'; From 05c0f7a3628d839e151bcdf4614600e3e9ff155c Mon Sep 17 00:00:00 2001 From: zalexa19 Date: Wed, 18 Feb 2026 11:06:38 +0100 Subject: [PATCH 14/15] typos --- client/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/README.md b/client/README.md index 41ebd8c8..c29a2e91 100644 --- a/client/README.md +++ b/client/README.md @@ -169,8 +169,8 @@ export const strikeKeys = { πŸ’‘ **Note:** This function creates two query keys for us. 1. all: the key looks like this: ['strikes'] -2. list: the key looks like this: ['stikes', 'list', `traineeId`]. - And when invalidating the cache, if you do `queryKey: strikeKeys.list(traineeId)`, it invalidates the cache for this specific traineeId. But if you call `queryKey: strikeKeys.all()`, it will invalidate all the cache queries that start with 'strikes'. Which is pretty cool :) +2. list: the key looks like this: ['strikes', 'list', `traineeId`]. + And when invalidating the cache, if you use `queryKey: strikeKeys.list(traineeId)`, it invalidates the cache for this specific traineeId. But if you call `queryKey: strikeKeys.all()`, it will invalidate all the cache queries that start with 'strikes'. Which is pretty cool :) `api/api.ts`: @@ -251,7 +251,7 @@ export const addStrike = async (traineeId: string, strike: Strike) => { }; ``` -Adn to use it in the component: +And to use it in the component: ```typescript import { useAddStrike } from './data/mutations'; From 6783c3356831058c82d8bc0f576be550099df0bc Mon Sep 17 00:00:00 2001 From: zalexa19 Date: Thu, 19 Feb 2026 20:38:24 +0100 Subject: [PATCH 15/15] removed unused import --- .../features/trainee-profile/education/strikes/data/mutations.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/client/src/features/trainee-profile/education/strikes/data/mutations.ts b/client/src/features/trainee-profile/education/strikes/data/mutations.ts index 9d4b77de..e007dbd4 100644 --- a/client/src/features/trainee-profile/education/strikes/data/mutations.ts +++ b/client/src/features/trainee-profile/education/strikes/data/mutations.ts @@ -2,7 +2,6 @@ import { QueryClient, useMutation, useQueryClient } from '@tanstack/react-query' import { addStrike, deleteStrike, editStrike } from '../api/api'; import { Strike } from '../models/strike'; -import { mapDomainToStrikeRequest } from '../api/mapper'; import { strikeKeys } from './keys'; const invalidateStrikesQuery = (queryClient: QueryClient, traineeId: string) => {