feat: update TODO.md and enhance dashboard components

- Marked several UI/UX tasks as complete in TODO.md, including improvements for Kanban icons, tag visibility, recent tasks display, and header responsiveness.
- Updated PriorityDistributionChart to adjust height for better layout.
- Refined IntegrationFilter to improve filter display and added new trigger class for dropdowns.
- Replaced RecentTaskTimeline with TaskCard in RecentTasks for better consistency.
- Enhanced TagDistributionChart with improved tooltip and legend styling.
- Updated DesktopControls and MobileControls to use lucide-react icons for filters and search functionality.
- Removed RecentTaskTimeline component for cleaner codebase.
This commit is contained in:
Julien Froidefond
2025-10-04 07:17:39 +02:00
parent c7ad1c0416
commit eac9e9a0bb
14 changed files with 213 additions and 302 deletions

16
TODO.md
View File

@@ -10,12 +10,12 @@
### 🎨 Design et Interface ### 🎨 Design et Interface
- [X] **Homepage cards** : toute en variant glass - [X] **Homepage cards** : toute en variant glass
- [ ] **Icône Kanban homepage** - Changer icône sur la page d'accueil, pas lisible (utiliser une lib) - [X] **Icône Kanban homepage** - Changer icône sur la page d'accueil, pas lisible (utiliser une lib)
- [ ] **Lisibilité label graph par tag** - Améliorer la lisibilité des labels dans les graphiques par tag - [x] **Lisibilité label graph par tag** - Améliorer la lisibilité des labels dans les graphiques par tag <!-- Amélioré marges, légendes, tailles de police, retiré emojis -->
- [ ] **Tag homepage** - Problème d'affichage des graphs de tags sur la homepage coté lisibilité, cezrtaines icones ne sont pas entièrement visible, et la légende est trop proche du graphe. - [x] **Tag homepage** - Problème d'affichage des graphs de tags sur la homepage côté lisibilité, certaines icones ne sont pas entièrement visible, et la légende est trop proche du graphe. <!-- Amélioré hauteur, marges, responsive -->
- [ ] **Tâches récentes** - Revoir l'affichage et la logique des tâches récentes - [x] **Tâches récentes** - Revoir l'affichage et la logique des tâches récentes <!-- Logique améliorée (tâches terminées récentes), responsive, icône claire -->
- [ ] **Header dépasse en tablet** - Corriger le débordement du header sur tablette - [x] **Header dépasse en tablet** - Corriger le débordement du header sur tablette <!-- Responsive amélioré, taille réglée, navigation adaptative -->
- [ ] **Icônes agenda et filtres** - Améliorer les icônes de l'agenda et des filtres dans desktop controls (utiliser une lib) - [x] **Icônes agenda et filtres** - Améliorer les icônes de l'agenda et des filtres dans desktop controls (utiliser une lib) <!-- Clock pour échéance, Settings pour filtres, Search visuelle -->
- [ ] **Réunion/tâche design** - Revoir le design des bouton dans dailySectrion : les toggles avoir un compposant ui - [ ] **Réunion/tâche design** - Revoir le design des bouton dans dailySectrion : les toggles avoir un compposant ui
- [ ] **Légende calendrier et padding** - Corriger l'espacement et la légende du calendrier dans daily - [ ] **Légende calendrier et padding** - Corriger l'espacement et la légende du calendrier dans daily
- [ ] **EditModal task couleur calendrier** - Problème de couleur en ajout de taches dans tous les icones calendriers; colmler au thème - [ ] **EditModal task couleur calendrier** - Problème de couleur en ajout de taches dans tous les icones calendriers; colmler au thème
@@ -29,13 +29,13 @@
- [ ] **Deux modales** - Problème de duplication de modales - [ ] **Deux modales** - Problème de duplication de modales
- [ ] **Control panel et select** - Problème avec les contrôles et sélecteurs - [ ] **Control panel et select** - Problème avec les contrôles et sélecteurs
- [ ] **TaskCard et Kanban transparence** - Appliquer la transparence sur le background et non sur la card - [ ] **TaskCard et Kanban transparence** - Appliquer la transparence sur le background et non sur la card
- [ ] **Recherche Kanban desktop controls** - Ajouter icône et label : "rechercher" pour rapetir - [X] **Recherche Kanban desktop controls** - Ajouter icône et label : "rechercher" pour rapetir
- [ ] **Largeur page Kanban** - Réduire légèrement la largeur et revoir toutes les autres pages - [ ] **Largeur page Kanban** - Réduire légèrement la largeur et revoir toutes les autres pages
- [ ] **Icône thème à gauche du profil** - Repositionner l'icône de thème dans le header - [ ] **Icône thème à gauche du profil** - Repositionner l'icône de thème dans le header
- [ ] **Déconnexion trop petit et couleur** - Améliorer le bouton de déconnexion - [ ] **Déconnexion trop petit et couleur** - Améliorer le bouton de déconnexion
- [ ] **Fond modal trop opaque** - Réduire l'opacité du fond des modales - [ ] **Fond modal trop opaque** - Réduire l'opacité du fond des modales
- [ ] **Couleurs thème clair et TFS Jira Kanban** - Harmoniser les couleurs du thème clair - [ ] **Couleurs thème clair et TFS Jira Kanban** - Harmoniser les couleurs du thème clair
- [ ] **États sélectionnés desktop control** - Revoir les couleurs des états sélectionnés pour avoir le joli bleu du dropdown partout - [X] **États sélectionnés desktop control** - Revoir les couleurs des états sélectionnés pour avoir le joli bleu du dropdown partout
- [ ] **Dépasse 1000 caractères en edit modal task** - Corriger la limite (pas de limite) et revoir la quickcard description - [ ] **Dépasse 1000 caractères en edit modal task** - Corriger la limite (pas de limite) et revoir la quickcard description
- [ ] **UI si échéance et trop de labels dans le footer de card** - Améliorer l'affichage en mode détaillé TaskCard; certains boutons sont sur deux lignes ce qui casse l'affichage - [ ] **UI si échéance et trop de labels dans le footer de card** - Améliorer l'affichage en mode détaillé TaskCard; certains boutons sont sur deux lignes ce qui casse l'affichage
- [ ] **Gravatar** - Implémenter l'affichage des avatars Gravatar - [ ] **Gravatar** - Implémenter l'affichage des avatars Gravatar

View File

@@ -63,7 +63,7 @@ export function PriorityDistributionChart({ data, title = "Distribution des Prio
return ( return (
<Card variant="glass" className="p-6"> <Card variant="glass" className="p-6">
<h3 className="text-lg font-semibold mb-4">{title}</h3> <h3 className="text-lg font-semibold mb-4">{title}</h3>
<div className="h-64"> <div className="h-70">
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer width="100%" height="100%">
<PieChart> <PieChart>
<Pie <Pie

View File

@@ -168,9 +168,9 @@ export function IntegrationFilter({
} else if (activeFilters.length === 1) { } else if (activeFilters.length === 1) {
const source = activeFilters[0]; const source = activeFilters[0];
const mode = getSourceMode(source.id); const mode = getSourceMode(source.id);
return mode === 'show' ? `Seulement ${source.label}` : `Sans ${source.label}`; return mode === 'show' ? `Solo ${source.label}` : `-${source.label}`;
} else { } else {
return `${activeFilters.length} filtres actifs`; return `+${activeFilters.length}`;
} }
}; };
@@ -268,7 +268,8 @@ export function IntegrationFilter({
variant={getMainButtonVariant()} variant={getMainButtonVariant()}
content={dropdownContent} content={dropdownContent}
placement={alignRight ? "bottom-end" : "bottom-start"} placement={alignRight ? "bottom-end" : "bottom-start"}
className={`min-w-[240px] ${alignRight ? "transform -translate-x-full" : ""}`} className={`min-w-[239px] max-h-[190px] overflow-y-auto ${alignRight ? "transform -translate-x-full" : ""}`}
triggerClassName="h-[33px] py-1 max-w-[140px] truncate"
/> />
); );
} }

View File

