feat: add 'Manager' link to Header component
- Introduced a new navigation link for the 'Manager' page in the Header component, improving user access to management features.
This commit is contained in:
481
components/dashboard/ManagerWeeklySummary.tsx
Normal file
481
components/dashboard/ManagerWeeklySummary.tsx
Normal file
@@ -0,0 +1,481 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { ManagerSummary } from '@/services/manager-summary';
|
||||||
|
import { Card, CardHeader, CardContent } from '@/components/ui/Card';
|
||||||
|
import { Button } from '@/components/ui/Button';
|
||||||
|
import { TagDisplay } from '@/components/ui/TagDisplay';
|
||||||
|
import { getPriorityConfig } from '@/lib/status-config';
|
||||||
|
import { useTasksContext } from '@/contexts/TasksContext';
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
import { fr } from 'date-fns/locale';
|
||||||
|
|
||||||
|
interface ManagerWeeklySummaryProps {
|
||||||
|
initialSummary: ManagerSummary;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ManagerWeeklySummary({ initialSummary }: ManagerWeeklySummaryProps) {
|
||||||
|
const [summary] = useState<ManagerSummary>(initialSummary);
|
||||||
|
const [activeView, setActiveView] = useState<'narrative' | 'accomplishments' | 'challenges'>('narrative');
|
||||||
|
const { tags: availableTags } = useTasksContext();
|
||||||
|
|
||||||
|
const handleRefresh = () => {
|
||||||
|
// SSR - refresh via page reload
|
||||||
|
window.location.reload();
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const formatPeriod = () => {
|
||||||
|
return `Semaine du ${format(summary.period.start, 'dd MMM', { locale: fr })} au ${format(summary.period.end, 'dd MMM yyyy', { locale: fr })}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPriorityBadgeStyle = (priority: 'low' | 'medium' | 'high') => {
|
||||||
|
const config = getPriorityConfig(priority);
|
||||||
|
const baseClasses = 'text-xs px-2 py-0.5 rounded font-medium';
|
||||||
|
|
||||||
|
switch (config.color) {
|
||||||
|
case 'blue':
|
||||||
|
return `${baseClasses} bg-blue-100 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400`;
|
||||||
|
case 'yellow':
|
||||||
|
return `${baseClasses} bg-yellow-100 dark:bg-yellow-900/20 text-yellow-600 dark:text-yellow-400`;
|
||||||
|
case 'purple':
|
||||||
|
return `${baseClasses} bg-purple-100 dark:bg-purple-900/20 text-purple-600 dark:text-purple-400`;
|
||||||
|
case 'red':
|
||||||
|
return `${baseClasses} bg-red-100 dark:bg-red-900/20 text-red-600 dark:text-red-400`;
|
||||||
|
default:
|
||||||
|
return `${baseClasses} bg-gray-100 dark:bg-gray-900/20 text-gray-600 dark:text-gray-400`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Header avec navigation */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold text-[var(--foreground)]">👔 Résumé Manager</h1>
|
||||||
|
<p className="text-[var(--muted-foreground)]">{formatPeriod()}</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={handleRefresh}
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
🔄 Actualiser
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Navigation des vues */}
|
||||||
|
<div className="border-b border-[var(--border)]">
|
||||||
|
<nav className="flex space-x-8">
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveView('narrative')}
|
||||||
|
className={`py-3 px-1 border-b-2 font-medium text-sm transition-colors ${
|
||||||
|
activeView === 'narrative'
|
||||||
|
? 'border-[var(--primary)] text-[var(--primary)]'
|
||||||
|
: 'border-transparent text-[var(--muted-foreground)] hover:text-[var(--foreground)]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
📝 Vue Executive
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveView('accomplishments')}
|
||||||
|
className={`py-3 px-1 border-b-2 font-medium text-sm transition-colors ${
|
||||||
|
activeView === 'accomplishments'
|
||||||
|
? 'border-[var(--primary)] text-[var(--primary)]'
|
||||||
|
: 'border-transparent text-[var(--muted-foreground)] hover:text-[var(--foreground)]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
✅ Accomplissements ({summary.keyAccomplishments.length})
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveView('challenges')}
|
||||||
|
className={`py-3 px-1 border-b-2 font-medium text-sm transition-colors ${
|
||||||
|
activeView === 'challenges'
|
||||||
|
? 'border-[var(--primary)] text-[var(--primary)]'
|
||||||
|
: 'border-transparent text-[var(--muted-foreground)] hover:text-[var(--foreground)]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
🎯 Enjeux à venir ({summary.upcomingChallenges.length})
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Vue Executive / Narrative */}
|
||||||
|
{activeView === 'narrative' && (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Résumé narratif */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<h2 className="text-lg font-semibold flex items-center gap-2">
|
||||||
|
📊 Résumé de la semaine
|
||||||
|
</h2>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="bg-blue-50 p-4 rounded-lg border-l-4 border-blue-400">
|
||||||
|
<h3 className="font-medium text-blue-900 mb-2">🎯 Points clés accomplis</h3>
|
||||||
|
<p className="text-blue-800">{summary.narrative.weekHighlight}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-yellow-50 p-4 rounded-lg border-l-4 border-yellow-400">
|
||||||
|
<h3 className="font-medium text-yellow-900 mb-2">⚡ Défis traités</h3>
|
||||||
|
<p className="text-yellow-800">{summary.narrative.mainChallenges}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-green-50 p-4 rounded-lg border-l-4 border-green-400">
|
||||||
|
<h3 className="font-medium text-green-900 mb-2">🔮 Focus semaine prochaine</h3>
|
||||||
|
<p className="text-green-800">{summary.narrative.nextWeekFocus}</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Métriques rapides */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<h2 className="text-lg font-semibold">📈 Métriques en bref</h2>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
|
<div className="text-center p-4 bg-blue-50 rounded-lg">
|
||||||
|
<div className="text-2xl font-bold text-blue-600">
|
||||||
|
{summary.metrics.totalTasksCompleted}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-blue-600">Tâches complétées</div>
|
||||||
|
<div className="text-xs text-blue-500">
|
||||||
|
dont {summary.metrics.highPriorityTasksCompleted} priorité haute
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center p-4 bg-green-50 rounded-lg">
|
||||||
|
<div className="text-2xl font-bold text-green-600">
|
||||||
|
{summary.metrics.totalCheckboxesCompleted}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-green-600">Todos complétés</div>
|
||||||
|
<div className="text-xs text-green-500">
|
||||||
|
dont {summary.metrics.meetingCheckboxesCompleted} meetings
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center p-4 bg-purple-50 rounded-lg">
|
||||||
|
<div className="text-2xl font-bold text-purple-600">
|
||||||
|
{summary.keyAccomplishments.filter(a => a.impact === 'high').length}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-purple-600">Items à fort impact</div>
|
||||||
|
<div className="text-xs text-purple-500">
|
||||||
|
/ {summary.keyAccomplishments.length} accomplissements
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center p-4 bg-orange-50 rounded-lg">
|
||||||
|
<div className="text-2xl font-bold text-orange-600">
|
||||||
|
{summary.upcomingChallenges.filter(c => c.priority === 'high').length}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-orange-600">Priorités critiques</div>
|
||||||
|
<div className="text-xs text-orange-500">
|
||||||
|
/ {summary.upcomingChallenges.length} enjeux
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Top accomplissements */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<h2 className="text-lg font-semibold">🏆 Top accomplissements</h2>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||||
|
{summary.keyAccomplishments.length === 0 ? (
|
||||||
|
<div className="col-span-3 text-center py-8 text-[var(--muted-foreground)]">
|
||||||
|
<p>Aucun accomplissement significatif trouvé cette semaine.</p>
|
||||||
|
<p className="text-sm mt-2">Ajoutez des tâches avec priorité haute/medium ou des meetings.</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
summary.keyAccomplishments.slice(0, 6).map((accomplishment, index) => (
|
||||||
|
<div
|
||||||
|
key={accomplishment.id}
|
||||||
|
className="relative bg-[var(--card)] border border-[var(--border)] rounded-lg p-3 hover:bg-green-50/50 dark:hover:bg-green-950/20 transition-all duration-200 group"
|
||||||
|
>
|
||||||
|
{/* 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 bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300 rounded-full text-xs font-bold flex items-center justify-center">
|
||||||
|
#{index + 1}
|
||||||
|
</span>
|
||||||
|
<span className={getPriorityBadgeStyle(accomplishment.impact)}>
|
||||||
|
{getPriorityConfig(accomplishment.impact).label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-[var(--muted-foreground)]">
|
||||||
|
{format(accomplishment.completedAt, 'dd/MM', { locale: fr })}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Titre */}
|
||||||
|
<h4 className="font-semibold text-sm text-[var(--foreground)] mb-2 line-clamp-2">
|
||||||
|
{accomplishment.title}
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
{/* Tags */}
|
||||||
|
{accomplishment.tags && accomplishment.tags.length > 0 && (
|
||||||
|
<div className="mb-2">
|
||||||
|
<TagDisplay
|
||||||
|
tags={accomplishment.tags}
|
||||||
|
availableTags={availableTags}
|
||||||
|
size="sm"
|
||||||
|
maxTags={2}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Description si disponible */}
|
||||||
|
{accomplishment.description && (
|
||||||
|
<p className="text-xs text-[var(--muted-foreground)] line-clamp-2 leading-relaxed mb-2">
|
||||||
|
{accomplishment.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Count de todos */}
|
||||||
|
{accomplishment.todosCount > 0 && (
|
||||||
|
<div className="flex items-center gap-1 text-xs text-[var(--muted-foreground)]">
|
||||||
|
<span>📋</span>
|
||||||
|
<span>{accomplishment.todosCount} todo{accomplishment.todosCount > 1 ? 's' : ''}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Top challenges */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<h2 className="text-lg font-semibold">🎯 Top enjeux à venir</h2>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||||
|
{summary.upcomingChallenges.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">Ajoutez des tâches non complétées avec priorité haute/medium.</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
summary.upcomingChallenges.slice(0, 6).map((challenge, index) => (
|
||||||
|
<div
|
||||||
|
key={challenge.id}
|
||||||
|
className="relative bg-[var(--card)] border border-[var(--border)] rounded-lg p-3 hover:bg-orange-50/50 dark:hover:bg-orange-950/20 transition-all duration-200 group"
|
||||||
|
>
|
||||||
|
{/* 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 bg-orange-100 dark:bg-orange-900/30 text-orange-700 dark:text-orange-300 rounded-full text-xs font-bold flex items-center justify-center">
|
||||||
|
#{index + 1}
|
||||||
|
</span>
|
||||||
|
<span className={getPriorityBadgeStyle(challenge.priority)}>
|
||||||
|
{getPriorityConfig(challenge.priority).label}
|
||||||
|
</span>
|
||||||
|
</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}
|
||||||
|
size="sm"
|
||||||
|
maxTags={2}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Description si disponible */}
|
||||||
|
{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 > 0 && (
|
||||||
|
<div className="flex items-center gap-1 text-xs text-[var(--muted-foreground)]">
|
||||||
|
<span>📋</span>
|
||||||
|
<span>{challenge.todosCount} todo{challenge.todosCount > 1 ? 's' : ''}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Vue détaillée des accomplissements */}
|
||||||
|
{activeView === 'accomplishments' && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<h2 className="text-lg font-semibold">✅ Accomplissements de la semaine</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>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
{summary.keyAccomplishments.map((accomplishment, index) => (
|
||||||
|
<div
|
||||||
|
key={accomplishment.id}
|
||||||
|
className="relative bg-[var(--card)] border border-[var(--border)] rounded-lg p-3 hover:bg-green-50/50 dark:hover:bg-green-950/20 transition-all duration-200 group"
|
||||||
|
>
|
||||||
|
{/* 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 bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300 rounded-full text-xs font-bold flex items-center justify-center">
|
||||||
|
#{index + 1}
|
||||||
|
</span>
|
||||||
|
<span className={getPriorityBadgeStyle(accomplishment.impact)}>
|
||||||
|
{getPriorityConfig(accomplishment.impact).label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-[var(--muted-foreground)]">
|
||||||
|
{format(accomplishment.completedAt, 'dd/MM', { locale: fr })}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Titre */}
|
||||||
|
<h4 className="font-semibold text-sm text-[var(--foreground)] mb-2 line-clamp-2">
|
||||||
|
{accomplishment.title}
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
{/* Tags */}
|
||||||
|
{accomplishment.tags && accomplishment.tags.length > 0 && (
|
||||||
|
<div className="mb-2">
|
||||||
|
<TagDisplay
|
||||||
|
tags={accomplishment.tags}
|
||||||
|
availableTags={availableTags}
|
||||||
|
size="sm"
|
||||||
|
maxTags={3}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Description si disponible */}
|
||||||
|
{accomplishment.description && (
|
||||||
|
<p className="text-xs text-[var(--muted-foreground)] line-clamp-3 leading-relaxed mb-2">
|
||||||
|
{accomplishment.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Count de todos */}
|
||||||
|
{accomplishment.todosCount > 0 && (
|
||||||
|
<div className="flex items-center gap-1 text-xs text-[var(--muted-foreground)]">
|
||||||
|
<span>📋</span>
|
||||||
|
<span>{accomplishment.todosCount} todo{accomplishment.todosCount > 1 ? 's' : ''}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Vue détaillée des challenges */}
|
||||||
|
{activeView === 'challenges' && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<h2 className="text-lg font-semibold">🎯 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>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
{summary.upcomingChallenges.map((challenge, index) => (
|
||||||
|
<div
|
||||||
|
key={challenge.id}
|
||||||
|
className="relative bg-[var(--card)] border border-[var(--border)] rounded-lg p-3 hover:bg-orange-50/50 dark:hover:bg-orange-950/20 transition-all duration-200 group"
|
||||||
|
>
|
||||||
|
{/* 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 bg-orange-100 dark:bg-orange-900/30 text-orange-700 dark:text-orange-300 rounded-full text-xs font-bold flex items-center justify-center">
|
||||||
|
#{index + 1}
|
||||||
|
</span>
|
||||||
|
<span className={getPriorityBadgeStyle(challenge.priority)}>
|
||||||
|
{getPriorityConfig(challenge.priority).label}
|
||||||
|
</span>
|
||||||
|
</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}
|
||||||
|
size="sm"
|
||||||
|
maxTags={3}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Description si disponible */}
|
||||||
|
{challenge.description && (
|
||||||
|
<p className="text-xs text-[var(--muted-foreground)] line-clamp-3 leading-relaxed mb-2">
|
||||||
|
{challenge.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Count de todos */}
|
||||||
|
{challenge.todosCount > 0 && (
|
||||||
|
<div className="flex items-center gap-1 text-xs text-[var(--muted-foreground)]">
|
||||||
|
<span>📋</span>
|
||||||
|
<span>{challenge.todosCount} todo{challenge.todosCount > 1 ? 's' : ''}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -54,6 +54,7 @@ export function Header({ title = "TowerControl", subtitle = "Task Management", s
|
|||||||
{ href: '/kanban', label: 'Kanban' },
|
{ href: '/kanban', label: 'Kanban' },
|
||||||
{ href: '/daily', label: 'Daily' },
|
{ href: '/daily', label: 'Daily' },
|
||||||
{ href: '/weekly-summary', label: 'Hebdo' },
|
{ href: '/weekly-summary', label: 'Hebdo' },
|
||||||
|
{ href: '/weekly-manager', label: 'Manager' },
|
||||||
...(isJiraConfigured ? [{ href: '/jira-dashboard', label: `Jira${jiraConfig?.projectKey ? ` (${jiraConfig.projectKey})` : ''}` }] : []),
|
...(isJiraConfigured ? [{ href: '/jira-dashboard', label: `Jira${jiraConfig?.projectKey ? ` (${jiraConfig.projectKey})` : ''}` }] : []),
|
||||||
{ href: '/settings', label: 'Settings' }
|
{ href: '/settings', label: 'Settings' }
|
||||||
];
|
];
|
||||||
|
|||||||
563
services/manager-summary.ts
Normal file
563
services/manager-summary.ts
Normal file
@@ -0,0 +1,563 @@
|
|||||||
|
import { prisma } from './database';
|
||||||
|
import { startOfWeek, endOfWeek } from 'date-fns';
|
||||||
|
|
||||||
|
type TaskType = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description?: string | null;
|
||||||
|
priority: string; // high, medium, low
|
||||||
|
completedAt?: Date | null;
|
||||||
|
createdAt: Date;
|
||||||
|
taskTags?: {
|
||||||
|
tag: {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type CheckboxType = {
|
||||||
|
id: string;
|
||||||
|
text: string;
|
||||||
|
isChecked: boolean;
|
||||||
|
type: string; // task, meeting
|
||||||
|
date: Date;
|
||||||
|
createdAt: Date;
|
||||||
|
task?: {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
priority: string;
|
||||||
|
taskTags?: {
|
||||||
|
tag: {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
}[];
|
||||||
|
} | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface KeyAccomplishment {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
tags: string[];
|
||||||
|
impact: 'high' | 'medium' | 'low';
|
||||||
|
completedAt: Date;
|
||||||
|
relatedItems: string[]; // IDs des tâches/checkboxes liées
|
||||||
|
todosCount: number; // Nombre de todos associés
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpcomingChallenge {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
tags: string[];
|
||||||
|
priority: 'high' | 'medium' | 'low';
|
||||||
|
estimatedEffort: 'days' | 'weeks' | 'hours';
|
||||||
|
blockers: string[];
|
||||||
|
deadline?: Date;
|
||||||
|
relatedItems: string[]; // IDs des tâches/checkboxes liées
|
||||||
|
todosCount: number; // Nombre de todos associés
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ManagerSummary {
|
||||||
|
period: {
|
||||||
|
start: Date;
|
||||||
|
end: Date;
|
||||||
|
};
|
||||||
|
keyAccomplishments: KeyAccomplishment[];
|
||||||
|
upcomingChallenges: UpcomingChallenge[];
|
||||||
|
metrics: {
|
||||||
|
totalTasksCompleted: number;
|
||||||
|
totalCheckboxesCompleted: number;
|
||||||
|
highPriorityTasksCompleted: number;
|
||||||
|
meetingCheckboxesCompleted: number;
|
||||||
|
completionRate: number;
|
||||||
|
focusAreas: { [category: string]: number };
|
||||||
|
};
|
||||||
|
narrative: {
|
||||||
|
weekHighlight: string;
|
||||||
|
mainChallenges: string;
|
||||||
|
nextWeekFocus: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ManagerSummaryService {
|
||||||
|
/**
|
||||||
|
* Génère un résumé orienté manager pour la semaine
|
||||||
|
*/
|
||||||
|
static async getManagerSummary(date: Date = new Date()): Promise<ManagerSummary> {
|
||||||
|
const weekStart = startOfWeek(date, { weekStartsOn: 1 }); // Lundi
|
||||||
|
const weekEnd = endOfWeek(date, { weekStartsOn: 1 }); // Dimanche
|
||||||
|
|
||||||
|
// Récupérer les données de base
|
||||||
|
const [tasks, checkboxes] = await Promise.all([
|
||||||
|
this.getCompletedTasks(weekStart, weekEnd),
|
||||||
|
this.getCompletedCheckboxes(weekStart, weekEnd)
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Analyser et extraire les accomplissements clés
|
||||||
|
const keyAccomplishments = this.extractKeyAccomplishments(tasks, checkboxes);
|
||||||
|
|
||||||
|
// Identifier les défis à venir
|
||||||
|
const upcomingChallenges = await this.identifyUpcomingChallenges();
|
||||||
|
|
||||||
|
// Calculer les métriques
|
||||||
|
const metrics = this.calculateMetrics(tasks, checkboxes);
|
||||||
|
|
||||||
|
// Générer le narratif
|
||||||
|
const narrative = this.generateNarrative(keyAccomplishments, upcomingChallenges);
|
||||||
|
|
||||||
|
return {
|
||||||
|
period: { start: weekStart, end: weekEnd },
|
||||||
|
keyAccomplishments,
|
||||||
|
upcomingChallenges,
|
||||||
|
metrics,
|
||||||
|
narrative
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les tâches complétées de la semaine
|
||||||
|
*/
|
||||||
|
private static async getCompletedTasks(startDate: Date, endDate: Date) {
|
||||||
|
const tasks = await prisma.task.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
// Tâches avec completedAt dans la période (priorité)
|
||||||
|
{
|
||||||
|
completedAt: {
|
||||||
|
gte: startDate,
|
||||||
|
lte: endDate
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Tâches avec status 'done' et updatedAt dans la période
|
||||||
|
{
|
||||||
|
status: 'done',
|
||||||
|
updatedAt: {
|
||||||
|
gte: startDate,
|
||||||
|
lte: endDate
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Tâches avec status 'archived' récemment (aussi des accomplissements)
|
||||||
|
{
|
||||||
|
status: 'archived',
|
||||||
|
updatedAt: {
|
||||||
|
gte: startDate,
|
||||||
|
lte: endDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
completedAt: 'desc'
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
title: true,
|
||||||
|
description: true,
|
||||||
|
priority: true,
|
||||||
|
completedAt: true,
|
||||||
|
createdAt: true,
|
||||||
|
taskTags: {
|
||||||
|
select: {
|
||||||
|
tag: {
|
||||||
|
select: {
|
||||||
|
name: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return tasks;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les checkboxes complétées de la semaine
|
||||||
|
*/
|
||||||
|
private static async getCompletedCheckboxes(startDate: Date, endDate: Date) {
|
||||||
|
const checkboxes = await prisma.dailyCheckbox.findMany({
|
||||||
|
where: {
|
||||||
|
isChecked: true,
|
||||||
|
date: {
|
||||||
|
gte: startDate,
|
||||||
|
lte: endDate
|
||||||
|
}
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
text: true,
|
||||||
|
isChecked: true,
|
||||||
|
type: true,
|
||||||
|
date: true,
|
||||||
|
createdAt: true,
|
||||||
|
task: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
title: true,
|
||||||
|
priority: true,
|
||||||
|
taskTags: {
|
||||||
|
select: {
|
||||||
|
tag: {
|
||||||
|
select: {
|
||||||
|
name: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
date: 'desc'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return checkboxes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extrait les accomplissements clés basés sur la priorité
|
||||||
|
*/
|
||||||
|
private static extractKeyAccomplishments(tasks: TaskType[], checkboxes: CheckboxType[]): KeyAccomplishment[] {
|
||||||
|
const accomplishments: KeyAccomplishment[] = [];
|
||||||
|
|
||||||
|
// Tâches: prendre toutes les high/medium priority, et quelques low si significatives
|
||||||
|
tasks.forEach(task => {
|
||||||
|
const priority = task.priority.toLowerCase();
|
||||||
|
|
||||||
|
// Convertir priorité task en impact accomplissement
|
||||||
|
let impact: 'high' | 'medium' | 'low';
|
||||||
|
if (priority === 'high') {
|
||||||
|
impact = 'high';
|
||||||
|
} else if (priority === 'medium') {
|
||||||
|
impact = 'medium';
|
||||||
|
} else {
|
||||||
|
// Pour les low priority, ne garder que si c'est vraiment significatif
|
||||||
|
if (!this.isSignificantTask(task.title)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
impact = 'low';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compter les todos (checkboxes) associés à cette tâche
|
||||||
|
const relatedTodos = checkboxes.filter(cb => cb.task?.id === task.id);
|
||||||
|
|
||||||
|
accomplishments.push({
|
||||||
|
id: `task-${task.id}`,
|
||||||
|
title: task.title,
|
||||||
|
description: task.description || undefined,
|
||||||
|
tags: task.taskTags?.map(tt => tt.tag.name) || [],
|
||||||
|
impact,
|
||||||
|
completedAt: task.completedAt || new Date(),
|
||||||
|
relatedItems: [task.id, ...relatedTodos.map(t => t.id)],
|
||||||
|
todosCount: relatedTodos.length // Nombre réel de todos associés
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// AJOUTER SEULEMENT les meetings importants standalone (non liés à une tâche)
|
||||||
|
const standaloneMeetings = checkboxes.filter(checkbox =>
|
||||||
|
checkbox.type === 'meeting' && !checkbox.task // Meetings non liés à une tâche
|
||||||
|
);
|
||||||
|
|
||||||
|
standaloneMeetings.forEach(meeting => {
|
||||||
|
accomplishments.push({
|
||||||
|
id: `meeting-${meeting.id}`,
|
||||||
|
title: `📅 ${meeting.text}`,
|
||||||
|
tags: [], // Meetings n'ont pas de tags par défaut
|
||||||
|
impact: 'medium', // Meetings sont importants
|
||||||
|
completedAt: meeting.date,
|
||||||
|
relatedItems: [meeting.id],
|
||||||
|
todosCount: 1 // Un meeting = 1 todo
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Trier par impact puis par date
|
||||||
|
return accomplishments
|
||||||
|
.sort((a, b) => {
|
||||||
|
const impactOrder = { high: 3, medium: 2, low: 1 };
|
||||||
|
if (impactOrder[a.impact] !== impactOrder[b.impact]) {
|
||||||
|
return impactOrder[b.impact] - impactOrder[a.impact];
|
||||||
|
}
|
||||||
|
return b.completedAt.getTime() - a.completedAt.getTime();
|
||||||
|
})
|
||||||
|
.slice(0, 12); // Plus d'items maintenant qu'on filtre mieux
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Identifie les défis et enjeux à venir
|
||||||
|
*/
|
||||||
|
private static async identifyUpcomingChallenges(): Promise<UpcomingChallenge[]> {
|
||||||
|
|
||||||
|
// Récupérer les tâches à venir (priorité high/medium en premier)
|
||||||
|
const upcomingTasks = await prisma.task.findMany({
|
||||||
|
where: {
|
||||||
|
completedAt: null
|
||||||
|
},
|
||||||
|
orderBy: [
|
||||||
|
{ priority: 'asc' }, // high < medium < low
|
||||||
|
{ createdAt: 'desc' }
|
||||||
|
],
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
title: true,
|
||||||
|
description: true,
|
||||||
|
priority: true,
|
||||||
|
createdAt: true,
|
||||||
|
taskTags: {
|
||||||
|
select: {
|
||||||
|
tag: {
|
||||||
|
select: {
|
||||||
|
name: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
take: 30
|
||||||
|
});
|
||||||
|
|
||||||
|
// Récupérer les checkboxes récurrentes non complétées (meetings + tâches prioritaires)
|
||||||
|
const upcomingCheckboxes = await prisma.dailyCheckbox.findMany({
|
||||||
|
where: {
|
||||||
|
isChecked: false,
|
||||||
|
date: {
|
||||||
|
gte: new Date()
|
||||||
|
},
|
||||||
|
OR: [
|
||||||
|
{ type: 'meeting' },
|
||||||
|
{
|
||||||
|
task: {
|
||||||
|
priority: {
|
||||||
|
in: ['high', 'medium']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
text: true,
|
||||||
|
isChecked: true,
|
||||||
|
type: true,
|
||||||
|
date: true,
|
||||||
|
createdAt: true,
|
||||||
|
task: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
title: true,
|
||||||
|
priority: true,
|
||||||
|
taskTags: {
|
||||||
|
select: {
|
||||||
|
tag: {
|
||||||
|
select: {
|
||||||
|
name: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
orderBy: [
|
||||||
|
{ date: 'asc' },
|
||||||
|
{ createdAt: 'asc' }
|
||||||
|
],
|
||||||
|
take: 20
|
||||||
|
});
|
||||||
|
|
||||||
|
const challenges: UpcomingChallenge[] = [];
|
||||||
|
|
||||||
|
// Analyser les tâches - se baser sur la priorité réelle
|
||||||
|
upcomingTasks.forEach((task) => {
|
||||||
|
const taskPriority = task.priority.toLowerCase();
|
||||||
|
|
||||||
|
// Convertir priorité task en priorité challenge
|
||||||
|
let priority: 'high' | 'medium' | 'low';
|
||||||
|
if (taskPriority === 'high') {
|
||||||
|
priority = 'high';
|
||||||
|
} else if (taskPriority === 'medium') {
|
||||||
|
priority = 'medium';
|
||||||
|
} else {
|
||||||
|
// Pour les low priority, ne garder que si c'est vraiment challengeant
|
||||||
|
if (!this.isChallengingTask(task.title)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
priority = 'low';
|
||||||
|
}
|
||||||
|
|
||||||
|
const estimatedEffort = this.estimateEffort(task.title, task.description || undefined);
|
||||||
|
|
||||||
|
challenges.push({
|
||||||
|
id: `task-${task.id}`,
|
||||||
|
title: task.title,
|
||||||
|
description: task.description || undefined,
|
||||||
|
tags: task.taskTags?.map(tt => tt.tag.name) || [],
|
||||||
|
priority,
|
||||||
|
estimatedEffort,
|
||||||
|
blockers: this.identifyBlockers(task.title, task.description || undefined),
|
||||||
|
relatedItems: [task.id],
|
||||||
|
todosCount: 0 // TODO: compter les todos associés à cette tâche
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ajouter les meetings importants comme challenges
|
||||||
|
upcomingCheckboxes.forEach(checkbox => {
|
||||||
|
if (checkbox.type === 'meeting') {
|
||||||
|
challenges.push({
|
||||||
|
id: `checkbox-${checkbox.id}`,
|
||||||
|
title: checkbox.text,
|
||||||
|
tags: checkbox.task?.taskTags?.map(tt => tt.tag.name) || [],
|
||||||
|
priority: 'medium', // Meetings sont medium par défaut
|
||||||
|
estimatedEffort: 'hours',
|
||||||
|
blockers: [],
|
||||||
|
relatedItems: [checkbox.id],
|
||||||
|
todosCount: 1 // Une checkbox = 1 todo
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return challenges
|
||||||
|
.sort((a, b) => {
|
||||||
|
const priorityOrder = { high: 3, medium: 2, low: 1 };
|
||||||
|
return priorityOrder[b.priority] - priorityOrder[a.priority];
|
||||||
|
})
|
||||||
|
.slice(0, 10); // Plus d'items maintenant qu'on filtre mieux
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Estime l'effort requis
|
||||||
|
*/
|
||||||
|
private static estimateEffort(title: string, description?: string): 'days' | 'weeks' | 'hours' {
|
||||||
|
const content = `${title} ${description || ''}`.toLowerCase();
|
||||||
|
|
||||||
|
if (content.includes('architecture') || content.includes('migration') || content.includes('refactor')) {
|
||||||
|
return 'weeks';
|
||||||
|
}
|
||||||
|
if (content.includes('feature') || content.includes('implement') || content.includes('integration')) {
|
||||||
|
return 'days';
|
||||||
|
}
|
||||||
|
return 'hours';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Identifie les blockers potentiels
|
||||||
|
*/
|
||||||
|
private static identifyBlockers(title: string, description?: string): string[] {
|
||||||
|
const content = `${title} ${description || ''}`.toLowerCase();
|
||||||
|
const blockers: string[] = [];
|
||||||
|
|
||||||
|
if (content.includes('depends') || content.includes('waiting')) {
|
||||||
|
blockers.push('Dépendances externes');
|
||||||
|
}
|
||||||
|
if (content.includes('approval') || content.includes('review')) {
|
||||||
|
blockers.push('Validation requise');
|
||||||
|
}
|
||||||
|
if (content.includes('design') && !content.includes('implement')) {
|
||||||
|
blockers.push('Spécifications incomplètes');
|
||||||
|
}
|
||||||
|
|
||||||
|
return blockers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Détermine si une tâche est significative
|
||||||
|
*/
|
||||||
|
private static isSignificantTask(title: string): boolean {
|
||||||
|
const significantKeywords = [
|
||||||
|
'release', 'deploy', 'launch', 'milestone',
|
||||||
|
'architecture', 'design', 'strategy',
|
||||||
|
'integration', 'migration', 'optimization'
|
||||||
|
];
|
||||||
|
return significantKeywords.some(keyword => title.toLowerCase().includes(keyword));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Détermine si une checkbox est significative
|
||||||
|
*/
|
||||||
|
private static isSignificantCheckbox(text: string): boolean {
|
||||||
|
const content = text.toLowerCase();
|
||||||
|
return content.length > 30 || // Checkboxes détaillées
|
||||||
|
content.includes('meeting') ||
|
||||||
|
content.includes('review') ||
|
||||||
|
content.includes('call') ||
|
||||||
|
content.includes('presentation');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Détermine si une tâche représente un défi
|
||||||
|
*/
|
||||||
|
private static isChallengingTask(title: string): boolean {
|
||||||
|
const challengingKeywords = [
|
||||||
|
'complex', 'difficult', 'challenge',
|
||||||
|
'architecture', 'performance', 'security',
|
||||||
|
'integration', 'migration', 'optimization'
|
||||||
|
];
|
||||||
|
return challengingKeywords.some(keyword => title.toLowerCase().includes(keyword));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Analyse les patterns dans les checkboxes pour identifier des enjeux
|
||||||
|
*/
|
||||||
|
private static analyzeCheckboxPatterns(): UpcomingChallenge[] {
|
||||||
|
// Pour l'instant, retourner un array vide
|
||||||
|
// À implémenter selon les besoins spécifiques
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calcule les métriques résumées
|
||||||
|
*/
|
||||||
|
private static calculateMetrics(tasks: TaskType[], checkboxes: CheckboxType[]) {
|
||||||
|
const totalTasksCompleted = tasks.length;
|
||||||
|
const totalCheckboxesCompleted = checkboxes.length;
|
||||||
|
|
||||||
|
// Calculer les métriques détaillées
|
||||||
|
const highPriorityTasksCompleted = tasks.filter(t => t.priority.toLowerCase() === 'high').length;
|
||||||
|
const meetingCheckboxesCompleted = checkboxes.filter(c => c.type === 'meeting').length;
|
||||||
|
|
||||||
|
// Analyser la répartition par catégorie
|
||||||
|
const focusAreas: { [category: string]: number } = {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalTasksCompleted,
|
||||||
|
totalCheckboxesCompleted,
|
||||||
|
highPriorityTasksCompleted,
|
||||||
|
meetingCheckboxesCompleted,
|
||||||
|
completionRate: 0, // À calculer par rapport aux objectifs
|
||||||
|
focusAreas
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Génère le narratif pour le manager
|
||||||
|
*/
|
||||||
|
private static generateNarrative(
|
||||||
|
accomplishments: KeyAccomplishment[],
|
||||||
|
challenges: UpcomingChallenge[]
|
||||||
|
) {
|
||||||
|
// Points forts de la semaine
|
||||||
|
const topAccomplishments = accomplishments.slice(0, 3);
|
||||||
|
const weekHighlight = topAccomplishments.length > 0
|
||||||
|
? `Cette semaine, j'ai principalement progressé sur ${topAccomplishments.map(a => a.title).join(', ')}.`
|
||||||
|
: 'Semaine focalisée sur l\'exécution des tâches quotidiennes.';
|
||||||
|
|
||||||
|
// Défis rencontrés
|
||||||
|
const highImpactItems = accomplishments.filter(a => a.impact === 'high');
|
||||||
|
const mainChallenges = highImpactItems.length > 0
|
||||||
|
? `Les principaux enjeux traités ont été liés aux ${[...new Set(highImpactItems.flatMap(a => a.tags))].join(', ')}.`
|
||||||
|
: 'Pas de blockers majeurs rencontrés cette semaine.';
|
||||||
|
|
||||||
|
// Focus semaine prochaine
|
||||||
|
const topChallenges = challenges.slice(0, 3);
|
||||||
|
const nextWeekFocus = topChallenges.length > 0
|
||||||
|
? `La semaine prochaine sera concentrée sur ${topChallenges.map(c => c.title).join(', ')}.`
|
||||||
|
: 'Continuation du travail en cours selon les priorités établies.';
|
||||||
|
|
||||||
|
return {
|
||||||
|
weekHighlight,
|
||||||
|
mainChallenges,
|
||||||
|
nextWeekFocus
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
32
src/app/weekly-manager/WeeklyManagerPageClient.tsx
Normal file
32
src/app/weekly-manager/WeeklyManagerPageClient.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { TasksProvider } from '@/contexts/TasksContext';
|
||||||
|
import { UserPreferencesProvider } from '@/contexts/UserPreferencesContext';
|
||||||
|
import ManagerWeeklySummary from '@/components/dashboard/ManagerWeeklySummary';
|
||||||
|
import { ManagerSummary } from '@/services/manager-summary';
|
||||||
|
import { Task, Tag, UserPreferences } from '@/lib/types';
|
||||||
|
|
||||||
|
interface WeeklyManagerPageClientProps {
|
||||||
|
initialSummary: ManagerSummary;
|
||||||
|
initialTasks: Task[];
|
||||||
|
initialTags: (Tag & { usage: number })[];
|
||||||
|
initialPreferences: UserPreferences;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WeeklyManagerPageClient({
|
||||||
|
initialSummary,
|
||||||
|
initialTasks,
|
||||||
|
initialTags,
|
||||||
|
initialPreferences
|
||||||
|
}: WeeklyManagerPageClientProps) {
|
||||||
|
return (
|
||||||
|
<UserPreferencesProvider initialPreferences={initialPreferences}>
|
||||||
|
<TasksProvider
|
||||||
|
initialTasks={initialTasks}
|
||||||
|
initialTags={initialTags}
|
||||||
|
>
|
||||||
|
<ManagerWeeklySummary initialSummary={initialSummary} />
|
||||||
|
</TasksProvider>
|
||||||
|
</UserPreferencesProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
36
src/app/weekly-manager/page.tsx
Normal file
36
src/app/weekly-manager/page.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { Header } from '@/components/ui/Header';
|
||||||
|
import { ManagerSummaryService } from '@/services/manager-summary';
|
||||||
|
import { tasksService } from '@/services/tasks';
|
||||||
|
import { tagsService } from '@/services/tags';
|
||||||
|
import { userPreferencesService } from '@/services/user-preferences';
|
||||||
|
import { WeeklyManagerPageClient } from './WeeklyManagerPageClient';
|
||||||
|
|
||||||
|
// Force dynamic rendering (no static generation)
|
||||||
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
|
export default async function WeeklyManagerPage() {
|
||||||
|
// SSR - Récupération des données côté serveur
|
||||||
|
const [summary, initialTasks, initialTags, initialPreferences] = await Promise.all([
|
||||||
|
ManagerSummaryService.getManagerSummary(),
|
||||||
|
tasksService.getTasks(),
|
||||||
|
tagsService.getTags(),
|
||||||
|
userPreferencesService.getAllPreferences()
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-[var(--background)]">
|
||||||
|
<Header />
|
||||||
|
|
||||||
|
<div className="container mx-auto px-4 py-8">
|
||||||
|
<div className="max-w-6xl mx-auto">
|
||||||
|
<WeeklyManagerPageClient
|
||||||
|
initialSummary={summary}
|
||||||
|
initialTasks={initialTasks}
|
||||||
|
initialTags={initialTags}
|
||||||
|
initialPreferences={initialPreferences}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user