All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 12m53s
210 lines
7.6 KiB
TypeScript
210 lines
7.6 KiB
TypeScript
'use client';
|
|
|
|
import Link from 'next/link';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui';
|
|
import { Badge } 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;
|
|
}
|
|
|
|
export function OKRCard({ okr, teamId }: OKRCardProps) {
|
|
const progress = okr.progress || 0;
|
|
const progressColor =
|
|
progress >= 75 ? 'var(--success)' : progress >= 25 ? 'var(--accent)' : 'var(--destructive)';
|
|
|
|
return (
|
|
<Link href={`/teams/${teamId}/okrs/${okr.id}`}>
|
|
<Card hover className="h-full">
|
|
<CardHeader>
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex-1">
|
|
<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>
|
|
<Badge
|
|
style={{
|
|
backgroundColor: 'color-mix(in srgb, var(--purple) 15%, transparent)',
|
|
color: 'var(--purple)',
|
|
}}
|
|
>
|
|
{okr.period}
|
|
</Badge>
|
|
</div>
|
|
</CardHeader>
|
|
<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>
|
|
</Card>
|
|
</Link>
|
|
);
|
|
}
|
|
|