From 86c26b5af87a2d234cf26f18be654672a712f4e4 Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Wed, 7 Jan 2026 17:22:33 +0100 Subject: [PATCH] fix: improve error handling in API routes and update date handling for OKR and Key Result submissions --- .../api/okrs/[id]/key-results/[krId]/route.ts | 5 +- src/app/api/okrs/[id]/route.ts | 18 ++- src/app/api/teams/[id]/members/route.ts | 5 +- src/app/teams/[id]/okrs/[okrId]/edit/page.tsx | 37 ++++- src/app/teams/[id]/okrs/[okrId]/page.tsx | 2 +- src/app/teams/[id]/okrs/new/page.tsx | 4 +- src/components/okrs/OKRForm.tsx | 146 +++++++++++++----- src/components/swot/QuadrantHelp.tsx | 4 +- src/components/teams/DeleteTeamButton.tsx | 17 +- src/services/teams.ts | 2 +- 10 files changed, 170 insertions(+), 70 deletions(-) diff --git a/src/app/api/okrs/[id]/key-results/[krId]/route.ts b/src/app/api/okrs/[id]/key-results/[krId]/route.ts index ee5c192..84a9883 100644 --- a/src/app/api/okrs/[id]/key-results/[krId]/route.ts +++ b/src/app/api/okrs/[id]/key-results/[krId]/route.ts @@ -49,10 +49,11 @@ export async function PATCH( const updated = await updateKeyResult(krId, Number(currentValue), notes || null); return NextResponse.json(updated); - } catch (error: any) { + } catch (error) { console.error('Error updating key result:', error); + const errorMessage = error instanceof Error ? error.message : 'Erreur lors de la mise à jour du Key Result'; return NextResponse.json( - { error: error.message || 'Erreur lors de la mise à jour du Key Result' }, + { error: errorMessage }, { status: 500 } ); } diff --git a/src/app/api/okrs/[id]/route.ts b/src/app/api/okrs/[id]/route.ts index 062dbbb..90803e4 100644 --- a/src/app/api/okrs/[id]/route.ts +++ b/src/app/api/okrs/[id]/route.ts @@ -90,20 +90,21 @@ export async function PATCH(request: Request, { params }: { params: Promise<{ id // Remove keyResultsUpdates from updateData as it's not part of UpdateOKRInput const { keyResultsUpdates, ...okrUpdateData } = body; const finalUpdateData: UpdateOKRInput = { ...okrUpdateData }; - if (finalUpdateData.startDate) { - finalUpdateData.startDate = new Date(finalUpdateData.startDate as any); + if (finalUpdateData.startDate && typeof finalUpdateData.startDate === 'string') { + finalUpdateData.startDate = new Date(finalUpdateData.startDate); } - if (finalUpdateData.endDate) { - finalUpdateData.endDate = new Date(finalUpdateData.endDate as any); + if (finalUpdateData.endDate && typeof finalUpdateData.endDate === 'string') { + finalUpdateData.endDate = new Date(finalUpdateData.endDate); } const updated = await updateOKR(id, finalUpdateData, keyResultsUpdates); return NextResponse.json(updated); - } catch (error: any) { + } catch (error) { console.error('Error updating OKR:', error); + const errorMessage = error instanceof Error ? error.message : 'Erreur lors de la mise à jour de l\'OKR'; return NextResponse.json( - { error: error.message || 'Erreur lors de la mise à jour de l\'OKR' }, + { error: errorMessage }, { status: 500 } ); } @@ -132,10 +133,11 @@ export async function DELETE(request: Request, { params }: { params: Promise<{ i await deleteOKR(id); return NextResponse.json({ success: true }); - } catch (error: any) { + } catch (error) { console.error('Error deleting OKR:', error); + const errorMessage = error instanceof Error ? error.message : 'Erreur lors de la suppression de l\'OKR'; return NextResponse.json( - { error: error.message || 'Erreur lors de la suppression de l\'OKR' }, + { error: errorMessage }, { status: 500 } ); } diff --git a/src/app/api/teams/[id]/members/route.ts b/src/app/api/teams/[id]/members/route.ts index e946007..6fb1f83 100644 --- a/src/app/api/teams/[id]/members/route.ts +++ b/src/app/api/teams/[id]/members/route.ts @@ -28,10 +28,11 @@ export async function POST(request: Request, { params }: { params: Promise<{ id: const member = await addTeamMember(id, userId, role || 'MEMBER'); return NextResponse.json(member, { status: 201 }); - } catch (error: any) { + } catch (error) { console.error('Error adding team member:', error); + const errorMessage = error instanceof Error ? error.message : 'Erreur lors de l\'ajout du membre'; return NextResponse.json( - { error: error.message || 'Erreur lors de l\'ajout du membre' }, + { error: errorMessage }, { status: 500 } ); } diff --git a/src/app/teams/[id]/okrs/[okrId]/edit/page.tsx b/src/app/teams/[id]/okrs/[okrId]/edit/page.tsx index 6cdcd9a..199f516 100644 --- a/src/app/teams/[id]/okrs/[okrId]/edit/page.tsx +++ b/src/app/teams/[id]/okrs/[okrId]/edit/page.tsx @@ -54,7 +54,23 @@ export default function EditOKRPage() { }); }, [okrId, teamId]); - const handleSubmit = async (data: CreateOKRInput & { keyResultsUpdates?: { create?: any[]; update?: any[]; delete?: string[] } }) => { + type KeyResultUpdate = { + id: string; + title?: string; + targetValue?: number; + unit?: string; + order?: number; + }; + + const handleSubmit = async (data: CreateOKRInput & { + startDate: Date | string; + endDate: Date | string; + keyResultsUpdates?: { + create?: Array<{ title: string; targetValue: number; unit: string; order: number }>; + update?: KeyResultUpdate[]; + delete?: string[] + } + }) => { // Convert to UpdateOKRInput format const updateData = { objective: data.objective, @@ -64,7 +80,18 @@ export default function EditOKRPage() { endDate: typeof data.endDate === 'string' ? new Date(data.endDate) : data.endDate, }; - const payload: any = { + const payload: { + objective: string; + description?: string; + period: string; + startDate: string; + endDate: string; + keyResultsUpdates?: { + create?: Array<{ title: string; targetValue: number; unit: string; order: number }>; + update?: KeyResultUpdate[]; + delete?: string[]; + }; + } = { ...updateData, startDate: updateData.startDate.toISOString(), endDate: updateData.endDate.toISOString(), @@ -83,7 +110,7 @@ export default function EditOKRPage() { if (!response.ok) { const error = await response.json(); - throw new Error(error.error || 'Erreur lors de la mise à jour de l\'OKR'); + throw new Error(error.error || 'Erreur lors de la mise à jour de l'OKR'); } router.push(`/teams/${teamId}/okrs/${okrId}`); @@ -121,12 +148,12 @@ export default function EditOKRPage() {
- ← Retour à l'OKR + ← Retour à l'OKR
-

Modifier l'OKR

+

Modifier l'OKR

- ← Retour à l'équipe + ← Retour à l'équipe
diff --git a/src/app/teams/[id]/okrs/new/page.tsx b/src/app/teams/[id]/okrs/new/page.tsx index 2e7a113..e681da8 100644 --- a/src/app/teams/[id]/okrs/new/page.tsx +++ b/src/app/teams/[id]/okrs/new/page.tsx @@ -45,7 +45,7 @@ export default function NewOKRPage() { if (!response.ok) { const error = await response.json(); - throw new Error(error.error || 'Erreur lors de la création de l\'OKR'); + throw new Error(error.error || 'Erreur lors de la création de l'OKR'); } router.push(`/teams/${teamId}`); @@ -64,7 +64,7 @@ export default function NewOKRPage() {
- ← Retour à l'équipe + ← Retour à l'équipe
diff --git a/src/components/okrs/OKRForm.tsx b/src/components/okrs/OKRForm.tsx index d5327c1..cf9021f 100644 --- a/src/components/okrs/OKRForm.tsx +++ b/src/components/okrs/OKRForm.tsx @@ -61,9 +61,29 @@ interface KeyResultEditInput extends CreateKeyResultInput { id?: string; // If present, it's an existing Key Result to update } +type KeyResultUpdate = { + id: string; + title?: string; + targetValue?: number; + unit?: string; + order?: number; +}; + interface OKRFormProps { teamMembers: TeamMember[]; - onSubmit: (data: CreateOKRInput & { keyResultsUpdates?: { create?: CreateKeyResultInput[]; update?: Array<{ id: string; title?: string; targetValue?: number; unit?: string; order?: number }>; delete?: string[] } }) => Promise; + onSubmit: ( + data: + | (CreateOKRInput & { + startDate: Date | string; + endDate: Date | string; + keyResultsUpdates?: { + create?: CreateKeyResultInput[]; + update?: KeyResultUpdate[]; + delete?: string[]; + }; + }) + | CreateOKRInput + ) => Promise; onCancel: () => void; initialData?: Partial & { keyResults?: KeyResult[] }; } @@ -83,13 +103,17 @@ export function OKRForm({ teamMembers, onSubmit, onCancel, initialData }: OKRFor // Initialize Key Results from existing ones if in edit mode, otherwise start with one empty const [keyResults, setKeyResults] = useState(() => { if (initialData?.keyResults && initialData.keyResults.length > 0) { - return initialData.keyResults.map((kr) => ({ - id: kr.id, - title: kr.title, - targetValue: kr.targetValue, - unit: kr.unit, - order: kr.order, - })); + return initialData.keyResults.map((kr): KeyResultEditInput => { + const result = { + title: kr.title, + targetValue: kr.targetValue, + unit: kr.unit || '%', + order: kr.order, + }; + // @ts-expect-error - id is added to extend CreateKeyResultInput to KeyResultEditInput + result.id = kr.id; + return result as KeyResultEditInput; + }); } return [{ title: '', targetValue: 100, unit: '%', order: 0 }]; }); @@ -125,7 +149,11 @@ export function OKRForm({ teamMembers, onSubmit, onCancel, initialData }: OKRFor setKeyResults(keyResults.filter((_, i) => i !== index).map((kr, i) => ({ ...kr, order: i }))); }; - const updateKeyResult = (index: number, field: keyof KeyResultEditInput, value: any) => { + const updateKeyResult = ( + index: number, + field: keyof KeyResultEditInput, + value: string | number + ) => { const updated = [...keyResults]; updated[index] = { ...updated[index], [field]: value }; setKeyResults(updated); @@ -164,48 +192,67 @@ export function OKRForm({ teamMembers, onSubmit, onCancel, initialData }: OKRFor } const isEditMode = !!initialData?.teamMemberId; - + if (isEditMode) { + // Type guard for Key Results with id + type KeyResultWithId = KeyResultEditInput & { id: string }; + const hasId = (kr: KeyResultEditInput): kr is KeyResultWithId => !!kr.id; + // In edit mode, separate existing Key Results from new ones - const existingKeyResults = keyResults.filter((kr) => kr.id); - const newKeyResults = keyResults.filter((kr) => !kr.id); - const originalKeyResults = initialData?.keyResults || []; - const originalIds = new Set(originalKeyResults.map((kr) => kr.id)); - const currentIds = new Set(existingKeyResults.map((kr) => kr.id)); - + const existingKeyResults: KeyResultWithId[] = keyResults.filter(hasId); + const originalKeyResults: KeyResult[] = initialData?.keyResults || []; + const originalIds = new Set(originalKeyResults.map((kr: KeyResult) => kr.id)); + const currentIds = new Set(existingKeyResults.map((kr: KeyResultWithId) => kr.id)); + // Find deleted Key Results const deletedIds = Array.from(originalIds).filter((id) => !currentIds.has(id)); - + // Find updated Key Results (compare with original) const updated = existingKeyResults - .map((kr) => { - const original = originalKeyResults.find((okr) => okr.id === kr.id); + .map((kr: KeyResultWithId) => { + const original = originalKeyResults.find((okr: KeyResult) => okr.id === kr.id); if (!original) return null; - - const changes: { id: string; title?: string; targetValue?: number; unit?: string; order?: number } = { id: kr.id }; + + const changes: { + id: string; + title?: string; + targetValue?: number; + unit?: string; + order?: number; + } = { id: kr.id }; if (original.title !== kr.title) changes.title = kr.title; if (original.targetValue !== kr.targetValue) changes.targetValue = kr.targetValue; if (original.unit !== kr.unit) changes.unit = kr.unit; if (original.order !== kr.order) changes.order = kr.order; - + return Object.keys(changes).length > 1 ? changes : null; // More than just 'id' }) - .filter((u): u is { id: string; title?: string; targetValue?: number; unit?: string; order?: number } => u !== null); - + .filter( + ( + u + ): u is { + id: string; + title?: string; + targetValue?: number; + unit?: string; + order?: number; + } => u !== null + ); + // Update order for all Key Results based on their position const allKeyResultsWithOrder = keyResults.map((kr, i) => ({ ...kr, order: i })); - const existingWithOrder = allKeyResultsWithOrder.filter((kr) => kr.id); + const existingWithOrder = allKeyResultsWithOrder.filter(hasId) as KeyResultWithId[]; const newWithOrder = allKeyResultsWithOrder.filter((kr) => !kr.id); - + // Update order for existing Key Results that changed position const orderUpdates = existingWithOrder .map((kr) => { - const original = originalKeyResults.find((okr) => okr.id === kr.id); + const original = originalKeyResults.find((okr: KeyResult) => okr.id === kr.id); if (!original || original.order === kr.order) return null; return { id: kr.id, order: kr.order }; }) .filter((u): u is { id: string; order: number } => u !== null); - + // Merge order updates with other updates const allUpdates = [...updated]; orderUpdates.forEach((orderUpdate) => { @@ -216,21 +263,29 @@ export function OKRForm({ teamMembers, onSubmit, onCancel, initialData }: OKRFor allUpdates.push(orderUpdate); } }); - + await onSubmit({ teamMemberId, objective, description: description || undefined, period: finalPeriod, - startDate: startDateObj.toISOString() as any, - endDate: endDateObj.toISOString() as any, + startDate: startDateObj.toISOString(), + endDate: endDateObj.toISOString(), keyResults: [], // Not used in edit mode keyResultsUpdates: { - create: newWithOrder.length > 0 ? newWithOrder.map((kr) => ({ title: kr.title, targetValue: kr.targetValue, unit: kr.unit || '%', order: kr.order })) : undefined, + create: + newWithOrder.length > 0 + ? newWithOrder.map((kr) => ({ + title: kr.title, + targetValue: kr.targetValue, + unit: kr.unit || '%', + order: kr.order, + })) + : undefined, update: allUpdates.length > 0 ? allUpdates : undefined, delete: deletedIds.length > 0 ? deletedIds : undefined, }, - } as any); + }); } else { // In create mode, just send Key Results normally await onSubmit({ @@ -238,8 +293,8 @@ export function OKRForm({ teamMembers, onSubmit, onCancel, initialData }: OKRFor objective, description: description || undefined, period: finalPeriod, - startDate: startDateObj.toISOString() as any, - endDate: endDateObj.toISOString() as any, + startDate: startDateObj, + endDate: endDateObj, keyResults: keyResults.map((kr, i) => ({ ...kr, order: i })), }); } @@ -341,7 +396,8 @@ export function OKRForm({ teamMembers, onSubmit, onCancel, initialData }: OKRFor {period && period !== 'custom' && period !== '' && (

- Les dates sont automatiquement définies selon le trimestre sélectionné. Vous pouvez les modifier si nécessaire. + Les dates sont automatiquement définies selon le trimestre sélectionné. Vous pouvez les + modifier si nécessaire.

)} @@ -351,13 +407,22 @@ export function OKRForm({ teamMembers, onSubmit, onCancel, initialData }: OKRFor -
{keyResults.map((kr, index) => ( -
+
Key Result {index + 1} {keyResults.length > 1 && ( @@ -420,11 +485,10 @@ export function OKRForm({ teamMembers, onSubmit, onCancel, initialData }: OKRFor ? 'Modification...' : 'Création...' : initialData?.teamMemberId - ? 'Modifier l\'OKR' - : 'Créer l\'OKR'} + ? "Modifier l'OKR" + : "Créer l'OKR"}
); } - diff --git a/src/components/swot/QuadrantHelp.tsx b/src/components/swot/QuadrantHelp.tsx index 986c39f..e6e04a5 100644 --- a/src/components/swot/QuadrantHelp.tsx +++ b/src/components/swot/QuadrantHelp.tsx @@ -62,9 +62,9 @@ interface QuadrantHelpProps { category: SwotCategory; } -export function QuadrantHelp({ category }: QuadrantHelpProps) { +export function QuadrantHelp({ category: _category }: QuadrantHelpProps) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars const [isOpen, setIsOpen] = useState(false); - const content = HELP_CONTENT[category]; return ( - setShowModal(false)} title="Supprimer l'équipe" size="sm"> + setShowModal(false)} + title="Supprimer l'équipe" + size="sm" + >

Êtes-vous sûr de vouloir supprimer l'équipe{' '} "{teamName}" ?

- Cette action est irréversible. Tous les membres, OKRs et données associées seront supprimés. + Cette action est irréversible. Tous les membres, OKRs et données associées seront + supprimés.