← back to Ventura Corridor
src/server/appointments/calendar.ts
94 lines
/**
* Google Calendar v3 helpers — fetch only, no SDK.
*
* Two operations:
* - busy(...) freebusy.query → array of {start,end} busy windows for a date
* - insertEvent(...) events.insert → push an appointment onto the owner's primary cal
* - deleteEvent(...) events.delete → wipe on cancel
*/
import { getValidAccessToken } from './oauth.ts';
const CAL_BASE = 'https://www.googleapis.com/calendar/v3';
export interface BusyWindow { start: string; end: string; } // ISO strings (UTC)
export async function busy(
ownerEmail: string,
startISO: string,
endISO: string,
): Promise<BusyWindow[]> {
const token = await getValidAccessToken(ownerEmail);
if (!token) return [];
const r = await fetch(`${CAL_BASE}/freeBusy`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
timeMin: startISO,
timeMax: endISO,
items: [{ id: 'primary' }],
}),
});
if (!r.ok) {
console.error('[appointments] freeBusy failed', r.status, await r.text());
return [];
}
const j: any = await r.json();
return (j.calendars?.primary?.busy as BusyWindow[] | undefined) ?? [];
}
export async function insertEvent(
ownerEmail: string,
payload: {
summary: string;
description: string;
startISO: string;
endISO: string;
attendeeEmail?: string;
},
): Promise<string | null> {
const token = await getValidAccessToken(ownerEmail);
if (!token) return null;
const body: any = {
summary: payload.summary,
description: payload.description,
start: { dateTime: payload.startISO },
end: { dateTime: payload.endISO },
};
if (payload.attendeeEmail) {
body.attendees = [{ email: payload.attendeeEmail }];
}
const r = await fetch(`${CAL_BASE}/calendars/primary/events?sendUpdates=none`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
if (!r.ok) {
console.error('[appointments] events.insert failed', r.status, await r.text());
return null;
}
const j: any = await r.json();
return j.id ?? null;
}
export async function deleteEvent(
ownerEmail: string,
eventId: string,
): Promise<boolean> {
const token = await getValidAccessToken(ownerEmail);
if (!token) return false;
const r = await fetch(
`${CAL_BASE}/calendars/primary/events/${encodeURIComponent(eventId)}?sendUpdates=none`,
{
method: 'DELETE',
headers: { 'Authorization': `Bearer ${token}` },
},
);
return r.ok || r.status === 410; // 410 = already gone
}