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) {
|
export async function GET(request: NextRequest) {
|
||||||
console.log('[LEANTIME_TASKS] 🔵 API CALLED - Starting request');
|
console.log('[LEANTIME_TASKS] 🔵 API CALLED - Starting request');
|
||||||
try {
|
try {
|
||||||
@ -99,22 +176,36 @@ export async function GET(request: NextRequest) {
|
|||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
const forceRefresh = url.searchParams.get('refresh') === 'true';
|
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
|
// Try to get data from cache if not forcing refresh
|
||||||
if (!forceRefresh) {
|
if (!forceRefresh) {
|
||||||
const cachedTasks = await getCachedTasksData(session.user.id);
|
const cachedTasks = await getCachedTasksData(session.user.id);
|
||||||
if (cachedTasks && Array.isArray(cachedTasks)) {
|
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 filteredCachedTasks = cachedTasks.filter((task: any) => {
|
||||||
const taskStatus = task.status;
|
const taskStatus = task.status;
|
||||||
if (taskStatus !== null && taskStatus !== undefined) {
|
if (taskStatus !== null && taskStatus !== undefined) {
|
||||||
const statusNum = typeof taskStatus === 'string' ? parseInt(taskStatus, 10) : taskStatus;
|
const statusStr = String(taskStatus);
|
||||||
// In Leantime: status 0, 3, 5 = DONE
|
if (doneStatusValues.has(statusStr)) {
|
||||||
if (statusNum === 0 || statusNum === 3 || statusNum === 5 || taskStatus === '0' || taskStatus === '3' || taskStatus === '5' || taskStatus === 'Done' || taskStatus === 'done' || taskStatus === 'DONE') {
|
|
||||||
logger.debug('[LEANTIME_TASKS] Filtering out done task from cache', {
|
logger.debug('[LEANTIME_TASKS] Filtering out done task from cache', {
|
||||||
id: task.id,
|
id: task.id,
|
||||||
headline: task.headline,
|
headline: task.headline,
|
||||||
status: taskStatus,
|
status: taskStatus,
|
||||||
statusNum,
|
|
||||||
});
|
});
|
||||||
return false;
|
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', {
|
logger.debug('[LEANTIME_TASKS] Fetching tasks for Leantime user', {
|
||||||
emailHash: Buffer.from(session.user.email.toLowerCase()).toString('base64').slice(0, 12),
|
emailHash: Buffer.from(session.user.email.toLowerCase()).toString('base64').slice(0, 12),
|
||||||
});
|
});
|
||||||
|
|
||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'X-API-Key': process.env.LEANTIME_TOKEN!
|
'X-API-Key': process.env.LEANTIME_TOKEN!
|
||||||
@ -217,23 +297,16 @@ export async function GET(request: NextRequest) {
|
|||||||
const tasks = data.result
|
const tasks = data.result
|
||||||
.filter((task: any) => {
|
.filter((task: any) => {
|
||||||
// Filter out any task (main or subtask) that has status Done
|
// Filter out any task (main or subtask) that has status Done
|
||||||
// In Leantime: status 3 = DONE (see status-labels/route.ts)
|
// Use dynamic status values from Leantime status labels
|
||||||
// Also check for status 5 as fallback
|
|
||||||
// Handle both number and string formats, and check for null/undefined
|
|
||||||
const taskStatus = task.status;
|
const taskStatus = task.status;
|
||||||
if (taskStatus !== null && taskStatus !== undefined) {
|
if (taskStatus !== null && taskStatus !== undefined) {
|
||||||
const statusNum = typeof taskStatus === 'string' ? parseInt(taskStatus, 10) : taskStatus;
|
const statusStr = String(taskStatus);
|
||||||
const statusStr = typeof taskStatus === 'string' ? taskStatus.trim().toLowerCase() : String(taskStatus).trim().toLowerCase();
|
if (doneStatusValues.has(statusStr)) {
|
||||||
const isDone = statusNum === 0 || statusNum === 3 || statusNum === 5 || statusStr === '0' || statusStr === '3' || statusStr === '5' || statusStr === 'done';
|
|
||||||
|
|
||||||
if (isDone) {
|
|
||||||
logger.debug('[LEANTIME_TASKS] Filtering out done task', {
|
logger.debug('[LEANTIME_TASKS] Filtering out done task', {
|
||||||
id: task.id,
|
id: task.id,
|
||||||
headline: task.headline,
|
headline: task.headline,
|
||||||
status: taskStatus,
|
status: taskStatus,
|
||||||
statusType: typeof taskStatus,
|
statusType: typeof taskStatus,
|
||||||
statusNum,
|
|
||||||
statusStr,
|
|
||||||
});
|
});
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -125,13 +125,13 @@ export function Duties() {
|
|||||||
statusType: typeof t.status,
|
statusType: typeof t.status,
|
||||||
})));
|
})));
|
||||||
leantimeTasks = leantimeData;
|
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 doneTasks = leantimeData.filter((t: Task) => {
|
||||||
const taskStatus = (t as any).status; // Use any to handle potential string/number mismatch
|
const taskStatus = (t as any).status; // Use any to handle potential string/number mismatch
|
||||||
if (taskStatus === null || taskStatus === undefined) return false;
|
if (taskStatus === null || taskStatus === undefined) return false;
|
||||||
const statusNum = typeof taskStatus === 'string' ? parseInt(taskStatus, 10) : taskStatus;
|
const statusNum = typeof taskStatus === 'string' ? parseInt(taskStatus, 10) : taskStatus;
|
||||||
const statusStr = typeof taskStatus === 'string' ? taskStatus.toLowerCase() : String(taskStatus).toLowerCase();
|
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) {
|
if (doneTasks.length > 0) {
|
||||||
console.warn('[Devoirs Widget] ⚠️ Received done tasks from Leantime API:', doneTasks.map((t: Task) => ({
|
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 taskStatus = typeof rawStatus === 'string' ? parseInt(rawStatus, 10) : rawStatus;
|
||||||
const statusStr = typeof rawStatus === 'string' ? rawStatus.toLowerCase().trim() : String(rawStatus).toLowerCase().trim();
|
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) {
|
if (isDone) {
|
||||||
console.warn('[Devoirs Widget] ⚠️ Filtering out done task before notification:', {
|
console.warn('[Devoirs Widget] ⚠️ Filtering out done task before notification:', {
|
||||||
id: task.id,
|
id: task.id,
|
||||||
|
|||||||
@ -159,28 +159,28 @@ export function useRocketChatCalls() {
|
|||||||
|
|
||||||
// Handle incoming call events
|
// Handle incoming call events
|
||||||
if (callEvent.type === 'call-incoming') {
|
if (callEvent.type === 'call-incoming') {
|
||||||
console.log('[useRocketChatCalls] 🎉 INCOMING CALL DETECTED!', {
|
console.log('[useRocketChatCalls] 🎉 INCOMING CALL DETECTED!', {
|
||||||
from: callEvent.from.username,
|
from: callEvent.from.username,
|
||||||
roomId: callEvent.roomId,
|
roomId: callEvent.roomId,
|
||||||
roomName: callEvent.roomName,
|
roomName: callEvent.roomName,
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.info('[useRocketChatCalls] Incoming call detected', {
|
logger.info('[useRocketChatCalls] Incoming call detected', {
|
||||||
from: callEvent.from.username,
|
from: callEvent.from.username,
|
||||||
roomId: callEvent.roomId,
|
roomId: callEvent.roomId,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Show incoming call notification UI (Outlook-style rectangle)
|
// Show incoming call notification UI (Outlook-style rectangle)
|
||||||
setIncomingCall({
|
setIncomingCall({
|
||||||
from: {
|
from: {
|
||||||
userId: callEvent.from.userId,
|
userId: callEvent.from.userId,
|
||||||
username: callEvent.from.username,
|
username: callEvent.from.username,
|
||||||
name: callEvent.from.name || callEvent.from.username,
|
name: callEvent.from.name || callEvent.from.username,
|
||||||
},
|
},
|
||||||
roomId: callEvent.roomId,
|
roomId: callEvent.roomId,
|
||||||
roomName: callEvent.roomName || callEvent.roomId,
|
roomName: callEvent.roomName || callEvent.roomId,
|
||||||
timestamp: callEvent.timestamp,
|
timestamp: callEvent.timestamp,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Track the current call's roomId so we can match it when the call ends
|
// Track the current call's roomId so we can match it when the call ends
|
||||||
currentCallRoomIdRef.current = callEvent.roomId;
|
currentCallRoomIdRef.current = callEvent.roomId;
|
||||||
@ -205,36 +205,36 @@ export function useRocketChatCalls() {
|
|||||||
callTimeoutRef.current = null;
|
callTimeoutRef.current = null;
|
||||||
}, 20000); // 20 seconds safety timeout - typical max ringing duration
|
}, 20000); // 20 seconds safety timeout - typical max ringing duration
|
||||||
|
|
||||||
console.log('[useRocketChatCalls] 📞 Incoming call notification UI set', {
|
console.log('[useRocketChatCalls] 📞 Incoming call notification UI set', {
|
||||||
from: callEvent.from.username,
|
from: callEvent.from.username,
|
||||||
roomId: callEvent.roomId,
|
roomId: callEvent.roomId,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Trigger notification badge
|
// Trigger notification badge
|
||||||
// For calls, we want to increment the existing count, not replace it
|
// For calls, we want to increment the existing count, not replace it
|
||||||
// So we fetch current count first, then increment
|
// So we fetch current count first, then increment
|
||||||
triggerNotification({
|
triggerNotification({
|
||||||
source: 'rocketchat',
|
source: 'rocketchat',
|
||||||
count: 1, // This will be added to existing count in the registry
|
count: 1, // This will be added to existing count in the registry
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: `call-${callEvent.roomId}-${Date.now()}`,
|
id: `call-${callEvent.roomId}-${Date.now()}`,
|
||||||
title: `📞 Appel entrant de ${callEvent.from.name || callEvent.from.username}`,
|
title: `📞 Appel entrant de ${callEvent.from.name || callEvent.from.username}`,
|
||||||
message: `Appel vidéo/audio dans ${callEvent.roomName || 'chat'}`,
|
message: `Appel vidéo/audio dans ${callEvent.roomName || 'chat'}`,
|
||||||
link: `/parole?room=${callEvent.roomId}`,
|
link: `/parole?room=${callEvent.roomId}`,
|
||||||
timestamp: callEvent.timestamp,
|
timestamp: callEvent.timestamp,
|
||||||
metadata: {
|
metadata: {
|
||||||
type: 'call',
|
type: 'call',
|
||||||
from: callEvent.from,
|
from: callEvent.from,
|
||||||
roomId: callEvent.roomId,
|
roomId: callEvent.roomId,
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
},
|
||||||
}).then(() => {
|
],
|
||||||
console.log('[useRocketChatCalls] ✅ Notification triggered successfully');
|
}).then(() => {
|
||||||
}).catch((error) => {
|
console.log('[useRocketChatCalls] ✅ Notification triggered successfully');
|
||||||
console.error('[useRocketChatCalls] ❌ Error triggering notification', error);
|
}).catch((error) => {
|
||||||
});
|
console.error('[useRocketChatCalls] ❌ Error triggering notification', error);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -47,15 +47,15 @@ export class N8nService {
|
|||||||
let response: Response;
|
let response: Response;
|
||||||
try {
|
try {
|
||||||
response = await fetchWithTimeout(deleteWebhookUrl, {
|
response = await fetchWithTimeout(deleteWebhookUrl, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
timeout: 30000, // 30 seconds
|
timeout: 30000, // 30 seconds
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'x-api-key': this.apiKey
|
'x-api-key': this.apiKey
|
||||||
},
|
},
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.debug('Deletion webhook response received', {
|
logger.debug('Deletion webhook response received', {
|
||||||
status: response.status,
|
status: response.status,
|
||||||
statusText: response.statusText,
|
statusText: response.statusText,
|
||||||
@ -197,15 +197,15 @@ export class N8nService {
|
|||||||
let response: Response;
|
let response: Response;
|
||||||
try {
|
try {
|
||||||
response = await fetchWithTimeout(this.webhookUrl, {
|
response = await fetchWithTimeout(this.webhookUrl, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
timeout: 30000, // 30 seconds
|
timeout: 30000, // 30 seconds
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'x-api-key': this.apiKey
|
'x-api-key': this.apiKey
|
||||||
},
|
},
|
||||||
body: JSON.stringify(cleanData),
|
body: JSON.stringify(cleanData),
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.debug('Webhook response received', {
|
logger.debug('Webhook response received', {
|
||||||
status: response.status,
|
status: response.status,
|
||||||
statusText: response.statusText,
|
statusText: response.statusText,
|
||||||
|
|||||||
@ -529,10 +529,10 @@ export class RocketChatCallListener {
|
|||||||
isIncomingCall,
|
isIncomingCall,
|
||||||
isCallEnded
|
isCallEnded
|
||||||
});
|
});
|
||||||
if (args.length > 0) {
|
if (args.length > 0) {
|
||||||
this.handleCallEvent(args[0]);
|
this.handleCallEvent(args[0]);
|
||||||
} else {
|
} else {
|
||||||
this.handleCallEvent(message.fields);
|
this.handleCallEvent(message.fields);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -635,13 +635,13 @@ export class RocketChatCallListener {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
logger.debug('[ROCKETCHAT_CALL_LISTENER] 📢 Stream notify user message', {
|
logger.debug('[ROCKETCHAT_CALL_LISTENER] 📢 Stream notify user message', {
|
||||||
msg: message.msg,
|
msg: message.msg,
|
||||||
collection: message.collection,
|
collection: message.collection,
|
||||||
eventName,
|
eventName,
|
||||||
hasArgs: args.length > 0,
|
hasArgs: args.length > 0,
|
||||||
argsCount: args.length,
|
argsCount: args.length,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user