feat: enhance OKR management by adding permission checks for editing and deleting, and updating OKR forms to handle key results more effectively
Some checks failed
Deploy with Docker Compose / deploy (push) Failing after 4m44s

This commit is contained in:
Julien Froidefond
2026-01-07 16:48:23 +01:00
parent 5f661c8bfd
commit ca9b68ebbd
8 changed files with 562 additions and 100 deletions

View File

@@ -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) {

View File

@@ -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<OKRWithTeamMember | null>(null);
const [teamMembers, setTeamMembers] = useState<TeamMember[]>([]);
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 (
<main className="mx-auto max-w-4xl px-4 py-8">
<div className="text-center">Chargement...</div>
</main>
);
}
if (!okr) {
return (
<main className="mx-auto max-w-4xl px-4 py-8">
<div className="text-center">OKR non trouvé</div>
</main>
);
}
// Prepare initial data for the form
const initialData: Partial<CreateOKRInput> & { 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 (
<main className="mx-auto max-w-4xl px-4 py-8">
<div className="mb-6">
<Link href={`/teams/${teamId}/okrs/${okrId}`} className="text-muted hover:text-foreground">
Retour à l'OKR
</Link>
</div>
<Card className="p-6">
<h1 className="text-2xl font-bold text-foreground mb-6">Modifier l'OKR</h1>
<OKRForm
teamMembers={teamMembers}
onSubmit={handleSubmit}
onCancel={() => router.push(`/teams/${teamId}/okrs/${okrId}`)}
initialData={initialData}
/>
</Card>
</main>
);
}

View File

@@ -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<OKRWithTeamMember | null>(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 (
<main className="mx-auto max-w-4xl px-4 py-8">
@@ -222,19 +223,30 @@ export default function OKRDetailPage() {
</div>
{/* Actions */}
{isAdmin && (
{(canEdit || canDelete) && (
<div className="mt-4 flex gap-2">
<Button
onClick={handleDelete}
variant="outline"
size="sm"
style={{
color: 'var(--destructive)',
borderColor: 'var(--destructive)',
}}
>
Supprimer
</Button>
{canEdit && (
<Button
onClick={() => router.push(`/teams/${teamId}/okrs/${okrId}/edit`)}
variant="outline"
size="sm"
>
Éditer
</Button>
)}
{canDelete && (
<Button
onClick={handleDelete}
variant="outline"
size="sm"
style={{
color: 'var(--destructive)',
borderColor: 'var(--destructive)',
}}
>
Supprimer
</Button>
)}
</div>
)}
</CardContent>

View File

@@ -78,7 +78,7 @@ export default async function TeamDetailPage({ params }: TeamDetailPageProps) {
</Card>
{/* OKRs Section */}
<OKRsList okrsData={okrsData} teamId={id} />
<OKRsList okrsData={okrsData} teamId={id} isAdmin={isAdmin} />
</main>
);
}