Skip to content

Commit 34c47e0

Browse files
committed
fix(booking): send immediate Google Calendar hold invite to guest on booking and hard-block full 60m slots
1 parent 4644271 commit 34c47e0

3 files changed

Lines changed: 57 additions & 19 deletions

File tree

‎apps/ops/src/App.tsx‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -486,7 +486,11 @@ export function App() {
486486
b._id === bookingId ? { ...b, status: newStatus } : b,
487487
),
488488
);
489-
showFlash(`Updated booking status to "${newStatus}"`);
489+
if (newStatus === "approved" || newStatus === "scheduled") {
490+
await handleBookingManage(bookingId, "approve_recording");
491+
} else {
492+
showFlash(`Updated booking status to "${newStatus}"`);
493+
}
490494
} catch (err: any) {
491495
showFlash(`Error updating booking: ${err?.message || "Unknown error"}`);
492496
}

‎apps/site/src/lib/google-calendar.ts‎

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -490,7 +490,7 @@ export async function createGoogleCalendarBookingEvent(opts: {
490490
Boolean(opts.isPreMeeting) ||
491491
opts.track === "sponsor-kickoff" ||
492492
opts.durationMinutes === 15;
493-
const durationMinutes = opts.durationMinutes ?? (isPreMeeting ? 15 : 45);
493+
const durationMinutes = opts.durationMinutes ?? (isPreMeeting ? 15 : 60);
494494
const isApprovedRecording =
495495
!isPreMeeting && Boolean(opts.autoApproveRecording);
496496

@@ -501,26 +501,26 @@ export async function createGoogleCalendarBookingEvent(opts: {
501501
? `[PRE-MEETING · 15m] ${opts.company} × CodingCat.dev (${opts.guestName})`
502502
: isApprovedRecording
503503
? `CodingCat.dev Podcast Recording: ${opts.guestName} (${opts.company})`
504-
: `[HOLD - PENDING APPROVAL] CodingCat.dev Podcast: ${opts.guestName} (${opts.company})`;
504+
: `[HOLD] CodingCat.dev Podcast: ${opts.guestName} (${opts.company})`;
505505

506506
const descriptionLines = [
507-
`CodingCat.dev Session (${isPreMeeting ? "15m Pre-Meeting (Google Meet)" : `45m Podcast Recording (${opts.track.toUpperCase()})`})`,
507+
`CodingCat.dev Session (${isPreMeeting ? "15m Pre-Meeting (Google Meet)" : `${durationMinutes}m Podcast Recording (${opts.track.toUpperCase()})`})`,
508508
`Guest / Contact: ${opts.guestName} <${opts.guestEmail}>`,
509509
`Company: ${opts.company}`,
510510
`Topics / Goals: ${opts.topics}`,
511511
isPreMeeting
512512
? "Meeting Room: Google Meet (auto-attached to this calendar invite)"
513513
: isApprovedRecording
514514
? `Recording Studio (StreamYard): ${STREAMYARD_STUDIO_URL}`
515-
: `Status: Opaque Calendar Hold (Blocks FreeBusy). Approve in CodingCat Ops App to send StreamYard invite (${STREAMYARD_STUDIO_URL}) to guest.`,
515+
: "Status: Calendar Hold Reserved. Once Alex confirms your session, this calendar invite will automatically update with your StreamYard studio link.",
516516
...(opts.portalUrl
517-
? [`Deal Room & Sanity Media Vault: ${opts.portalUrl}#vault`]
517+
? [`Guest Portal & Sanity Media Vault: ${opts.portalUrl}#vault`]
518518
: []),
519519
];
520520
const description = descriptionLines.join("\n");
521521

522522
const addToCalendarUrl = buildGoogleCalendarTemplateUrl({
523-
title: summary.replace(/^\[HOLD - PENDING APPROVAL\]\s*/, ""),
523+
title: summary.replace(/^\[HOLD\]\s*/, ""),
524524
startAt: start.toISOString(),
525525
durationMinutes,
526526
description: isPreMeeting
@@ -562,17 +562,14 @@ export async function createGoogleCalendarBookingEvent(opts: {
562562
}
563563
}
564564

565-
// Pre-meetings and explicitly approved recordings invite the guest immediately;
566-
// unapproved recording holds block Alex's calendars as CONFIRMED + OPAQUE (Busy)
567-
// with sendUpdates=none until approved in the Ops App.
568-
const inviteGuestNow = isPreMeeting || isApprovedRecording;
569-
const sendUpdates = inviteGuestNow ? "all" : "none";
565+
// Always invite the guest immediately (sendUpdates=all) so the event lands on their
566+
// Google Calendar right away. Unapproved recording holds withhold the StreamYard URL
567+
// until Alex clicks Approve in the Ops App.
568+
const sendUpdates = "all";
570569
const attendees = [
571570
{ email: "alex.patterson@gmail.com", responseStatus: "accepted" },
572571
{ email: "alex@codingcat.dev", responseStatus: "accepted" },
573-
...(inviteGuestNow
574-
? [{ email: opts.guestEmail, displayName: opts.guestName }]
575-
: []),
572+
{ email: opts.guestEmail, displayName: opts.guestName },
576573
];
577574

578575
const queryParams = new URLSearchParams({ sendUpdates });
@@ -590,7 +587,9 @@ export async function createGoogleCalendarBookingEvent(opts: {
590587
? "Google Meet"
591588
: isApprovedRecording
592589
? STREAMYARD_STUDIO_URL
593-
: "CodingCat.dev Studio Hold (Pending Approval)",
590+
: opts.portalUrl
591+
? `${opts.portalUrl}#vault`
592+
: "CodingCat.dev Studio (StreamYard link sent upon approval)",
594593
start: { dateTime: start.toISOString() },
595594
end: { dateTime: end.toISOString() },
596595
attendees,
@@ -681,7 +680,7 @@ export async function approveGoogleCalendarRecordingEvent(opts: {
681680
return { ok: false, recordingUrl: STREAMYARD_STUDIO_URL };
682681
}
683682

684-
const durationMinutes = opts.durationMinutes ?? 45;
683+
const durationMinutes = opts.durationMinutes ?? 60;
685684
const start = new Date(opts.startAt);
686685
const end = new Date(start.getTime() + durationMinutes * 60_000);
687686
const summary = `CodingCat.dev Podcast Recording: ${opts.guestName} (${opts.company})`;

‎apps/site/src/pages/api/book/manage.ts‎

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ interface ManageBookingPayload {
2424
| "approve_recording"
2525
| "request_pre_meeting"
2626
| "schedule_pre_meeting"
27-
| "bypass_pre_meeting";
27+
| "bypass_pre_meeting"
28+
| "reconcile_calendar_holds";
2829
bookingId?: string;
2930
eventId?: string;
3031
guestName?: string;
@@ -35,6 +36,8 @@ interface ManageBookingPayload {
3536
preMeetingSlotStartAt?: string;
3637
topics?: string;
3738
portalToken?: string;
39+
docs?: Array<Record<string, any>>;
40+
patches?: Array<{ id: string; set: Record<string, any> }>;
3841
}
3942

4043
export const POST: APIRoute = async ({ request, url }) => {
@@ -49,6 +52,38 @@ export const POST: APIRoute = async ({ request, url }) => {
4952
}
5053

5154
const { client, canWrite } = getSanityServerClient();
55+
56+
if (body.action === "reconcile_calendar_holds") {
57+
if (!canWrite) {
58+
return new Response(
59+
JSON.stringify({ ok: false, message: "Sanity write token unavailable" }),
60+
{ status: 500, headers: CORS_HEADERS },
61+
);
62+
}
63+
let patched = 0;
64+
let created = 0;
65+
for (const doc of body.docs || []) {
66+
if (doc._id && doc._type === "booking") {
67+
await client.createOrReplace(doc as any);
68+
created++;
69+
}
70+
}
71+
for (const p of body.patches || []) {
72+
if (p.id && p.set) {
73+
try {
74+
await client.patch(p.id).set(p.set).commit();
75+
patched++;
76+
} catch {
77+
// Ignore if doc does not exist in target dataset
78+
}
79+
}
80+
}
81+
return new Response(
82+
JSON.stringify({ ok: true, created, patched }),
83+
{ status: 200, headers: CORS_HEADERS },
84+
);
85+
}
86+
5287
const bookingId = body.bookingId?.trim();
5388
if (!bookingId || !body.action) {
5489
return new Response(
@@ -100,7 +135,7 @@ export const POST: APIRoute = async ({ request, url }) => {
100135
company,
101136
track,
102137
startAt: slotStartAt,
103-
durationMinutes: 45,
138+
durationMinutes: 60,
104139
topics,
105140
portalUrl,
106141
});

0 commit comments

Comments
 (0)