My-Weekly-ToDo-List/src/lib/microsoft-todo.ts
mARTin 8842123caf feat: improve calendar sync, event modal, and task list management
- Fix All Day checkbox positioning in CalendarEventModal (own row)
- Add provider name to calendar dropdown (Google/Apple/Outlook)
- Optimistic UI updates after event save/delete (no reload needed)
- Force-refresh calendar cache after event mutations
- Reduce background sync interval from 5min to 2min
- Support forceRefresh in background-sync API
- Use shared Prisma singleton in tasks sync route
- Add per-provider task list fetching and sync checkboxes
- Add allDay support to event creation and editing

v1.4.0
2026-02-24 11:01:15 +01:00

402 lines
11 KiB
TypeScript

// @ts-nocheck
/* eslint-disable @typescript-eslint/no-explicit-any */
const GRAPH_ENDPOINT = 'https://graph.microsoft.com/v1.0';
export interface MicrosoftTodoList {
id: string;
displayName: string;
isOwner: boolean;
isShared: boolean;
wellknownListName: string;
}
export interface MicrosoftChecklistItem {
id: string;
displayName: string;
isChecked: boolean;
createdDateTime: string;
}
export interface MicrosoftTodoTask {
id: string;
title: string;
body?: {
content: string;
contentType: string;
};
status: 'notStarted' | 'inProgress' | 'completed' | 'waitingOnOthers' | 'deferred';
importance: 'low' | 'normal' | 'high';
dueDateTime?: {
dateTime: string;
timeZone: string;
};
completedDateTime?: {
dateTime: string;
timeZone: string;
};
createdDateTime: string;
lastModifiedDateTime: string;
checklistItems?: MicrosoftChecklistItem[];
}
/**
* Create a new Microsoft To-Do task list.
*/
export const createMsTodoList = async (accessToken: string, title: string): Promise<MicrosoftTodoList> => {
try {
const response = await fetch(`${GRAPH_ENDPOINT}/me/todo/lists`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ displayName: title })
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error?.message || 'Failed to create Microsoft To-Do list');
}
return await response.json();
} catch (error) {
console.error('Error creating Microsoft To-Do list:', error);
throw error;
}
};
/**
* Fetch all Microsoft To-Do task lists.
*/
export const fetchMsTodoLists = async (accessToken: string): Promise<MicrosoftTodoList[]> => {
const response = await fetch(`${GRAPH_ENDPOINT}/me/todo/lists`, {
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
const err = await response.text();
console.error('Error fetching Microsoft To-Do lists:', err);
throw new Error(`Failed to fetch To-Do lists: ${response.status} ${response.statusText}`);
}
const data = await response.json();
return (data.value || []) as MicrosoftTodoList[];
};
/**
* Fetch active tasks from a specific To-Do list (for import).
*/
export const fetchMsTodoTasks = async (
accessToken: string,
listId: string
): Promise<MicrosoftTodoTask[]> => {
const params = new URLSearchParams({
'$filter': "status ne 'completed'",
'$top': '100',
'$select': 'id,title,body,status,importance,dueDateTime,createdDateTime,lastModifiedDateTime'
});
const response = await fetch(
`${GRAPH_ENDPOINT}/me/todo/lists/${encodeURIComponent(listId)}/tasks?${params.toString()}`,
{
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
}
}
);
if (!response.ok) {
const err = await response.text();
console.error(`Error fetching tasks from list ${listId}:`, err);
throw new Error(`Failed to fetch To-Do tasks: ${response.status} ${response.statusText}`);
}
const data = await response.json();
return (data.value || []) as MicrosoftTodoTask[];
};
/**
* Fetch all tasks (including completed) with optional modified-since filter (for sync).
*/
export const fetchMsTodoTasksForSync = async (
accessToken: string,
listId: string,
modifiedSince?: string
): Promise<MicrosoftTodoTask[]> => {
const selectFields = 'id,title,body,status,dueDateTime,createdDateTime,lastModifiedDateTime';
const params = new URLSearchParams({
'$top': '100',
'$select': selectFields
});
if (modifiedSince) {
params.set('$filter', `lastModifiedDateTime gt ${modifiedSince}`);
}
const response = await fetch(
`${GRAPH_ENDPOINT}/me/todo/lists/${encodeURIComponent(listId)}/tasks?${params.toString()}`,
{
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
}
}
);
if (!response.ok) {
const err = await response.text();
console.error(`Error fetching sync tasks from list ${listId}:`, err);
throw new Error(`Failed to fetch To-Do tasks for sync: ${response.status} ${response.statusText}`);
}
const data = await response.json();
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/${encodeURIComponent(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.
*/
export const updateMsTodoTask = async (
accessToken: string,
listId: string,
taskId: string,
updates: {
title?: string;
body?: string;
status?: 'notStarted' | 'completed';
dueDateTime?: string | null;
}
): Promise<MicrosoftTodoTask> => {
const body: any = {};
if (updates.title !== undefined) body.title = updates.title;
if (updates.body !== undefined) body.body = { content: updates.body, contentType: 'text' };
if (updates.status !== undefined) {
body.status = updates.status;
if (updates.status === 'completed') {
body.completedDateTime = {
dateTime: new Date().toISOString(),
timeZone: 'UTC'
};
} else {
body.completedDateTime = null;
}
}
if (updates.dueDateTime !== undefined) {
body.dueDateTime = updates.dueDateTime
? { dateTime: new Date(updates.dueDateTime).toISOString(), timeZone: 'UTC' }
: null;
}
const response = await fetch(
`${GRAPH_ENDPOINT}/me/todo/lists/${encodeURIComponent(listId)}/tasks/${encodeURIComponent(taskId)}`,
{
method: 'PATCH',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
}
);
if (!response.ok) {
const err = await response.text();
throw new Error(`Failed to update To-Do task: ${err}`);
}
return response.json();
};
/**
* Delete a Microsoft To-Do task.
*/
export const deleteMsTodoTask = async (
accessToken: string,
listId: string,
taskId: string
): Promise<void> => {
const response = await fetch(
`${GRAPH_ENDPOINT}/me/todo/lists/${encodeURIComponent(listId)}/tasks/${encodeURIComponent(taskId)}`,
{
method: 'DELETE',
headers: {
'Authorization': `Bearer ${accessToken}`
}
}
);
if (!response.ok) {
const err = await response.text();
throw new Error(`Failed to delete To-Do task: ${err}`);
}
};
/**
* Check if a Microsoft To-Do task status maps to completed.
*/
export const isMsTodoTaskCompleted = (status: MicrosoftTodoTask['status']): boolean => {
return status === 'completed';
};
/**
* Fetch checklist items (sub-tasks) for a Microsoft To-Do task.
*/
export const fetchMsChecklistItems = async (
accessToken: string,
listId: string,
taskId: string
): Promise<MicrosoftChecklistItem[]> => {
const response = await fetch(
`${GRAPH_ENDPOINT}/me/todo/lists/${encodeURIComponent(listId)}/tasks/${encodeURIComponent(taskId)}/checklistItems`,
{
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
}
}
);
if (!response.ok) {
const err = await response.text();
console.error(`Error fetching checklist items for task ${taskId}:`, err);
return [];
}
const data = await response.json();
return (data.value || []) as MicrosoftChecklistItem[];
};
/**
* Create a checklist item (sub-task) for a Microsoft To-Do task.
*/
export const createMsChecklistItem = async (
accessToken: string,
listId: string,
taskId: string,
displayName: string
): Promise<MicrosoftChecklistItem> => {
const response = await fetch(
`${GRAPH_ENDPOINT}/me/todo/lists/${encodeURIComponent(listId)}/tasks/${encodeURIComponent(taskId)}/checklistItems`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ displayName })
}
);
if (!response.ok) {
const err = await response.text();
throw new Error(`Failed to create checklist item: ${err}`);
}
return response.json();
};
/**
* Update a checklist item (sub-task) for a Microsoft To-Do task.
*/
export const updateMsChecklistItem = async (
accessToken: string,
listId: string,
taskId: string,
checklistItemId: string,
updates: { displayName?: string; isChecked?: boolean }
): Promise<MicrosoftChecklistItem> => {
const response = await fetch(
`${GRAPH_ENDPOINT}/me/todo/lists/${encodeURIComponent(listId)}/tasks/${encodeURIComponent(taskId)}/checklistItems/${encodeURIComponent(checklistItemId)}`,
{
method: 'PATCH',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(updates)
}
);
if (!response.ok) {
const err = await response.text();
throw new Error(`Failed to update checklist item: ${err}`);
}
return response.json();
};
/**
* Delete a checklist item (sub-task) from a Microsoft To-Do task.
*/
export const deleteMsChecklistItem = async (
accessToken: string,
listId: string,
taskId: string,
checklistItemId: string
): Promise<void> => {
const response = await fetch(
`${GRAPH_ENDPOINT}/me/todo/lists/${encodeURIComponent(listId)}/tasks/${encodeURIComponent(taskId)}/checklistItems/${encodeURIComponent(checklistItemId)}`,
{
method: 'DELETE',
headers: {
'Authorization': `Bearer ${accessToken}`
}
}
);
if (!response.ok) {
const err = await response.text();
throw new Error(`Failed to delete checklist item: ${err}`);
}
};