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:
parent
220015b2b6
commit
c8080f23ff
@ -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": {
|
||||||
|
|||||||
@ -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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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.
|
||||||
*/
|
*/
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user