refactor(ManagerWeeklySummary): replace AchievementCard and ChallengeCard with TaskCard, implement tag filtering for accomplishments and challenges, and enhance UI for better data presentation

This commit is contained in:
Julien Froidefond
2025-11-26 08:40:42 +01:00
parent 4c0f227e27
commit 4fc41a5b2c
6 changed files with 286 additions and 477 deletions

View File

@@ -1,12 +1,12 @@
'use client'; 'use client';
import { useState } from 'react'; import { useState, useMemo } from 'react';
import { ManagerSummary } from '@/services/analytics/manager-summary'; import { ManagerSummary } from '@/services/analytics/manager-summary';
import { Card, CardHeader, CardContent } from '@/components/ui/Card'; import { Card, CardHeader, CardContent } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import { Tabs, TabItem } from '@/components/ui/Tabs'; import { Tabs, TabItem } from '@/components/ui/Tabs';
import { AchievementCard } from '@/components/ui/AchievementCard'; import { TaskCard } from '@/components/ui/TaskCard';
import { ChallengeCard } from '@/components/ui/ChallengeCard'; import { FilterChip } from '@/components/ui/FilterChip';
import { useTasksContext } from '@/contexts/TasksContext'; import { useTasksContext } from '@/contexts/TasksContext';
import { MetricsTab } from './MetricsTab'; import { MetricsTab } from './MetricsTab';
import { format } from 'date-fns'; import { format } from 'date-fns';
@@ -22,15 +22,19 @@ export default function ManagerWeeklySummary({
initialSummary, initialSummary,
}: ManagerWeeklySummaryProps) { }: ManagerWeeklySummaryProps) {
const [summary] = useState<ManagerSummary>(initialSummary); const [summary] = useState<ManagerSummary>(initialSummary);
const [activeView, setActiveView] = useState< const [activeView, setActiveView] = useState<'narrative' | 'metrics'>(
'narrative' | 'accomplishments' | 'challenges' | 'metrics' 'narrative'
>('narrative'); );
const [selectedAccomplishmentTags, setSelectedAccomplishmentTags] = useState<
string[]
>([]);
const [selectedChallengeTags, setSelectedChallengeTags] = useState<string[]>(
[]
);
const { tags: availableTags } = useTasksContext(); const { tags: availableTags } = useTasksContext();
const handleTabChange = (tabId: string) => { const handleTabChange = (tabId: string) => {
setActiveView( setActiveView(tabId as 'narrative' | 'metrics');
tabId as 'narrative' | 'accomplishments' | 'challenges' | 'metrics'
);
}; };
const handleRefresh = () => { const handleRefresh = () => {
@@ -42,21 +46,99 @@ export default function ManagerWeeklySummary({
return `7 derniers jours (${format(summary.period.start, 'dd MMM', { locale: fr })} - ${format(summary.period.end, 'dd MMM yyyy', { locale: fr })})`; return `7 derniers jours (${format(summary.period.start, 'dd MMM', { locale: fr })} - ${format(summary.period.end, 'dd MMM yyyy', { locale: fr })})`;
}; };
// Calculer les compteurs pour chaque tag basés uniquement sur les accomplishments
const accomplishmentTagCounts = useMemo(() => {
const counts: Record<string, number> = {};
availableTags.forEach((tag) => {
counts[tag.name] = summary.keyAccomplishments.filter((a) =>
a.tags.includes(tag.name)
).length;
});
return counts;
}, [availableTags, summary.keyAccomplishments]);
// Calculer les compteurs pour chaque tag basés uniquement sur les challenges
const challengeTagCounts = useMemo(() => {
const counts: Record<string, number> = {};
availableTags.forEach((tag) => {
counts[tag.name] = summary.upcomingChallenges.filter((c) =>
c.tags.includes(tag.name)
).length;
});
return counts;
}, [availableTags, summary.upcomingChallenges]);
// Trier les tags pour les accomplishments par nombre d'utilisation (décroissant)
const sortedAccomplishmentTags = useMemo(() => {
return [...availableTags]
.filter((tag) => (accomplishmentTagCounts[tag.name] || 0) > 0)
.sort((a, b) => {
const countA = accomplishmentTagCounts[a.name] || 0;
const countB = accomplishmentTagCounts[b.name] || 0;
return countB - countA;
});
}, [availableTags, accomplishmentTagCounts]);
// Trier les tags pour les challenges par nombre d'utilisation (décroissant)
const sortedChallengeTags = useMemo(() => {
return [...availableTags]
.filter((tag) => (challengeTagCounts[tag.name] || 0) > 0)
.sort((a, b) => {
const countA = challengeTagCounts[a.name] || 0;
const countB = challengeTagCounts[b.name] || 0;
return countB - countA;
});
}, [availableTags, challengeTagCounts]);
// Filtrer les accomplishments selon les tags sélectionnés
const filteredAccomplishments = useMemo(() => {
if (selectedAccomplishmentTags.length === 0) {
return summary.keyAccomplishments;
}
return summary.keyAccomplishments.filter((accomplishment) =>
selectedAccomplishmentTags.some((tag) =>
accomplishment.tags.includes(tag)
)
);
}, [summary.keyAccomplishments, selectedAccomplishmentTags]);
// Filtrer les challenges selon les tags sélectionnés
const filteredChallenges = useMemo(() => {
if (selectedChallengeTags.length === 0) {
return summary.upcomingChallenges;
}
return summary.upcomingChallenges.filter((challenge) =>
selectedChallengeTags.some((tag) => challenge.tags.includes(tag))
);
}, [summary.upcomingChallenges, selectedChallengeTags]);
const handleAccomplishmentTagToggle = (tagName: string) => {
setSelectedAccomplishmentTags((prev) =>
prev.includes(tagName)
? prev.filter((t) => t !== tagName)
: [...prev, tagName]
);
};
const handleChallengeTagToggle = (tagName: string) => {
setSelectedChallengeTags((prev) =>
prev.includes(tagName)
? prev.filter((t) => t !== tagName)
: [...prev, tagName]
);
};
const handleClearAccomplishmentFilters = () => {
setSelectedAccomplishmentTags([]);
};
const handleClearChallengeFilters = () => {
setSelectedChallengeTags([]);
};
// Configuration des onglets // Configuration des onglets
const tabItems: TabItem[] = [ const tabItems: TabItem[] = [
{ id: 'narrative', label: 'Vue Executive', icon: '📝' }, { id: 'narrative', label: 'Vue Executive', icon: '📝' },
{
id: 'accomplishments',
label: 'Accomplissements',
icon: '✅',
count: summary.keyAccomplishments.length,
},
{
id: 'challenges',
label: 'Enjeux à venir',
icon: '🎯',
count: summary.upcomingChallenges.length,
},
{ id: 'metrics', label: 'Métriques', icon: '📊' }, { id: 'metrics', label: 'Métriques', icon: '📊' },
]; ];
@@ -197,7 +279,7 @@ export default function ManagerWeeklySummary({
</Card> </Card>
</div> </div>
{/* Top accomplissements */} {/* Accomplissements détaillés */}
<Card variant="elevated"> <Card variant="elevated">
<CardHeader <CardHeader
className="pb-4" className="pb-4"
@@ -211,42 +293,106 @@ export default function ManagerWeeklySummary({
className="text-lg font-semibold flex items-center gap-2" className="text-lg font-semibold flex items-center gap-2"
style={{ color: 'var(--success)' }} style={{ color: 'var(--success)' }}
> >
<Emoji emoji="🏆" /> Top accomplissements <Emoji emoji="🏆" /> Accomplissements
</h2> </h2>
<p className="text-sm text-[var(--muted-foreground)] mt-1">
{filteredAccomplishments.length} accomplissement
{filteredAccomplishments.length > 1 ? 's' : ''} affiché
{selectedAccomplishmentTags.length > 0 && (
<span className="ml-2">
(sur {summary.keyAccomplishments.length} total)
</span>
)}
{selectedAccomplishmentTags.length === 0 && (
<>
{' '}
{summary.metrics.totalTasksCompleted} tâches {' '}
{summary.metrics.totalCheckboxesCompleted} todos complétés
</>
)}
</p>
</CardHeader> </CardHeader>
{/* Filtres par tags pour les accomplishments */}
{sortedAccomplishmentTags.length > 0 && (
<CardContent className="pb-4 border-b border-[var(--border)]">
<div className="flex items-center justify-between mb-2">
<h3 className="text-xs font-semibold flex items-center gap-2 text-[var(--muted-foreground)]">
<Emoji emoji="🏷️" /> Filtres
</h3>
{selectedAccomplishmentTags.length > 0 && (
<Button
onClick={handleClearAccomplishmentFilters}
variant="ghost"
size="sm"
className="text-xs h-6"
>
Réinitialiser ({selectedAccomplishmentTags.length})
</Button>
)}
</div>
<div className="flex flex-wrap gap-2">
{sortedAccomplishmentTags.map((tag) => (
<FilterChip
key={tag.id}
onClick={() => handleAccomplishmentTagToggle(tag.name)}
variant={
selectedAccomplishmentTags.includes(tag.name)
? 'selected'
: 'tag'
}
color={tag.color}
count={accomplishmentTagCounts[tag.name]}
>
{tag.name}
</FilterChip>
))}
</div>
</CardContent>
)}
<CardContent> <CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{summary.keyAccomplishments.length === 0 ? ( {filteredAccomplishments.length === 0 ? (
<div className="col-span-3 text-center py-8 text-[var(--muted-foreground)]"> <div className="col-span-3 text-center py-8 text-[var(--muted-foreground)]">
<p> <div className="text-4xl mb-4">
<Emoji emoji="📭" />
</div>
<p className="text-lg mb-2">
Aucun accomplissement significatif trouvé cette semaine. Aucun accomplissement significatif trouvé cette semaine.
</p> </p>
<p className="text-sm mt-2"> <p className="text-sm">
Ajoutez des tâches avec priorité haute/medium ou des Ajoutez des tâches avec priorité haute/medium ou des
meetings. meetings.
</p> </p>
</div> </div>
) : ( ) : (
summary.keyAccomplishments filteredAccomplishments.map((accomplishment) => (
.slice(0, 6) <TaskCard
.map((accomplishment, index) => ( key={accomplishment.id}
<AchievementCard variant="detailed"
key={accomplishment.id} title={accomplishment.title}
achievement={accomplishment} description={accomplishment.description}
availableTags={ tags={accomplishment.tags}
availableTags as (Tag & { usage: number })[] priority={
} accomplishment.impact === 'high'
index={index} ? 'high'
showDescription={true} : accomplishment.impact === 'medium'
maxTags={2} ? 'medium'
/> : 'low'
)) }
status="done"
completedAt={accomplishment.completedAt}
todosCount={accomplishment.todosCount}
availableTags={availableTags as Tag[]}
source="manual"
style={{ opacity: 1 }}
/>
))
)} )}
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
{/* Top challenges */} {/* Enjeux à venir détaillés */}
<Card variant="elevated"> <Card variant="elevated">
<CardHeader <CardHeader
className="pb-4" className="pb-4"
@@ -260,34 +406,106 @@ export default function ManagerWeeklySummary({
className="text-lg font-semibold flex items-center gap-2" className="text-lg font-semibold flex items-center gap-2"
style={{ color: 'var(--destructive)' }} style={{ color: 'var(--destructive)' }}
> >
<Emoji emoji="🎯" /> Top enjeux à venir <Emoji emoji="🎯" /> Enjeux à venir
</h2> </h2>
<p className="text-sm text-[var(--muted-foreground)] mt-1">
{filteredChallenges.length} défi
{filteredChallenges.length > 1 ? 's' : ''} affiché
{selectedChallengeTags.length > 0 && (
<span className="ml-2">
(sur {summary.upcomingChallenges.length} total)
</span>
)}
{selectedChallengeTags.length === 0 && (
<>
{' '}
{' '}
{
summary.upcomingChallenges.filter(
(c) => c.priority === 'high'
).length
}{' '}
priorité haute {' '}
{
summary.upcomingChallenges.filter(
(c) => c.blockers.length > 0
).length
}{' '}
avec blockers
</>
)}
</p>
</CardHeader> </CardHeader>
<CardContent> {/* Filtres par tags pour les challenges */}
{sortedChallengeTags.length > 0 && (
<CardContent className="pb-4 border-b border-[var(--border)]">
<div className="flex items-center justify-between mb-2">
<h3 className="text-xs font-semibold flex items-center gap-2 text-[var(--muted-foreground)]">
<Emoji emoji="🏷️" /> Filtres
</h3>
{selectedChallengeTags.length > 0 && (
<Button
onClick={handleClearChallengeFilters}
variant="ghost"
size="sm"
className="text-xs h-6"
>
Réinitialiser ({selectedChallengeTags.length})
</Button>
)}
</div>
<div className="flex flex-wrap gap-2">
{sortedChallengeTags.map((tag) => (
<FilterChip
key={tag.id}
onClick={() => handleChallengeTagToggle(tag.name)}
variant={
selectedChallengeTags.includes(tag.name)
? 'selected'
: 'tag'
}
color={tag.color}
count={challengeTagCounts[tag.name]}
>
{tag.name}
</FilterChip>
))}
</div>
</CardContent>
)}
<CardContent
className={sortedChallengeTags.length > 0 ? 'pt-4' : ''}
>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{summary.upcomingChallenges.length === 0 ? ( {filteredChallenges.length === 0 ? (
<div className="col-span-3 text-center py-8 text-[var(--muted-foreground)]"> <div className="col-span-3 text-center py-8 text-[var(--muted-foreground)]">
<p>Aucun enjeu prioritaire trouvé.</p> <div className="text-4xl mb-4">
<p className="text-sm mt-2"> <Emoji emoji="🎯" />
</div>
<p className="text-lg mb-2">
Aucun enjeu prioritaire trouvé.
</p>
<p className="text-sm">
Ajoutez des tâches non complétées avec priorité Ajoutez des tâches non complétées avec priorité
haute/medium. haute/medium.
</p> </p>
</div> </div>
) : ( ) : (
summary.upcomingChallenges filteredChallenges.map((challenge) => (
.slice(0, 6) <TaskCard
.map((challenge, index) => ( key={challenge.id}
<ChallengeCard variant="detailed"
key={challenge.id} title={challenge.title}
challenge={challenge} description={challenge.description}
availableTags={ tags={challenge.tags}
availableTags as (Tag & { usage: number })[] priority={challenge.priority}
} status="todo"
index={index} dueDate={challenge.deadline}
showDescription={true} todosCount={challenge.todosCount}
maxTags={2} availableTags={availableTags as Tag[]}
/> source="manual"
)) />
))
)} )}
</div> </div>
</CardContent> </CardContent>
@@ -295,118 +513,6 @@ export default function ManagerWeeklySummary({
</div> </div>
)} )}
{/* Vue détaillée des accomplissements */}
{activeView === 'accomplishments' && (
<Card variant="elevated">
<CardHeader>
<h2 className="text-lg font-semibold">
<Emoji emoji="✅" /> Accomplissements des 7 derniers jours
</h2>
<p className="text-sm text-[var(--muted-foreground)]">
{summary.keyAccomplishments.length} accomplissements significatifs
{summary.metrics.totalTasksCompleted} tâches {' '}
{summary.metrics.totalCheckboxesCompleted} todos complétés
</p>
</CardHeader>
<CardContent className="space-y-6">
{summary.keyAccomplishments.length === 0 ? (
<div
className="p-8 text-center rounded-xl border-2"
style={{
backgroundColor:
'color-mix(in srgb, var(--muted) 15%, transparent)',
borderColor:
'color-mix(in srgb, var(--muted) 40%, var(--border))',
color: 'var(--muted-foreground)',
}}
>
<div className="text-4xl mb-4">
<Emoji emoji="📭" />
</div>
<p className="text-lg mb-2">
Aucun accomplissement significatif trouvé cette semaine.
</p>
<p className="text-sm">
Ajoutez des tâches avec priorité haute/medium ou des meetings.
</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{summary.keyAccomplishments.map((accomplishment, index) => (
<AchievementCard
key={accomplishment.id}
achievement={accomplishment}
availableTags={availableTags as (Tag & { usage: number })[]}
index={index}
showDescription={true}
maxTags={3}
/>
))}
</div>
)}
</CardContent>
</Card>
)}
{/* Vue détaillée des challenges */}
{activeView === 'challenges' && (
<Card variant="elevated">
<CardHeader>
<h2 className="text-lg font-semibold">
<Emoji emoji="🎯" /> Enjeux et défis à venir
</h2>
<p className="text-sm text-[var(--muted-foreground)]">
{summary.upcomingChallenges.length} défis identifiés {' '}
{
summary.upcomingChallenges.filter((c) => c.priority === 'high')
.length
}{' '}
priorité haute {' '}
{
summary.upcomingChallenges.filter((c) => c.blockers.length > 0)
.length
}{' '}
avec blockers
</p>
</CardHeader>
<CardContent className="space-y-6">
{summary.upcomingChallenges.length === 0 ? (
<div
className="p-8 text-center rounded-xl border-2"
style={{
backgroundColor:
'color-mix(in srgb, var(--muted) 15%, transparent)',
borderColor:
'color-mix(in srgb, var(--muted) 40%, var(--border))',
color: 'var(--muted-foreground)',
}}
>
<div className="text-4xl mb-4">
<Emoji emoji="🎯" />
</div>
<p className="text-lg mb-2">Aucun enjeu prioritaire trouvé.</p>
<p className="text-sm">
Ajoutez des tâches non complétées avec priorité haute/medium.
</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{summary.upcomingChallenges.map((challenge, index) => (
<ChallengeCard
key={challenge.id}
challenge={challenge}
availableTags={availableTags as (Tag & { usage: number })[]}
index={index}
showDescription={true}
maxTags={3}
/>
))}
</div>
)}
</CardContent>
</Card>
)}
{/* Vue Métriques */} {/* Vue Métriques */}
{activeView === 'metrics' && <MetricsTab />} {activeView === 'metrics' && <MetricsTab />}
</div> </div>

View File

@@ -6,58 +6,9 @@ import { StatCard } from '@/components/ui/StatCard';
import { ActionCard } from '@/components/ui/ActionCard'; import { ActionCard } from '@/components/ui/ActionCard';
import { TaskCard } from '@/components/ui/TaskCard'; import { TaskCard } from '@/components/ui/TaskCard';
import { MetricCard } from '@/components/ui/MetricCard'; import { MetricCard } from '@/components/ui/MetricCard';
import { AchievementCard } from '@/components/ui/AchievementCard';
import { ChallengeCard } from '@/components/ui/ChallengeCard';
import { SkeletonCard } from '@/components/ui/SkeletonCard'; import { SkeletonCard } from '@/components/ui/SkeletonCard';
import { AchievementData } from '@/components/ui/AchievementCard';
import { ChallengeData } from '@/components/ui/ChallengeCard';
export function CardsSection() { export function CardsSection() {
const sampleAchievements: AchievementData[] = [
{
id: '1',
title: 'Refactoring de la page Daily',
description: 'Migration vers les composants UI génériques',
impact: 'high',
completedAt: new Date(),
updatedAt: new Date(),
tags: ['refactoring', 'ui'],
todosCount: 8,
},
{
id: '2',
title: 'Implémentation du système de thèmes',
description: 'Ajout de 10 nouveaux thèmes avec CSS variables',
impact: 'medium',
completedAt: new Date(Date.now() - 86400000),
updatedAt: new Date(Date.now() - 86400000),
tags: ['themes', 'css'],
todosCount: 3,
},
];
const sampleChallenges: ChallengeData[] = [
{
id: '1',
title: 'Migration vers Next.js 15',
description: 'Mise à jour majeure avec nouvelles fonctionnalités',
priority: 'high',
deadline: new Date(Date.now() + 7 * 86400000),
tags: ['migration', 'nextjs'],
todosCount: 12,
blockers: ['Tests à mettre à jour'],
},
{
id: '2',
title: 'Optimisation des performances',
description: 'Réduction du temps de chargement',
priority: 'medium',
deadline: new Date(Date.now() + 14 * 86400000),
tags: ['performance', 'optimization'],
todosCount: 5,
},
];
return ( return (
<section id="cards" className="space-y-8"> <section id="cards" className="space-y-8">
<h2 className="text-2xl font-mono font-semibold text-[var(--foreground)] border-b border-[var(--border)] pb-3"> <h2 className="text-2xl font-mono font-semibold text-[var(--foreground)] border-b border-[var(--border)] pb-3">
@@ -154,40 +105,6 @@ export function CardsSection() {
</div> </div>
</div> </div>
{/* Achievement Cards */}
<div className="space-y-4">
<h3 className="text-lg font-medium text-[var(--foreground)]">
Achievement Cards
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{sampleAchievements.map((achievement, index) => (
<AchievementCard
key={achievement.id}
achievement={achievement}
availableTags={[]}
index={index}
/>
))}
</div>
</div>
{/* Challenge Cards */}
<div className="space-y-4">
<h3 className="text-lg font-medium text-[var(--foreground)]">
Challenge Cards
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{sampleChallenges.map((challenge, index) => (
<ChallengeCard
key={challenge.id}
challenge={challenge}
availableTags={[]}
index={index}
/>
))}
</div>
</div>
{/* Metric Cards */} {/* Metric Cards */}
<div className="space-y-4"> <div className="space-y-4">
<h3 className="text-lg font-medium text-[var(--foreground)]"> <h3 className="text-lg font-medium text-[var(--foreground)]">

View File

@@ -1,116 +0,0 @@
'use client';
import { format } from 'date-fns';
import { fr } from 'date-fns/locale';
import { TagDisplay } from '@/components/ui/TagDisplay';
import { PriorityBadge } from '@/components/ui/PriorityBadge';
import { Tag } from '@/lib/types';
import { Emoji } from '@/components/ui/Emoji';
export interface AchievementData {
id: string;
title: string;
description?: string;
impact: 'low' | 'medium' | 'high';
completedAt: Date;
updatedAt: Date;
tags?: string[];
todosCount?: number;
}
interface AchievementCardProps {
achievement: AchievementData;
availableTags: (Tag & { usage: number })[];
index: number;
showDescription?: boolean;
maxTags?: number;
className?: string;
}
export function AchievementCard({
achievement,
availableTags,
index,
showDescription = true,
maxTags = 2,
className = '',
}: AchievementCardProps) {
// Détecter si c'est un todo (ID commence par "todo-")
const isTodo = achievement.id.startsWith('todo-');
return (
<div
className={`relative border border-[var(--border)] rounded-lg p-3 transition-all duration-200 group ${className}`}
style={{
backgroundColor: isTodo
? 'color-mix(in srgb, var(--accent) 8%, var(--card))'
: 'color-mix(in srgb, var(--success) 5%, var(--card))',
}}
>
{/* Barre colorée gauche */}
<div className="absolute left-0 top-0 bottom-0 w-1 bg-green-500 rounded-l-lg"></div>
{/* Header compact */}
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<span className="w-5 h-5 rounded-full text-xs font-bold flex items-center justify-center text-[var(--success)] bg-[var(--success)]/15 border border-[var(--success)]/25">
#{index + 1}
</span>
<PriorityBadge priority={achievement.impact} />
</div>
<div className="text-xs text-[var(--muted-foreground)] text-right">
<div>
Terminé: {format(achievement.completedAt, 'dd/MM', { locale: fr })}
</div>
{achievement.updatedAt &&
achievement.updatedAt.getTime() !==
achievement.completedAt.getTime() && (
<div>
Mis à jour:{' '}
{format(achievement.updatedAt, 'dd/MM', { locale: fr })}
</div>
)}
</div>
</div>
{/* Titre */}
<h4 className="font-semibold text-sm text-[var(--foreground)] mb-2 line-clamp-2">
{achievement.title}
</h4>
{/* Tags */}
{achievement.tags && achievement.tags.length > 0 && (
<div className="mb-2">
<TagDisplay
tags={achievement.tags}
availableTags={availableTags as Tag[]}
size="sm"
maxTags={maxTags}
/>
</div>
)}
{/* Description si disponible */}
{showDescription && achievement.description && (
<p className="text-xs text-[var(--muted-foreground)] line-clamp-2 leading-relaxed mb-2">
{achievement.description}
</p>
)}
{/* Count de todos - seulement pour les tâches, pas pour les todos standalone */}
{!isTodo &&
achievement.todosCount !== undefined &&
achievement.todosCount > 0 && (
<div className="flex items-center gap-1 text-xs text-[var(--muted-foreground)]">
<span>
<Emoji emoji="📋" />
</span>
<span>
{achievement.todosCount} todo
{achievement.todosCount > 1 ? 's' : ''}
</span>
</div>
)}
</div>
);
}

View File

@@ -1,101 +0,0 @@
'use client';
import { format } from 'date-fns';
import { fr } from 'date-fns/locale';
import { TagDisplay } from '@/components/ui/TagDisplay';
import { PriorityBadge } from '@/components/ui/PriorityBadge';
import { Tag } from '@/lib/types';
import { Emoji } from '@/components/ui/Emoji';
export interface ChallengeData {
id: string;
title: string;
description?: string;
priority: 'low' | 'medium' | 'high';
deadline?: Date;
tags?: string[];
todosCount?: number;
blockers?: string[];
}
interface ChallengeCardProps {
challenge: ChallengeData;
availableTags: (Tag & { usage: number })[];
index: number;
showDescription?: boolean;
maxTags?: number;
className?: string;
}
export function ChallengeCard({
challenge,
availableTags,
index,
showDescription = true,
maxTags = 2,
className = '',
}: ChallengeCardProps) {
return (
<div
className={`relative border border-[var(--border)] rounded-lg p-3 transition-all duration-200 group ${className}`}
style={{
backgroundColor:
'color-mix(in srgb, var(--destructive) 5%, var(--card))',
}}
>
{/* Barre colorée gauche */}
<div className="absolute left-0 top-0 bottom-0 w-1 bg-orange-500 rounded-l-lg"></div>
{/* Header compact */}
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<span className="w-5 h-5 rounded-full text-xs font-bold flex items-center justify-center text-[var(--accent)] bg-[var(--accent)]/15 border border-[var(--accent)]/25">
#{index + 1}
</span>
<PriorityBadge priority={challenge.priority} />
</div>
{challenge.deadline && (
<span className="text-xs text-[var(--muted-foreground)]">
{format(challenge.deadline, 'dd/MM', { locale: fr })}
</span>
)}
</div>
{/* Titre */}
<h4 className="font-semibold text-sm text-[var(--foreground)] mb-2 line-clamp-2">
{challenge.title}
</h4>
{/* Tags */}
{challenge.tags && challenge.tags.length > 0 && (
<div className="mb-2">
<TagDisplay
tags={challenge.tags}
availableTags={availableTags as Tag[]}
size="sm"
maxTags={maxTags}
/>
</div>
)}
{/* Description si disponible */}
{showDescription && challenge.description && (
<p className="text-xs text-[var(--muted-foreground)] line-clamp-2 leading-relaxed mb-2">
{challenge.description}
</p>
)}
{/* Count de todos */}
{challenge.todosCount !== undefined && challenge.todosCount > 0 && (
<div className="flex items-center gap-1 text-xs text-[var(--muted-foreground)]">
<span>
<Emoji emoji="📋" />
</span>
<span>
{challenge.todosCount} todo{challenge.todosCount > 1 ? 's' : ''}
</span>
</div>
)}
</div>
);
}

View File

@@ -27,8 +27,6 @@ export { DropZone } from './DropZone';
export { Tabs } from './Tabs'; export { Tabs } from './Tabs';
export { PriorityBadge } from './PriorityBadge'; export { PriorityBadge } from './PriorityBadge';
export { StatusBadge } from './StatusBadge'; export { StatusBadge } from './StatusBadge';
export { AchievementCard } from './AchievementCard';
export { ChallengeCard } from './ChallengeCard';
// Composants Daily // Composants Daily
export { CheckboxItem } from './CheckboxItem'; export { CheckboxItem } from './CheckboxItem';

View File

@@ -254,12 +254,16 @@ export class ManagerSummaryService {
} }
// Compter TOUS les todos associés à cette tâche (pas seulement ceux de la période) // Compter TOUS les todos associés à cette tâche (pas seulement ceux de la période)
// Exclure les todos de type "meeting" (réunion)
// car l'accomplissement c'est la tâche complétée, pas seulement les todos de la période // car l'accomplissement c'est la tâche complétée, pas seulement les todos de la période
const allRelatedTodos = await prisma.dailyCheckbox.count({ const allRelatedTodos = await prisma.dailyCheckbox.count({
where: { where: {
task: { task: {
id: task.id, id: task.id,
}, },
type: {
not: 'meeting', // Exclure les réunions
},
}, },
}); });
@@ -278,18 +282,19 @@ export class ManagerSummaryService {
// AJOUTER les todos standalone avec la nouvelle règle de priorité // AJOUTER les todos standalone avec la nouvelle règle de priorité
// Exclure les todos déjà comptés dans les tâches complétées // Exclure les todos déjà comptés dans les tâches complétées
// Exclure les todos de type "meeting" (réunion)
const standaloneTodos = checkboxes.filter( const standaloneTodos = checkboxes.filter(
(checkbox) => !checkbox.task // Todos non liés à une tâche (checkbox) => !checkbox.task && checkbox.type !== 'meeting' // Todos non liés à une tâche et pas de type meeting
); );
standaloneTodos.forEach((todo) => { standaloneTodos.forEach((todo) => {
// Appliquer la nouvelle règle de priorité : // Appliquer la nouvelle règle de priorité :
// Si pas de tâche associée, priorité faible (même pour les meetings) // Si pas de tâche associée, priorité faible
const impact: 'high' | 'medium' | 'low' = 'low'; const impact: 'high' | 'medium' | 'low' = 'low';
accomplishments.push({ accomplishments.push({
id: `todo-${todo.id}`, id: `todo-${todo.id}`,
title: todo.type === 'meeting' ? `📅 ${todo.text}` : todo.text, title: todo.text,
tags: [], // Todos standalone n'ont pas de tags par défaut tags: [], // Todos standalone n'ont pas de tags par défaut
impact, impact,
completedAt: todo.date, completedAt: todo.date,