fix: improve error handling in API routes and update date handling for OKR and Key Result submissions
Some checks failed
Deploy with Docker Compose / deploy (push) Failing after 3m38s

This commit is contained in:
Julien Froidefond
2026-01-07 17:22:33 +01:00
parent 97045342b7
commit 86c26b5af8
10 changed files with 170 additions and 70 deletions

View File

@@ -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<void>;
onSubmit: (
data:
| (CreateOKRInput & {
startDate: Date | string;
endDate: Date | string;
keyResultsUpdates?: {
create?: CreateKeyResultInput[];
update?: KeyResultUpdate[];
delete?: string[];
};
})
| CreateOKRInput
) => Promise<void>;
onCancel: () => void;
initialData?: Partial<CreateOKRInput> & { 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<KeyResultEditInput[]>(() => {
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
</div>
{period && period !== 'custom' && period !== '' && (
<p className="text-sm text-muted-foreground">
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.
</p>
)}
@@ -351,13 +407,22 @@ export function OKRForm({ teamMembers, onSubmit, onCancel, initialData }: OKRFor
<label className="block text-sm font-medium text-foreground">
Key Results * ({keyResults.length}/5)
</label>
<Button type="button" onClick={addKeyResult} variant="outline" size="sm" disabled={keyResults.length >= 5}>
<Button
type="button"
onClick={addKeyResult}
variant="outline"
size="sm"
disabled={keyResults.length >= 5}
>
+ Ajouter
</Button>
</div>
<div className="space-y-4">
{keyResults.map((kr, index) => (
<div key={kr.id || `new-${index}`} className="rounded-lg border border-border bg-card p-4">
<div
key={kr.id || `new-${index}`}
className="rounded-lg border border-border bg-card p-4"
>
<div className="mb-3 flex items-center justify-between">
<span className="text-sm font-medium text-foreground">Key Result {index + 1}</span>
{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"}
</Button>
</div>
</form>
);
}