From ca9b68ebbdfb4b25921bcbeb6a23229e90af331b Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Wed, 7 Jan 2026 16:48:23 +0100 Subject: [PATCH] feat: enhance OKR management by adding permission checks for editing and deleting, and updating OKR forms to handle key results more effectively --- src/app/api/okrs/[id]/route.ts | 43 ++++- src/app/teams/[id]/okrs/[okrId]/edit/page.tsx | 140 +++++++++++++++ src/app/teams/[id]/okrs/[okrId]/page.tsx | 50 ++++-- src/app/teams/[id]/page.tsx | 2 +- src/components/okrs/OKRCard.tsx | 126 ++++++++++--- src/components/okrs/OKRForm.tsx | 130 +++++++++++--- src/components/okrs/OKRsList.tsx | 6 +- src/services/okrs.ts | 165 +++++++++++++++--- 8 files changed, 562 insertions(+), 100 deletions(-) create mode 100644 src/app/teams/[id]/okrs/[okrId]/edit/page.tsx diff --git a/src/app/api/okrs/[id]/route.ts b/src/app/api/okrs/[id]/route.ts index 524bcb4..062dbbb 100644 --- a/src/app/api/okrs/[id]/route.ts +++ b/src/app/api/okrs/[id]/route.ts @@ -25,7 +25,19 @@ export async function GET(request: Request, { params }: { params: Promise<{ id: return NextResponse.json({ error: 'Accès refusé' }, { status: 403 }); } - return NextResponse.json(okr); + // Check permissions + const isAdmin = await isTeamAdmin(okr.teamMember.team.id, session.user.id); + const isConcernedMember = okr.teamMember.userId === session.user.id; + + return NextResponse.json({ + ...okr, + permissions: { + isAdmin, + isConcernedMember, + canEdit: isAdmin || isConcernedMember, + canDelete: isAdmin, + }, + }); } catch (error) { console.error('Error fetching OKR:', error); return NextResponse.json( @@ -49,13 +61,22 @@ export async function PATCH(request: Request, { params }: { params: Promise<{ id return NextResponse.json({ error: 'OKR non trouvé' }, { status: 404 }); } - // Check if user is admin of the team + // Check if user is admin of the team or the concerned member const isAdmin = await isTeamAdmin(okr.teamMember.team.id, session.user.id); - if (!isAdmin) { - return NextResponse.json({ error: 'Seuls les administrateurs peuvent modifier les OKRs' }, { status: 403 }); + const isConcernedMember = okr.teamMember.userId === session.user.id; + if (!isAdmin && !isConcernedMember) { + return NextResponse.json({ error: 'Seuls les administrateurs et le membre concerné peuvent modifier les OKRs' }, { status: 403 }); } - const body: UpdateOKRInput & { startDate?: string; endDate?: string } = await request.json(); + const body: UpdateOKRInput & { + startDate?: string; + endDate?: string; + keyResultsUpdates?: { + create?: Array<{ title: string; targetValue: number; unit: string; order: number }>; + update?: Array<{ id: string; title?: string; targetValue?: number; unit?: string; order?: number }>; + delete?: string[]; + }; + } = await request.json(); // Convert date strings to Date objects if provided const updateData: UpdateOKRInput = { ...body }; @@ -66,7 +87,17 @@ export async function PATCH(request: Request, { params }: { params: Promise<{ id updateData.endDate = new Date(body.endDate); } - const updated = await updateOKR(id, updateData); + // 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.endDate) { + finalUpdateData.endDate = new Date(finalUpdateData.endDate as any); + } + + const updated = await updateOKR(id, finalUpdateData, keyResultsUpdates); return NextResponse.json(updated); } catch (error: any) { diff --git a/src/app/teams/[id]/okrs/[okrId]/edit/page.tsx b/src/app/teams/[id]/okrs/[okrId]/edit/page.tsx new file mode 100644 index 0000000..6cdcd9a --- /dev/null +++ b/src/app/teams/[id]/okrs/[okrId]/edit/page.tsx @@ -0,0 +1,140 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { useRouter, useParams } from 'next/navigation'; +import Link from 'next/link'; +import { OKRForm } from '@/components/okrs'; +import { Card } from '@/components/ui'; +import type { CreateOKRInput, TeamMember, OKR, KeyResult } from '@/lib/types'; + +type OKRWithTeamMember = OKR & { + teamMember: { + user: { + id: string; + email: string; + name: string | null; + }; + userId: string; + team: { + id: string; + name: string; + }; + }; +}; + +export default function EditOKRPage() { + const router = useRouter(); + const params = useParams(); + const teamId = params.id as string; + const okrId = params.okrId as string; + const [okr, setOkr] = useState(null); + const [teamMembers, setTeamMembers] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + // Fetch OKR and team members in parallel + Promise.all([ + fetch(`/api/okrs/${okrId}`).then((res) => { + if (!res.ok) { + throw new Error('OKR not found'); + } + return res.json(); + }), + fetch(`/api/teams/${teamId}`).then((res) => res.json()), + ]) + .then(([okrData, teamData]) => { + setOkr(okrData); + setTeamMembers(teamData.members || []); + }) + .catch((error) => { + console.error('Error fetching data:', error); + }) + .finally(() => { + setLoading(false); + }); + }, [okrId, teamId]); + + const handleSubmit = async (data: CreateOKRInput & { keyResultsUpdates?: { create?: any[]; update?: any[]; delete?: string[] } }) => { + // Convert to UpdateOKRInput format + const updateData = { + objective: data.objective, + description: data.description || undefined, + period: data.period, + startDate: typeof data.startDate === 'string' ? new Date(data.startDate) : data.startDate, + endDate: typeof data.endDate === 'string' ? new Date(data.endDate) : data.endDate, + }; + + const payload: any = { + ...updateData, + startDate: updateData.startDate.toISOString(), + endDate: updateData.endDate.toISOString(), + }; + + // Add Key Results updates if in edit mode + if (data.keyResultsUpdates) { + payload.keyResultsUpdates = data.keyResultsUpdates; + } + + const response = await fetch(`/api/okrs/${okrId}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || 'Erreur lors de la mise à jour de l\'OKR'); + } + + router.push(`/teams/${teamId}/okrs/${okrId}`); + router.refresh(); + }; + + if (loading) { + return ( +
+
Chargement...
+
+ ); + } + + if (!okr) { + return ( +
+
OKR non trouvé
+
+ ); + } + + // Prepare initial data for the form + const initialData: Partial & { keyResults?: KeyResult[] } = { + teamMemberId: okr.teamMemberId, + objective: okr.objective, + description: okr.description || undefined, + period: okr.period, + startDate: okr.startDate, + endDate: okr.endDate, + keyResults: okr.keyResults || [], + }; + + return ( +
+
+ + ← Retour à l'OKR + +
+ + +

