Replace OAuth Google Calendar sync with a one-click 'Add to calendar' link — no setup needed, works for any Google account
This commit is contained in:
1 parent
7654a7b616
commit
47b003bbe3
18 files changed
+499
-487
No files matched your search
@@ -0,0 +1,53 @@
|
||||
import { formatBookingSummary } from "./format";
|
||||
import type { BookingDTO } from "./service";
|
||||
|
||||
// Russia abolished DST in 2014 — Moscow is a fixed UTC+3 year-round, so this
|
||||
// doesn't need a timezone library or the server's own TZ setting.
|
||||
const MOSCOW_UTC_OFFSET_HOURS = 3;
|
||||
|
||||
function moscowLocalToUtc(dayUtcMidnight: Date, time: string): Date {
|
||||
const [hh, mm] = time.split(":").map(Number);
|
||||
const ms = dayUtcMidnight.getTime() + ((hh || 0) - MOSCOW_UTC_OFFSET_HOURS) * 3600_000 + (mm || 0) * 60_000;
|
||||
return new Date(ms);
|
||||
}
|
||||
|
||||
function toGoogleDateParam(d: Date): string {
|
||||
return d.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
|
||||
}
|
||||
|
||||
/**
|
||||
* Google Calendar's "quick add" URL — pre-fills a new-event form that the
|
||||
* viewer saves to their own calendar with one click. No API keys, no OAuth,
|
||||
* works for anyone with a Google account. https://calendar.google.com/calendar/render?action=TEMPLATE...
|
||||
*/
|
||||
export function buildGoogleCalendarUrl(booking: BookingDTO): string {
|
||||
const start = moscowLocalToUtc(booking.date, booking.checkIn);
|
||||
let end = moscowLocalToUtc(booking.date, booking.checkOut);
|
||||
if (end <= start) end = new Date(end.getTime() + 24 * 3600_000); // overnight check-out
|
||||
|
||||
const objectNames = booking.objects.map((o) => o.name).join(", ") || "—";
|
||||
const details = formatBookingSummary({
|
||||
bookingNumber: booking.bookingNumber,
|
||||
contactName: booking.contactName,
|
||||
phone: booking.phone,
|
||||
objectNames: booking.objects.map((o) => o.name),
|
||||
date: booking.date,
|
||||
checkIn: booking.checkIn,
|
||||
checkOut: booking.checkOut,
|
||||
guestCount: booking.guestCount,
|
||||
cost: booking.cost,
|
||||
prepayment: booking.prepayment,
|
||||
comment: booking.comment,
|
||||
source: booking.source,
|
||||
bookedByName: booking.bookedByName,
|
||||
});
|
||||
|
||||
const params = new URLSearchParams({
|
||||
action: "TEMPLATE",
|
||||
text: `#${booking.bookingNumber} ${objectNames} — ${booking.contactName}`,
|
||||
dates: `${toGoogleDateParam(start)}/${toGoogleDateParam(end)}`,
|
||||
details,
|
||||
});
|
||||
|
||||
return `https://calendar.google.com/calendar/render?${params.toString()}`;
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { and, eq, gte, lt, inArray, sql } from "drizzle-orm";
|
||||
import { db } from "@/lib/db/client";
|
||||
import { bookings, bookingObjects, bookableObjects, users } from "@/lib/db/schema";
|
||||
import { createCalendarEvent, updateCalendarEvent, deleteCalendarEvent } from "@/lib/google/calendar";
|
||||
|
||||
export interface BookableObjectDTO {
|
||||
id: string;
|
||||
@@ -53,7 +52,6 @@ export interface BookingDTO {
|
||||
status: "confirmed" | "cancelled";
|
||||
objects: { id: string; name: string }[];
|
||||
bookedByName: string;
|
||||
googleEventId: string | null;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
@@ -83,7 +81,6 @@ async function toBookingDTO(row: typeof bookings.$inferSelect): Promise<BookingD
|
||||
status: row.status,
|
||||
objects: objRows,
|
||||
bookedByName: bookedBy?.name ?? "—",
|
||||
googleEventId: row.googleEventId,
|
||||
createdAt: row.createdAt,
|
||||
};
|
||||
}
|
||||
@@ -129,20 +126,7 @@ export async function createBooking(input: CreateBookingInput): Promise<BookingD
|
||||
await db.insert(bookingObjects).values(input.objectIds.map((objectId) => ({ bookingId: row.id, objectId })));
|
||||
}
|
||||
|
||||
const dto = await toBookingDTO(row);
|
||||
|
||||
// Best-effort Google Calendar sync — never blocks/fails booking creation.
|
||||
try {
|
||||
const googleEventId = await createCalendarEvent(dto);
|
||||
if (googleEventId) {
|
||||
await db.update(bookings).set({ googleEventId }).where(eq(bookings.id, row.id));
|
||||
dto.googleEventId = googleEventId;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Google Calendar sync failed for booking", row.bookingNumber, err);
|
||||
}
|
||||
|
||||
return dto;
|
||||
return toBookingDTO(row);
|
||||
}
|
||||
|
||||
export interface UpdateBookingInput {
|
||||
@@ -176,29 +160,7 @@ export async function updateBooking(id: string, input: UpdateBookingInput): Prom
|
||||
}
|
||||
const row = await db.query.bookings.findFirst({ where: eq(bookings.id, id) });
|
||||
if (!row) return null;
|
||||
const dto = await toBookingDTO(row);
|
||||
|
||||
// Best-effort Google Calendar sync — never blocks/fails the update.
|
||||
try {
|
||||
if (dto.status === "cancelled" && dto.googleEventId) {
|
||||
await deleteCalendarEvent(dto.googleEventId);
|
||||
await db.update(bookings).set({ googleEventId: null }).where(eq(bookings.id, id));
|
||||
dto.googleEventId = null;
|
||||
} else if (dto.status === "confirmed" && dto.googleEventId) {
|
||||
await updateCalendarEvent(dto);
|
||||
} else if (dto.status === "confirmed" && !dto.googleEventId) {
|
||||
// Never synced yet (created before Google was connected, or just restored from cancelled).
|
||||
const googleEventId = await createCalendarEvent(dto);
|
||||
if (googleEventId) {
|
||||
await db.update(bookings).set({ googleEventId }).where(eq(bookings.id, id));
|
||||
dto.googleEventId = googleEventId;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Google Calendar sync failed for booking", dto.bookingNumber, err);
|
||||
}
|
||||
|
||||
return dto;
|
||||
return toBookingDTO(row);
|
||||
}
|
||||
|
||||
export async function getBooking(id: string): Promise<BookingDTO | null> {
|
||||
|
||||
Reference in new issue
Block a user