/* 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 => { 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 => { 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[]; }; /** * Delete a Microsoft To-Do task list. */ export const deleteMsTodoList = async ( accessToken: string, listId: string ): Promise => { const response = await fetch( `${GRAPH_ENDPOINT}/me/todo/lists/${encodeURIComponent(listId)}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${accessToken}` } } ); // 404/410 mean it's already gone — treat as success if (response.status === 404 || response.status === 410) return; if (!response.ok) { const err = await response.text(); throw new Error(`Failed to delete To-Do list: ${err}`); } }; /** * Fetch active tasks from a specific To-Do list (for import). */ export const fetchMsTodoTasks = async ( accessToken: string, listId: string ): Promise => { const params = new URLSearchParams({ '$filter': "status ne 'completed'", '$top': '100' }); 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 => { const allTasks: MicrosoftTodoTask[] = []; let url: string | null = (() => { const params = new URLSearchParams({ '$top': '100' }); if (modifiedSince) { params.set('$filter', `lastModifiedDateTime gt ${modifiedSince}`); } return `${GRAPH_ENDPOINT}/me/todo/lists/${encodeURIComponent(listId)}/tasks?${params.toString()}`; })(); while (url) { const response: Response = await fetch(url, { 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: any = await response.json(); allTasks.push(...((data.value || []) as MicrosoftTodoTask[])); url = data['@odata.nextLink'] || null; } return allTasks; }; /** * Microsoft Graph delta sync: returns only the tasks that changed (created, * updated, or deleted) since the previous delta token, plus a fresh * `deltaToken` to use on the next call. Pass `null` to start a new chain. * * Deleted tasks come back with an `@removed` field; we surface them via * the optional `_deleted` boolean on the task object. * * Cost: typically a few hundred bytes per call when nothing changed. */ export const fetchMsTodoTasksDelta = async ( accessToken: string, listId: string, previousDeltaToken: string | null, ): Promise<{ tasks: (MicrosoftTodoTask & { _deleted?: boolean })[]; deltaToken: string | null }> => { const tasks: (MicrosoftTodoTask & { _deleted?: boolean })[] = []; let url: string | null; if (previousDeltaToken) { // Resume from where we left off url = `${GRAPH_ENDPOINT}/me/todo/lists/${encodeURIComponent(listId)}/tasks/delta?$deltatoken=${encodeURIComponent(previousDeltaToken)}`; } else { // Initial sync — Graph will paginate via @odata.nextLink, then return @odata.deltaLink url = `${GRAPH_ENDPOINT}/me/todo/lists/${encodeURIComponent(listId)}/tasks/delta`; } let deltaToken: string | null = null; while (url) { const response: Response = await fetch(url, { headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json' } }); if (!response.ok) { const err = await response.text(); // 410 Gone = delta token expired (>30 days). Caller should retry with null. if (response.status === 410) { throw Object.assign(new Error('Delta token expired'), { code: 'DELTA_EXPIRED' }); } console.error(`Error fetching delta from list ${listId}:`, err); throw new Error(`Failed to fetch To-Do delta: ${response.status} ${response.statusText}`); } const data: any = await response.json(); for (const item of (data.value || [])) { if (item['@removed']) { tasks.push({ ...item, _deleted: true } as any); } else { tasks.push(item as MicrosoftTodoTask); } } const nextLink: string | undefined = data['@odata.nextLink']; const deltaLink: string | undefined = data['@odata.deltaLink']; if (deltaLink) { // Final page — extract the new delta token const match = deltaLink.match(/[?&]\$deltatoken=([^&]+)/); deltaToken = match ? decodeURIComponent(match[1]) : null; url = null; } else if (nextLink) { url = nextLink; } else { url = null; } } return { tasks, deltaToken }; }; /** * 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; importance?: 'low' | 'normal' | 'high'; } ): 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' }; } if (taskData.importance) { requestBody.importance = taskData.importance; } 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; importance?: 'low' | 'normal' | 'high'; } ): Promise => { 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; } if (updates.importance !== undefined) body.importance = updates.importance; 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 => { 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 => { 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 => { 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 => { 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 => { 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}`); } };