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
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 2m26s
This commit is contained in:
@@ -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 */}
|
||||
|
||||
@@ -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':
|
||||
|
||||
Reference in New Issue
Block a user