import { Notification, NotificationCount } from '@/lib/types/notification'; import { NotificationAdapter } from './notification-adapter.interface'; import { getServerSession } from 'next-auth'; import { authOptions } from "@/app/api/auth/options"; // 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 } export class LeantimeAdapter implements NotificationAdapter { readonly sourceName = 'leantime'; private apiUrl: string; private apiToken: string; constructor() { this.apiUrl = process.env.LEANTIME_API_URL || ''; this.apiToken = process.env.LEANTIME_TOKEN || ''; console.log('[LEANTIME_ADAPTER] Initialized with API URL and token'); } async getNotifications(userId: string, page = 1, limit = 20): Promise { console.log(`[LEANTIME_ADAPTER] getNotifications called for userId: ${userId}, page: ${page}, limit: ${limit}`); try { // Get the user's email directly from the session const email = await this.getUserEmail(); console.log(`[LEANTIME_ADAPTER] Retrieved email from session:`, email || 'null'); if (!email) { console.error('[LEANTIME_ADAPTER] Could not get user email from session'); return []; } const leantimeUserId = await this.getLeantimeUserId(email); console.log(`[LEANTIME_ADAPTER] Retrieved Leantime userId for email ${email}:`, leantimeUserId || 'null'); if (!leantimeUserId) { console.error('[LEANTIME_ADAPTER] User not found in Leantime:', email); return []; } // Calculate pagination limits const limitStart = (page - 1) * limit; const limitEnd = limit; // Make request to Leantime API using the correct jsonrpc method console.log('[LEANTIME_ADAPTER] Sending request to get all notifications'); const jsonRpcBody = { jsonrpc: '2.0', method: 'leantime.rpc.Notifications.Notifications.getAllNotifications', params: { userId: leantimeUserId, showNewOnly: 0, // Get all notifications, not just unread limitStart: limitStart, limitEnd: limitEnd, filterOptions: [] // No additional filters }, id: 1 }; console.log('[LEANTIME_ADAPTER] Request body:', JSON.stringify(jsonRpcBody)); const response = await fetch(`${this.apiUrl}/api/jsonrpc`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-API-Key': this.apiToken }, body: JSON.stringify(jsonRpcBody) }); console.log('[LEANTIME_ADAPTER] Response status:', response.status); if (!response.ok) { const errorText = await response.text(); console.error('[LEANTIME_ADAPTER] Failed to fetch Leantime notifications:', { status: response.status, body: errorText.substring(0, 200) + (errorText.length > 200 ? '...' : '') }); return []; } const responseText = await response.text(); console.log('[LEANTIME_ADAPTER] Raw response (truncated):', responseText.substring(0, 200) + (responseText.length > 200 ? '...' : '')); const data = JSON.parse(responseText); console.log('[LEANTIME_ADAPTER] Parsed response data:', { hasResult: !!data.result, resultIsArray: Array.isArray(data.result), resultLength: Array.isArray(data.result) ? data.result.length : 'n/a', error: data.error }); if (!data.result || !Array.isArray(data.result)) { 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'); } return []; } const notifications = this.transformNotifications(data.result, userId); console.log('[LEANTIME_ADAPTER] Transformed notifications count:', notifications.length); return notifications; } catch (error) { console.error('[LEANTIME_ADAPTER] Error fetching Leantime notifications:', error); return []; } } async getNotificationCount(userId: string): Promise { console.log(`[LEANTIME_ADAPTER] getNotificationCount called for userId: ${userId}`); try { // Get all notifications and count them const notifications = await this.getNotifications(userId, 1, 100); // Get up to 100 for counting // Count total and unread const totalCount = notifications.length; const unreadCount = notifications.filter(n => !n.isRead).length; console.log('[LEANTIME_ADAPTER] Notification counts:', { total: totalCount, unread: unreadCount }); return { total: totalCount, unread: unreadCount, sources: { leantime: { total: totalCount, unread: unreadCount } } }; } catch (error) { console.error('[LEANTIME_ADAPTER] Error fetching notification count:', error); return { total: 0, unread: 0, sources: { leantime: { total: 0, unread: 0 } } }; } } async markAsRead(userId: string, notificationId: string): Promise { console.log(`[LEANTIME_ADAPTER] markAsRead called for ${notificationId}`); try { // Extract the source ID from our compound ID const sourceId = notificationId.replace(`${this.sourceName}-`, ''); // Get user email and ID 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; } // Make request to Leantime API to mark notification as read const jsonRpcBody = { jsonrpc: '2.0', method: 'leantime.rpc.Notifications.Notifications.markNotificationAsRead', params: { userId: leantimeUserId, notificationId: parseInt(sourceId) }, id: 1 }; const response = await fetch(`${this.apiUrl}/api/jsonrpc`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-API-Key': this.apiToken }, body: JSON.stringify(jsonRpcBody) }); if (!response.ok) { console.error(`[LEANTIME_ADAPTER] Failed to mark notification as read: ${response.status}`); return false; } const data = await response.json(); return data.result === true || data.result === "true" || !!data.result; } catch (error) { console.error('[LEANTIME_ADAPTER] Error marking notification as read:', error); return false; } } async markAllAsRead(userId: string): Promise { console.log(`[LEANTIME_ADAPTER] markAllAsRead called for ${userId}`); try { // Get user email and ID 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; } // Make request to Leantime API to mark all notifications as read const jsonRpcBody = { jsonrpc: '2.0', method: 'leantime.rpc.Notifications.Notifications.markAllNotificationsAsRead', params: { userId: leantimeUserId }, id: 1 }; const response = await fetch(`${this.apiUrl}/api/jsonrpc`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-API-Key': this.apiToken }, body: JSON.stringify(jsonRpcBody) }); if (!response.ok) { console.error(`[LEANTIME_ADAPTER] Failed to mark all notifications as read: ${response.status}`); return false; } const data = await response.json(); return data.result === true || data.result === "true" || !!data.result; } catch (error) { console.error('[LEANTIME_ADAPTER] Error marking all notifications as read:', error); return false; } } async isConfigured(): Promise { return !!(this.apiUrl && this.apiToken); } private transformNotifications(data: any[], userId: string): Notification[] { if (!Array.isArray(data)) { return []; } return data.map(notification => { // Determine properties from notification object // Adjust these based on actual structure of Leantime notifications const id = notification.id || notification._id || notification.notificationId; const message = notification.message || notification.text || notification.content || ''; const type = notification.type || 'notification'; const read = notification.read || notification.isRead || 0; const date = notification.date || notification.datetime || notification.createdDate || new Date().toISOString(); const url = notification.url || notification.link || ''; return { id: `${this.sourceName}-${id}`, source: this.sourceName as 'leantime', sourceId: id.toString(), type: type, title: type, // Use type as title if no specific title field message: message, link: url.startsWith('http') ? url : `${this.apiUrl}${url.startsWith('/') ? '' : '/'}${url}`, isRead: read === 1 || read === true, timestamp: new Date(date), priority: 'normal', user: { id: userId, name: notification.username || notification.userName || '' }, metadata: { // Include any other useful fields from notification moduleId: notification.moduleId || notification.module || '', projectId: notification.projectId || '', } }; }); } // Helper function to get user's email directly from the session private async getUserEmail(): Promise { try { const session = await getServerSession(authOptions); if (!session || !session.user?.email) { return null; } return session.user.email; } catch (error) { console.error('[LEANTIME_ADAPTER] Error getting user email from session:', error); return null; } } // Helper function to get Leantime user ID by email private async getLeantimeUserId(email: string): Promise { try { if (!this.apiToken) { return null; } 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.users.getAll', id: 1 }), }); if (!response.ok) { return null; } const data = await response.json(); if (!data.result || !Array.isArray(data.result)) { return null; } const users = data.result; // Find user with matching email (check in both username and email fields) const user = users.find((u: any) => u.username === email || u.email === email || (typeof u.username === 'string' && u.username.toLowerCase() === email.toLowerCase()) ); if (user) { return user.id; } return null; } catch (error) { console.error('[LEANTIME_ADAPTER] Error getting Leantime user ID:', error); return null; } } }