Widget Devoir Finition
This commit is contained in:
parent
929f1e0c03
commit
690de0ab5b
192
LEANTIME_TASKS_FLOW.md
Normal file
192
LEANTIME_TASKS_FLOW.md
Normal file
@ -0,0 +1,192 @@
|
||||
# Flow complet des tâches Leantime dans le widget Devoirs
|
||||
|
||||
## 1. BACKEND - API Route `/api/leantime/tasks` (app/api/leantime/tasks/route.ts)
|
||||
|
||||
### Étape 1.1 : Vérification de session
|
||||
- **Ligne 91-95** : Vérifie la session utilisateur
|
||||
- Si pas de session → retourne 401 Unauthorized
|
||||
|
||||
### Étape 1.2 : Vérification du cache Redis
|
||||
- **Ligne 103-141** : Si `forceRefresh=false`, vérifie le cache Redis
|
||||
- **Ligne 107-123** : **FILTRE 1** - Filtre les tâches "done" du cache :
|
||||
- Statuts filtrés : `0`, `3`, `5` (ou strings `'0'`, `'3'`, `'5'`, `'Done'`, `'done'`, `'DONE'`)
|
||||
- Si des tâches "done" sont trouvées dans le cache, elles sont supprimées
|
||||
- Le cache est mis à jour avec les tâches filtrées
|
||||
- Si cache valide → retourne les tâches filtrées du cache (ligne 139)
|
||||
|
||||
### Étape 1.3 : Récupération de l'ID utilisateur Leantime
|
||||
- **Ligne 146** : `getLeantimeUserId(session.user.email)`
|
||||
- Appelle l'API Leantime `leantime.rpc.users.getAll`
|
||||
- Trouve l'utilisateur par `username === email`
|
||||
- Retourne `user.id` ou `null`
|
||||
|
||||
### Étape 1.4 : Appel API Leantime
|
||||
- **Ligne 165-178** : Appelle `leantime.rpc.tickets.getAll` avec :
|
||||
- `userId: userId`
|
||||
- `status: "all"` (récupère TOUTES les tâches, tous statuts confondus)
|
||||
- **Ligne 193-195** : Log toutes les tâches brutes reçues de Leantime
|
||||
|
||||
### Étape 1.5 : Filtrage des tâches
|
||||
- **Ligne 217-249** : **FILTRE 2** - Filtre les tâches :
|
||||
|
||||
**a) Filtre par statut "done" (ligne 223-240)** :
|
||||
- Statuts filtrés : `0`, `3`, `5` (ou strings `'0'`, `'3'`, `'5'`, `'done'`)
|
||||
- Si `isDone === true` → la tâche est exclue (`return false`)
|
||||
|
||||
**b) Filtre par éditeur (ligne 242-248)** :
|
||||
- Seules les tâches où `task.editorId === userId` sont gardées
|
||||
- Si `taskEditorId !== currentUserId` → la tâche est exclue
|
||||
|
||||
### Étape 1.6 : Transformation des données
|
||||
- **Ligne 250-266** : Transforme les tâches Leantime en format standard :
|
||||
```typescript
|
||||
{
|
||||
id: task.id.toString(),
|
||||
headline: task.headline,
|
||||
projectName: task.projectName,
|
||||
projectId: task.projectId,
|
||||
status: task.status, // ⚠️ LE STATUT EST INCLUS ICI
|
||||
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
|
||||
}
|
||||
```
|
||||
|
||||
### Étape 1.7 : Mise en cache
|
||||
- **Ligne 292** : Met en cache les tâches filtrées dans Redis
|
||||
- **Ligne 294** : Retourne les tâches filtrées en JSON
|
||||
|
||||
---
|
||||
|
||||
## 2. FRONTEND - Widget Devoirs (components/flow.tsx)
|
||||
|
||||
### Étape 2.1 : Appel API
|
||||
- **Ligne 107-113** : Appelle `/api/leantime/tasks` (ou `/api/leantime/tasks?refresh=true`)
|
||||
- **Ligne 117-127** : Récupère les données JSON et les assigne à `leantimeTasks`
|
||||
- **Ligne 121-126** : Log toutes les tâches reçues avec leur statut
|
||||
|
||||
### Étape 2.2 : Détection des tâches "done" (pour log uniquement)
|
||||
- **Ligne 129-143** : Détecte les tâches avec statut `5` ou `'done'` (⚠️ NE FILTRE PAS LE STATUT 0)
|
||||
- Affiche un warning si des tâches "done" sont trouvées
|
||||
|
||||
### Étape 2.3 : Analyse des statuts (pour log uniquement)
|
||||
- **Ligne 164-178** : Crée `leantimeStatusDetails` avec calcul de `isDone`
|
||||
- **Ligne 168** : `isDone = statusNum === 0 || statusNum === 3 || statusNum === 5 || ...`
|
||||
- **Ligne 193** : Filtre les tâches "done" pour le log `doneTasksCount`
|
||||
- **Ligne 201-206** : Log le breakdown des statuts
|
||||
|
||||
### Étape 2.4 : Combinaison avec Twenty CRM
|
||||
- **Ligne 161** : Combine `leantimeTasks` et `twentyCrmTasks` dans `allTasks`
|
||||
|
||||
### Étape 2.5 : Filtrage frontend
|
||||
- **Ligne 242-281** : **FILTRE 3** - Filtre les tâches :
|
||||
|
||||
**a) Filtre par statut "done" (ligne 245-260)** :
|
||||
- Statuts filtrés : `0`, `3`, `5` (ou strings `'0'`, `'3'`, `'5'`, `'done'`)
|
||||
- Si le statut correspond → la tâche est exclue (`return false`)
|
||||
|
||||
**b) Filtre par date (ligne 262-280)** :
|
||||
- Exclut les tâches sans `dateToFinish`
|
||||
- Garde uniquement les tâches avec `dateToFinish <= today` (overdue ou due today)
|
||||
|
||||
### Étape 2.6 : Tri
|
||||
- **Ligne 296-317** : Trie les tâches :
|
||||
1. Par `dateToFinish` (oldest first)
|
||||
2. Si dates égales, par statut (statut `4` en premier)
|
||||
|
||||
### Étape 2.7 : Notification badge
|
||||
- **Ligne 362-366** : Met à jour le badge de notification avec le nombre de tâches
|
||||
|
||||
### Étape 2.8 : Affichage
|
||||
- **Ligne 368** : `setTasks(sortedTasks)` - Met à jour l'état React
|
||||
|
||||
### Étape 2.9 : Filtrage avant notification Outlook
|
||||
- **Ligne 372-390** : **FILTRE 4** - Filtre les tâches avant d'envoyer l'événement `tasks-updated` :
|
||||
- **Ligne 380** : ⚠️ **PROBLÈME ICI** - Ne filtre que `3` et `5`, PAS le statut `0` !
|
||||
- Si `isDone === true` → la tâche est exclue
|
||||
- **Ligne 391-398** : Transforme les tâches pour l'événement (sans inclure le `status`)
|
||||
|
||||
---
|
||||
|
||||
## 3. PROBLÈMES IDENTIFIÉS
|
||||
|
||||
### Problème 1 : Filtre ligne 380 dans flow.tsx
|
||||
- **Ligne 380** : `isDone = taskStatus === 3 || taskStatus === 5 || ...`
|
||||
- **Manque** : `taskStatus === 0`
|
||||
- **Impact** : Les tâches avec statut `0` passent le filtre et sont envoyées dans l'événement `tasks-updated`
|
||||
|
||||
### Problème 2 : Détection ligne 129-135 dans flow.tsx
|
||||
- **Ligne 134** : Ne détecte que le statut `5`, pas `0` ni `3`
|
||||
- **Impact** : Le log `⚠️ Received done tasks` ne détecte pas toutes les tâches "done"
|
||||
|
||||
### Problème 3 : Le statut n'est pas inclus dans l'événement tasks-updated
|
||||
- **Ligne 391-398** : L'objet envoyé dans l'événement ne contient pas le champ `status`
|
||||
- **Impact** : Les hooks qui écoutent `tasks-updated` ne peuvent pas filtrer par statut
|
||||
|
||||
---
|
||||
|
||||
## 4. FLOW RÉSUMÉ
|
||||
|
||||
```
|
||||
1. API Leantime (toutes les tâches, tous statuts)
|
||||
↓
|
||||
2. FILTRE 1 (cache) : Exclut statuts 0, 3, 5
|
||||
↓
|
||||
3. FILTRE 2 (API backend) : Exclut statuts 0, 3, 5 + filtre par editorId
|
||||
↓
|
||||
4. Transformation + Cache Redis
|
||||
↓
|
||||
5. Frontend reçoit les tâches (avec statut inclus)
|
||||
↓
|
||||
6. FILTRE 3 (frontend) : Exclut statuts 0, 3, 5 + filtre par date
|
||||
↓
|
||||
7. Tri par date
|
||||
↓
|
||||
8. FILTRE 4 (avant notification) : ⚠️ Exclut seulement statuts 3, 5 (MANQUE 0)
|
||||
↓
|
||||
9. Événement tasks-updated (sans statut dans l'objet)
|
||||
↓
|
||||
10. Affichage dans le widget
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. CORRECTIONS NÉCESSAIRES
|
||||
|
||||
1. **Ligne 380 dans flow.tsx** : Ajouter `taskStatus === 0` au filtre ✅ CORRIGÉ
|
||||
2. **Ligne 134 dans flow.tsx** : Ajouter détection des statuts `0` et `3` ✅ CORRIGÉ
|
||||
3. **Ligne 391-398 dans flow.tsx** : Inclure le champ `status` dans l'objet envoyé dans l'événement
|
||||
|
||||
---
|
||||
|
||||
## 6. SIGNIFICATION DES STATUTS LEANTIME
|
||||
|
||||
D'après le code :
|
||||
|
||||
### Mapping standard (app/api/leantime/status-labels/route.ts) :
|
||||
- **Status 1** = 'NEW'
|
||||
- **Status 2** = 'INPROGRESS'
|
||||
- **Status 3** = 'DONE' ⚠️
|
||||
- **Status 0** = Non défini (tombe dans `default: 'UNKNOWN'`)
|
||||
|
||||
### Mapping dans le widget (components/flow.tsx) :
|
||||
- **Status 1** = 'New'
|
||||
- **Status 2** = 'Blocked'
|
||||
- **Status 3** = 'In Progress' ⚠️ **INCOHÉRENCE avec status-labels**
|
||||
- **Status 4** = 'Waiting for Approval'
|
||||
- **Status 5** = 'Done'
|
||||
- **Status 0** = 'Unknown' (par défaut)
|
||||
|
||||
### Statuts filtrés comme "done" :
|
||||
- **Status 0** = "Done" (dans votre instance Leantime, configuration personnalisée)
|
||||
- **Status 3** = "Done" (selon `status-labels/route.ts`)
|
||||
- **Status 5** = "Done" (selon `flow.tsx`)
|
||||
|
||||
**Note** : Il y a une incohérence entre les deux mappings. Le statut 3 est mappé à "DONE" dans `status-labels` mais à "In Progress" dans `flow.tsx`. Le statut 0 n'est pas standard dans Leantime mais semble être utilisé comme "done" dans votre instance.
|
||||
@ -85,6 +85,83 @@ async function getLeantimeUserId(email: string): Promise<number | null> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all status labels for a user and identify which status values correspond to "done"
|
||||
* Returns a Set of status values (as strings) that are marked as "done"
|
||||
*/
|
||||
async function getDoneStatusValues(userId: number): Promise<Set<string>> {
|
||||
const doneStatusValues = new Set<string>();
|
||||
|
||||
try {
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-API-Key': process.env.LEANTIME_TOKEN!
|
||||
};
|
||||
|
||||
const data = await fetchJsonWithTimeout(`${process.env.LEANTIME_API_URL}/api/jsonrpc`, {
|
||||
method: 'POST',
|
||||
timeout: 10000,
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
method: 'leantime.rpc.Tickets.Tickets.getAllStatusLabelsByUserId',
|
||||
params: {
|
||||
userId: userId
|
||||
},
|
||||
id: 1
|
||||
}),
|
||||
});
|
||||
|
||||
if (!data.result || !Array.isArray(data.result)) {
|
||||
logger.warn('[LEANTIME_TASKS] Invalid response format from getAllStatusLabelsByUserId, using fallback');
|
||||
// Fallback to default values if API fails
|
||||
return new Set(['0', '3', '5']);
|
||||
}
|
||||
|
||||
// data.result is an array of projects, each with labels
|
||||
// Each label has: id, name, statusType, class, etc.
|
||||
data.result.forEach((project: any) => {
|
||||
if (project.labels && Array.isArray(project.labels)) {
|
||||
project.labels.forEach((label: any) => {
|
||||
// Check if the label name (case-insensitive) contains "done"
|
||||
const labelName = String(label.name || '').toLowerCase().trim();
|
||||
if (labelName === 'done' || labelName.includes('done')) {
|
||||
// The status value is typically in label.id or label.name
|
||||
// We need to extract the numeric status value
|
||||
const statusValue = String(label.id || label.name || '');
|
||||
doneStatusValues.add(statusValue);
|
||||
|
||||
// Also try to extract numeric value if it's in a format like "projectId-status"
|
||||
const parts = statusValue.split('-');
|
||||
if (parts.length > 1) {
|
||||
doneStatusValues.add(parts[parts.length - 1]); // Last part is usually the status
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
logger.debug('[LEANTIME_TASKS] Identified done status values', {
|
||||
doneStatusValues: Array.from(doneStatusValues),
|
||||
projectsCount: data.result.length,
|
||||
});
|
||||
|
||||
// If no done statuses found, use fallback
|
||||
if (doneStatusValues.size === 0) {
|
||||
logger.warn('[LEANTIME_TASKS] No done status labels found, using fallback values');
|
||||
return new Set(['0', '3', '5']);
|
||||
}
|
||||
|
||||
return doneStatusValues;
|
||||
} catch (error) {
|
||||
logger.error('[LEANTIME_TASKS] Error fetching status labels, using fallback', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
// Fallback to default values if API fails
|
||||
return new Set(['0', '3', '5']);
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
console.log('[LEANTIME_TASKS] 🔵 API CALLED - Starting request');
|
||||
try {
|
||||
@ -99,22 +176,36 @@ export async function GET(request: NextRequest) {
|
||||
const url = new URL(request.url);
|
||||
const forceRefresh = url.searchParams.get('refresh') === 'true';
|
||||
|
||||
// Get Leantime user ID first (needed for status labels)
|
||||
const userId = await getLeantimeUserId(session.user.email);
|
||||
|
||||
if (!userId) {
|
||||
logger.error('[LEANTIME_TASKS] User not found in Leantime', {
|
||||
emailHash: Buffer.from(session.user.email.toLowerCase()).toString('base64').slice(0, 12),
|
||||
});
|
||||
return NextResponse.json({ error: "User not found in Leantime" }, { status: 404 });
|
||||
}
|
||||
|
||||
// Get done status values dynamically from Leantime status labels
|
||||
const doneStatusValues = await getDoneStatusValues(userId);
|
||||
logger.debug('[LEANTIME_TASKS] Done status values identified', {
|
||||
doneStatusValues: Array.from(doneStatusValues),
|
||||
});
|
||||
|
||||
// Try to get data from cache if not forcing refresh
|
||||
if (!forceRefresh) {
|
||||
const cachedTasks = await getCachedTasksData(session.user.id);
|
||||
if (cachedTasks && Array.isArray(cachedTasks)) {
|
||||
// Filter out done tasks from cache as well (in case cache contains old data)
|
||||
// Filter out done tasks from cache using dynamic status values
|
||||
const filteredCachedTasks = cachedTasks.filter((task: any) => {
|
||||
const taskStatus = task.status;
|
||||
if (taskStatus !== null && taskStatus !== undefined) {
|
||||
const statusNum = typeof taskStatus === 'string' ? parseInt(taskStatus, 10) : taskStatus;
|
||||
// In Leantime: status 0, 3, 5 = DONE
|
||||
if (statusNum === 0 || statusNum === 3 || statusNum === 5 || taskStatus === '0' || taskStatus === '3' || taskStatus === '5' || taskStatus === 'Done' || taskStatus === 'done' || taskStatus === 'DONE') {
|
||||
const statusStr = String(taskStatus);
|
||||
if (doneStatusValues.has(statusStr)) {
|
||||
logger.debug('[LEANTIME_TASKS] Filtering out done task from cache', {
|
||||
id: task.id,
|
||||
headline: task.headline,
|
||||
status: taskStatus,
|
||||
statusNum,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
@ -140,21 +231,10 @@ export async function GET(request: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug('[LEANTIME_TASKS] Fetching tasks for user', {
|
||||
emailHash: Buffer.from(session.user.email.toLowerCase()).toString('base64').slice(0, 12),
|
||||
});
|
||||
const userId = await getLeantimeUserId(session.user.email);
|
||||
|
||||
if (!userId) {
|
||||
logger.error('[LEANTIME_TASKS] User not found in Leantime', {
|
||||
emailHash: Buffer.from(session.user.email.toLowerCase()).toString('base64').slice(0, 12),
|
||||
});
|
||||
return NextResponse.json({ error: "User not found in Leantime" }, { status: 404 });
|
||||
}
|
||||
|
||||
logger.debug('[LEANTIME_TASKS] Fetching tasks for Leantime user', {
|
||||
emailHash: Buffer.from(session.user.email.toLowerCase()).toString('base64').slice(0, 12),
|
||||
});
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-API-Key': process.env.LEANTIME_TOKEN!
|
||||
@ -217,23 +297,16 @@ export async function GET(request: NextRequest) {
|
||||
const tasks = data.result
|
||||
.filter((task: any) => {
|
||||
// Filter out any task (main or subtask) that has status Done
|
||||
// In Leantime: status 3 = DONE (see status-labels/route.ts)
|
||||
// Also check for status 5 as fallback
|
||||
// Handle both number and string formats, and check for null/undefined
|
||||
// Use dynamic status values from Leantime status labels
|
||||
const taskStatus = task.status;
|
||||
if (taskStatus !== null && taskStatus !== undefined) {
|
||||
const statusNum = typeof taskStatus === 'string' ? parseInt(taskStatus, 10) : taskStatus;
|
||||
const statusStr = typeof taskStatus === 'string' ? taskStatus.trim().toLowerCase() : String(taskStatus).trim().toLowerCase();
|
||||
const isDone = statusNum === 0 || statusNum === 3 || statusNum === 5 || statusStr === '0' || statusStr === '3' || statusStr === '5' || statusStr === 'done';
|
||||
|
||||
if (isDone) {
|
||||
const statusStr = String(taskStatus);
|
||||
if (doneStatusValues.has(statusStr)) {
|
||||
logger.debug('[LEANTIME_TASKS] Filtering out done task', {
|
||||
id: task.id,
|
||||
headline: task.headline,
|
||||
status: taskStatus,
|
||||
statusType: typeof taskStatus,
|
||||
statusNum,
|
||||
statusStr,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -125,13 +125,13 @@ export function Duties() {
|
||||
statusType: typeof t.status,
|
||||
})));
|
||||
leantimeTasks = leantimeData;
|
||||
// Log tasks with status 5 to debug
|
||||
// Log tasks with status 0, 3, or 5 (done) to debug
|
||||
const doneTasks = leantimeData.filter((t: Task) => {
|
||||
const taskStatus = (t as any).status; // Use any to handle potential string/number mismatch
|
||||
if (taskStatus === null || taskStatus === undefined) return false;
|
||||
const statusNum = typeof taskStatus === 'string' ? parseInt(taskStatus, 10) : taskStatus;
|
||||
const statusStr = typeof taskStatus === 'string' ? taskStatus.toLowerCase() : String(taskStatus).toLowerCase();
|
||||
return statusNum === 5 || statusStr === '5' || statusStr === 'done';
|
||||
return statusNum === 0 || statusNum === 3 || statusNum === 5 || statusStr === '0' || statusStr === '3' || statusStr === '5' || statusStr === 'done';
|
||||
});
|
||||
if (doneTasks.length > 0) {
|
||||
console.warn('[Devoirs Widget] ⚠️ Received done tasks from Leantime API:', doneTasks.map((t: Task) => ({
|
||||
@ -377,7 +377,7 @@ export function Duties() {
|
||||
}
|
||||
const taskStatus = typeof rawStatus === 'string' ? parseInt(rawStatus, 10) : rawStatus;
|
||||
const statusStr = typeof rawStatus === 'string' ? rawStatus.toLowerCase().trim() : String(rawStatus).toLowerCase().trim();
|
||||
const isDone = taskStatus === 3 || taskStatus === 5 || statusStr === '3' || statusStr === '5' || statusStr === 'done';
|
||||
const isDone = taskStatus === 0 || taskStatus === 3 || taskStatus === 5 || statusStr === '0' || statusStr === '3' || statusStr === '5' || statusStr === 'done';
|
||||
if (isDone) {
|
||||
console.warn('[Devoirs Widget] ⚠️ Filtering out done task before notification:', {
|
||||
id: task.id,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user