Reposition notes popup to floating vertical container and various integration fixes

This commit is contained in:
mARTin 2026-02-24 12:26:02 +01:00
parent 8842123caf
commit 368928c016
14 changed files with 410 additions and 192 deletions

View File

@ -1,6 +1,6 @@
{
"name": "my-weekly-todo-list",
"version": "1.4.0",
"version": "1.5.0",
"description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view",
"main": "index.js",
"scripts": {

View File

@ -39,7 +39,7 @@ export async function POST(request: NextRequest) {
if (!session?.user?.email) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const body = await request.json();
const { calendarId, title, description, start, end, location, allDay } = body;
const { calendarId, title, description, start, end, location, allDay, recurrence, url } = body;
console.log('[API] Creating event:', { calendarId, title, start, end });
@ -60,8 +60,10 @@ export async function POST(request: NextRequest) {
start,
end,
location,
allDay: !!allDay
});
allDay: !!allDay,
recurrence,
url,
} as any);
// Update cache
upsertCachedEvent(userId, connection.id, connection.provider, event)
@ -81,7 +83,7 @@ export async function PATCH(request: NextRequest) {
if (!session?.user?.email) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const body = await request.json();
const { calendarId, eventId, title, description, start, end, location, allDay } = body;
const { calendarId, eventId, title, description, start, end, location, allDay, recurrence, url } = body;
console.log('[API] Updating event:', { calendarId, eventId, title });
@ -102,8 +104,10 @@ export async function PATCH(request: NextRequest) {
start,
end,
location,
allDay: allDay !== undefined ? !!allDay : undefined
});
allDay: allDay !== undefined ? !!allDay : undefined,
recurrence,
url,
} as any);
// Update cache
upsertCachedEvent(userId, connection.id, connection.provider, event)

View File

