feat: refactor ObjectivesPage to utilize ObjectivesList component for improved rendering and simplify OKR status handling in OKRCard with compact view option
Some checks failed
Deploy with Docker Compose / deploy (push) Failing after 4m17s
Some checks failed
Deploy with Docker Compose / deploy (push) Failing after 4m17s
This commit is contained in:
@@ -2,72 +2,8 @@ import { auth } from '@/lib/auth';
|
|||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { getUserOKRs } from '@/services/okrs';
|
import { getUserOKRs } from '@/services/okrs';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui';
|
import { Card } from '@/components/ui';
|
||||||
import { Badge } from '@/components/ui';
|
import { ObjectivesList } from '@/components/okrs/ObjectivesList';
|
||||||
import { getGravatarUrl } from '@/lib/gravatar';
|
|
||||||
import type { 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)',
|
|
||||||
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',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getKeyResultStatusColor(status: KeyResultStatus): { bg: string; color: string } {
|
|
||||||
switch (status) {
|
|
||||||
case 'NOT_STARTED':
|
|
||||||
return {
|
|
||||||
bg: 'color-mix(in srgb, #6b7280 12%, transparent)',
|
|
||||||
color: '#6b7280',
|
|
||||||
};
|
|
||||||
case 'IN_PROGRESS':
|
|
||||||
return {
|
|
||||||
bg: 'color-mix(in srgb, #3b82f6 12%, transparent)',
|
|
||||||
color: '#3b82f6',
|
|
||||||
};
|
|
||||||
case 'COMPLETED':
|
|
||||||
return {
|
|
||||||
bg: 'color-mix(in srgb, #10b981 12%, transparent)',
|
|
||||||
color: '#10b981',
|
|
||||||
};
|
|
||||||
case 'AT_RISK':
|
|
||||||
return {
|
|
||||||
bg: 'color-mix(in srgb, #f59e0b 12%, transparent)',
|
|
||||||
color: '#f59e0b',
|
|
||||||
};
|
|
||||||
default:
|
|
||||||
return {
|
|
||||||
bg: 'color-mix(in srgb, #6b7280 12%, transparent)',
|
|
||||||
color: '#6b7280',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function ObjectivesPage() {
|
export default async function ObjectivesPage() {
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
@@ -120,8 +56,8 @@ export default async function ObjectivesPage() {
|
|||||||
<div className="text-5xl mb-4">🎯</div>
|
<div className="text-5xl mb-4">🎯</div>
|
||||||
<h3 className="text-xl font-semibold text-foreground mb-2">Aucun OKR défini</h3>
|
<h3 className="text-xl font-semibold text-foreground mb-2">Aucun OKR défini</h3>
|
||||||
<p className="text-muted mb-6">
|
<p className="text-muted mb-6">
|
||||||
Vous n'avez pas encore d'OKR défini. Contactez un administrateur d'équipe pour
|
Vous n'avez pas encore d'OKR défini. Contactez un administrateur d'équipe
|
||||||
en créer.
|
pour en créer.
|
||||||
</p>
|
</p>
|
||||||
<Link href="/teams">
|
<Link href="/teams">
|
||||||
<span className="inline-block rounded-lg bg-[var(--purple)] px-4 py-2 text-white hover:opacity-90">
|
<span className="inline-block rounded-lg bg-[var(--purple)] px-4 py-2 text-white hover:opacity-90">
|
||||||
@@ -130,172 +66,8 @@ export default async function ObjectivesPage() {
|
|||||||
</Link>
|
</Link>
|
||||||
</Card>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-8">
|
<ObjectivesList okrsByPeriod={okrsByPeriod} periods={periods} />
|
||||||
{periods.map((period) => {
|
|
||||||
const periodOKRs = okrsByPeriod[period];
|
|
||||||
const totalProgress =
|
|
||||||
periodOKRs.reduce((sum, okr) => sum + (okr.progress || 0), 0) / periodOKRs.length;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={period} className="space-y-4">
|
|
||||||
{/* Period Header */}
|
|
||||||
<div className="flex items-center justify-between border-b border-border pb-3">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<Badge
|
|
||||||
style={{
|
|
||||||
backgroundColor: 'color-mix(in srgb, var(--purple) 15%, transparent)',
|
|
||||||
color: 'var(--purple)',
|
|
||||||
fontSize: '14px',
|
|
||||||
padding: '6px 12px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{period}
|
|
||||||
</Badge>
|
|
||||||
<span className="text-sm text-muted">
|
|
||||||
{periodOKRs.length} OKR{periodOKRs.length !== 1 ? 's' : ''}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="text-sm font-medium text-foreground">
|
|
||||||
Progression moyenne: <span style={{ color: 'var(--primary)' }}>{Math.round(totalProgress)}%</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* OKRs Grid */}
|
|
||||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
|
||||||
{periodOKRs.map((okr) => {
|
|
||||||
const progress = okr.progress || 0;
|
|
||||||
const progressColor =
|
|
||||||
progress >= 75 ? '#10b981' : progress >= 25 ? '#f59e0b' : '#ef4444';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Link key={okr.id} href={`/teams/${okr.team.id}/okrs/${okr.id}`}>
|
|
||||||
<Card hover className="h-full flex flex-col">
|
|
||||||
<CardHeader>
|
|
||||||
<div className="flex items-start justify-between mb-2">
|
|
||||||
<CardTitle className="text-lg flex-1 line-clamp-2">{okr.objective}</CardTitle>
|
|
||||||
<Badge style={getOKRStatusColor(okr.status)}>
|
|
||||||
{OKR_STATUS_LABELS[okr.status]}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
{okr.description && (
|
|
||||||
<p className="text-sm text-muted line-clamp-2">{okr.description}</p>
|
|
||||||
)}
|
|
||||||
<div className="mt-2 flex items-center gap-2 text-xs text-muted">
|
|
||||||
<span>👥</span>
|
|
||||||
<span>{okr.team.name}</span>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex-1 flex flex-col">
|
|
||||||
{/* Progress Bar */}
|
|
||||||
<div className="mb-4">
|
|
||||||
<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>
|
|
||||||
|
|
||||||
{/* Key Results Preview */}
|
|
||||||
{okr.keyResults && okr.keyResults.length > 0 && (
|
|
||||||
<div className="mt-auto space-y-2">
|
|
||||||
<div className="text-xs font-medium text-muted uppercase tracking-wide">
|
|
||||||
Key Results ({okr.keyResults.length})
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{okr.keyResults.slice(0, 3).map((kr) => {
|
|
||||||
const krProgress =
|
|
||||||
kr.targetValue > 0 ? (kr.currentValue / kr.targetValue) * 100 : 0;
|
|
||||||
const krProgressColor =
|
|
||||||
krProgress >= 100
|
|
||||||
? '#10b981'
|
|
||||||
: krProgress >= 50
|
|
||||||
? '#f59e0b'
|
|
||||||
: '#ef4444';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={kr.id} className="space-y-1">
|
|
||||||
<div className="flex items-start justify-between gap-2">
|
|
||||||
<span className="text-xs text-foreground flex-1 line-clamp-1">
|
|
||||||
{kr.title}
|
|
||||||
</span>
|
|
||||||
<Badge
|
|
||||||
style={{
|
|
||||||
...getKeyResultStatusColor(kr.status),
|
|
||||||
fontSize: '9px',
|
|
||||||
padding: '1px 4px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{KEY_RESULT_STATUS_LABELS[kr.status]}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
<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 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>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{okr.keyResults.length > 3 && (
|
|
||||||
<div className="text-xs text-muted text-center pt-1">
|
|
||||||
+{okr.keyResults.length - 3} autre{okr.keyResults.length - 3 !== 1 ? 's' : ''}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Dates */}
|
|
||||||
<div className="mt-4 pt-4 border-t border-border flex items-center justify-between text-xs text-muted">
|
|
||||||
<span>
|
|
||||||
{new Date(okr.startDate).toLocaleDateString('fr-FR', {
|
|
||||||
day: 'numeric',
|
|
||||||
month: 'short',
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
<span>→</span>
|
|
||||||
<span>
|
|
||||||
{new Date(okr.endDate).toLocaleDateString('fr-FR', {
|
|
||||||
day: 'numeric',
|
|
||||||
month: 'short',
|
|
||||||
year: 'numeric',
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useTransition } from 'react';
|
import { useTransition } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui';
|
||||||
import { Badge } from '@/components/ui';
|
import { Badge } from '@/components/ui';
|
||||||
import { Button } from '@/components/ui';
|
|
||||||
import { getGravatarUrl } from '@/lib/gravatar';
|
import { getGravatarUrl } from '@/lib/gravatar';
|
||||||
import type { OKR, KeyResult, OKRStatus, KeyResultStatus } from '@/lib/types';
|
import type { OKR, KeyResult, OKRStatus, KeyResultStatus } from '@/lib/types';
|
||||||
import { OKR_STATUS_LABELS, KEY_RESULT_STATUS_LABELS } from '@/lib/types';
|
import { OKR_STATUS_LABELS, KEY_RESULT_STATUS_LABELS } from '@/lib/types';
|
||||||
@@ -75,9 +74,10 @@ interface OKRCardProps {
|
|||||||
okr: OKR & { teamMember?: { user: { id: string; email: string; name: string | null } } };
|
okr: OKR & { teamMember?: { user: { id: string; email: string; name: string | null } } };
|
||||||
teamId: string;
|
teamId: string;
|
||||||
isAdmin?: boolean;
|
isAdmin?: boolean;
|
||||||
|
compact?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function OKRCard({ okr, teamId, isAdmin = false }: OKRCardProps) {
|
export function OKRCard({ okr, teamId, isAdmin = false, compact = false }: OKRCardProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [isPending, startTransition] = useTransition();
|
const [isPending, startTransition] = useTransition();
|
||||||
const progress = okr.progress || 0;
|
const progress = okr.progress || 0;
|
||||||
@@ -100,18 +100,128 @@ export function OKRCard({ okr, teamId, isAdmin = false }: OKRCardProps) {
|
|||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const error = await response.json();
|
const error = await response.json();
|
||||||
alert(error.error || 'Erreur lors de la suppression de l\'OKR');
|
alert(error.error || "Erreur lors de la suppression de l'OKR");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
router.refresh();
|
router.refresh();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting OKR:', error);
|
console.error('Error deleting OKR:', error);
|
||||||
alert('Erreur lors de la suppression de l\'OKR');
|
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 && (
|
||||||
|
<button
|
||||||
|
onClick={handleDelete}
|
||||||
|
className="h-5 w-5 p-0 flex items-center justify-center rounded hover:bg-destructive/10 transition-colors 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}
|
||||||
|
title="Supprimer l'OKR"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="h-3 w-3"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2.5}
|
||||||
|
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<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 (
|
return (
|
||||||
<Card hover className="h-full relative group">
|
<Card hover className="h-full relative group">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -208,9 +318,7 @@ export function OKRCard({ okr, teamId, isAdmin = false }: OKRCardProps) {
|
|||||||
{/* Status */}
|
{/* Status */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-sm text-muted">Statut:</span>
|
<span className="text-sm text-muted">Statut:</span>
|
||||||
<Badge style={getOKRStatusColor(okr.status)}>
|
<Badge style={getOKRStatusColor(okr.status)}>{OKR_STATUS_LABELS[okr.status]}</Badge>
|
||||||
{OKR_STATUS_LABELS[okr.status]}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Key Results List */}
|
{/* Key Results List */}
|
||||||
@@ -223,7 +331,8 @@ export function OKRCard({ okr, teamId, isAdmin = false }: OKRCardProps) {
|
|||||||
{okr.keyResults
|
{okr.keyResults
|
||||||
.sort((a, b) => a.order - b.order)
|
.sort((a, b) => a.order - b.order)
|
||||||
.map((kr: KeyResult) => {
|
.map((kr: KeyResult) => {
|
||||||
const krProgress = kr.targetValue > 0 ? (kr.currentValue / kr.targetValue) * 100 : 0;
|
const krProgress =
|
||||||
|
kr.targetValue > 0 ? (kr.currentValue / kr.targetValue) * 100 : 0;
|
||||||
const krProgressColor =
|
const krProgressColor =
|
||||||
krProgress >= 100
|
krProgress >= 100
|
||||||
? 'var(--success)'
|
? 'var(--success)'
|
||||||
@@ -234,7 +343,9 @@ export function OKRCard({ okr, teamId, isAdmin = false }: OKRCardProps) {
|
|||||||
return (
|
return (
|
||||||
<div key={kr.id} className="space-y-1">
|
<div key={kr.id} className="space-y-1">
|
||||||
<div className="flex items-start justify-between gap-2">
|
<div className="flex items-start justify-between gap-2">
|
||||||
<span className="text-sm text-foreground flex-1 line-clamp-2">{kr.title}</span>
|
<span className="text-sm text-foreground flex-1 line-clamp-2">
|
||||||
|
{kr.title}
|
||||||
|
</span>
|
||||||
<Badge
|
<Badge
|
||||||
style={{
|
style={{
|
||||||
...getKeyResultStatusColor(kr.status),
|
...getKeyResultStatusColor(kr.status),
|
||||||
@@ -276,4 +387,3 @@ export function OKRCard({ okr, teamId, isAdmin = false }: OKRCardProps) {
|
|||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { OKRCard } from './OKRCard';
|
import { OKRCard } from './OKRCard';
|
||||||
import { Card, ToggleGroup, type ToggleOption } from '@/components/ui';
|
import { Card, ToggleGroup } from '@/components/ui';
|
||||||
import { getGravatarUrl } from '@/lib/gravatar';
|
import { getGravatarUrl } from '@/lib/gravatar';
|
||||||
import type { OKR } from '@/lib/types';
|
import type { OKR } from '@/lib/types';
|
||||||
|
|
||||||
type ViewMode = 'grid' | 'grouped';
|
type ViewMode = 'grid' | 'grouped';
|
||||||
|
type CardViewMode = 'detailed' | 'compact';
|
||||||
|
|
||||||
interface OKRsListProps {
|
interface OKRsListProps {
|
||||||
okrsData: Array<{
|
okrsData: Array<{
|
||||||
@@ -21,8 +22,23 @@ interface OKRsListProps {
|
|||||||
isAdmin?: boolean;
|
isAdmin?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CARD_VIEW_STORAGE_KEY = 'okr-card-view-mode';
|
||||||
|
|
||||||
export function OKRsList({ okrsData, teamId, isAdmin = false }: OKRsListProps) {
|
export function OKRsList({ okrsData, teamId, isAdmin = false }: OKRsListProps) {
|
||||||
const [viewMode, setViewMode] = useState<ViewMode>('grouped');
|
const [viewMode, setViewMode] = useState<ViewMode>('grouped');
|
||||||
|
const [cardViewMode, setCardViewMode] = useState<CardViewMode>(() => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const stored = localStorage.getItem(CARD_VIEW_STORAGE_KEY);
|
||||||
|
return (stored as CardViewMode) || 'detailed';
|
||||||
|
}
|
||||||
|
return 'detailed';
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
localStorage.setItem(CARD_VIEW_STORAGE_KEY, cardViewMode);
|
||||||
|
}
|
||||||
|
}, [cardViewMode]);
|
||||||
|
|
||||||
// Flatten OKRs for grid view
|
// Flatten OKRs for grid view
|
||||||
const allOKRs = okrsData.flatMap((tm) =>
|
const allOKRs = okrsData.flatMap((tm) =>
|
||||||
@@ -39,9 +55,7 @@ export function OKRsList({ okrsData, teamId, isAdmin = false }: OKRsListProps) {
|
|||||||
<Card className="p-12 text-center">
|
<Card className="p-12 text-center">
|
||||||
<div className="text-5xl mb-4">🎯</div>
|
<div className="text-5xl mb-4">🎯</div>
|
||||||
<h3 className="text-xl font-semibold text-foreground mb-2">Aucun OKR défini</h3>
|
<h3 className="text-xl font-semibold text-foreground mb-2">Aucun OKR défini</h3>
|
||||||
<p className="text-muted">
|
<p className="text-muted">Aucun OKR n'a encore été défini pour cette équipe</p>
|
||||||
Aucun OKR n'a encore été défini pour cette équipe
|
|
||||||
</p>
|
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -49,8 +63,43 @@ export function OKRsList({ okrsData, teamId, isAdmin = false }: OKRsListProps) {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{/* View Toggle */}
|
{/* View Toggle */}
|
||||||
<div className="mb-6 flex items-center justify-between">
|
<div className="mb-6 flex items-center justify-between gap-4">
|
||||||
<h2 className="text-2xl font-bold text-foreground">OKRs</h2>
|
<h2 className="text-2xl font-bold text-foreground">OKRs</h2>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<ToggleGroup
|
||||||
|
value={cardViewMode}
|
||||||
|
onChange={setCardViewMode}
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
value: 'detailed',
|
||||||
|
label: 'Détaillée',
|
||||||
|
icon: (
|
||||||
|
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M4 6h16M4 12h16M4 18h16"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'compact',
|
||||||
|
label: 'Mini',
|
||||||
|
icon: (
|
||||||
|
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M4 6h16M4 10h16M4 14h16M4 18h16"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
<ToggleGroup
|
<ToggleGroup
|
||||||
value={viewMode}
|
value={viewMode}
|
||||||
onChange={setViewMode}
|
onChange={setViewMode}
|
||||||
@@ -60,7 +109,12 @@ export function OKRsList({ okrsData, teamId, isAdmin = false }: OKRsListProps) {
|
|||||||
label: 'Par membre',
|
label: 'Par membre',
|
||||||
icon: (
|
icon: (
|
||||||
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -81,6 +135,7 @@ export function OKRsList({ okrsData, teamId, isAdmin = false }: OKRsListProps) {
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Grouped View */}
|
{/* Grouped View */}
|
||||||
{viewMode === 'grouped' ? (
|
{viewMode === 'grouped' ? (
|
||||||
@@ -113,7 +168,9 @@ export function OKRsList({ okrsData, teamId, isAdmin = false }: OKRsListProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* OKRs Grid */}
|
{/* OKRs Grid */}
|
||||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
<div
|
||||||
|
className={`grid gap-6 ${cardViewMode === 'compact' ? 'md:grid-cols-2 lg:grid-cols-3' : 'md:grid-cols-2 lg:grid-cols-3'}`}
|
||||||
|
>
|
||||||
{teamMember.okrs.map((okr) => (
|
{teamMember.okrs.map((okr) => (
|
||||||
<OKRCard
|
<OKRCard
|
||||||
key={okr.id}
|
key={okr.id}
|
||||||
@@ -125,6 +182,7 @@ export function OKRsList({ okrsData, teamId, isAdmin = false }: OKRsListProps) {
|
|||||||
}}
|
}}
|
||||||
teamId={teamId}
|
teamId={teamId}
|
||||||
isAdmin={isAdmin}
|
isAdmin={isAdmin}
|
||||||
|
compact={cardViewMode === 'compact'}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -133,13 +191,20 @@ export function OKRsList({ okrsData, teamId, isAdmin = false }: OKRsListProps) {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
/* Grid View */
|
/* Grid View */
|
||||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
<div
|
||||||
|
className={`grid gap-6 ${cardViewMode === 'compact' ? 'md:grid-cols-2 lg:grid-cols-3' : 'md:grid-cols-2 lg:grid-cols-3'}`}
|
||||||
|
>
|
||||||
{allOKRs.map((okr) => (
|
{allOKRs.map((okr) => (
|
||||||
<OKRCard key={okr.id} okr={okr} teamId={teamId} isAdmin={isAdmin} />
|
<OKRCard
|
||||||
|
key={okr.id}
|
||||||
|
okr={okr}
|
||||||
|
teamId={teamId}
|
||||||
|
isAdmin={isAdmin}
|
||||||
|
compact={cardViewMode === 'compact'}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
122
src/components/okrs/ObjectivesList.tsx
Normal file
122
src/components/okrs/ObjectivesList.tsx
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { OKRCard } from './OKRCard';
|
||||||
|
import { ToggleGroup } from '@/components/ui';
|
||||||
|
import type { OKR } from '@/lib/types';
|
||||||
|
|
||||||
|
type CardViewMode = 'detailed' | 'compact';
|
||||||
|
|
||||||
|
interface ObjectivesListProps {
|
||||||
|
okrsByPeriod: Record<
|
||||||
|
string,
|
||||||
|
Array<OKR & { progress?: number; team: { id: string; name: string } }>
|
||||||
|
>;
|
||||||
|
periods: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const CARD_VIEW_STORAGE_KEY = 'okr-card-view-mode';
|
||||||
|
|
||||||
|
export function ObjectivesList({ okrsByPeriod, periods }: ObjectivesListProps) {
|
||||||
|
const [cardViewMode, setCardViewMode] = useState<CardViewMode>(() => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const stored = localStorage.getItem(CARD_VIEW_STORAGE_KEY);
|
||||||
|
return (stored as CardViewMode) || 'detailed';
|
||||||
|
}
|
||||||
|
return 'detailed';
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
localStorage.setItem(CARD_VIEW_STORAGE_KEY, cardViewMode);
|
||||||
|
}
|
||||||
|
}, [cardViewMode]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-8">
|
||||||
|
{/* Global View Toggle */}
|
||||||
|
<div className="flex items-center justify-end mb-4">
|
||||||
|
<ToggleGroup
|
||||||
|
value={cardViewMode}
|
||||||
|
onChange={setCardViewMode}
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
value: 'detailed',
|
||||||
|
label: 'Détaillée',
|
||||||
|
icon: (
|
||||||
|
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M4 6h16M4 12h16M4 18h16"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'compact',
|
||||||
|
label: 'Mini',
|
||||||
|
icon: (
|
||||||
|
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M4 6h16M4 10h16M4 14h16M4 18h16"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{periods.map((period) => {
|
||||||
|
const periodOKRs = okrsByPeriod[period];
|
||||||
|
const totalProgress =
|
||||||
|
periodOKRs.reduce((sum, okr) => sum + (okr.progress || 0), 0) / periodOKRs.length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={period} className="space-y-4">
|
||||||
|
{/* Period Header */}
|
||||||
|
<div className="flex items-center justify-between border-b border-border pb-3">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span
|
||||||
|
className="rounded-lg px-3 py-1 text-sm font-medium"
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'color-mix(in srgb, var(--purple) 15%, transparent)',
|
||||||
|
color: 'var(--purple)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{period}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm text-muted">
|
||||||
|
{periodOKRs.length} OKR{periodOKRs.length !== 1 ? 's' : ''}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm font-medium text-foreground">
|
||||||
|
Progression moyenne:{' '}
|
||||||
|
<span style={{ color: 'var(--primary)' }}>{Math.round(totalProgress)}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* OKRs Grid */}
|
||||||
|
<div
|
||||||
|
className={`grid gap-6 ${cardViewMode === 'compact' ? 'md:grid-cols-2 lg:grid-cols-3' : 'md:grid-cols-2 lg:grid-cols-3'}`}
|
||||||
|
>
|
||||||
|
{periodOKRs.map((okr) => (
|
||||||
|
<OKRCard
|
||||||
|
key={okr.id}
|
||||||
|
okr={okr}
|
||||||
|
teamId={okr.team.id}
|
||||||
|
compact={cardViewMode === 'compact'}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,4 +2,5 @@ export { OKRCard } from './OKRCard';
|
|||||||
export { OKRForm } from './OKRForm';
|
export { OKRForm } from './OKRForm';
|
||||||
export { KeyResultItem } from './KeyResultItem';
|
export { KeyResultItem } from './KeyResultItem';
|
||||||
export { OKRsList } from './OKRsList';
|
export { OKRsList } from './OKRsList';
|
||||||
|
export { ObjectivesList } from './ObjectivesList';
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user