NeahNew/lib/services/notifications/leantime-adapter.ts
2025-05-04 11:35:50 +02:00

428 lines
15 KiB
TypeScript

import { Notification, NotificationCount } from '@/lib/types/notification';
import { NotificationAdapter } from './notification-adapter.interface';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/app/api/auth/[...nextauth]/route';
// 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 {
readonly sourceName = 'leantime';
private apiUrl: string;
private apiToken: string;
constructor() {
// Load from environment variables, matching the pattern used in other Leantime integrations
this.apiUrl = process.env.LEANTIME_API_URL || '';
this.apiToken = process.env.LEANTIME_TOKEN || '';
// Log configuration on initialization
console.log('[LEANTIME_ADAPTER] Initialized with:', {
apiUrlConfigured: !!this.apiUrl,
apiTokenConfigured: !!this.apiToken,
apiUrlPrefix: this.apiUrl ? this.apiUrl.substring(0, 30) + '...' : 'not set',
});
}
async getNotifications(userId: string, page = 1, limit = 20): Promise<Notification[]> {
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 [];
}
// Since notifications API doesn't work, get assigned tasks instead
console.log('[LEANTIME_ADAPTER] Getting assigned tasks as notifications');
const jsonRpcBody = {
jsonrpc: '2.0',
method: 'leantime.rpc.tickets.getAll', // Using the tasks API
params: {
userId: leantimeUserId,
status: "open" // Only get open tasks
},
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 tasks:', {
status: response.status,
body: errorText.substring(0, 200) + (errorText.length > 200 ? '...' : '')
});
return [];
}
const responseText = await response.text();
console.log('[LEANTIME_ADAPTER] Raw response:', 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 tasks API');
}
return [];
}
// 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 tasks:', error);
return [];
}
}
async getNotificationCount(userId: string): Promise<NotificationCount> {
console.log(`[LEANTIME_ADAPTER] getNotificationCount called for userId: ${userId}`);
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 this.getEmptyCount();
}
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 this.getEmptyCount();
}
// 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.tickets.getAll', // Using the tasks API
params: {
userId: leantimeUserId,
status: "open" // Only count open tasks
},
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 task count:', {
status: response.status,
body: errorText.substring(0, 200) + (errorText.length > 200 ? '...' : '')
});
return this.getEmptyCount();
}
const responseText = await response.text();
console.log('[LEANTIME_ADAPTER] Raw response:', 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 tasks API');
}
return this.getEmptyCount();
}
// 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] Task counts:', { total: totalCount, urgent: urgentCount });
return {
total: totalCount,
unread: urgentCount,
sources: {
leantime: {
total: totalCount,
unread: urgentCount
}
}
};
} catch (error) {
console.error('[LEANTIME_ADAPTER] Error fetching Leantime task count:', error);
return this.getEmptyCount();
}
}
async markAsRead(userId: string, notificationId: string): Promise<boolean> {
// 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<boolean> {
// 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<boolean> {
return !!(this.apiUrl && this.apiToken);
}
private getEmptyCount(): NotificationCount {
return {
total: 0,
unread: 0,
sources: {
leantime: {
total: 0,
unread: 0
}
}
};
}
private transformTasksToNotifications(tasks: LeantimeTask[], userId: string): Notification[] {
if (!Array.isArray(tasks)) {
return [];
}
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
private async getUserEmail(): Promise<string | null> {
console.log(`[LEANTIME_ADAPTER] Getting email from session`);
try {
const session = await getServerSession(authOptions);
if (!session || !session.user?.email) {
console.error('[LEANTIME_ADAPTER] No session or email found');
return null;
}
console.log(`[LEANTIME_ADAPTER] Got email from session: ${session.user.email}`);
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 (using similar pattern to the tasks API)
private async getLeantimeUserId(email: string): Promise<number | null> {
console.log(`[LEANTIME_ADAPTER] Getting Leantime userId for email: ${email}`);
try {
if (!this.apiToken) {
console.error('[LEANTIME_ADAPTER] LEANTIME_TOKEN is not set in environment variables');
return null;
}
console.log('[LEANTIME_ADAPTER] Sending request to get all Leantime users');
const jsonRpcBody = {
jsonrpc: '2.0',
method: 'leantime.rpc.users.getAll',
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 users:', {
status: response.status,
body: errorText.substring(0, 200) + (errorText.length > 200 ? '...' : '')
});
return null;
}
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)) {
console.error('[LEANTIME_ADAPTER] Invalid response format from Leantime users API');
return null;
}
const users = data.result;
console.log(`[LEANTIME_ADAPTER] Searching for user with email ${email} among ${users.length} users`);
// Log a few user objects to check structure
if (users.length > 0) {
console.log('[LEANTIME_ADAPTER] Sample user object:', JSON.stringify(users[0]));
}
const user = users.find((u: any) => u.username === email);
if (user) {
console.log('[LEANTIME_ADAPTER] Found Leantime user:', { id: user.id, username: user.username });
return user.id;
} else {
console.log('[LEANTIME_ADAPTER] No Leantime user found for email:', email);
// Try alternative property names that might contain the email
const alternativeUser = users.find((u: any) =>
u.email === email ||
u.mail === email ||
(typeof u.username === 'string' && u.username.toLowerCase() === email.toLowerCase())
);
if (alternativeUser) {
console.log('[LEANTIME_ADAPTER] Found Leantime user with alternative property match:', {
id: alternativeUser.id,
username: alternativeUser.username,
email: alternativeUser.email || alternativeUser.mail
});
return alternativeUser.id;
}
return null;
}
} catch (error) {
console.error('[LEANTIME_ADAPTER] Error getting Leantime user ID:', error);
return null;
}
}
}