feat: refactor theme management and enhance color customization
- Cleaned up theme architecture by consolidating CSS variables and removing redundant theme applications, ensuring a single source of truth for theming. - Implemented a dark mode override and improved color management using CSS variables for better customization. - Updated various components to utilize new color variables, enhancing maintainability and visual consistency across the application. - Added detailed tasks in TODO.md for future enhancements related to user preferences and color customization features.
This commit is contained in:
167
.cursor/rules/css-variables-theme.mdc
Normal file
167
.cursor/rules/css-variables-theme.mdc
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
---
|
||||||
|
alwaysApply: true
|
||||||
|
description: CSS Variables theme system best practices
|
||||||
|
---
|
||||||
|
|
||||||
|
# CSS Variables Theme System
|
||||||
|
|
||||||
|
## Core Principle: Pure CSS Variables for Theming
|
||||||
|
|
||||||
|
This project uses **CSS Variables exclusively** for theming. No Tailwind `dark:` classes or conditional CSS classes.
|
||||||
|
|
||||||
|
## ✅ Architecture Pattern
|
||||||
|
|
||||||
|
### CSS Structure
|
||||||
|
```css
|
||||||
|
:root {
|
||||||
|
/* Light theme (default values) */
|
||||||
|
--background: #f1f5f9;
|
||||||
|
--foreground: #0f172a;
|
||||||
|
--primary: #0891b2;
|
||||||
|
--success: #059669;
|
||||||
|
--destructive: #dc2626;
|
||||||
|
--accent: #d97706;
|
||||||
|
--purple: #8b5cf6;
|
||||||
|
--yellow: #eab308;
|
||||||
|
--green: #059669;
|
||||||
|
--blue: #2563eb;
|
||||||
|
--gray: #6b7280;
|
||||||
|
--gray-light: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
/* Dark theme (override values) */
|
||||||
|
--background: #1e293b;
|
||||||
|
--foreground: #f1f5f9;
|
||||||
|
--primary: #06b6d4;
|
||||||
|
--success: #10b981;
|
||||||
|
--destructive: #ef4444;
|
||||||
|
--accent: #f59e0b;
|
||||||
|
--purple: #8b5cf6;
|
||||||
|
--yellow: #eab308;
|
||||||
|
--green: #10b981;
|
||||||
|
--blue: #3b82f6;
|
||||||
|
--gray: #9ca3af;
|
||||||
|
--gray-light: #374151;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Theme Application
|
||||||
|
- **Single source of truth**: [ThemeContext.tsx](mdc:src/contexts/ThemeContext.tsx) applies theme class to `document.documentElement`
|
||||||
|
- **No duplication**: Theme is applied only once, not in multiple places
|
||||||
|
- **SSR safe**: Initial theme from server-side preferences
|
||||||
|
|
||||||
|
## ✅ Component Usage Patterns
|
||||||
|
|
||||||
|
### Correct: Using CSS Variables
|
||||||
|
```tsx
|
||||||
|
// ✅ GOOD: CSS Variables in className
|
||||||
|
<div className="bg-[var(--card)] text-[var(--foreground)] border-[var(--border)]">
|
||||||
|
|
||||||
|
// ✅ GOOD: CSS Variables in style prop
|
||||||
|
<div style={{ color: 'var(--primary)', backgroundColor: 'var(--card)' }}>
|
||||||
|
|
||||||
|
// ✅ GOOD: CSS Variables with color-mix for transparency
|
||||||
|
<div style={{
|
||||||
|
backgroundColor: 'color-mix(in srgb, var(--primary) 10%, transparent)',
|
||||||
|
borderColor: 'color-mix(in srgb, var(--primary) 20%, var(--border))'
|
||||||
|
}}>
|
||||||
|
```
|
||||||
|
|
||||||
|
### ❌ Forbidden: Tailwind Dark Mode Classes
|
||||||
|
```tsx
|
||||||
|
// ❌ BAD: Tailwind dark: classes
|
||||||
|
<div className="bg-white dark:bg-gray-800 text-black dark:text-white">
|
||||||
|
|
||||||
|
// ❌ BAD: Conditional classes
|
||||||
|
<div className={theme === 'dark' ? 'bg-gray-800' : 'bg-white'}>
|
||||||
|
|
||||||
|
// ❌ BAD: Hardcoded colors
|
||||||
|
<div className="bg-red-500 text-blue-600">
|
||||||
|
```
|
||||||
|
|
||||||
|
## ✅ Color System
|
||||||
|
|
||||||
|
### Semantic Color Tokens
|
||||||
|
- `--background`: Main background color
|
||||||
|
- `--foreground`: Main text color
|
||||||
|
- `--card`: Card/panel background
|
||||||
|
- `--card-hover`: Card hover state
|
||||||
|
- `--card-column`: Column background (darker than cards)
|
||||||
|
- `--border`: Border color
|
||||||
|
- `--input`: Input field background
|
||||||
|
- `--primary`: Primary brand color
|
||||||
|
- `--primary-foreground`: Text on primary background
|
||||||
|
- `--muted`: Muted text color
|
||||||
|
- `--muted-foreground`: Secondary text color
|
||||||
|
- `--accent`: Accent color (orange/amber)
|
||||||
|
- `--destructive`: Error/danger color (red)
|
||||||
|
- `--success`: Success color (green)
|
||||||
|
- `--purple`: Purple accent
|
||||||
|
- `--yellow`: Yellow accent
|
||||||
|
- `--green`: Green accent
|
||||||
|
- `--blue`: Blue accent
|
||||||
|
- `--gray`: Gray color
|
||||||
|
- `--gray-light`: Light gray background
|
||||||
|
|
||||||
|
### Color Mixing Patterns
|
||||||
|
```css
|
||||||
|
/* Background with transparency */
|
||||||
|
background-color: color-mix(in srgb, var(--primary) 10%, transparent);
|
||||||
|
|
||||||
|
/* Border with transparency */
|
||||||
|
border-color: color-mix(in srgb, var(--primary) 20%, var(--border));
|
||||||
|
|
||||||
|
/* Text with opacity */
|
||||||
|
color: color-mix(in srgb, var(--destructive) 80%, transparent);
|
||||||
|
```
|
||||||
|
|
||||||
|
## ✅ Theme Context Usage
|
||||||
|
|
||||||
|
### ThemeProvider Setup
|
||||||
|
```tsx
|
||||||
|
// In layout.tsx
|
||||||
|
<ThemeProvider initialTheme={initialPreferences.viewPreferences.theme}>
|
||||||
|
{children}
|
||||||
|
</ThemeProvider>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Component Usage
|
||||||
|
```tsx
|
||||||
|
import { useTheme } from '@/contexts/ThemeContext';
|
||||||
|
|
||||||
|
function MyComponent() {
|
||||||
|
const { theme, toggleTheme, setTheme } = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button onClick={toggleTheme}>
|
||||||
|
Switch to {theme === 'dark' ? 'light' : 'dark'} theme
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## ✅ Future Extensibility
|
||||||
|
|
||||||
|
This system is designed to support:
|
||||||
|
- **Custom color themes**: Easy to add new color variables
|
||||||
|
- **User preferences**: Colors can be dynamically changed
|
||||||
|
- **Theme presets**: Multiple predefined themes
|
||||||
|
- **Accessibility**: High contrast modes
|
||||||
|
|
||||||
|
## 🚨 Anti-patterns to Avoid
|
||||||
|
|
||||||
|
1. **Don't mix approaches**: Never use both CSS variables and Tailwind dark: classes
|
||||||
|
2. **Don't duplicate theme application**: Theme should be applied only in ThemeContext
|
||||||
|
3. **Don't hardcode colors**: Always use semantic color tokens
|
||||||
|
4. **Don't use conditional classes**: Use CSS variables instead
|
||||||
|
5. **Don't forget transparency**: Use `color-mix()` for semi-transparent colors
|
||||||
|
|
||||||
|
## 📁 Key Files
|
||||||
|
|
||||||
|
- [globals.css](mdc:src/app/globals.css) - CSS Variables definitions
|
||||||
|
- [ThemeContext.tsx](mdc:src/contexts/ThemeContext.tsx) - Theme management
|
||||||
|
- [UserPreferencesContext.tsx](mdc:src/contexts/UserPreferencesContext.tsx) - Preferences sync
|
||||||
|
- [layout.tsx](mdc:src/app/layout.tsx) - Theme provider setup
|
||||||
|
|
||||||
|
Remember: **CSS Variables are the single source of truth for theming. Keep it pure and consistent.**
|
||||||
33
TODO.md
33
TODO.md
@@ -1,9 +1,40 @@
|
|||||||
# TowerControl v2.0 - Gestionnaire de tâches moderne
|
# TowerControl v2.0 - Gestionnaire de tâches moderne
|
||||||
|
|
||||||
## Idées à developper
|
## Idées à developper
|
||||||
|
- [x] Refacto et intégration design : mode sombre et clair sont souvent mal généré par défaut <!-- Diagnostic terminé -->
|
||||||
- [ ] Personnalisation : couleurs
|
- [ ] Personnalisation : couleurs
|
||||||
- [ ] Optimisations Perf : requetes DB
|
- [ ] Optimisations Perf : requetes DB
|
||||||
- [ ] PWA et mode offline
|
- [ ] PWA et mode offline
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 **REFACTORING THÈME & PERSONNALISATION COULEURS**
|
||||||
|
|
||||||
|
### **Phase 1: Nettoyage Architecture Thème**
|
||||||
|
- [x] **Décider de la stratégie** : CSS Variables vs Tailwind Dark Mode vs Hybride <!-- CSS Variables choisi -->
|
||||||
|
- [x] **Configurer tailwind.config.js** avec `darkMode: 'class'` si nécessaire <!-- Annulé : CSS Variables pur -->
|
||||||
|
- [x] **Supprimer la double application** du thème (layout.tsx + ThemeContext + UserPreferencesContext) <!-- ThemeContext est maintenant la source unique -->
|
||||||
|
- [x] **Refactorer les CSS variables** : `:root` pour défaut, `.dark/.light` pour override <!-- Architecture CSS propre avec :root neutre -->
|
||||||
|
- [x] **Nettoyer les composants** : supprimer classes `dark:` hardcodées, utiliser uniquement CSS variables <!-- TERMINÉ : toutes les occurrences supprimées -->
|
||||||
|
- [ ] **Corriger les problèmes d'hydration** mismatch et flashs de thème
|
||||||
|
- [ ] **Créer un système de design cohérent** avec tokens de couleur
|
||||||
|
|
||||||
|
### **Phase 2: Système Couleurs Personnalisées**
|
||||||
|
- [ ] **Étendre le modèle UserPreferences** pour supporter des couleurs personnalisées
|
||||||
|
- [ ] **Créer un service de gestion** des couleurs personnalisées
|
||||||
|
- [ ] **Créer une interface de configuration** des couleurs personnalisées
|
||||||
|
- [ ] **Implémenter le système CSS** pour les couleurs personnalisées dynamiques
|
||||||
|
- [ ] **Créer un système de presets** de thèmes (Tech Dark, Corporate Light, etc.)
|
||||||
|
- [ ] **Ajouter la validation des contrastes** pour les couleurs personnalisées
|
||||||
|
- [ ] **Permettre export/import** des configurations de thème personnalisées
|
||||||
|
|
||||||
|
### **Problèmes identifiés actuellement :**
|
||||||
|
- ❌ Approche hybride incohérente (CSS Variables + Tailwind `dark:` + classes conditionnelles)
|
||||||
|
- ❌ Double application du thème (3 endroits différents)
|
||||||
|
- ❌ Pas de configuration Tailwind pour `darkMode`
|
||||||
|
- ❌ Hydration mismatch avec flashs
|
||||||
|
- ❌ CSS Variables mal optimisées (`:root` contient le thème sombre)
|
||||||
|
- ❌ Couleurs hardcodées dans certains composants
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +1,7 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
/* Dark theme (default) */
|
/* Valeurs par défaut (Light theme) */
|
||||||
--background: #1e293b; /* slate-800 - encore plus clair */
|
|
||||||
--foreground: #f1f5f9; /* slate-100 */
|
|
||||||
--card: #334155; /* slate-700 - beaucoup plus clair pour contraste fort */
|
|
||||||
--card-hover: #475569; /* slate-600 */
|
|
||||||
--card-column: #0f172a; /* slate-900 - plus foncé que les cartes */
|
|
||||||
--border: #64748b; /* slate-500 - encore plus clair */
|
|
||||||
--input: #334155; /* slate-700 - plus clair */
|
|
||||||
--primary: #06b6d4; /* cyan-500 */
|
|
||||||
--primary-foreground: #f1f5f9; /* slate-100 */
|
|
||||||
--muted: #64748b; /* slate-500 */
|
|
||||||
--muted-foreground: #94a3b8; /* slate-400 */
|
|
||||||
--accent: #f59e0b; /* amber-500 */
|
|
||||||
--destructive: #ef4444; /* red-500 */
|
|
||||||
--success: #10b981; /* emerald-500 */
|
|
||||||
}
|
|
||||||
|
|
||||||
.light {
|
|
||||||
/* Light theme */
|
|
||||||
--background: #f1f5f9; /* slate-100 */
|
--background: #f1f5f9; /* slate-100 */
|
||||||
--foreground: #0f172a; /* slate-900 */
|
--foreground: #0f172a; /* slate-900 */
|
||||||
--card: #ffffff; /* white */
|
--card: #ffffff; /* white */
|
||||||
@@ -34,6 +16,36 @@
|
|||||||
--accent: #d97706; /* amber-600 */
|
--accent: #d97706; /* amber-600 */
|
||||||
--destructive: #dc2626; /* red-600 */
|
--destructive: #dc2626; /* red-600 */
|
||||||
--success: #059669; /* emerald-600 */
|
--success: #059669; /* emerald-600 */
|
||||||
|
--purple: #8b5cf6; /* purple-500 */
|
||||||
|
--yellow: #eab308; /* yellow-500 */
|
||||||
|
--green: #059669; /* emerald-600 */
|
||||||
|
--blue: #2563eb; /* blue-600 */
|
||||||
|
--gray: #6b7280; /* gray-500 */
|
||||||
|
--gray-light: #e5e7eb; /* gray-200 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
/* Dark theme override */
|
||||||
|
--background: #1e293b; /* slate-800 - encore plus clair */
|
||||||
|
--foreground: #f1f5f9; /* slate-100 */
|
||||||
|
--card: #334155; /* slate-700 - beaucoup plus clair pour contraste fort */
|
||||||
|
--card-hover: #475569; /* slate-600 */
|
||||||
|
--card-column: #0f172a; /* slate-900 - plus foncé que les cartes */
|
||||||
|
--border: #64748b; /* slate-500 - encore plus clair */
|
||||||
|
--input: #334155; /* slate-700 - plus clair */
|
||||||
|
--primary: #06b6d4; /* cyan-500 */
|
||||||
|
--primary-foreground: #f1f5f9; /* slate-100 */
|
||||||
|
--muted: #64748b; /* slate-500 */
|
||||||
|
--muted-foreground: #94a3b8; /* slate-400 */
|
||||||
|
--accent: #f59e0b; /* amber-500 */
|
||||||
|
--destructive: #ef4444; /* red-500 */
|
||||||
|
--success: #10b981; /* emerald-500 */
|
||||||
|
--purple: #8b5cf6; /* purple-500 */
|
||||||
|
--yellow: #eab308; /* yellow-500 */
|
||||||
|
--green: #10b981; /* emerald-500 */
|
||||||
|
--blue: #3b82f6; /* blue-500 */
|
||||||
|
--gray: #9ca3af; /* gray-400 */
|
||||||
|
--gray-light: #374151; /* gray-700 */
|
||||||
}
|
}
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
@@ -100,16 +112,16 @@ body {
|
|||||||
|
|
||||||
.outline-card-purple {
|
.outline-card-purple {
|
||||||
@apply p-2.5 rounded-lg border transition-all hover:shadow-sm hover:scale-[1.01];
|
@apply p-2.5 rounded-lg border transition-all hover:shadow-sm hover:scale-[1.01];
|
||||||
color: #8b5cf6; /* purple-500 */
|
color: var(--purple);
|
||||||
background-color: color-mix(in srgb, #8b5cf6 8%, transparent);
|
background-color: color-mix(in srgb, var(--purple) 8%, transparent);
|
||||||
border-color: color-mix(in srgb, #8b5cf6 25%, var(--border));
|
border-color: color-mix(in srgb, var(--purple) 25%, var(--border));
|
||||||
}
|
}
|
||||||
|
|
||||||
.outline-card-yellow {
|
.outline-card-yellow {
|
||||||
@apply p-2.5 rounded-lg border transition-all hover:shadow-sm hover:scale-[1.01];
|
@apply p-2.5 rounded-lg border transition-all hover:shadow-sm hover:scale-[1.01];
|
||||||
color: #eab308; /* yellow-500 */
|
color: var(--yellow);
|
||||||
background-color: color-mix(in srgb, #eab308 8%, transparent);
|
background-color: color-mix(in srgb, var(--yellow) 8%, transparent);
|
||||||
border-color: color-mix(in srgb, #eab308 25%, var(--border));
|
border-color: color-mix(in srgb, var(--yellow) 25%, var(--border));
|
||||||
}
|
}
|
||||||
|
|
||||||
.outline-card-gray {
|
.outline-card-gray {
|
||||||
@@ -143,9 +155,9 @@ body {
|
|||||||
|
|
||||||
.outline-metric-purple {
|
.outline-metric-purple {
|
||||||
@apply text-center p-4 rounded-lg border transition-all hover:shadow-sm hover:scale-[1.01];
|
@apply text-center p-4 rounded-lg border transition-all hover:shadow-sm hover:scale-[1.01];
|
||||||
color: #8b5cf6; /* purple-500 */
|
color: var(--purple);
|
||||||
background-color: color-mix(in srgb, #8b5cf6 8%, transparent);
|
background-color: color-mix(in srgb, var(--purple) 8%, transparent);
|
||||||
border-color: color-mix(in srgb, #8b5cf6 25%, var(--border));
|
border-color: color-mix(in srgb, var(--purple) 25%, var(--border));
|
||||||
}
|
}
|
||||||
|
|
||||||
.outline-metric-gray {
|
.outline-metric-gray {
|
||||||
@@ -157,8 +169,8 @@ body {
|
|||||||
|
|
||||||
/* Animations tech */
|
/* Animations tech */
|
||||||
@keyframes glow {
|
@keyframes glow {
|
||||||
0%, 100% { box-shadow: 0 0 5px rgba(6, 182, 212, 0.3); }
|
0%, 100% { box-shadow: 0 0 5px var(--primary); }
|
||||||
50% { box-shadow: 0 0 20px rgba(6, 182, 212, 0.6); }
|
50% { box-shadow: 0 0 20px var(--primary); }
|
||||||
}
|
}
|
||||||
|
|
||||||
.animate-glow {
|
.animate-glow {
|
||||||
|
|||||||
@@ -262,7 +262,7 @@ export function JiraDashboardPageClient({ initialJiraConfig, initialAnalytics }:
|
|||||||
{error && (
|
{error && (
|
||||||
<Card className="mb-6 border-red-500/20 bg-red-500/10">
|
<Card className="mb-6 border-red-500/20 bg-red-500/10">
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<div className="flex items-center gap-2 text-red-600 dark:text-red-400">
|
<div className="flex items-center gap-2" style={{ color: 'var(--destructive)' }}>
|
||||||
<span>❌</span>
|
<span>❌</span>
|
||||||
<span>{error}</span>
|
<span>{error}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -273,7 +273,7 @@ export function JiraDashboardPageClient({ initialJiraConfig, initialAnalytics }:
|
|||||||
{exportError && (
|
{exportError && (
|
||||||
<Card className="mb-6 border-orange-500/20 bg-orange-500/10">
|
<Card className="mb-6 border-orange-500/20 bg-orange-500/10">
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<div className="flex items-center gap-2 text-orange-600 dark:text-orange-400">
|
<div className="flex items-center gap-2" style={{ color: 'var(--accent)' }}>
|
||||||
<span>⚠️</span>
|
<span>⚠️</span>
|
||||||
<span>Erreur d'export: {exportError}</span>
|
<span>Erreur d'export: {exportError}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export default async function RootLayout({
|
|||||||
const initialPreferences = await userPreferencesService.getAllPreferences();
|
const initialPreferences = await userPreferencesService.getAllPreferences();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang="en" className={initialPreferences.viewPreferences.theme}>
|
<html lang="fr">
|
||||||
<body
|
<body
|
||||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ export function BackupTimelineChart({ stats = [], className = '' }: BackupTimeli
|
|||||||
<div className={`
|
<div className={`
|
||||||
relative h-8 rounded border-2 transition-all duration-200 cursor-pointer flex items-center justify-center text-xs font-medium
|
relative h-8 rounded border-2 transition-all duration-200 cursor-pointer flex items-center justify-center text-xs font-medium
|
||||||
${stat.total === 0
|
${stat.total === 0
|
||||||
? 'bg-gray-50 dark:bg-gray-800 border-gray-200 dark:border-gray-700 text-gray-400'
|
? 'border-[var(--border)] text-[var(--muted-foreground)]'
|
||||||
: 'border-transparent'
|
: 'border-transparent'
|
||||||
}
|
}
|
||||||
`}>
|
`}>
|
||||||
@@ -152,58 +152,58 @@ export function BackupTimelineChart({ stats = [], className = '' }: BackupTimeli
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Légende claire */}
|
{/* Légende claire */}
|
||||||
<div className="mb-6 p-3 bg-gray-50 dark:bg-gray-800/50 rounded-lg">
|
<div className="mb-6 p-3 rounded-lg" style={{ backgroundColor: 'var(--card-hover)' }}>
|
||||||
<h4 className="text-sm font-medium mb-3 text-gray-700 dark:text-gray-300">Légende</h4>
|
<h4 className="text-sm font-medium mb-3 text-[var(--foreground)]">Légende</h4>
|
||||||
<div className="grid grid-cols-2 gap-3 text-sm">
|
<div className="grid grid-cols-2 gap-3 text-sm">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="w-6 h-6 bg-blue-500 rounded flex items-center justify-center text-white text-xs font-bold">15</div>
|
<div className="w-6 h-6 bg-blue-500 rounded flex items-center justify-center text-white text-xs font-bold">15</div>
|
||||||
<span className="text-gray-700 dark:text-gray-300">Manuel seul</span>
|
<span className="text-[var(--foreground)]">Manuel seul</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="w-6 h-6 bg-green-500 rounded flex items-center justify-center text-white text-xs font-bold">15</div>
|
<div className="w-6 h-6 bg-green-500 rounded flex items-center justify-center text-white text-xs font-bold">15</div>
|
||||||
<span className="text-gray-700 dark:text-gray-300">Auto seul</span>
|
<span className="text-[var(--foreground)]">Auto seul</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="w-6 h-6 bg-gradient-to-br from-blue-500 to-green-500 rounded flex items-center justify-center text-white text-xs font-bold">15</div>
|
<div className="w-6 h-6 bg-gradient-to-br from-blue-500 to-green-500 rounded flex items-center justify-center text-white text-xs font-bold">15</div>
|
||||||
<span className="text-gray-700 dark:text-gray-300">Manuel + Auto</span>
|
<span className="text-[var(--foreground)]">Manuel + Auto</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="w-6 h-6 bg-gray-200 dark:bg-gray-700 border-2 border-gray-300 dark:border-gray-600 rounded flex items-center justify-center text-gray-500 text-xs">15</div>
|
<div className="w-6 h-6 border-2 rounded flex items-center justify-center text-xs" style={{ backgroundColor: 'var(--gray-light)', borderColor: 'var(--border)', color: 'var(--muted-foreground)' }}>15</div>
|
||||||
<span className="text-gray-700 dark:text-gray-300">Aucune</span>
|
<span className="text-[var(--foreground)]">Aucune</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3 text-xs text-gray-600 dark:text-gray-400">
|
<div className="mt-3 text-xs text-[var(--muted-foreground)]">
|
||||||
💡 Le badge orange indique le nombre total quand > 1
|
💡 Le badge orange indique le nombre total quand > 1
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Statistiques résumées */}
|
{/* Statistiques résumées */}
|
||||||
<div className="grid grid-cols-3 gap-3 text-center">
|
<div className="grid grid-cols-3 gap-3 text-center">
|
||||||
<div className="p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
|
<div className="p-3 rounded-lg" style={{ backgroundColor: 'color-mix(in srgb, var(--blue) 10%, transparent)' }}>
|
||||||
<div className="text-xl font-bold text-blue-600">
|
<div className="text-xl font-bold" style={{ color: 'var(--blue)' }}>
|
||||||
{safeStats.reduce((sum, s) => sum + s.manual, 0)}
|
{safeStats.reduce((sum, s) => sum + s.manual, 0)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-blue-600 font-medium">Manuelles</div>
|
<div className="text-xs font-medium" style={{ color: 'var(--blue)' }}>Manuelles</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3 bg-green-50 dark:bg-green-900/20 rounded-lg">
|
<div className="p-3 rounded-lg" style={{ backgroundColor: 'color-mix(in srgb, var(--green) 10%, transparent)' }}>
|
||||||
<div className="text-xl font-bold text-green-600">
|
<div className="text-xl font-bold" style={{ color: 'var(--green)' }}>
|
||||||
{safeStats.reduce((sum, s) => sum + s.automatic, 0)}
|
{safeStats.reduce((sum, s) => sum + s.automatic, 0)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-green-600 font-medium">Automatiques</div>
|
<div className="text-xs font-medium" style={{ color: 'var(--green)' }}>Automatiques</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3 bg-purple-50 dark:bg-purple-900/20 rounded-lg">
|
<div className="p-3 rounded-lg" style={{ backgroundColor: 'color-mix(in srgb, var(--purple) 10%, transparent)' }}>
|
||||||
<div className="text-xl font-bold text-purple-600">
|
<div className="text-xl font-bold" style={{ color: 'var(--purple)' }}>
|
||||||
{safeStats.reduce((sum, s) => sum + s.total, 0)}
|
{safeStats.reduce((sum, s) => sum + s.total, 0)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-purple-600 font-medium">Total</div>
|
<div className="text-xs font-medium" style={{ color: 'var(--purple)' }}>Total</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -19,13 +19,13 @@ export function WeeklyActivityHeatmap({ data, className }: WeeklyActivityHeatmap
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Obtenir la couleur basée sur l'intensité
|
// Obtenir la couleur basée sur l'intensité
|
||||||
const getColorClass = (intensity: number) => {
|
const getColorStyle = (intensity: number) => {
|
||||||
if (intensity === 0) return 'bg-gray-100 dark:bg-gray-800';
|
if (intensity === 0) return { backgroundColor: 'var(--gray-light)' };
|
||||||
if (intensity < 0.2) return 'bg-green-100 dark:bg-green-900/30';
|
if (intensity < 0.2) return { backgroundColor: 'color-mix(in srgb, var(--green) 20%, transparent)' };
|
||||||
if (intensity < 0.4) return 'bg-green-200 dark:bg-green-800/50';
|
if (intensity < 0.4) return { backgroundColor: 'color-mix(in srgb, var(--green) 40%, transparent)' };
|
||||||
if (intensity < 0.6) return 'bg-green-300 dark:bg-green-700/70';
|
if (intensity < 0.6) return { backgroundColor: 'color-mix(in srgb, var(--green) 60%, transparent)' };
|
||||||
if (intensity < 0.8) return 'bg-green-400 dark:bg-green-600/80';
|
if (intensity < 0.8) return { backgroundColor: 'color-mix(in srgb, var(--green) 80%, transparent)' };
|
||||||
return 'bg-green-500 dark:bg-green-500';
|
return { backgroundColor: 'var(--green)' };
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -46,14 +46,15 @@ export function WeeklyActivityHeatmap({ data, className }: WeeklyActivityHeatmap
|
|||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
{data.map((day, index) => {
|
{data.map((day, index) => {
|
||||||
const intensity = getIntensity(day);
|
const intensity = getIntensity(day);
|
||||||
const colorClass = getColorClass(intensity);
|
const colorStyle = getColorStyle(intensity);
|
||||||
const totalActivity = day.completed + day.newTasks;
|
const totalActivity = day.completed + day.newTasks;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={index} className="text-center">
|
<div key={index} className="text-center">
|
||||||
{/* Carré de couleur */}
|
{/* Carré de couleur */}
|
||||||
<div
|
<div
|
||||||
className={`w-8 h-8 rounded ${colorClass} border border-[var(--border)] flex items-center justify-center transition-all hover:scale-110 cursor-help group relative`}
|
className="w-8 h-8 rounded border border-[var(--border)] flex items-center justify-center transition-all hover:scale-110 cursor-help group relative"
|
||||||
|
style={colorStyle}
|
||||||
title={`${day.dayName}: ${totalActivity} activités (${day.completed} complétées, ${day.newTasks} créées)`}
|
title={`${day.dayName}: ${totalActivity} activités (${day.completed} complétées, ${day.newTasks} créées)`}
|
||||||
>
|
>
|
||||||
{/* Tooltip au hover */}
|
{/* Tooltip au hover */}
|
||||||
@@ -87,12 +88,12 @@ export function WeeklyActivityHeatmap({ data, className }: WeeklyActivityHeatmap
|
|||||||
<div className="flex items-center justify-center gap-2 text-xs text-[var(--muted-foreground)]">
|
<div className="flex items-center justify-center gap-2 text-xs text-[var(--muted-foreground)]">
|
||||||
<span>Moins</span>
|
<span>Moins</span>
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
<div className="w-3 h-3 bg-gray-100 dark:bg-gray-800 border border-[var(--border)] rounded"></div>
|
<div className="w-3 h-3 border border-[var(--border)] rounded" style={{ backgroundColor: 'var(--gray-light)' }}></div>
|
||||||
<div className="w-3 h-3 bg-green-100 dark:bg-green-900/30 border border-[var(--border)] rounded"></div>
|
<div className="w-3 h-3 border border-[var(--border)] rounded" style={{ backgroundColor: 'color-mix(in srgb, var(--green) 20%, transparent)' }}></div>
|
||||||
<div className="w-3 h-3 bg-green-200 dark:bg-green-800/50 border border-[var(--border)] rounded"></div>
|
<div className="w-3 h-3 border border-[var(--border)] rounded" style={{ backgroundColor: 'color-mix(in srgb, var(--green) 40%, transparent)' }}></div>
|
||||||
<div className="w-3 h-3 bg-green-300 dark:bg-green-700/70 border border-[var(--border)] rounded"></div>
|
<div className="w-3 h-3 border border-[var(--border)] rounded" style={{ backgroundColor: 'color-mix(in srgb, var(--green) 60%, transparent)' }}></div>
|
||||||
<div className="w-3 h-3 bg-green-400 dark:bg-green-600/80 border border-[var(--border)] rounded"></div>
|
<div className="w-3 h-3 border border-[var(--border)] rounded" style={{ backgroundColor: 'color-mix(in srgb, var(--green) 80%, transparent)' }}></div>
|
||||||
<div className="w-3 h-3 bg-green-500 dark:bg-green-500 border border-[var(--border)] rounded"></div>
|
<div className="w-3 h-3 border border-[var(--border)] rounded" style={{ backgroundColor: 'var(--green)' }}></div>
|
||||||
</div>
|
</div>
|
||||||
<span>Plus</span>
|
<span>Plus</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export function CriticalDeadlinesCard({ overdue, critical, warning }: CriticalDe
|
|||||||
return {
|
return {
|
||||||
icon: '🔴',
|
icon: '🔴',
|
||||||
text: task.daysRemaining === -1 ? 'En retard de 1 jour' : `En retard de ${Math.abs(task.daysRemaining)} jours`,
|
text: task.daysRemaining === -1 ? 'En retard de 1 jour' : `En retard de ${Math.abs(task.daysRemaining)} jours`,
|
||||||
style: 'text-red-700 bg-red-50/40 border-red-200/60 dark:bg-red-950/20 dark:border-red-800/40 dark:text-red-300'
|
style: 'border-[var(--destructive)]/60'
|
||||||
};
|
};
|
||||||
} else if (task.urgencyLevel === 'critical') {
|
} else if (task.urgencyLevel === 'critical') {
|
||||||
return {
|
return {
|
||||||
@@ -35,13 +35,13 @@ export function CriticalDeadlinesCard({ overdue, critical, warning }: CriticalDe
|
|||||||
text: task.daysRemaining === 0 ? 'Échéance aujourd\'hui' :
|
text: task.daysRemaining === 0 ? 'Échéance aujourd\'hui' :
|
||||||
task.daysRemaining === 1 ? 'Échéance demain' :
|
task.daysRemaining === 1 ? 'Échéance demain' :
|
||||||
`Dans ${task.daysRemaining} jours`,
|
`Dans ${task.daysRemaining} jours`,
|
||||||
style: 'text-orange-700 bg-orange-50/40 border-orange-200/60 dark:bg-orange-950/20 dark:border-orange-800/40 dark:text-orange-300'
|
style: 'border-[var(--accent)]/60'
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
icon: '🟡',
|
icon: '🟡',
|
||||||
text: `Dans ${task.daysRemaining} jours`,
|
text: `Dans ${task.daysRemaining} jours`,
|
||||||
style: 'text-yellow-700 bg-yellow-50/40 border-yellow-200/60 dark:bg-yellow-950/20 dark:border-yellow-800/40 dark:text-yellow-300'
|
style: 'border-[var(--yellow)]/60'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -71,7 +71,7 @@ export function CriticalDeadlinesCard({ overdue, critical, warning }: CriticalDe
|
|||||||
<h3 className="text-lg font-semibold mb-4">Tâches Urgentes</h3>
|
<h3 className="text-lg font-semibold mb-4">Tâches Urgentes</h3>
|
||||||
<div className="text-center py-8">
|
<div className="text-center py-8">
|
||||||
<div className="text-4xl mb-2">🎉</div>
|
<div className="text-4xl mb-2">🎉</div>
|
||||||
<h4 className="text-lg font-medium text-green-600 dark:text-green-400 mb-2">Excellent !</h4>
|
<h4 className="text-lg font-medium mb-2" style={{ color: 'var(--green)' }}>Excellent !</h4>
|
||||||
<p className="text-sm text-[var(--muted-foreground)]">
|
<p className="text-sm text-[var(--muted-foreground)]">
|
||||||
Aucune tâche urgente ou critique
|
Aucune tâche urgente ou critique
|
||||||
</p>
|
</p>
|
||||||
@@ -89,7 +89,7 @@ export function CriticalDeadlinesCard({ overdue, critical, warning }: CriticalDe
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2 max-h-40 overflow-y-auto scrollbar-thin scrollbar-thumb-gray-300 dark:scrollbar-thumb-gray-600 scrollbar-track-transparent pr-2">
|
<div className="space-y-2 max-h-40 overflow-y-auto scrollbar-thin scrollbar-track-transparent pr-2" style={{ scrollbarColor: 'var(--muted) transparent' }}>
|
||||||
{urgentTasks.map((task) => {
|
{urgentTasks.map((task) => {
|
||||||
const urgencyStyle = getUrgencyStyle(task);
|
const urgencyStyle = getUrgencyStyle(task);
|
||||||
|
|
||||||
@@ -157,17 +157,17 @@ export function CriticalDeadlinesCard({ overdue, critical, warning }: CriticalDe
|
|||||||
<div className="pt-3 border-t border-[var(--border)] mt-4">
|
<div className="pt-3 border-t border-[var(--border)] mt-4">
|
||||||
<div className="flex flex-wrap gap-3 text-xs text-[var(--muted-foreground)] justify-center">
|
<div className="flex flex-wrap gap-3 text-xs text-[var(--muted-foreground)] justify-center">
|
||||||
{overdue.length > 0 && (
|
{overdue.length > 0 && (
|
||||||
<span className="text-red-600/80 dark:text-red-400/80 font-medium">
|
<span className="font-medium" style={{ color: 'var(--destructive)' }}>
|
||||||
{overdue.length} en retard
|
{overdue.length} en retard
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{critical.length > 0 && (
|
{critical.length > 0 && (
|
||||||
<span className="text-orange-600/80 dark:text-orange-400/80 font-medium">
|
<span className="font-medium" style={{ color: 'var(--accent)' }}>
|
||||||
{critical.length} critique{critical.length > 1 ? 's' : ''}
|
{critical.length} critique{critical.length > 1 ? 's' : ''}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{warning.length > 0 && (
|
{warning.length > 0 && (
|
||||||
<span className="text-yellow-600/80 dark:text-yellow-400/80 font-medium">
|
<span className="font-medium" style={{ color: 'var(--yellow)' }}>
|
||||||
{warning.length} attention
|
{warning.length} attention
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -22,21 +22,21 @@ export function DeadlineRiskCard({ metrics }: DeadlineRiskCardProps) {
|
|||||||
|
|
||||||
const getRiskColor = (level: string) => {
|
const getRiskColor = (level: string) => {
|
||||||
switch (level) {
|
switch (level) {
|
||||||
case 'critical': return 'text-red-600 dark:text-red-400';
|
case 'critical': return { color: 'var(--destructive)' };
|
||||||
case 'high': return 'text-orange-600 dark:text-orange-400';
|
case 'high': return { color: 'var(--accent)' };
|
||||||
case 'medium': return 'text-yellow-600 dark:text-yellow-400';
|
case 'medium': return { color: 'var(--yellow)' };
|
||||||
case 'low': return 'text-green-600 dark:text-green-400';
|
case 'low': return { color: 'var(--green)' };
|
||||||
default: return 'text-gray-600 dark:text-gray-400';
|
default: return { color: 'var(--muted-foreground)' };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getRiskBgColor = (level: string) => {
|
const getRiskBgColor = (level: string) => {
|
||||||
switch (level) {
|
switch (level) {
|
||||||
case 'critical': return 'bg-red-50/30 border-red-200/50 dark:bg-red-950/20 dark:border-red-800/30';
|
case 'critical': return { backgroundColor: 'color-mix(in srgb, var(--destructive) 10%, transparent)', borderColor: 'color-mix(in srgb, var(--destructive) 30%, var(--border))' };
|
||||||
case 'high': return 'bg-orange-50/30 border-orange-200/50 dark:bg-orange-950/20 dark:border-orange-800/30';
|
case 'high': return { backgroundColor: 'color-mix(in srgb, var(--accent) 10%, transparent)', borderColor: 'color-mix(in srgb, var(--accent) 30%, var(--border))' };
|
||||||
case 'medium': return 'bg-yellow-50/30 border-yellow-200/50 dark:bg-yellow-950/20 dark:border-yellow-800/30';
|
case 'medium': return { backgroundColor: 'color-mix(in srgb, var(--yellow) 10%, transparent)', borderColor: 'color-mix(in srgb, var(--yellow) 30%, var(--border))' };
|
||||||
case 'low': return 'bg-green-50/30 border-green-200/50 dark:bg-green-950/20 dark:border-green-800/30';
|
case 'low': return { backgroundColor: 'color-mix(in srgb, var(--green) 10%, transparent)', borderColor: 'color-mix(in srgb, var(--green) 30%, var(--border))' };
|
||||||
default: return 'bg-gray-50/30 border-gray-200/50 dark:bg-gray-950/20 dark:border-gray-800/30';
|
default: return { backgroundColor: 'color-mix(in srgb, var(--muted) 10%, transparent)', borderColor: 'color-mix(in srgb, var(--muted) 30%, var(--border))' };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ export function DeadlineRiskCard({ metrics }: DeadlineRiskCardProps) {
|
|||||||
<span className="text-2xl">{getRiskIcon(riskAnalysis.riskLevel)}</span>
|
<span className="text-2xl">{getRiskIcon(riskAnalysis.riskLevel)}</span>
|
||||||
<h3 className="text-lg font-semibold">Niveau de Risque</h3>
|
<h3 className="text-lg font-semibold">Niveau de Risque</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className={`text-3xl font-bold ${getRiskColor(riskAnalysis.riskLevel)}`}>
|
<div className="text-3xl font-bold" style={getRiskColor(riskAnalysis.riskLevel)}>
|
||||||
{riskAnalysis.riskScore}
|
{riskAnalysis.riskScore}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -69,11 +69,11 @@ export function DeadlineRiskCard({ metrics }: DeadlineRiskCardProps) {
|
|||||||
<div className="grid grid-cols-2 gap-2 text-sm">
|
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span className="text-[var(--muted-foreground)]">En retard:</span>
|
<span className="text-[var(--muted-foreground)]">En retard:</span>
|
||||||
<span className="font-medium text-red-600/80 dark:text-red-400/80">{metrics.summary.overdueCount}</span>
|
<span className="font-medium" style={{ color: 'var(--destructive)' }}>{metrics.summary.overdueCount}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span className="text-[var(--muted-foreground)]">Critique:</span>
|
<span className="text-[var(--muted-foreground)]">Critique:</span>
|
||||||
<span className="font-medium text-orange-600/80 dark:text-orange-400/80">{metrics.summary.criticalCount}</span>
|
<span className="font-medium" style={{ color: 'var(--accent)' }}>{metrics.summary.criticalCount}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -15,29 +15,29 @@ export function DeadlineSummaryCard({ metrics }: DeadlineSummaryCardProps) {
|
|||||||
label: 'En retard',
|
label: 'En retard',
|
||||||
count: summary.overdueCount,
|
count: summary.overdueCount,
|
||||||
icon: '⏰',
|
icon: '⏰',
|
||||||
color: 'text-red-600 dark:text-red-400',
|
color: 'var(--destructive)',
|
||||||
bgColor: 'bg-red-100/50 dark:bg-red-900/30'
|
bgColor: 'color-mix(in srgb, var(--destructive) 10%, transparent)'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Critique (0-2j)',
|
label: 'Critique (0-2j)',
|
||||||
count: summary.criticalCount,
|
count: summary.criticalCount,
|
||||||
icon: '🚨',
|
icon: '🚨',
|
||||||
color: 'text-orange-600 dark:text-orange-400',
|
color: 'var(--accent)',
|
||||||
bgColor: 'bg-orange-100/50 dark:bg-orange-900/30'
|
bgColor: 'color-mix(in srgb, var(--accent) 10%, transparent)'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Attention (3-7j)',
|
label: 'Attention (3-7j)',
|
||||||
count: summary.warningCount,
|
count: summary.warningCount,
|
||||||
icon: '⚠️',
|
icon: '⚠️',
|
||||||
color: 'text-yellow-600 dark:text-yellow-400',
|
color: 'var(--yellow)',
|
||||||
bgColor: 'bg-yellow-100/50 dark:bg-yellow-900/30'
|
bgColor: 'color-mix(in srgb, var(--yellow) 10%, transparent)'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'À venir (8-14j)',
|
label: 'À venir (8-14j)',
|
||||||
count: summary.upcomingCount,
|
count: summary.upcomingCount,
|
||||||
icon: '📅',
|
icon: '📅',
|
||||||
color: 'text-blue-600 dark:text-blue-400',
|
color: 'var(--blue)',
|
||||||
bgColor: 'bg-blue-100/50 dark:bg-blue-900/30'
|
bgColor: 'color-mix(in srgb, var(--blue) 10%, transparent)'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -54,12 +54,12 @@ export function DeadlineSummaryCard({ metrics }: DeadlineSummaryCardProps) {
|
|||||||
{summaryItems.map((item, index) => (
|
{summaryItems.map((item, index) => (
|
||||||
<div key={index} className="flex items-center justify-between">
|
<div key={index} className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className={`w-8 h-8 ${item.bgColor} rounded-full flex items-center justify-center text-sm`}>
|
<div className="w-8 h-8 rounded-full flex items-center justify-center text-sm" style={{ backgroundColor: item.bgColor }}>
|
||||||
{item.icon}
|
{item.icon}
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm font-medium">{item.label}</span>
|
<span className="text-sm font-medium">{item.label}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={`text-lg font-bold ${item.color}`}>
|
<div className="text-lg font-bold" style={{ color: item.color }}>
|
||||||
{item.count}
|
{item.count}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -73,7 +73,7 @@ export function DeadlineSummaryCard({ metrics }: DeadlineSummaryCardProps) {
|
|||||||
{summary.totalWithDeadlines - summary.overdueCount - summary.criticalCount}/{summary.totalWithDeadlines}
|
{summary.totalWithDeadlines - summary.overdueCount - summary.criticalCount}/{summary.totalWithDeadlines}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full bg-gray-200/50 dark:bg-gray-700/50 rounded-full h-2 mt-2">
|
<div className="w-full rounded-full h-2 mt-2" style={{ backgroundColor: 'var(--gray-light)' }}>
|
||||||
<div
|
<div
|
||||||
className="bg-green-500/80 h-2 rounded-full transition-all duration-300"
|
className="bg-green-500/80 h-2 rounded-full transition-all duration-300"
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -131,19 +131,19 @@ export function BurndownChart({ sprintHistory, className }: BurndownChartProps)
|
|||||||
{/* Légende visuelle */}
|
{/* Légende visuelle */}
|
||||||
<div className="mb-4 flex justify-center gap-6 text-sm">
|
<div className="mb-4 flex justify-center gap-6 text-sm">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="w-4 h-0.5 bg-green-600 dark:bg-green-500 border-dashed border-t-2 border-green-600 dark:border-green-500"></div>
|
<div className="w-4 h-0.5 border-dashed border-t-2" style={{ backgroundColor: 'var(--green)', borderColor: 'var(--green)' }}></div>
|
||||||
<span className="text-green-600 dark:text-green-500">Idéal</span>
|
<span style={{ color: 'var(--green)' }}>Idéal</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="w-4 h-0.5 bg-blue-600 dark:bg-blue-500"></div>
|
<div className="w-4 h-0.5" style={{ backgroundColor: 'var(--blue)' }}></div>
|
||||||
<span className="text-blue-600 dark:text-blue-500">Réel</span>
|
<span style={{ color: 'var(--blue)' }}>Réel</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Métriques */}
|
{/* Métriques */}
|
||||||
<div className="grid grid-cols-3 gap-4 text-center">
|
<div className="grid grid-cols-3 gap-4 text-center">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-medium text-green-500">
|
<div className="text-sm font-medium" style={{ color: 'var(--green)' }}>
|
||||||
{currentSprint.plannedPoints}
|
{currentSprint.plannedPoints}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-[var(--muted-foreground)]">
|
<div className="text-xs text-[var(--muted-foreground)]">
|
||||||
@@ -151,7 +151,7 @@ export function BurndownChart({ sprintHistory, className }: BurndownChartProps)
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-medium text-blue-500">
|
<div className="text-sm font-medium" style={{ color: 'var(--blue)' }}>
|
||||||
{currentSprint.completedPoints}
|
{currentSprint.completedPoints}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-[var(--muted-foreground)]">
|
<div className="text-xs text-[var(--muted-foreground)]">
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export function CollaborationMatrix({ analytics, className }: CollaborationMatri
|
|||||||
if (!isClient) {
|
if (!isClient) {
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
<div className="animate-pulse bg-gray-200 dark:bg-gray-700 rounded-lg h-96" />
|
<div className="animate-pulse rounded-lg h-96" style={{ backgroundColor: 'var(--gray-light)' }} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -96,9 +96,9 @@ const mostIsolated = collaborationData.reduce((max, current) =>
|
|||||||
// Couleur d'intensité
|
// Couleur d'intensité
|
||||||
const getIntensityColor = (intensity: 'low' | 'medium' | 'high') => {
|
const getIntensityColor = (intensity: 'low' | 'medium' | 'high') => {
|
||||||
switch (intensity) {
|
switch (intensity) {
|
||||||
case 'high': return 'bg-green-600 dark:bg-green-500';
|
case 'high': return { backgroundColor: 'var(--green)' };
|
||||||
case 'medium': return 'bg-yellow-600 dark:bg-yellow-500';
|
case 'medium': return { backgroundColor: 'var(--yellow)' };
|
||||||
case 'low': return 'bg-gray-500 dark:bg-gray-400';
|
case 'low': return { backgroundColor: 'var(--gray)' };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -117,10 +117,13 @@ const mostIsolated = collaborationData.reduce((max, current) =>
|
|||||||
<span className="text-xs text-[var(--muted-foreground)]">
|
<span className="text-xs text-[var(--muted-foreground)]">
|
||||||
Score: {person.collaborationScore}
|
Score: {person.collaborationScore}
|
||||||
</span>
|
</span>
|
||||||
<div className={`w-3 h-3 rounded-full ${
|
<div
|
||||||
person.isolation < 30 ? 'bg-green-600 dark:bg-green-500' :
|
className="w-3 h-3 rounded-full"
|
||||||
person.isolation < 60 ? 'bg-yellow-600 dark:bg-yellow-500' : 'bg-red-600 dark:bg-red-500'
|
style={{
|
||||||
}`} />
|
backgroundColor: person.isolation < 30 ? 'var(--green)' :
|
||||||
|
person.isolation < 60 ? 'var(--yellow)' : 'var(--destructive)'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
@@ -132,7 +135,7 @@ const mostIsolated = collaborationData.reduce((max, current) =>
|
|||||||
</span>
|
</span>
|
||||||
<div className="flex items-center gap-2 flex-shrink-0">
|
<div className="flex items-center gap-2 flex-shrink-0">
|
||||||
<span>{dep.sharedTickets} tickets</span>
|
<span>{dep.sharedTickets} tickets</span>
|
||||||
<div className={`w-2 h-2 rounded-full ${getIntensityColor(dep.intensity)}`} />
|
<div className="w-2 h-2 rounded-full" style={getIntensityColor(dep.intensity)} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
@@ -159,11 +162,11 @@ const mostIsolated = collaborationData.reduce((max, current) =>
|
|||||||
const ranges = [[0, 30], [30, 50], [50, 70], [70, 100]];
|
const ranges = [[0, 30], [30, 50], [50, 70], [70, 100]];
|
||||||
const [min, max] = ranges[index];
|
const [min, max] = ranges[index];
|
||||||
const count = collaborationData.filter(d => d.isolation >= min && d.isolation < max).length;
|
const count = collaborationData.filter(d => d.isolation >= min && d.isolation < max).length;
|
||||||
const colors = ['bg-green-600 dark:bg-green-500', 'bg-blue-600 dark:bg-blue-500', 'bg-yellow-600 dark:bg-yellow-500', 'bg-red-600 dark:bg-red-500'];
|
const colors = ['var(--green)', 'var(--blue)', 'var(--yellow)', 'var(--destructive)'];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={level} className="flex items-center gap-2 text-xs">
|
<div key={level} className="flex items-center gap-2 text-xs">
|
||||||
<div className={`w-3 h-3 rounded-sm ${colors[index]}`} />
|
<div className="w-3 h-3 rounded-sm" style={{ backgroundColor: colors[index] }} />
|
||||||
<span className="flex-1 truncate">{level}</span>
|
<span className="flex-1 truncate">{level}</span>
|
||||||
<span className="font-mono text-xs">{count}</span>
|
<span className="font-mono text-xs">{count}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -203,7 +206,7 @@ const mostIsolated = collaborationData.reduce((max, current) =>
|
|||||||
{ intensity: 'low' as const, label: 'Faible' }
|
{ intensity: 'low' as const, label: 'Faible' }
|
||||||
].map(item => (
|
].map(item => (
|
||||||
<div key={item.intensity} className="flex items-center gap-2 text-xs">
|
<div key={item.intensity} className="flex items-center gap-2 text-xs">
|
||||||
<div className={`w-2 h-2 rounded-full ${getIntensityColor(item.intensity)}`} />
|
<div className="w-2 h-2 rounded-full" style={getIntensityColor(item.intensity)} />
|
||||||
<span>{item.label}</span>
|
<span>{item.label}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -257,25 +260,25 @@ const mostIsolated = collaborationData.reduce((max, current) =>
|
|||||||
<h4 className="text-sm font-medium mb-2">Recommandations d'équipe</h4>
|
<h4 className="text-sm font-medium mb-2">Recommandations d'équipe</h4>
|
||||||
<div className="space-y-2 text-sm">
|
<div className="space-y-2 text-sm">
|
||||||
{avgIsolation > 60 && (
|
{avgIsolation > 60 && (
|
||||||
<div className="flex items-center gap-2 text-red-600 dark:text-red-400">
|
<div className="flex items-center gap-2" style={{ color: 'var(--destructive)' }}>
|
||||||
<span>⚠️</span>
|
<span>⚠️</span>
|
||||||
<span>Isolation élevée - Encourager le pair programming et les reviews croisées</span>
|
<span>Isolation élevée - Encourager le pair programming et les reviews croisées</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{avgIsolation < 30 && (
|
{avgIsolation < 30 && (
|
||||||
<div className="flex items-center gap-2 text-green-600 dark:text-green-400">
|
<div className="flex items-center gap-2" style={{ color: 'var(--green)' }}>
|
||||||
<span>✅</span>
|
<span>✅</span>
|
||||||
<span>Excellente collaboration - L'équipe travaille bien ensemble</span>
|
<span>Excellente collaboration - L'équipe travaille bien ensemble</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{mostIsolated && mostIsolated.isolation > 80 && (
|
{mostIsolated && mostIsolated.isolation > 80 && (
|
||||||
<div className="flex items-center gap-2 text-orange-600 dark:text-orange-400">
|
<div className="flex items-center gap-2" style={{ color: 'var(--accent)' }}>
|
||||||
<span>👥</span>
|
<span>👥</span>
|
||||||
<span>Attention à {mostIsolated.displayName} - Considérer du mentoring ou du binômage</span>
|
<span>Attention à {mostIsolated.displayName} - Considérer du mentoring ou du binômage</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{collaborationData.filter(d => d.dependencies.length === 0).length > 0 && (
|
{collaborationData.filter(d => d.dependencies.length === 0).length > 0 && (
|
||||||
<div className="flex items-center gap-2 text-blue-600 dark:text-blue-400">
|
<div className="flex items-center gap-2" style={{ color: 'var(--blue)' }}>
|
||||||
<span>🔗</span>
|
<span>🔗</span>
|
||||||
<span>Quelques membres travaillent en silo - Organiser des sessions de partage</span>
|
<span>Quelques membres travaillent en silo - Organiser des sessions de partage</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ export function JiraSync({ onSyncComplete, className = "" }: JiraSyncProps) {
|
|||||||
<CardHeader className="pb-3">
|
<CardHeader className="pb-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-2 h-2 rounded-full bg-blue-500 dark:bg-blue-400 animate-pulse"></div>
|
<div className="w-2 h-2 rounded-full animate-pulse" style={{ backgroundColor: 'var(--blue)' }}></div>
|
||||||
<h3 className="font-mono text-sm font-bold text-blue-400 uppercase tracking-wider">
|
<h3 className="font-mono text-sm font-bold text-blue-400 uppercase tracking-wider">
|
||||||
JIRA SYNC
|
JIRA SYNC
|
||||||
</h3>
|
</h3>
|
||||||
|
|||||||
@@ -205,31 +205,31 @@ export function PredictabilityMetrics({ sprintHistory, className }: Predictabili
|
|||||||
<h4 className="text-sm font-medium mb-2">Analyse de predictabilité</h4>
|
<h4 className="text-sm font-medium mb-2">Analyse de predictabilité</h4>
|
||||||
<div className="space-y-2 text-sm">
|
<div className="space-y-2 text-sm">
|
||||||
{averageAccuracy > 80 && (
|
{averageAccuracy > 80 && (
|
||||||
<div className="flex items-center gap-2 text-green-600 dark:text-green-400">
|
<div className="flex items-center gap-2" style={{ color: 'var(--green)' }}>
|
||||||
<span>✅</span>
|
<span>✅</span>
|
||||||
<span>Excellente predictabilité - L'équipe estime bien sa capacité</span>
|
<span>Excellente predictabilité - L'équipe estime bien sa capacité</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{averageAccuracy < 60 && (
|
{averageAccuracy < 60 && (
|
||||||
<div className="flex items-center gap-2 text-red-600 dark:text-red-400">
|
<div className="flex items-center gap-2" style={{ color: 'var(--destructive)' }}>
|
||||||
<span>⚠️</span>
|
<span>⚠️</span>
|
||||||
<span>Predictabilité faible - Revoir les méthodes d'estimation</span>
|
<span>Predictabilité faible - Revoir les méthodes d'estimation</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{averageVariance > 25 && (
|
{averageVariance > 25 && (
|
||||||
<div className="flex items-center gap-2 text-orange-600 dark:text-orange-400">
|
<div className="flex items-center gap-2" style={{ color: 'var(--accent)' }}>
|
||||||
<span>📊</span>
|
<span>📊</span>
|
||||||
<span>Variance élevée - Considérer des sprints plus courts ou un meilleur découpage</span>
|
<span>Variance élevée - Considérer des sprints plus courts ou un meilleur découpage</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{trend > 10 && (
|
{trend > 10 && (
|
||||||
<div className="flex items-center gap-2 text-green-600 dark:text-green-400">
|
<div className="flex items-center gap-2" style={{ color: 'var(--green)' }}>
|
||||||
<span>📈</span>
|
<span>📈</span>
|
||||||
<span>Tendance positive - L'équipe s'améliore dans ses estimations</span>
|
<span>Tendance positive - L'équipe s'améliore dans ses estimations</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{trend < -10 && (
|
{trend < -10 && (
|
||||||
<div className="flex items-center gap-2 text-red-600 dark:text-red-400">
|
<div className="flex items-center gap-2" style={{ color: 'var(--destructive)' }}>
|
||||||
<span>📉</span>
|
<span>📉</span>
|
||||||
<span>Tendance négative - Attention aux changements récents (équipe, processus)</span>
|
<span>Tendance négative - Attention aux changements récents (équipe, processus)</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -190,19 +190,19 @@ export function QualityMetrics({ analytics, className }: QualityMetricsProps) {
|
|||||||
<h4 className="text-sm font-medium mb-2">Analyse qualité</h4>
|
<h4 className="text-sm font-medium mb-2">Analyse qualité</h4>
|
||||||
<div className="space-y-2 text-sm">
|
<div className="space-y-2 text-sm">
|
||||||
{bugRatio > 25 && (
|
{bugRatio > 25 && (
|
||||||
<div className="flex items-center gap-2 text-red-600 dark:text-red-400">
|
<div className="flex items-center gap-2" style={{ color: 'var(--destructive)' }}>
|
||||||
<span>⚠️</span>
|
<span>⚠️</span>
|
||||||
<span>Ratio de bugs élevé ({bugRatio}%) - Attention à la dette technique</span>
|
<span>Ratio de bugs élevé ({bugRatio}%) - Attention à la dette technique</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{bugRatio <= 15 && (
|
{bugRatio <= 15 && (
|
||||||
<div className="flex items-center gap-2 text-green-600 dark:text-green-400">
|
<div className="flex items-center gap-2" style={{ color: 'var(--green)' }}>
|
||||||
<span>✅</span>
|
<span>✅</span>
|
||||||
<span>Excellent ratio de bugs ({bugRatio}%) - Bonne qualité du code</span>
|
<span>Excellent ratio de bugs ({bugRatio}%) - Bonne qualité du code</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{issueTypes.stories > issueTypes.bugs * 3 && (
|
{issueTypes.stories > issueTypes.bugs * 3 && (
|
||||||
<div className="flex items-center gap-2 text-blue-600 dark:text-blue-400">
|
<div className="flex items-center gap-2" style={{ color: 'var(--blue)' }}>
|
||||||
<span>🚀</span>
|
<span>🚀</span>
|
||||||
<span>Focus positif sur les fonctionnalités - Bon équilibre produit</span>
|
<span>Focus positif sur les fonctionnalités - Bon équilibre produit</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -138,16 +138,16 @@ export function ThroughputChart({ sprintHistory, className }: ThroughputChartPro
|
|||||||
{/* Légende visuelle */}
|
{/* Légende visuelle */}
|
||||||
<div className="mb-4 flex justify-center gap-6 text-sm">
|
<div className="mb-4 flex justify-center gap-6 text-sm">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="w-4 h-3 bg-blue-600 dark:bg-blue-500 rounded-sm"></div>
|
<div className="w-4 h-3 rounded-sm" style={{ backgroundColor: 'var(--blue)' }}></div>
|
||||||
<span className="text-blue-600 dark:text-blue-500">Points complétés</span>
|
<span style={{ color: 'var(--blue)' }}>Points complétés</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="w-4 h-0.5 bg-green-600 dark:bg-green-500"></div>
|
<div className="w-4 h-0.5" style={{ backgroundColor: 'var(--green)' }}></div>
|
||||||
<span className="text-green-600 dark:text-green-500">Throughput</span>
|
<span style={{ color: 'var(--green)' }}>Throughput</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="w-4 h-0.5 bg-orange-600 dark:bg-orange-500 border-dashed border-t-2 border-orange-600 dark:border-orange-500"></div>
|
<div className="w-4 h-0.5 border-dashed border-t-2" style={{ backgroundColor: 'var(--accent)', borderColor: 'var(--accent)' }}></div>
|
||||||
<span className="text-orange-600 dark:text-orange-500">Tendance</span>
|
<span style={{ color: 'var(--accent)' }}>Tendance</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -271,11 +271,14 @@ export default function BackupSettingsPageClient({ initialData }: BackupSettings
|
|||||||
if (!message) return null;
|
if (!message) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`text-xs mt-2 px-2 py-1 rounded transition-all inline-block ${
|
<div
|
||||||
message.type === 'success'
|
className="text-xs mt-2 px-2 py-1 rounded transition-all inline-block border"
|
||||||
? 'text-green-700 dark:text-green-300 bg-green-50 dark:bg-green-950/20 border border-green-200 dark:border-green-800/20'
|
style={{
|
||||||
: 'text-red-700 dark:text-red-300 bg-red-50 dark:bg-red-950/20 border border-red-200 dark:border-red-800/20'
|
color: message.type === 'success' ? 'var(--success)' : 'var(--destructive)',
|
||||||
}`}>
|
backgroundColor: message.type === 'success' ? 'color-mix(in srgb, var(--success) 10%, transparent)' : 'color-mix(in srgb, var(--destructive) 10%, transparent)',
|
||||||
|
borderColor: message.type === 'success' ? 'color-mix(in srgb, var(--success) 20%, var(--border))' : 'color-mix(in srgb, var(--destructive) 20%, var(--border))'
|
||||||
|
}}
|
||||||
|
>
|
||||||
{message.text}
|
{message.text}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -561,11 +564,13 @@ export default function BackupSettingsPageClient({ initialData }: BackupSettings
|
|||||||
<span className="text-xs text-[var(--muted-foreground)]">
|
<span className="text-xs text-[var(--muted-foreground)]">
|
||||||
{formatFileSize(backup.size)}
|
{formatFileSize(backup.size)}
|
||||||
</span>
|
</span>
|
||||||
<span className={`text-xs px-1.5 py-0.5 rounded ${
|
<span
|
||||||
backup.type === 'manual'
|
className="text-xs px-1.5 py-0.5 rounded"
|
||||||
? 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300'
|
style={{
|
||||||
: 'bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300'
|
color: backup.type === 'manual' ? 'var(--blue)' : 'var(--muted-foreground)',
|
||||||
}`}>
|
backgroundColor: backup.type === 'manual' ? 'color-mix(in srgb, var(--blue) 10%, transparent)' : 'color-mix(in srgb, var(--muted) 10%, transparent)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
{backup.type === 'manual' ? 'Manuel' : 'Auto'}
|
{backup.type === 'manual' ? 'Manuel' : 'Auto'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -341,8 +341,8 @@ export function JiraConfigForm() {
|
|||||||
{validationResult && (
|
{validationResult && (
|
||||||
<div className={`mt-2 p-2 rounded text-sm ${
|
<div className={`mt-2 p-2 rounded text-sm ${
|
||||||
validationResult.type === 'success'
|
validationResult.type === 'success'
|
||||||
? 'bg-green-50 border border-green-200 text-green-800 dark:bg-green-900/20 dark:border-green-800 dark:text-green-200'
|
? 'border border-[var(--success)]/20'
|
||||||
: 'bg-red-50 border border-red-200 text-red-800 dark:bg-red-900/20 dark:border-red-800 dark:text-red-200'
|
: 'border border-[var(--destructive)]/20'
|
||||||
}`}>
|
}`}>
|
||||||
{validationResult.text}
|
{validationResult.text}
|
||||||
</div>
|
</div>
|
||||||
@@ -433,11 +433,14 @@ export function JiraConfigForm() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{message && (
|
{message && (
|
||||||
<div className={`p-4 rounded border ${
|
<div
|
||||||
message.type === 'success'
|
className="p-4 rounded border"
|
||||||
? 'bg-green-50 border-green-200 text-green-800 dark:bg-green-900/20 dark:border-green-800 dark:text-green-200'
|
style={{
|
||||||
: 'bg-red-50 border-red-200 text-red-800 dark:bg-red-900/20 dark:border-red-800 dark:text-red-200'
|
color: message.type === 'success' ? 'var(--success)' : 'var(--destructive)',
|
||||||
}`}>
|
backgroundColor: message.type === 'success' ? 'color-mix(in srgb, var(--success) 10%, transparent)' : 'color-mix(in srgb, var(--destructive) 10%, transparent)',
|
||||||
|
borderColor: message.type === 'success' ? 'color-mix(in srgb, var(--success) 20%, var(--border))' : 'color-mix(in srgb, var(--destructive) 20%, var(--border))'
|
||||||
|
}}
|
||||||
|
>
|
||||||
{message.text}
|
{message.text}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -339,16 +339,16 @@ export function TfsConfigForm() {
|
|||||||
|
|
||||||
{/* Actions de gestion des données TFS */}
|
{/* Actions de gestion des données TFS */}
|
||||||
{isTfsConfigured && (
|
{isTfsConfigured && (
|
||||||
<div className="p-4 bg-[var(--card)] rounded border border-orange-200 dark:border-orange-800">
|
<div className="p-4 bg-[var(--card)] rounded border" style={{ borderColor: 'color-mix(in srgb, var(--accent) 30%, var(--border))', backgroundColor: 'color-mix(in srgb, var(--accent) 5%, var(--card))', color: 'var(--accent)' }}>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-medium text-orange-800 dark:text-orange-200">
|
<h3 className="font-medium" style={{ color: 'var(--accent)' }}>
|
||||||
⚠️ Gestion des données
|
⚠️ Gestion des données
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-orange-600 dark:text-orange-300">
|
<p className="text-sm" style={{ color: 'var(--accent)' }}>
|
||||||
Supprimez toutes les tâches TFS synchronisées de la base locale
|
Supprimez toutes les tâches TFS synchronisées de la base locale
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-orange-500 dark:text-orange-400 mt-1">
|
<p className="text-xs mt-1" style={{ color: 'var(--accent)' }}>
|
||||||
<strong>Attention:</strong> Cette action est irréversible et
|
<strong>Attention:</strong> Cette action est irréversible et
|
||||||
supprimera définitivement toutes les tâches importées depuis
|
supprimera définitivement toutes les tâches importées depuis
|
||||||
Azure DevOps.
|
Azure DevOps.
|
||||||
@@ -624,11 +624,12 @@ export function TfsConfigForm() {
|
|||||||
|
|
||||||
{message && (
|
{message && (
|
||||||
<div
|
<div
|
||||||
className={`p-4 rounded border ${
|
className="p-4 rounded border"
|
||||||
message.type === 'success'
|
style={{
|
||||||
? 'bg-green-50 border-green-200 text-green-800 dark:bg-green-900/20 dark:border-green-800 dark:text-green-200'
|
color: message.type === 'success' ? 'var(--success)' : 'var(--destructive)',
|
||||||
: 'bg-red-50 border-red-200 text-red-800 dark:bg-red-900/20 dark:border-red-800 dark:text-red-200'
|
backgroundColor: message.type === 'success' ? 'color-mix(in srgb, var(--success) 10%, transparent)' : 'color-mix(in srgb, var(--destructive) 10%, transparent)',
|
||||||
}`}
|
borderColor: message.type === 'success' ? 'color-mix(in srgb, var(--success) 20%, var(--border))' : 'color-mix(in srgb, var(--destructive) 20%, var(--border))'
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{message.text}
|
{message.text}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -43,11 +43,9 @@ export function QuickActions({
|
|||||||
Créer une sauvegarde des données
|
Créer une sauvegarde des données
|
||||||
</p>
|
</p>
|
||||||
{messages.backup && (
|
{messages.backup && (
|
||||||
<p className={`text-xs mt-1 ${
|
<p className="text-xs mt-1" style={{
|
||||||
messages.backup.type === 'success'
|
color: messages.backup.type === 'success' ? 'var(--success)' : 'var(--destructive)'
|
||||||
? 'text-green-600 dark:text-green-400'
|
}}>
|
||||||
: 'text-red-600 dark:text-red-400'
|
|
||||||
}`}>
|
|
||||||
{messages.backup.text}
|
{messages.backup.text}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -72,11 +70,9 @@ export function QuickActions({
|
|||||||
Tester la connexion Jira
|
Tester la connexion Jira
|
||||||
</p>
|
</p>
|
||||||
{messages.jira && (
|
{messages.jira && (
|
||||||
<p className={`text-xs mt-1 ${
|
<p className="text-xs mt-1" style={{
|
||||||
messages.jira.type === 'success'
|
color: messages.jira.type === 'success' ? 'var(--success)' : 'var(--destructive)'
|
||||||
? 'text-green-600 dark:text-green-400'
|
}}>
|
||||||
: 'text-red-600 dark:text-red-400'
|
|
||||||
}`}>
|
|
||||||
{messages.jira.text}
|
{messages.jira.text}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -8,11 +8,10 @@ import {
|
|||||||
updateColumnVisibility as updateColumnVisibilityAction,
|
updateColumnVisibility as updateColumnVisibilityAction,
|
||||||
toggleObjectivesVisibility as toggleObjectivesVisibilityAction,
|
toggleObjectivesVisibility as toggleObjectivesVisibilityAction,
|
||||||
toggleObjectivesCollapse as toggleObjectivesCollapseAction,
|
toggleObjectivesCollapse as toggleObjectivesCollapseAction,
|
||||||
toggleTheme as toggleThemeAction,
|
|
||||||
setTheme as setThemeAction,
|
|
||||||
toggleFontSize as toggleFontSizeAction,
|
toggleFontSize as toggleFontSizeAction,
|
||||||
toggleColumnVisibility as toggleColumnVisibilityAction
|
toggleColumnVisibility as toggleColumnVisibilityAction
|
||||||
} from '@/actions/preferences';
|
} from '@/actions/preferences';
|
||||||
|
import { useTheme } from './ThemeContext';
|
||||||
|
|
||||||
interface UserPreferencesContextType {
|
interface UserPreferencesContextType {
|
||||||
preferences: UserPreferences;
|
preferences: UserPreferences;
|
||||||
@@ -77,14 +76,17 @@ const defaultPreferences: UserPreferences = {
|
|||||||
export function UserPreferencesProvider({ children, initialPreferences }: UserPreferencesProviderProps) {
|
export function UserPreferencesProvider({ children, initialPreferences }: UserPreferencesProviderProps) {
|
||||||
const [preferences, setPreferences] = useState<UserPreferences>(initialPreferences || defaultPreferences);
|
const [preferences, setPreferences] = useState<UserPreferences>(initialPreferences || defaultPreferences);
|
||||||
const [isPending, startTransition] = useTransition();
|
const [isPending, startTransition] = useTransition();
|
||||||
|
const { theme, toggleTheme: themeToggleTheme, setTheme: themeSetTheme } = useTheme();
|
||||||
|
|
||||||
// Synchroniser le thème avec le ThemeProvider global (si disponible)
|
// Synchroniser les préférences avec le thème actuel du ThemeContext
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window !== 'undefined') {
|
if (preferences.viewPreferences.theme !== theme) {
|
||||||
// Appliquer le thème au document
|
setPreferences(prev => ({
|
||||||
document.documentElement.className = preferences.viewPreferences.theme;
|
...prev,
|
||||||
|
viewPreferences: { ...prev.viewPreferences, theme }
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}, [preferences.viewPreferences.theme]);
|
}, [theme, preferences.viewPreferences.theme]);
|
||||||
|
|
||||||
// === KANBAN FILTERS ===
|
// === KANBAN FILTERS ===
|
||||||
|
|
||||||
@@ -149,16 +151,12 @@ export function UserPreferencesProvider({ children, initialPreferences }: UserPr
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const toggleTheme = useCallback(() => {
|
const toggleTheme = useCallback(() => {
|
||||||
startTransition(async () => {
|
themeToggleTheme();
|
||||||
await toggleThemeAction();
|
}, [themeToggleTheme]);
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const setTheme = useCallback((theme: 'light' | 'dark') => {
|
const setTheme = useCallback((theme: 'light' | 'dark') => {
|
||||||
startTransition(async () => {
|
themeSetTheme(theme);
|
||||||
await setThemeAction(theme);
|
}, [themeSetTheme]);
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const toggleFontSize = useCallback(() => {
|
const toggleFontSize = useCallback(() => {
|
||||||
startTransition(async () => {
|
startTransition(async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user