Files
workshop-manager/src/components/weekly-checkin/CurrentQuarterOKRs.tsx

352 lines
11 KiB
TypeScript

'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui';
import { Badge } from '@/components/ui';
import { Input } from '@/components/ui';
import { Button } from '@/components/ui';
import type { OKR, KeyResult } from '@/lib/types';
import { OKR_STATUS_LABELS } from '@/lib/types';
type OKRWithTeam = OKR & {
team?: {
id: string;
name: string;
} | null;
};
interface CurrentQuarterOKRsProps {
okrs: OKRWithTeam[];
period: string;
canEdit?: boolean;
}
export function CurrentQuarterOKRs({ okrs, period, canEdit = false }: CurrentQuarterOKRsProps) {
const [isExpanded, setIsExpanded] = useState(true);
const router = useRouter();
if (okrs.length === 0) {
return null;
}
return (
<Card className="mb-6">
<CardHeader>
<CardTitle className="flex items-center justify-between">
<button
onClick={() => setIsExpanded(!isExpanded)}
className="flex items-center gap-2 hover:opacity-80 transition-opacity"
>
<span>🎯</span>
<span>Objectifs du trimestre ({period})</span>
<svg
className={`w-5 h-5 transition-transform ${isExpanded ? 'rotate-180' : ''}`}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 9l-7 7-7-7"
/>
</svg>
</button>
</CardTitle>
</CardHeader>
{isExpanded && (
<CardContent>
<div className="space-y-3">
{okrs.map((okr) => {
const statusColors = getOKRStatusColor(okr.status);
return (
<div
key={okr.id}
className="rounded-lg border border-border bg-card p-3 hover:bg-card-hover transition-colors"
>
<div className="flex items-start justify-between gap-3">
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<EditableObjective
okr={okr}
canEdit={canEdit}
onUpdate={() => router.refresh()}
/>
<Badge
variant="default"
style={{
backgroundColor: statusColors.bg,
color: statusColors.color,
borderColor: statusColors.color + '30',
}}
>
{OKR_STATUS_LABELS[okr.status]}
</Badge>
{okr.progress !== undefined && (
<span className="text-xs text-muted whitespace-nowrap">
{okr.progress}%
</span>
)}
</div>
{okr.description && (
<p className="text-sm text-muted mb-2">{okr.description}</p>
)}
{okr.keyResults && okr.keyResults.length > 0 && (
<ul className="space-y-1 mt-2">
{okr.keyResults.slice(0, 5).map((kr) => (
<EditableKeyResultRow
key={kr.id}
kr={kr}
okrId={okr.id}
canEdit={canEdit}
onUpdate={() => router.refresh()}
/>
))}
{okr.keyResults.length > 5 && (
<li className="text-xs text-muted pl-3.5">
+{okr.keyResults.length - 5} autre
{okr.keyResults.length - 5 > 1 ? 's' : ''}
</li>
)}
</ul>
)}
{okr.team && (
<div className="mt-2">
<span className="text-xs text-muted">Équipe: {okr.team.name}</span>
</div>
)}
</div>
</div>
</div>
);
})}
</div>
<div className="mt-4 pt-4 border-t border-border">
<Link
href="/objectives"
className="text-sm text-primary hover:underline flex items-center gap-1"
>
Voir tous les objectifs
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 5l7 7-7 7"
/>
</svg>
</Link>
</div>
</CardContent>
)}
</Card>
);
}
function EditableObjective({
okr,
canEdit,
onUpdate,
}: {
okr: OKRWithTeam;
canEdit: boolean;
onUpdate: () => void;
}) {
const [isEditing, setIsEditing] = useState(false);
const [objective, setObjective] = useState(okr.objective);
const [updating, setUpdating] = useState(false);
const handleSave = async () => {
setUpdating(true);
try {
const res = await fetch(`/api/okrs/${okr.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ objective: objective.trim() }),
});
if (!res.ok) {
const err = await res.json();
alert(err.error || 'Erreur lors de la mise à jour');
return;
}
setIsEditing(false);
onUpdate();
} catch (e) {
console.error(e);
alert('Erreur lors de la mise à jour');
} finally {
setUpdating(false);
}
};
if (canEdit && isEditing) {
return (
<div className="flex items-center gap-2 flex-1 min-w-0">
<Input
value={objective}
onChange={(e) => setObjective(e.target.value)}
className="flex-1 h-8 text-sm"
autoFocus
/>
<Button size="sm" onClick={handleSave} disabled={updating} className="h-7 text-xs">
{updating ? '...' : 'OK'}
</Button>
<Button
size="sm"
variant="outline"
onClick={() => {
setIsEditing(false);
setObjective(okr.objective);
}}
className="h-7 text-xs"
>
Annuler
</Button>
</div>
);
}
return (
<h4
className={`font-medium text-foreground ${canEdit ? 'cursor-pointer hover:underline' : ''}`}
onClick={canEdit ? () => setIsEditing(true) : undefined}
role={canEdit ? 'button' : undefined}
>
{okr.objective}
</h4>
);
}
function EditableKeyResultRow({
kr,
okrId,
canEdit,
onUpdate,
}: {
kr: KeyResult;
okrId: string;
canEdit: boolean;
onUpdate: () => void;
}) {
const [isEditing, setIsEditing] = useState(false);
const [currentValue, setCurrentValue] = useState(kr.currentValue);
const [updating, setUpdating] = useState(false);
const krProgress = kr.targetValue > 0 ? Math.round((kr.currentValue / kr.targetValue) * 100) : 0;
const handleSave = async () => {
setUpdating(true);
try {
const res = await fetch(`/api/okrs/${okrId}/key-results/${kr.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ currentValue: Number(currentValue) }),
});
if (!res.ok) {
const err = await res.json();
alert(err.error || 'Erreur lors de la mise à jour');
return;
}
setIsEditing(false);
onUpdate();
} catch (e) {
console.error(e);
alert('Erreur lors de la mise à jour');
} finally {
setUpdating(false);
}
};
if (canEdit && isEditing) {
return (
<li className="text-xs flex items-center gap-2 py-1">
<span className="w-1.5 h-1.5 rounded-full bg-primary flex-shrink-0" />
<span className="flex-1 min-w-0 text-muted">{kr.title}</span>
<div className="flex items-center gap-1 flex-shrink-0 whitespace-nowrap">
<Input
type="number"
value={currentValue}
onChange={(e) => setCurrentValue(Number(e.target.value))}
min={0}
max={kr.targetValue * 2}
step="0.1"
className="h-6 w-16 text-xs"
/>
<span className="text-muted">
/ {kr.targetValue} {kr.unit}
</span>
</div>
<div className="flex items-center gap-1 flex-shrink-0">
<Button size="sm" onClick={handleSave} disabled={updating} className="h-6 text-xs">
{updating ? '...' : 'OK'}
</Button>
<Button
size="sm"
variant="outline"
onClick={() => {
setIsEditing(false);
setCurrentValue(kr.currentValue);
}}
className="h-6 text-xs"
>
Annuler
</Button>
</div>
</li>
);
}
return (
<li className="text-xs text-muted flex items-center gap-2 py-1">
<span className="w-1.5 h-1.5 rounded-full bg-primary flex-shrink-0" />
<span className="flex-1 min-w-0">{kr.title}</span>
<span className="text-muted whitespace-nowrap flex-shrink-0">
{kr.currentValue}/{kr.targetValue} {kr.unit}
</span>
<span className="text-xs whitespace-nowrap flex-shrink-0">({krProgress}%)</span>
{canEdit && (
<button
type="button"
onClick={() => setIsEditing(true)}
className="text-primary hover:underline text-xs"
>
Modifier
</button>
)}
</li>
);
}
function getOKRStatusColor(status: OKR['status']): { 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',
};
}
}