16 KiB
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
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(); }, [])- AppellefetchTasks()au montage - Refresh manuel: Bouton de rafraîchissement dans le header du widget
2. Appel API Frontend (components/flow.tsx)
Fonction: fetchTasks()
const fetchTasks = async () => {
setLoading(true);
setError(null);
try {
const response = await fetch('/api/leantime/tasks?refresh=true');
// ...
}
}
Points importants:
- ✅ Utilise
?refresh=truepour 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
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)
// 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
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
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
usernamecorrespond à l'email de session - Retourne l'ID Leantime de l'utilisateur
Erreurs possibles:
- ❌
LEANTIME_TOKENnon défini → Retournenull - ❌ 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
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)
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:
- ✅ Exclut les tâches "Done" (status = 5)
- ✅ Uniquement les tâches où l'utilisateur est l'éditeur (
editorId === userId)
Transformation:
- Convertit les IDs en string
- Normalise les dates (
nullsi absentes) - Ajoute les champs
typeetdependingTicketIdpour identifier les sous-tâches
6.2. Mise en cache Redis
await cacheTasksData(session.user.id, tasks);
Fonction: cacheTasksData(userId, data) dans lib/redis.ts
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
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:
- Par date d'échéance (ascendant - plus anciennes en premier)
- Tâches avec date avant celles sans date
- Status 4 (Waiting for Approval) en priorité si dates égales
7.2. Limitation du nombre de résultats
setTasks(sortedTasks.slice(0, 7)); // Affiche maximum 7 tâches
8. Affichage dans le Widget
8.1. Structure du Widget
<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
- 🔴 Rouge si date passée (
- 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=truedans l'URL
Filtrage
- Backend: Exclut status=5 et filtre par
editorId - Frontend: Double vérification status=5 et tri personnalisé
Tri
- Date d'échéance (ascendant)
- Tâches avec date en premier
- 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()retournenull→ Erreur 404 - Pas de fallback si Leantime est indisponible
🚀 Recommandations
1. Utiliser le Cache par Défaut
// Au lieu de toujours utiliser ?refresh=true
const response = await fetch('/api/leantime/tasks'); // Sans refresh
2. Intégrer useUnifiedRefresh
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
-
Frontend:
components/flow.tsx- Widget principal
-
Backend:
app/api/leantime/tasks/route.ts- Route API
-
Cache:
lib/redis.ts- FonctionscacheTasksData()etgetCachedTasksData()
-
Configuration:
- Variables d'environnement:
LEANTIME_API_URLLEANTIME_TOKEN
- Variables d'environnement:
🔍 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 bruteconsole.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