import { NextRequest, NextResponse } from 'next/server'; import { getServerSession } from 'next-auth'; import { authOptions } from "@/lib/auth"; import { PrismaClient } from '@prisma/client'; import { createGoogleClient, fetchGoogleTasks, fetchGoogleTaskLists } from '@/lib/google-tasks'; import { fetchMsTodoLists, fetchMsTodoTasks, isMsTodoTaskCompleted, fetchMsChecklistItems } from '@/lib/microsoft-todo'; import { getOutlookAccessToken } from '@/lib/outlook-token'; const prisma = new PrismaClient(); interface SourceList { id: string; title: string; } interface ImportedTask { title: string; description: string; externalId: string; externalListId: string; dueDate: Date | null; status: string; sourceListTitle: string; parentExternalId?: string; } export async function POST(req: NextRequest) { try { const session = await getServerSession(authOptions); if (!session?.user?.email) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } const body = await req.json(); const { provider, sourceLists } = body; if (!provider || !['google', 'outlook'].includes(provider)) { return NextResponse.json({ error: 'Invalid provider' }, { status: 400 }); } const user = await prisma.user.findUnique({ where: { email: session.user.email } }); if (!user) { return NextResponse.json({ error: 'User not found' }, { status: 404 }); } // Normalize sourceLists let lists: SourceList[] = []; if (Array.isArray(sourceLists)) { lists = sourceLists.map((item: any) => typeof item === 'string' ? { id: item, title: item } : { id: item.id, title: item.title || item.id } ); } const importedTasks: ImportedTask[] = []; let targetLists: SourceList[] = lists; if (provider === 'google') { const account = await prisma.account.findFirst({ where: { userId: user.id, provider: 'google' } }); if (!account || !account.access_token) { return NextResponse.json({ error: 'Google account not connected' }, { status: 400 }); } const client = createGoogleClient(account.access_token, account.refresh_token || undefined); if (targetLists.length === 0) { const googleLists = await fetchGoogleTaskLists(client); if (googleLists.length > 0) { targetLists = [{ id: googleLists[0].id, title: googleLists[0].title }]; } } for (const sourceList of targetLists) { try { const googleTasks = await fetchGoogleTasks(client, sourceList.id); importedTasks.push(...googleTasks.map(t => ({ title: t.title, description: t.notes || '', externalId: t.id, externalListId: sourceList.id, dueDate: t.due ? new Date(t.due) : null, status: t.status, sourceListTitle: sourceList.title, parentExternalId: t.parent || undefined, }))); } catch (e) { console.error(`Error fetching Google tasks for list ${sourceList.id}:`, e); } } } if (provider === 'outlook') { const accessToken = await getOutlookAccessToken(user.id); if (!accessToken) { return NextResponse.json({ error: 'Outlook account not connected' }, { status: 400 }); } if (targetLists.length === 0) { const msTodoLists = await fetchMsTodoLists(accessToken); if (msTodoLists.length > 0) { const defaultList = msTodoLists.find(l => l.wellknownListName === 'defaultList') || msTodoLists[0]; targetLists = [{ id: defaultList.id, title: defaultList.displayName }]; } } for (const sourceList of targetLists) { try { const msTasks = await fetchMsTodoTasks(accessToken, sourceList.id); for (const t of msTasks) { importedTasks.push({ title: t.title, description: t.body?.content || '', externalId: t.id, externalListId: sourceList.id, dueDate: t.dueDateTime ? new Date(t.dueDateTime.dateTime) : null, status: isMsTodoTaskCompleted(t.status) ? 'completed' : 'notStarted', sourceListTitle: sourceList.title, }); // Fetch checklist items as sub-tasks try { const checklistItems = await fetchMsChecklistItems(accessToken, sourceList.id, t.id); for (const item of checklistItems) { importedTasks.push({ title: item.displayName, description: '', externalId: item.id, externalListId: sourceList.id, dueDate: null, status: item.isChecked ? 'completed' : 'notStarted', sourceListTitle: sourceList.title, parentExternalId: t.id, }); } } catch (e) { console.error(`Error fetching checklist items for task ${t.id}:`, e); } } } catch (e) { console.error(`Error fetching MS Todo tasks for list ${sourceList.id}:`, e); } } } // Group tasks by source list title const tasksByList = new Map(); // Pre-initialize with all target lists to ensure they are created even if empty for (const tl of targetLists) { tasksByList.set(tl.title, []); } for (const task of importedTasks) { const listTitle = task.sourceListTitle; if (!tasksByList.has(listTitle)) { tasksByList.set(listTitle, []); } tasksByList.get(listTitle)!.push(task); } console.log(`[IMPORT] Total tasks: ${importedTasks.length} across ${tasksByList.size} lists`); let count = 0; let updatedCount = 0; let listsCreated = 0; // Track externalId -> local task ID for parent-child linking const externalToLocalId = new Map(); // Tasks that need parent linking after creation const pendingParentLinks: { localId: string; parentExternalId: string }[] = []; for (const [listTitle, tasks] of tasksByList) { let somedayList = await prisma.somedayList.findFirst({ where: { userId: user.id, title: listTitle } }); if (!somedayList) { somedayList = await prisma.somedayList.create({ data: { userId: user.id, title: listTitle, order: 0 } }); listsCreated++; console.log(`[IMPORT] Created SomedayList "${listTitle}" (${somedayList.id})`); } // First pass: create/update all tasks (parents first via sorting) const sortedTasks = [...tasks].sort((a, b) => { // Parent tasks (no parentExternalId) come first if (!a.parentExternalId && b.parentExternalId) return -1; if (a.parentExternalId && !b.parentExternalId) return 1; return 0; }); for (const task of sortedTasks) { const existingTask = await prisma.task.findFirst({ where: { userId: user.id, externalId: task.externalId, externalProvider: provider } }); if (existingTask) { await prisma.task.update({ where: { id: existingTask.id }, data: { somedayListId: somedayList.id, lastSyncedAt: new Date() } }); externalToLocalId.set(task.externalId, existingTask.id); if (task.parentExternalId) { pendingParentLinks.push({ localId: existingTask.id, parentExternalId: task.parentExternalId }); } updatedCount++; } else { const newTask = await prisma.task.create({ data: { userId: user.id, title: task.title, description: task.description, completed: task.status === 'completed' || task.status === 'COMPLETED', somedayListId: somedayList.id, externalId: task.externalId, externalProvider: provider, externalListId: task.externalListId, lastSyncedAt: new Date() } }); externalToLocalId.set(task.externalId, newTask.id); if (task.parentExternalId) { pendingParentLinks.push({ localId: newTask.id, parentExternalId: task.parentExternalId }); } count++; } } } // Second pass: link parent-child relationships for (const { localId, parentExternalId } of pendingParentLinks) { const parentLocalId = externalToLocalId.get(parentExternalId); if (parentLocalId) { await prisma.task.update({ where: { id: localId }, data: { parentTaskId: parentLocalId } }); } } console.log(`[IMPORT] Done! Created: ${count}, Updated: ${updatedCount}, Lists created: ${listsCreated}`); return NextResponse.json({ success: true, count, updatedCount, listsCreated }); } catch (error: unknown) { console.error('Import error:', error); const message = error instanceof Error ? error.message : 'Import failed'; return NextResponse.json({ error: message }, { status: 500 }); } }