vision refactor
This commit is contained in:
parent
c9acec83dc
commit
0aaaf468ff
500
DEVOIRS_WIDGET_FLOW_ANALYSIS.md
Normal file
500
DEVOIRS_WIDGET_FLOW_ANALYSIS.md
Normal file
@ -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<TaskWithDate[]>([]);
|
||||
const [error, setError] = useState<string | null>(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
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Devoirs</CardTitle>
|
||||
<Button onClick={() => fetchTasks()}>Refresh</Button>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{loading ? <Spinner /> :
|
||||
error ? <Error /> :
|
||||
tasks.length === 0 ? <EmptyState /> :
|
||||
<TaskList />}
|
||||
</CardContent>
|
||||
</Card>
|
||||
```
|
||||
|
||||
#### 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
|
||||
@ -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<TaskWithDate[]>([]);
|
||||
const [error, setError] = useState<string | null>(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() {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => fetchTasks()}
|
||||
onClick={handleManualRefresh}
|
||||
disabled={refreshing}
|
||||
className="h-7 w-7 p-0 hover:bg-gray-100/50 rounded-full"
|
||||
>
|
||||
<RefreshCw className="h-3.5 w-3.5 text-gray-600" />
|
||||
<RefreshCw className={`h-3.5 w-3.5 text-gray-600 ${refreshing ? 'animate-spin' : ''}`} />
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent className="p-3">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user