@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from "@/lib/auth";
import { prisma } from '@/lib/prisma';
import { createGoogleClient, updateGoogleTaskStatus, updateGoogleTask, deleteGoogleTask, fetchGoogleTasksForSync, GoogleTask } from '@/lib/google-tasks';
import { createGoogleClient, createGoogleTask, updateGoogleTaskStatus, updateGoogleTask, deleteGoogleTask, fetchGoogleTasksForSync, GoogleTask } from '@/lib/google-tasks';
import { fetchMsTodoTasksForSync, updateMsTodoTask, deleteMsTodoTask, createMsTodoTask, isMsTodoTaskCompleted } from '@/lib/microsoft-todo';
import { getOutlookAccessToken } from '@/lib/outlook-token';
@ -492,6 +492,35 @@ export async function POST(req: NextRequest) {
return NextResponse.json({ success: true, task: updatedTask });
}
if (provider === 'google') {
const account = await prisma.account.findFirst({
where: { userId: task.userId, provider: 'google' }
});
if (!account?.access_token) {
return NextResponse.json({ error: 'Google token not available' }, { status: 400 });
}
const client = createGoogleClient(account.access_token, account.refresh_token || undefined);
const created = await createGoogleTask(client, listExternalId, {
title: task.title,
notes: task.description || undefined,
due: task.scheduledDate ? task.scheduledDate.toISOString() : undefined,
});
const updatedTask = await prisma.task.update({
where: { id: taskId },
data: {
externalId: created.id,
externalProvider: 'google',
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) {

View File

@ -982,6 +982,27 @@ h3 {
flex-wrap: nowrap;
}
/* Time-grid tasks: show actions below task text as a dropdown toolbar */
.time-slot-task > .task-actions {
position: absolute;
top: auto;
bottom: auto;
left: 0;
right: auto;
margin-top: 0;
background: var(--weekly-bg, white);
border: 1px solid var(--weekly-border, #e0e0e0);
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0,0,0,0.12);
padding: 2px 4px;
z-index: 20;
}
.dark .time-slot-task > .task-actions {
background: #2a2a2a;
border-color: #404040;
}
.task-action-btn {
background: none;
border: none;

View File

@ -32,6 +32,8 @@ export default function CalendarEventModal({
const [title, setTitle] = useState(event?.title || '');
const [description, setDescription] = useState(event?.description || '');
const [location, setLocation] = useState(event?.location || '');
const [url, setUrl] = useState(event?.url || '');
const [recurrence, setRecurrence] = useState(event?.recurrence || 'none');
const [calendarId, setCalendarId] = useState(event?.calendarId || (availableCalendars.length > 0 ? availableCalendars[0].id : ''));
// Date/Time State
@ -90,6 +92,8 @@ export default function CalendarEventModal({
title,
description,
location,
url: url || undefined,
recurrence: recurrence !== 'none' ? recurrence : undefined,
calendarId,
allDay,
start: { dateTime: startDate.toISOString() },
@ -218,6 +222,23 @@ export default function CalendarEventModal({
</div>
</div>
{/* Recurrence */}
<div>
<label style={{ display: 'block', marginBottom: '5px', fontSize: '0.9rem', color: '#666' }}>Repeat</label>
<select
value={recurrence}
onChange={e => setRecurrence(e.target.value)}
style={{ width: '100%', padding: '8px', border: '1px solid #ddd', borderRadius: '4px' }}
>
<option value="none">Never</option>
<option value="daily">Every Day</option>
<option value="weekly">Every Week</option>
<option value="biweekly">Every 2 Weeks</option>
<option value="monthly">Every Month</option>
<option value="yearly">Every Year</option>
</select>
</div>
{/* Location */}
<div>
<label style={{ display: 'block', marginBottom: '5px', fontSize: '0.9rem', color: '#666' }}>Location</label>
@ -230,9 +251,21 @@ export default function CalendarEventModal({
/>
</div>
{/* URL */}
<div>
<label style={{ display: 'block', marginBottom: '5px', fontSize: '0.9rem', color: '#666' }}>URL</label>
<input
type="url"
value={url}
onChange={e => setUrl(e.target.value)}
placeholder="https://..."
style={{ width: '100%', padding: '8px', border: '1px solid #ddd', borderRadius: '4px' }}
/>
</div>
{/* Description */}
<div>
<label style={{ display: 'block', marginBottom: '5px', fontSize: '0.9rem', color: '#666' }}>Description</label>
<label style={{ display: 'block', marginBottom: '5px', fontSize: '0.9rem', color: '#666' }}>Notes</label>
<textarea
value={description}
onChange={e => setDescription(e.target.value)}

View File

@ -269,21 +269,12 @@ export function GridTaskBlock({
</span>
)}
{(true) && (
</div>
{/* Task Actions - shown below text on hover */}
<div
className="task-actions"
style={{
display: "flex",
alignItems: "center",
gap: "2px",
marginLeft: "4px",
flexShrink: 0,
flexWrap: "nowrap",
background: darkMode ? "rgba(0,0,0,0.6)" : "rgba(255,255,255,0.8)",
padding: "1px 3px",
borderRadius: "4px",
boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
}}
onClick={(e) => e.stopPropagation()}
>
<button
className={`task-action-btn ${task.completed ? "active text-green-600 dark:text-green-500" : ""}`}
@ -329,6 +320,15 @@ export function GridTaskBlock({
</svg>
</button>
)}
{!task.completed && (
<button
className={`task-action-btn ${task.isRecurring ? "active" : ""}`}
onClick={(e) => { e.stopPropagation(); setSelectedTaskForRecurrence(task); }}
title={task.isRecurring ? "Edit recurrence" : "Make recurring"}
>
<Repeat size={12} />
</button>
)}
<button className="task-action-btn delete text-red-500 hover:text-red-700 hover:bg-red-100/50 dark:hover:bg-red-900/30 rounded" onClick={(e) => { e.stopPropagation(); deleteTask(task.id); }} title="Delete">
<svg viewBox="0 0 24 24" width="12" height="12" stroke="currentColor" strokeWidth="2.5" fill="none" strokeLinecap="round" strokeLinejoin="round">
<line x1="18" y1="6" x2="6" y2="18" />
@ -336,8 +336,6 @@ export function GridTaskBlock({
</svg>
</button>
</div>
)}
</div>
{/* Inline Expanders Container */}
<div style={{ paddingLeft: "4px", paddingRight: "4px", paddingBottom: "10px", marginTop: "4px" }}>

View File

@ -42,6 +42,7 @@ import {
Trash2,
Undo2,
Redo2,
AlertCircle,
} from "lucide-react";
// Types
@ -96,7 +97,7 @@ interface CalendarEvent {
title: string;
startTime: string;
endTime: string;
source: "google" | "apple";
source: "google" | "apple" | "outlook";
calendarId?: string;
calendarTitle?: string;
calendarColor?: string;
@ -487,6 +488,7 @@ export default function WeeklyView() {
const [viewDays, setViewDays] = useState(7);
const [isLoading, setIsLoading] = useState(true);
const [isSyncing, setIsSyncing] = useState(false);
const [syncError, setSyncError] = useState<string | null>(null);
const syncCountRef = useRef(0);
const startSync = useCallback(() => { syncCountRef.current++; setIsSyncing(true); }, []);
const endSync = useCallback(() => { syncCountRef.current = Math.max(0, syncCountRef.current - 1); if (syncCountRef.current === 0) setIsSyncing(false); }, []);
@ -934,13 +936,23 @@ export default function WeeklyView() {
// Optimistically add/update from API response, then force refresh cache
const data = await res.json();
if (data.event) {
// Transform API shape (start.dateTime/end.dateTime) to frontend shape (startTime/endTime)
const ev = data.event;
const frontendEvent: CalendarEvent = {
id: ev.id,
title: ev.title,
startTime: ev.start?.dateTime || ev.start?.date || ev.startTime || '',
endTime: ev.end?.dateTime || ev.end?.date || ev.endTime || '',
source: ev.source,
calendarId: ev.calendarId,
calendarTitle: ev.calendarTitle,
calendarColor: ev.backgroundColor || ev.calendarColor,
};
setRawCalendarEvents(prev => {
if (eventData.id) {
// Update existing
return prev.map(e => e.id === eventData.id ? data.event : e);
return prev.map(e => e.id === eventData.id ? frontendEvent : e);
}
// Add new
return [...prev, data.event];
return [...prev, frontendEvent];
});
}
// Also force-refresh from provider to ensure full sync
@ -1072,7 +1084,9 @@ export default function WeeklyView() {
}
}
} catch (e) {
// Silent fail for background sync
console.error("[SYNC] Task sync error:", e);
setSyncError("Task sync failed");
setTimeout(() => setSyncError(null), 10000);
}
},
2 * 60 * 1000,
@ -1108,7 +1122,9 @@ export default function WeeklyView() {
}
}
} catch (e) {
// Silent fail for background sync
console.error("[SYNC] Calendar sync error:", e);
setSyncError("Calendar sync failed");
setTimeout(() => setSyncError(null), 10000);
}
},
2 * 60 * 1000,
@ -3085,6 +3101,8 @@ export default function WeeklyView() {
} catch (error) {
console.error("Error syncing:", error);
setSyncStatus("idle");
setSyncError("Sync failed");
setTimeout(() => setSyncError(null), 10000);
}
};
@ -3489,7 +3507,12 @@ export default function WeeklyView() {
<div className="flex items-center justify-center gap-6 absolute left-1/2 transform -translate-x-1/2 group">
{/* Week & Year */}
<div className="whitespace-nowrap flex items-center gap-2">
{(isLoading || isSyncing || syncStatus === "syncing") ? (
{syncError ? (
<div className="flex items-center gap-1 text-red-500" title={syncError}>
<AlertCircle size={14} />
<span className="text-xs">{syncError}</span>
</div>
) : (isLoading || isSyncing || syncStatus === "syncing") ? (
<div className="weekly-spinner" title="Syncing..."></div>
) : (
<button
@ -7866,31 +7889,70 @@ function SettingsSidebar({
{conn.calendars &&
Array.isArray(conn.calendars) &&
conn.calendars.length > 0 ? (
<ul
style={{ paddingLeft: "24px", listStyle: "none" }}
<div style={{ paddingLeft: "8px" }}>
{/* Column Headers */}
<div
style={{
display: "flex",
alignItems: "center",
gap: "8px",
marginBottom: "6px",
paddingBottom: "4px",
borderBottom: "1px solid var(--weekly-border, #eee)",
}}
>
<span style={{ flex: 1, fontSize: "0.75rem", color: "var(--weekly-text-light)", fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.5px" }}>
Calendar
</span>
<span style={{ width: "50px", textAlign: "center", fontSize: "0.75rem", color: "var(--weekly-text-light)", fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.5px" }}>
Display
</span>
<span style={{ width: "50px", textAlign: "center", fontSize: "0.75rem", color: "var(--weekly-text-light)", fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.5px" }}>
Edit
</span>
</div>
{/* Calendar Rows */}
{conn.calendars.map((cal: any) => {
const isShared = /⚠/.test(cal.title);
const cleanTitle = cal.title
.replace(/\s*⚠️?\s*/g, "")
.trim();
return (
<li
<div
key={cal.id}
style={{
display: "flex",
alignItems: "center",
gap: "16px",
marginBottom: "8px",
}}
>
<div
style={{
display: "flex",
alignItems: "center",
gap: "8px",
padding: "3px 0",
}}
>
{/* Calendar Name */}
<span
style={{
flex: 1,
fontSize: "0.9rem",
color: "var(--weekly-text)",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
>
{cleanTitle}
{isShared && (
<span title="Shared calendar" style={{ marginLeft: "4px", fontSize: "0.75rem", opacity: 0.5 }}>
🔗
</span>
)}
{cal.isPrimary && (
<span style={{ fontSize: "0.8em", color: "var(--weekly-text-light)", marginLeft: "4px" }}>
(Primary)
</span>
)}
</span>
{/* Display checkbox */}
<span style={{ width: "50px", textAlign: "center" }}>
<input
type="checkbox"
checked={cal.selected !== false}
@ -7901,51 +7963,10 @@ function SettingsSidebar({
}
style={{ cursor: "pointer" }}
/>
<span
style={{
fontSize: "0.9rem",
color: "var(--weekly-text)",
}}
>
{cleanTitle}
<span style={{ fontSize: '0.85em', color: 'var(--weekly-text-light)', marginLeft: '4px' }}>
({conn.provider === 'google' ? 'Google Calendar' : conn.provider === 'apple' ? 'Apple Calendar' : 'Outlook Calendar'})
</span>
{isShared && (
<span
title="Shared calendar"
style={{
marginLeft: "5px",
fontSize: "0.75rem",
opacity: 0.5,
}}
>
🔗
</span>
)}
{cal.isPrimary && (
<span
style={{
fontSize: "0.8em",
color: "var(--weekly-text-light)",
marginLeft: "4px",
}}
>
(Primary)
</span>
)}
</span>
</div>
{/* Editable toggle */}
<div
style={{
display: "flex",
alignItems: "center",
gap: "4px",
opacity: 0.8,
}}
>
{/* Edit checkbox */}
<span style={{ width: "50px", textAlign: "center" }}>
<input
type="checkbox"
checked={cal.editable === true}
@ -7954,27 +7975,14 @@ function SettingsSidebar({
editable: e.target.checked,
})
}
style={{
cursor: "pointer",
width: "12px",
height: "12px",
}}
style={{ cursor: "pointer" }}
title="Allow adding/editing events"
/>
<span
style={{
fontSize: "0.8rem",
color: "#888",
}}
title="Allow adding/editing events"
>
Editable
</span>
</div>
</li>
);
})}
</ul>
</div>
) : (
<div
style={{
@ -8095,16 +8103,29 @@ function SettingsSidebar({
)}
</div>
<ul
<div
style={{
listStyle: "none",
padding: 0,
margin: 0,
display: "flex",
flexDirection: "column",
gap: "4px",
}}
>
{/* Column header */}
{providerLists.length > 0 && (
<div style={{
display: "flex",
alignItems: "center",
padding: "2px 8px",
fontSize: "0.75rem",
fontWeight: 600,
color: "var(--weekly-text-light, #888)",
textTransform: "uppercase",
letterSpacing: "0.05em",
}}>
<span style={{ flex: 1 }}>List</span>
<span style={{ width: "50px", textAlign: "center" }}>Sync</span>
</div>
)}
{providerLists.map((list: { id: string; title: string }) => {
const isSynced = somedayLists.some(
(sl: SomedayList) =>
@ -8112,17 +8133,20 @@ function SettingsSidebar({
sl.externalProvider === conn.provider,
);
return (
<li
<div
key={list.id}
style={{
display: "flex",
alignItems: "center",
gap: "10px",
padding: "4px 8px",
borderRadius: "4px",
background: "rgba(0,0,0,0.02)",
}}
>
<span style={{ flex: 1, fontSize: "0.9rem" }}>
{list.title}
</span>
<span style={{ width: "50px", textAlign: "center" }}>
<input
type="checkbox"
checked={isSynced}
@ -8136,10 +8160,8 @@ function SettingsSidebar({
}
disabled={importingTasksState}
/>
<span style={{ fontSize: "0.9rem" }}>
{list.title}
</span>
</li>
</div>
);
})}
{!isFetching && providerLists.length === 0 && (
@ -8153,7 +8175,7 @@ function SettingsSidebar({
No task lists found.
</div>
)}
</ul>
</div>
</div>
);
})}

View File

@ -244,6 +244,8 @@ export const createEvent = async (
title: string;
description?: string;
location?: string;
url?: string;
recurrence?: string;
start: { dateTime?: string; date?: string };
end: { dateTime?: string; date?: string };
}
@ -293,6 +295,20 @@ export const createEvent = async (
const description = eventData.description ? `DESCRIPTION:${eventData.description.replace(/\n/g, '\\n')}\r\n` : '';
const location = eventData.location ? `LOCATION:${eventData.location.replace(/,/g, '\\,')}\r\n` : '';
const url = eventData.url ? `URL:${eventData.url}\r\n` : '';
let rruleLine = '';
if (eventData.recurrence) {
const rruleMap: Record<string, string> = {
daily: 'RRULE:FREQ=DAILY',
weekly: 'RRULE:FREQ=WEEKLY',
biweekly: 'RRULE:FREQ=WEEKLY;INTERVAL=2',
monthly: 'RRULE:FREQ=MONTHLY',
yearly: 'RRULE:FREQ=YEARLY',
};
if (rruleMap[eventData.recurrence]) {
rruleLine = `${rruleMap[eventData.recurrence]}\r\n`;
}
}
const iCalString = `BEGIN:VCALENDAR
VERSION:2.0
@ -303,7 +319,7 @@ DTSTAMP:${dtStamp}
DTSTART${dtStartParam}:${dtStart}
DTEND${dtEndParam}:${dtEnd}
SUMMARY:${eventData.title}
${description}${location}END:VEVENT
${description}${location}${url}${rruleLine}END:VEVENT
END:VCALENDAR`;
console.log('[APPLE CALENDAR] Creating event with iCal:', iCalString);

View File

@ -4,7 +4,7 @@
import { prisma } from './prisma';
import { getCalendarEvents, CalendarEvent, CalendarConnection } from './calendar-events';
const STALE_THRESHOLD_MS = 15 * 60 * 1000; // 15 minutes
const STALE_THRESHOLD_MS = 2 * 60 * 1000; // 2 minutes — match background sync interval
/**
* Get the Monday (start of ISO week) for a given date.

View File

@ -19,6 +19,8 @@ export interface CalendarEvent {
date?: string;
};
location?: string;
url?: string;
recurrence?: string;
source: 'google' | 'apple' | 'outlook';
calendarId: string;
calendarTitle: string;
@ -26,6 +28,45 @@ export interface CalendarEvent {
allDay?: boolean;
}
/**
* Convert friendly recurrence name to RRULE string
*/
function toRRule(recurrence?: string): string | null {
switch (recurrence) {
case 'daily': return 'RRULE:FREQ=DAILY';
case 'weekly': return 'RRULE:FREQ=WEEKLY';
case 'biweekly': return 'RRULE:FREQ=WEEKLY;INTERVAL=2';
case 'monthly': return 'RRULE:FREQ=MONTHLY';
case 'yearly': return 'RRULE:FREQ=YEARLY';
default: return null;
}
}
/**
* Convert friendly recurrence name to Outlook Graph recurrence object
*/
function toOutlookRecurrence(recurrence?: string, startDate?: Date): any {
if (!recurrence || recurrence === 'none') return undefined;
const start = startDate || new Date();
const range = {
type: 'noEnd',
startDate: start.toISOString().split('T')[0],
};
switch (recurrence) {
case 'daily':
return { pattern: { type: 'daily', interval: 1 }, range };
case 'weekly':
return { pattern: { type: 'weekly', interval: 1, daysOfWeek: [['sunday','monday','tuesday','wednesday','thursday','friday','saturday'][start.getDay()]] }, range };
case 'biweekly':
return { pattern: { type: 'weekly', interval: 2, daysOfWeek: [['sunday','monday','tuesday','wednesday','thursday','friday','saturday'][start.getDay()]] }, range };
case 'monthly':
return { pattern: { type: 'absoluteMonthly', interval: 1, dayOfMonth: start.getDate() }, range };
case 'yearly':
return { pattern: { type: 'absoluteYearly', interval: 1, dayOfMonth: start.getDate(), month: start.getMonth() + 1 }, range };
default: return undefined;
}
}
// Google Calendar event color mapping (colorId -> hex color)
const GOOGLE_EVENT_COLORS: Record<string, string> = {
'1': '#7986cb', // Lavender
@ -510,12 +551,15 @@ export const createCalendarEvent = async (
);
// Map to Google format
const rrule = toRRule(event.recurrence);
const googleEvent: any = {
summary: event.title,
description: event.description,
start: event.start,
end: event.end,
location: event.location,
...(rrule ? { recurrence: [rrule] } : {}),
...(event.url ? { source: { url: event.url, title: event.url } } : {}),
};
const createdEvent = await import('./google-calendar').then(m =>
@ -544,13 +588,15 @@ export const createCalendarEvent = async (
else throw new Error('Failed to refresh token');
}
const startDate = event.start?.dateTime ? new Date(event.start.dateTime) : new Date();
const createdEvent = await createOutlookEvent(accessToken, calendarId, {
summary: event.title,
description: event.description,
start: event.start,
end: event.end,
location: event.location,
allDay: event.allDay
allDay: event.allDay,
recurrence: toOutlookRecurrence(event.recurrence, startDate),
});
return {
@ -581,6 +627,8 @@ export const createCalendarEvent = async (
title: event.title!,
description: event.description,
location: event.location,
url: event.url,
recurrence: event.recurrence,
start: event.start!,
end: event.end!
})
@ -627,12 +675,15 @@ export const updateCalendarEvent = async (
);
// Map to Google format
const rrule = toRRule(event.recurrence);
const googleEvent: any = {};
if (event.title !== undefined) googleEvent.summary = event.title;
if (event.description !== undefined) googleEvent.description = event.description;
if (event.start !== undefined) googleEvent.start = event.start;
if (event.end !== undefined) googleEvent.end = event.end;
if (event.location !== undefined) googleEvent.location = event.location;
if (rrule) googleEvent.recurrence = [rrule];
if (event.url) googleEvent.source = { url: event.url, title: event.url };
const updatedEvent = await import('./google-calendar').then(m =>
m.updateEvent(oauth2Client, accessToken, calendarId, eventId, googleEvent)
@ -657,13 +708,15 @@ export const updateCalendarEvent = async (
else throw new Error('Failed to refresh token');
}
const startDate = event.start?.dateTime ? new Date(event.start.dateTime) : new Date();
const updatedEvent = await updateOutlookEvent(accessToken, calendarId, eventId, {
summary: event.title,
description: event.description,
start: event.start,
end: event.end,
location: event.location,
allDay: event.allDay
allDay: event.allDay,
recurrence: toOutlookRecurrence(event.recurrence, startDate),
});
return {

View File

@ -118,15 +118,18 @@ export const createEvent = async (
oauth2Client.setCredentials({ access_token: accessToken });
try {
const calendar = google.calendar({ version: 'v3', auth: oauth2Client });
const response = await calendar.events.insert({
calendarId,
requestBody: {
const requestBody: any = {
summary: event.summary,
description: event.description,
start: event.start,
end: event.end,
location: event.location,
},
};
if ((event as any).recurrence) requestBody.recurrence = (event as any).recurrence;
if ((event as any).source) requestBody.source = (event as any).source;
const response = await calendar.events.insert({
calendarId,
requestBody,
});
return response.data as any;
} catch (error) {
@ -148,16 +151,19 @@ export const updateEvent = async (
oauth2Client.setCredentials({ access_token: accessToken });
try {
const calendar = google.calendar({ version: 'v3', auth: oauth2Client });
const response = await calendar.events.patch({
calendarId,
eventId,
requestBody: {
const requestBody: any = {
summary: event.summary,
description: event.description,
start: event.start,
end: event.end,
location: event.location,
},
};
if ((event as any).recurrence) requestBody.recurrence = (event as any).recurrence;
if ((event as any).source) requestBody.source = (event as any).source;
const response = await calendar.events.patch({
calendarId,
eventId,
requestBody,
});
return response.data as any;
} catch (error) {

View File

@ -180,6 +180,40 @@ export const fetchGoogleTasksForSync = async (client: OAuth2Client, taskListId:
}
};
/**
* Create a new task in a task list
*/
export const createGoogleTask = async (
client: OAuth2Client,
taskListId: string,
taskData: { title: string; notes?: string; due?: string }
): Promise<GoogleTask> => {
const service = google.tasks({ version: 'v1', auth: client });
try {
const requestBody: any = { title: taskData.title };
if (taskData.notes) requestBody.notes = taskData.notes;
if (taskData.due) requestBody.due = taskData.due;
const response = await service.tasks.insert({
tasklist: taskListId,
requestBody,
});
const item = response.data;
return {
id: item.id!,
title: item.title!,
notes: item.notes || undefined,
status: item.status!,
due: item.due || undefined,
updated: item.updated!,
parent: (item as any).parent || undefined,
};
} catch (error) {
console.error('Error creating Google Task:', error);
throw error;
}
};
export const updateGoogleTaskStatus = async (client: OAuth2Client, taskListId: string, taskId: string, status: 'needsAction' | 'completed'): Promise<GoogleTask> => {
const service = google.tasks({ version: 'v1', auth: client });
try {

View File

@ -218,7 +218,8 @@ export const createEvent = async (
isAllDay: !!event.allDay,
location: {
displayName: event.location || ''
}
},
...(event.recurrence ? { recurrence: event.recurrence } : {}),
})
});
@ -265,7 +266,8 @@ export const updateEvent = async (
isAllDay: event.allDay !== undefined ? !!event.allDay : undefined,
location: {
displayName: event.location || ''
}
},
...(event.recurrence ? { recurrence: event.recurrence } : {}),
})
});

File diff suppressed because one or more lines are too long