import { NextRequest, NextResponse } from "next/server"; import { getServerSession } from "next-auth/next"; import { authOptions } from "@/app/api/auth/options"; import { getCachedTasksData, cacheTasksData } from "@/lib/redis"; import { logger } from "@/lib/logger"; import { fetchWithTimeout, fetchJsonWithTimeout } from '@/lib/utils/fetch-with-timeout'; interface Task { id: 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; } async function getLeantimeUserId(email: string): Promise { try { if (!process.env.LEANTIME_TOKEN) { logger.error('[LEANTIME_TASKS] LEANTIME_TOKEN is not set in environment variables'); return null; } logger.debug('[LEANTIME_TASKS] Fetching Leantime users', { emailHash: Buffer.from(email.toLowerCase()).toString('base64').slice(0, 12), apiUrlPresent: !!process.env.LEANTIME_API_URL, tokenLength: process.env.LEANTIME_TOKEN.length, }); const headers: Record = { 'Content-Type': 'application/json', 'X-API-Key': process.env.LEANTIME_TOKEN }; let data; try { data = await fetchJsonWithTimeout(`${process.env.LEANTIME_API_URL}/api/jsonrpc`, { method: 'POST', timeout: 10000, // 10 seconds headers, body: JSON.stringify({ jsonrpc: '2.0', method: 'leantime.rpc.users.getAll', id: 1 }), }); } catch (error) { logger.error('[LEANTIME_TASKS] Failed to fetch Leantime users', { error: error instanceof Error ? error.message : String(error), }); return null; } if (!data.result || !Array.isArray(data.result)) { logger.error('[LEANTIME_TASKS] Invalid response format from Leantime users API'); return null; } const users = data.result; const user = users.find((u: any) => u.username === email); if (user) { logger.debug('[LEANTIME_TASKS] Found Leantime user', { emailHash: Buffer.from(email.toLowerCase()).toString('base64').slice(0, 12), }); } else { logger.warn('[LEANTIME_TASKS] No Leantime user found for username', { emailHash: Buffer.from(email.toLowerCase()).toString('base64').slice(0, 12), }); } return user ? user.id : null; } catch (error) { logger.error('[LEANTIME_TASKS] Error fetching Leantime user ID', { error: error instanceof Error ? error.message : String(error), }); return null; } } export async function GET(request: NextRequest) { try { const session = await getServerSession(authOptions); if (!session?.user?.email) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } // Check for force refresh parameter const url = new URL(request.url); const forceRefresh = url.searchParams.get('refresh') === 'true'; // 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) const filteredCachedTasks = cachedTasks.filter((task: any) => { const taskStatus = task.status; if (taskStatus !== null && taskStatus !== undefined) { const statusNum = typeof taskStatus === 'string' ? parseInt(taskStatus, 10) : taskStatus; if (statusNum === 5 || taskStatus === '5' || taskStatus === 'Done' || taskStatus === 'done' || taskStatus === 'DONE') { logger.debug('[LEANTIME_TASKS] Filtering out done task from cache', { id: task.id, headline: task.headline, status: taskStatus, statusNum, }); return false; } } return true; }); if (filteredCachedTasks.length !== cachedTasks.length) { logger.debug('[LEANTIME_TASKS] Filtered done tasks from cache', { before: cachedTasks.length, after: filteredCachedTasks.length, removed: cachedTasks.length - filteredCachedTasks.length, }); // Update cache with filtered tasks await cacheTasksData(session.user.id, filteredCachedTasks); } logger.debug('[LEANTIME_TASKS] Using cached tasks data', { emailHash: Buffer.from(session.user.email.toLowerCase()).toString('base64').slice(0, 12), taskCount: filteredCachedTasks.length, }); return NextResponse.json(filteredCachedTasks); } } 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 = { 'Content-Type': 'application/json', 'X-API-Key': process.env.LEANTIME_TOKEN! }; let data; try { data = await fetchJsonWithTimeout(`${process.env.LEANTIME_API_URL}/api/jsonrpc`, { method: 'POST', timeout: 10000, // 10 seconds headers, body: JSON.stringify({ jsonrpc: '2.0', method: 'leantime.rpc.tickets.getAll', params: { userId: userId, status: "all" }, id: 1 }), }); } catch (error) { logger.error('[LEANTIME_TASKS] Failed to fetch tasks from Leantime', { error: error instanceof Error ? error.message : String(error), }); throw new Error('Failed to fetch tasks from Leantime'); } if (!data.result || !Array.isArray(data.result)) { logger.error('[LEANTIME_TASKS] Invalid response format from Leantime tasks API'); throw new Error('Invalid response format from Leantime'); } // Log detailed status information before filtering const statusBreakdownBefore = data.result.reduce((acc: any, task: any) => { const status = task.status; const statusKey = String(status); if (!acc[statusKey]) { acc[statusKey] = { count: 0, type: typeof status, sample: [] }; } acc[statusKey].count++; if (acc[statusKey].sample.length < 3) { acc[statusKey].sample.push({ id: task.id, headline: task.headline }); } return acc; }, {}); logger.debug('[LEANTIME_TASKS] Received tasks summary', { count: data.result.length, statusBreakdown: statusBreakdownBefore, idsSample: data.result.slice(0, 20).map((task: any) => task.id), }); const tasks = data.result .filter((task: any) => { // Filter out any task (main or subtask) that has status Done (5) // Handle both number and string formats, and check for null/undefined 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 === 5 || statusStr === '5' || statusStr === 'done'; if (isDone) { logger.debug('[LEANTIME_TASKS] Filtering out done task', { id: task.id, headline: task.headline, status: taskStatus, statusType: typeof taskStatus, statusNum, statusStr, }); return false; } } // Convert both to strings for comparison to handle any type mismatches const taskEditorId = String(task.editorId).trim(); const currentUserId = String(userId).trim(); // Only show tasks where the user is the editor const isUserEditor = taskEditorId === currentUserId; return isUserEditor; }) .map((task: any) => ({ id: task.id.toString(), headline: task.headline, projectName: task.projectName, projectId: task.projectId, status: task.status, 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, // Added type field to identify subtasks dependingTicketId: task.dependingTicketId || null // Added parent task reference })); // Log detailed status information for debugging const statusBreakdown = tasks.reduce((acc: any, task: any) => { const status = task.status; const statusKey = String(status); if (!acc[statusKey]) { acc[statusKey] = 0; } acc[statusKey]++; return acc; }, {}); logger.debug('[LEANTIME_TASKS] Filtered tasks for user', { userId, count: tasks.length, statusBreakdown, sampleTasks: tasks.slice(0, 5).map((t: any) => ({ id: t.id, headline: t.headline, status: t.status, statusType: typeof t.status, })), }); // Cache the results await cacheTasksData(session.user.id, tasks); return NextResponse.json(tasks); } catch (error) { logger.error('[LEANTIME_TASKS] Error in tasks route', { error: error instanceof Error ? error.message : String(error), }); return NextResponse.json( { error: "Failed to fetch tasks" }, { status: 500 } ); } }