- 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
261 lines
11 KiB
TypeScript
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 });
|
|
}
|
|
}
|