All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 12m53s
187 lines
5.9 KiB
TypeScript
187 lines
5.9 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { Input } from '@/components/ui';
|
|
import { Textarea } from '@/components/ui';
|
|
import { Button } from '@/components/ui';
|
|
import { Badge } from '@/components/ui';
|
|
import type { KeyResult, KeyResultStatus } from '@/lib/types';
|
|
import { KEY_RESULT_STATUS_LABELS } from '@/lib/types';
|
|
|
|
// Helper function for Key Result status colors
|
|
function getKeyResultStatusColor(status: KeyResultStatus): { bg: string; color: string } {
|
|
switch (status) {
|
|
case 'NOT_STARTED':
|
|
return {
|
|
bg: 'color-mix(in srgb, #6b7280 15%, transparent)', // gray-500
|
|
color: '#6b7280',
|
|
};
|
|
case 'IN_PROGRESS':
|
|
return {
|
|
bg: 'color-mix(in srgb, #3b82f6 15%, transparent)', // blue-500
|
|
color: '#3b82f6',
|
|
};
|
|
case 'COMPLETED':
|
|
return {
|
|
bg: 'color-mix(in srgb, #10b981 15%, transparent)', // green-500
|
|
color: '#10b981',
|
|
};
|
|
case 'AT_RISK':
|
|
return {
|
|
bg: 'color-mix(in srgb, #f59e0b 15%, transparent)', // amber-500 (orange/yellow)
|
|
color: '#f59e0b',
|
|
};
|
|
default:
|
|
return {
|
|
bg: 'color-mix(in srgb, #6b7280 15%, transparent)',
|
|
color: '#6b7280',
|
|
};
|
|
}
|
|
}
|
|
|
|
interface KeyResultItemProps {
|
|
keyResult: KeyResult;
|
|
okrId: string;
|
|
canEdit: boolean;
|
|
onUpdate?: () => void;
|
|
}
|
|
|
|
export function KeyResultItem({ keyResult, okrId, canEdit, onUpdate }: KeyResultItemProps) {
|
|
const [currentValue, setCurrentValue] = useState(keyResult.currentValue);
|
|
const [notes, setNotes] = useState(keyResult.notes || '');
|
|
const [updating, setUpdating] = useState(false);
|
|
const [isEditing, setIsEditing] = useState(false);
|
|
|
|
const progress = keyResult.targetValue > 0 ? (currentValue / keyResult.targetValue) * 100 : 0;
|
|
const progressColor =
|
|
progress >= 100 ? 'var(--success)' : progress >= 50 ? 'var(--accent)' : 'var(--destructive)';
|
|
|
|
const handleUpdate = async () => {
|
|
setUpdating(true);
|
|
try {
|
|
const response = await fetch(`/api/okrs/${okrId}/key-results/${keyResult.id}`, {
|
|
method: 'PATCH',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
currentValue: Number(currentValue),
|
|
notes: notes || null,
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
alert(error.error || 'Erreur lors de la mise à jour');
|
|
return;
|
|
}
|
|
|
|
setIsEditing(false);
|
|
onUpdate?.();
|
|
} catch (error) {
|
|
console.error('Error updating key result:', error);
|
|
alert('Erreur lors de la mise à jour');
|
|
} finally {
|
|
setUpdating(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="rounded-xl border border-border bg-card p-4">
|
|
<div className="mb-3">
|
|
<div className="flex items-start justify-between">
|
|
<h4 className="font-medium text-foreground">{keyResult.title}</h4>
|
|
<Badge style={getKeyResultStatusColor(keyResult.status)}>
|
|
{KEY_RESULT_STATUS_LABELS[keyResult.status]}
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Progress */}
|
|
<div className="mb-3">
|
|
<div className="mb-1 flex items-center justify-between text-sm">
|
|
<span className="text-muted">
|
|
{currentValue} / {keyResult.targetValue} {keyResult.unit}
|
|
</span>
|
|
<span className="font-medium" style={{ color: progressColor }}>
|
|
{Math.round(progress)}%
|
|
</span>
|
|
</div>
|
|
<div className="h-2 w-full overflow-hidden rounded-full bg-card-column">
|
|
<div
|
|
className="h-full transition-all"
|
|
style={{
|
|
width: `${Math.min(progress, 100)}%`,
|
|
backgroundColor: progressColor,
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Edit Form */}
|
|
{canEdit && (
|
|
<div className="space-y-3 border-t border-border pt-3">
|
|
{isEditing ? (
|
|
<>
|
|
<div>
|
|
<label className="block text-sm font-medium text-foreground mb-1">
|
|
Valeur actuelle
|
|
</label>
|
|
<Input
|
|
type="number"
|
|
value={currentValue}
|
|
onChange={(e) => setCurrentValue(Number(e.target.value))}
|
|
min={0}
|
|
max={keyResult.targetValue * 2}
|
|
step="0.1"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-foreground mb-1">Notes</label>
|
|
<Textarea
|
|
value={notes}
|
|
onChange={(e) => setNotes(e.target.value)}
|
|
placeholder="Ajouter des notes..."
|
|
rows={2}
|
|
/>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<Button onClick={handleUpdate} disabled={updating} size="sm">
|
|
{updating ? 'Mise à jour...' : 'Enregistrer'}
|
|
</Button>
|
|
<Button
|
|
onClick={() => {
|
|
setIsEditing(false);
|
|
setCurrentValue(keyResult.currentValue);
|
|
setNotes(keyResult.notes || '');
|
|
}}
|
|
variant="outline"
|
|
size="sm"
|
|
>
|
|
Annuler
|
|
</Button>
|
|
</div>
|
|
</>
|
|
) : (
|
|
<div>
|
|
{keyResult.notes && (
|
|
<div className="mb-2 text-sm text-muted">
|
|
<strong>Notes:</strong> {keyResult.notes}
|
|
</div>
|
|
)}
|
|
<Button onClick={() => setIsEditing(true)} variant="outline" size="sm">
|
|
Mettre à jour la progression
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{!canEdit && keyResult.notes && (
|
|
<div className="mt-3 border-t border-border pt-3 text-sm text-muted">
|
|
<strong>Notes:</strong> {keyResult.notes}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|