All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 2m57s
365 lines
14 KiB
TypeScript
365 lines
14 KiB
TypeScript
'use client';
|
|
|
|
import { useTransition } from 'react';
|
|
import { useRouter } from 'next/navigation';
|
|
import Link from 'next/link';
|
|
import { Badge, Card, CardContent, CardHeader, CardTitle, IconButton, IconTrash } from '@/components/ui';
|
|
import { getGravatarUrl } from '@/lib/gravatar';
|
|
import type { OKR, KeyResult, OKRStatus, KeyResultStatus } from '@/lib/types';
|
|
import { OKR_STATUS_LABELS, KEY_RESULT_STATUS_LABELS } from '@/lib/types';
|
|
|
|
// Helper functions for status colors
|
|
function getOKRStatusColor(status: OKRStatus): { 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 (success)
|
|
color: '#10b981',
|
|
};
|
|
case 'CANCELLED':
|
|
return {
|
|
bg: 'color-mix(in srgb, #ef4444 15%, transparent)', // red-500 (destructive)
|
|
color: '#ef4444',
|
|
};
|
|
default:
|
|
return {
|
|
bg: 'color-mix(in srgb, #6b7280 15%, transparent)',
|
|
color: '#6b7280',
|
|
};
|
|
}
|
|
}
|
|
|
|
function getKeyResultStatusColor(status: KeyResultStatus): { bg: string; color: string } {
|
|
switch (status) {
|
|
case 'NOT_STARTED':
|
|
return {
|
|
bg: 'color-mix(in srgb, #6b7280 12%, transparent)', // gray-500
|
|
color: '#6b7280',
|
|
};
|
|
case 'IN_PROGRESS':
|
|
return {
|
|
bg: 'color-mix(in srgb, #3b82f6 12%, transparent)', // blue-500
|
|
color: '#3b82f6',
|
|
};
|
|
case 'COMPLETED':
|
|
return {
|
|
bg: 'color-mix(in srgb, #10b981 12%, transparent)', // green-500
|
|
color: '#10b981',
|
|
};
|
|
case 'AT_RISK':
|
|
return {
|
|
bg: 'color-mix(in srgb, #f59e0b 12%, transparent)', // amber-500 (orange/yellow)
|
|
color: '#f59e0b',
|
|
};
|
|
default:
|
|
return {
|
|
bg: 'color-mix(in srgb, #6b7280 12%, transparent)',
|
|
color: '#6b7280',
|
|
};
|
|
}
|
|
}
|
|
|
|
interface OKRCardProps {
|
|
okr: OKR & { teamMember?: { user: { id: string; email: string; name: string | null } } };
|
|
teamId: string;
|
|
isAdmin?: boolean;
|
|
compact?: boolean;
|
|
}
|
|
|
|
export function OKRCard({ okr, teamId, isAdmin = false, compact = false }: OKRCardProps) {
|
|
const router = useRouter();
|
|
const [isPending, startTransition] = useTransition();
|
|
const progress = okr.progress || 0;
|
|
const progressColor =
|
|
progress >= 75 ? 'var(--success)' : progress >= 25 ? 'var(--accent)' : 'var(--destructive)';
|
|
|
|
const handleDelete = (e: React.MouseEvent) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
if (!confirm(`Êtes-vous sûr de vouloir supprimer l'OKR "${okr.objective}" ?`)) {
|
|
return;
|
|
}
|
|
|
|
startTransition(async () => {
|
|
try {
|
|
const response = await fetch(`/api/okrs/${okr.id}`, {
|
|
method: 'DELETE',
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
alert(error.error || "Erreur lors de la suppression de l'OKR");
|
|
return;
|
|
}
|
|
|
|
router.refresh();
|
|
} catch (error) {
|
|
console.error('Error deleting OKR:', error);
|
|
alert("Erreur lors de la suppression de l'OKR");
|
|
}
|
|
});
|
|
};
|
|
|
|
if (compact) {
|
|
return (
|
|
<Card hover className="relative group">
|
|
<Link href={`/teams/${teamId}/okrs/${okr.id}`}>
|
|
<CardHeader className="pb-2">
|
|
<div className="flex items-start justify-between gap-3">
|
|
<div className="flex-1 min-w-0 flex items-start gap-3">
|
|
<span className="text-xl flex-shrink-0">🎯</span>
|
|
<div className="flex-1 min-w-0">
|
|
<CardTitle className="text-lg leading-snug mb-1.5 line-clamp-2">
|
|
{okr.objective}
|
|
</CardTitle>
|
|
{okr.teamMember && (
|
|
<div className="flex items-center gap-1.5">
|
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
<img
|
|
src={getGravatarUrl(okr.teamMember.user.email, 96)}
|
|
alt={okr.teamMember.user.name || okr.teamMember.user.email}
|
|
width={16}
|
|
height={16}
|
|
className="rounded-full flex-shrink-0"
|
|
/>
|
|
<span className="text-xs text-muted line-clamp-1">
|
|
{okr.teamMember.user.name || okr.teamMember.user.email}
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-1.5 flex-shrink-0 relative z-10">
|
|
{isAdmin && (
|
|
<IconButton
|
|
icon={<IconTrash className="h-3 w-3" />}
|
|
label="Supprimer l'OKR"
|
|
variant="destructive"
|
|
size="xs"
|
|
onClick={handleDelete}
|
|
className="flex-shrink-0"
|
|
style={{
|
|
color: 'var(--destructive)',
|
|
border: '1px solid color-mix(in srgb, var(--destructive) 40%, transparent)',
|
|
backgroundColor: 'color-mix(in srgb, var(--destructive) 5%, transparent)',
|
|
}}
|
|
disabled={isPending}
|
|
/>
|
|
)}
|
|
<Badge
|
|
style={{
|
|
backgroundColor: 'color-mix(in srgb, var(--purple) 15%, transparent)',
|
|
color: 'var(--purple)',
|
|
fontSize: '11px',
|
|
padding: '2px 6px',
|
|
}}
|
|
>
|
|
{okr.period}
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="pt-0 pb-3">
|
|
<div className="flex items-center gap-4">
|
|
{/* Progress Bar */}
|
|
<div className="flex-1 min-w-0">
|
|
<div className="mb-1 flex items-center justify-between text-xs">
|
|
<span className="text-muted">Progression</span>
|
|
<span className="font-medium" style={{ color: progressColor }}>
|
|
{progress}%
|
|
</span>
|
|
</div>
|
|
<div className="h-1.5 w-full overflow-hidden rounded-full bg-card-column">
|
|
<div
|
|
className="h-full transition-all"
|
|
style={{
|
|
width: `${progress}%`,
|
|
backgroundColor: progressColor,
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Status */}
|
|
<div className="flex items-center gap-2 flex-shrink-0">
|
|
<Badge style={getOKRStatusColor(okr.status)} className="text-xs px-2 py-0.5">
|
|
{OKR_STATUS_LABELS[okr.status]}
|
|
</Badge>
|
|
{okr.keyResults && okr.keyResults.length > 0 && (
|
|
<span className="text-xs text-muted whitespace-nowrap">
|
|
{okr.keyResults.length} KR{okr.keyResults.length !== 1 ? 's' : ''}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Link>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Card hover className="h-full relative group">
|
|
<CardHeader>
|
|
<div className="flex items-start justify-between gap-2">
|
|
<Link href={`/teams/${teamId}/okrs/${okr.id}`} className="flex-1 min-w-0">
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex-1 pr-2">
|
|
<CardTitle className="flex items-center gap-2">
|
|
<span className="text-xl">🎯</span>
|
|
{okr.objective}
|
|
</CardTitle>
|
|
{okr.teamMember && (
|
|
<div className="mt-2 flex items-center gap-2">
|
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
<img
|
|
src={getGravatarUrl(okr.teamMember.user.email, 96)}
|
|
alt={okr.teamMember.user.name || okr.teamMember.user.email}
|
|
width={24}
|
|
height={24}
|
|
className="rounded-full"
|
|
/>
|
|
<span className="text-sm text-muted">
|
|
{okr.teamMember.user.name || okr.teamMember.user.email}
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</Link>
|
|
|
|
{/* Action Zone */}
|
|
<div className="flex items-center gap-2 flex-shrink-0 relative z-10">
|
|
{isAdmin && (
|
|
<IconButton
|
|
icon={<IconTrash className="h-4 w-4" />}
|
|
label="Supprimer l'OKR"
|
|
variant="destructive"
|
|
size="sm"
|
|
onClick={handleDelete}
|
|
className="flex-shrink-0"
|
|
style={{
|
|
color: 'var(--destructive)',
|
|
border: '1px solid color-mix(in srgb, var(--destructive) 40%, transparent)',
|
|
backgroundColor: 'color-mix(in srgb, var(--destructive) 5%, transparent)',
|
|
}}
|
|
disabled={isPending}
|
|
/>
|
|
)}
|
|
<Badge
|
|
style={{
|
|
backgroundColor: 'color-mix(in srgb, var(--purple) 15%, transparent)',
|
|
color: 'var(--purple)',
|
|
}}
|
|
>
|
|
{okr.period}
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
</CardHeader>
|
|
<Link href={`/teams/${teamId}/okrs/${okr.id}`}>
|
|
<CardContent>
|
|
<div className="space-y-3">
|
|
{/* Progress Bar */}
|
|
<div>
|
|
<div className="mb-1 flex items-center justify-between text-sm">
|
|
<span className="text-muted">Progression</span>
|
|
<span className="font-medium" style={{ color: progressColor }}>
|
|
{progress}%
|
|
</span>
|
|
</div>
|
|
<div className="h-2 w-full overflow-hidden rounded-full bg-card-column">
|
|
<div
|
|
className="h-full transition-all"
|
|
style={{
|
|
width: `${progress}%`,
|
|
backgroundColor: progressColor,
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Status */}
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-sm text-muted">Statut:</span>
|
|
<Badge style={getOKRStatusColor(okr.status)}>{OKR_STATUS_LABELS[okr.status]}</Badge>
|
|
</div>
|
|
|
|
{/* Key Results List */}
|
|
{okr.keyResults && okr.keyResults.length > 0 && (
|
|
<div className="space-y-2 pt-2 border-t border-border">
|
|
<div className="text-xs font-medium text-muted uppercase tracking-wide">
|
|
Key Results ({okr.keyResults.length})
|
|
</div>
|
|
<div className="space-y-2">
|
|
{okr.keyResults
|
|
.sort((a, b) => a.order - b.order)
|
|
.map((kr: KeyResult) => {
|
|
const krProgress =
|
|
kr.targetValue > 0 ? (kr.currentValue / kr.targetValue) * 100 : 0;
|
|
const krProgressColor =
|
|
krProgress >= 100
|
|
? 'var(--success)'
|
|
: krProgress >= 50
|
|
? 'var(--accent)'
|
|
: 'var(--destructive)';
|
|
|
|
return (
|
|
<div key={kr.id} className="space-y-1">
|
|
<div className="flex items-start justify-between gap-2">
|
|
<span className="text-sm text-foreground flex-1 line-clamp-2">
|
|
{kr.title}
|
|
</span>
|
|
<Badge
|
|
style={{
|
|
...getKeyResultStatusColor(kr.status),
|
|
fontSize: '10px',
|
|
padding: '2px 6px',
|
|
}}
|
|
>
|
|
{KEY_RESULT_STATUS_LABELS[kr.status]}
|
|
</Badge>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<div className="flex items-center justify-between text-xs text-muted">
|
|
<span>
|
|
{kr.currentValue} / {kr.targetValue} {kr.unit}
|
|
</span>
|
|
<span className="font-medium" style={{ color: krProgressColor }}>
|
|
{Math.round(krProgress)}%
|
|
</span>
|
|
</div>
|
|
<div className="h-1.5 w-full overflow-hidden rounded-full bg-card-column">
|
|
<div
|
|
className="h-full transition-all"
|
|
style={{
|
|
width: `${Math.min(krProgress, 100)}%`,
|
|
backgroundColor: krProgressColor,
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</CardContent>
|
|
</Link>
|
|
</Card>
|
|
);
|
|
}
|