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