From c8080f23ffda37cfdfef3cd21694cb46dd874829 Mon Sep 17 00:00:00 2001 From: mARTin Date: Tue, 24 Feb 2026 00:39:19 +0100 Subject: [PATCH] feat: add bidirectional Microsoft To-Do task creation sync New tasks created in synced someday lists are now pushed to Microsoft To-Do automatically. Added createMsTodoTask() API function, POST handler on /api/tasks/sync, and frontend wiring to call sync after local task creation in externally-linked lists. v1.3.0 Co-Authored-By: Claude Opus 4.6 --- package.json | 2 +- src/app/api/tasks/sync/route.ts | 71 ++++++++++++++++++++++++++++++++- src/components/WeeklyView.tsx | 28 ++++++++++++- src/lib/microsoft-todo.ts | 44 ++++++++++++++++++++ 4 files changed, 142 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 2c6e03f..1fd6ca4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "my-weekly-todo-list", - "version": "1.2.3", + "version": "1.3.0", "description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view", "main": "index.js", "scripts": { diff --git a/src/app/api/tasks/sync/route.ts b/src/app/api/tasks/sync/route.ts index c818bbc..8d49e11 100644 --- a/src/app/api/tasks/sync/route.ts +++ b/src/app/api/tasks/sync/route.ts @@ -3,7 +3,7 @@ import { getServerSession } from 'next-auth'; import { authOptions } from "@/lib/auth"; import { PrismaClient } from '@prisma/client'; import { createGoogleClient, updateGoogleTaskStatus, updateGoogleTask, deleteGoogleTask, fetchGoogleTasksForSync, GoogleTask } from '@/lib/google-tasks'; -import { fetchMsTodoTasksForSync, updateMsTodoTask, deleteMsTodoTask, isMsTodoTaskCompleted } from '@/lib/microsoft-todo'; +import { fetchMsTodoTasksForSync, updateMsTodoTask, deleteMsTodoTask, createMsTodoTask, isMsTodoTaskCompleted } from '@/lib/microsoft-todo'; import { getOutlookAccessToken } from '@/lib/outlook-token'; const prisma = new PrismaClient(); @@ -432,3 +432,72 @@ export async function PATCH(req: NextRequest) { return NextResponse.json({ error: message }, { status: 500 }); } } + +// POST - Push a newly created local task to the external provider +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 { taskId } = body; + + if (!taskId) { + return NextResponse.json({ error: 'Task ID required' }, { status: 400 }); + } + + const task = await prisma.task.findUnique({ + where: { id: taskId }, + include: { user: true, somedayList: true } + }); + + if (!task) { + return NextResponse.json({ error: 'Task not found' }, { status: 404 }); + } + + if (task.user.email !== session.user.email) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 403 }); + } + + // Task must be in a someday list that has an external link + if (!task.somedayList?.externalId || !task.somedayList?.externalProvider) { + return NextResponse.json({ error: 'List is not linked to an external provider' }, { status: 400 }); + } + + const provider = task.somedayList.externalProvider; + const listExternalId = task.somedayList.externalId; + + if (provider === 'outlook') { + const outlookToken = await getOutlookAccessToken(task.userId); + if (!outlookToken) { + return NextResponse.json({ error: 'Outlook token not available' }, { status: 400 }); + } + + const created = await createMsTodoTask(outlookToken, listExternalId, { + title: task.title, + body: task.description || undefined, + }); + + const updatedTask = await prisma.task.update({ + where: { id: taskId }, + data: { + externalId: created.id, + externalProvider: 'outlook', + externalListId: listExternalId, + lastSyncedAt: new Date(), + } + }); + + return NextResponse.json({ success: true, task: updatedTask }); + } + + return NextResponse.json({ error: `Provider "${provider}" creation sync not supported yet` }, { status: 400 }); + + } catch (error: unknown) { + console.error('Task creation sync error:', error); + const message = error instanceof Error ? error.message : 'Task creation sync failed'; + return NextResponse.json({ error: message }, { status: 500 }); + } +} diff --git a/src/components/WeeklyView.tsx b/src/components/WeeklyView.tsx index 7e55718..9ae37ea 100644 --- a/src/components/WeeklyView.tsx +++ b/src/components/WeeklyView.tsx @@ -4800,7 +4800,7 @@ export default function WeeklyView() { }); if (res.ok) { const data = await res.json(); - const newTask = { + let newTask = { ...data.task, createdAt: new Date(data.task.createdAt), updatedAt: new Date(data.task.updatedAt), @@ -4812,6 +4812,32 @@ export default function WeeklyView() { : l, ), ); + + // Push to external provider if list is synced + if (list.externalProvider) { + try { + const syncRes = await fetch("/api/tasks/sync", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ taskId: newTask.id }), + }); + if (syncRes.ok) { + const syncData = await syncRes.json(); + if (syncData.task) { + newTask = { ...newTask, ...syncData.task }; + setSomedayLists((prev) => + prev.map((l) => + l.id === list.id + ? { ...l, tasks: l.tasks.map(t => t.id === newTask.id ? newTask : t) } + : l, + ), + ); + } + } + } catch (syncErr) { + console.error("Failed to sync new task to external provider:", syncErr); + } + } } } catch (e) { console.error(e); diff --git a/src/lib/microsoft-todo.ts b/src/lib/microsoft-todo.ts index 412f8df..9f41e0a 100644 --- a/src/lib/microsoft-todo.ts +++ b/src/lib/microsoft-todo.ts @@ -158,6 +158,50 @@ export const fetchMsTodoTasksForSync = async ( return (data.value || []) as MicrosoftTodoTask[]; }; +/** + * Create a new Microsoft To-Do task in a list. + */ +export const createMsTodoTask = async ( + accessToken: string, + listId: string, + taskData: { + title: string; + body?: string; + dueDateTime?: string; + } +): Promise => { + const requestBody: any = { title: taskData.title }; + + if (taskData.body) { + requestBody.body = { content: taskData.body, contentType: 'text' }; + } + if (taskData.dueDateTime) { + requestBody.dueDateTime = { + dateTime: new Date(taskData.dueDateTime).toISOString(), + timeZone: 'UTC' + }; + } + + const response = await fetch( + `${GRAPH_ENDPOINT}/me/todo/lists/${listId}/tasks`, + { + method: 'POST', + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify(requestBody) + } + ); + + if (!response.ok) { + const err = await response.text(); + throw new Error(`Failed to create To-Do task: ${err}`); + } + + return response.json(); +}; + /** * Update a Microsoft To-Do task. */