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.
This commit is contained in:
67
TODO.md
67
TODO.md
@@ -1,13 +1,7 @@
|
|||||||
# TowerControl v2.0 - Gestionnaire de tâches moderne
|
# TowerControl v2.0 - Gestionnaire de tâches moderne
|
||||||
|
|
||||||
## Autre Todos #2
|
## Autre Todos
|
||||||
- [x] Synchro Jira auto en background timé comme pour la synchro de sauvegarde
|
- [ ] Désactiver le hover sur les taskCard
|
||||||
- [ ] 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 :)
|
|
||||||
|
|
||||||
## 🔧 Phase 6: Fonctionnalités avancées (Priorité 6)
|
## 🔧 Phase 6: Fonctionnalités avancées (Priorité 6)
|
||||||
|
|
||||||
@@ -87,61 +81,6 @@
|
|||||||
- [ ] Configuration unifiée des filtres et synchronisations
|
- [ ] Configuration unifiée des filtres et synchronisations
|
||||||
- [ ] Dashboard multi-intégrations
|
- [ ] 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)
|
### 👥 Gestion multi-utilisateurs (PROJET MAJEUR)
|
||||||
@@ -195,7 +134,7 @@ src/
|
|||||||
- [ ] Breakpoints adaptés pour tablettes
|
- [ ] Breakpoints adaptés pour tablettes
|
||||||
|
|
||||||
- [ ] **Phase 2: Interface mobile pour les tâches**
|
- [ ] **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
|
- [ ] Liste verticale avec statuts en badges
|
||||||
- [ ] Actions par swipe (marquer terminé, changer statut)
|
- [ ] Actions par swipe (marquer terminé, changer statut)
|
||||||
- [ ] Filtres simplifiés (dropdown au lieu de sidebar)
|
- [ ] Filtres simplifiés (dropdown au lieu de sidebar)
|
||||||
|
|||||||
@@ -304,3 +304,68 @@ Endpoints complexes → API Routes conservées
|
|||||||
- [x] Filtrage par composant, version, type de ticket
|
- [x] Filtrage par composant, version, type de ticket
|
||||||
- [x] Vue détaillée par sprint avec drill-down
|
- [x] Vue détaillée par sprint avec drill-down
|
||||||
- [x] ~~Intégration avec les daily notes (mentions des blockers)~~ (supprimé)
|
- [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 :)
|
||||||
28
src/app/api/daily/checkboxes/[id]/archive/route.ts
Normal file
28
src/app/api/daily/checkboxes/[id]/archive/route.ts
Normal file
@@ -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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
29
src/app/api/daily/pending/route.ts
Normal file
29
src/app/api/daily/pending/route.ts
Normal file
@@ -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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import { Button } from '@/components/ui/Button';
|
|||||||
import { Card } from '@/components/ui/Card';
|
import { Card } from '@/components/ui/Card';
|
||||||
import { DailyCalendar } from '@/components/daily/DailyCalendar';
|
import { DailyCalendar } from '@/components/daily/DailyCalendar';
|
||||||
import { DailySection } from '@/components/daily/DailySection';
|
import { DailySection } from '@/components/daily/DailySection';
|
||||||
|
import { PendingTasksSection } from '@/components/daily/PendingTasksSection';
|
||||||
import { dailyClient } from '@/clients/daily-client';
|
import { dailyClient } from '@/clients/daily-client';
|
||||||
import { Header } from '@/components/ui/Header';
|
import { Header } from '@/components/ui/Header';
|
||||||
import { getPreviousWorkday, formatDateLong, isToday, generateDateTitle, formatDateShort, isYesterday } from '@/lib/date-utils';
|
import { getPreviousWorkday, formatDateLong, isToday, generateDateTitle, formatDateShort, isYesterday } from '@/lib/date-utils';
|
||||||
@@ -45,6 +46,7 @@ export function DailyPageClient({
|
|||||||
} = useDaily(initialDate, initialDailyView);
|
} = useDaily(initialDate, initialDailyView);
|
||||||
|
|
||||||
const [dailyDates, setDailyDates] = useState<string[]>(initialDailyDates);
|
const [dailyDates, setDailyDates] = useState<string[]>(initialDailyDates);
|
||||||
|
const [refreshTrigger, setRefreshTrigger] = useState(0);
|
||||||
|
|
||||||
// Fonction pour rafraîchir la liste des dates avec des dailies
|
// Fonction pour rafraîchir la liste des dates avec des dailies
|
||||||
const refreshDailyDates = async () => {
|
const refreshDailyDates = async () => {
|
||||||
@@ -79,12 +81,14 @@ export function DailyPageClient({
|
|||||||
|
|
||||||
const handleToggleCheckbox = async (checkboxId: string) => {
|
const handleToggleCheckbox = async (checkboxId: string) => {
|
||||||
await toggleCheckbox(checkboxId);
|
await toggleCheckbox(checkboxId);
|
||||||
|
setRefreshTrigger(prev => prev + 1); // Trigger refresh pour les tâches en attente
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteCheckbox = async (checkboxId: string) => {
|
const handleDeleteCheckbox = async (checkboxId: string) => {
|
||||||
await deleteCheckbox(checkboxId);
|
await deleteCheckbox(checkboxId);
|
||||||
// Refresh dates après suppression pour mettre à jour le calendrier
|
// Refresh dates après suppression pour mettre à jour le calendrier
|
||||||
await refreshDailyDates();
|
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) => {
|
const handleUpdateCheckbox = async (checkboxId: string, text: string, type: DailyCheckboxType, taskId?: string) => {
|
||||||
@@ -254,6 +258,13 @@ export function DailyPageClient({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Section des tâches en attente */}
|
||||||
|
<PendingTasksSection
|
||||||
|
onToggleCheckbox={handleToggleCheckbox}
|
||||||
|
onDeleteCheckbox={handleDeleteCheckbox}
|
||||||
|
refreshTrigger={refreshTrigger}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Footer avec stats - dans le flux normal */}
|
{/* Footer avec stats - dans le flux normal */}
|
||||||
{dailyView && (
|
{dailyView && (
|
||||||
<Card className="mt-8 p-4">
|
<Card className="mt-8 p-4">
|
||||||
|
|||||||
@@ -153,6 +153,34 @@ export class DailyClient {
|
|||||||
const response = await httpClient.get<{ dates: string[] }>('/daily/dates');
|
const response = await httpClient.get<{ dates: string[] }>('/daily/dates');
|
||||||
return response.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<DailyCheckbox[]> {
|
||||||
|
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<ApiCheckbox[]>(`/daily/pending${queryString ? `?${queryString}` : ''}`);
|
||||||
|
return result.map((cb: ApiCheckbox) => this.transformCheckboxDates(cb));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Archive une checkbox
|
||||||
|
*/
|
||||||
|
async archiveCheckbox(checkboxId: string): Promise<DailyCheckbox> {
|
||||||
|
const result = await httpClient.patch<ApiCheckbox>(`/daily/checkboxes/${checkboxId}/archive`);
|
||||||
|
return this.transformCheckboxDates(result);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Instance singleton du client
|
// Instance singleton du client
|
||||||
|
|||||||
239
src/components/daily/PendingTasksSection.tsx
Normal file
239
src/components/daily/PendingTasksSection.tsx
Normal file
@@ -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<void>;
|
||||||
|
onDeleteCheckbox: (checkboxId: string) => Promise<void>;
|
||||||
|
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<DailyCheckbox[]>([]);
|
||||||
|
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 (
|
||||||
|
<Card className="mt-6">
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<button
|
||||||
|
onClick={() => setIsCollapsed(!isCollapsed)}
|
||||||
|
className="flex items-center gap-2 text-lg font-semibold hover:text-[var(--primary)] transition-colors"
|
||||||
|
>
|
||||||
|
<span className={`transform transition-transform ${isCollapsed ? 'rotate-0' : 'rotate-90'}`}>
|
||||||
|
▶️
|
||||||
|
</span>
|
||||||
|
📋 Tâches en attente
|
||||||
|
{pendingCount > 0 && (
|
||||||
|
<span className="bg-[var(--warning)] text-[var(--warning-foreground)] px-2 py-1 rounded-full text-xs font-medium">
|
||||||
|
{pendingCount}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{!isCollapsed && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{/* Filtres rapides */}
|
||||||
|
<select
|
||||||
|
value={filters.maxDays}
|
||||||
|
onChange={(e) => setFilters(prev => ({ ...prev, maxDays: parseInt(e.target.value) }))}
|
||||||
|
className="text-xs px-2 py-1 border border-[var(--border)] rounded bg-[var(--background)]"
|
||||||
|
>
|
||||||
|
<option value={7}>7 derniers jours</option>
|
||||||
|
<option value={14}>14 derniers jours</option>
|
||||||
|
<option value={30}>30 derniers jours</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select
|
||||||
|
value={filters.type}
|
||||||
|
onChange={(e) => setFilters(prev => ({ ...prev, type: e.target.value as 'all' | DailyCheckboxType }))}
|
||||||
|
className="text-xs px-2 py-1 border border-[var(--border)] rounded bg-[var(--background)]"
|
||||||
|
>
|
||||||
|
<option value="all">Tous types</option>
|
||||||
|
<option value="task">Tâches</option>
|
||||||
|
<option value="meeting">Réunions</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={loadPendingTasks}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{loading ? '🔄' : '↻'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
{!isCollapsed && (
|
||||||
|
<CardContent>
|
||||||
|
{loading ? (
|
||||||
|
<div className="text-center py-4 text-[var(--muted-foreground)]">
|
||||||
|
Chargement des tâches en attente...
|
||||||
|
</div>
|
||||||
|
) : pendingTasks.length === 0 ? (
|
||||||
|
<div className="text-center py-4 text-[var(--muted-foreground)]">
|
||||||
|
🎉 Aucune tâche en attente ! Excellent travail.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{pendingTasks.map((task) => {
|
||||||
|
const daysAgo = getDaysAgo(task.date);
|
||||||
|
const isArchived = task.text.includes('[ARCHIVÉ]');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={task.id}
|
||||||
|
className={`flex items-center gap-3 p-3 rounded-lg border border-[var(--border)] ${
|
||||||
|
isArchived ? 'opacity-60 bg-[var(--muted)]/20' : 'bg-[var(--card)]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{/* Checkbox */}
|
||||||
|
<button
|
||||||
|
onClick={() => handleToggleTask(task.id)}
|
||||||
|
disabled={isArchived}
|
||||||
|
className={`w-5 h-5 rounded border-2 flex items-center justify-center transition-colors ${
|
||||||
|
isArchived
|
||||||
|
? 'border-[var(--muted)] cursor-not-allowed'
|
||||||
|
: 'border-[var(--border)] hover:border-[var(--primary)]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{task.isChecked && <span className="text-[var(--primary)]">✓</span>}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Contenu */}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<span>{getTypeIcon(task.type)}</span>
|
||||||
|
<span className={`text-sm font-medium ${isArchived ? 'line-through' : ''}`}>
|
||||||
|
{task.text}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3 text-xs text-[var(--muted-foreground)]">
|
||||||
|
<span>{formatDateShort(task.date)}</span>
|
||||||
|
<span className={getAgeColor(task.date)}>
|
||||||
|
{daysAgo === 0 ? 'Aujourd\'hui' :
|
||||||
|
daysAgo === 1 ? 'Hier' :
|
||||||
|
`Il y a ${daysAgo} jours`}
|
||||||
|
</span>
|
||||||
|
{task.task && (
|
||||||
|
<span className="text-[var(--primary)]">
|
||||||
|
🔗 {task.task.title}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{!isArchived && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleArchiveTask(task.id)}
|
||||||
|
title="Archiver cette tâche"
|
||||||
|
className="text-xs px-2 py-1"
|
||||||
|
>
|
||||||
|
📦
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleDeleteTask(task.id)}
|
||||||
|
title="Supprimer cette tâche"
|
||||||
|
className="text-xs px-2 py-1 text-[var(--destructive)] hover:text-[var(--destructive)]"
|
||||||
|
>
|
||||||
|
🗑️
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -63,6 +63,15 @@ export function formatDateShort(date: Date): string {
|
|||||||
return formatDateForDisplay(date, 'DISPLAY_SHORT');
|
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)
|
* Formate une date longue pour l'affichage (lundi 1 décembre 2025)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -257,6 +257,76 @@ export class DailyService {
|
|||||||
return formatDateForAPI(checkbox.date);
|
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<DailyCheckbox[]> {
|
||||||
|
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<DailyCheckbox> {
|
||||||
|
// 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
|
// Instance singleton du service
|
||||||
|
|||||||
Reference in New Issue
Block a user