- Fix Google recurring events missing isRecurring flag (recurringEventId was not mapped from Google API response) - Add note icon (FileText) on events with descriptions, tooltip on hover - Cross-day drag-and-drop: detect day column under cursor and shift date - Fix event detail popup opening after drag/resize by setting ref before async operations - Add data-date attribute to day columns for drag target detection - Use CSS class for icon row layout to prevent overlap v1.67.0 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
247 lines
6.8 KiB
TypeScript
247 lines
6.8 KiB
TypeScript
// Google Calendar OAuth wrapper
|
|
import { google } from 'googleapis';
|
|
|
|
export interface GoogleCalendarEvent {
|
|
id: string;
|
|
summary: string;
|
|
description?: string;
|
|
start: {
|
|
dateTime?: string;
|
|
date?: string;
|
|
};
|
|
end: {
|
|
dateTime?: string;
|
|
date?: string;
|
|
};
|
|
attendees?: Array<{
|
|
email: string;
|
|
displayName?: string;
|
|
responseStatus?: string;
|
|
}>;
|
|
location?: string;
|
|
colorId?: string;
|
|
reminders?: {
|
|
useDefault: boolean;
|
|
overrides?: Array<{ method: string; minutes: number }>;
|
|
};
|
|
transparency?: string; // 'opaque' | 'transparent'
|
|
visibility?: string; // 'default' | 'public' | 'private' | 'confidential'
|
|
recurrence?: string[];
|
|
}
|
|
|
|
export interface GoogleCalendar {
|
|
id: string;
|
|
summary: string;
|
|
primary?: boolean;
|
|
backgroundColor?: string;
|
|
}
|
|
|
|
/**
|
|
* Initialize Google Calendar OAuth client
|
|
*/
|
|
export const initializeOAuth = (clientId: string, clientSecret: string, redirectUri: string) => {
|
|
const oauth2Client = new google.auth.OAuth2(
|
|
clientId,
|
|
clientSecret,
|
|
redirectUri
|
|
);
|
|
|
|
return oauth2Client;
|
|
};
|
|
|
|
/**
|
|
* Get user calendar list using OAuth token
|
|
*/
|
|
export const getUserCalendars = async (
|
|
oauth2Client: any,
|
|
accessToken: string
|
|
): Promise<GoogleCalendar[]> => {
|
|
// Set access token
|
|
oauth2Client.setCredentials({ access_token: accessToken });
|
|
|
|
try {
|
|
const calendar = google.calendar({ version: 'v3', auth: oauth2Client });
|
|
const response = await calendar.calendarList.list();
|
|
|
|
return response.data.items?.map((item: any) => ({
|
|
id: item.id,
|
|
summary: item.summary,
|
|
primary: item.primary,
|
|
backgroundColor: item.backgroundColor,
|
|
})) || [];
|
|
} catch (error) {
|
|
console.error('Error fetching user calendars:', error);
|
|
throw new Error('Failed to fetch user calendars');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Get upcoming events for a specified time period
|
|
*/
|
|
export const getUpcomingEvents = async (
|
|
oauth2Client: any,
|
|
accessToken: string,
|
|
calendarId: string,
|
|
timeMin: string,
|
|
timeMax: string
|
|
): Promise<GoogleCalendarEvent[]> => {
|
|
// Set access token
|
|
oauth2Client.setCredentials({ access_token: accessToken });
|
|
|
|
try {
|
|
const calendar = google.calendar({ version: 'v3', auth: oauth2Client });
|
|
const response = await calendar.events.list({
|
|
calendarId,
|
|
timeMin,
|
|
timeMax,
|
|
singleEvents: true,
|
|
orderBy: 'startTime',
|
|
});
|
|
|
|
return response.data.items?.map((item: any) => ({
|
|
id: item.id,
|
|
summary: item.summary,
|
|
description: item.description,
|
|
start: item.start,
|
|
end: item.end,
|
|
attendees: item.attendees?.map((a: any) => ({
|
|
email: a.email,
|
|
displayName: a.displayName,
|
|
responseStatus: a.responseStatus,
|
|
})),
|
|
location: item.location,
|
|
colorId: item.colorId,
|
|
reminders: item.reminders,
|
|
transparency: item.transparency,
|
|
visibility: item.visibility,
|
|
recurringEventId: item.recurringEventId,
|
|
})) || [];
|
|
} catch (error) {
|
|
console.error('Error fetching upcoming events:', error);
|
|
throw new Error('Failed to fetch upcoming events');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Create a new event
|
|
*/
|
|
export const createEvent = async (
|
|
oauth2Client: any,
|
|
accessToken: string,
|
|
calendarId: string,
|
|
event: Partial<GoogleCalendarEvent>
|
|
): Promise<GoogleCalendarEvent> => {
|
|
oauth2Client.setCredentials({ access_token: accessToken });
|
|
try {
|
|
const calendar = google.calendar({ version: 'v3', auth: oauth2Client });
|
|
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;
|
|
if (event.reminders) requestBody.reminders = event.reminders;
|
|
if (event.transparency) requestBody.transparency = event.transparency;
|
|
if (event.visibility) requestBody.visibility = event.visibility;
|
|
if (event.attendees) requestBody.attendees = event.attendees;
|
|
const response = await calendar.events.insert({
|
|
calendarId,
|
|
requestBody,
|
|
});
|
|
return response.data as any;
|
|
} catch (error) {
|
|
console.error('Error creating event:', error);
|
|
throw new Error('Failed to create event');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Update an existing event
|
|
*/
|
|
export const updateEvent = async (
|
|
oauth2Client: any,
|
|
accessToken: string,
|
|
calendarId: string,
|
|
eventId: string,
|
|
event: Partial<GoogleCalendarEvent>
|
|
): Promise<GoogleCalendarEvent> => {
|
|
oauth2Client.setCredentials({ access_token: accessToken });
|
|
try {
|
|
const calendar = google.calendar({ version: 'v3', auth: oauth2Client });
|
|
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;
|
|
if (event.reminders) requestBody.reminders = event.reminders;
|
|
if (event.transparency) requestBody.transparency = event.transparency;
|
|
if (event.visibility) requestBody.visibility = event.visibility;
|
|
if (event.attendees) requestBody.attendees = event.attendees;
|
|
const response = await calendar.events.patch({
|
|
calendarId,
|
|
eventId,
|
|
requestBody,
|
|
});
|
|
return response.data as any;
|
|
} catch (error) {
|
|
console.error('Error updating event:', error);
|
|
throw new Error('Failed to update event');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Get a single event by ID
|
|
*/
|
|
export const getEvent = async (
|
|
oauth2Client: any,
|
|
accessToken: string,
|
|
calendarId: string,
|
|
eventId: string
|
|
): Promise<GoogleCalendarEvent | null> => {
|
|
oauth2Client.setCredentials({ access_token: accessToken });
|
|
try {
|
|
const calendar = google.calendar({ version: 'v3', auth: oauth2Client });
|
|
const response = await calendar.events.get({
|
|
calendarId,
|
|
eventId,
|
|
});
|
|
return response.data as any;
|
|
} catch (error) {
|
|
console.error('Error getting event:', error);
|
|
return null;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Delete an event
|
|
*/
|
|
export const deleteEvent = async (
|
|
oauth2Client: any,
|
|
accessToken: string,
|
|
calendarId: string,
|
|
eventId: string
|
|
): Promise<void> => {
|
|
oauth2Client.setCredentials({ access_token: accessToken });
|
|
try {
|
|
const calendar = google.calendar({ version: 'v3', auth: oauth2Client });
|
|
await calendar.events.delete({
|
|
calendarId,
|
|
eventId,
|
|
});
|
|
} catch (error: any) {
|
|
// 410 Gone means already deleted — treat as success
|
|
if (error?.code === 410 || error?.status === 410) {
|
|
console.log('[GOOGLE] Event already deleted (410 Gone), treating as success');
|
|
return;
|
|
}
|
|
console.error('Error deleting event:', error);
|
|
throw new Error('Failed to delete event');
|
|
}
|
|
}; |