Files
workshop-manager/src/components/okrs/KeyResultItem.tsx
Julien Froidefond 5f661c8bfd
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 12m53s
feat: introduce Teams & OKRs feature with models, types, and UI components for team management and objective tracking
2026-01-07 10:11:59 +01:00

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>
);
}