diff --git a/DEVOIRS_WIDGET_FLOW_ANALYSIS.md b/DEVOIRS_WIDGET_FLOW_ANALYSIS.md new file mode 100644 index 0000000..81c3aaa --- /dev/null +++ b/DEVOIRS_WIDGET_FLOW_ANALYSIS.md @@ -0,0 +1,500 @@ +# Analyse du Flow du Widget "Devoirs" + +## ๐Ÿ“‹ Vue d'ensemble + +Le widget "Devoirs" affiche les tรขches Leantime assignรฉes ร  l'utilisateur connectรฉ. Il rรฉcupรจre les donnรฉes depuis l'API Leantime via un endpoint Next.js qui utilise un systรจme de cache Redis. + +--- + +## ๐Ÿ”„ Flow Complet + +### 1. **Initialisation du Widget** (`components/flow.tsx`) + +**Fichier:** `components/flow.tsx` +**Composant:** `Duties()` + +#### ร‰tat initial +```typescript +const [tasks, setTasks] = useState([]); +const [error, setError] = useState(null); +const [loading, setLoading] = useState(true); +const [refreshing, setRefreshing] = useState(false); +``` + +#### Cycle de vie +- **Mount:** `useEffect(() => { fetchTasks(); }, [])` - Appelle `fetchTasks()` au montage +- **Refresh manuel:** Bouton de rafraรฎchissement dans le header du widget + +--- + +### 2. **Appel API Frontend** (`components/flow.tsx`) + +**Fonction:** `fetchTasks()` + +```typescript +const fetchTasks = async () => { + setLoading(true); + setError(null); + try { + const response = await fetch('/api/leantime/tasks?refresh=true'); + // ... + } +} +``` + +**Points importants:** +- โœ… Utilise `?refresh=true` pour **bypasser le cache Redis** +- โœ… Appelรฉ au montage du composant +- โœ… Appelรฉ manuellement via le bouton de rafraรฎchissement + +**URL:** `GET /api/leantime/tasks?refresh=true` + +--- + +### 3. **Route API Backend** (`app/api/leantime/tasks/route.ts`) + +**Fichier:** `app/api/leantime/tasks/route.ts` +**Mรฉthode:** `GET` + +#### 3.1. Authentification +```typescript +const session = await getServerSession(authOptions); +if (!session?.user?.email) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); +} +``` + +#### 3.2. Gestion du Cache Redis + +**Clรฉ de cache:** `widget:tasks:${userId}` +**TTL:** 10 minutes (600 secondes) + +```typescript +// Check for force refresh parameter +const url = new URL(request.url); +const forceRefresh = url.searchParams.get('refresh') === 'true'; + +// Try to get data from cache if not forcing refresh +if (!forceRefresh) { + const cachedTasks = await getCachedTasksData(session.user.id); + if (cachedTasks) { + return NextResponse.json(cachedTasks); // โšก Retour immรฉdiat si cache hit + } +} +``` + +**Comportement:** +- Si `?refresh=true` โ†’ **Ignore le cache**, va chercher les donnรฉes fraรฎches +- Si pas de `refresh` โ†’ **Vรฉrifie le cache Redis d'abord** +- Si cache hit โ†’ Retourne immรฉdiatement les donnรฉes en cache +- Si cache miss โ†’ Continue avec la rรฉcupรฉration depuis Leantime + +--- + +### 4. **Rรฉcupรฉration de l'ID Utilisateur Leantime** + +**Fonction:** `getLeantimeUserId(email: string)` + +#### 4.1. Appel API Leantime - Users +```typescript +const response = await fetch(`${process.env.LEANTIME_API_URL}/api/jsonrpc`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-API-Key': process.env.LEANTIME_TOKEN + }, + body: JSON.stringify({ + jsonrpc: '2.0', + method: 'leantime.rpc.users.getAll', + id: 1 + }), +}); +``` + +**Mรฉthode RPC:** `leantime.rpc.users.getAll` +**Objectif:** Rรฉcupรฉrer tous les utilisateurs Leantime pour trouver celui correspondant ร  l'email de session + +#### 4.2. Recherche de l'utilisateur +```typescript +const users = data.result; +const user = users.find((u: any) => u.username === email); +return user ? user.id : null; +``` + +**Logique:** +- Parcourt tous les utilisateurs Leantime +- Trouve celui dont `username` correspond ร  l'email de session +- Retourne l'ID Leantime de l'utilisateur + +**Erreurs possibles:** +- โŒ `LEANTIME_TOKEN` non dรฉfini โ†’ Retourne `null` +- โŒ API Leantime non accessible โ†’ Retourne `null` +- โŒ Utilisateur non trouvรฉ โ†’ Retourne `null` +- โŒ Rรฉponse invalide โ†’ Retourne `null` + +--- + +### 5. **Rรฉcupรฉration des Tรขches Leantime** + +**Mรฉthode RPC:** `leantime.rpc.tickets.getAll` + +```typescript +const response = await fetch(`${process.env.LEANTIME_API_URL}/api/jsonrpc`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-API-Key': process.env.LEANTIME_TOKEN! + }, + body: JSON.stringify({ + jsonrpc: '2.0', + method: 'leantime.rpc.tickets.getAll', + params: { + userId: userId, + status: "all" + }, + id: 1 + }), +}); +``` + +**Paramรจtres:** +- `userId`: ID Leantime de l'utilisateur (rรฉcupรฉrรฉ ร  l'รฉtape 4) +- `status: "all"`: Rรฉcupรจre toutes les tรขches, peu importe leur statut + +**Rรฉponse:** Tableau de toutes les tรขches Leantime de l'utilisateur + +--- + +### 6. **Filtrage et Transformation des Tรขches** + +#### 6.1. Filtrage cรดtรฉ Backend (`app/api/leantime/tasks/route.ts`) + +```typescript +const tasks = data.result + .filter((task: any) => { + // 1. Exclure les tรขches avec status "Done" (5) + if (task.status === 5) { + return false; + } + + // 2. Filtrer uniquement les tรขches oรน l'utilisateur est l'รฉditeur + const taskEditorId = String(task.editorId).trim(); + const currentUserId = String(userId).trim(); + const isUserEditor = taskEditorId === currentUserId; + return isUserEditor; + }) + .map((task: any) => ({ + id: task.id.toString(), + headline: task.headline, + projectName: task.projectName, + projectId: task.projectId, + status: task.status, + dateToFinish: task.dateToFinish || null, + milestone: task.type || null, + details: task.description || null, + createdOn: task.dateCreated, + editedOn: task.editedOn || null, + editorId: task.editorId, + editorFirstname: task.editorFirstname, + editorLastname: task.editorLastname, + type: task.type || null, + dependingTicketId: task.dependingTicketId || null + })); +``` + +**Critรจres de filtrage:** +1. โœ… **Exclut les tรขches "Done"** (status = 5) +2. โœ… **Uniquement les tรขches oรน l'utilisateur est l'รฉditeur** (`editorId === userId`) + +**Transformation:** +- Convertit les IDs en string +- Normalise les dates (`null` si absentes) +- Ajoute les champs `type` et `dependingTicketId` pour identifier les sous-tรขches + +#### 6.2. Mise en cache Redis + +```typescript +await cacheTasksData(session.user.id, tasks); +``` + +**Fonction:** `cacheTasksData(userId, data)` dans `lib/redis.ts` + +```typescript +const key = KEYS.TASKS(userId); // `widget:tasks:${userId}` +await redis.set(key, JSON.stringify(data), 'EX', TTL.TASKS); // TTL = 600 secondes (10 min) +``` + +--- + +### 7. **Traitement Frontend** (`components/flow.tsx`) + +#### 7.1. Filtrage supplรฉmentaire cรดtรฉ Frontend + +```typescript +const sortedTasks = data + .filter((task: Task) => { + // Double filtrage: exclure les tรขches Done (dรฉjร  fait cรดtรฉ backend, mais sรฉcuritรฉ) + const isNotDone = task.status !== 5; + return isNotDone; + }) + .sort((a: Task, b: Task) => { + // 1. Trier par dateToFinish (plus anciennes en premier) + const dateA = getValidDate(a); + const dateB = getValidDate(b); + + if (dateA && dateB) { + const timeA = new Date(dateA).getTime(); + const timeB = new Date(dateB).getTime(); + if (timeA !== timeB) { + return timeA - timeB; + } + } + + // 2. Si une seule tรขche a une date, la mettre en premier + if (dateA) return -1; + if (dateB) return 1; + + // 3. Si dates รฉgales ou absentes, prioriser status 4 (Waiting for Approval) + if (a.status === 4 && b.status !== 4) return -1; + if (b.status === 4 && a.status !== 4) return 1; + + return 0; + }); +``` + +**Logique de tri:** +1. **Par date d'รฉchรฉance** (ascendant - plus anciennes en premier) +2. **Tรขches avec date** avant celles sans date +3. **Status 4** (Waiting for Approval) en prioritรฉ si dates รฉgales + +#### 7.2. Limitation du nombre de rรฉsultats + +```typescript +setTasks(sortedTasks.slice(0, 7)); // Affiche maximum 7 tรขches +``` + +--- + +### 8. **Affichage dans le Widget** + +#### 8.1. Structure du Widget + +```tsx + + + Devoirs + + + + {loading ? : + error ? : + tasks.length === 0 ? : + } + + +``` + +#### 8.2. Composant TaskDate + +Affiche la date d'รฉchรฉance avec: +- **Format:** Mois (court) / Jour / Annรฉe +- **Couleurs:** + - ๐Ÿ”ด Rouge si date passรฉe (`isPastDue`) + - ๐Ÿ”ต Bleu si date future +- **Fallback:** "NO DATE" si pas de date ou date invalide + +#### 8.3. Lien vers Leantime + +Chaque tรขche est cliquable et redirige vers: +``` +https://agilite.slm-lab.net/tickets/showTicket/${task.id} +``` + +--- + +## ๐Ÿ“Š Diagramme de Flow + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ 1. Widget Mount (components/flow.tsx) โ”‚ +โ”‚ useEffect(() => fetchTasks()) โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ–ผ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ 2. Frontend API Call โ”‚ +โ”‚ GET /api/leantime/tasks?refresh=true โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ–ผ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ 3. Backend Route (app/api/leantime/tasks/route.ts) โ”‚ +โ”‚ - Vรฉrifie session โ”‚ +โ”‚ - Check cache Redis (si !refresh) โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ โ”‚ + โ–ผ โ–ผ + Cache Hit? Cache Miss? + โ”‚ โ”‚ + โ”‚ โ–ผ + Return Cache โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ 4. getLeantimeUserId() โ”‚ + โ”‚ - API: users.getAll โ”‚ + โ”‚ - Find by email โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ–ผ + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ 5. Fetch Tasks โ”‚ + โ”‚ - API: tickets.getAll โ”‚ + โ”‚ - Filter: !status=5 โ”‚ + โ”‚ - Filter: editorId โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ–ผ + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ 6. Cache Results โ”‚ + โ”‚ Redis: widget:tasks โ”‚ + โ”‚ TTL: 10 minutes โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ–ผ + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ 7. Return JSON โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ–ผ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ 8. Frontend Processing (components/flow.tsx) โ”‚ +โ”‚ - Filter: !status=5 (double check) โ”‚ +โ”‚ - Sort: by dateToFinish, then status โ”‚ +โ”‚ - Slice: first 7 tasks โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ–ผ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ 9. Render Widget โ”‚ +โ”‚ - TaskDate component โ”‚ +โ”‚ - Links to Leantime โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +--- + +## ๐Ÿ”‘ Points Clรฉs + +### Cache Redis +- **Clรฉ:** `widget:tasks:${userId}` +- **TTL:** 10 minutes (600 secondes) +- **Bypass:** `?refresh=true` dans l'URL + +### Filtrage +1. **Backend:** Exclut status=5 et filtre par `editorId` +2. **Frontend:** Double vรฉrification status=5 et tri personnalisรฉ + +### Tri +1. Date d'รฉchรฉance (ascendant) +2. Tรขches avec date en premier +3. Status 4 (Waiting for Approval) prioritaire + +### Limitation +- Maximum **7 tรขches** affichรฉes + +### Statuts des Tรขches +- **1:** New (Bleu) +- **2:** Blocked (Rouge) +- **3:** In Progress (Jaune) +- **4:** Waiting for Approval (Violet) +- **5:** Done (Gris) - **Exclu de l'affichage** + +--- + +## โš ๏ธ Problรจmes Identifiรฉs + +### 1. Double Filtrage +- Le backend filtre dรฉjร  les tรขches Done (status=5) +- Le frontend refilt รฉgalement โ†’ **Redondant** + +### 2. Bypass Cache Systรฉmatique +- Le widget utilise toujours `?refresh=true` +- **Ne profite jamais du cache Redis** โ†’ Augmente la charge serveur + +### 3. Pas de Refresh Automatique +- Aucun polling automatique +- Seulement au mount et refresh manuel +- **Pas d'intรฉgration avec `useUnifiedRefresh`** + +### 4. Gestion d'Erreurs +- Si `getLeantimeUserId()` retourne `null` โ†’ Erreur 404 +- Pas de fallback si Leantime est indisponible + +--- + +## ๐Ÿš€ Recommandations + +### 1. Utiliser le Cache par Dรฉfaut +```typescript +// Au lieu de toujours utiliser ?refresh=true +const response = await fetch('/api/leantime/tasks'); // Sans refresh +``` + +### 2. Intรฉgrer useUnifiedRefresh +```typescript +const { refresh, isActive } = useUnifiedRefresh({ + resource: 'tasks', + interval: 300000, // 5 minutes + enabled: status === 'authenticated', + onRefresh: fetchTasks, + priority: 'low', +}); +``` + +### 3. Retirer le Double Filtrage +- Supprimer le filtrage status=5 cรดtรฉ frontend (dรฉjร  fait cรดtรฉ backend) + +### 4. Amรฉliorer la Gestion d'Erreurs +- Afficher un message clair si l'utilisateur n'est pas trouvรฉ dans Leantime +- Fallback si Leantime est indisponible + +--- + +## ๐Ÿ“ Fichiers Concernรฉs + +1. **Frontend:** + - `components/flow.tsx` - Widget principal + +2. **Backend:** + - `app/api/leantime/tasks/route.ts` - Route API + +3. **Cache:** + - `lib/redis.ts` - Fonctions `cacheTasksData()` et `getCachedTasksData()` + +4. **Configuration:** + - Variables d'environnement: + - `LEANTIME_API_URL` + - `LEANTIME_TOKEN` + +--- + +## ๐Ÿ” Logs et Debug + +### Logs Backend +- `[LEANTIME_TASKS]` - Prรฉfixe pour tous les logs +- Logs de debug pour chaque รฉtape du flow +- Logs d'erreur en cas d'รฉchec + +### Logs Frontend +- `console.log('Raw API response:', data)` - Rรฉponse brute +- `console.log('Sorted and filtered tasks:', ...)` - Tรขches triรฉes +- Logs de filtrage pour chaque tรขche + +--- + +## ๐Ÿ“Š Mรฉtriques + +- **Cache TTL:** 10 minutes +- **Limite d'affichage:** 7 tรขches +- **Statuts affichรฉs:** 1, 2, 3, 4 (exclut 5) +- **Tri:** Date d'รฉchรฉance โ†’ Status 4 prioritaire diff --git a/components/flow.tsx b/components/flow.tsx index 3406057..82ab93d 100644 --- a/components/flow.tsx +++ b/components/flow.tsx @@ -1,10 +1,13 @@ "use client"; import { useEffect, useState } from "react"; +import { useSession } from "next-auth/react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { RefreshCw, Share2, Folder } from "lucide-react"; import { Badge } from "@/components/ui/badge"; +import { useUnifiedRefresh } from "@/hooks/use-unified-refresh"; +import { REFRESH_INTERVALS } from "@/lib/constants/refresh-intervals"; interface Task { id: number; @@ -39,6 +42,7 @@ interface TaskWithDate extends Task { } export function Duties() { + const { data: session, status } = useSession(); const [tasks, setTasks] = useState([]); const [error, setError] = useState(null); const [loading, setLoading] = useState(true); @@ -88,11 +92,17 @@ export function Duties() { return null; }; - const fetchTasks = async () => { - setLoading(true); + const fetchTasks = async (forceRefresh = false) => { + // Only show loading spinner on initial load, not on auto-refresh + if (!tasks.length) { + setLoading(true); + } + setRefreshing(true); setError(null); try { - const response = await fetch('/api/leantime/tasks?refresh=true'); + // Use cache by default, only bypass with ?refresh=true for manual refresh + const url = forceRefresh ? '/api/leantime/tasks?refresh=true' : '/api/leantime/tasks'; + const response = await fetch(url); if (!response.ok) { throw new Error('Failed to fetch tasks'); } @@ -106,18 +116,9 @@ export function Duties() { return; } - // Filter out tasks with status Done (5) and sort by dateToFinish + // Backend already filters out status=5 (Done) and filters by editorId + // Only sort by dateToFinish here const sortedTasks = data - .filter((task: Task) => { - // Filter out any task (main or subtask) that has status Done (5) - const isNotDone = task.status !== 5; - if (!isNotDone) { - console.log(`Filtering out Done task ${task.id} (type: ${task.type || 'main'}, status: ${task.status})`); - } else { - console.log(`Keeping task ${task.id}: status=${task.status} (${getStatusLabel(task.status)}), type=${task.type || 'main'}`); - } - return isNotDone; - }) .sort((a: Task, b: Task) => { // First sort by dateToFinish (oldest first) const dateA = getValidDate(a); @@ -144,7 +145,7 @@ export function Duties() { return 0; }); - console.log('Sorted and filtered tasks:', sortedTasks.map(t => ({ + console.log('Sorted tasks:', sortedTasks.map(t => ({ id: t.id, date: t.dateToFinish, status: t.status, @@ -156,12 +157,32 @@ export function Duties() { setError(error instanceof Error ? error.message : 'Failed to fetch tasks'); } finally { setLoading(false); + setRefreshing(false); } }; + // Initial fetch on mount useEffect(() => { - fetchTasks(); - }, []); + if (status === 'authenticated') { + fetchTasks(false); // Use cache on initial load + } + }, [status]); + + // Integrate unified refresh for automatic polling + const { refresh } = useUnifiedRefresh({ + resource: 'duties', + interval: REFRESH_INTERVALS.DUTIES, // 2 minutes + enabled: status === 'authenticated', + onRefresh: async () => { + await fetchTasks(false); // Use cache for auto-refresh + }, + priority: 'medium', + }); + + // Manual refresh handler (bypasses cache) + const handleManualRefresh = async () => { + await fetchTasks(true); // Force refresh, bypass cache + }; // Update the TaskDate component to handle dates better const TaskDate = ({ task }: { task: TaskWithDate }) => { @@ -225,10 +246,11 @@ export function Duties() {