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:
@@ -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>
|
||||
|
||||
@@ -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)]">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user