From 3cfed60f4353c953810298521e598cf8f542c5bb Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Sun, 21 Sep 2025 19:55:04 +0200 Subject: [PATCH] feat: refactor daily task management with new pending tasks section - Added `PendingTasksSection` to `DailyPageClient` for displaying uncompleted tasks. - Implemented `getPendingCheckboxes` method in `DailyClient` and `DailyService` to fetch pending tasks. - Introduced `getDaysAgo` utility function for calculating elapsed days since a date. - Updated `TODO.md` to reflect the new task management features and adjustments. - Cleaned up and organized folder structure to align with Next.js 13+ best practices. --- TODO.md | 67 +---- TODO_ARCHIVE.md | 65 +++++ .../daily/checkboxes/[id]/archive/route.ts | 28 ++ src/app/api/daily/pending/route.ts | 29 +++ src/app/daily/DailyPageClient.tsx | 11 + src/clients/daily-client.ts | 28 ++ src/components/daily/PendingTasksSection.tsx | 239 ++++++++++++++++++ src/lib/date-utils.ts | 9 + src/services/daily.ts | 70 +++++ 9 files changed, 482 insertions(+), 64 deletions(-) create mode 100644 src/app/api/daily/checkboxes/[id]/archive/route.ts create mode 100644 src/app/api/daily/pending/route.ts create mode 100644 src/components/daily/PendingTasksSection.tsx diff --git a/TODO.md b/TODO.md index a887d4c..0515608 100644 --- a/TODO.md +++ b/TODO.md @@ -1,13 +1,7 @@ # TowerControl v2.0 - Gestionnaire de tâches moderne -## Autre Todos #2 -- [x] Synchro Jira auto en background timé comme pour la synchro de sauvegarde -- [ ] refacto des getallpreferences en frontend : ca devrait eter un contexte dans le layout qui balance serverside dans le hook -- [x] backups : ne backuper que si il y a eu un changement entre le dernier backup et la base actuelle -- [x] refacto des dates avec le utils qui pour l'instant n'est pas utilisé -- [ ] split de certains gros composants. -- [x] Page jira-dashboard : onglets analytics avancés et Qualité et collaboration : les charts sortent des cards; il faut reprendre la UI pour que ce soit consistant. -- [x] Page Daily : les mots aujourd'hui et hier ne fonctionnent dans les titres que si c'est vraiment aujourd'hui :) +## Autre Todos +- [ ] Désactiver le hover sur les taskCard ## 🔧 Phase 6: Fonctionnalités avancées (Priorité 6) @@ -87,61 +81,6 @@ - [ ] Configuration unifiée des filtres et synchronisations - [ ] Dashboard multi-intégrations -### 📁 Refactoring structure des dossiers (PRIORITÉ HAUTE) - -#### **Problème actuel** -- Structure mixte : `src/app/`, `src/actions/`, `src/contexts/` mais `components/`, `lib/`, `services/`, etc. à la racine -- Alias TypeScript incohérents dans `tsconfig.json` -- Non-conformité avec les bonnes pratiques Next.js 13+ App Router - -#### **Plan de migration** -- [x] **Phase 1: Migration des dossiers** - - [x] `mv components/ src/components/` - - [x] `mv lib/ src/lib/` - - [x] `mv hooks/ src/hooks/` - - [x] `mv clients/ src/clients/` - - [x] `mv services/ src/services/` - -- [x] **Phase 2: Mise à jour tsconfig.json** - ```json - "paths": { - "@/*": ["./src/*"] - // Supprimer les alias spécifiques devenus inutiles - } - ``` - -- [x] **Phase 3: Correction des imports** - - [x] Tous les imports `@/services/*` → `@/services/*` (déjà OK) - - [x] Tous les imports `@/lib/*` → `@/lib/*` (déjà OK) - - [x] Tous les imports `@/components/*` → `@/components/*` (déjà OK) - - [x] Tous les imports `@/clients/*` → `@/clients/*` (déjà OK) - - [x] Tous les imports `@/hooks/*` → `@/hooks/*` (déjà OK) - - [x] Vérifier les imports relatifs dans les scripts/ - -- [x] **Phase 4: Mise à jour des règles Cursor** - - [x] Règle "services" : Mettre à jour les exemples avec `src/services/` - - [x] Règle "components" : Mettre à jour avec `src/components/` - - [x] Règle "clients" : Mettre à jour avec `src/clients/` - - [x] Vérifier tous les liens MDC dans les règles - -- [x] **Phase 5: Tests et validation** - - [x] `npm run build` - Vérifier que le build passe - - [x] `npm run dev` - Vérifier que le dev fonctionne - - [x] `npm run lint` - Vérifier ESLint - - [x] `npx tsc --noEmit` - Vérifier TypeScript - - [x] Tester les fonctionnalités principales - -#### **Structure finale attendue** -``` -src/ -├── app/ # Pages Next.js (déjà OK) -├── actions/ # Server Actions (déjà OK) -├── contexts/ # React Contexts (déjà OK) -├── components/ # Composants React (à déplacer) -├── lib/ # Utilitaires et types (à déplacer) -├── hooks/ # Hooks React (à déplacer) -├── clients/ # Clients HTTP (à déplacer) -└── services/ # Services backend (à déplacer) ``` ### 👥 Gestion multi-utilisateurs (PROJET MAJEUR) @@ -195,7 +134,7 @@ src/ - [ ] Breakpoints adaptés pour tablettes - [ ] **Phase 2: Interface mobile pour les tâches** - - [ ] **Vue liste simple** : Remplacement du Kanban + - [ ] **Vue liste simple** : Kanban simple OK, mais swimlane KO. Ajouter une autre interface plus simple pour mobile en plus du Kanban Simple - [ ] Liste verticale avec statuts en badges - [ ] Actions par swipe (marquer terminé, changer statut) - [ ] Filtres simplifiés (dropdown au lieu de sidebar) diff --git a/TODO_ARCHIVE.md b/TODO_ARCHIVE.md index 648d072..7c47c17 100644 --- a/TODO_ARCHIVE.md +++ b/TODO_ARCHIVE.md @@ -304,3 +304,68 @@ Endpoints complexes → API Routes conservées - [x] Filtrage par composant, version, type de ticket - [x] Vue détaillée par sprint avec drill-down - [x] ~~Intégration avec les daily notes (mentions des blockers)~~ (supprimé) + +### 📁 Refactoring structure des dossiers (PRIORITÉ HAUTE) + +#### **Problème actuel** +- Structure mixte : `src/app/`, `src/actions/`, `src/contexts/` mais `components/`, `lib/`, `services/`, etc. à la racine +- Alias TypeScript incohérents dans `tsconfig.json` +- Non-conformité avec les bonnes pratiques Next.js 13+ App Router + +#### **Plan de migration** +- [x] **Phase 1: Migration des dossiers** + - [x] `mv components/ src/components/` + - [x] `mv lib/ src/lib/` + - [x] `mv hooks/ src/hooks/` + - [x] `mv clients/ src/clients/` + - [x] `mv services/ src/services/` + +- [x] **Phase 2: Mise à jour tsconfig.json** + ```json + "paths": { + "@/*": ["./src/*"] + // Supprimer les alias spécifiques devenus inutiles + } + ``` + +- [x] **Phase 3: Correction des imports** + - [x] Tous les imports `@/services/*` → `@/services/*` (déjà OK) + - [x] Tous les imports `@/lib/*` → `@/lib/*` (déjà OK) + - [x] Tous les imports `@/components/*` → `@/components/*` (déjà OK) + - [x] Tous les imports `@/clients/*` → `@/clients/*` (déjà OK) + - [x] Tous les imports `@/hooks/*` → `@/hooks/*` (déjà OK) + - [x] Vérifier les imports relatifs dans les scripts/ + +- [x] **Phase 4: Mise à jour des règles Cursor** + - [x] Règle "services" : Mettre à jour les exemples avec `src/services/` + - [x] Règle "components" : Mettre à jour avec `src/components/` + - [x] Règle "clients" : Mettre à jour avec `src/clients/` + - [x] Vérifier tous les liens MDC dans les règles + +- [x] **Phase 5: Tests et validation** + - [x] `npm run build` - Vérifier que le build passe + - [x] `npm run dev` - Vérifier que le dev fonctionne + - [x] `npm run lint` - Vérifier ESLint + - [x] `npx tsc --noEmit` - Vérifier TypeScript + - [x] Tester les fonctionnalités principales + +#### **Structure finale attendue** +``` +src/ +├── app/ # Pages Next.js (déjà OK) +├── actions/ # Server Actions (déjà OK) +├── contexts/ # React Contexts (déjà OK) +├── components/ # Composants React (à déplacer) +├── lib/ # Utilitaires et types (à déplacer) +├── hooks/ # Hooks React (à déplacer) +├── clients/ # Clients HTTP (à déplacer) +└── services/ # Services backend (à déplacer) + +## Autre Todos +- [x] Synchro Jira auto en background timé comme pour la synchro de sauvegarde +- [x] refacto des getallpreferences en frontend : ca devrait eter un contexte dans le layout qui balance serverside dans le hook +- [x] backups : ne backuper que si il y a eu un changement entre le dernier backup et la base actuelle +- [x] refacto des dates avec le utils qui pour l'instant n'est pas utilisé +- [x] split de certains gros composants. +- [x] Page jira-dashboard : onglets analytics avancés et Qualité et collaboration : les charts sortent des cards; il faut reprendre la UI pour que ce soit consistant. +- [x] Page Daily : les mots aujourd'hui et hier ne fonctionnent dans les titres que si c'est vraiment aujourd'hui :) \ No newline at end of file diff --git a/src/app/api/daily/checkboxes/[id]/archive/route.ts b/src/app/api/daily/checkboxes/[id]/archive/route.ts new file mode 100644 index 0000000..df33251 --- /dev/null +++ b/src/app/api/daily/checkboxes/[id]/archive/route.ts @@ -0,0 +1,28 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { dailyService } from '@/services/daily'; + +export async function PATCH( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { id: checkboxId } = await params; + + if (!checkboxId) { + return NextResponse.json( + { error: 'Checkbox ID is required' }, + { status: 400 } + ); + } + + const archivedCheckbox = await dailyService.archiveCheckbox(checkboxId); + + return NextResponse.json(archivedCheckbox); + } catch (error) { + console.error('Error archiving checkbox:', error); + return NextResponse.json( + { error: 'Failed to archive checkbox' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/daily/pending/route.ts b/src/app/api/daily/pending/route.ts new file mode 100644 index 0000000..3164d43 --- /dev/null +++ b/src/app/api/daily/pending/route.ts @@ -0,0 +1,29 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { dailyService } from '@/services/daily'; +import { DailyCheckboxType } from '@/lib/types'; + +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + + const maxDays = searchParams.get('maxDays') ? parseInt(searchParams.get('maxDays')!) : undefined; + const excludeToday = searchParams.get('excludeToday') === 'true'; + const type = searchParams.get('type') as DailyCheckboxType | undefined; + const limit = searchParams.get('limit') ? parseInt(searchParams.get('limit')!) : undefined; + + const pendingCheckboxes = await dailyService.getPendingCheckboxes({ + maxDays, + excludeToday, + type, + limit + }); + + return NextResponse.json(pendingCheckboxes); + } catch (error) { + console.error('Error fetching pending checkboxes:', error); + return NextResponse.json( + { error: 'Failed to fetch pending checkboxes' }, + { status: 500 } + ); + } +} diff --git a/src/app/daily/DailyPageClient.tsx b/src/app/daily/DailyPageClient.tsx index 589f154..47c2228 100644 --- a/src/app/daily/DailyPageClient.tsx +++ b/src/app/daily/DailyPageClient.tsx @@ -8,6 +8,7 @@ import { Button } from '@/components/ui/Button'; import { Card } from '@/components/ui/Card'; import { DailyCalendar } from '@/components/daily/DailyCalendar'; import { DailySection } from '@/components/daily/DailySection'; +import { PendingTasksSection } from '@/components/daily/PendingTasksSection'; import { dailyClient } from '@/clients/daily-client'; import { Header } from '@/components/ui/Header'; import { getPreviousWorkday, formatDateLong, isToday, generateDateTitle, formatDateShort, isYesterday } from '@/lib/date-utils'; @@ -45,6 +46,7 @@ export function DailyPageClient({ } = useDaily(initialDate, initialDailyView); const [dailyDates, setDailyDates] = useState(initialDailyDates); + const [refreshTrigger, setRefreshTrigger] = useState(0); // Fonction pour rafraîchir la liste des dates avec des dailies const refreshDailyDates = async () => { @@ -79,12 +81,14 @@ export function DailyPageClient({ const handleToggleCheckbox = async (checkboxId: string) => { await toggleCheckbox(checkboxId); + setRefreshTrigger(prev => prev + 1); // Trigger refresh pour les tâches en attente }; const handleDeleteCheckbox = async (checkboxId: string) => { await deleteCheckbox(checkboxId); // Refresh dates après suppression pour mettre à jour le calendrier await refreshDailyDates(); + setRefreshTrigger(prev => prev + 1); // Trigger refresh pour les tâches en attente }; const handleUpdateCheckbox = async (checkboxId: string, text: string, type: DailyCheckboxType, taskId?: string) => { @@ -254,6 +258,13 @@ export function DailyPageClient({ )} + {/* Section des tâches en attente */} + + {/* Footer avec stats - dans le flux normal */} {dailyView && ( diff --git a/src/clients/daily-client.ts b/src/clients/daily-client.ts index 5fe8840..3dd015b 100644 --- a/src/clients/daily-client.ts +++ b/src/clients/daily-client.ts @@ -153,6 +153,34 @@ export class DailyClient { const response = await httpClient.get<{ dates: string[] }>('/daily/dates'); return response.dates; } + + /** + * Récupère les checkboxes en attente (non cochées) + */ + async getPendingCheckboxes(options?: { + maxDays?: number; + excludeToday?: boolean; + type?: 'task' | 'meeting'; + limit?: number; + }): Promise { + const params = new URLSearchParams(); + if (options?.maxDays) params.append('maxDays', options.maxDays.toString()); + if (options?.excludeToday !== undefined) params.append('excludeToday', options.excludeToday.toString()); + if (options?.type) params.append('type', options.type); + if (options?.limit) params.append('limit', options.limit.toString()); + + const queryString = params.toString(); + const result = await httpClient.get(`/daily/pending${queryString ? `?${queryString}` : ''}`); + return result.map((cb: ApiCheckbox) => this.transformCheckboxDates(cb)); + } + + /** + * Archive une checkbox + */ + async archiveCheckbox(checkboxId: string): Promise { + const result = await httpClient.patch(`/daily/checkboxes/${checkboxId}/archive`); + return this.transformCheckboxDates(result); + } } // Instance singleton du client diff --git a/src/components/daily/PendingTasksSection.tsx b/src/components/daily/PendingTasksSection.tsx new file mode 100644 index 0000000..b8e5b60 --- /dev/null +++ b/src/components/daily/PendingTasksSection.tsx @@ -0,0 +1,239 @@ +'use client'; + +import { useState, useEffect, useCallback } from 'react'; +import { Card, CardHeader, CardContent } from '@/components/ui/Card'; +import { Button } from '@/components/ui/Button'; +import { DailyCheckbox, DailyCheckboxType } from '@/lib/types'; +import { dailyClient } from '@/clients/daily-client'; +import { formatDateShort, getDaysAgo } from '@/lib/date-utils'; + +interface PendingTasksSectionProps { + onToggleCheckbox: (checkboxId: string) => Promise; + onDeleteCheckbox: (checkboxId: string) => Promise; + refreshTrigger?: number; // Pour forcer le refresh depuis le parent +} + +export function PendingTasksSection({ + onToggleCheckbox, + onDeleteCheckbox, + refreshTrigger +}: PendingTasksSectionProps) { + const [isCollapsed, setIsCollapsed] = useState(true); + const [pendingTasks, setPendingTasks] = useState([]); + const [loading, setLoading] = useState(false); + const [filters, setFilters] = useState({ + maxDays: 7, + type: 'all' as 'all' | DailyCheckboxType, + limit: 50 + }); + + // Charger les tâches en attente + const loadPendingTasks = useCallback(async () => { + setLoading(true); + try { + const tasks = await dailyClient.getPendingCheckboxes({ + maxDays: filters.maxDays, + excludeToday: true, + type: filters.type === 'all' ? undefined : filters.type, + limit: filters.limit + }); + setPendingTasks(tasks); + } catch (error) { + console.error('Erreur lors du chargement des tâches en attente:', error); + } finally { + setLoading(false); + } + }, [filters]); + + // Charger au montage et quand les filtres changent + useEffect(() => { + if (!isCollapsed) { + loadPendingTasks(); + } + }, [isCollapsed, filters, refreshTrigger, loadPendingTasks]); + + // Gérer l'archivage d'une tâche + const handleArchiveTask = async (checkboxId: string) => { + try { + await dailyClient.archiveCheckbox(checkboxId); + await loadPendingTasks(); // Recharger la liste + } catch (error) { + console.error('Erreur lors de l\'archivage:', error); + } + }; + + // Gérer le cochage d'une tâche + const handleToggleTask = async (checkboxId: string) => { + await onToggleCheckbox(checkboxId); + await loadPendingTasks(); // Recharger la liste + }; + + // Gérer la suppression d'une tâche + const handleDeleteTask = async (checkboxId: string) => { + await onDeleteCheckbox(checkboxId); + await loadPendingTasks(); // Recharger la liste + }; + + // Obtenir la couleur selon l'ancienneté + const getAgeColor = (date: Date) => { + const days = getDaysAgo(date); + if (days <= 1) return 'text-green-600'; + if (days <= 3) return 'text-yellow-600'; + if (days <= 7) return 'text-orange-600'; + return 'text-red-600'; + }; + + // Obtenir l'icône selon le type + const getTypeIcon = (type: DailyCheckboxType) => { + return type === 'meeting' ? '🤝' : '📋'; + }; + + const pendingCount = pendingTasks.length; + + return ( + + +
+ + + {!isCollapsed && ( +
+ {/* Filtres rapides */} + + + + + +
+ )} +
+
+ + {!isCollapsed && ( + + {loading ? ( +
+ Chargement des tâches en attente... +
+ ) : pendingTasks.length === 0 ? ( +
+ 🎉 Aucune tâche en attente ! Excellent travail. +
+ ) : ( +
+ {pendingTasks.map((task) => { + const daysAgo = getDaysAgo(task.date); + const isArchived = task.text.includes('[ARCHIVÉ]'); + + return ( +
+ {/* Checkbox */} + + + {/* Contenu */} +
+
+ {getTypeIcon(task.type)} + + {task.text} + +
+
+ {formatDateShort(task.date)} + + {daysAgo === 0 ? 'Aujourd\'hui' : + daysAgo === 1 ? 'Hier' : + `Il y a ${daysAgo} jours`} + + {task.task && ( + + 🔗 {task.task.title} + + )} +
+
+ + {/* Actions */} +
+ {!isArchived && ( + + )} + +
+
+ ); + })} +
+ )} +
+ )} +
+ ); +} diff --git a/src/lib/date-utils.ts b/src/lib/date-utils.ts index f3f2346..a2e373b 100644 --- a/src/lib/date-utils.ts +++ b/src/lib/date-utils.ts @@ -63,6 +63,15 @@ export function formatDateShort(date: Date): string { return formatDateForDisplay(date, 'DISPLAY_SHORT'); } +/** + * Calcule le nombre de jours écoulés depuis une date + */ +export function getDaysAgo(date: Date): number { + const today = getToday(); + const diffTime = today.getTime() - normalizeDate(date).getTime(); + return Math.floor(diffTime / (1000 * 60 * 60 * 24)); +} + /** * Formate une date longue pour l'affichage (lundi 1 décembre 2025) */ diff --git a/src/services/daily.ts b/src/services/daily.ts index ff89d1d..c30ac8b 100644 --- a/src/services/daily.ts +++ b/src/services/daily.ts @@ -257,6 +257,76 @@ export class DailyService { return formatDateForAPI(checkbox.date); }); } + + /** + * Récupère toutes les checkboxes non cochées (tâches en attente) + */ + async getPendingCheckboxes(options?: { + maxDays?: number; + excludeToday?: boolean; + type?: DailyCheckboxType; + limit?: number; + }): Promise { + const today = normalizeDate(getToday()); + const maxDays = options?.maxDays ?? 30; + const excludeToday = options?.excludeToday ?? true; + + // Calculer la date limite (maxDays jours en arrière) + const limitDate = new Date(today); + limitDate.setDate(limitDate.getDate() - maxDays); + + // Construire les conditions de filtrage + const whereConditions: { + isChecked: boolean; + date: { + gte: Date; + lt?: Date; + lte?: Date; + }; + type?: DailyCheckboxType; + } = { + isChecked: false, + date: { + gte: limitDate, + ...(excludeToday ? { lt: today } : { lte: today }) + } + }; + + // Filtrer par type si spécifié + if (options?.type) { + whereConditions.type = options.type; + } + + const checkboxes = await prisma.dailyCheckbox.findMany({ + where: whereConditions, + include: { task: true }, + orderBy: [ + { date: 'desc' }, + { order: 'asc' } + ], + ...(options?.limit ? { take: options.limit } : {}) + }); + + return checkboxes.map(this.mapPrismaCheckbox); + } + + /** + * Archive une checkbox (marque comme archivée sans la cocher) + */ + async archiveCheckbox(checkboxId: string): Promise { + // Pour l'instant, on utilise un champ text pour marquer comme archivé + // Plus tard on pourra ajouter un champ dédié dans la DB + const checkbox = await prisma.dailyCheckbox.update({ + where: { id: checkboxId }, + data: { + text: (await prisma.dailyCheckbox.findUnique({ where: { id: checkboxId } }))?.text + ' [ARCHIVÉ]', + updatedAt: new Date() + }, + include: { task: true } + }); + + return this.mapPrismaCheckbox(checkbox); + } } // Instance singleton du service