← 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
}