feat: add editable functionality for current quarter OKRs, allowing participants and team admins to modify objectives and key results, enhancing user interaction and collaboration
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 2m26s

This commit is contained in:
Julien Froidefond
2026-02-18 08:31:32 +01:00
parent ee13f8ba99
commit 35228441e3
2 changed files with 215 additions and 23 deletions

View File

@@ -87,9 +87,25 @@ export default async function WeeklyCheckInSessionPage({ params }: WeeklyCheckIn
</div>
</div>
{/* Current Quarter OKRs */}
{/* Current Quarter OKRs - editable by participant or team admin */}
{currentQuarterOKRs.length > 0 && (
<CurrentQuarterOKRs okrs={currentQuarterOKRs} period={currentQuarterPeriod} />
<CurrentQuarterOKRs
okrs={currentQuarterOKRs}
period={currentQuarterPeriod}
canEdit={
(!!resolvedParticipant.matchedUser &&
authSession.user.id === resolvedParticipant.matchedUser.id) ||
(() => {
const participantTeamIds = new Set(
currentQuarterOKRs.map((okr) => okr.team?.id).filter(Boolean) as string[]
);
const adminTeamIds = userTeams
.filter((t) => t.userRole === 'ADMIN')
.map((t) => t.id);
return adminTeamIds.some((tid) => participantTeamIds.has(tid));
})()
}
/>
)}
{/* Live Wrapper + Board */}

View File

@@ -1,10 +1,13 @@
'use client';
import { useState } 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 type { OKR } from '@/lib/types';
import { Input } from '@/components/ui';
import { Button } from '@/components/ui';
import type { OKR, KeyResult } from '@/lib/types';
import { OKR_STATUS_LABELS } from '@/lib/types';
type OKRWithTeam = OKR & {
@@ -17,10 +20,12 @@ type OKRWithTeam = OKR & {
interface CurrentQuarterOKRsProps {
okrs: OKRWithTeam[];
period: string;
canEdit?: boolean;
}
export function CurrentQuarterOKRs({ okrs, period }: CurrentQuarterOKRsProps) {
export function CurrentQuarterOKRs({ okrs, period, canEdit = false }: CurrentQuarterOKRsProps) {
const [isExpanded, setIsExpanded] = useState(true);
const router = useRouter();
if (okrs.length === 0) {
return null;
@@ -60,7 +65,11 @@ export function CurrentQuarterOKRs({ okrs, period }: CurrentQuarterOKRsProps) {
<div className="flex items-start justify-between gap-3">
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<h4 className="font-medium text-foreground">{okr.objective}</h4>
<EditableObjective
okr={okr}
canEdit={canEdit}
onUpdate={() => router.refresh()}
/>
<Badge
variant="default"
style={{
@@ -72,7 +81,7 @@ export function CurrentQuarterOKRs({ okrs, period }: CurrentQuarterOKRsProps) {
{OKR_STATUS_LABELS[okr.status]}
</Badge>
{okr.progress !== undefined && (
<span className="text-xs text-muted">{okr.progress}%</span>
<span className="text-xs text-muted whitespace-nowrap">{okr.progress}%</span>
)}
</div>
{okr.description && (
@@ -80,24 +89,18 @@ export function CurrentQuarterOKRs({ okrs, period }: CurrentQuarterOKRsProps) {
)}
{okr.keyResults && okr.keyResults.length > 0 && (
<ul className="space-y-1 mt-2">
{okr.keyResults.slice(0, 3).map((kr) => {
const krProgress = kr.targetValue > 0
? Math.round((kr.currentValue / kr.targetValue) * 100)
: 0;
return (
<li key={kr.id} className="text-xs text-muted flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-primary" />
<span className="flex-1">{kr.title}</span>
<span className="text-muted">
{kr.currentValue}/{kr.targetValue} {kr.unit}
</span>
<span className="text-xs">({krProgress}%)</span>
</li>
);
})}
{okr.keyResults.length > 3 && (
{okr.keyResults.slice(0, 5).map((kr) => (
<EditableKeyResultRow
key={kr.id}
kr={kr}
okrId={okr.id}
canEdit={canEdit}
onUpdate={() => router.refresh()}
/>
))}
{okr.keyResults.length > 5 && (
<li className="text-xs text-muted pl-3.5">
+{okr.keyResults.length - 3} autre{okr.keyResults.length - 3 > 1 ? 's' : ''}
+{okr.keyResults.length - 5} autre{okr.keyResults.length - 5 > 1 ? 's' : ''}
</li>
)}
</ul>
@@ -130,6 +133,179 @@ export function CurrentQuarterOKRs({ okrs, period }: CurrentQuarterOKRsProps) {
);
}
function EditableObjective({
okr,
canEdit,
onUpdate,
}: {
okr: OKRWithTeam;
canEdit: boolean;
onUpdate: () => void;
}) {
const [isEditing, setIsEditing] = useState(false);
const [objective, setObjective] = useState(okr.objective);
const [updating, setUpdating] = useState(false);
const handleSave = async () => {
setUpdating(true);
try {
const res = await fetch(`/api/okrs/${okr.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ objective: objective.trim() }),
});
if (!res.ok) {
const err = await res.json();
alert(err.error || 'Erreur lors de la mise à jour');
return;
}
setIsEditing(false);
onUpdate();
} catch (e) {
console.error(e);
alert('Erreur lors de la mise à jour');
} finally {
setUpdating(false);
}
};
if (canEdit && isEditing) {
return (
<div className="flex items-center gap-2 flex-1 min-w-0">
<Input
value={objective}
onChange={(e) => setObjective(e.target.value)}
className="flex-1 h-8 text-sm"
autoFocus
/>
<Button size="sm" onClick={handleSave} disabled={updating} className="h-7 text-xs">
{updating ? '...' : 'OK'}
</Button>
<Button
size="sm"
variant="outline"
onClick={() => {
setIsEditing(false);
setObjective(okr.objective);
}}
className="h-7 text-xs"
>
Annuler
</Button>
</div>
);
}
return (
<h4
className={`font-medium text-foreground ${canEdit ? 'cursor-pointer hover:underline' : ''}`}
onClick={canEdit ? () => setIsEditing(true) : undefined}
role={canEdit ? 'button' : undefined}
>
{okr.objective}
</h4>
);
}
function EditableKeyResultRow({
kr,
okrId,
canEdit,
onUpdate,
}: {
kr: KeyResult;
okrId: string;
canEdit: boolean;
onUpdate: () => void;
}) {
const [isEditing, setIsEditing] = useState(false);
const [currentValue, setCurrentValue] = useState(kr.currentValue);
const [updating, setUpdating] = useState(false);
const krProgress =
kr.targetValue > 0 ? Math.round((kr.currentValue / kr.targetValue) * 100) : 0;
const handleSave = async () => {
setUpdating(true);
try {
const res = await fetch(`/api/okrs/${okrId}/key-results/${kr.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ currentValue: Number(currentValue) }),
});
if (!res.ok) {
const err = await res.json();
alert(err.error || 'Erreur lors de la mise à jour');
return;
}
setIsEditing(false);
onUpdate();
} catch (e) {
console.error(e);
alert('Erreur lors de la mise à jour');
} finally {
setUpdating(false);
}
};
if (canEdit && isEditing) {
return (
<li className="text-xs flex items-center gap-2 py-1">
<span className="w-1.5 h-1.5 rounded-full bg-primary flex-shrink-0" />
<span className="flex-1 min-w-0 text-muted">{kr.title}</span>
<div className="flex items-center gap-1 flex-shrink-0 whitespace-nowrap">
<Input
type="number"
value={currentValue}
onChange={(e) => setCurrentValue(Number(e.target.value))}
min={0}
max={kr.targetValue * 2}
step="0.1"
className="h-6 w-16 text-xs"
/>
<span className="text-muted">/ {kr.targetValue} {kr.unit}</span>
</div>
<div className="flex items-center gap-1 flex-shrink-0">
<Button size="sm" onClick={handleSave} disabled={updating} className="h-6 text-xs">
{updating ? '...' : 'OK'}
</Button>
<Button
size="sm"
variant="outline"
onClick={() => {
setIsEditing(false);
setCurrentValue(kr.currentValue);
}}
className="h-6 text-xs"
>
Annuler
</Button>
</div>
</li>
);
}
return (
<li className="text-xs text-muted flex items-center gap-2 py-1">
<span className="w-1.5 h-1.5 rounded-full bg-primary flex-shrink-0" />
<span className="flex-1 min-w-0">{kr.title}</span>
<span className="text-muted whitespace-nowrap flex-shrink-0">
{kr.currentValue}/{kr.targetValue} {kr.unit}
</span>
<span className="text-xs whitespace-nowrap flex-shrink-0">({krProgress}%)</span>
{canEdit && (
<button
type="button"
onClick={() => setIsEditing(true)}
className="text-primary hover:underline text-xs"
>
Modifier
</button>
)}
</li>
);
}
function getOKRStatusColor(status: OKR['status']): { bg: string; color: string } {
switch (status) {
case 'NOT_STARTED':