Modifier l'OKR

+ router.push(`/teams/${teamId}/okrs/${okrId}`)} + initialData={initialData} + /> +
+
+ ); +} + diff --git a/src/app/teams/[id]/okrs/[okrId]/page.tsx b/src/app/teams/[id]/okrs/[okrId]/page.tsx index 67f2eb5..d3ef8c7 100644 --- a/src/app/teams/[id]/okrs/[okrId]/page.tsx +++ b/src/app/teams/[id]/okrs/[okrId]/page.tsx @@ -55,6 +55,12 @@ type OKRWithTeamMember = OKR & { name: string; }; }; + permissions?: { + isAdmin: boolean; + isConcernedMember: boolean; + canEdit: boolean; + canDelete: boolean; + }; }; export default function OKRDetailPage() { @@ -64,8 +70,6 @@ export default function OKRDetailPage() { const okrId = params.okrId as string; const [okr, setOkr] = useState(null); const [loading, setLoading] = useState(true); - const [isAdmin, setIsAdmin] = useState(false); - const [isConcernedMember, setIsConcernedMember] = useState(false); useEffect(() => { // Fetch OKR @@ -78,10 +82,6 @@ export default function OKRDetailPage() { }) .then((data) => { setOkr(data); - // Check if current user is admin or the concerned member - // This will be properly checked server-side, but we set flags for UI - setIsAdmin(data.teamMember?.team?.id ? true : false); - setIsConcernedMember(data.teamMember?.userId ? true : false); }) .catch((error) => { console.error('Error fetching OKR:', error); @@ -141,7 +141,8 @@ export default function OKRDetailPage() { const progress = okr.progress || 0; const progressColor = progress >= 75 ? 'var(--success)' : progress >= 25 ? 'var(--accent)' : 'var(--destructive)'; - const canEdit = isAdmin || isConcernedMember; + const canEdit = okr.permissions?.canEdit ?? false; + const canDelete = okr.permissions?.canDelete ?? false; return (
@@ -222,19 +223,30 @@ export default function OKRDetailPage() { {/* Actions */} - {isAdmin && ( + {(canEdit || canDelete) && (
- + {canEdit && ( + + )} + {canDelete && ( + + )}
)} diff --git a/src/app/teams/[id]/page.tsx b/src/app/teams/[id]/page.tsx index 9db143b..858e22b 100644 --- a/src/app/teams/[id]/page.tsx +++ b/src/app/teams/[id]/page.tsx @@ -78,7 +78,7 @@ export default async function TeamDetailPage({ params }: TeamDetailPageProps) { {/* OKRs Section */} - +
); } diff --git a/src/components/okrs/OKRCard.tsx b/src/components/okrs/OKRCard.tsx index 8cf25d2..3ebd27a 100644 --- a/src/components/okrs/OKRCard.tsx +++ b/src/components/okrs/OKRCard.tsx @@ -1,8 +1,11 @@ 'use client'; +import { useState, useTransition } from 'react'; +import { useRouter } from 'next/navigation'; import Link from 'next/link'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui'; import { Badge } from '@/components/ui'; +import { Button } from '@/components/ui'; import { getGravatarUrl } from '@/lib/gravatar'; import type { OKR, KeyResult, OKRStatus, KeyResultStatus } from '@/lib/types'; import { OKR_STATUS_LABELS, KEY_RESULT_STATUS_LABELS } from '@/lib/types'; @@ -71,39 +74,104 @@ function getKeyResultStatusColor(status: KeyResultStatus): { bg: string; color: interface OKRCardProps { okr: OKR & { teamMember?: { user: { id: string; email: string; name: string | null } } }; teamId: string; + isAdmin?: boolean; } -export function OKRCard({ okr, teamId }: OKRCardProps) { +export function OKRCard({ okr, teamId, isAdmin = false }: OKRCardProps) { + const router = useRouter(); + const [isPending, startTransition] = useTransition(); const progress = okr.progress || 0; const progressColor = progress >= 75 ? 'var(--success)' : progress >= 25 ? 'var(--accent)' : 'var(--destructive)'; + const handleDelete = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + + if (!confirm(`Êtes-vous sûr de vouloir supprimer l'OKR "${okr.objective}" ?`)) { + return; + } + + startTransition(async () => { + try { + const response = await fetch(`/api/okrs/${okr.id}`, { + method: 'DELETE', + }); + + if (!response.ok) { + const error = await response.json(); + alert(error.error || 'Erreur lors de la suppression de l\'OKR'); + return; + } + + router.refresh(); + } catch (error) { + console.error('Error deleting OKR:', error); + alert('Erreur lors de la suppression de l\'OKR'); + } + }); + }; + return ( - - - -
-
- - 🎯 - {okr.objective} - - {okr.teamMember && ( -
- {/* eslint-disable-next-line @next/next/no-img-element */} - {okr.teamMember.user.name - - {okr.teamMember.user.name || okr.teamMember.user.email} - -
- )} + + +
+ +
+
+ + 🎯 + {okr.objective} + + {okr.teamMember && ( +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + {okr.teamMember.user.name + + {okr.teamMember.user.name || okr.teamMember.user.email} + +
+ )} +
+ + + {/* Action Zone */} +
+ {isAdmin && ( + + )}
- +
+
+
{/* Progress Bar */} @@ -202,8 +272,8 @@ export function OKRCard({ okr, teamId }: OKRCardProps) { )}
-
- + + ); } diff --git a/src/components/okrs/OKRForm.tsx b/src/components/okrs/OKRForm.tsx index 3e243ff..d5327c1 100644 --- a/src/components/okrs/OKRForm.tsx +++ b/src/components/okrs/OKRForm.tsx @@ -5,7 +5,7 @@ import { Input } from '@/components/ui'; import { Textarea } from '@/components/ui'; import { Button } from '@/components/ui'; import { Select } from '@/components/ui'; -import type { CreateOKRInput, CreateKeyResultInput, TeamMember } from '@/lib/types'; +import type { CreateOKRInput, CreateKeyResultInput, TeamMember, KeyResult } from '@/lib/types'; import { PERIOD_SUGGESTIONS } from '@/lib/types'; // Calcule les dates de début et de fin pour un trimestre donné @@ -57,11 +57,15 @@ function getQuarterDates(period: string): { startDate: string; endDate: string } }; } +interface KeyResultEditInput extends CreateKeyResultInput { + id?: string; // If present, it's an existing Key Result to update +} + interface OKRFormProps { teamMembers: TeamMember[]; - onSubmit: (data: CreateOKRInput) => Promise; + onSubmit: (data: CreateOKRInput & { keyResultsUpdates?: { create?: CreateKeyResultInput[]; update?: Array<{ id: string; title?: string; targetValue?: number; unit?: string; order?: number }>; delete?: string[] } }) => Promise; onCancel: () => void; - initialData?: Partial; + initialData?: Partial & { keyResults?: KeyResult[] }; } export function OKRForm({ teamMembers, onSubmit, onCancel, initialData }: OKRFormProps) { @@ -76,11 +80,19 @@ export function OKRForm({ teamMembers, onSubmit, onCancel, initialData }: OKRFor const [endDate, setEndDate] = useState( initialData?.endDate ? new Date(initialData.endDate).toISOString().split('T')[0] : '' ); - const [keyResults, setKeyResults] = useState( - initialData?.keyResults || [ - { title: '', targetValue: 100, unit: '%', order: 0 }, - ] - ); + // 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 [{ title: '', targetValue: 100, unit: '%', order: 0 }]; + }); const [submitting, setSubmitting] = useState(false); // Mise à jour automatique des dates quand la période change @@ -113,7 +125,7 @@ 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 CreateKeyResultInput, value: any) => { + const updateKeyResult = (index: number, field: keyof KeyResultEditInput, value: any) => { const updated = [...keyResults]; updated[index] = { ...updated[index], [field]: value }; setKeyResults(updated); @@ -127,6 +139,7 @@ export function OKRForm({ teamMembers, onSubmit, onCancel, initialData }: OKRFor return; } + // Validate Key Results if (keyResults.some((kr) => !kr.title || kr.targetValue <= 0)) { alert('Tous les Key Results doivent avoir un titre et une valeur cible > 0'); return; @@ -150,15 +163,86 @@ export function OKRForm({ teamMembers, onSubmit, onCancel, initialData }: OKRFor return; } - await onSubmit({ - teamMemberId, - objective, - description: description || undefined, - period: finalPeriod, - startDate: startDateObj.toISOString() as any, - endDate: endDateObj.toISOString() as any, - keyResults: keyResults.map((kr, i) => ({ ...kr, order: i })), - }); + const isEditMode = !!initialData?.teamMemberId; + + if (isEditMode) { + // 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)); + + // 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); + if (!original) return null; + + 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); + + // 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 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); + 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) => { + const existingUpdate = allUpdates.find((u) => u.id === orderUpdate.id); + if (existingUpdate) { + existingUpdate.order = orderUpdate.order; + } else { + allUpdates.push(orderUpdate); + } + }); + + await onSubmit({ + teamMemberId, + objective, + description: description || undefined, + period: finalPeriod, + startDate: startDateObj.toISOString() as any, + endDate: endDateObj.toISOString() as any, + 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, + 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({ + teamMemberId, + objective, + description: description || undefined, + period: finalPeriod, + startDate: startDateObj.toISOString() as any, + endDate: endDateObj.toISOString() as any, + keyResults: keyResults.map((kr, i) => ({ ...kr, order: i })), + }); + } } catch (error) { console.error('Error submitting OKR:', error); } finally { @@ -273,7 +357,7 @@ export function OKRForm({ teamMembers, onSubmit, onCancel, initialData }: OKRFor
{keyResults.map((kr, index) => ( -
+
Key Result {index + 1} {keyResults.length > 1 && ( @@ -331,7 +415,13 @@ export function OKRForm({ teamMembers, onSubmit, onCancel, initialData }: OKRFor disabled={submitting} className="bg-[var(--purple)] text-white hover:opacity-90 border-transparent" > - {submitting ? 'Création...' : 'Créer l\'OKR'} + {submitting + ? initialData?.teamMemberId + ? 'Modification...' + : 'Création...' + : initialData?.teamMemberId + ? 'Modifier l\'OKR' + : 'Créer l\'OKR'}
diff --git a/src/components/okrs/OKRsList.tsx b/src/components/okrs/OKRsList.tsx index 3a2b283..a387122 100644 --- a/src/components/okrs/OKRsList.tsx +++ b/src/components/okrs/OKRsList.tsx @@ -18,9 +18,10 @@ interface OKRsListProps { okrs: Array; }>; teamId: string; + isAdmin?: boolean; } -export function OKRsList({ okrsData, teamId }: OKRsListProps) { +export function OKRsList({ okrsData, teamId, isAdmin = false }: OKRsListProps) { const [viewMode, setViewMode] = useState('grouped'); // Flatten OKRs for grid view @@ -123,6 +124,7 @@ export function OKRsList({ okrsData, teamId }: OKRsListProps) { }, }} teamId={teamId} + isAdmin={isAdmin} /> ))}
@@ -133,7 +135,7 @@ export function OKRsList({ okrsData, teamId }: OKRsListProps) { /* Grid View */
{allOKRs.map((okr) => ( - + ))}
)} diff --git a/src/services/okrs.ts b/src/services/okrs.ts index 2340597..d1f9e8a 100644 --- a/src/services/okrs.ts +++ b/src/services/okrs.ts @@ -1,5 +1,5 @@ import { prisma } from '@/services/database'; -import type { CreateOKRInput, UpdateOKRInput, UpdateKeyResultInput, OKRStatus, KeyResultStatus } from '@/lib/types'; +import type { UpdateOKRInput, OKRStatus, KeyResultStatus } from '@/lib/types'; export async function createOKR( teamMemberId: string, @@ -183,30 +183,105 @@ export async function getUserOKRs(userId: string) { ); } -export async function updateOKR(okrId: string, data: UpdateOKRInput) { - const okr = await prisma.oKR.update({ - where: { id: okrId }, - data: { - ...(data.objective !== undefined && { objective: data.objective }), - ...(data.description !== undefined && { description: data.description || null }), - ...(data.period !== undefined && { period: data.period }), - ...(data.startDate !== undefined && { startDate: data.startDate }), - ...(data.endDate !== undefined && { endDate: data.endDate }), - ...(data.status !== undefined && { status: data.status }), - }, - include: { - keyResults: { - orderBy: { - order: 'asc', +export async function updateOKR( + okrId: string, + data: UpdateOKRInput, + keyResultsUpdates?: { + create?: Array<{ title: string; targetValue: number; unit: string; order: number }>; + update?: Array<{ + id: string; + title?: string; + targetValue?: number; + unit?: string; + order?: number; + }>; + delete?: string[]; + } +) { + return prisma.$transaction(async (tx) => { + // Update OKR + await tx.oKR.update({ + where: { id: okrId }, + data: { + ...(data.objective !== undefined && { objective: data.objective }), + ...(data.description !== undefined && { description: data.description || null }), + ...(data.period !== undefined && { period: data.period }), + ...(data.startDate !== undefined && { startDate: data.startDate }), + ...(data.endDate !== undefined && { endDate: data.endDate }), + ...(data.status !== undefined && { status: data.status }), + }, + }); + + // Handle Key Results updates if provided + if (keyResultsUpdates) { + // Delete Key Results + if (keyResultsUpdates.delete && keyResultsUpdates.delete.length > 0) { + await tx.keyResult.deleteMany({ + where: { + id: { in: keyResultsUpdates.delete }, + okrId, + }, + }); + } + + // Create new Key Results + if (keyResultsUpdates.create && keyResultsUpdates.create.length > 0) { + await Promise.all( + keyResultsUpdates.create.map((kr) => + tx.keyResult.create({ + data: { + okrId, + title: kr.title, + targetValue: kr.targetValue, + currentValue: 0, + unit: kr.unit || '%', + status: 'NOT_STARTED', + order: kr.order, + }, + }) + ) + ); + } + + // Update existing Key Results + if (keyResultsUpdates.update && keyResultsUpdates.update.length > 0) { + await Promise.all( + keyResultsUpdates.update.map((kr) => + tx.keyResult.update({ + where: { id: kr.id }, + data: { + ...(kr.title !== undefined && { title: kr.title }), + ...(kr.targetValue !== undefined && { targetValue: kr.targetValue }), + ...(kr.unit !== undefined && { unit: kr.unit }), + ...(kr.order !== undefined && { order: kr.order }), + }, + }) + ) + ); + } + } + + // Fetch updated OKR with Key Results + const updatedOkr = await tx.oKR.findUnique({ + where: { id: okrId }, + include: { + keyResults: { + orderBy: { + order: 'asc', + }, }, }, - }, - }); + }); - return { - ...okr, - progress: calculateOKRProgressFromKeyResults(okr.keyResults), - }; + if (!updatedOkr) { + throw new Error('OKR not found after update'); + } + + return { + ...updatedOkr, + progress: calculateOKRProgressFromKeyResults(updatedOkr.keyResults), + }; + }); } export async function deleteOKR(okrId: string) { @@ -216,6 +291,47 @@ export async function deleteOKR(okrId: string) { }); } +export async function createKeyResult( + okrId: string, + title: string, + targetValue: number, + unit: string, + order: number +) { + return prisma.keyResult.create({ + data: { + okrId, + title, + targetValue, + currentValue: 0, + unit: unit || '%', + status: 'NOT_STARTED', + order, + }, + }); +} + +export async function updateKeyResultMetadata( + krId: string, + data: { title?: string; targetValue?: number; unit?: string; order?: number } +) { + return prisma.keyResult.update({ + where: { id: krId }, + data: { + ...(data.title !== undefined && { title: data.title }), + ...(data.targetValue !== undefined && { targetValue: data.targetValue }), + ...(data.unit !== undefined && { unit: data.unit }), + ...(data.order !== undefined && { order: data.order }), + }, + }); +} + +export async function deleteKeyResult(krId: string) { + return prisma.keyResult.delete({ + where: { id: krId }, + }); +} + export async function updateKeyResult(krId: string, currentValue: number, notes: string | null) { // Auto-update status based on progress const kr = await prisma.keyResult.findUnique({ @@ -293,7 +409,9 @@ export function calculateOKRProgress(okrId: string): Promise { }); } -function calculateOKRProgressFromKeyResults(keyResults: Array<{ currentValue: number; targetValue: number }>): number { +function calculateOKRProgressFromKeyResults( + keyResults: Array<{ currentValue: number; targetValue: number }> +): number { if (keyResults.length === 0) { return 0; } @@ -305,4 +423,3 @@ function calculateOKRProgressFromKeyResults(keyResults: Array<{ currentValue: nu return Math.round(totalProgress / keyResults.length); } -