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";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import { useSession } from "next-auth/react";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { RefreshCw, Share2, Folder } from "lucide-react";
|
import { RefreshCw, Share2, Folder } from "lucide-react";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { useUnifiedRefresh } from "@/hooks/use-unified-refresh";
|
||||||
|
import { REFRESH_INTERVALS } from "@/lib/constants/refresh-intervals";
|
||||||
|
|
||||||
interface Task {
|
interface Task {
|
||||||
id: number;
|
id: number;
|
||||||
@ -39,6 +42,7 @@ interface TaskWithDate extends Task {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Duties() {
|
export function Duties() {
|
||||||
|
const { data: session, status } = useSession();
|
||||||
const [tasks, setTasks] = useState<TaskWithDate[]>([]);
|
const [tasks, setTasks] = useState<TaskWithDate[]>([]);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@ -88,11 +92,17 @@ export function Duties() {
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchTasks = async () => {
|
const fetchTasks = async (forceRefresh = false) => {
|
||||||
setLoading(true);
|
// Only show loading spinner on initial load, not on auto-refresh
|
||||||
|
if (!tasks.length) {
|
||||||
|
setLoading(true);
|
||||||
|
}
|
||||||
|
setRefreshing(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
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) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to fetch tasks');
|
throw new Error('Failed to fetch tasks');
|
||||||
}
|
}
|
||||||
@ -106,18 +116,9 @@ export function Duties() {
|
|||||||
return;
|
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
|
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) => {
|
.sort((a: Task, b: Task) => {
|
||||||
// First sort by dateToFinish (oldest first)
|
// First sort by dateToFinish (oldest first)
|
||||||
const dateA = getValidDate(a);
|
const dateA = getValidDate(a);
|
||||||
@ -144,7 +145,7 @@ export function Duties() {
|
|||||||
return 0;
|
return 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('Sorted and filtered tasks:', sortedTasks.map(t => ({
|
console.log('Sorted tasks:', sortedTasks.map(t => ({
|
||||||
id: t.id,
|
id: t.id,
|
||||||
date: t.dateToFinish,
|
date: t.dateToFinish,
|
||||||
status: t.status,
|
status: t.status,
|
||||||
@ -156,12 +157,32 @@ export function Duties() {
|
|||||||
setError(error instanceof Error ? error.message : 'Failed to fetch tasks');
|
setError(error instanceof Error ? error.message : 'Failed to fetch tasks');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
setRefreshing(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Initial fetch on mount
|
||||||
useEffect(() => {
|
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
|
// Update the TaskDate component to handle dates better
|
||||||
const TaskDate = ({ task }: { task: TaskWithDate }) => {
|
const TaskDate = ({ task }: { task: TaskWithDate }) => {
|
||||||
@ -225,10 +246,11 @@ export function Duties() {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={() => fetchTasks()}
|
onClick={handleManualRefresh}
|
||||||
|
disabled={refreshing}
|
||||||
className="h-7 w-7 p-0 hover:bg-gray-100/50 rounded-full"
|
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>
|
</Button>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="p-3">
|
<CardContent className="p-3">
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user