NeahStable/DEVOIRS_WIDGET_FLOW_ANALYSIS.md
2026-01-15 22:15:59 +01:00

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(); }, []) - Appelle fetchTasks() 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=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

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=trueIgnore le cache, va chercher les données fraîches
  • Si pas de refreshVé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 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

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:

  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

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:

  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

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
  • 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

// 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

  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