352 lines
11 KiB
TypeScript
352 lines
11 KiB
TypeScript
'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 { 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 & {
|
|
team?: {
|
|
id: string;
|
|
name: string;
|
|
} | null;
|
|
};
|
|
|
|
interface CurrentQuarterOKRsProps {
|
|
okrs: OKRWithTeam[];
|
|
period: string;
|
|
canEdit?: boolean;
|
|
}
|
|
|
|
export function CurrentQuarterOKRs({ okrs, period, canEdit = false }: CurrentQuarterOKRsProps) {
|
|
const [isExpanded, setIsExpanded] = useState(true);
|
|
const router = useRouter();
|
|
|
|
if (okrs.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<Card className="mb-6">
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center justify-between">
|
|
<button
|
|
onClick={() => setIsExpanded(!isExpanded)}
|
|
className="flex items-center gap-2 hover:opacity-80 transition-opacity"
|
|
>
|
|
<span>🎯</span>
|
|
<span>Objectifs du trimestre ({period})</span>
|
|
<svg
|
|
className={`w-5 h-5 transition-transform ${isExpanded ? 'rotate-180' : ''}`}
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M19 9l-7 7-7-7"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
</CardTitle>
|
|
</CardHeader>
|
|
{isExpanded && (
|
|
<CardContent>
|
|
<div className="space-y-3">
|
|
{okrs.map((okr) => {
|
|
const statusColors = getOKRStatusColor(okr.status);
|
|
return (
|
|
<div
|
|
key={okr.id}
|
|
className="rounded-lg border border-border bg-card p-3 hover:bg-card-hover transition-colors"
|
|
>
|
|
<div className="flex items-start justify-between gap-3">
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<EditableObjective
|
|
okr={okr}
|
|
canEdit={canEdit}
|
|
onUpdate={() => router.refresh()}
|
|
/>
|
|
<Badge
|
|
variant="default"
|
|
style={{
|
|
backgroundColor: statusColors.bg,
|
|
color: statusColors.color,
|
|
borderColor: statusColors.color + '30',
|
|
}}
|
|
>
|
|
{OKR_STATUS_LABELS[okr.status]}
|
|
</Badge>
|
|
{okr.progress !== undefined && (
|
|
<span className="text-xs text-muted whitespace-nowrap">
|
|
{okr.progress}%
|
|
</span>
|
|
)}
|
|
</div>
|
|
{okr.description && (
|
|
<p className="text-sm text-muted mb-2">{okr.description}</p>
|
|
)}
|
|
{okr.keyResults && okr.keyResults.length > 0 && (
|
|
<ul className="space-y-1 mt-2">
|
|
{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 - 5} autre
|
|
{okr.keyResults.length - 5 > 1 ? 's' : ''}
|
|
</li>
|
|
)}
|
|
</ul>
|
|
)}
|
|
{okr.team && (
|
|
<div className="mt-2">
|
|
<span className="text-xs text-muted">Équipe: {okr.team.name}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
<div className="mt-4 pt-4 border-t border-border">
|
|
<Link
|
|
href="/objectives"
|
|
className="text-sm text-primary hover:underline flex items-center gap-1"
|
|
>
|
|
Voir tous les objectifs
|
|
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M9 5l7 7-7 7"
|
|
/>
|
|
</svg>
|
|
</Link>
|
|
</div>
|
|
</CardContent>
|
|
)}
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
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':
|
|
return {
|
|
bg: 'color-mix(in srgb, #6b7280 15%, transparent)',
|
|
color: '#6b7280',
|
|
};
|
|
case 'IN_PROGRESS':
|
|
return {
|
|
bg: 'color-mix(in srgb, #3b82f6 15%, transparent)',
|
|
color: '#3b82f6',
|
|
};
|
|
case 'COMPLETED':
|
|
return {
|
|
bg: 'color-mix(in srgb, #10b981 15%, transparent)',
|
|
color: '#10b981',
|
|
};
|
|
case 'CANCELLED':
|
|
return {
|
|
bg: 'color-mix(in srgb, #ef4444 15%, transparent)',
|
|
color: '#ef4444',
|
|
};
|
|
default:
|
|
return {
|
|
bg: 'color-mix(in srgb, #6b7280 15%, transparent)',
|
|
color: '#6b7280',
|
|
};
|
|
}
|
|
}
|