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
|
### 🎨 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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}`;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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}
|
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>
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user