From 4fc41a5b2c1af04ca77464680f6b766c394bba68 Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Wed, 26 Nov 2025 08:40:42 +0100 Subject: [PATCH] refactor(ManagerWeeklySummary): replace AchievementCard and ChallengeCard with TaskCard, implement tag filtering for accomplishments and challenges, and enhance UI for better data presentation --- .../dashboard/ManagerWeeklySummary.tsx | 450 +++++++++++------- .../ui-showcase/sections/CardsSection.tsx | 83 ---- src/components/ui/AchievementCard.tsx | 116 ----- src/components/ui/ChallengeCard.tsx | 101 ---- src/components/ui/index.ts | 2 - src/services/analytics/manager-summary.ts | 11 +- 6 files changed, 286 insertions(+), 477 deletions(-) delete mode 100644 src/components/ui/AchievementCard.tsx delete mode 100644 src/components/ui/ChallengeCard.tsx diff --git a/src/components/dashboard/ManagerWeeklySummary.tsx b/src/components/dashboard/ManagerWeeklySummary.tsx index 5e3cc40..8a4bb38 100644 --- a/src/components/dashboard/ManagerWeeklySummary.tsx +++ b/src/components/dashboard/ManagerWeeklySummary.tsx @@ -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(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( + [] + ); 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 = {}; + 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 = {}; + 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({ - {/* Top accomplissements */} + {/* Accomplissements détaillés */} - Top accomplissements + Accomplissements +

+ {filteredAccomplishments.length} accomplissement + {filteredAccomplishments.length > 1 ? 's' : ''} affiché + {selectedAccomplishmentTags.length > 0 && ( + + (sur {summary.keyAccomplishments.length} total) + + )} + {selectedAccomplishmentTags.length === 0 && ( + <> + {' '} + • {summary.metrics.totalTasksCompleted} tâches •{' '} + {summary.metrics.totalCheckboxesCompleted} todos complétés + + )} +

+ {/* Filtres par tags pour les accomplishments */} + {sortedAccomplishmentTags.length > 0 && ( + +
+

+ Filtres +

+ {selectedAccomplishmentTags.length > 0 && ( + + )} +
+
+ {sortedAccomplishmentTags.map((tag) => ( + handleAccomplishmentTagToggle(tag.name)} + variant={ + selectedAccomplishmentTags.includes(tag.name) + ? 'selected' + : 'tag' + } + color={tag.color} + count={accomplishmentTagCounts[tag.name]} + > + {tag.name} + + ))} +
+
+ )}
- {summary.keyAccomplishments.length === 0 ? ( + {filteredAccomplishments.length === 0 ? (
-

+

+ +
+

Aucun accomplissement significatif trouvé cette semaine.

-

+

Ajoutez des tâches avec priorité haute/medium ou des meetings.

) : ( - summary.keyAccomplishments - .slice(0, 6) - .map((accomplishment, index) => ( - - )) + filteredAccomplishments.map((accomplishment) => ( + + )) )}
- {/* Top challenges */} + {/* Enjeux à venir détaillés */} - Top enjeux à venir + Enjeux à venir +

+ {filteredChallenges.length} défi + {filteredChallenges.length > 1 ? 's' : ''} affiché + {selectedChallengeTags.length > 0 && ( + + (sur {summary.upcomingChallenges.length} total) + + )} + {selectedChallengeTags.length === 0 && ( + <> + {' '} + •{' '} + { + summary.upcomingChallenges.filter( + (c) => c.priority === 'high' + ).length + }{' '} + priorité haute •{' '} + { + summary.upcomingChallenges.filter( + (c) => c.blockers.length > 0 + ).length + }{' '} + avec blockers + + )} +

- + {/* Filtres par tags pour les challenges */} + {sortedChallengeTags.length > 0 && ( + +
+

+ Filtres +

+ {selectedChallengeTags.length > 0 && ( + + )} +
+
+ {sortedChallengeTags.map((tag) => ( + handleChallengeTagToggle(tag.name)} + variant={ + selectedChallengeTags.includes(tag.name) + ? 'selected' + : 'tag' + } + color={tag.color} + count={challengeTagCounts[tag.name]} + > + {tag.name} + + ))} +
+
+ )} + 0 ? 'pt-4' : ''} + >
- {summary.upcomingChallenges.length === 0 ? ( + {filteredChallenges.length === 0 ? (
-

Aucun enjeu prioritaire trouvé.

-

+

+ +
+

+ Aucun enjeu prioritaire trouvé. +

+

Ajoutez des tâches non complétées avec priorité haute/medium.

) : ( - summary.upcomingChallenges - .slice(0, 6) - .map((challenge, index) => ( - - )) + filteredChallenges.map((challenge) => ( + + )) )}
@@ -295,118 +513,6 @@ export default function ManagerWeeklySummary({ )} - {/* Vue détaillée des accomplissements */} - {activeView === 'accomplishments' && ( - - -

- Accomplissements des 7 derniers jours -

-

- {summary.keyAccomplishments.length} accomplissements significatifs - • {summary.metrics.totalTasksCompleted} tâches •{' '} - {summary.metrics.totalCheckboxesCompleted} todos complétés -

-
- - {summary.keyAccomplishments.length === 0 ? ( -
-
- -
-

- Aucun accomplissement significatif trouvé cette semaine. -

-

- Ajoutez des tâches avec priorité haute/medium ou des meetings. -

-
- ) : ( -
- {summary.keyAccomplishments.map((accomplishment, index) => ( - - ))} -
- )} -
-
- )} - - {/* Vue détaillée des challenges */} - {activeView === 'challenges' && ( - - -

- Enjeux et défis à venir -

-

- {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 -

-
- - {summary.upcomingChallenges.length === 0 ? ( -
-
- -
-

Aucun enjeu prioritaire trouvé.

-

- Ajoutez des tâches non complétées avec priorité haute/medium. -

-
- ) : ( -
- {summary.upcomingChallenges.map((challenge, index) => ( - - ))} -
- )} -
-
- )} - {/* Vue Métriques */} {activeView === 'metrics' && } diff --git a/src/components/ui-showcase/sections/CardsSection.tsx b/src/components/ui-showcase/sections/CardsSection.tsx index a0a4b4a..0521b84 100644 --- a/src/components/ui-showcase/sections/CardsSection.tsx +++ b/src/components/ui-showcase/sections/CardsSection.tsx @@ -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 (

@@ -154,40 +105,6 @@ export function CardsSection() { - {/* Achievement Cards */} -
-

- Achievement Cards -

-
- {sampleAchievements.map((achievement, index) => ( - - ))} -
-
- - {/* Challenge Cards */} -
-

- Challenge Cards -

-
- {sampleChallenges.map((challenge, index) => ( - - ))} -
-
- {/* Metric Cards */}

diff --git a/src/components/ui/AchievementCard.tsx b/src/components/ui/AchievementCard.tsx deleted file mode 100644 index 1589b1a..0000000 --- a/src/components/ui/AchievementCard.tsx +++ /dev/null @@ -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 ( -
- {/* Barre colorée gauche */} -
- - {/* Header compact */} -
-
- - #{index + 1} - - -
-
-
- Terminé: {format(achievement.completedAt, 'dd/MM', { locale: fr })} -
- {achievement.updatedAt && - achievement.updatedAt.getTime() !== - achievement.completedAt.getTime() && ( -
- Mis à jour:{' '} - {format(achievement.updatedAt, 'dd/MM', { locale: fr })} -
- )} -
-
- - {/* Titre */} -

- {achievement.title} -

- - {/* Tags */} - {achievement.tags && achievement.tags.length > 0 && ( -
- -
- )} - - {/* Description si disponible */} - {showDescription && achievement.description && ( -

- {achievement.description} -

- )} - - {/* Count de todos - seulement pour les tâches, pas pour les todos standalone */} - {!isTodo && - achievement.todosCount !== undefined && - achievement.todosCount > 0 && ( -
- - - - - {achievement.todosCount} todo - {achievement.todosCount > 1 ? 's' : ''} - -
- )} -
- ); -} diff --git a/src/components/ui/ChallengeCard.tsx b/src/components/ui/ChallengeCard.tsx deleted file mode 100644 index 773f439..0000000 --- a/src/components/ui/ChallengeCard.tsx +++ /dev/null @@ -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 ( -
- {/* Barre colorée gauche */} -
- - {/* Header compact */} -
-
- - #{index + 1} - - -
- {challenge.deadline && ( - - {format(challenge.deadline, 'dd/MM', { locale: fr })} - - )} -
- - {/* Titre */} -

- {challenge.title} -

- - {/* Tags */} - {challenge.tags && challenge.tags.length > 0 && ( -
- -
- )} - - {/* Description si disponible */} - {showDescription && challenge.description && ( -

- {challenge.description} -

- )} - - {/* Count de todos */} - {challenge.todosCount !== undefined && challenge.todosCount > 0 && ( -
- - - - - {challenge.todosCount} todo{challenge.todosCount > 1 ? 's' : ''} - -
- )} -
- ); -} diff --git a/src/components/ui/index.ts b/src/components/ui/index.ts index 375d156..dd93349 100644 --- a/src/components/ui/index.ts +++ b/src/components/ui/index.ts @@ -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'; diff --git a/src/services/analytics/manager-summary.ts b/src/services/analytics/manager-summary.ts index 27088c7..12aab8a 100644 --- a/src/services/analytics/manager-summary.ts +++ b/src/services/analytics/manager-summary.ts @@ -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,