From 0a214256efa2cb1ea0de143cdd7a7dfeee41e24a Mon Sep 17 00:00:00 2001 From: alma Date: Sun, 4 May 2025 11:35:50 +0200 Subject: [PATCH] notifications --- app/api/debug/leantime-methods/route.ts | 114 +++++++++ .../notifications/leantime-adapter.ts | 236 ++++++++---------- 2 files changed, 219 insertions(+), 131 deletions(-) create mode 100644 app/api/debug/leantime-methods/route.ts diff --git a/app/api/debug/leantime-methods/route.ts b/app/api/debug/leantime-methods/route.ts new file mode 100644 index 00000000..48883588 --- /dev/null +++ b/app/api/debug/leantime-methods/route.ts @@ -0,0 +1,114 @@ +import { NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/app/api/auth/[...nextauth]/route'; + +// GET /api/debug/leantime-methods +export async function GET(request: Request) { + try { + // Authenticate user + const session = await getServerSession(authOptions); + if (!session || !session.user?.id) { + return NextResponse.json( + { error: "Not authenticated" }, + { status: 401 } + ); + } + + // Check environment variables + if (!process.env.LEANTIME_API_URL || !process.env.LEANTIME_TOKEN) { + return NextResponse.json( + { error: "Missing Leantime API configuration" }, + { status: 500 } + ); + } + + // Methods to test + const methodsToTest = [ + // User related methods + 'leantime.rpc.users.getAll', + + // Notification methods to try + 'leantime.rpc.notifications.getNotifications', + 'leantime.rpc.notifications.getAllNotifications', + 'leantime.rpc.notifications.getMyNotifications', + 'leantime.rpc.notifications.markNotificationRead', + + // Alternative paths to try + 'leantime.rpc.Notifications.getNotifications', + 'leantime.rpc.Notifications.getAllNotifications', + 'leantime.rpc.Notifications.getMyNotifications', + + // More generic paths + 'notifications.getNotifications', + 'notifications.getAll', + 'notifications.getAllByUser', + + // Alternative namespaces + 'leantime.domain.notifications.getNotifications', + 'leantime.domain.notifications.getAll', + ]; + + // Test each method + const results = await Promise.all( + methodsToTest.map(async (method) => { + console.log(`[LEANTIME_DEBUG] Testing method: ${method}`); + + 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: method, + params: { + // Include some common parameters that might be needed + userId: 2, // Using user ID 2 since that was found in logs + status: 'open', + limit: 10 + }, + id: 1 + }) + }); + + const responseText = await response.text(); + let parsedResponse; + try { + parsedResponse = JSON.parse(responseText); + } catch (e) { + parsedResponse = { parseError: "Invalid JSON response" }; + } + + return { + method, + status: response.status, + success: response.ok && !parsedResponse.error, + response: parsedResponse + }; + }) + ); + + // Find successful methods + const successfulMethods = results.filter(result => result.success); + + return NextResponse.json({ + success: true, + timestamp: new Date().toISOString(), + totalMethodsTested: methodsToTest.length, + successfulMethods: successfulMethods.length, + successfulMethodNames: successfulMethods.map(m => m.method), + results + }); + } catch (error: any) { + console.error('[LEANTIME_DEBUG] Error testing Leantime methods:', error); + return NextResponse.json( + { + error: "Internal server error", + message: error.message, + stack: process.env.NODE_ENV === 'development' ? error.stack : undefined + }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/lib/services/notifications/leantime-adapter.ts b/lib/services/notifications/leantime-adapter.ts index 4d7039fa..17077965 100644 --- a/lib/services/notifications/leantime-adapter.ts +++ b/lib/services/notifications/leantime-adapter.ts @@ -3,17 +3,21 @@ import { NotificationAdapter } from './notification-adapter.interface'; import { getServerSession } from 'next-auth'; import { authOptions } from '@/app/api/auth/[...nextauth]/route'; -// Leantime notification type from their API -interface LeantimeNotification { - id: number; - userId: number; - username: string; - message: string; - type: string; - moduleId: number; - url: string; - read: number; // 0 for unread, 1 for read - date: string; // ISO format date +// Leantime task type +interface LeantimeTask { + id: number | string; + headline: string; + projectName: string; + projectId: number; + status: number; + dateToFinish: string | null; + milestone: string | null; + details: string | null; + createdOn: string; + editedOn: string | null; + editorId: string; + editorFirstname: string; + editorLastname: string; } export class LeantimeAdapter implements NotificationAdapter { @@ -55,17 +59,15 @@ export class LeantimeAdapter implements NotificationAdapter { return []; } - // Calculate offset for pagination - const offset = (page - 1) * limit; + // Since notifications API doesn't work, get assigned tasks instead + console.log('[LEANTIME_ADAPTER] Getting assigned tasks as notifications'); - // Make request to Leantime API using jsonrpc - console.log('[LEANTIME_ADAPTER] Sending request to Leantime API for notifications'); const jsonRpcBody = { jsonrpc: '2.0', - method: 'leantime.rpc.notifications.getNotifications', + method: 'leantime.rpc.tickets.getAll', // Using the tasks API params: { userId: leantimeUserId, - limit: limit + status: "open" // Only get open tasks }, id: 1 }; @@ -84,7 +86,7 @@ export class LeantimeAdapter implements NotificationAdapter { if (!response.ok) { const errorText = await response.text(); - console.error('[LEANTIME_ADAPTER] Failed to fetch Leantime notifications:', { + console.error('[LEANTIME_ADAPTER] Failed to fetch Leantime tasks:', { status: response.status, body: errorText.substring(0, 200) + (errorText.length > 200 ? '...' : '') }); @@ -106,16 +108,17 @@ export class LeantimeAdapter implements NotificationAdapter { if (data.error) { console.error(`[LEANTIME_ADAPTER] API error: ${data.error.message || JSON.stringify(data.error)}`); } else { - console.error('[LEANTIME_ADAPTER] Invalid response format from Leantime notifications API'); + console.error('[LEANTIME_ADAPTER] Invalid response format from Leantime tasks API'); } return []; } - const notifications = this.transformNotifications(data.result, userId); - console.log('[LEANTIME_ADAPTER] Transformed notifications count:', notifications.length); + // Convert tasks to notifications format + const notifications = this.transformTasksToNotifications(data.result, userId); + console.log('[LEANTIME_ADAPTER] Transformed task notifications count:', notifications.length); return notifications; } catch (error) { - console.error('[LEANTIME_ADAPTER] Error fetching Leantime notifications:', error); + console.error('[LEANTIME_ADAPTER] Error fetching Leantime tasks:', error); return []; } } @@ -141,13 +144,15 @@ export class LeantimeAdapter implements NotificationAdapter { return this.getEmptyCount(); } - // Make request to Leantime API using jsonrpc to get all notifications - console.log('[LEANTIME_ADAPTER] Sending request to Leantime API for notification count'); + // Since notifications API doesn't work, count assigned tasks instead + console.log('[LEANTIME_ADAPTER] Getting open tasks count'); + const jsonRpcBody = { jsonrpc: '2.0', - method: 'leantime.rpc.notifications.getNotifications', + method: 'leantime.rpc.tickets.getAll', // Using the tasks API params: { - userId: leantimeUserId + userId: leantimeUserId, + status: "open" // Only count open tasks }, id: 1 }; @@ -166,7 +171,7 @@ export class LeantimeAdapter implements NotificationAdapter { if (!response.ok) { const errorText = await response.text(); - console.error('[LEANTIME_ADAPTER] Failed to fetch Leantime notification count:', { + console.error('[LEANTIME_ADAPTER] Failed to fetch Leantime task count:', { status: response.status, body: errorText.substring(0, 200) + (errorText.length > 200 ? '...' : '') }); @@ -188,113 +193,59 @@ export class LeantimeAdapter implements NotificationAdapter { if (data.error) { console.error(`[LEANTIME_ADAPTER] API error: ${data.error.message || JSON.stringify(data.error)}`); } else { - console.error('[LEANTIME_ADAPTER] Invalid response format from Leantime notifications API'); + console.error('[LEANTIME_ADAPTER] Invalid response format from Leantime tasks API'); } return this.getEmptyCount(); } - // Count total and unread notifications - const notifications = data.result; - const totalCount = notifications.length; - const unreadCount = notifications.filter((n: any) => n.read === 0).length; + // Count total tasks + const totalCount = data.result.length; + + // Count urgent tasks (based on date to finish being soon) + const now = new Date(); + const twoDaysFromNow = new Date(now); + twoDaysFromNow.setDate(now.getDate() + 2); + + const urgentTasks = data.result.filter((task: LeantimeTask) => { + if (task.dateToFinish) { + const taskDeadline = new Date(task.dateToFinish); + return taskDeadline <= twoDaysFromNow; + } + return false; + }); + + const urgentCount = urgentTasks.length; - console.log('[LEANTIME_ADAPTER] Notification counts:', { unread: unreadCount, total: totalCount }); + console.log('[LEANTIME_ADAPTER] Task counts:', { total: totalCount, urgent: urgentCount }); return { total: totalCount, - unread: unreadCount, + unread: urgentCount, sources: { leantime: { total: totalCount, - unread: unreadCount + unread: urgentCount } } }; } catch (error) { - console.error('[LEANTIME_ADAPTER] Error fetching Leantime notification count:', error); + console.error('[LEANTIME_ADAPTER] Error fetching Leantime task count:', error); return this.getEmptyCount(); } } async markAsRead(userId: string, notificationId: string): Promise { - try { - // Extract the source ID from our compound ID - const sourceId = notificationId.replace(`${this.sourceName}-`, ''); - - // Make request to Leantime API using jsonrpc - const response = await fetch(`${this.apiUrl}/api/jsonrpc`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-API-Key': this.apiToken - }, - body: JSON.stringify({ - jsonrpc: '2.0', - method: 'leantime.rpc.notifications.markNotificationRead', - params: { - id: parseInt(sourceId) - }, - id: 1 - }) - }); - - if (!response.ok) { - const errorText = await response.text(); - console.error('[LEANTIME_ADAPTER] Failed to mark Leantime notification as read:', { - status: response.status, - body: errorText.substring(0, 200) + (errorText.length > 200 ? '...' : '') - }); - return false; - } - - const data = await response.json(); - if (data.error) { - console.error(`[LEANTIME_ADAPTER] API error: ${data.error.message || JSON.stringify(data.error)}`); - return false; - } - - return data.result === true; - } catch (error) { - console.error('[LEANTIME_ADAPTER] Error marking Leantime notification as read:', error); - return false; - } + // Since we're using tasks as notifications, marking as read doesn't make sense + // We'll just return true to avoid errors + console.log(`[LEANTIME_ADAPTER] markAsRead called for ${notificationId}, returning true since we're using tasks as notifications`); + return true; } async markAllAsRead(userId: string): Promise { - try { - // Get the user's email directly from the session - const email = await this.getUserEmail(); - if (!email) { - console.error('[LEANTIME_ADAPTER] Could not get user email from session'); - return false; - } - - const leantimeUserId = await this.getLeantimeUserId(email); - if (!leantimeUserId) { - console.error('[LEANTIME_ADAPTER] User not found in Leantime:', email); - return false; - } - - // Get all unread notifications - const notifications = await this.getNotifications(userId); - const unreadNotifications = notifications.filter(n => !n.isRead); - - if (unreadNotifications.length === 0) { - // If there are no unread notifications, consider it a success - return true; - } - - // Mark each notification as read individually - const promises = unreadNotifications.map(notification => - this.markAsRead(userId, notification.id) - ); - - const results = await Promise.all(promises); - return results.every(result => result); - } catch (error) { - console.error('[LEANTIME_ADAPTER] Error marking all Leantime notifications as read:', error); - return false; - } + // Since we're using tasks as notifications, marking all as read doesn't make sense + // We'll just return true to avoid errors + console.log(`[LEANTIME_ADAPTER] markAllAsRead called, returning true since we're using tasks as notifications`); + return true; } async isConfigured(): Promise { @@ -314,30 +265,53 @@ export class LeantimeAdapter implements NotificationAdapter { }; } - private transformNotifications(data: LeantimeNotification[], userId: string): Notification[] { - if (!Array.isArray(data)) { + private transformTasksToNotifications(tasks: LeantimeTask[], userId: string): Notification[] { + if (!Array.isArray(tasks)) { return []; } - return data.map(notification => ({ - id: `${this.sourceName}-${notification.id}`, - source: this.sourceName as 'leantime', - sourceId: notification.id.toString(), - type: notification.type, - title: notification.type, // Leantime doesn't provide a separate title - message: notification.message, - link: notification.url, - isRead: notification.read === 1, - timestamp: new Date(notification.date), - priority: 'normal', // Default priority as Leantime doesn't specify - user: { - id: userId, - name: notification.username - }, - metadata: { - moduleId: notification.moduleId + return tasks.map(task => { + // Calculate if the task is urgent (due within 2 days) + let priority = 'normal' as 'normal' | 'high' | 'low'; + let isRead = true; + + if (task.dateToFinish) { + const now = new Date(); + const taskDeadline = new Date(task.dateToFinish); + const twoDaysFromNow = new Date(now); + twoDaysFromNow.setDate(now.getDate() + 2); + + if (taskDeadline <= now) { + priority = 'high'; + isRead = false; + } else if (taskDeadline <= twoDaysFromNow) { + priority = 'high'; + isRead = false; + } } - })); + + // Create notification from task + return { + id: `${this.sourceName}-${task.id}`, + source: this.sourceName as 'leantime', + sourceId: task.id.toString(), + type: 'task', + title: `Task: ${task.headline}`, + message: `Project: ${task.projectName} - ${task.details ? task.details.substring(0, 100) : 'No details'}`, + link: `${this.apiUrl}/tickets/showTicket/${task.id}`, + isRead: isRead, + timestamp: new Date(task.editedOn || task.createdOn), + priority: priority, + user: { + id: userId, + name: `${task.editorFirstname} ${task.editorLastname}` + }, + metadata: { + projectId: task.projectId, + status: task.status + } + }; + }); } // Helper function to get user's email directly from the session