- 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
402 lines
11 KiB
TypeScript
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}`);
|
|
}
|
|
};
|