My-Weekly-ToDo-List/src/app/api/tasks/import/route.ts
mARTin f9d578ddf1 fix: time grid layout alignment and task drag/drop collision detection
- Removed boxed background from grid tasks and added hover-only resize handle

- Fixed vertical drop calculation using correct cell sizes

- Aligned time column header perfectly using a structural ghost replica

- Added collision detection to prevent dragging tasks over occupied slots
2026-02-23 21:29:56 +01:00

261 lines
11 KiB
TypeScript

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<string, ImportedTask[]>();
// 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<string, string>();
// 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 });
}
}