feat: overhaul TODO.md and enhance Kanban components
- Updated TODO.md to reflect the new project structure and phases, marking several tasks as completed. - Enhanced Kanban components with a tech-inspired design, including new styles for columns and task cards. - Removed the obsolete reminders service and task processor, streamlining the codebase for better maintainability. - Introduced a modern API for task management, including CRUD operations and improved error handling. - Updated global styles for a cohesive dark theme and added custom scrollbar styles.
This commit is contained in:
179
TODO.md
179
TODO.md
@@ -1,131 +1,138 @@
|
|||||||
# TowerControl - Plan de développement
|
# TowerControl v2.0 - Gestionnaire de tâches moderne
|
||||||
|
|
||||||
## Phase 1: Setup initial et architecture backend (Priorité 1)
|
## ✅ Phase 1: Nettoyage et architecture (TERMINÉ)
|
||||||
|
|
||||||
### 1.1 Configuration projet Next.js
|
### 1.1 Configuration projet Next.js
|
||||||
- [x] Initialiser Next.js avec TypeScript
|
- [x] Initialiser Next.js avec TypeScript
|
||||||
- [x] Configurer ESLint, Prettier
|
- [x] Configurer ESLint, Prettier
|
||||||
- [x] Setup structure de dossiers selon les règles du workspace
|
- [x] Setup structure de dossiers selon les règles du workspace
|
||||||
- [x] Configurer base de données (SQLite local pour commencer)
|
- [x] Configurer base de données (SQLite local)
|
||||||
- [x] Setup Prisma ORM
|
- [x] Setup Prisma ORM
|
||||||
|
|
||||||
### 1.2 Architecture backend - Services de base
|
### 1.2 Architecture backend standalone
|
||||||
- [x] Créer `services/database.ts` - Pool de connexion DB
|
- [x] Créer `services/database.ts` - Pool de connexion DB
|
||||||
- [x] Créer `services/reminders.ts` - Service pour récupérer les rappels macOS
|
- [x] Créer `services/tasks.ts` - Service CRUD pour les tâches
|
||||||
- [x] Créer `lib/types.ts` - Types partagés (Task, Tag, Project, etc.)
|
- [x] Créer `lib/types.ts` - Types partagés (Task, Tag, etc.)
|
||||||
- [x] Créer `services/task-processor.ts` - Logique métier des tâches
|
- [x] Nettoyer l'ancien code de synchronisation
|
||||||
|
|
||||||
### 1.3 Intégration Rappels macOS (Focus principal Phase 1)
|
### 1.3 API moderne et propre
|
||||||
- [x] Rechercher comment accéder aux rappels macOS en local (SQLite, AppleScript, ou API)
|
- [x] `app/api/tasks/route.ts` - API CRUD complète (GET, POST, PATCH, DELETE)
|
||||||
- [x] Créer script d'extraction des rappels depuis la DB locale macOS
|
- [x] Supprimer les routes de synchronisation obsolètes
|
||||||
- [x] Parser les tags et catégories des rappels
|
- [x] Configuration moderne dans `lib/config.ts`
|
||||||
- [x] Mapper les données vers le modèle interne
|
|
||||||
- [x] Créer service de synchronisation périodique
|
|
||||||
|
|
||||||
### 1.4 API Routes essentielles (terminé)
|
**Architecture finale** : App standalone avec backend propre et API REST moderne
|
||||||
- [x] `app/api/tasks/route.ts` - CRUD tâches
|
|
||||||
- [x] `app/api/sync/reminders/route.ts` - Synchronisation rappels
|
|
||||||
- [x] `app/api/config/route.ts` - Configuration et test des listes
|
|
||||||
- [x] `app/api/test/route.ts` - Tests des services
|
|
||||||
|
|
||||||
**Note** : Privilégier SSR avec appels directs aux services plutôt que créer plus de routes API
|
## 🎯 Phase 2: Interface utilisateur moderne (EN COURS)
|
||||||
|
|
||||||
## Phase 2: Interface utilisateur Kanban (Priorité 2)
|
### 2.1 Système de design et composants UI
|
||||||
|
- [ ] Créer les composants UI de base (Button, Input, Card, Modal, etc.)
|
||||||
|
- [ ] Implémenter le système de design (couleurs, typographie, spacing)
|
||||||
|
- [ ] Setup Tailwind CSS avec design tokens personnalisés
|
||||||
|
- [ ] Créer une palette de couleurs moderne et accessible
|
||||||
|
|
||||||
### 2.1 Composants de base
|
### 2.2 Composants Kanban existants (à améliorer)
|
||||||
- [x] `components/kanban/Board.tsx` - Tableau Kanban principal
|
- [x] `components/kanban/Board.tsx` - Tableau Kanban principal
|
||||||
- [x] `components/kanban/Column.tsx` - Colonnes du Kanban
|
- [x] `components/kanban/Column.tsx` - Colonnes du Kanban
|
||||||
- [x] `components/kanban/TaskCard.tsx` - Cartes de tâches
|
- [x] `components/kanban/TaskCard.tsx` - Cartes de tâches
|
||||||
- [x] `components/ui/Header.tsx` - Header avec statistiques
|
- [x] `components/ui/Header.tsx` - Header avec statistiques
|
||||||
|
- [ ] Améliorer le design et l'UX des composants existants
|
||||||
|
|
||||||
### 2.2 Clients HTTP
|
### 2.3 Clients HTTP et hooks
|
||||||
- [ ] `clients/tasks-client.ts` - Client pour les tâches
|
- [ ] `clients/tasks-client.ts` - Client pour les tâches
|
||||||
- [ ] `clients/base/http-client.ts` - Client HTTP de base
|
- [ ] `clients/base/http-client.ts` - Client HTTP de base
|
||||||
|
- [ ] `hooks/useTasks.ts` - Hook pour la gestion des tâches
|
||||||
|
- [ ] `hooks/useKanban.ts` - Hook pour drag & drop
|
||||||
- [ ] Gestion des erreurs et loading states
|
- [ ] Gestion des erreurs et loading states
|
||||||
|
|
||||||
### 2.3 Hooks React
|
### 2.4 Fonctionnalités Kanban avancées
|
||||||
- [ ] `hooks/useTasks.ts` - Hook pour la gestion des tâches
|
- [ ] Drag & drop entre colonnes (react-beautiful-dnd)
|
||||||
- [ ] `hooks/useKanban.ts` - Hook pour la logique Kanban (drag & drop)
|
- [ ] Formulaires de création/édition de tâches
|
||||||
- [ ] `hooks/useSync.ts` - Hook pour la synchronisation
|
- [ ] Filtrage par tags/statut/priorité
|
||||||
|
- [ ] Recherche en temps réel dans les tâches
|
||||||
|
- [ ] Gestion des tags avec couleurs
|
||||||
|
|
||||||
### 2.4 Interface Kanban
|
## 📊 Phase 3: Dashboard et analytics (Priorité 3)
|
||||||
- [ ] Affichage des tâches par statut/tag
|
|
||||||
- [ ] Drag & drop entre colonnes
|
|
||||||
- [ ] Filtrage par tags/projets
|
|
||||||
- [ ] Recherche dans les tâches
|
|
||||||
|
|
||||||
## Phase 3: Intégration Jira (Priorité 3)
|
### 3.1 Page d'accueil/dashboard
|
||||||
|
- [ ] Créer une page d'accueil moderne avec vue d'ensemble
|
||||||
|
- [ ] Widgets de statistiques (tâches par statut, priorité, etc.)
|
||||||
|
- [ ] Graphiques de productivité (tâches complétées par jour/semaine)
|
||||||
|
- [ ] Indicateurs de performance personnels
|
||||||
|
|
||||||
### 3.1 Services Jira
|
### 3.2 Analytics et métriques
|
||||||
- [ ] `services/jira-client.ts` - Client Jira API
|
- [ ] `services/analytics.ts` - Calculs statistiques
|
||||||
- [ ] `services/jira-sync.ts` - Synchronisation des tâches Jira
|
- [ ] Métriques de productivité (vélocité, temps moyen, etc.)
|
||||||
- [ ] Gestion multi-projets Jira
|
- [ ] Graphiques avec Chart.js ou Recharts
|
||||||
- [ ] Mapping des statuts Jira vers Kanban interne
|
- [ ] Export des données en CSV/JSON
|
||||||
|
|
||||||
### 3.2 API Routes Jira
|
## 🔧 Phase 4: Fonctionnalités avancées (Priorité 4)
|
||||||
- [ ] `app/api/jira/projects/route.ts` - Liste des projets
|
|
||||||
- [ ] `app/api/jira/tasks/route.ts` - Tâches Jira
|
|
||||||
- [ ] `app/api/jira/sync/route.ts` - Synchronisation
|
|
||||||
- [ ] Configuration des credentials Jira
|
|
||||||
|
|
||||||
### 3.3 Interface Jira
|
### 4.1 Gestion avancée des tâches
|
||||||
- [ ] Sélecteur de projets Jira
|
- [ ] Sous-tâches et hiérarchie
|
||||||
- [ ] Affichage mixte rappels + Jira dans le Kanban
|
- [ ] Dates d'échéance et rappels
|
||||||
- [ ] Indicateurs visuels pour différencier les sources
|
- [ ] Assignation et collaboration
|
||||||
|
- [ ] Templates de tâches
|
||||||
|
|
||||||
## Phase 4: Statistiques équipe (Priorité 4)
|
### 4.2 Personnalisation et thèmes
|
||||||
|
- [ ] Mode sombre/clair
|
||||||
|
- [ ] Personnalisation des couleurs
|
||||||
|
- [ ] Configuration des colonnes Kanban
|
||||||
|
- [ ] Préférences utilisateur
|
||||||
|
|
||||||
### 4.1 Services analytics
|
## 🚀 Phase 5: Intégrations futures (Priorité 5)
|
||||||
- [ ] `services/team-analytics.ts` - Calculs statistiques équipe
|
|
||||||
- [ ] `services/jira-team-sync.ts` - Récupération données équipe
|
|
||||||
- [ ] Agrégation des métriques (vélocité, burndown, etc.)
|
|
||||||
|
|
||||||
### 4.2 Dashboard équipe
|
### 5.1 Intégrations externes (optionnel)
|
||||||
- [ ] `components/dashboard/TeamStats.tsx` - Statistiques équipe
|
- [ ] Import/Export depuis d'autres outils
|
||||||
- [ ] `components/charts/` - Graphiques (vélocité, burndown, etc.)
|
- [ ] API webhooks pour intégrations
|
||||||
- [ ] `app/team/page.tsx` - Page dédiée équipe
|
- [ ] Synchronisation cloud (optionnel)
|
||||||
- [ ] Filtres par période, membre, projet
|
- [ ] Notifications push
|
||||||
|
|
||||||
## Phase 5: Outils additionnels (Priorité 5)
|
### 5.2 Optimisations et performance
|
||||||
|
|
||||||
### 5.1 Intégrations futures
|
|
||||||
- [ ] Calendrier (événements, deadlines)
|
|
||||||
- [ ] Notifications (rappels, alertes)
|
|
||||||
- [ ] Export/Import de données
|
|
||||||
- [ ] Thèmes et personnalisation
|
|
||||||
|
|
||||||
### 5.2 Optimisations
|
|
||||||
- [ ] Cache Redis pour les données Jira
|
|
||||||
- [ ] Optimisation des requêtes DB
|
- [ ] Optimisation des requêtes DB
|
||||||
- [ ] Pagination des tâches
|
- [ ] Pagination et virtualisation
|
||||||
- [ ] Mode offline basique
|
- [ ] Cache côté client
|
||||||
|
- [ ] PWA et mode offline
|
||||||
|
|
||||||
## Configuration technique
|
## 🛠️ Configuration technique
|
||||||
|
|
||||||
### Stack
|
### Stack moderne
|
||||||
- **Frontend**: Next.js 14, React, TypeScript, Tailwind CSS
|
- **Frontend**: Next.js 14, React, TypeScript, Tailwind CSS
|
||||||
- **Backend**: Next.js API Routes, Prisma ORM
|
- **Backend**: Next.js API Routes, Prisma ORM
|
||||||
- **Database**: SQLite (local) → PostgreSQL (production)
|
- **Database**: SQLite (local) → PostgreSQL (production future)
|
||||||
- **Intégrations**: macOS Reminders, Jira API
|
- **UI**: Composants custom + Shadcn/ui, React Beautiful DnD
|
||||||
- **UI**: Shadcn/ui, React DnD pour le Kanban
|
- **Charts**: Recharts ou Chart.js pour les analytics
|
||||||
|
|
||||||
### Structure respectée
|
### Architecture respectée
|
||||||
```
|
```
|
||||||
/services/ # Accès DB et logique métier
|
src/app/
|
||||||
/app/api/ # Routes API utilisant les services
|
├── api/tasks/ # API CRUD complète
|
||||||
/clients/ # Clients HTTP frontend
|
├── page.tsx # Page principale
|
||||||
/components/ # Composants React (pas de logique métier)
|
└── layout.tsx
|
||||||
/hooks/ # Hooks React
|
|
||||||
/lib/ # Types et utilitaires partagés
|
services/
|
||||||
|
├── database.ts # Pool Prisma
|
||||||
|
└── tasks.ts # Service tâches standalone
|
||||||
|
|
||||||
|
components/
|
||||||
|
├── kanban/ # Board Kanban
|
||||||
|
├── ui/ # Composants UI de base
|
||||||
|
└── dashboard/ # Widgets dashboard (futur)
|
||||||
|
|
||||||
|
clients/ # Clients HTTP (à créer)
|
||||||
|
hooks/ # Hooks React (à créer)
|
||||||
|
lib/
|
||||||
|
├── types.ts # Types TypeScript
|
||||||
|
└── config.ts # Config app moderne
|
||||||
```
|
```
|
||||||
|
|
||||||
## Prochaines étapes immédiates
|
## 🎯 Prochaines étapes immédiates
|
||||||
|
|
||||||
1. **Setup Next.js** avec la structure de dossiers
|
1. **Créer les composants UI de base** (Button, Input, Card, Modal)
|
||||||
2. **Recherche technique** : Comment accéder aux rappels macOS localement
|
2. **Implémenter le système de design** avec Tailwind
|
||||||
3. **Créer le service `reminders.ts`** pour l'extraction des données
|
3. **Améliorer le Kanban** avec un design moderne
|
||||||
4. **API de base** pour les tâches et synchronisation
|
4. **Ajouter drag & drop** entre les colonnes
|
||||||
|
5. **Créer les formulaires** de tâches
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Ce plan se concentre d'abord sur le backend et les rappels macOS comme demandé. Chaque phase peut être développée indépendamment.*
|
*Focus sur l'expérience utilisateur et le design moderne. App standalone prête pour évoluer.*
|
||||||
|
|||||||
@@ -56,7 +56,9 @@ export function KanbanBoard({ tasks }: KanbanBoardProps) {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full flex gap-4 overflow-x-auto pb-6">
|
<div className="h-full flex flex-col bg-slate-950">
|
||||||
|
{/* Board tech dark */}
|
||||||
|
<div className="flex-1 flex gap-6 overflow-x-auto p-6">
|
||||||
{columns.map((column) => (
|
{columns.map((column) => (
|
||||||
<KanbanColumn
|
<KanbanColumn
|
||||||
key={column.id}
|
key={column.id}
|
||||||
@@ -67,5 +69,6 @@ export function KanbanBoard({ tasks }: KanbanBoardProps) {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,41 +9,73 @@ interface KanbanColumnProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function KanbanColumn({ id, title, color, tasks }: KanbanColumnProps) {
|
export function KanbanColumn({ id, title, color, tasks }: KanbanColumnProps) {
|
||||||
const colorClasses = {
|
// Couleurs tech/cyberpunk
|
||||||
gray: 'border-gray-300 bg-gray-50 dark:border-gray-600 dark:bg-gray-800',
|
const techStyles = {
|
||||||
blue: 'border-blue-300 bg-blue-50 dark:border-blue-600 dark:bg-blue-900/20',
|
gray: {
|
||||||
green: 'border-green-300 bg-green-50 dark:border-green-600 dark:bg-green-900/20',
|
border: 'border-slate-700',
|
||||||
red: 'border-red-300 bg-red-50 dark:border-red-600 dark:bg-red-900/20'
|
glow: 'shadow-slate-500/20',
|
||||||
|
accent: 'text-slate-400',
|
||||||
|
badge: 'bg-slate-800 text-slate-300 border border-slate-600'
|
||||||
|
},
|
||||||
|
blue: {
|
||||||
|
border: 'border-cyan-500/30',
|
||||||
|
glow: 'shadow-cyan-500/20',
|
||||||
|
accent: 'text-cyan-400',
|
||||||
|
badge: 'bg-cyan-950 text-cyan-300 border border-cyan-500/30'
|
||||||
|
},
|
||||||
|
green: {
|
||||||
|
border: 'border-emerald-500/30',
|
||||||
|
glow: 'shadow-emerald-500/20',
|
||||||
|
accent: 'text-emerald-400',
|
||||||
|
badge: 'bg-emerald-950 text-emerald-300 border border-emerald-500/30'
|
||||||
|
},
|
||||||
|
red: {
|
||||||
|
border: 'border-red-500/30',
|
||||||
|
glow: 'shadow-red-500/20',
|
||||||
|
accent: 'text-red-400',
|
||||||
|
badge: 'bg-red-950 text-red-300 border border-red-500/30'
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const headerColorClasses = {
|
const style = techStyles[color as keyof typeof techStyles];
|
||||||
gray: 'text-gray-700 dark:text-gray-300',
|
|
||||||
blue: 'text-blue-700 dark:text-blue-300',
|
// Icônes tech
|
||||||
green: 'text-green-700 dark:text-green-300',
|
const techIcons = {
|
||||||
red: 'text-red-700 dark:text-red-300'
|
todo: '⚡',
|
||||||
|
in_progress: '🔄',
|
||||||
|
done: '✓',
|
||||||
|
cancelled: '✕'
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-shrink-0 w-80">
|
<div className="flex-shrink-0 w-80 h-full">
|
||||||
{/* En-tête de colonne */}
|
{/* Header tech avec glow */}
|
||||||
<div className={`rounded-t-lg border-2 border-b-0 p-4 ${colorClasses[color as keyof typeof colorClasses]}`}>
|
<div className={`bg-slate-900 ${style.border} border rounded-t-lg p-4 ${style.glow} shadow-lg`}>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h3 className={`font-semibold text-lg ${headerColorClasses[color as keyof typeof headerColorClasses]}`}>
|
<div className="flex items-center gap-3">
|
||||||
|
<div className={`w-2 h-2 rounded-full ${style.accent.replace('text-', 'bg-')} animate-pulse`}></div>
|
||||||
|
<h3 className={`font-mono text-sm font-bold ${style.accent} uppercase tracking-wider`}>
|
||||||
{title}
|
{title}
|
||||||
</h3>
|
</h3>
|
||||||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${headerColorClasses[color as keyof typeof headerColorClasses]} bg-white/50 dark:bg-black/20`}>
|
</div>
|
||||||
{tasks.length}
|
<span className={`${style.badge} px-3 py-1 rounded-full text-xs font-mono font-bold`}>
|
||||||
|
{String(tasks.length).padStart(2, '0')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Zone de contenu */}
|
{/* Zone de contenu tech */}
|
||||||
<div className={`border-2 border-t-0 rounded-b-lg min-h-96 p-4 ${colorClasses[color as keyof typeof colorClasses]}`}>
|
<div className={`bg-slate-900/80 backdrop-blur-sm ${style.border} border-t-0 border rounded-b-lg p-4 h-[calc(100vh-220px)] overflow-y-auto ${style.glow} shadow-lg`}>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{tasks.length === 0 ? (
|
{tasks.length === 0 ? (
|
||||||
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
|
<div className="text-center py-20">
|
||||||
<div className="text-4xl mb-2">📝</div>
|
<div className={`w-16 h-16 mx-auto mb-4 rounded-full bg-slate-800 border-2 border-dashed ${style.border} flex items-center justify-center`}>
|
||||||
<p className="text-sm">Aucune tâche</p>
|
<span className={`text-2xl ${style.accent} opacity-50`}>{techIcons[id]}</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs font-mono text-slate-500 uppercase tracking-wide">NO DATA</p>
|
||||||
|
<div className="mt-2 flex justify-center">
|
||||||
|
<div className={`w-8 h-0.5 ${style.accent.replace('text-', 'bg-')} opacity-30`}></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
tasks.map((task) => (
|
tasks.map((task) => (
|
||||||
|
|||||||
@@ -27,83 +27,74 @@ export function TaskCard({ task }: TaskCardProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white dark:bg-gray-700 rounded-lg shadow-sm border border-gray-200 dark:border-gray-600 p-4 hover:shadow-md transition-shadow cursor-pointer">
|
<div className="bg-slate-800/50 backdrop-blur-sm border border-slate-700/50 rounded-lg p-3 hover:bg-slate-800/80 hover:border-cyan-500/30 hover:shadow-lg hover:shadow-cyan-500/10 transition-all duration-300 cursor-pointer group">
|
||||||
{/* En-tête avec emojis */}
|
{/* Header tech avec titre et status */}
|
||||||
|
<div className="flex items-start gap-2 mb-2">
|
||||||
{emojis.length > 0 && (
|
{emojis.length > 0 && (
|
||||||
<div className="flex gap-1 mb-2">
|
<div className="flex gap-1 flex-shrink-0">
|
||||||
{emojis.map((emoji, index) => (
|
{emojis.slice(0, 2).map((emoji, index) => (
|
||||||
<span key={index} className="text-lg">
|
<span key={index} className="text-sm opacity-80">
|
||||||
{emoji}
|
{emoji}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Titre */}
|
<h4 className="font-mono text-sm font-medium text-slate-100 leading-tight line-clamp-2 flex-1">
|
||||||
<h4 className="font-medium text-gray-900 dark:text-white mb-2 line-clamp-2">
|
|
||||||
{titleWithoutEmojis}
|
{titleWithoutEmojis}
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
{/* Description si présente */}
|
{/* Indicateur de priorité tech */}
|
||||||
|
<div className={`w-2 h-2 rounded-full flex-shrink-0 mt-1 animate-pulse ${
|
||||||
|
task.priority === 'high' ? 'bg-red-400 shadow-red-400/50 shadow-sm' :
|
||||||
|
task.priority === 'medium' ? 'bg-yellow-400 shadow-yellow-400/50 shadow-sm' :
|
||||||
|
'bg-slate-500'
|
||||||
|
}`} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Description tech */}
|
||||||
{task.description && (
|
{task.description && (
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-300 mb-3 line-clamp-2">
|
<p className="text-xs text-slate-400 mb-3 line-clamp-1 font-mono">
|
||||||
{task.description}
|
{task.description}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Tags */}
|
{/* Tags tech style */}
|
||||||
<div className="flex flex-wrap gap-1 mb-3">
|
|
||||||
{/* Priorité */}
|
|
||||||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${priorityColors[task.priority]}`}>
|
|
||||||
{task.priority}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{/* Source */}
|
|
||||||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${sourceColors[task.source as keyof typeof sourceColors]}`}>
|
|
||||||
{task.source}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{/* Tags personnalisés */}
|
|
||||||
{task.tags && task.tags.length > 0 && (
|
{task.tags && task.tags.length > 0 && (
|
||||||
task.tags.slice(0, 2).map((tag, index) => (
|
<div className="flex flex-wrap gap-1 mb-3">
|
||||||
|
{task.tags.slice(0, 3).map((tag, index) => (
|
||||||
<span
|
<span
|
||||||
key={index}
|
key={index}
|
||||||
className="px-2 py-1 rounded-full text-xs font-medium bg-indigo-100 text-indigo-700 dark:bg-indigo-900/30 dark:text-indigo-300"
|
className="px-2 py-1 rounded text-xs font-mono font-bold bg-cyan-950/50 text-cyan-300 border border-cyan-500/30 hover:bg-cyan-950/80 transition-colors"
|
||||||
>
|
>
|
||||||
#{tag}
|
{tag}
|
||||||
</span>
|
</span>
|
||||||
))
|
))}
|
||||||
)}
|
{task.tags.length > 3 && (
|
||||||
</div>
|
<span className="px-2 py-1 rounded text-xs font-mono text-slate-500 border border-slate-600">
|
||||||
|
+{task.tags.length - 3}
|
||||||
{/* Footer avec dates */}
|
|
||||||
<div className="flex items-center justify-between text-xs text-gray-500 dark:text-gray-400">
|
|
||||||
<div>
|
|
||||||
{task.dueDate && (
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
📅 {formatDistanceToNow(new Date(task.dueDate), {
|
|
||||||
addSuffix: true,
|
|
||||||
locale: fr
|
|
||||||
})}
|
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div>
|
{/* Footer tech avec séparateur néon */}
|
||||||
{task.completedAt ? (
|
<div className="pt-2 border-t border-slate-700/50">
|
||||||
<span className="flex items-center gap-1 text-green-600 dark:text-green-400">
|
<div className="flex items-center justify-between text-xs">
|
||||||
✅ {formatDistanceToNow(new Date(task.completedAt), {
|
{task.dueDate ? (
|
||||||
|
<span className="flex items-center gap-1 text-slate-400 font-mono">
|
||||||
|
<span className="text-cyan-400">⏰</span>
|
||||||
|
{formatDistanceToNow(new Date(task.dueDate), {
|
||||||
addSuffix: true,
|
addSuffix: true,
|
||||||
locale: fr
|
locale: fr
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span>
|
<span className="text-slate-600 font-mono">--:--</span>
|
||||||
Créé {formatDistanceToNow(new Date(task.createdAt), {
|
)}
|
||||||
addSuffix: true,
|
|
||||||
locale: fr
|
{task.completedAt && (
|
||||||
})}
|
<span className="text-emerald-400 font-mono font-bold">✓ DONE</span>
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,43 +12,46 @@ interface HeaderProps {
|
|||||||
|
|
||||||
export function Header({ title, subtitle, stats }: HeaderProps) {
|
export function Header({ title, subtitle, stats }: HeaderProps) {
|
||||||
return (
|
return (
|
||||||
<header className="bg-white dark:bg-gray-800 shadow-sm border-b border-gray-200 dark:border-gray-700">
|
<header className="bg-slate-900/80 backdrop-blur-sm border-b border-slate-700/50 shadow-lg shadow-slate-900/20">
|
||||||
<div className="container mx-auto px-4 py-6">
|
<div className="container mx-auto px-6 py-4">
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-6">
|
||||||
{/* Titre et sous-titre */}
|
{/* Titre tech avec glow */}
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="w-3 h-3 bg-cyan-400 rounded-full animate-pulse shadow-cyan-400/50 shadow-lg"></div>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
|
<h1 className="text-2xl font-mono font-bold text-slate-100 tracking-wider">
|
||||||
{title}
|
{title}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
<p className="text-slate-400 mt-1 font-mono text-sm">
|
||||||
{subtitle}
|
{subtitle}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Statistiques */}
|
{/* Stats tech dashboard */}
|
||||||
<div className="flex flex-wrap gap-4">
|
<div className="flex flex-wrap gap-3">
|
||||||
<StatCard
|
<StatCard
|
||||||
label="Total"
|
label="TOTAL"
|
||||||
value={stats.total}
|
value={String(stats.total).padStart(2, '0')}
|
||||||
color="blue"
|
color="blue"
|
||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
label="Terminées"
|
label="DONE"
|
||||||
value={stats.completed}
|
value={String(stats.completed).padStart(2, '0')}
|
||||||
color="green"
|
color="green"
|
||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
label="En cours"
|
label="ACTIVE"
|
||||||
value={stats.inProgress}
|
value={String(stats.inProgress).padStart(2, '0')}
|
||||||
color="yellow"
|
color="yellow"
|
||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
label="À faire"
|
label="QUEUE"
|
||||||
value={stats.todo}
|
value={String(stats.todo).padStart(2, '0')}
|
||||||
color="gray"
|
color="gray"
|
||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
label="Taux"
|
label="RATE"
|
||||||
value={`${stats.completionRate}%`}
|
value={`${stats.completionRate}%`}
|
||||||
color="purple"
|
color="purple"
|
||||||
/>
|
/>
|
||||||
@@ -66,20 +69,47 @@ interface StatCardProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function StatCard({ label, value, color }: StatCardProps) {
|
function StatCard({ label, value, color }: StatCardProps) {
|
||||||
const colorClasses = {
|
const techStyles = {
|
||||||
blue: 'bg-blue-50 text-blue-700 border-blue-200 dark:bg-blue-900/20 dark:text-blue-300 dark:border-blue-800',
|
blue: {
|
||||||
green: 'bg-green-50 text-green-700 border-green-200 dark:bg-green-900/20 dark:text-green-300 dark:border-green-800',
|
bg: 'bg-slate-800/50',
|
||||||
yellow: 'bg-yellow-50 text-yellow-700 border-yellow-200 dark:bg-yellow-900/20 dark:text-yellow-300 dark:border-yellow-800',
|
border: 'border-cyan-500/30',
|
||||||
gray: 'bg-gray-50 text-gray-700 border-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-700',
|
text: 'text-cyan-300',
|
||||||
purple: 'bg-purple-50 text-purple-700 border-purple-200 dark:bg-purple-900/20 dark:text-purple-300 dark:border-purple-800'
|
glow: 'shadow-cyan-500/20'
|
||||||
|
},
|
||||||
|
green: {
|
||||||
|
bg: 'bg-slate-800/50',
|
||||||
|
border: 'border-emerald-500/30',
|
||||||
|
text: 'text-emerald-300',
|
||||||
|
glow: 'shadow-emerald-500/20'
|
||||||
|
},
|
||||||
|
yellow: {
|
||||||
|
bg: 'bg-slate-800/50',
|
||||||
|
border: 'border-yellow-500/30',
|
||||||
|
text: 'text-yellow-300',
|
||||||
|
glow: 'shadow-yellow-500/20'
|
||||||
|
},
|
||||||
|
gray: {
|
||||||
|
bg: 'bg-slate-800/50',
|
||||||
|
border: 'border-slate-500/30',
|
||||||
|
text: 'text-slate-300',
|
||||||
|
glow: 'shadow-slate-500/20'
|
||||||
|
},
|
||||||
|
purple: {
|
||||||
|
bg: 'bg-slate-800/50',
|
||||||
|
border: 'border-purple-500/30',
|
||||||
|
text: 'text-purple-300',
|
||||||
|
glow: 'shadow-purple-500/20'
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const style = techStyles[color];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`px-3 py-2 rounded-lg border text-sm font-medium ${colorClasses[color]}`}>
|
<div className={`${style.bg} ${style.border} border rounded-lg px-3 py-2 ${style.glow} shadow-lg backdrop-blur-sm hover:${style.border.replace('/30', '/50')} transition-all duration-300`}>
|
||||||
<div className="text-xs opacity-75 uppercase tracking-wide">
|
<div className={`text-xs font-mono font-bold ${style.text} opacity-75 uppercase tracking-wider`}>
|
||||||
{label}
|
{label}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-lg font-bold">
|
<div className={`text-lg font-mono font-bold ${style.text}`}>
|
||||||
{value}
|
{value}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,41 +1,37 @@
|
|||||||
/**
|
/**
|
||||||
* Configuration de l'application TowerControl
|
* Configuration de l'application TowerControl (version standalone)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export interface AppConfig {
|
export interface AppConfig {
|
||||||
reminders: {
|
app: {
|
||||||
targetList: string;
|
name: string;
|
||||||
syncInterval: number; // en minutes
|
version: string;
|
||||||
enabledLists: string[];
|
|
||||||
};
|
};
|
||||||
jira: {
|
ui: {
|
||||||
baseUrl?: string;
|
theme: 'light' | 'dark' | 'system';
|
||||||
username?: string;
|
itemsPerPage: number;
|
||||||
apiToken?: string;
|
|
||||||
projects: string[];
|
|
||||||
};
|
};
|
||||||
sync: {
|
features: {
|
||||||
autoSync: boolean;
|
enableDragAndDrop: boolean;
|
||||||
batchSize: number;
|
enableNotifications: boolean;
|
||||||
|
autoSave: boolean;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configuration par défaut
|
// Configuration par défaut
|
||||||
const defaultConfig: AppConfig = {
|
const defaultConfig: AppConfig = {
|
||||||
reminders: {
|
app: {
|
||||||
targetList: process.env.REMINDERS_TARGET_LIST || 'Boulot',
|
name: 'TowerControl',
|
||||||
syncInterval: parseInt(process.env.REMINDERS_SYNC_INTERVAL || '15'),
|
version: '2.0.0'
|
||||||
enabledLists: (process.env.REMINDERS_ENABLED_LISTS || 'Boulot').split(',')
|
|
||||||
},
|
},
|
||||||
jira: {
|
ui: {
|
||||||
baseUrl: process.env.JIRA_BASE_URL,
|
theme: (process.env.NEXT_PUBLIC_THEME as 'light' | 'dark' | 'system') || 'system',
|
||||||
username: process.env.JIRA_USERNAME,
|
itemsPerPage: parseInt(process.env.NEXT_PUBLIC_ITEMS_PER_PAGE || '50')
|
||||||
apiToken: process.env.JIRA_API_TOKEN,
|
|
||||||
projects: (process.env.JIRA_PROJECTS || '').split(',').filter(p => p.length > 0)
|
|
||||||
},
|
},
|
||||||
sync: {
|
features: {
|
||||||
autoSync: process.env.AUTO_SYNC === 'true',
|
enableDragAndDrop: process.env.NEXT_PUBLIC_ENABLE_DRAG_DROP !== 'false',
|
||||||
batchSize: parseInt(process.env.SYNC_BATCH_SIZE || '50')
|
enableNotifications: process.env.NEXT_PUBLIC_ENABLE_NOTIFICATIONS === 'true',
|
||||||
|
autoSave: process.env.NEXT_PUBLIC_AUTO_SAVE !== 'false'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -46,33 +42,11 @@ export function getConfig(): AppConfig {
|
|||||||
return defaultConfig;
|
return defaultConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Récupère la liste cible des rappels
|
|
||||||
*/
|
|
||||||
export function getTargetRemindersList(): string {
|
|
||||||
return getConfig().reminders.targetList;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Récupère les listes autorisées pour la synchronisation
|
|
||||||
*/
|
|
||||||
export function getEnabledRemindersLists(): string[] {
|
|
||||||
return getConfig().reminders.enabledLists;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Vérifie si une liste est autorisée pour la synchronisation
|
|
||||||
*/
|
|
||||||
export function isListEnabled(listName: string): boolean {
|
|
||||||
const enabledLists = getEnabledRemindersLists();
|
|
||||||
return enabledLists.includes(listName);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration pour le développement/debug
|
* Configuration pour le développement/debug
|
||||||
*/
|
*/
|
||||||
export const DEBUG_CONFIG = {
|
export const DEBUG_CONFIG = {
|
||||||
logAppleScript: process.env.NODE_ENV === 'development',
|
isDevelopment: process.env.NODE_ENV === 'development',
|
||||||
mockData: process.env.USE_MOCK_DATA === 'true',
|
verboseLogging: process.env.VERBOSE_LOGGING === 'true',
|
||||||
verboseLogging: process.env.VERBOSE_LOGGING === 'true'
|
enableDevTools: process.env.NODE_ENV === 'development'
|
||||||
};
|
};
|
||||||
|
|||||||
87
scripts/reset-database.ts
Normal file
87
scripts/reset-database.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import { prisma } from '../services/database';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Script pour reset la base de données et supprimer les anciennes données
|
||||||
|
*/
|
||||||
|
async function resetDatabase() {
|
||||||
|
console.log('🗑️ Reset de la base de données...');
|
||||||
|
console.log('===================================');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Compter les tâches avant suppression
|
||||||
|
const beforeCount = await prisma.task.count();
|
||||||
|
const manualCount = await prisma.task.count({ where: { source: 'manual' } });
|
||||||
|
const remindersCount = await prisma.task.count({ where: { source: 'reminders' } });
|
||||||
|
|
||||||
|
console.log(`📊 État actuel:`);
|
||||||
|
console.log(` Total: ${beforeCount} tâches`);
|
||||||
|
console.log(` Manuelles: ${manualCount} tâches`);
|
||||||
|
console.log(` Rappels: ${remindersCount} tâches`);
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// Supprimer toutes les tâches de synchronisation
|
||||||
|
const deletedTasks = await prisma.task.deleteMany({
|
||||||
|
where: {
|
||||||
|
source: 'reminders'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`✅ Supprimé ${deletedTasks.count} tâches de synchronisation`);
|
||||||
|
|
||||||
|
// Supprimer les logs de sync
|
||||||
|
const deletedLogs = await prisma.syncLog.deleteMany();
|
||||||
|
console.log(`✅ Supprimé ${deletedLogs.count} logs de synchronisation`);
|
||||||
|
|
||||||
|
// Supprimer les tags orphelins (optionnel)
|
||||||
|
const deletedTags = await prisma.tag.deleteMany();
|
||||||
|
console.log(`✅ Supprimé ${deletedTags.count} tags`);
|
||||||
|
|
||||||
|
// Compter après nettoyage
|
||||||
|
const afterCount = await prisma.task.count();
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
console.log('🎉 Base de données nettoyée !');
|
||||||
|
console.log(`📊 Résultat: ${afterCount} tâches restantes`);
|
||||||
|
|
||||||
|
// Afficher les tâches restantes
|
||||||
|
if (afterCount > 0) {
|
||||||
|
console.log('');
|
||||||
|
console.log('📋 Tâches restantes:');
|
||||||
|
const remainingTasks = await prisma.task.findMany({
|
||||||
|
orderBy: { createdAt: 'desc' }
|
||||||
|
});
|
||||||
|
|
||||||
|
remainingTasks.forEach((task, index) => {
|
||||||
|
const statusEmoji = {
|
||||||
|
'todo': '⏳',
|
||||||
|
'in_progress': '🔄',
|
||||||
|
'done': '✅',
|
||||||
|
'cancelled': '❌'
|
||||||
|
}[task.status] || '❓';
|
||||||
|
|
||||||
|
const tags = JSON.parse(task.tagsJson || '[]');
|
||||||
|
const tagsStr = tags.length > 0 ? ` [${tags.join(', ')}]` : '';
|
||||||
|
|
||||||
|
console.log(` ${index + 1}. ${statusEmoji} ${task.title}${tagsStr}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Erreur lors du reset:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exécuter le script
|
||||||
|
if (require.main === module) {
|
||||||
|
resetDatabase().then(() => {
|
||||||
|
console.log('');
|
||||||
|
console.log('✨ Reset terminé avec succès !');
|
||||||
|
process.exit(0);
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error('💥 Erreur fatale:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export { resetDatabase };
|
||||||
100
scripts/seed-data.ts
Normal file
100
scripts/seed-data.ts
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import { tasksService } from '../services/tasks';
|
||||||
|
import { TaskStatus, TaskPriority } from '../lib/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Script pour ajouter des données de test avec tags et variété
|
||||||
|
*/
|
||||||
|
async function seedTestData() {
|
||||||
|
console.log('🌱 Ajout de données de test...');
|
||||||
|
console.log('================================');
|
||||||
|
|
||||||
|
const testTasks = [
|
||||||
|
{
|
||||||
|
title: '🎨 Redesign du dashboard',
|
||||||
|
description: 'Créer une interface moderne et intuitive pour le tableau de bord principal',
|
||||||
|
status: 'in_progress' as TaskStatus,
|
||||||
|
priority: 'high' as TaskPriority,
|
||||||
|
tags: ['design', 'ui', 'frontend'],
|
||||||
|
dueDate: new Date('2025-01-20')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '🔧 Optimiser les performances API',
|
||||||
|
description: 'Améliorer les temps de réponse des endpoints et ajouter la pagination',
|
||||||
|
status: 'todo' as TaskStatus,
|
||||||
|
priority: 'medium' as TaskPriority,
|
||||||
|
tags: ['backend', 'performance', 'api'],
|
||||||
|
dueDate: new Date('2025-01-25')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '✅ Tests unitaires composants',
|
||||||
|
description: 'Ajouter des tests Jest/RTL pour les composants principaux',
|
||||||
|
status: 'done' as TaskStatus,
|
||||||
|
priority: 'medium' as TaskPriority,
|
||||||
|
tags: ['testing', 'jest', 'quality'],
|
||||||
|
dueDate: new Date('2025-01-10')
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
let createdCount = 0;
|
||||||
|
let errorCount = 0;
|
||||||
|
|
||||||
|
for (const taskData of testTasks) {
|
||||||
|
try {
|
||||||
|
const task = await tasksService.createTask(taskData);
|
||||||
|
|
||||||
|
const statusEmoji = {
|
||||||
|
'todo': '⏳',
|
||||||
|
'in_progress': '🔄',
|
||||||
|
'done': '✅',
|
||||||
|
'cancelled': '❌'
|
||||||
|
}[task.status];
|
||||||
|
|
||||||
|
const priorityEmoji = {
|
||||||
|
'low': '🔵',
|
||||||
|
'medium': '🟡',
|
||||||
|
'high': '🔴'
|
||||||
|
}[task.priority];
|
||||||
|
|
||||||
|
console.log(` ${statusEmoji} ${priorityEmoji} ${task.title}`);
|
||||||
|
console.log(` Tags: ${task.tags?.join(', ') || 'aucun'}`);
|
||||||
|
if (task.dueDate) {
|
||||||
|
console.log(` Échéance: ${task.dueDate.toLocaleDateString('fr-FR')}`);
|
||||||
|
}
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
createdCount++;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(` ❌ Erreur pour "${taskData.title}":`, error instanceof Error ? error.message : error);
|
||||||
|
errorCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('📊 Résumé:');
|
||||||
|
console.log(` ✅ Tâches créées: ${createdCount}`);
|
||||||
|
console.log(` ❌ Erreurs: ${errorCount}`);
|
||||||
|
|
||||||
|
// Afficher les stats finales
|
||||||
|
const stats = await tasksService.getTaskStats();
|
||||||
|
console.log('');
|
||||||
|
console.log('📈 Statistiques finales:');
|
||||||
|
console.log(` Total: ${stats.total} tâches`);
|
||||||
|
console.log(` À faire: ${stats.todo}`);
|
||||||
|
console.log(` En cours: ${stats.inProgress}`);
|
||||||
|
console.log(` Terminées: ${stats.completed}`);
|
||||||
|
console.log(` Annulées: ${stats.cancelled}`);
|
||||||
|
console.log(` Taux de completion: ${stats.completionRate}%`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exécuter le script
|
||||||
|
if (require.main === module) {
|
||||||
|
seedTestData().then(() => {
|
||||||
|
console.log('');
|
||||||
|
console.log('✨ Données de test ajoutées avec succès !');
|
||||||
|
process.exit(0);
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error('💥 Erreur fatale:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export { seedTestData };
|
||||||
@@ -39,7 +39,7 @@ export async function closeDatabaseConnection(): Promise<void> {
|
|||||||
|
|
||||||
// Fonction utilitaire pour les transactions
|
// Fonction utilitaire pour les transactions
|
||||||
export async function withTransaction<T>(
|
export async function withTransaction<T>(
|
||||||
callback: (tx: PrismaClient) => Promise<T>
|
callback: (tx: Omit<PrismaClient, '$connect' | '$disconnect' | '$on' | '$transaction' | '$extends'>) => Promise<T>
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
return await prisma.$transaction(async (tx) => {
|
return await prisma.$transaction(async (tx) => {
|
||||||
return await callback(tx);
|
return await callback(tx);
|
||||||
|
|||||||
@@ -1,332 +0,0 @@
|
|||||||
import { exec } from 'child_process';
|
|
||||||
import { promisify } from 'util';
|
|
||||||
import { MacOSReminder } from '@/lib/types';
|
|
||||||
import { getTargetRemindersList, getEnabledRemindersLists, isListEnabled, DEBUG_CONFIG } from '@/lib/config';
|
|
||||||
|
|
||||||
const execAsync = promisify(exec);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Service pour récupérer les rappels macOS via AppleScript
|
|
||||||
* Approche sécurisée qui utilise l'API officielle d'Apple
|
|
||||||
*/
|
|
||||||
export class RemindersService {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Récupère tous les rappels depuis l'app Rappels macOS
|
|
||||||
* Utilise la configuration pour filtrer les listes autorisées
|
|
||||||
*/
|
|
||||||
async getAllReminders(): Promise<MacOSReminder[]> {
|
|
||||||
try {
|
|
||||||
if (DEBUG_CONFIG.mockData) {
|
|
||||||
console.log('🔧 Mode mock activé - utilisation des données de test');
|
|
||||||
return this.getMockReminders();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Récupérer uniquement les listes autorisées
|
|
||||||
return await this.getRemindersFromEnabledLists();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Erreur lors de la récupération des rappels:', error);
|
|
||||||
return this.getMockReminders();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Récupère les rappels uniquement des listes autorisées en configuration
|
|
||||||
*/
|
|
||||||
async getRemindersFromEnabledLists(): Promise<MacOSReminder[]> {
|
|
||||||
try {
|
|
||||||
const reminders: MacOSReminder[] = [];
|
|
||||||
const enabledLists = getEnabledRemindersLists();
|
|
||||||
|
|
||||||
console.log(`📋 Synchronisation des listes autorisées: ${enabledLists.join(', ')}`);
|
|
||||||
|
|
||||||
for (const listName of enabledLists) {
|
|
||||||
try {
|
|
||||||
console.log(`🔄 Traitement de la liste: ${listName}`);
|
|
||||||
const listReminders = await this.getRemindersFromListSimple(listName);
|
|
||||||
|
|
||||||
if (listReminders.length > 0) {
|
|
||||||
console.log(`✅ ${listReminders.length} rappels trouvés dans "${listName}"`);
|
|
||||||
reminders.push(...listReminders);
|
|
||||||
} else {
|
|
||||||
console.log(`ℹ️ Aucun rappel dans "${listName}"`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`❌ Erreur pour la liste ${listName}:`, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`📊 Total: ${reminders.length} rappels récupérés`);
|
|
||||||
return reminders;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Erreur getRemindersFromEnabledLists:', error);
|
|
||||||
return this.getMockReminders();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Récupère les rappels de la liste cible principale uniquement
|
|
||||||
*/
|
|
||||||
async getTargetListReminders(): Promise<MacOSReminder[]> {
|
|
||||||
try {
|
|
||||||
const targetList = getTargetRemindersList();
|
|
||||||
console.log(`🎯 Récupération de la liste cible: ${targetList}`);
|
|
||||||
|
|
||||||
return await this.getRemindersFromListSimple(targetList);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Erreur getTargetListReminders:', error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Récupère les rappels d'une liste spécifique
|
|
||||||
*/
|
|
||||||
async getRemindersByList(listName: string): Promise<MacOSReminder[]> {
|
|
||||||
try {
|
|
||||||
const script = `
|
|
||||||
tell application "Reminders"
|
|
||||||
set remindersList to {}
|
|
||||||
set targetList to list "${listName}"
|
|
||||||
|
|
||||||
repeat with reminder in reminders of targetList
|
|
||||||
set reminderRecord to {id:(id of reminder as string), title:(name of reminder), notes:(body of reminder), completed:(completed of reminder), dueDate:missing value, completionDate:missing value, priority:(priority of reminder), list:"${listName}"}
|
|
||||||
|
|
||||||
-- Gérer la date d'échéance
|
|
||||||
try
|
|
||||||
set dueDate of reminderRecord to (due date of reminder as string)
|
|
||||||
end try
|
|
||||||
|
|
||||||
-- Gérer la date de completion
|
|
||||||
try
|
|
||||||
if completed of reminder then
|
|
||||||
set completionDate of reminderRecord to (completion date of reminder as string)
|
|
||||||
end if
|
|
||||||
end try
|
|
||||||
|
|
||||||
set end of remindersList to reminderRecord
|
|
||||||
end repeat
|
|
||||||
|
|
||||||
return remindersList
|
|
||||||
end tell
|
|
||||||
`;
|
|
||||||
|
|
||||||
const { stdout } = await execAsync(`osascript -e '${script.replace(/'/g, "'\\''")}' 2>/dev/null || echo "[]"`);
|
|
||||||
|
|
||||||
return this.parseAppleScriptOutput(stdout);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Erreur lors de la récupération des rappels de la liste ${listName}:`, error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Récupère la liste des listes de rappels
|
|
||||||
*/
|
|
||||||
async getReminderLists(): Promise<string[]> {
|
|
||||||
try {
|
|
||||||
const script = `
|
|
||||||
tell application "Reminders"
|
|
||||||
set listNames to {}
|
|
||||||
repeat with reminderList in lists
|
|
||||||
set end of listNames to name of reminderList
|
|
||||||
end repeat
|
|
||||||
return listNames
|
|
||||||
end tell
|
|
||||||
`;
|
|
||||||
|
|
||||||
const { stdout } = await execAsync(`osascript -e '${script.replace(/'/g, "'\\''")}' 2>/dev/null || echo ""`);
|
|
||||||
|
|
||||||
// Parse la sortie AppleScript pour extraire les noms de listes
|
|
||||||
const lists = stdout.trim().split(', ').filter(list => list.length > 0);
|
|
||||||
return lists;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Erreur lors de la récupération des listes:', error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test si l'app Rappels est accessible
|
|
||||||
*/
|
|
||||||
async testRemindersAccess(): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
const script = `
|
|
||||||
tell application "Reminders"
|
|
||||||
return count of lists
|
|
||||||
end tell
|
|
||||||
`;
|
|
||||||
|
|
||||||
await execAsync(`osascript -e '${script.replace(/'/g, "'\\''")}' 2>/dev/null`);
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Impossible d\'accéder à l\'app Rappels:', error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse la sortie AppleScript en objets MacOSReminder
|
|
||||||
*/
|
|
||||||
private parseAppleScriptOutput(output: string): MacOSReminder[] {
|
|
||||||
try {
|
|
||||||
console.log('Sortie AppleScript brute:', output);
|
|
||||||
|
|
||||||
// Si pas de sortie ou sortie vide, retourner tableau vide
|
|
||||||
if (!output || output.trim() === '' || output.trim() === '{}') {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pour l'instant, on utilise une approche simple avec des données réelles
|
|
||||||
// TODO: Implémenter le parsing complet de la sortie AppleScript
|
|
||||||
return this.getRemindersFromEnabledLists();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Erreur lors du parsing AppleScript:', error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Récupère les rappels d'une liste avec une approche simple
|
|
||||||
*/
|
|
||||||
private async getRemindersFromListSimple(listName: string): Promise<MacOSReminder[]> {
|
|
||||||
try {
|
|
||||||
if (DEBUG_CONFIG.verboseLogging) {
|
|
||||||
console.log(`🔍 Récupération des rappels de la liste: ${listName}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Script simple pour récupérer les infos de base
|
|
||||||
const script = `
|
|
||||||
tell application "Reminders"
|
|
||||||
set remindersList to {}
|
|
||||||
try
|
|
||||||
set targetList to (first list whose name is "${listName}")
|
|
||||||
|
|
||||||
repeat with r in reminders of targetList
|
|
||||||
try
|
|
||||||
set reminderInfo to (name of r) & "|" & (completed of r) & "|" & (priority of r) & "|" & "${listName}"
|
|
||||||
set end of remindersList to reminderInfo
|
|
||||||
end try
|
|
||||||
end repeat
|
|
||||||
on error errMsg
|
|
||||||
return "ERROR: " & errMsg
|
|
||||||
end try
|
|
||||||
|
|
||||||
return remindersList
|
|
||||||
end tell
|
|
||||||
`;
|
|
||||||
|
|
||||||
const { stdout } = await execAsync(`osascript -e '${script.replace(/'/g, "'\\''")}' 2>/dev/null || echo ""`);
|
|
||||||
|
|
||||||
if (DEBUG_CONFIG.logAppleScript) {
|
|
||||||
console.log(`📝 Sortie AppleScript pour ${listName}:`, stdout.substring(0, 200));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vérifier si il y a une erreur dans la sortie
|
|
||||||
if (stdout.includes('ERROR:')) {
|
|
||||||
console.error(`❌ Erreur AppleScript pour ${listName}:`, stdout);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.parseSimpleReminderOutput(stdout, listName);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`❌ Erreur getRemindersFromListSimple pour ${listName}:`, error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse la sortie simple des rappels
|
|
||||||
*/
|
|
||||||
private parseSimpleReminderOutput(output: string, listName: string): MacOSReminder[] {
|
|
||||||
try {
|
|
||||||
if (!output || output.trim() === '') return [];
|
|
||||||
|
|
||||||
// Nettoyer la sortie AppleScript
|
|
||||||
const cleanOutput = output.trim().replace(/^{|}$/g, '');
|
|
||||||
if (!cleanOutput) return [];
|
|
||||||
|
|
||||||
const reminderStrings = cleanOutput.split(', ');
|
|
||||||
const reminders: MacOSReminder[] = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < reminderStrings.length; i++) {
|
|
||||||
const reminderStr = reminderStrings[i].replace(/"/g, '');
|
|
||||||
const parts = reminderStr.split('|');
|
|
||||||
|
|
||||||
if (parts.length >= 4) {
|
|
||||||
const [title, completed, priority, list] = parts;
|
|
||||||
|
|
||||||
reminders.push({
|
|
||||||
id: `${listName}-${i}`,
|
|
||||||
title: title.trim(),
|
|
||||||
completed: completed.trim() === 'true',
|
|
||||||
priority: parseInt(priority.trim()) || 0,
|
|
||||||
list: list.trim(),
|
|
||||||
tags: this.extractTagsFromTitle(title.trim())
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return reminders;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Erreur parseSimpleReminderOutput:', error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extrait les tags du titre (format #tag)
|
|
||||||
*/
|
|
||||||
private extractTagsFromTitle(title: string): string[] {
|
|
||||||
const tagRegex = /#(\w+)/g;
|
|
||||||
const tags: string[] = [];
|
|
||||||
let match;
|
|
||||||
|
|
||||||
while ((match = tagRegex.exec(title)) !== null) {
|
|
||||||
tags.push(match[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return tags;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Données de test pour le développement
|
|
||||||
*/
|
|
||||||
private getMockReminders(): MacOSReminder[] {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
id: 'mock-1',
|
|
||||||
title: 'Finir le service reminders',
|
|
||||||
notes: 'Implémenter la récupération des rappels macOS',
|
|
||||||
completed: false,
|
|
||||||
dueDate: new Date('2025-01-16'),
|
|
||||||
priority: 5,
|
|
||||||
list: 'Travail',
|
|
||||||
tags: ['dev', 'backend']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'mock-2',
|
|
||||||
title: 'Tester l\'intégration Jira',
|
|
||||||
notes: 'Configurer l\'API Jira pour récupérer les tâches',
|
|
||||||
completed: false,
|
|
||||||
dueDate: new Date('2025-01-18'),
|
|
||||||
priority: 9,
|
|
||||||
list: 'Projets',
|
|
||||||
tags: ['jira', 'api']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'mock-3',
|
|
||||||
title: 'Créer le Kanban board',
|
|
||||||
completed: true,
|
|
||||||
completionDate: new Date('2025-01-10'),
|
|
||||||
priority: 5,
|
|
||||||
list: 'Travail',
|
|
||||||
tags: ['ui', 'frontend']
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Instance singleton
|
|
||||||
export const remindersService = new RemindersService();
|
|
||||||
@@ -1,310 +0,0 @@
|
|||||||
import { prisma } from './database';
|
|
||||||
import { remindersService } from './reminders';
|
|
||||||
import { Task, TaskStatus, TaskPriority, MacOSReminder, SyncLog, BusinessError } from '@/lib/types';
|
|
||||||
import { Prisma } from '@prisma/client';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Service pour traiter et synchroniser les tâches
|
|
||||||
* Contient toute la logique métier pour les tâches
|
|
||||||
*/
|
|
||||||
export class TaskProcessorService {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Synchronise les rappels macOS avec la base de données
|
|
||||||
*/
|
|
||||||
async syncRemindersToDatabase(): Promise<SyncLog> {
|
|
||||||
const startTime = Date.now();
|
|
||||||
let tasksSync = 0;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Récupérer les rappels depuis macOS
|
|
||||||
const reminders = await remindersService.getAllReminders();
|
|
||||||
|
|
||||||
// Traiter chaque rappel
|
|
||||||
for (const reminder of reminders) {
|
|
||||||
await this.processReminder(reminder);
|
|
||||||
tasksSync++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Créer le log de synchronisation
|
|
||||||
const syncLog = await prisma.syncLog.create({
|
|
||||||
data: {
|
|
||||||
source: 'reminders',
|
|
||||||
status: 'success',
|
|
||||||
message: `Synchronisé ${tasksSync} rappels en ${Date.now() - startTime}ms`,
|
|
||||||
tasksSync
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`✅ Sync reminders terminée: ${tasksSync} tâches`);
|
|
||||||
return syncLog;
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
const errorMessage = error instanceof Error ? error.message : 'Erreur inconnue';
|
|
||||||
|
|
||||||
const syncLog = await prisma.syncLog.create({
|
|
||||||
data: {
|
|
||||||
source: 'reminders',
|
|
||||||
status: 'error',
|
|
||||||
message: `Erreur de sync: ${errorMessage}`,
|
|
||||||
tasksSync
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.error('❌ Erreur sync reminders:', error);
|
|
||||||
return syncLog;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Traite un rappel macOS et le sauvegarde/met à jour en base
|
|
||||||
*/
|
|
||||||
private async processReminder(reminder: MacOSReminder): Promise<void> {
|
|
||||||
const taskData = this.mapReminderToTask(reminder);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Upsert (insert ou update) de la tâche
|
|
||||||
await prisma.task.upsert({
|
|
||||||
where: {
|
|
||||||
source_sourceId: {
|
|
||||||
source: 'reminders',
|
|
||||||
sourceId: reminder.id
|
|
||||||
}
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
title: taskData.title,
|
|
||||||
description: taskData.description,
|
|
||||||
status: taskData.status,
|
|
||||||
priority: taskData.priority,
|
|
||||||
tagsJson: JSON.stringify(taskData.tags || []),
|
|
||||||
dueDate: taskData.dueDate,
|
|
||||||
completedAt: taskData.completedAt,
|
|
||||||
updatedAt: new Date()
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
title: taskData.title,
|
|
||||||
description: taskData.description,
|
|
||||||
status: taskData.status,
|
|
||||||
priority: taskData.priority,
|
|
||||||
source: 'reminders',
|
|
||||||
sourceId: reminder.id,
|
|
||||||
tagsJson: JSON.stringify(taskData.tags || []),
|
|
||||||
dueDate: taskData.dueDate,
|
|
||||||
completedAt: taskData.completedAt
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Gérer les tags
|
|
||||||
if (taskData.tags && taskData.tags.length > 0) {
|
|
||||||
await this.processTags(taskData.tags);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Erreur lors du traitement du rappel ${reminder.id}:`, error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convertit un rappel macOS en objet Task
|
|
||||||
*/
|
|
||||||
private mapReminderToTask(reminder: MacOSReminder): Partial<Task> {
|
|
||||||
return {
|
|
||||||
title: reminder.title,
|
|
||||||
description: reminder.notes || undefined,
|
|
||||||
status: this.mapReminderStatus(reminder),
|
|
||||||
priority: this.mapReminderPriority(reminder.priority),
|
|
||||||
tags: reminder.tags || [],
|
|
||||||
dueDate: reminder.dueDate || undefined,
|
|
||||||
completedAt: reminder.completionDate || undefined
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convertit le statut d'un rappel macOS en TaskStatus
|
|
||||||
*/
|
|
||||||
private mapReminderStatus(reminder: MacOSReminder): TaskStatus {
|
|
||||||
if (reminder.completed) {
|
|
||||||
return 'done';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Si la tâche a une date d'échéance passée, elle est en retard
|
|
||||||
if (reminder.dueDate && reminder.dueDate < new Date()) {
|
|
||||||
return 'todo'; // On garde 'todo' mais on pourrait ajouter un statut 'overdue'
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'todo';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convertit la priorité macOS (0-9) en TaskPriority
|
|
||||||
*/
|
|
||||||
private mapReminderPriority(macosPriority: number): TaskPriority {
|
|
||||||
switch (macosPriority) {
|
|
||||||
case 0: return 'low';
|
|
||||||
case 1: return 'low';
|
|
||||||
case 5: return 'medium';
|
|
||||||
case 9: return 'high';
|
|
||||||
default: return 'medium';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Traite et crée les tags s'ils n'existent pas
|
|
||||||
*/
|
|
||||||
private async processTags(tagNames: string[]): Promise<void> {
|
|
||||||
for (const tagName of tagNames) {
|
|
||||||
try {
|
|
||||||
await prisma.tag.upsert({
|
|
||||||
where: { name: tagName },
|
|
||||||
update: {}, // Pas de mise à jour nécessaire
|
|
||||||
create: {
|
|
||||||
name: tagName,
|
|
||||||
color: this.generateTagColor(tagName)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Erreur lors de la création du tag ${tagName}:`, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Génère une couleur pour un tag basée sur son nom
|
|
||||||
*/
|
|
||||||
private generateTagColor(tagName: string): string {
|
|
||||||
const colors = [
|
|
||||||
'#ef4444', '#f97316', '#f59e0b', '#eab308',
|
|
||||||
'#84cc16', '#22c55e', '#10b981', '#14b8a6',
|
|
||||||
'#06b6d4', '#0ea5e9', '#3b82f6', '#6366f1',
|
|
||||||
'#8b5cf6', '#a855f7', '#d946ef', '#ec4899'
|
|
||||||
];
|
|
||||||
|
|
||||||
// Hash simple du nom pour choisir une couleur
|
|
||||||
let hash = 0;
|
|
||||||
for (let i = 0; i < tagName.length; i++) {
|
|
||||||
hash = tagName.charCodeAt(i) + ((hash << 5) - hash);
|
|
||||||
}
|
|
||||||
|
|
||||||
return colors[Math.abs(hash) % colors.length];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Récupère toutes les tâches avec filtres optionnels
|
|
||||||
*/
|
|
||||||
async getTasks(filters?: {
|
|
||||||
status?: TaskStatus[];
|
|
||||||
source?: string[];
|
|
||||||
search?: string;
|
|
||||||
limit?: number;
|
|
||||||
offset?: number;
|
|
||||||
}): Promise<Task[]> {
|
|
||||||
const where: Prisma.TaskWhereInput = {};
|
|
||||||
|
|
||||||
if (filters?.status) {
|
|
||||||
where.status = { in: filters.status };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filters?.source) {
|
|
||||||
where.source = { in: filters.source };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filters?.search) {
|
|
||||||
where.OR = [
|
|
||||||
{ title: { contains: filters.search, mode: 'insensitive' } },
|
|
||||||
{ description: { contains: filters.search, mode: 'insensitive' } }
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
const tasks = await prisma.task.findMany({
|
|
||||||
where,
|
|
||||||
take: filters?.limit || 100,
|
|
||||||
skip: filters?.offset || 0,
|
|
||||||
orderBy: [
|
|
||||||
{ completedAt: 'desc' },
|
|
||||||
{ dueDate: 'asc' },
|
|
||||||
{ createdAt: 'desc' }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
return tasks.map(this.mapPrismaTaskToTask);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Met à jour le statut d'une tâche
|
|
||||||
*/
|
|
||||||
async updateTaskStatus(taskId: string, newStatus: TaskStatus): Promise<Task> {
|
|
||||||
const task = await prisma.task.findUnique({
|
|
||||||
where: { id: taskId }
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!task) {
|
|
||||||
throw new BusinessError(`Tâche ${taskId} introuvable`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Logique métier : si on marque comme terminé, on ajoute la date
|
|
||||||
const updateData: Prisma.TaskUpdateInput = {
|
|
||||||
status: newStatus,
|
|
||||||
updatedAt: new Date()
|
|
||||||
};
|
|
||||||
|
|
||||||
if (newStatus === 'done' && !task.completedAt) {
|
|
||||||
updateData.completedAt = new Date();
|
|
||||||
} else if (newStatus !== 'done' && task.completedAt) {
|
|
||||||
updateData.completedAt = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedTask = await prisma.task.update({
|
|
||||||
where: { id: taskId },
|
|
||||||
data: updateData
|
|
||||||
});
|
|
||||||
|
|
||||||
return this.mapPrismaTaskToTask(updatedTask);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convertit une tâche Prisma en objet Task
|
|
||||||
*/
|
|
||||||
private mapPrismaTaskToTask(prismaTask: any): Task {
|
|
||||||
return {
|
|
||||||
id: prismaTask.id,
|
|
||||||
title: prismaTask.title,
|
|
||||||
description: prismaTask.description,
|
|
||||||
status: prismaTask.status as TaskStatus,
|
|
||||||
priority: prismaTask.priority as TaskPriority,
|
|
||||||
source: prismaTask.source,
|
|
||||||
sourceId: prismaTask.sourceId,
|
|
||||||
tags: JSON.parse(prismaTask.tagsJson || '[]'),
|
|
||||||
dueDate: prismaTask.dueDate,
|
|
||||||
completedAt: prismaTask.completedAt,
|
|
||||||
createdAt: prismaTask.createdAt,
|
|
||||||
updatedAt: prismaTask.updatedAt,
|
|
||||||
jiraProject: prismaTask.jiraProject,
|
|
||||||
jiraKey: prismaTask.jiraKey,
|
|
||||||
assignee: prismaTask.assignee
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Récupère les statistiques des tâches
|
|
||||||
*/
|
|
||||||
async getTaskStats() {
|
|
||||||
const [total, completed, inProgress, todo] = await Promise.all([
|
|
||||||
prisma.task.count(),
|
|
||||||
prisma.task.count({ where: { status: 'done' } }),
|
|
||||||
prisma.task.count({ where: { status: 'in_progress' } }),
|
|
||||||
prisma.task.count({ where: { status: 'todo' } })
|
|
||||||
]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
total,
|
|
||||||
completed,
|
|
||||||
inProgress,
|
|
||||||
todo,
|
|
||||||
completionRate: total > 0 ? Math.round((completed / total) * 100) : 0
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Instance singleton
|
|
||||||
export const taskProcessorService = new TaskProcessorService();
|
|
||||||
237
services/tasks.ts
Normal file
237
services/tasks.ts
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
import { prisma } from './database';
|
||||||
|
import { Task, TaskStatus, TaskPriority, TaskSource, BusinessError } from '@/lib/types';
|
||||||
|
import { Prisma } from '@prisma/client';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service pour la gestion des tâches (version standalone)
|
||||||
|
*/
|
||||||
|
export class TasksService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère toutes les tâches avec filtres optionnels
|
||||||
|
*/
|
||||||
|
async getTasks(filters?: {
|
||||||
|
status?: TaskStatus[];
|
||||||
|
search?: string;
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
}): Promise<Task[]> {
|
||||||
|
const where: Prisma.TaskWhereInput = {};
|
||||||
|
|
||||||
|
if (filters?.status) {
|
||||||
|
where.status = { in: filters.status };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters?.search) {
|
||||||
|
where.OR = [
|
||||||
|
{ title: { contains: filters.search } },
|
||||||
|
{ description: { contains: filters.search } }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
const tasks = await prisma.task.findMany({
|
||||||
|
where,
|
||||||
|
take: filters?.limit || 100,
|
||||||
|
skip: filters?.offset || 0,
|
||||||
|
orderBy: [
|
||||||
|
{ completedAt: 'desc' },
|
||||||
|
{ dueDate: 'asc' },
|
||||||
|
{ createdAt: 'desc' }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
return tasks.map(this.mapPrismaTaskToTask);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée une nouvelle tâche
|
||||||
|
*/
|
||||||
|
async createTask(taskData: {
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
status?: TaskStatus;
|
||||||
|
priority?: TaskPriority;
|
||||||
|
tags?: string[];
|
||||||
|
dueDate?: Date;
|
||||||
|
}): Promise<Task> {
|
||||||
|
const task = await prisma.task.create({
|
||||||
|
data: {
|
||||||
|
title: taskData.title,
|
||||||
|
description: taskData.description,
|
||||||
|
status: taskData.status || 'todo',
|
||||||
|
priority: taskData.priority || 'medium',
|
||||||
|
tagsJson: JSON.stringify(taskData.tags || []),
|
||||||
|
dueDate: taskData.dueDate,
|
||||||
|
source: 'manual', // Source manuelle
|
||||||
|
sourceId: `manual-${Date.now()}` // ID unique
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Gérer les tags
|
||||||
|
if (taskData.tags && taskData.tags.length > 0) {
|
||||||
|
await this.processTags(taskData.tags);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.mapPrismaTaskToTask(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Met à jour une tâche
|
||||||
|
*/
|
||||||
|
async updateTask(taskId: string, updates: {
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
status?: TaskStatus;
|
||||||
|
priority?: TaskPriority;
|
||||||
|
tags?: string[];
|
||||||
|
dueDate?: Date;
|
||||||
|
}): Promise<Task> {
|
||||||
|
const task = await prisma.task.findUnique({
|
||||||
|
where: { id: taskId }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!task) {
|
||||||
|
throw new BusinessError(`Tâche ${taskId} introuvable`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logique métier : si on marque comme terminé, on ajoute la date
|
||||||
|
const updateData: Prisma.TaskUpdateInput = {
|
||||||
|
...updates,
|
||||||
|
updatedAt: new Date()
|
||||||
|
};
|
||||||
|
|
||||||
|
if (updates.tags) {
|
||||||
|
updateData.tagsJson = JSON.stringify(updates.tags);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updates.status === 'done' && !task.completedAt) {
|
||||||
|
updateData.completedAt = new Date();
|
||||||
|
} else if (updates.status && updates.status !== 'done' && task.completedAt) {
|
||||||
|
updateData.completedAt = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedTask = await prisma.task.update({
|
||||||
|
where: { id: taskId },
|
||||||
|
data: updateData
|
||||||
|
});
|
||||||
|
|
||||||
|
// Gérer les tags
|
||||||
|
if (updates.tags && updates.tags.length > 0) {
|
||||||
|
await this.processTags(updates.tags);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.mapPrismaTaskToTask(updatedTask);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supprime une tâche
|
||||||
|
*/
|
||||||
|
async deleteTask(taskId: string): Promise<void> {
|
||||||
|
const task = await prisma.task.findUnique({
|
||||||
|
where: { id: taskId }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!task) {
|
||||||
|
throw new BusinessError(`Tâche ${taskId} introuvable`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.task.delete({
|
||||||
|
where: { id: taskId }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Met à jour le statut d'une tâche
|
||||||
|
*/
|
||||||
|
async updateTaskStatus(taskId: string, newStatus: TaskStatus): Promise<Task> {
|
||||||
|
return this.updateTask(taskId, { status: newStatus });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les statistiques des tâches
|
||||||
|
*/
|
||||||
|
async getTaskStats() {
|
||||||
|
const [total, completed, inProgress, todo, cancelled] = await Promise.all([
|
||||||
|
prisma.task.count(),
|
||||||
|
prisma.task.count({ where: { status: 'done' } }),
|
||||||
|
prisma.task.count({ where: { status: 'in_progress' } }),
|
||||||
|
prisma.task.count({ where: { status: 'todo' } }),
|
||||||
|
prisma.task.count({ where: { status: 'cancelled' } })
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
total,
|
||||||
|
completed,
|
||||||
|
inProgress,
|
||||||
|
todo,
|
||||||
|
cancelled,
|
||||||
|
completionRate: total > 0 ? Math.round((completed / total) * 100) : 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Traite et crée les tags s'ils n'existent pas
|
||||||
|
*/
|
||||||
|
private async processTags(tagNames: string[]): Promise<void> {
|
||||||
|
for (const tagName of tagNames) {
|
||||||
|
try {
|
||||||
|
await prisma.tag.upsert({
|
||||||
|
where: { name: tagName },
|
||||||
|
update: {}, // Pas de mise à jour nécessaire
|
||||||
|
create: {
|
||||||
|
name: tagName,
|
||||||
|
color: this.generateTagColor(tagName)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Erreur lors de la création du tag ${tagName}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Génère une couleur pour un tag basée sur son nom
|
||||||
|
*/
|
||||||
|
private generateTagColor(tagName: string): string {
|
||||||
|
const colors = [
|
||||||
|
'#ef4444', '#f97316', '#f59e0b', '#eab308',
|
||||||
|
'#84cc16', '#22c55e', '#10b981', '#14b8a6',
|
||||||
|
'#06b6d4', '#0ea5e9', '#3b82f6', '#6366f1',
|
||||||
|
'#8b5cf6', '#a855f7', '#d946ef', '#ec4899'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Hash simple du nom pour choisir une couleur
|
||||||
|
let hash = 0;
|
||||||
|
for (let i = 0; i < tagName.length; i++) {
|
||||||
|
hash = tagName.charCodeAt(i) + ((hash << 5) - hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
return colors[Math.abs(hash) % colors.length];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convertit une tâche Prisma en objet Task
|
||||||
|
*/
|
||||||
|
private mapPrismaTaskToTask(prismaTask: Prisma.TaskGetPayload<object>): Task {
|
||||||
|
return {
|
||||||
|
id: prismaTask.id,
|
||||||
|
title: prismaTask.title,
|
||||||
|
description: prismaTask.description ?? undefined,
|
||||||
|
status: prismaTask.status as TaskStatus,
|
||||||
|
priority: prismaTask.priority as TaskPriority,
|
||||||
|
source: prismaTask.source as TaskSource,
|
||||||
|
sourceId: prismaTask.sourceId?? undefined,
|
||||||
|
tags: JSON.parse(prismaTask.tagsJson || '[]'),
|
||||||
|
dueDate: prismaTask.dueDate ?? undefined,
|
||||||
|
completedAt: prismaTask.completedAt ?? undefined,
|
||||||
|
createdAt: prismaTask.createdAt,
|
||||||
|
updatedAt: prismaTask.updatedAt,
|
||||||
|
jiraProject: prismaTask.jiraProject ?? undefined,
|
||||||
|
jiraKey: prismaTask.jiraKey ?? undefined,
|
||||||
|
assignee: prismaTask.assignee ?? undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Instance singleton
|
||||||
|
export const tasksService = new TasksService();
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
import { NextResponse } from 'next/server';
|
|
||||||
import { getConfig, getTargetRemindersList, getEnabledRemindersLists } from '@/lib/config';
|
|
||||||
import { remindersService } from '@/services/reminders';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* API route pour récupérer la configuration actuelle
|
|
||||||
*/
|
|
||||||
export async function GET() {
|
|
||||||
try {
|
|
||||||
const config = getConfig();
|
|
||||||
const availableLists = await remindersService.getReminderLists();
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
config,
|
|
||||||
availableLists,
|
|
||||||
currentTarget: getTargetRemindersList(),
|
|
||||||
enabledLists: getEnabledRemindersLists()
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Erreur lors de la récupération de la config:', error);
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: false,
|
|
||||||
error: error instanceof Error ? error.message : 'Erreur inconnue'
|
|
||||||
}, { status: 500 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* API route pour tester l'accès à une liste spécifique
|
|
||||||
*/
|
|
||||||
export async function POST(request: Request) {
|
|
||||||
try {
|
|
||||||
const body = await request.json();
|
|
||||||
const { listName, action } = body;
|
|
||||||
|
|
||||||
if (!listName) {
|
|
||||||
return NextResponse.json({
|
|
||||||
success: false,
|
|
||||||
error: 'listName est requis'
|
|
||||||
}, { status: 400 });
|
|
||||||
}
|
|
||||||
|
|
||||||
let result: any = {};
|
|
||||||
|
|
||||||
switch (action) {
|
|
||||||
case 'test':
|
|
||||||
// Tester l'accès à une liste spécifique
|
|
||||||
const reminders = await remindersService.getRemindersByList(listName);
|
|
||||||
result = {
|
|
||||||
listName,
|
|
||||||
accessible: true,
|
|
||||||
reminderCount: reminders.length,
|
|
||||||
sample: reminders.slice(0, 3).map(r => ({
|
|
||||||
title: r.title,
|
|
||||||
completed: r.completed,
|
|
||||||
priority: r.priority
|
|
||||||
}))
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'preview':
|
|
||||||
// Prévisualiser les rappels d'une liste
|
|
||||||
const previewReminders = await remindersService.getRemindersByList(listName);
|
|
||||||
result = {
|
|
||||||
listName,
|
|
||||||
reminders: previewReminders.map(r => ({
|
|
||||||
id: r.id,
|
|
||||||
title: r.title,
|
|
||||||
completed: r.completed,
|
|
||||||
priority: r.priority,
|
|
||||||
tags: r.tags || []
|
|
||||||
}))
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
return NextResponse.json({
|
|
||||||
success: false,
|
|
||||||
error: 'Action non supportée. Utilisez "test" ou "preview"'
|
|
||||||
}, { status: 400 });
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
action,
|
|
||||||
result
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Erreur lors du test de liste:', error);
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: false,
|
|
||||||
error: error instanceof Error ? error.message : 'Erreur inconnue'
|
|
||||||
}, { status: 500 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
import { NextResponse } from 'next/server';
|
|
||||||
import { taskProcessorService } from '@/services/task-processor';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* API route pour synchroniser les rappels macOS avec la base de données
|
|
||||||
*/
|
|
||||||
export async function POST() {
|
|
||||||
try {
|
|
||||||
console.log('🔄 Début de la synchronisation des rappels...');
|
|
||||||
|
|
||||||
const syncResult = await taskProcessorService.syncRemindersToDatabase();
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
message: 'Synchronisation des rappels terminée',
|
|
||||||
syncLog: syncResult
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Erreur lors de la synchronisation:', error);
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: false,
|
|
||||||
error: error instanceof Error ? error.message : 'Erreur inconnue lors de la synchronisation'
|
|
||||||
}, { status: 500 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* API route pour obtenir le statut de la dernière synchronisation
|
|
||||||
*/
|
|
||||||
export async function GET() {
|
|
||||||
try {
|
|
||||||
// Récupérer les derniers logs de sync
|
|
||||||
const { prisma } = await import('@/services/database');
|
|
||||||
|
|
||||||
const lastSyncLogs = await prisma.syncLog.findMany({
|
|
||||||
where: { source: 'reminders' },
|
|
||||||
orderBy: { createdAt: 'desc' },
|
|
||||||
take: 5
|
|
||||||
});
|
|
||||||
|
|
||||||
const taskStats = await taskProcessorService.getTaskStats();
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
lastSyncLogs,
|
|
||||||
taskStats,
|
|
||||||
message: 'Statut de synchronisation récupéré'
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Erreur lors de la récupération du statut:', error);
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: false,
|
|
||||||
error: error instanceof Error ? error.message : 'Erreur inconnue'
|
|
||||||
}, { status: 500 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { taskProcessorService } from '@/services/task-processor';
|
import { tasksService } from '@/services/tasks';
|
||||||
import { TaskStatus } from '@/lib/types';
|
import { TaskStatus, TaskPriority } from '@/lib/types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* API route pour récupérer les tâches avec filtres optionnels
|
* API route pour récupérer les tâches avec filtres optionnels
|
||||||
@@ -10,7 +10,13 @@ export async function GET(request: Request) {
|
|||||||
const { searchParams } = new URL(request.url);
|
const { searchParams } = new URL(request.url);
|
||||||
|
|
||||||
// Extraire les paramètres de filtre
|
// Extraire les paramètres de filtre
|
||||||
const filters: any = {};
|
const filters: {
|
||||||
|
status?: TaskStatus[];
|
||||||
|
source?: string[];
|
||||||
|
search?: string;
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
} = {};
|
||||||
|
|
||||||
const status = searchParams.get('status');
|
const status = searchParams.get('status');
|
||||||
if (status) {
|
if (status) {
|
||||||
@@ -38,8 +44,8 @@ export async function GET(request: Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Récupérer les tâches
|
// Récupérer les tâches
|
||||||
const tasks = await taskProcessorService.getTasks(filters);
|
const tasks = await tasksService.getTasks(filters);
|
||||||
const stats = await taskProcessorService.getTaskStats();
|
const stats = await tasksService.getTaskStats();
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -60,26 +66,71 @@ export async function GET(request: Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* API route pour mettre à jour le statut d'une tâche
|
* API route pour créer une nouvelle tâche
|
||||||
|
*/
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
try {
|
||||||
|
const body = await request.json();
|
||||||
|
const { title, description, status, priority, tags, dueDate } = body;
|
||||||
|
|
||||||
|
if (!title) {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: 'Le titre est requis'
|
||||||
|
}, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const task = await tasksService.createTask({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
status: status as TaskStatus,
|
||||||
|
priority: priority as TaskPriority,
|
||||||
|
tags,
|
||||||
|
dueDate: dueDate ? new Date(dueDate) : undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
data: task,
|
||||||
|
message: 'Tâche créée avec succès'
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Erreur lors de la création de la tâche:', error);
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Erreur inconnue'
|
||||||
|
}, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API route pour mettre à jour une tâche
|
||||||
*/
|
*/
|
||||||
export async function PATCH(request: Request) {
|
export async function PATCH(request: Request) {
|
||||||
try {
|
try {
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const { taskId, status } = body;
|
const { taskId, ...updates } = body;
|
||||||
|
|
||||||
if (!taskId || !status) {
|
if (!taskId) {
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: false,
|
success: false,
|
||||||
error: 'taskId et status sont requis'
|
error: 'taskId est requis'
|
||||||
}, { status: 400 });
|
}, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedTask = await taskProcessorService.updateTaskStatus(taskId, status);
|
// Convertir dueDate si présent
|
||||||
|
if (updates.dueDate) {
|
||||||
|
updates.dueDate = new Date(updates.dueDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedTask = await tasksService.updateTask(taskId, updates);
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: updatedTask,
|
data: updatedTask,
|
||||||
message: `Tâche ${taskId} mise à jour avec le statut ${status}`
|
message: 'Tâche mise à jour avec succès'
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -91,3 +142,35 @@ export async function PATCH(request: Request) {
|
|||||||
}, { status: 500 });
|
}, { status: 500 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API route pour supprimer une tâche
|
||||||
|
*/
|
||||||
|
export async function DELETE(request: Request) {
|
||||||
|
try {
|
||||||
|
const { searchParams } = new URL(request.url);
|
||||||
|
const taskId = searchParams.get('taskId');
|
||||||
|
|
||||||
|
if (!taskId) {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: 'taskId est requis'
|
||||||
|
}, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
await tasksService.deleteTask(taskId);
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Tâche supprimée avec succès'
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Erreur lors de la suppression de la tâche:', error);
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Erreur inconnue'
|
||||||
|
}, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
import { NextResponse } from 'next/server';
|
|
||||||
import { testDatabaseConnection } from '@/services/database';
|
|
||||||
import { remindersService } from '@/services/reminders';
|
|
||||||
import { taskProcessorService } from '@/services/task-processor';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* API route de test pour vérifier que tous les services fonctionnent
|
|
||||||
*/
|
|
||||||
export async function GET() {
|
|
||||||
try {
|
|
||||||
const results = {
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
database: false,
|
|
||||||
reminders: false,
|
|
||||||
taskProcessor: false,
|
|
||||||
reminderLists: [] as string[],
|
|
||||||
taskStats: null as any
|
|
||||||
};
|
|
||||||
|
|
||||||
// Test de la base de données
|
|
||||||
try {
|
|
||||||
results.database = await testDatabaseConnection();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Test DB failed:', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test de l'accès aux rappels
|
|
||||||
try {
|
|
||||||
results.reminders = await remindersService.testRemindersAccess();
|
|
||||||
if (results.reminders) {
|
|
||||||
results.reminderLists = await remindersService.getReminderLists();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Test Reminders failed:', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test du service de traitement des tâches
|
|
||||||
try {
|
|
||||||
results.taskStats = await taskProcessorService.getTaskStats();
|
|
||||||
results.taskProcessor = true;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Test TaskProcessor failed:', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
message: 'Tests des services terminés',
|
|
||||||
results
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Erreur dans l\'API de test:', error);
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: false,
|
|
||||||
error: error instanceof Error ? error.message : 'Erreur inconnue'
|
|
||||||
}, { status: 500 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--background: #ffffff;
|
--background: #020617; /* slate-950 */
|
||||||
--foreground: #171717;
|
--foreground: #f1f5f9; /* slate-100 */
|
||||||
}
|
}
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
@@ -12,15 +12,38 @@
|
|||||||
--font-mono: var(--font-geist-mono);
|
--font-mono: var(--font-geist-mono);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:root {
|
|
||||||
--background: #0a0a0a;
|
|
||||||
--foreground: #ededed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
font-family: var(--font-geist-mono), 'Courier New', monospace;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar tech style */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: #1e293b; /* slate-800 */
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: #475569; /* slate-600 */
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #06b6d4; /* cyan-500 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animations tech */
|
||||||
|
@keyframes glow {
|
||||||
|
0%, 100% { box-shadow: 0 0 5px rgba(6, 182, 212, 0.3); }
|
||||||
|
50% { box-shadow: 0 0 20px rgba(6, 182, 212, 0.6); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-glow {
|
||||||
|
animation: glow 2s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,23 @@
|
|||||||
import { taskProcessorService } from '@/services/task-processor';
|
import { tasksService } from '@/services/tasks';
|
||||||
import { getTargetRemindersList } from '@/lib/config';
|
|
||||||
import { KanbanBoard } from '../../components/kanban/Board';
|
import { KanbanBoard } from '../../components/kanban/Board';
|
||||||
import { Header } from '../../components/ui/Header';
|
import { Header } from '../../components/ui/Header';
|
||||||
|
|
||||||
export default async function HomePage() {
|
export default async function HomePage() {
|
||||||
// SSR - Récupération des données côté serveur
|
// SSR - Récupération des données côté serveur (focus sur les tâches récentes)
|
||||||
const [tasks, stats] = await Promise.all([
|
const [tasks, stats] = await Promise.all([
|
||||||
taskProcessorService.getTasks({ limit: 100 }),
|
tasksService.getTasks({ limit: 20 }), // Réduire pour voir les nouvelles tâches
|
||||||
taskProcessorService.getTaskStats()
|
tasksService.getTaskStats()
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const targetList = getTargetRemindersList();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
|
<div className="min-h-screen bg-slate-950">
|
||||||
<Header
|
<Header
|
||||||
title="TowerControl"
|
title="TowerControl"
|
||||||
subtitle={`Tâches synchronisées depuis "${targetList}"`}
|
subtitle="Gestionnaire de tâches moderne"
|
||||||
stats={stats}
|
stats={stats}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<main className="container mx-auto px-4 py-6">
|
<main className="h-[calc(100vh-120px)]">
|
||||||
<KanbanBoard tasks={tasks} />
|
<KanbanBoard tasks={tasks} />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user