Compare commits
No commits in common. "3842ad96880b15edd0b8ee6af651a497b20ec265" and "3e38c6cb1eb61346ef2ba9f0e750c18bb794a950" have entirely different histories.
3842ad9688
...
3e38c6cb1e
|
|
@ -102,5 +102,3 @@ function escapeHtml(s: string): string {
|
|||
}
|
||||
|
||||
customElements.define("folk-schedule-admin", FolkScheduleAdmin);
|
||||
|
||||
export {};
|
||||
|
|
|
|||
|
|
@ -91,5 +91,3 @@ function escapeHtml(s: string): string {
|
|||
}
|
||||
|
||||
customElements.define("folk-schedule-booking", FolkScheduleBooking);
|
||||
|
||||
export {};
|
||||
|
|
|
|||
|
|
@ -123,5 +123,3 @@ function escapeHtml(s: string): string {
|
|||
}
|
||||
|
||||
customElements.define("folk-schedule-cancel", FolkScheduleCancel);
|
||||
|
||||
export {};
|
||||
|
|
|
|||
|
|
@ -1,236 +0,0 @@
|
|||
/**
|
||||
* Availability engine — port of schedule-jeffemmett/src/lib/availability.ts.
|
||||
*
|
||||
* Pure computation: takes config doc + bookings doc + a range, returns free
|
||||
* slots. No side effects. Two callers:
|
||||
* - GET /api/availability (public booking page)
|
||||
* - Admin dashboard preview
|
||||
*
|
||||
* Deliberately avoids date-fns / date-fns-tz to keep the module tree light —
|
||||
* uses Intl.DateTimeFormat for timezone math instead.
|
||||
*/
|
||||
|
||||
import type {
|
||||
ScheduleConfigDoc,
|
||||
ScheduleBookingsDoc,
|
||||
AvailabilityRule,
|
||||
AvailabilityOverride,
|
||||
CachedGcalEvent,
|
||||
} from "../schemas";
|
||||
|
||||
export interface TimeSlot {
|
||||
/** UTC ISO string — acts as stable slot id. */
|
||||
id: string;
|
||||
startUtc: string;
|
||||
endUtc: string;
|
||||
/** YYYY-MM-DD in viewer timezone. */
|
||||
date: string;
|
||||
/** "9:00 AM" in viewer timezone. */
|
||||
startDisplay: string;
|
||||
endDisplay: string;
|
||||
startMs: number;
|
||||
endMs: number;
|
||||
}
|
||||
|
||||
export interface GetSlotsOpts {
|
||||
rangeStartMs: number;
|
||||
rangeEndMs: number;
|
||||
viewerTimezone: string;
|
||||
/** Client-selected duration override (clamped 15–120). */
|
||||
durationOverride?: number;
|
||||
}
|
||||
|
||||
interface Interval { startMs: number; endMs: number; }
|
||||
|
||||
// ── Timezone helpers (no external deps) ──
|
||||
|
||||
/**
|
||||
* Return the UTC ms offset to apply to a wall-clock instant in `tz` to get
|
||||
* the real UTC instant. Uses Intl.DateTimeFormat with `timeZoneName: 'longOffset'`.
|
||||
*/
|
||||
function tzOffsetMinutes(dateMs: number, tz: string): number {
|
||||
const dtf = new Intl.DateTimeFormat("en-US", {
|
||||
timeZone: tz,
|
||||
year: "numeric", month: "2-digit", day: "2-digit",
|
||||
hour: "2-digit", minute: "2-digit", second: "2-digit",
|
||||
hour12: false,
|
||||
});
|
||||
const parts = dtf.formatToParts(new Date(dateMs));
|
||||
const map: Record<string, string> = {};
|
||||
for (const p of parts) if (p.type !== "literal") map[p.type] = p.value;
|
||||
const asUTC = Date.UTC(
|
||||
Number(map.year),
|
||||
Number(map.month) - 1,
|
||||
Number(map.day),
|
||||
Number(map.hour === "24" ? "0" : map.hour),
|
||||
Number(map.minute),
|
||||
Number(map.second),
|
||||
);
|
||||
return Math.round((asUTC - dateMs) / 60000);
|
||||
}
|
||||
|
||||
/** Convert a wall-clock time in `tz` (y/m/d/h/m) to UTC ms. */
|
||||
function wallClockToUtcMs(y: number, m: number, d: number, h: number, min: number, tz: string): number {
|
||||
// First pass: treat as UTC, then correct by the tz offset at that moment.
|
||||
const guess = Date.UTC(y, m - 1, d, h, min, 0);
|
||||
const off = tzOffsetMinutes(guess, tz);
|
||||
return guess - off * 60000;
|
||||
}
|
||||
|
||||
/** Format a UTC ms in `tz` as YYYY-MM-DD. */
|
||||
function fmtDate(ms: number, tz: string): string {
|
||||
const dtf = new Intl.DateTimeFormat("en-CA", {
|
||||
timeZone: tz, year: "numeric", month: "2-digit", day: "2-digit",
|
||||
});
|
||||
return dtf.format(new Date(ms));
|
||||
}
|
||||
|
||||
/** Format a UTC ms in `tz` as "h:mm AM/PM". */
|
||||
function fmtTime(ms: number, tz: string): string {
|
||||
const dtf = new Intl.DateTimeFormat("en-US", {
|
||||
timeZone: tz, hour: "numeric", minute: "2-digit", hour12: true,
|
||||
});
|
||||
return dtf.format(new Date(ms));
|
||||
}
|
||||
|
||||
/** Day-of-week in `tz` (0=Sun..6=Sat). */
|
||||
function dayOfWeekInTz(ms: number, tz: string): number {
|
||||
const dtf = new Intl.DateTimeFormat("en-US", { timeZone: tz, weekday: "short" });
|
||||
const short = dtf.format(new Date(ms));
|
||||
return ({ Sun: 0, Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6 } as Record<string, number>)[short] ?? 0;
|
||||
}
|
||||
|
||||
function parseHHMM(s: string): [number, number] {
|
||||
const [h, m] = s.split(":").map((n) => Number(n));
|
||||
return [h || 0, m || 0];
|
||||
}
|
||||
|
||||
// ── Main engine ──
|
||||
|
||||
export function getAvailableSlots(
|
||||
config: ScheduleConfigDoc,
|
||||
bookings: ScheduleBookingsDoc,
|
||||
opts: GetSlotsOpts,
|
||||
): TimeSlot[] {
|
||||
const { settings, rules: rulesMap, overrides: overridesMap, googleEvents } = config;
|
||||
const hostTz = settings.timezone || "UTC";
|
||||
const slotMin = opts.durationOverride
|
||||
? Math.max(15, Math.min(120, opts.durationOverride))
|
||||
: settings.slotDurationMin;
|
||||
|
||||
const now = Date.now();
|
||||
const earliest = now + settings.minNoticeHours * 3600_000;
|
||||
const latest = now + settings.maxAdvanceDays * 86400_000;
|
||||
|
||||
const startMs = Math.max(opts.rangeStartMs, earliest);
|
||||
const endMs = Math.min(opts.rangeEndMs, latest);
|
||||
if (startMs >= endMs) return [];
|
||||
|
||||
const rules = Object.values(rulesMap).filter((r) => r.isActive);
|
||||
const overrides = Object.values(overridesMap);
|
||||
const gcalBusy: CachedGcalEvent[] = Object.values(googleEvents).filter(
|
||||
(e) => e.transparency === "opaque" && e.status !== "cancelled",
|
||||
);
|
||||
|
||||
// Build busy intervals from confirmed bookings + gcal cache
|
||||
const busy: Interval[] = [];
|
||||
for (const b of Object.values(bookings.bookings)) {
|
||||
if (b.status !== "confirmed") continue;
|
||||
if (b.endTime < startMs || b.startTime > endMs) continue;
|
||||
busy.push({
|
||||
startMs: b.startTime - settings.bufferBeforeMin * 60000,
|
||||
endMs: b.endTime + settings.bufferAfterMin * 60000,
|
||||
});
|
||||
}
|
||||
for (const g of gcalBusy) {
|
||||
if (g.endTime < startMs || g.startTime > endMs) continue;
|
||||
busy.push({ startMs: g.startTime, endMs: g.endTime });
|
||||
}
|
||||
|
||||
const slots: TimeSlot[] = [];
|
||||
|
||||
// Walk day-by-day in host timezone
|
||||
const msPerDay = 86400_000;
|
||||
// Start at midnight in host tz of the effective range start
|
||||
const startDate = fmtDate(startMs, hostTz);
|
||||
const endDate = fmtDate(endMs, hostTz);
|
||||
|
||||
let cursor = parseDate(startDate);
|
||||
const end = parseDate(endDate);
|
||||
|
||||
while (cursor.getTime() <= end.getTime()) {
|
||||
const dateStr = toDateStr(cursor);
|
||||
const override = overrides.find((o) => o.date === dateStr);
|
||||
if (override?.isBlocked) {
|
||||
cursor = addDays(cursor, 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Windows for this day
|
||||
type Window = { startHHMM: string; endHHMM: string };
|
||||
let windows: Window[] = [];
|
||||
if (override && override.startTime && override.endTime) {
|
||||
windows = [{ startHHMM: override.startTime, endHHMM: override.endTime }];
|
||||
} else if (!override) {
|
||||
const dow = dayOfWeekInTz(
|
||||
wallClockToUtcMs(cursor.getUTCFullYear(), cursor.getUTCMonth() + 1, cursor.getUTCDate(), 12, 0, hostTz),
|
||||
hostTz,
|
||||
);
|
||||
windows = rules
|
||||
.filter((r: AvailabilityRule) => r.dayOfWeek === dow)
|
||||
.map((r) => ({ startHHMM: r.startTime, endHHMM: r.endTime }));
|
||||
}
|
||||
|
||||
for (const w of windows) {
|
||||
const [sh, sm] = parseHHMM(w.startHHMM);
|
||||
const [eh, em] = parseHHMM(w.endHHMM);
|
||||
const wStart = wallClockToUtcMs(
|
||||
cursor.getUTCFullYear(), cursor.getUTCMonth() + 1, cursor.getUTCDate(), sh, sm, hostTz,
|
||||
);
|
||||
const wEnd = wallClockToUtcMs(
|
||||
cursor.getUTCFullYear(), cursor.getUTCMonth() + 1, cursor.getUTCDate(), eh, em, hostTz,
|
||||
);
|
||||
|
||||
let t = wStart;
|
||||
while (t + slotMin * 60000 <= wEnd) {
|
||||
const slotEnd = t + slotMin * 60000;
|
||||
if (t < earliest) { t += slotMin * 60000; continue; }
|
||||
if (t > latest) break;
|
||||
|
||||
const conflict = busy.some((b) => t < b.endMs && slotEnd > b.startMs);
|
||||
if (!conflict) {
|
||||
slots.push({
|
||||
id: new Date(t).toISOString(),
|
||||
startUtc: new Date(t).toISOString(),
|
||||
endUtc: new Date(slotEnd).toISOString(),
|
||||
date: fmtDate(t, opts.viewerTimezone),
|
||||
startDisplay: fmtTime(t, opts.viewerTimezone),
|
||||
endDisplay: fmtTime(slotEnd, opts.viewerTimezone),
|
||||
startMs: t,
|
||||
endMs: slotEnd,
|
||||
});
|
||||
}
|
||||
t += slotMin * 60000;
|
||||
}
|
||||
}
|
||||
|
||||
cursor = addDays(cursor, 1);
|
||||
}
|
||||
|
||||
return slots;
|
||||
|
||||
// ── local helpers ──
|
||||
function parseDate(s: string): Date {
|
||||
const [y, m, d] = s.split("-").map(Number);
|
||||
return new Date(Date.UTC(y, m - 1, d));
|
||||
}
|
||||
function toDateStr(d: Date): string {
|
||||
const y = d.getUTCFullYear();
|
||||
const m = String(d.getUTCMonth() + 1).padStart(2, "0");
|
||||
const day = String(d.getUTCDate()).padStart(2, "0");
|
||||
return `${y}-${m}-${day}`;
|
||||
}
|
||||
function addDays(d: Date, n: number): Date {
|
||||
return new Date(d.getTime() + n * msPerDay);
|
||||
}
|
||||
}
|
||||
|
|
@ -40,7 +40,6 @@ import {
|
|||
type Invitation,
|
||||
type EntityRef,
|
||||
} from "./schemas";
|
||||
import { getAvailableSlots } from "./lib/availability";
|
||||
|
||||
let _syncServer: SyncServer | null = null;
|
||||
|
||||
|
|
@ -105,25 +104,22 @@ async function requireAdmin(c: any): Promise<{ ok: true; did: string } | { ok: f
|
|||
let claims;
|
||||
try { claims = await verifyToken(token); }
|
||||
catch { return { ok: false, status: 401, error: "Invalid token" }; }
|
||||
const space = String(c.req.param("space") || "");
|
||||
const resolved = await resolveCallerRole(space, claims);
|
||||
if (!resolved || !roleAtLeast(resolved.role, "moderator")) {
|
||||
return { ok: false, status: 403, error: "Moderator role required" };
|
||||
}
|
||||
return { ok: true, did: String(claims.did || claims.sub || "") };
|
||||
const space = c.req.param("space");
|
||||
const role = await resolveCallerRole(space, claims);
|
||||
if (!roleAtLeast(role, "moderator")) return { ok: false, status: 403, error: "Moderator role required" };
|
||||
return { ok: true, did: claims.did };
|
||||
}
|
||||
|
||||
// ── Public routes ──
|
||||
|
||||
routes.get("/", (c) => {
|
||||
const space = String(c.req.param("space") || "demo");
|
||||
const space = c.req.param("space");
|
||||
return c.html(
|
||||
renderShell({
|
||||
title: `${space} — Book a time | rSpace`,
|
||||
moduleId: "rschedule",
|
||||
spaceSlug: space,
|
||||
modules: getModuleInfoList(),
|
||||
theme: "dark",
|
||||
space,
|
||||
enabledModules: getModuleInfoList().map((m) => m.id),
|
||||
scripts: `<script type="module" src="/modules/rschedule/folk-schedule-booking.js?v=1"></script>`,
|
||||
styles: `<link rel="stylesheet" href="/modules/rschedule/booking.css">`,
|
||||
body: `<folk-schedule-booking space="${space}"></folk-schedule-booking>`,
|
||||
|
|
@ -132,15 +128,14 @@ routes.get("/", (c) => {
|
|||
});
|
||||
|
||||
routes.get("/cancel/:id", (c) => {
|
||||
const space = String(c.req.param("space") || "demo");
|
||||
const id = String(c.req.param("id") || "");
|
||||
const space = c.req.param("space");
|
||||
const id = c.req.param("id");
|
||||
return c.html(
|
||||
renderShell({
|
||||
title: `Cancel booking | rSpace`,
|
||||
moduleId: "rschedule",
|
||||
spaceSlug: space,
|
||||
modules: getModuleInfoList(),
|
||||
theme: "dark",
|
||||
space,
|
||||
enabledModules: getModuleInfoList().map((m) => m.id),
|
||||
scripts: `<script type="module" src="/modules/rschedule/folk-schedule-cancel.js?v=1"></script>`,
|
||||
body: `<folk-schedule-cancel space="${space}" booking-id="${id}"></folk-schedule-cancel>`,
|
||||
}),
|
||||
|
|
@ -148,14 +143,13 @@ routes.get("/cancel/:id", (c) => {
|
|||
});
|
||||
|
||||
routes.get("/admin", (c) => {
|
||||
const space = String(c.req.param("space") || "demo");
|
||||
const space = c.req.param("space");
|
||||
return c.html(
|
||||
renderShell({
|
||||
title: `${space} — Schedule admin | rSpace`,
|
||||
moduleId: "rschedule",
|
||||
spaceSlug: space,
|
||||
modules: getModuleInfoList(),
|
||||
theme: "dark",
|
||||
space,
|
||||
enabledModules: getModuleInfoList().map((m) => m.id),
|
||||
scripts: `<script type="module" src="/modules/rschedule/folk-schedule-admin.js?v=1"></script>`,
|
||||
body: `<folk-schedule-admin space="${space}"></folk-schedule-admin>`,
|
||||
}),
|
||||
|
|
@ -165,7 +159,7 @@ routes.get("/admin", (c) => {
|
|||
// ── Public API ──
|
||||
|
||||
routes.get("/api/settings/public", (c) => {
|
||||
const space = String(c.req.param("space") || "");
|
||||
const space = c.req.param("space");
|
||||
const doc = ensureConfigDoc(space);
|
||||
const s = doc.settings;
|
||||
return c.json({
|
||||
|
|
@ -178,33 +172,15 @@ routes.get("/api/settings/public", (c) => {
|
|||
});
|
||||
});
|
||||
|
||||
routes.get("/api/availability", (c) => {
|
||||
const space = String(c.req.param("space") || "");
|
||||
const config = ensureConfigDoc(space);
|
||||
const bookings = ensureBookingsDoc(space);
|
||||
|
||||
const fromStr = c.req.query("from");
|
||||
const toStr = c.req.query("to");
|
||||
const viewerTz = String(c.req.query("tz") || config.settings.timezone || "UTC");
|
||||
const durationStr = c.req.query("duration");
|
||||
|
||||
const now = Date.now();
|
||||
const rangeStartMs = fromStr ? Date.parse(fromStr) : now;
|
||||
const rangeEndMs = toStr ? Date.parse(toStr) : now + 30 * 86400_000;
|
||||
|
||||
const slots = getAvailableSlots(config, bookings, {
|
||||
rangeStartMs,
|
||||
rangeEndMs,
|
||||
viewerTimezone: viewerTz,
|
||||
durationOverride: durationStr ? Number(durationStr) : undefined,
|
||||
});
|
||||
return c.json({ slots });
|
||||
routes.get("/api/availability", (_c) => {
|
||||
// Phase C wires the real engine. For now return empty so UI renders.
|
||||
return _c.json({ slots: [] });
|
||||
});
|
||||
|
||||
routes.get("/api/bookings/:id", (c) => {
|
||||
const space = String(c.req.param("space") || "");
|
||||
const id = String(c.req.param("id") || "");
|
||||
const token = String(c.req.query("token") || "");
|
||||
const space = c.req.param("space");
|
||||
const id = c.req.param("id");
|
||||
const token = c.req.query("token");
|
||||
const doc = ensureBookingsDoc(space);
|
||||
const booking = doc.bookings[id];
|
||||
if (!booking) return c.json({ error: "Not found" }, 404);
|
||||
|
|
@ -222,7 +198,7 @@ routes.get("/api/bookings/:id", (c) => {
|
|||
});
|
||||
|
||||
routes.post("/api/bookings", async (c) => {
|
||||
const space = String(c.req.param("space") || "");
|
||||
const space = c.req.param("space");
|
||||
const body = await c.req.json().catch(() => null);
|
||||
if (!body?.startTime || !body?.endTime || !body?.guestName || !body?.guestEmail) {
|
||||
return c.json({ error: "Missing required fields" }, 400);
|
||||
|
|
@ -283,8 +259,8 @@ routes.post("/api/bookings", async (c) => {
|
|||
});
|
||||
|
||||
routes.post("/api/bookings/:id/cancel", async (c) => {
|
||||
const space = String(c.req.param("space") || "");
|
||||
const id = String(c.req.param("id") || "");
|
||||
const space = c.req.param("space");
|
||||
const id = c.req.param("id");
|
||||
const body = await c.req.json().catch(() => ({}));
|
||||
const token = body?.token || c.req.query("token");
|
||||
const docId = scheduleBookingsDocId(space);
|
||||
|
|
@ -308,7 +284,7 @@ routes.post("/api/bookings/:id/cancel", async (c) => {
|
|||
routes.get("/api/admin/settings", async (c) => {
|
||||
const auth = await requireAdmin(c);
|
||||
if (!auth.ok) return c.json({ error: auth.error }, auth.status as any);
|
||||
const space = String(c.req.param("space") || "");
|
||||
const space = c.req.param("space");
|
||||
const doc = ensureConfigDoc(space);
|
||||
return c.json({ settings: doc.settings, googleAuth: { connected: doc.googleAuth.connected, email: doc.googleAuth.email } });
|
||||
});
|
||||
|
|
@ -316,7 +292,7 @@ routes.get("/api/admin/settings", async (c) => {
|
|||
routes.put("/api/admin/settings", async (c) => {
|
||||
const auth = await requireAdmin(c);
|
||||
if (!auth.ok) return c.json({ error: auth.error }, auth.status as any);
|
||||
const space = String(c.req.param("space") || "");
|
||||
const space = c.req.param("space");
|
||||
const patch = await c.req.json().catch(() => ({}));
|
||||
_syncServer!.changeDoc<ScheduleConfigDoc>(scheduleConfigDocId(space), "update settings", (d) => {
|
||||
Object.assign(d.settings, patch);
|
||||
|
|
@ -328,7 +304,7 @@ routes.put("/api/admin/settings", async (c) => {
|
|||
routes.get("/api/admin/availability", async (c) => {
|
||||
const auth = await requireAdmin(c);
|
||||
if (!auth.ok) return c.json({ error: auth.error }, auth.status as any);
|
||||
const space = String(c.req.param("space") || "");
|
||||
const space = c.req.param("space");
|
||||
const doc = ensureConfigDoc(space);
|
||||
return c.json({ rules: Object.values(doc.rules) });
|
||||
});
|
||||
|
|
@ -336,7 +312,7 @@ routes.get("/api/admin/availability", async (c) => {
|
|||
routes.post("/api/admin/availability", async (c) => {
|
||||
const auth = await requireAdmin(c);
|
||||
if (!auth.ok) return c.json({ error: auth.error }, auth.status as any);
|
||||
const space = String(c.req.param("space") || "");
|
||||
const space = c.req.param("space");
|
||||
const body = await c.req.json().catch(() => ({}));
|
||||
const id = `rule-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
||||
_syncServer!.changeDoc<ScheduleConfigDoc>(scheduleConfigDocId(space), `add rule ${id}`, (d) => {
|
||||
|
|
@ -355,8 +331,8 @@ routes.post("/api/admin/availability", async (c) => {
|
|||
routes.put("/api/admin/availability/:id", async (c) => {
|
||||
const auth = await requireAdmin(c);
|
||||
if (!auth.ok) return c.json({ error: auth.error }, auth.status as any);
|
||||
const space = String(c.req.param("space") || "");
|
||||
const id = String(c.req.param("id") || "");
|
||||
const space = c.req.param("space");
|
||||
const id = c.req.param("id");
|
||||
const patch = await c.req.json().catch(() => ({}));
|
||||
_syncServer!.changeDoc<ScheduleConfigDoc>(scheduleConfigDocId(space), `update rule ${id}`, (d) => {
|
||||
if (!d.rules[id]) return;
|
||||
|
|
@ -368,8 +344,8 @@ routes.put("/api/admin/availability/:id", async (c) => {
|
|||
routes.delete("/api/admin/availability/:id", async (c) => {
|
||||
const auth = await requireAdmin(c);
|
||||
if (!auth.ok) return c.json({ error: auth.error }, auth.status as any);
|
||||
const space = String(c.req.param("space") || "");
|
||||
const id = String(c.req.param("id") || "");
|
||||
const space = c.req.param("space");
|
||||
const id = c.req.param("id");
|
||||
_syncServer!.changeDoc<ScheduleConfigDoc>(scheduleConfigDocId(space), `delete rule ${id}`, (d) => {
|
||||
delete d.rules[id];
|
||||
});
|
||||
|
|
@ -379,7 +355,7 @@ routes.delete("/api/admin/availability/:id", async (c) => {
|
|||
routes.get("/api/admin/overrides", async (c) => {
|
||||
const auth = await requireAdmin(c);
|
||||
if (!auth.ok) return c.json({ error: auth.error }, auth.status as any);
|
||||
const space = String(c.req.param("space") || "");
|
||||
const space = c.req.param("space");
|
||||
const doc = ensureConfigDoc(space);
|
||||
return c.json({ overrides: Object.values(doc.overrides) });
|
||||
});
|
||||
|
|
@ -387,7 +363,7 @@ routes.get("/api/admin/overrides", async (c) => {
|
|||
routes.post("/api/admin/overrides", async (c) => {
|
||||
const auth = await requireAdmin(c);
|
||||
if (!auth.ok) return c.json({ error: auth.error }, auth.status as any);
|
||||
const space = String(c.req.param("space") || "");
|
||||
const space = c.req.param("space");
|
||||
const body = await c.req.json().catch(() => ({}));
|
||||
if (!body?.date) return c.json({ error: "date required (YYYY-MM-DD)" }, 400);
|
||||
const id = `ov-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
||||
|
|
@ -408,8 +384,8 @@ routes.post("/api/admin/overrides", async (c) => {
|
|||
routes.delete("/api/admin/overrides/:id", async (c) => {
|
||||
const auth = await requireAdmin(c);
|
||||
if (!auth.ok) return c.json({ error: auth.error }, auth.status as any);
|
||||
const space = String(c.req.param("space") || "");
|
||||
const id = String(c.req.param("id") || "");
|
||||
const space = c.req.param("space");
|
||||
const id = c.req.param("id");
|
||||
_syncServer!.changeDoc<ScheduleConfigDoc>(scheduleConfigDocId(space), `delete override ${id}`, (d) => {
|
||||
delete d.overrides[id];
|
||||
});
|
||||
|
|
@ -419,7 +395,7 @@ routes.delete("/api/admin/overrides/:id", async (c) => {
|
|||
routes.get("/api/admin/bookings", async (c) => {
|
||||
const auth = await requireAdmin(c);
|
||||
if (!auth.ok) return c.json({ error: auth.error }, auth.status as any);
|
||||
const space = String(c.req.param("space") || "");
|
||||
const space = c.req.param("space");
|
||||
const doc = ensureBookingsDoc(space);
|
||||
const rows = Object.values(doc.bookings)
|
||||
.sort((a, b) => b.startTime - a.startTime)
|
||||
|
|
@ -437,7 +413,7 @@ routes.get("/api/admin/bookings", async (c) => {
|
|||
});
|
||||
|
||||
routes.get("/api/invitations", (c) => {
|
||||
const space = String(c.req.param("space") || "");
|
||||
const space = c.req.param("space");
|
||||
const doc = ensureInvitationsDoc(space);
|
||||
return c.json({
|
||||
invitations: Object.values(doc.invitations).sort((a, b) => a.startTime - b.startTime),
|
||||
|
|
@ -447,7 +423,7 @@ routes.get("/api/invitations", (c) => {
|
|||
routes.get("/api/admin/google/status", async (c) => {
|
||||
const auth = await requireAdmin(c);
|
||||
if (!auth.ok) return c.json({ error: auth.error }, auth.status as any);
|
||||
const space = String(c.req.param("space") || "");
|
||||
const space = c.req.param("space");
|
||||
const doc = ensureConfigDoc(space);
|
||||
return c.json(doc.googleAuth);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue