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:
16
TODO.md
16
TODO.md
@@ -10,12 +10,12 @@
|
||||
|
||||
### 🎨 Design et Interface
|
||||
- [X] **Homepage cards** : toute en variant glass
|
||||
- [ ] **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
|
||||
- [ ] **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.
|
||||
- [ ] **Tâches récentes** - Revoir l'affichage et la logique des tâches récentes
|
||||
- [ ] **Header dépasse en tablet** - Corriger le débordement du header sur tablette
|
||||
- [ ] **Icônes agenda et filtres** - Améliorer les icônes de l'agenda et des filtres dans desktop controls (utiliser une lib)
|
||||
- [X] **Icône Kanban homepage** - Changer icône sur la page d'accueil, pas lisible (utiliser une lib)
|
||||
- [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 -->
|
||||
- [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 -->
|
||||
- [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 -->
|
||||
- [x] **Header dépasse en tablet** - Corriger le débordement du header sur tablette <!-- Responsive amélioré, taille réglée, navigation adaptative -->
|
||||
- [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
|
||||
- [ ] **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
|
||||
@@ -29,13 +29,13 @@
|
||||
- [ ] **Deux modales** - Problème de duplication de modales
|
||||
- [ ] **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
|
||||
- [ ] **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
|
||||
- [ ] **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
|
||||
- [ ] **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
|
||||
- [ ] **É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
|
||||
- [ ] **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
|
||||
|
||||
@@ -63,7 +63,7 @@ export function PriorityDistributionChart({ data, title = "Distribution des Prio
|
||||
return (
|
||||
<Card variant="glass" className="p-6">
|
||||
<h3 className="text-lg font-semibold mb-4">{title}</h3>
|
||||
<div className="h-64">
|
||||
<div className="h-70">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart>
|
||||
<Pie
|
||||
|
||||
@@ -168,9 +168,9 @@ export function IntegrationFilter({
|
||||
} else if (activeFilters.length === 1) {
|
||||
const source = activeFilters[0];
|
||||
const mode = getSourceMode(source.id);
|
||||
return mode === 'show' ? `Seulement ${source.label}` : `Sans ${source.label}`;
|
||||
return mode === 'show' ? `Solo ${source.label}` : `-${source.label}`;
|
||||
} else {
|
||||
return `${activeFilters.length} filtres actifs`;
|
||||
return `+${activeFilters.length}`;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -268,7 +268,8 @@ export function IntegrationFilter({
|
||||
variant={getMainButtonVariant()}
|
||||
content={dropdownContent}
|
||||
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"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
import { Task } from '@/lib/types';
|
||||
import { Card } from '@/components/ui/Card';
|
||||
import { RecentTaskTimeline } from '@/components/ui/RecentTaskTimeline';
|
||||
import { TaskCard } from '@/components/ui/TaskCard';
|
||||
import { useTasksContext } from '@/contexts/TasksContext';
|
||||
import Link from 'next/link';
|
||||
import { Clipboard } from 'lucide-react';
|
||||
import { Clipboard, Clock } from 'lucide-react';
|
||||
|
||||
interface RecentTasksProps {
|
||||
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
|
||||
.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);
|
||||
|
||||
|
||||
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">
|
||||
<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">
|
||||
<button className="text-sm text-[var(--primary)] hover:underline">
|
||||
Voir toutes
|
||||
<button className="text-sm text-[var(--primary)] hover:text-[var(--primary)]/80 hover:underline font-medium transition-colors">
|
||||
Voir toutes →
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{recentTasks.length === 0 ? (
|
||||
<div className="text-center py-8 text-[var(--muted-foreground)]">
|
||||
<Clipboard className="w-12 h-12 mx-auto mb-3 opacity-50" />
|
||||
<p>Aucune tâche disponible</p>
|
||||
<p className="text-sm">Créez votre première tâche pour commencer</p>
|
||||
<div className="text-center py-6 sm:py-8 text-[var(--muted-foreground)]">
|
||||
<Clipboard className="w-8 h-8 sm:w-12 sm:h-12 mx-auto mb-3 opacity-50" />
|
||||
<p className="text-sm sm:text-base">Aucune tâche récente</p>
|
||||
<p className="text-xs sm:text-sm opacity-75">Créez une nouvelle tâche pour commencer</p>
|
||||
</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) => (
|
||||
<RecentTaskTimeline
|
||||
<TaskCard
|
||||
key={task.id}
|
||||
variant="detailed"
|
||||
title={task.title}
|
||||
description={task.description}
|
||||
status={task.status}
|
||||
@@ -66,13 +86,10 @@ export function RecentTasks({ tasks, selectedSources = [], hiddenSources = [] }:
|
||||
tags={task.tags || []}
|
||||
dueDate={task.dueDate}
|
||||
completedAt={task.completedAt}
|
||||
updatedAt={task.updatedAt}
|
||||
source={task.source || 'manual'}
|
||||
jiraKey={task.jiraKey}
|
||||
tfsPullRequestId={task.tfsPullRequestId}
|
||||
availableTags={availableTags}
|
||||
onClick={() => {
|
||||
// Navigation vers le kanban avec la tâche sélectionnée
|
||||
window.location.href = `/kanban?taskId=${task.id}`;
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -10,31 +10,33 @@ interface TagDistributionChartProps {
|
||||
}
|
||||
|
||||
export function TagDistributionChart({ metrics, className }: TagDistributionChartProps) {
|
||||
|
||||
|
||||
// Préparer les données pour le graphique en camembert
|
||||
const pieData = metrics.tagDistribution.slice(0, 8).map((tag) => ({
|
||||
name: tag.tagName,
|
||||
value: tag.count,
|
||||
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)
|
||||
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,
|
||||
usage: tag.usage,
|
||||
completionRate: tag.completionRate,
|
||||
avgPriority: tag.avgPriority,
|
||||
color: tag.tagColor
|
||||
color: tag.tagColor // Garder la couleur originale du tag
|
||||
}));
|
||||
|
||||
// Tooltip personnalisé pour le camembert
|
||||
const PieTooltip = ({ active, payload }: { active?: boolean; payload?: Array<{ payload: { name: string; value: number; percentage: number } }> }) => {
|
||||
// Tooltip personnalisé pour les tags (distribution)
|
||||
const TagTooltip = ({ active, payload }: { active?: boolean; payload?: Array<{ payload: { name: string; value: number; percentage: number } }> }) => {
|
||||
if (active && payload && payload.length) {
|
||||
const data = payload[0].payload;
|
||||
return (
|
||||
<div className="bg-[var(--card)] border border-[var(--border)] rounded-lg p-3 shadow-lg">
|
||||
<p className="font-medium mb-1">{data.name}</p>
|
||||
<p className="text-sm text-[var(--foreground)]">
|
||||
<div className="bg-[var(--card)] border border-[var(--border)] rounded-lg p-4 shadow-xl">
|
||||
<p className="font-semibold mb-2 text-[var(--foreground)]">{data.name}</p>
|
||||
<p className="text-sm text-[var(--muted-foreground)]">
|
||||
{data.value} tâches ({data.percentage.toFixed(1)}%)
|
||||
</p>
|
||||
</div>
|
||||
@@ -48,12 +50,12 @@ export function TagDistributionChart({ metrics, className }: TagDistributionChar
|
||||
if (active && payload && payload.length) {
|
||||
const data = payload[0].payload;
|
||||
return (
|
||||
<div className="bg-[var(--card)] border border-[var(--border)] rounded-lg p-3 shadow-lg">
|
||||
<p className="font-medium mb-1">{data.name}</p>
|
||||
<p className="text-sm text-[var(--foreground)]">
|
||||
<div className="bg-[var(--card)] border border-[var(--border)] rounded-lg p-4 shadow-xl">
|
||||
<p className="font-semibold mb-2 text-[var(--foreground)]">{data.name}</p>
|
||||
<p className="text-sm text-[var(--muted-foreground)] mb-1">
|
||||
{data.usage} tâches
|
||||
</p>
|
||||
<p className="text-sm text-[var(--muted-foreground)]">
|
||||
<p className="text-xs text-[var(--muted-foreground)]">
|
||||
Taux de completion: {data.completionRate.toFixed(1)}%
|
||||
</p>
|
||||
</div>
|
||||
@@ -62,19 +64,17 @@ export function TagDistributionChart({ metrics, className }: TagDistributionChar
|
||||
return null;
|
||||
};
|
||||
|
||||
// Légende personnalisée
|
||||
const CustomLegend = ({ payload }: { payload?: Array<{ value: string; color: string }> }) => {
|
||||
if (!payload) return null;
|
||||
|
||||
// Légende personnalisée qui utilise directement les données du graphique
|
||||
const CustomLegend = () => {
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2 mt-4">
|
||||
{payload.map((entry, index) => (
|
||||
<div key={index} className="flex items-center gap-2 text-sm">
|
||||
<div className="flex flex-wrap gap-4 mt-6">
|
||||
{pieData.map((entry, index) => (
|
||||
<div key={index} className="flex items-center gap-3">
|
||||
<div
|
||||
className="w-3 h-3 rounded-full"
|
||||
className="w-4 h-4 rounded-full border border-[var(--border)]"
|
||||
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>
|
||||
@@ -83,56 +83,60 @@ export function TagDistributionChart({ metrics, className }: TagDistributionChar
|
||||
|
||||
return (
|
||||
<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 */}
|
||||
<Card variant="glass" className="p-6">
|
||||
<h3 className="text-lg font-semibold mb-4">🏷️ Distribution par Tags</h3>
|
||||
|
||||
<div className="h-64">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={pieData}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
labelLine={false}
|
||||
label={(props: PieLabelRenderProps) => {
|
||||
const { name, percent } = props;
|
||||
const percentValue = typeof percent === 'number' ? percent : 0;
|
||||
return percentValue > 0.05 ? `${name}: ${(percentValue * 100).toFixed(1)}%` : '';
|
||||
}}
|
||||
outerRadius={80}
|
||||
fill="#8884d8"
|
||||
dataKey="value"
|
||||
nameKey="name"
|
||||
>
|
||||
{pieData.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip content={<PieTooltip />} />
|
||||
<Legend content={<CustomLegend />} />
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</Card>
|
||||
<Card variant="glass" className="p-6">
|
||||
<h3 className="text-lg font-semibold mb-4 text-[var(--foreground)]">Distribution par Tags</h3>
|
||||
|
||||
<div className="h-60">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={pieData}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
labelLine={false}
|
||||
label={(props: PieLabelRenderProps) => {
|
||||
const { percent } = props;
|
||||
return typeof percent === 'number' && percent > 0.05 ? `${Math.round(percent * 100)}%` : '';
|
||||
}}
|
||||
outerRadius={80}
|
||||
fill="#8884d8"
|
||||
dataKey="value"
|
||||
nameKey="name"
|
||||
>
|
||||
{pieData.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip content={<TagTooltip />} />
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
|
||||
<CustomLegend />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Top tags - Barres */}
|
||||
<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%">
|
||||
<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)" />
|
||||
<XAxis
|
||||
dataKey="name"
|
||||
tick={{ fontSize: 12 }}
|
||||
tick={{ fontSize: 13, fill: 'var(--muted-foreground)' }}
|
||||
angle={-45}
|
||||
textAnchor="end"
|
||||
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 />} />
|
||||
<Bar
|
||||
dataKey="usage"
|
||||
@@ -149,8 +153,8 @@ export function TagDistributionChart({ metrics, className }: TagDistributionChar
|
||||
</div>
|
||||
|
||||
{/* Statistiques des tags */}
|
||||
<Card variant="glass" className="p-6 mt-6">
|
||||
<h3 className="text-lg font-semibold mb-4">📈 Statistiques des Tags</h3>
|
||||
<Card variant="glass" className="p-6 mt-8">
|
||||
<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="text-center">
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Button, ToggleButton, SearchInput, ControlPanel, ControlSection, Contro
|
||||
import { IntegrationFilter } from '@/components/dashboard/IntegrationFilter';
|
||||
import { FontSizeToggle } from '@/components/ui/FontSizeToggle';
|
||||
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 {
|
||||
showFilters: boolean;
|
||||
@@ -81,13 +81,17 @@ export function DesktopControls({
|
||||
{/* 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">
|
||||
{/* Section gauche : Recherche + Boutons principaux */}
|
||||
<ControlSection>
|
||||
{/* Champ de recherche */}
|
||||
<SearchInput
|
||||
value={localSearch}
|
||||
onChange={handleSearchChange}
|
||||
placeholder="Rechercher des tâches..."
|
||||
/>
|
||||
<ControlSection className="items-center gap-2">
|
||||
{/* Champ de recherche avec icône */}
|
||||
<div className="relative flex-1 min-w-0">
|
||||
<SearchInput
|
||||
value={localSearch}
|
||||
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>
|
||||
<ToggleButton
|
||||
@@ -95,7 +99,7 @@ export function DesktopControls({
|
||||
isActive={showFilters}
|
||||
count={activeFiltersCount}
|
||||
onClick={onToggleFilters}
|
||||
icon={<Filter className="w-4 h-4" />}
|
||||
icon={<Settings className="w-4 h-4" />}
|
||||
>
|
||||
Filtres
|
||||
</ToggleButton>
|
||||
@@ -113,14 +117,16 @@ export function DesktopControls({
|
||||
variant="cyan"
|
||||
isActive={kanbanFilters.showWithDueDate}
|
||||
onClick={handleDueDateFilterToggle}
|
||||
title={kanbanFilters.showWithDueDate ? "Afficher toutes les tâches" : "Afficher seulement les tâches avec date de fin"}
|
||||
icon={<Calendar className="w-4 h-4" />}
|
||||
/>
|
||||
title={kanbanFilters.showWithDueDate ? "Afficher toutes les tâches" : "Afficher seulement les tâches avec échéance"}
|
||||
icon={<Clock className="w-4 h-4" />}
|
||||
>
|
||||
Échéance
|
||||
</ToggleButton>
|
||||
</ControlGroup>
|
||||
</ControlSection>
|
||||
|
||||
{/* 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">
|
||||
{/* Raccourcis Sources (Jira & TFS) */}
|
||||
<IntegrationFilter
|
||||
@@ -134,6 +140,7 @@ export function DesktopControls({
|
||||
onClick={onToggleCompactView}
|
||||
title={compactView ? "Vue détaillée" : "Vue compacte"}
|
||||
icon={compactView ? <List className="w-4 h-4" /> : <Grid3X3 className="w-4 h-4" />}
|
||||
className="h-[33px]"
|
||||
>
|
||||
{compactView ? 'Détaillée' : 'Compacte'}
|
||||
</ToggleButton>
|
||||
@@ -144,6 +151,7 @@ export function DesktopControls({
|
||||
onClick={onToggleSwimlanes}
|
||||
title={swimlanesByTags ? "Vue standard" : "Vue swimlanes"}
|
||||
icon={swimlanesByTags ? <Layout className="w-4 h-4" /> : <Grid3X3 className="w-4 h-4" />}
|
||||
className="h-[33px]"
|
||||
>
|
||||
{swimlanesByTags ? 'Standard' : 'Swimlanes'}
|
||||
</ToggleButton>
|
||||
@@ -155,9 +163,8 @@ export function DesktopControls({
|
||||
{/* Bouton d'ajout de tâche */}
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
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" />
|
||||
Nouvelle tâche
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Button, ToggleButton, ControlPanel } from '@/components/ui';
|
||||
import { IntegrationFilter } from '@/components/dashboard/IntegrationFilter';
|
||||
import { FontSizeToggle } from '@/components/ui/FontSizeToggle';
|
||||
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 {
|
||||
showFilters: boolean;
|
||||
@@ -77,7 +77,7 @@ export function MobileControls({
|
||||
onToggleFilters();
|
||||
setIsMenuOpen(false);
|
||||
}}
|
||||
icon={<Filter className="w-4 h-4" />}
|
||||
icon={<Settings className="w-4 h-4" />}
|
||||
>
|
||||
Filtres
|
||||
</ToggleButton>
|
||||
|
||||
@@ -9,7 +9,6 @@ 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 { RecentTaskTimeline } from '@/components/ui/RecentTaskTimeline';
|
||||
import { AchievementData } from '@/components/ui/AchievementCard';
|
||||
import { ChallengeData } from '@/components/ui/ChallengeCard';
|
||||
|
||||
@@ -226,52 +225,6 @@ export function CardsSection() {
|
||||
</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 */}
|
||||
<div className="space-y-4">
|
||||
|
||||
@@ -42,6 +42,8 @@ interface DropdownProps {
|
||||
className?: string;
|
||||
/** Classe CSS additionnelle pour le contenu */
|
||||
contentClassName?: string;
|
||||
/** Classe CSS additionnelle pour le bouton trigger */
|
||||
triggerClassName?: string;
|
||||
/** Callback quand le dropdown s'ouvre */
|
||||
onOpen?: () => void;
|
||||
/** Callback quand le dropdown se ferme */
|
||||
@@ -66,6 +68,7 @@ export function Dropdown({
|
||||
zIndex = 9999,
|
||||
className,
|
||||
contentClassName,
|
||||
triggerClassName,
|
||||
onOpen,
|
||||
onClose,
|
||||
open: controlledOpen,
|
||||
@@ -252,6 +255,7 @@ export function Dropdown({
|
||||
className={cn(
|
||||
'flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-md transition-colors',
|
||||
getVariantStyles(),
|
||||
triggerClassName,
|
||||
disabled && 'opacity-50 cursor-not-allowed'
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -22,6 +22,7 @@ export function Header({ title = "TowerControl", subtitle = "Task Management", s
|
||||
const { isConfigured: isJiraConfigured, config: jiraConfig } = useJiraConfig();
|
||||
const pathname = usePathname();
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
const [tabletMenuOpen, setTabletMenuOpen] = useState(false);
|
||||
const [themeDropdownOpen, setThemeDropdownOpen] = useState(false);
|
||||
const { openModal: openShortcutsModal } = useKeyboardShortcutsModal();
|
||||
const { data: session } = useSession();
|
||||
@@ -171,44 +172,97 @@ export function Header({ title = "TowerControl", subtitle = "Task Management", s
|
||||
|
||||
</div>
|
||||
|
||||
{/* Auth controls à droite mobile */}
|
||||
<div className="flex items-center gap-1">
|
||||
{/* Auth controls à droite mobile - dans la ligne principale */}
|
||||
<div className="hidden sm:block">
|
||||
<AuthButton />
|
||||
</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 */}
|
||||
<div className="hidden lg:flex items-center justify-between gap-6">
|
||||
{/* Titre et status */}
|
||||
<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 ${
|
||||
syncing
|
||||
? 'bg-yellow-400 animate-spin shadow-yellow-400/50'
|
||||
: 'bg-cyan-400 animate-pulse shadow-cyan-400/50'
|
||||
}`}></div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-mono font-bold text-[var(--foreground)] tracking-wider">
|
||||
{title}
|
||||
<div className="min-w-0">
|
||||
<h1 className="text-xl xl:text-2xl font-mono font-bold text-[var(--foreground)] tracking-wider truncate">
|
||||
<span className="sm:hidden">{title}</span>
|
||||
<span className="hidden sm:inline">{title}</span>
|
||||
</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}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navigation desktop */}
|
||||
<nav className="flex items-center gap-2">
|
||||
{navLinks.map(({ href, label }) => (
|
||||
<nav className="flex items-center gap-1 xl:gap-2 flex-wrap">
|
||||
{navLinks.slice(0, 4).map(({ href, label }) => (
|
||||
<Link
|
||||
key={href}
|
||||
href={href}
|
||||
className={getLinkClasses(href)}
|
||||
className={`${getLinkClasses(href)} text-xs xl:text-sm`}
|
||||
>
|
||||
{label}
|
||||
</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 */}
|
||||
<button
|
||||
onClick={openShortcutsModal}
|
||||
@@ -311,7 +365,7 @@ export function Header({ title = "TowerControl", subtitle = "Task Management", s
|
||||
className={getMobileLinkClasses('/profile')}
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
>
|
||||
👤 Profil
|
||||
Profil
|
||||
</Link>
|
||||
|
||||
{/* 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"
|
||||
>
|
||||
🚪 Déconnexion
|
||||
Déconnexion
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -65,7 +65,7 @@ const SearchInput = forwardRef<HTMLInputElement, SearchInputProps>(
|
||||
value={localValue}
|
||||
onChange={handleChange}
|
||||
placeholder={placeholder}
|
||||
className="bg-[var(--card)] border-[var(--border)] w-full"
|
||||
className="bg-[var(--card)] border-[var(--border)] w-full h-[34px]"
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -13,17 +13,17 @@ const ToggleButton = forwardRef<HTMLButtonElement, ToggleButtonProps>(
|
||||
({ className, variant = 'primary', size = 'md', isActive = false, icon, count, children, ...props }, ref) => {
|
||||
const variants = {
|
||||
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',
|
||||
accent: isActive
|
||||
? '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',
|
||||
secondary: isActive
|
||||
? 'bg-[var(--secondary)]/20 text-[var(--secondary)] border border-[var(--secondary)]/30'
|
||||
: 'bg-[var(--card)] text-[var(--muted-foreground)] border border-[var(--border)] hover:border-[var(--secondary)]/50',
|
||||
? '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',
|
||||
warning: isActive
|
||||
? 'bg-[var(--warning)]/20 text-[var(--warning)] border border-[var(--warning)]/30'
|
||||
: 'bg-[var(--card)] text-[var(--muted-foreground)] border border-[var(--border)] hover:border-[var(--warning)]/50',
|
||||
? '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',
|
||||
cyan: isActive
|
||||
? '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'
|
||||
@@ -33,8 +33,8 @@ const ToggleButton = forwardRef<HTMLButtonElement, ToggleButtonProps>(
|
||||
const isIconOnly = icon && !children && count === undefined;
|
||||
|
||||
const sizes = {
|
||||
sm: isIconOnly ? 'px-2 py-1.5 text-sm' : 'px-3 py-1.5 text-sm',
|
||||
md: 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-[5px] text-sm h-[34px]' : 'px-3 py-[5px] text-sm h-[34px]'
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -11,7 +11,6 @@ export { StatCard } from './StatCard';
|
||||
export { ProgressBar } from './ProgressBar';
|
||||
export { ActionCard } from './ActionCard';
|
||||
export { TaskCard } from './TaskCard';
|
||||
export { RecentTaskTimeline } from './RecentTaskTimeline';
|
||||
export { MetricCard } from './MetricCard';
|
||||
|
||||
// Composants Kanban
|
||||
|
||||
Reference in New Issue
Block a user