@@ -2,10 +2,10 @@
import { Task } from '@/lib/types'; import { Task } from '@/lib/types';
import { Card } from '@/components/ui/Card'; import { Card } from '@/components/ui/Card';
import { RecentTaskTimeline } from '@/components/ui/RecentTaskTimeline'; import { TaskCard } from '@/components/ui/TaskCard';
import { useTasksContext } from '@/contexts/TasksContext'; import { useTasksContext } from '@/contexts/TasksContext';
import Link from 'next/link'; import Link from 'next/link';
import { Clipboard } from 'lucide-react'; import { Clipboard, Clock } from 'lucide-react';
interface RecentTasksProps { interface RecentTasksProps {
tasks: Task[]; tasks: Task[];
@@ -31,34 +31,54 @@ export function RecentTasks({ tasks, selectedSources = [], hiddenSources = [] }:
); );
} }
// Prendre les 5 tâches les plus récentes (créées ou modifiées) // Prendre les 5 tâches les plus pertinentes (créées récemment ou modifiées récemment)
const recentTasks = filteredTasks const recentTasks = filteredTasks
.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime()) .filter(task => {
// Ne pas afficher les tâches terminées depuis plus de 7 jours
if (task.status === 'done' && task.completedAt) {
const sevenDaysAgo = new Date();
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
return task.completedAt > sevenDaysAgo;
}
return task.status !== 'done';
})
.sort((a, b) => {
// Prioriser les tâches non terminées et récentes
if (a.status === 'done' && b.status !== 'done') return 1;
if (b.status === 'done' && a.status !== 'done') return -1;
// Sinon trier par date de modification
return b.updatedAt.getTime() - a.updatedAt.getTime();
})
.slice(0, 5); .slice(0, 5);
return ( return (
<Card variant="glass" className="p-6 mt-8"> <Card variant="glass" className="p-4 sm:p-6 mt-8">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold">Tâches Récentes</h3> <div className="flex items-center gap-3">
<Clock className="w-5 h-5 text-[var(--primary)]" />
<h3 className="text-lg font-semibold text-[var(--foreground)]">Tâches Récentes</h3>
</div>
<Link href="/kanban"> <Link href="/kanban">
<button className="text-sm text-[var(--primary)] hover:underline"> <button className="text-sm text-[var(--primary)] hover:text-[var(--primary)]/80 hover:underline font-medium transition-colors">
Voir toutes Voir toutes
</button> </button>
</Link> </Link>
</div> </div>
{recentTasks.length === 0 ? ( {recentTasks.length === 0 ? (
<div className="text-center py-8 text-[var(--muted-foreground)]"> <div className="text-center py-6 sm:py-8 text-[var(--muted-foreground)]">
<Clipboard className="w-12 h-12 mx-auto mb-3 opacity-50" /> <Clipboard className="w-8 h-8 sm:w-12 sm:h-12 mx-auto mb-3 opacity-50" />
<p>Aucune tâche disponible</p> <p className="text-sm sm:text-base">Aucune tâche récente</p>
<p className="text-sm">Créez votre première tâche pour commencer</p> <p className="text-xs sm:text-sm opacity-75">Créez une nouvelle tâche pour commencer</p>
</div> </div>
) : ( ) : (
<div className="space-y-1"> <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
{recentTasks.map((task) => ( {recentTasks.map((task) => (
<RecentTaskTimeline <TaskCard
key={task.id} key={task.id}
variant="detailed"
title={task.title} title={task.title}
description={task.description} description={task.description}
status={task.status} status={task.status}
@@ -66,13 +86,10 @@ export function RecentTasks({ tasks, selectedSources = [], hiddenSources = [] }:
tags={task.tags || []} tags={task.tags || []}
dueDate={task.dueDate} dueDate={task.dueDate}
completedAt={task.completedAt} completedAt={task.completedAt}
updatedAt={task.updatedAt}
source={task.source || 'manual'} source={task.source || 'manual'}
jiraKey={task.jiraKey} jiraKey={task.jiraKey}
tfsPullRequestId={task.tfsPullRequestId} tfsPullRequestId={task.tfsPullRequestId}
availableTags={availableTags}
onClick={() => { onClick={() => {
// Navigation vers le kanban avec la tâche sélectionnée
window.location.href = `/kanban?taskId=${task.id}`; window.location.href = `/kanban?taskId=${task.id}`;
}} }}
/> />

View File

@@ -10,31 +10,33 @@ interface TagDistributionChartProps {
} }
export function TagDistributionChart({ metrics, className }: TagDistributionChartProps) { export function TagDistributionChart({ metrics, className }: TagDistributionChartProps) {
// Préparer les données pour le graphique en camembert // Préparer les données pour le graphique en camembert
const pieData = metrics.tagDistribution.slice(0, 8).map((tag) => ({ const pieData = metrics.tagDistribution.slice(0, 8).map((tag) => ({
name: tag.tagName, name: tag.tagName,
value: tag.count, value: tag.count,
percentage: tag.percentage, percentage: tag.percentage,
color: tag.tagColor color: tag.tagColor // Garder la couleur originale du tag
})); }));
// Préparer les données pour le graphique en barres (top tags) // Préparer les données pour le graphique en barres (top tags)
const barData = metrics.topTags.slice(0, 10).map(tag => ({ const barData = metrics.topTags.slice(0, 10).map((tag) => ({
name: tag.tagName.length > 12 ? `${tag.tagName.substring(0, 12)}...` : tag.tagName, name: tag.tagName.length > 12 ? `${tag.tagName.substring(0, 12)}...` : tag.tagName,
usage: tag.usage, usage: tag.usage,
completionRate: tag.completionRate, completionRate: tag.completionRate,
avgPriority: tag.avgPriority, avgPriority: tag.avgPriority,
color: tag.tagColor color: tag.tagColor // Garder la couleur originale du tag
})); }));
// Tooltip personnalisé pour le camembert // Tooltip personnalisé pour les tags (distribution)
const PieTooltip = ({ active, payload }: { active?: boolean; payload?: Array<{ payload: { name: string; value: number; percentage: number } }> }) => { const TagTooltip = ({ active, payload }: { active?: boolean; payload?: Array<{ payload: { name: string; value: number; percentage: number } }> }) => {
if (active && payload && payload.length) { if (active && payload && payload.length) {
const data = payload[0].payload; const data = payload[0].payload;
return ( return (
<div className="bg-[var(--card)] border border-[var(--border)] rounded-lg p-3 shadow-lg"> <div className="bg-[var(--card)] border border-[var(--border)] rounded-lg p-4 shadow-xl">
<p className="font-medium mb-1">{data.name}</p> <p className="font-semibold mb-2 text-[var(--foreground)]">{data.name}</p>
<p className="text-sm text-[var(--foreground)]"> <p className="text-sm text-[var(--muted-foreground)]">
{data.value} tâches ({data.percentage.toFixed(1)}%) {data.value} tâches ({data.percentage.toFixed(1)}%)
</p> </p>
</div> </div>
@@ -48,12 +50,12 @@ export function TagDistributionChart({ metrics, className }: TagDistributionChar
if (active && payload && payload.length) { if (active && payload && payload.length) {
const data = payload[0].payload; const data = payload[0].payload;
return ( return (
<div className="bg-[var(--card)] border border-[var(--border)] rounded-lg p-3 shadow-lg"> <div className="bg-[var(--card)] border border-[var(--border)] rounded-lg p-4 shadow-xl">
<p className="font-medium mb-1">{data.name}</p> <p className="font-semibold mb-2 text-[var(--foreground)]">{data.name}</p>
<p className="text-sm text-[var(--foreground)]"> <p className="text-sm text-[var(--muted-foreground)] mb-1">
{data.usage} tâches {data.usage} tâches
</p> </p>
<p className="text-sm text-[var(--muted-foreground)]"> <p className="text-xs text-[var(--muted-foreground)]">
Taux de completion: {data.completionRate.toFixed(1)}% Taux de completion: {data.completionRate.toFixed(1)}%
</p> </p>
</div> </div>
@@ -62,19 +64,17 @@ export function TagDistributionChart({ metrics, className }: TagDistributionChar
return null; return null;
}; };
// Légende personnalisée // Légende personnalisée qui utilise directement les données du graphique
const CustomLegend = ({ payload }: { payload?: Array<{ value: string; color: string }> }) => { const CustomLegend = () => {
if (!payload) return null;
return ( return (
<div className="flex flex-wrap gap-2 mt-4"> <div className="flex flex-wrap gap-4 mt-6">
{payload.map((entry, index) => ( {pieData.map((entry, index) => (
<div key={index} className="flex items-center gap-2 text-sm"> <div key={index} className="flex items-center gap-3">
<div <div
className="w-3 h-3 rounded-full" className="w-4 h-4 rounded-full border border-[var(--border)]"
style={{ backgroundColor: entry.color }} style={{ backgroundColor: entry.color }}
/> />
<span className="text-[var(--foreground)]">{entry.value}</span> <span className="text-sm text-[var(--muted-foreground)]">{entry.name}</span>
</div> </div>
))} ))}
</div> </div>
@@ -83,56 +83,60 @@ export function TagDistributionChart({ metrics, className }: TagDistributionChar
return ( return (
<div className={className}> <div className={className}>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> <div className="grid grid-cols-1 xl:grid-cols-2 gap-8">
{/* Distribution par tags - Camembert */} {/* Distribution par tags - Camembert */}
<Card variant="glass" className="p-6"> <Card variant="glass" className="p-6">
<h3 className="text-lg font-semibold mb-4">🏷 Distribution par Tags</h3> <h3 className="text-lg font-semibold mb-4 text-[var(--foreground)]">Distribution par Tags</h3>
<div className="h-64"> <div className="h-60">
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer width="100%" height="100%">
<PieChart> <PieChart>
<Pie <Pie
data={pieData} data={pieData}
cx="50%" cx="50%"
cy="50%" cy="50%"
labelLine={false} labelLine={false}
label={(props: PieLabelRenderProps) => { label={(props: PieLabelRenderProps) => {
const { name, percent } = props; const { percent } = props;
const percentValue = typeof percent === 'number' ? percent : 0; return typeof percent === 'number' && percent > 0.05 ? `${Math.round(percent * 100)}%` : '';
return percentValue > 0.05 ? `${name}: ${(percentValue * 100).toFixed(1)}%` : ''; }}
}} outerRadius={80}
outerRadius={80} fill="#8884d8"
fill="#8884d8" dataKey="value"
dataKey="value" nameKey="name"
nameKey="name" >
> {pieData.map((entry, index) => (
{pieData.map((entry, index) => ( <Cell key={`cell-${index}`} fill={entry.color} />
<Cell key={`cell-${index}`} fill={entry.color} /> ))}
))} </Pie>
</Pie> <Tooltip content={<TagTooltip />} />
<Tooltip content={<PieTooltip />} /> </PieChart>
<Legend content={<CustomLegend />} /> </ResponsiveContainer>
</PieChart>
</ResponsiveContainer> <CustomLegend />
</div> </div>
</Card> </Card>
{/* Top tags - Barres */} {/* Top tags - Barres */}
<Card variant="glass" className="p-6"> <Card variant="glass" className="p-6">
<h3 className="text-lg font-semibold mb-4">📊 Top Tags par Usage</h3> <h3 className="text-lg font-semibold mb-4 text-[var(--foreground)]">Top Tags par Usage</h3>
<div className="h-64"> <div className="h-80">
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer width="100%" height="100%">
<BarChart data={barData} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}> <BarChart data={barData} margin={{ top: 20, right: 30, left: 20, bottom: 20 }}>
<CartesianGrid strokeDasharray="3 3" stroke="var(--border)" /> <CartesianGrid strokeDasharray="3 3" stroke="var(--border)" />
<XAxis <XAxis
dataKey="name" dataKey="name"
tick={{ fontSize: 12 }} tick={{ fontSize: 13, fill: 'var(--muted-foreground)' }}
angle={-45} angle={-45}
textAnchor="end" textAnchor="end"
height={80} height={80}
style={{ fontFamily: 'ui-monospace, SFMono-Regular, monospace' }}
/>
<YAxis
tick={{ fontSize: 13, fill: 'var(--foreground)' }}
style={{ fontFamily: 'ui-monospace, SFMono-Regular, monospace' }}
/> />
<YAxis tick={{ fontSize: 12 }} />
<Tooltip content={<BarTooltip />} /> <Tooltip content={<BarTooltip />} />
<Bar <Bar
dataKey="usage" dataKey="usage"
@@ -149,8 +153,8 @@ export function TagDistributionChart({ metrics, className }: TagDistributionChar
</div> </div>
{/* Statistiques des tags */} {/* Statistiques des tags */}
<Card variant="glass" className="p-6 mt-6"> <Card variant="glass" className="p-6 mt-8">
<h3 className="text-lg font-semibold mb-4">📈 Statistiques des Tags</h3> <h3 className="text-lg font-semibold mb-4 text-[var(--foreground)]">Statistiques des Tags</h3>
<div className="grid grid-cols-2 md:grid-cols-5 gap-4"> <div className="grid grid-cols-2 md:grid-cols-5 gap-4">
<div className="text-center"> <div className="text-center">

View File

@@ -5,7 +5,7 @@ import { Button, ToggleButton, SearchInput, ControlPanel, ControlSection, Contro
import { IntegrationFilter } from '@/components/dashboard/IntegrationFilter'; import { IntegrationFilter } from '@/components/dashboard/IntegrationFilter';
import { FontSizeToggle } from '@/components/ui/FontSizeToggle'; import { FontSizeToggle } from '@/components/ui/FontSizeToggle';
import type { KanbanFilters } from '@/lib/types'; import type { KanbanFilters } from '@/lib/types';
import { Filter, Target, Calendar, Plus, List, Grid3X3, Layout } from 'lucide-react'; import { Target, Plus, List, Grid3X3, Layout, Clock, Search, Settings } from 'lucide-react';
interface DesktopControlsProps { interface DesktopControlsProps {
showFilters: boolean; showFilters: boolean;
@@ -81,13 +81,17 @@ export function DesktopControls({
{/* Layout responsive : deux lignes sur tablette, une ligne sur desktop */} {/* Layout responsive : deux lignes sur tablette, une ligne sur desktop */}
<div className="flex flex-col lg:flex-row lg:items-center gap-4 lg:gap-0 w-full"> <div className="flex flex-col lg:flex-row lg:items-center gap-4 lg:gap-0 w-full">
{/* Section gauche : Recherche + Boutons principaux */} {/* Section gauche : Recherche + Boutons principaux */}
<ControlSection> <ControlSection className="items-center gap-2">
{/* Champ de recherche */} {/* Champ de recherche avec icône */}
<SearchInput <div className="relative flex-1 min-w-0">
value={localSearch} <SearchInput
onChange={handleSearchChange} value={localSearch}
placeholder="Rechercher des tâches..." onChange={handleSearchChange}
/> placeholder="Rechercher"
className="pl-10"
/>
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-[var(--muted-foreground)] pointer-events-none" />
</div>
<ControlGroup> <ControlGroup>
<ToggleButton <ToggleButton
@@ -95,7 +99,7 @@ export function DesktopControls({
isActive={showFilters} isActive={showFilters}
count={activeFiltersCount} count={activeFiltersCount}
onClick={onToggleFilters} onClick={onToggleFilters}
icon={<Filter className="w-4 h-4" />} icon={<Settings className="w-4 h-4" />}
> >
Filtres Filtres
</ToggleButton> </ToggleButton>
@@ -113,14 +117,16 @@ export function DesktopControls({
variant="cyan" variant="cyan"
isActive={kanbanFilters.showWithDueDate} isActive={kanbanFilters.showWithDueDate}
onClick={handleDueDateFilterToggle} onClick={handleDueDateFilterToggle}
title={kanbanFilters.showWithDueDate ? "Afficher toutes les tâches" : "Afficher seulement les tâches avec date de fin"} title={kanbanFilters.showWithDueDate ? "Afficher toutes les tâches" : "Afficher seulement les tâches avec échéance"}
icon={<Calendar className="w-4 h-4" />} icon={<Clock className="w-4 h-4" />}
/> >
Échéance
</ToggleButton>
</ControlGroup> </ControlGroup>
</ControlSection> </ControlSection>
{/* Section droite : Raccourcis + Bouton Nouvelle tâche */} {/* Section droite : Raccourcis + Bouton Nouvelle tâche */}
<ControlSection className="justify-between lg:justify-start"> <ControlSection className="justify-between lg:justify-start items-center">
<ControlGroup className="border-l border-[var(--border)] ml-2 pl-2 pr-4"> <ControlGroup className="border-l border-[var(--border)] ml-2 pl-2 pr-4">
{/* Raccourcis Sources (Jira & TFS) */} {/* Raccourcis Sources (Jira & TFS) */}
<IntegrationFilter <IntegrationFilter
@@ -134,6 +140,7 @@ export function DesktopControls({
onClick={onToggleCompactView} onClick={onToggleCompactView}
title={compactView ? "Vue détaillée" : "Vue compacte"} title={compactView ? "Vue détaillée" : "Vue compacte"}
icon={compactView ? <List className="w-4 h-4" /> : <Grid3X3 className="w-4 h-4" />} icon={compactView ? <List className="w-4 h-4" /> : <Grid3X3 className="w-4 h-4" />}
className="h-[33px]"
> >
{compactView ? 'Détaillée' : 'Compacte'} {compactView ? 'Détaillée' : 'Compacte'}
</ToggleButton> </ToggleButton>
@@ -144,6 +151,7 @@ export function DesktopControls({
onClick={onToggleSwimlanes} onClick={onToggleSwimlanes}
title={swimlanesByTags ? "Vue standard" : "Vue swimlanes"} title={swimlanesByTags ? "Vue standard" : "Vue swimlanes"}
icon={swimlanesByTags ? <Layout className="w-4 h-4" /> : <Grid3X3 className="w-4 h-4" />} icon={swimlanesByTags ? <Layout className="w-4 h-4" /> : <Grid3X3 className="w-4 h-4" />}
className="h-[33px]"
> >
{swimlanesByTags ? 'Standard' : 'Swimlanes'} {swimlanesByTags ? 'Standard' : 'Swimlanes'}
</ToggleButton> </ToggleButton>
@@ -155,9 +163,8 @@ export function DesktopControls({
{/* Bouton d'ajout de tâche */} {/* Bouton d'ajout de tâche */}
<Button <Button
variant="primary" variant="primary"
size="sm"
onClick={onCreateTask} onClick={onCreateTask}
className="flex items-center gap-2" className="flex items-center gap-2 h-[34px] max-w-[180px] truncate"
> >
<Plus className="w-4 h-4" /> <Plus className="w-4 h-4" />
Nouvelle tâche Nouvelle tâche

View File

@@ -5,7 +5,7 @@ import { Button, ToggleButton, ControlPanel } from '@/components/ui';
import { IntegrationFilter } from '@/components/dashboard/IntegrationFilter'; import { IntegrationFilter } from '@/components/dashboard/IntegrationFilter';
import { FontSizeToggle } from '@/components/ui/FontSizeToggle'; import { FontSizeToggle } from '@/components/ui/FontSizeToggle';
import type { KanbanFilters } from '@/lib/types'; import type { KanbanFilters } from '@/lib/types';
import { Menu, Plus, Filter, Target, List, Grid3X3 } from 'lucide-react'; import { Menu, Plus, Settings, Target, List, Grid3X3 } from 'lucide-react';
interface MobileControlsProps { interface MobileControlsProps {
showFilters: boolean; showFilters: boolean;
@@ -77,7 +77,7 @@ export function MobileControls({
onToggleFilters(); onToggleFilters();
setIsMenuOpen(false); setIsMenuOpen(false);
}} }}
icon={<Filter className="w-4 h-4" />} icon={<Settings className="w-4 h-4" />}
> >
Filtres Filtres
</ToggleButton> </ToggleButton>

View File

@@ -9,7 +9,6 @@ import { MetricCard } from '@/components/ui/MetricCard';
import { AchievementCard } from '@/components/ui/AchievementCard'; import { AchievementCard } from '@/components/ui/AchievementCard';
import { ChallengeCard } from '@/components/ui/ChallengeCard'; import { ChallengeCard } from '@/components/ui/ChallengeCard';
import { SkeletonCard } from '@/components/ui/SkeletonCard'; import { SkeletonCard } from '@/components/ui/SkeletonCard';
import { RecentTaskTimeline } from '@/components/ui/RecentTaskTimeline';
import { AchievementData } from '@/components/ui/AchievementCard'; import { AchievementData } from '@/components/ui/AchievementCard';
import { ChallengeData } from '@/components/ui/ChallengeCard'; import { ChallengeData } from '@/components/ui/ChallengeCard';
@@ -226,52 +225,6 @@ export function CardsSection() {
</div> </div>
</div> </div>
{/* Recent Task Timeline */}
<div className="space-y-4">
<h3 className="text-lg font-medium text-[var(--foreground)]">Recent Task Timeline</h3>
<div className="space-y-1">
<RecentTaskTimeline
title="Implement user authentication"
description="Add login and registration functionality with JWT tokens"
status="in_progress"
priority="high"
tags={['auth', 'security', 'backend']}
dueDate={new Date(Date.now() + 3 * 86400000)}
source="jira"
jiraKey="PROJ-123"
updatedAt={new Date(Date.now() - 2 * 3600000)}
/>
<RecentTaskTimeline
title="Design new dashboard"
description="Create a modern dashboard interface with analytics"
status="todo"
priority="medium"
tags={['design', 'ui', 'frontend']}
dueDate={new Date(Date.now() + 7 * 86400000)}
source="manual"
updatedAt={new Date(Date.now() - 1 * 86400000)}
/>
<RecentTaskTimeline
title="Fix critical bug in payment system"
description="Resolve issue with payment processing"
status="done"
priority="high"
tags={['bug', 'payment', 'critical']}
completedAt={new Date(Date.now() - 1 * 3600000)}
source="tfs"
tfsPullRequestId={456}
/>
<RecentTaskTimeline
title="Update documentation"
description="Update API documentation for new endpoints"
status="in_progress"
priority="low"
tags={['docs', 'api']}
source="reminders"
updatedAt={new Date(Date.now() - 30 * 60000)}
/>
</div>
</div>
{/* Skeleton Cards */} {/* Skeleton Cards */}
<div className="space-y-4"> <div className="space-y-4">

View File

@@ -42,6 +42,8 @@ interface DropdownProps {
className?: string; className?: string;
/** Classe CSS additionnelle pour le contenu */ /** Classe CSS additionnelle pour le contenu */
contentClassName?: string; contentClassName?: string;
/** Classe CSS additionnelle pour le bouton trigger */
triggerClassName?: string;
/** Callback quand le dropdown s'ouvre */ /** Callback quand le dropdown s'ouvre */
onOpen?: () => void; onOpen?: () => void;
/** Callback quand le dropdown se ferme */ /** Callback quand le dropdown se ferme */
@@ -66,6 +68,7 @@ export function Dropdown({
zIndex = 9999, zIndex = 9999,
className, className,
contentClassName, contentClassName,
triggerClassName,
onOpen, onOpen,
onClose, onClose,
open: controlledOpen, open: controlledOpen,
@@ -252,6 +255,7 @@ export function Dropdown({
className={cn( className={cn(
'flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-md transition-colors', 'flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-md transition-colors',
getVariantStyles(), getVariantStyles(),
triggerClassName,
disabled && 'opacity-50 cursor-not-allowed' disabled && 'opacity-50 cursor-not-allowed'
)} )}
> >

View File

@@ -22,6 +22,7 @@ export function Header({ title = "TowerControl", subtitle = "Task Management", s
const { isConfigured: isJiraConfigured, config: jiraConfig } = useJiraConfig(); const { isConfigured: isJiraConfigured, config: jiraConfig } = useJiraConfig();
const pathname = usePathname(); const pathname = usePathname();
const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const [tabletMenuOpen, setTabletMenuOpen] = useState(false);
const [themeDropdownOpen, setThemeDropdownOpen] = useState(false); const [themeDropdownOpen, setThemeDropdownOpen] = useState(false);
const { openModal: openShortcutsModal } = useKeyboardShortcutsModal(); const { openModal: openShortcutsModal } = useKeyboardShortcutsModal();
const { data: session } = useSession(); const { data: session } = useSession();
@@ -171,44 +172,97 @@ export function Header({ title = "TowerControl", subtitle = "Task Management", s
</div> </div>
{/* Auth controls à droite mobile */} {/* Auth controls à droite mobile - dans la ligne principale */}
<div className="flex items-center gap-1"> <div className="hidden sm:block">
<AuthButton /> <AuthButton />
</div> </div>
</div> </div>
{/* Ligne Auth séparée sur très petits écrans */}
<div className="lg:hidden sm:hidden flex justify-end pt-2">
<AuthButton />
</div>
{/* Layout desktop - une seule ligne comme avant */} {/* Layout desktop - une seule ligne comme avant */}
<div className="hidden lg:flex items-center justify-between gap-6"> <div className="hidden lg:flex items-center justify-between gap-6">
{/* Titre et status */} {/* Titre et status */}
<div className="flex items-center gap-6"> <div className="flex items-center gap-6">
<div className="flex items-center gap-4 w-[300px]"> <div className="flex items-center gap-4 min-w-0">
<div className={`w-3 h-3 rounded-full shadow-lg ${ <div className={`w-3 h-3 rounded-full shadow-lg ${
syncing syncing
? 'bg-yellow-400 animate-spin shadow-yellow-400/50' ? 'bg-yellow-400 animate-spin shadow-yellow-400/50'
: 'bg-cyan-400 animate-pulse shadow-cyan-400/50' : 'bg-cyan-400 animate-pulse shadow-cyan-400/50'
}`}></div> }`}></div>
<div> <div className="min-w-0">
<h1 className="text-2xl font-mono font-bold text-[var(--foreground)] tracking-wider"> <h1 className="text-xl xl:text-2xl font-mono font-bold text-[var(--foreground)] tracking-wider truncate">
{title} <span className="sm:hidden">{title}</span>
<span className="hidden sm:inline">{title}</span>
</h1> </h1>
<p className="text-[var(--muted-foreground)] mt-1 font-mono text-sm"> <p className="text-[var(--muted-foreground)] mt-1 font-mono text-xs sm:text-sm truncate">
{subtitle} {subtitle}
</p> </p>
</div> </div>
</div> </div>
{/* Navigation desktop */} {/* Navigation desktop */}
<nav className="flex items-center gap-2"> <nav className="flex items-center gap-1 xl:gap-2 flex-wrap">
{navLinks.map(({ href, label }) => ( {navLinks.slice(0, 4).map(({ href, label }) => (
<Link <Link
key={href} key={href}
href={href} href={href}
className={getLinkClasses(href)} className={`${getLinkClasses(href)} text-xs xl:text-sm`}
> >
{label} {label}
</Link> </Link>
))} ))}
{/* Plus d'éléments sur très grands écrans */}
<div className="hidden 2xl:flex items-center gap-1">
{navLinks.slice(4).map(({ href, label }) => (
<Link
key={href}
href={href}
className={getLinkClasses(href)}
>
{label}
</Link>
))}
</div>
{/* Menu dropdown pour écrans moyens */}
<div className="xl:hidden relative">
<button
onClick={() => setTabletMenuOpen(!tabletMenuOpen)}
className="font-mono text-xs uppercase tracking-wider transition-colors px-2 py-1.5 rounded-md text-[var(--muted-foreground)] hover:text-[var(--primary)] hover:bg-[var(--card-hover)]"
title="Plus de liens"
>
</button>
{tabletMenuOpen && (
<>
{/* Backdrop */}
<div
className="fixed inset-0 z-[200]"
onClick={() => setTabletMenuOpen(false)}
/>
{/* Menu items */}
<div className="absolute right-0 top-full mt-2 bg-[var(--card)] border border-[var(--border)] rounded-lg shadow-lg z-[201] py-2 min-w-[119px]">
{navLinks.slice(4).map(({ href, label }) => (
<Link
key={href}
href={href}
className={`block px-4 py-2 text-sm transition-colors ${getMobileLinkClasses(href)}`}
onClick={() => setTabletMenuOpen(false)}
>
{label}
</Link>
))}
</div>
</>
)}
</div>
{/* Keyboard Shortcuts desktop */} {/* Keyboard Shortcuts desktop */}
<button <button
onClick={openShortcutsModal} onClick={openShortcutsModal}
@@ -311,7 +365,7 @@ export function Header({ title = "TowerControl", subtitle = "Task Management", s
className={getMobileLinkClasses('/profile')} className={getMobileLinkClasses('/profile')}
onClick={() => setMobileMenuOpen(false)} onClick={() => setMobileMenuOpen(false)}
> >
👤 Profil Profil
</Link> </Link>
{/* Bouton déconnexion */} {/* Bouton déconnexion */}
@@ -322,7 +376,7 @@ export function Header({ title = "TowerControl", subtitle = "Task Management", s
}} }}
className="font-mono text-sm uppercase tracking-wider transition-colors px-4 py-3 rounded-md block w-full text-left text-[var(--destructive)] hover:text-[var(--destructive)] hover:bg-[var(--destructive)]/10" className="font-mono text-sm uppercase tracking-wider transition-colors px-4 py-3 rounded-md block w-full text-left text-[var(--destructive)] hover:text-[var(--destructive)] hover:bg-[var(--destructive)]/10"
> >
🚪 Déconnexion Déconnexion
</button> </button>
</> </>
)} )}

View File

@@ -1,128 +0,0 @@
import { HTMLAttributes } from 'react';
import { cn } from '@/lib/utils';
import { StatusBadge } from './StatusBadge';
import { TaskStatus } from '@/lib/types';
import { PriorityBadge } from './PriorityBadge';
import { TagDisplay } from './TagDisplay';
import { formatDateForDisplay } from '@/lib/date-utils';
import { ChevronRight } from 'lucide-react';
interface RecentTaskTimelineProps extends HTMLAttributes<HTMLDivElement> {
title: string;
description?: string;
tags?: string[];
priority?: 'low' | 'medium' | 'high';
status?: TaskStatus;
dueDate?: Date;
completedAt?: Date;
updatedAt?: Date;
source?: 'manual' | 'jira' | 'tfs' | 'reminders';
jiraKey?: string;
tfsPullRequestId?: number;
onClick?: () => void;
className?: string;
availableTags?: Array<{ id: string; name: string; color: string }>;
}
export function RecentTaskTimeline({
title,
description,
tags = [],
priority,
status = 'todo',
dueDate,
completedAt,
updatedAt,
source = 'manual',
jiraKey,
tfsPullRequestId,
onClick,
className,
availableTags = [],
...props
}: RecentTaskTimelineProps) {
const getSourceIcon = () => {
switch (source) {
case 'jira':
return <div className="w-2 h-2 bg-[var(--primary)] rounded-full" />;
case 'tfs':
return <div className="w-2 h-2 bg-[var(--blue)] rounded-full" />;
case 'reminders':
return <div className="w-2 h-2 bg-[var(--accent)] rounded-full" />;
default:
return <div className="w-2 h-2 bg-[var(--gray)] rounded-full" />;
}
};
const getTimeInfo = () => {
if (completedAt) {
return `Terminé ${formatDateForDisplay(completedAt)}`;
}
if (dueDate) {
return `Échéance ${formatDateForDisplay(dueDate)}`;
}
if (updatedAt) {
return `Modifié ${formatDateForDisplay(updatedAt)}`;
}
return null;
};
return (
<div
className={cn(
"group relative flex items-start gap-4 p-3 rounded-lg hover:bg-[var(--card-hover)] transition-colors cursor-pointer",
className
)}
onClick={onClick}
{...props}
>
{/* Timeline dot */}
<div className="flex-shrink-0 mt-2">
{getSourceIcon()}
</div>
{/* Content */}
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between gap-2 mb-1">
<h4 className="font-medium text-[var(--foreground)] text-sm group-hover:text-[var(--primary)] transition-colors">
{title}
</h4>
<div className="flex items-center gap-1 flex-shrink-0">
{priority && <PriorityBadge priority={priority} />}
<StatusBadge status={status} />
</div>
</div>
{description && (
<p className="text-xs text-[var(--muted-foreground)] mb-2 line-clamp-1">
{description}
</p>
)}
{tags.length > 0 && (
<div className="mb-2">
<TagDisplay
tags={tags}
availableTags={availableTags}
maxTags={2}
size="sm"
/>
</div>
)}
<div className="flex items-center justify-between text-xs text-[var(--muted-foreground)]">
<div className="flex items-center gap-2">
{jiraKey && <span className="font-mono text-[var(--primary)]">{jiraKey}</span>}
{tfsPullRequestId && <span className="font-mono text-[var(--blue)]">PR #{tfsPullRequestId}</span>}
</div>
<span>{getTimeInfo()}</span>
</div>
</div>
{/* Arrow indicator */}
<div className="flex-shrink-0 mt-2 opacity-0 group-hover:opacity-100 transition-opacity">
<ChevronRight className="w-4 h-4 text-[var(--primary)]" />
</div>
</div>
);
}

View File

@@ -65,7 +65,7 @@ const SearchInput = forwardRef<HTMLInputElement, SearchInputProps>(
value={localValue} value={localValue}
onChange={handleChange} onChange={handleChange}
placeholder={placeholder} placeholder={placeholder}
className="bg-[var(--card)] border-[var(--border)] w-full" className="bg-[var(--card)] border-[var(--border)] w-full h-[34px]"
{...props} {...props}
/> />
</div> </div>

View File

@@ -13,17 +13,17 @@ const ToggleButton = forwardRef<HTMLButtonElement, ToggleButtonProps>(
({ className, variant = 'primary', size = 'md', isActive = false, icon, count, children, ...props }, ref) => { ({ className, variant = 'primary', size = 'md', isActive = false, icon, count, children, ...props }, ref) => {
const variants = { const variants = {
primary: isActive primary: isActive
? 'bg-[var(--primary)]/20 text-[var(--primary)] border border-[var(--primary)]/30' ? 'bg-[color-mix(in_srgb,var(--primary)_15%,transparent)] text-[var(--foreground)] border border-[var(--primary)]'
: 'bg-[var(--card)] text-[var(--muted-foreground)] border border-[var(--border)] hover:border-[var(--primary)]/50', : 'bg-[var(--card)] text-[var(--muted-foreground)] border border-[var(--border)] hover:border-[var(--primary)]/50',
accent: isActive accent: isActive
? 'bg-[var(--accent)]/20 text-[var(--accent)] border border-[var(--accent)]/30' ? 'bg-[var(--accent)]/20 text-[var(--accent)] border border-[var(--accent)]/30'
: 'bg-[var(--card)] text-[var(--muted-foreground)] border border-[var(--border)] hover:border-[var(--accent)]/50', : 'bg-[var(--card)] text-[var(--muted-foreground)] border border-[var(--border)] hover:border-[var(--accent)]/50',
secondary: isActive secondary: isActive
? 'bg-[var(--secondary)]/20 text-[var(--secondary)] border border-[var(--secondary)]/30' ? 'bg-[color-mix(in_srgb,var(--primary)_15%,transparent)] text-[var(--foreground)] border border-[var(--primary)]'
: 'bg-[var(--card)] text-[var(--muted-foreground)] border border-[var(--border)] hover:border-[var(--secondary)]/50', : 'bg-[var(--card)] text-[var(--muted-foreground)] border border-[var(--border)] hover:border-[var(--primary)]/50',
warning: isActive warning: isActive
? 'bg-[var(--warning)]/20 text-[var(--warning)] border border-[var(--warning)]/30' ? 'bg-[color-mix(in_srgb,var(--primary)_15%,transparent)] text-[var(--foreground)] border border-[var(--primary)]'
: 'bg-[var(--card)] text-[var(--muted-foreground)] border border-[var(--border)] hover:border-[var(--warning)]/50', : 'bg-[var(--card)] text-[var(--muted-foreground)] border border-[var(--border)] hover:border-[var(--primary)]/50',
cyan: isActive cyan: isActive
? 'bg-[var(--cyan)]/20 text-[var(--cyan)] border border-[var(--cyan)]/30' ? 'bg-[var(--cyan)]/20 text-[var(--cyan)] border border-[var(--cyan)]/30'
: 'bg-[var(--card)] text-[var(--muted-foreground)] border border-[var(--border)] hover:border-[var(--cyan)]/50' : 'bg-[var(--card)] text-[var(--muted-foreground)] border border-[var(--border)] hover:border-[var(--cyan)]/50'
@@ -33,8 +33,8 @@ const ToggleButton = forwardRef<HTMLButtonElement, ToggleButtonProps>(
const isIconOnly = icon && !children && count === undefined; const isIconOnly = icon && !children && count === undefined;
const sizes = { const sizes = {
sm: isIconOnly ? 'px-2 py-1.5 text-sm' : 'px-3 py-1.5 text-sm', sm: isIconOnly ? 'px-2 py-[5px] text-sm h-[34px]' : 'px-3 py-[5px] text-sm h-[34px]',
md: isIconOnly ? 'px-2 py-1.5 text-sm' : 'px-3 py-1.5 text-sm' md: isIconOnly ? 'px-2 py-[5px] text-sm h-[34px]' : 'px-3 py-[5px] text-sm h-[34px]'
}; };
return ( return (

View File

@@ -11,7 +11,6 @@ export { StatCard } from './StatCard';
export { ProgressBar } from './ProgressBar'; export { ProgressBar } from './ProgressBar';
export { ActionCard } from './ActionCard'; export { ActionCard } from './ActionCard';
export { TaskCard } from './TaskCard'; export { TaskCard } from './TaskCard';
export { RecentTaskTimeline } from './RecentTaskTimeline';
export { MetricCard } from './MetricCard'; export { MetricCard } from './MetricCard';
// Composants Kanban // Composants Kanban