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';
import { useState } from 'react';
import { useState, useMemo } from 'react';
import { ManagerSummary } from '@/services/analytics/manager-summary';
import { Card, CardHeader, CardContent } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button';
import { Tabs, TabItem } from '@/components/ui/Tabs';
import { AchievementCard } from '@/components/ui/AchievementCard';
import { ChallengeCard } from '@/components/ui/ChallengeCard';
import { TaskCard } from '@/components/ui/TaskCard';
import { FilterChip } from '@/components/ui/FilterChip';
import { useTasksContext } from '@/contexts/TasksContext';
import { MetricsTab } from './MetricsTab';
import { format } from 'date-fns';
@@ -22,15 +22,19 @@ export default function ManagerWeeklySummary({
initialSummary,
}: ManagerWeeklySummaryProps) {
const [summary] = useState<ManagerSummary>(initialSummary);
const [activeView, setActiveView] = useState<
'narrative' | 'accomplishments' | 'challenges' | 'metrics'
>('narrative');
const [activeView, setActiveView] = useState<'narrative' | 'metrics'>(
'narrative'
);
const [selectedAccomplishmentTags, setSelectedAccomplishmentTags] = useState<
string[]
>([]);
const [selectedChallengeTags, setSelectedChallengeTags] = useState<string[]>(
[]
);
const { tags: availableTags } = useTasksContext();
const handleTabChange = (tabId: string) => {
setActiveView(
tabId as 'narrative' | 'accomplishments' | 'challenges' | 'metrics'
);
setActiveView(tabId as 'narrative' | 'metrics');
};
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 })})`;
};
// 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
const tabItems: TabItem[] = [
{ 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: '📊' },
];
@@ -197,7 +279,7 @@ export default function ManagerWeeklySummary({
</Card>
</div>
{/* Top accomplissements */}
{/* Accomplissements détaillés */}
<Card variant="elevated">
<CardHeader
className="pb-4"
@@ -211,34 +293,98 @@ export default function ManagerWeeklySummary({
className="text-lg font-semibold flex items-center gap-2"
style={{ color: 'var(--success)' }}
>
<Emoji emoji="🏆" /> Top accomplissements
<Emoji emoji="🏆" /> Accomplissements
</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>
{/* 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>
<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)]">
<p>
<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 mt-2">
<p className="text-sm">
Ajoutez des tâches avec priorité haute/medium ou des
meetings.
</p>
</div>
) : (
summary.keyAccomplishments
.slice(0, 6)
.map((accomplishment, index) => (
<AchievementCard
filteredAccomplishments.map((accomplishment) => (
<TaskCard
key={accomplishment.id}
achievement={accomplishment}
availableTags={
availableTags as (Tag & { usage: number })[]
variant="detailed"
title={accomplishment.title}
description={accomplishment.description}
tags={accomplishment.tags}
priority={
accomplishment.impact === 'high'
? 'high'
: accomplishment.impact === 'medium'
? 'medium'
: 'low'
}
index={index}
showDescription={true}
maxTags={2}
status="done"
completedAt={accomplishment.completedAt}
todosCount={accomplishment.todosCount}
availableTags={availableTags as Tag[]}
source="manual"
style={{ opacity: 1 }}
/>
))
)}
@@ -246,7 +392,7 @@ export default function ManagerWeeklySummary({
</CardContent>
</Card>
{/* Top challenges */}
{/* Enjeux à venir détaillés */}
<Card variant="elevated">
<CardHeader
className="pb-4"
@@ -260,32 +406,104 @@ export default function ManagerWeeklySummary({
className="text-lg font-semibold flex items-center gap-2"
style={{ color: 'var(--destructive)' }}
>
<Emoji emoji="🎯" /> Top enjeux à venir
<Emoji emoji="🎯" /> Enjeux à venir
</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>
<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">
{summary.upcomingChallenges.length === 0 ? (
{filteredChallenges.length === 0 ? (
<div className="col-span-3 text-center py-8 text-[var(--muted-foreground)]">
<p>Aucun enjeu prioritaire trouvé.</p>
<p className="text-sm mt-2">
<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>
) : (
summary.upcomingChallenges
.slice(0, 6)
.map((challenge, index) => (
<ChallengeCard
filteredChallenges.map((challenge) => (
<TaskCard
key={challenge.id}
challenge={challenge}
availableTags={
availableTags as (Tag & { usage: number })[]
}
index={index}
showDescription={true}
maxTags={2}
variant="detailed"
title={challenge.title}
description={challenge.description}
tags={challenge.tags}
priority={challenge.priority}
status="todo"
dueDate={challenge.deadline}
todosCount={challenge.todosCount}
availableTags={availableTags as Tag[]}
source="manual"
/>
))
)}
@@ -295,118 +513,6 @@ export default function ManagerWeeklySummary({
</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 */}
{activeView === 'metrics' && <MetricsTab />}
</div>

View File

@@ -6,58 +6,9 @@ import { StatCard } from '@/components/ui/StatCard';
import { ActionCard } from '@/components/ui/ActionCard';
import { TaskCard } from '@/components/ui/TaskCard';
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 { AchievementData } from '@/components/ui/AchievementCard';
import { ChallengeData } from '@/components/ui/ChallengeCard';
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 (
<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">
@@ -154,40 +105,6 @@ export function CardsSection() {
</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 */}
<div className="space-y-4">
<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 { PriorityBadge } from './PriorityBadge';
export { StatusBadge } from './StatusBadge';
export { AchievementCard } from './AchievementCard';
export { ChallengeCard } from './ChallengeCard';
// Composants Daily
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)
// 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
const allRelatedTodos = await prisma.dailyCheckbox.count({
where: {
task: {
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é
// 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(
(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) => {
// 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';
accomplishments.push({
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
impact,
completedAt: todo.date,