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 <noreply@anthropic.com>
This commit is contained in:
mARTin 2026-02-24 00:39:19 +01:00
parent 220015b2b6
commit c8080f23ff
4 changed files with 142 additions and 3 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "my-weekly-todo-list", "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", "description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {

View File

@ -3,7 +3,7 @@ import { getServerSession } from 'next-auth';
import { authOptions } from "@/lib/auth"; import { authOptions } from "@/lib/auth";
import { PrismaClient } from '@prisma/client'; import { PrismaClient } from '@prisma/client';
import { createGoogleClient, updateGoogleTaskStatus, updateGoogleTask, deleteGoogleTask, fetchGoogleTasksForSync, GoogleTask } from '@/lib/google-tasks'; 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'; import { getOutlookAccessToken } from '@/lib/outlook-token';
const prisma = new PrismaClient(); const prisma = new PrismaClient();
@ -432,3 +432,72 @@ export async function PATCH(req: NextRequest) {
return NextResponse.json({ error: message }, { status: 500 }); 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 });
}
}

View File

@ -4800,7 +4800,7 @@ export default function WeeklyView() {
}); });
if (res.ok) { if (res.ok) {
const data = await res.json(); const data = await res.json();
const newTask = { let newTask = {
...data.task, ...data.task,
createdAt: new Date(data.task.createdAt), createdAt: new Date(data.task.createdAt),
updatedAt: new Date(data.task.updatedAt), updatedAt: new Date(data.task.updatedAt),
@ -4812,6 +4812,32 @@ export default function WeeklyView() {
: l, : 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) { } catch (e) {
console.error(e); console.error(e);

View File

@ -158,6 +158,50 @@ export const fetchMsTodoTasksForSync = async (
return (data.value || []) as MicrosoftTodoTask[]; 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<MicrosoftTodoTask> => {
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. * Update a Microsoft To-Do task.
*/ */