My-Weekly-ToDo-List/src/lib/microsoft-todo.ts
mARTin-B78 53de4017e5
Some checks are pending
Playwright Tests / test (push) Waiting to run
Test and SonarQube / quality (push) Waiting to run
fix: @ts-nocheck aus allen drei Dateien entfernen
Die Dateien schalteten die Typpruefung komplett ab und blieben deshalb auch
von ignoreBuildErrors: false unberuehrt -- eine Luecke im gerade erst
geschlossenen Netz.

- calendar-cache.ts brauchte die Ausnahme gar nicht: ohne sie null Fehler.
- microsoft-todo.ts hatte vier Faelle zirkulaerer Typinferenz
  (url -> data -> response -> url), aufgeloest durch explizite Annotationen.
- outlook-calendar.ts verbarg einen moeglichen Laufzeitfehler: Der Code
  streute response!-Assertions, eine Stelle war vergessen. Statt noch einer
  Assertion prueft er jetzt echt, ob nach drei Wiederholversuchen ueberhaupt
  eine Antwort vorliegt.

Ausserdem Block I in TODO.md: Hervorhebung des aktuellen Tages im
Spaltenkopf und die Symbole in Kalenderterminen nach oben rechts, beide mit
Fundstelle in globals.css.

Verifiziert: Typecheck 0 Fehler, npm run build Exit 0.

v1.118.2

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 13:59:57 +02:00

505 lines
15 KiB
TypeScript

/* 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[];
};
/**
* Delete a Microsoft To-Do task list.
*/
export const deleteMsTodoList = async (
accessToken: string,
listId: string
): Promise<void> => {
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<MicrosoftTodoTask[]> => {
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<MicrosoftTodoTask[]> => {
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<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'
};
}
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<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;
}
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<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}`);
}
};