NobGit
public imalexnord read

Slot

Calendar-first scheduling powered by Cloudflare Workers and Google Calendar.

Languages

Repository composition by tracked source files.

TypeScript
TypeScript 79% CSS 19% SQL 2% HTML 0%
Create file Wiki Documentation
Clone
https://nobgit.com/user/imalexnord/slot.git
ssh://[email protected]:2222/user/imalexnord/slot.git
App.tsx
import {
  Activity,
  ArrowLeft,
  ArrowRight,
  CalendarDays,
  CalendarPlus,
  Check,
  ChevronLeft,
  ChevronRight,
  Clock3,
  Copy,
  CreditCard,
  ExternalLink,
  Inbox,
  Loader2,
  LogOut,
  MapPin,
  MonitorUp,
  Phone,
  Search,
  Settings2,
  Sparkles,
  Users,
  Video,
} from 'lucide-react';
import { FormEvent, useEffect, useMemo, useRef, useState } from 'react';

type MeetingMode = 'google_meet' | 'phone' | 'in_person' | 'decide_later';

type PublicConfig = {
  title: string;
  subtitle: string;
  timezone: string;
  bookingWindowDays: number;
  durations: number[];
  defaultMeetingMode: MeetingMode;
  allowMeetingModes: MeetingMode[];
  availableWeekdays: number[];
  connected: boolean;
  payment?: PaymentOption;
};

type PaymentOption = {
  enabled: true;
  required: boolean;
  ready: boolean;
  ratePerMinute: number;
  freeDurations: number[];
  currency: string;
  label: string;
};

type DurationBlockReason = 'outside_hours' | 'unavailable';
type Slot = {
  start: string;
  time: string;
  durations: number[];
  blockedDurations?: Record<string, DurationBlockReason>;
};

type Confirmation = {
  id: string;
  bookingId?: string | null;
  start: string;
  end: string;
  duration: number;
  timezone: string;
  meetingMode: MeetingMode;
  meetUrl?: string | null;
  receiptUrl?: string | null;
};

type AppSettings = {
  title: string;
  subtitle: string;
  timezone: string;
  bookingWindowDays: number;
  minimumNoticeMinutes: number;
  bufferBeforeMinutes: number;
  bufferAfterMinutes: number;
  slotIncrementMinutes: number;
  durations: number[];
  calendarId: string;
  busyCalendarIds: string[];
  defaultMeetingMode: MeetingMode;
  allowMeetingModes: MeetingMode[];
  inPersonLocation: string;
  paymentEnabled: boolean;
  paymentRatePerMinute: number;
  paymentFreeDurations: number[];
  paymentCurrency: string;
  paymentLabel: string;
  availability: Record<string, Array<{ start: string; end: string }>>;
};

type BookingRow = {
  id: string;
  public_booking_id?: string | null;
  name: string;
  email: string;
  start_time: string;
  end_time: string;
  duration_minutes: number;
  meeting_mode: MeetingMode;
  status: string;
  meet_url?: string;
  calendar_provider?: string;
  calendar_sync_status?: string;
  calendar_sync_error?: string;
  calendar_sync_updated_at?: string;
  payment_status?: string;
  payment_amount?: number;
  payment_currency?: string;
  stripe_checkout_session_id?: string;
  stripe_payment_intent_id?: string;
  paid_at?: string;
  cancelled_at?: string;
  created_at?: string;
  rescheduled_at?: string;
  reschedule_count?: number;
  proposed_start_time?: string;
  proposed_end_time?: string;
  proposed_at?: string;
  proposal_status?: string;
  refund_status?: string;
  stripe_refund_id?: string;
  refunded_amount?: number;
  refunded_at?: string;
};

type ActivityStats = {
  total_bookings: number;
  upcoming: number;
  completed: number;
  cancelled: number;
  reschedules: number;
  paid_bookings: number;
  average_minutes: number;
  open_payment_holds: number;
};

type ActivityMoney = {
  currency: string;
  revenue: number;
  refunds: number;
  net: number;
};

type ActivityEvent = {
  id: string;
  booking_id?: string | null;
  type: string;
  occurred_at: string;
  metadata: Record<string, unknown>;
  name?: string | null;
  email?: string | null;
  start_time?: string | null;
  end_time?: string | null;
  duration_minutes?: number | null;
  payment_amount?: number | null;
  payment_currency?: string | null;
};

type ActivityData = {
  paymentsEnabled: boolean;
  stats: ActivityStats;
  money: ActivityMoney[];
  events: ActivityEvent[];
};

type AttendeeRow = {
  email: string;
  name: string;
  total_bookings: number;
  upcoming: number;
  completed: number;
  cancelled: number;
  reschedules: number;
  paid_bookings: number;
  refunded_bookings: number;
  total_minutes: number;
  average_minutes: number;
  first_booked_at?: string | null;
  last_booked_at?: string | null;
};

type AttendeeData = {
  paymentsEnabled: boolean;
  attendees: AttendeeRow[];
};

type RescheduleProposal = {
  title: string;
  name: string;
  currentStart: string;
  currentEnd: string;
  proposedStart: string;
  proposedEnd: string;
  duration: number;
  timezone: string;
  meetingMode: MeetingMode;
};

type ManagedBooking = {
  bookingId: string;
  name: string;
  start: string;
  end: string;
  duration: number;
  timezone: string;
  meetingMode: MeetingMode;
  status: string;
  paymentStatus: string;
  refundStatus: string;
  canCancel: boolean;
  canReschedule: boolean;
};

type ManageAction = 'none' | 'cancel' | 'reschedule';

type BookingScheduleAction = 'reschedule' | 'suggest';

type RecurringUnavailableRule = {
  id: string;
  weekdays: number[];
  start: string;
  end: string;
  label?: string | null;
};

type AvailabilityBlock = {
  id: string;
  date: string;
  start?: string | null;
  end?: string | null;
  allDay: boolean;
  label?: string | null;
};

type AdminSession = { authenticated: boolean; email: string; connected: boolean };
type ThemeId = 'light' | 'paper' | 'beige' | 'dark' | 'midnight' | 'rosewood' | 'forest' | 'true-black';

const modeMeta: Record<MeetingMode, { label: string; icon: typeof Video; help: string }> = {
  google_meet: { label: 'Google Meet', icon: Video, help: 'A Meet link is created automatically.' },
  phone: { label: 'Phone', icon: Phone, help: 'Add a number for the call.' },
  in_person: { label: 'In person', icon: MapPin, help: 'The location will be included in the invite.' },
  decide_later: { label: 'Decide later', icon: Sparkles, help: 'Book the time first and sort out the place later.' },
};

const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
const shortDayNames = ['MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT', 'SUN'];
const timezoneOptions = [
  'Europe/Stockholm',
  'Europe/London',
  'Europe/Berlin',
  'Europe/Paris',
  'Europe/Madrid',
  'Europe/Amsterdam',
  'Europe/Helsinki',
  'UTC',
  'America/New_York',
  'America/Chicago',
  'America/Denver',
  'America/Los_Angeles',
  'America/Toronto',
  'Asia/Tokyo',
  'Asia/Singapore',
  'Australia/Sydney',
];
const themes: Array<{ id: ThemeId; name: string; description: string; contrast: string }> = [
  { id: 'light', name: 'Light', description: 'Bright surfaces with dark text and clear contrast.', contrast: 'Light contrast' },
  { id: 'paper', name: 'Paper', description: 'Cool, document-like surfaces with crisp separation.', contrast: 'Crisp contrast' },
  { id: 'beige', name: 'Beige', description: 'Warm neutral surfaces for a softer reading experience.', contrast: 'Warm contrast' },
  { id: 'dark', name: 'Dark', description: 'Dark grey surfaces that reduce glare while keeping depth.', contrast: 'Soft contrast' },
  { id: 'midnight', name: 'Midnight', description: 'Deep navy surfaces for calm late-night work.', contrast: 'Cool contrast' },
  { id: 'rosewood', name: 'Rosewood', description: 'Warm dark surfaces with copper and walnut tones.', contrast: 'Warm dark' },
  { id: 'forest', name: 'Forest', description: 'Muted pine surfaces with restrained natural contrast.', contrast: 'Natural dark' },
  { id: 'true-black', name: 'True black', description: 'Pure black backgrounds with maximum dark-mode contrast.', contrast: 'High contrast' },
];

async function api<T>(url: string, init?: RequestInit): Promise<T> {
  const response = await fetch(url, {
    ...init,
    headers: {
      ...(init?.body ? { 'Content-Type': 'application/json' } : {}),
      ...(init?.headers ?? {}),
    },
  });
  const data = (await response.json().catch(() => ({}))) as T & { error?: string };
  if (!response.ok) throw new Error(data.error || `Request failed (${response.status})`);
  return data;
}

function checkoutSessionMode(sessionId?: string): 'test' | 'live' | 'unknown' {
  if (sessionId?.startsWith('cs_test_')) return 'test';
  if (sessionId?.startsWith('cs_live_')) return 'live';
  return 'unknown';
}

function dateKey(date: Date): string {
  const year = date.getFullYear();
  const month = String(date.getMonth() + 1).padStart(2, '0');
  const day = String(date.getDate()).padStart(2, '0');
  return `${year}-${month}-${day}`;
}

function parseDateKey(value: string): Date {
  const [y, m, d] = value.split('-').map(Number);
  return new Date(y, m - 1, d, 12, 0, 0);
}

function addDays(date: Date, amount: number): Date {
  const copy = new Date(date);
  copy.setDate(copy.getDate() + amount);
  return copy;
}

function todayInZone(timezone: string): string {
  const parts = new Intl.DateTimeFormat('en-CA', {
    timeZone: timezone,
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
  }).formatToParts(new Date());
  const map = Object.fromEntries(parts.filter((p) => p.type !== 'literal').map((p) => [p.type, p.value]));
  return `${map.year}-${map.month}-${map.day}`;
}

function dateKeyForInstant(iso: string, timezone: string): string {
  const parts = new Intl.DateTimeFormat('en-CA', {
    timeZone: timezone,
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
  }).formatToParts(new Date(iso));
  const map = Object.fromEntries(parts.filter((part) => part.type !== 'literal').map((part) => [part.type, part.value]));
  return `${map.year}-${map.month}-${map.day}`;
}

function prettyDate(key: string, timezone: string): string {
  return new Intl.DateTimeFormat('en-US', {
    timeZone: timezone,
    weekday: 'long',
    month: 'long',
    day: 'numeric',
  }).format(new Date(`${key}T12:00:00Z`));
}

function dayAriaLabel(date: Date): string {
  return new Intl.DateTimeFormat('en-US', {
    weekday: 'long',
    month: 'long',
    day: 'numeric',
    year: 'numeric',
  }).format(date);
}

function formatInstant(iso: string, timezone: string): string {
  return new Intl.DateTimeFormat('en-US', {
    timeZone: timezone,
    weekday: 'short',
    month: 'short',
    day: 'numeric',
    hour: '2-digit',
    minute: '2-digit',
    hourCycle: 'h23',
  }).format(new Date(iso));
}

function formatDateShort(iso: string, timezone: string): string {
  return new Intl.DateTimeFormat('en-US', {
    timeZone: timezone,
    weekday: 'short',
    month: 'short',
    day: 'numeric',
  }).format(new Date(iso));
}

function formatClock(iso: string, timezone: string): string {
  return new Intl.DateTimeFormat('en-US', {
    timeZone: timezone,
    hour: '2-digit',
    minute: '2-digit',
    hourCycle: 'h23',
  }).format(new Date(iso));
}

function formatMoneyMinor(amountMinor: number, currency: string): string {
  try {
    const formatter = new Intl.NumberFormat('en-US', {
      style: 'currency',
      currency: currency.toUpperCase(),
    });
    const digits = formatter.resolvedOptions().maximumFractionDigits ?? 2;
    return formatter.format(amountMinor / (10 ** digits));
  } catch {
    return `${amountMinor} ${currency.toUpperCase()}`;
  }
}

function formatMoneyMajor(amount: number, currency: string): string {
  try {
    return new Intl.NumberFormat('en-US', {
      style: 'currency',
      currency: currency.toUpperCase(),
      maximumFractionDigits: 2,
    }).format(amount);
  } catch {
    return `${amount.toFixed(2)} ${currency.toUpperCase()}`;
  }
}

function databaseInstant(value: string): string {
  if (/Z$|[+-]\d{2}:?\d{2}$/.test(value)) return value;
  return `${value.replace(' ', 'T')}Z`;
}

function activityMoneyText(money: ActivityMoney[], field: 'revenue' | 'refunds' | 'net', preferredCurrency: string): string {
  if (!money.length) return formatMoneyMinor(0, preferredCurrency);
  const sorted = [...money].sort((a, b) => {
    if (a.currency === preferredCurrency.toLowerCase()) return -1;
    if (b.currency === preferredCurrency.toLowerCase()) return 1;
    return a.currency.localeCompare(b.currency);
  });
  return sorted.map((row) => formatMoneyMinor(row[field], row.currency)).join(' · ');
}

function activityEventCopy(event: ActivityEvent, timezone: string): { title: string; detail: string } {
  const name = event.name || 'A visitor';
  const meta = event.metadata ?? {};
  const amount = typeof meta.amount === 'number' ? meta.amount : event.payment_amount;
  const currency = typeof meta.currency === 'string' ? meta.currency : event.payment_currency;
  const money = amount != null && currency ? formatMoneyMinor(amount, currency) : null;
  const duration = typeof meta.duration === 'number' ? meta.duration : event.duration_minutes;
  const eventStart = typeof meta.newStart === 'string'
    ? meta.newStart
    : typeof meta.proposedStart === 'string'
      ? meta.proposedStart
      : typeof meta.start === 'string'
        ? meta.start
        : event.start_time;
  const oldStart = typeof meta.oldStart === 'string' ? meta.oldStart : null;

  switch (event.type) {
    case 'booking_confirmed':
      return { title: `${name} booked a meeting`, detail: `${duration ?? ''}${duration ? ' min · ' : ''}${eventStart ? `${formatDateShort(eventStart, timezone)} at ${formatClock(eventStart, timezone)}` : 'Confirmed'}` };
    case 'payment_checkout_started':
      return { title: `${name} started checkout`, detail: `${money ? `${money} · ` : ''}${eventStart ? `${formatDateShort(eventStart, timezone)} at ${formatClock(eventStart, timezone)}` : 'Payment pending'}` };
    case 'payment_received':
      return { title: `Payment received from ${name}`, detail: money ?? 'Stripe payment completed' };
    case 'payment_hold_released':
      return { title: `${name}'s payment hold was released`, detail: eventStart ? `${formatDateShort(eventStart, timezone)} at ${formatClock(eventStart, timezone)} is available again` : 'The held time is available again' };
    case 'booking_rescheduled':
      return { title: `${name}'s meeting was rescheduled`, detail: `${oldStart ? `${formatDateShort(oldStart, timezone)} ${formatClock(oldStart, timezone)} → ` : ''}${eventStart ? `${formatDateShort(eventStart, timezone)} ${formatClock(eventStart, timezone)}` : 'New time saved'}` };
    case 'time_suggested':
      return { title: `New time suggested to ${name}`, detail: eventStart ? `${formatDateShort(eventStart, timezone)} at ${formatClock(eventStart, timezone)}` : 'Waiting for the guest' };
    case 'time_suggestion_accepted':
      return { title: `${name} accepted the suggested time`, detail: eventStart ? `${formatDateShort(eventStart, timezone)} at ${formatClock(eventStart, timezone)}` : 'Meeting rescheduled' };
    case 'refund_started':
      return { title: `Refund started for ${name}`, detail: money ?? 'Stripe is processing the refund' };
    case 'refund_succeeded':
      return { title: `Refund completed for ${name}`, detail: money ?? 'Stripe refund completed' };
    case 'refund_failed':
      return { title: `Refund failed for ${name}`, detail: money ? `${money} · Check Stripe` : 'Check Stripe for the failure details' };
    case 'booking_cancelled':
      return { title: `${name}'s meeting was cancelled`, detail: eventStart ? `${formatDateShort(eventStart, timezone)} at ${formatClock(eventStart, timezone)}` : 'Booking cancelled' };
    default:
      return { title: event.type.replaceAll('_', ' '), detail: event.email ?? 'Slot activity' };
  }
}

function monthCells(month: Date): Array<{ date: Date; current: boolean }> {
  const first = new Date(month.getFullYear(), month.getMonth(), 1, 12);
  const mondayIndex = (first.getDay() + 6) % 7;
  const start = addDays(first, -mondayIndex);
  return Array.from({ length: 42 }, (_, index) => {
    const date = addDays(start, index);
    return { date, current: date.getMonth() === month.getMonth() };
  });
}

function googleCalendarUrl(title: string, conf: Confirmation): string {
  const stamp = (iso: string) => iso.replace(/[-:]/g, '').replace(/\.\d{3}/, '');
  const params = new URLSearchParams({
    action: 'TEMPLATE',
    text: title,
    dates: `${stamp(conf.start)}/${stamp(conf.end)}`,
  });
  if (conf.meetUrl) params.set('details', `Google Meet: ${conf.meetUrl}`);
  return `https://calendar.google.com/calendar/render?${params.toString()}`;
}

function useCopy(): [boolean, (value: string) => void] {
  const [copied, setCopied] = useState(false);
  const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
  useEffect(() => () => clearTimeout(timer.current), []);
  function copy(value: string) {
    navigator.clipboard?.writeText(value).then(() => {
      setCopied(true);
      clearTimeout(timer.current);
      timer.current = setTimeout(() => setCopied(false), 1800);
    });
  }
  return [copied, copy];
}

function PublicBooking() {
  const [config, setConfig] = useState<PublicConfig | null>(null);
  const [month, setMonth] = useState(() => new Date());
  const [selectedDate, setSelectedDate] = useState<string>('');
  const [slots, setSlots] = useState<Slot[]>([]);
  const [selectedSlot, setSelectedSlot] = useState<Slot | null>(null);
  const [duration, setDuration] = useState<number | null>(null);
  const [meetingMode, setMeetingMode] = useState<MeetingMode>('google_meet');
  const [loadingSlots, setLoadingSlots] = useState(false);
  const [booking, setBooking] = useState(false);
  const [error, setError] = useState('');
  const [fieldErrors, setFieldErrors] = useState<{ name?: string; email?: string }>({});
  const [availabilityNotice, setAvailabilityNotice] = useState('');
  const [confirmation, setConfirmation] = useState<Confirmation | null>(null);
  const [paymentReturnState, setPaymentReturnState] = useState<{ kind: 'checking' | 'cancelled' | 'error'; message: string } | null>(null);
  const [manageOpen, setManageOpen] = useState(false);
  const [manageBookingId, setManageBookingId] = useState('');
  const [managedBooking, setManagedBooking] = useState<ManagedBooking | null>(null);
  const [manageAction, setManageAction] = useState<ManageAction>('none');
  const [manageDate, setManageDate] = useState('');
  const [manageSlots, setManageSlots] = useState<Array<{ start: string; time: string }>>([]);
  const [manageStart, setManageStart] = useState('');
  const [manageSlotsLoading, setManageSlotsLoading] = useState(false);
  const [manageState, setManageState] = useState<{ kind: 'idle' | 'loading' | 'success' | 'error'; message: string }>({ kind: 'idle', message: '' });

  useEffect(() => {
    api<PublicConfig>('/api/config')
      .then((data) => {
        setConfig(data);
        setMeetingMode(data.defaultMeetingMode);
        document.title = data.title;
      })
      .catch((err) => setError(err.message));
  }, []);


  useEffect(() => {
    const params = new URLSearchParams(window.location.search);
    const payment = params.get('payment');
    const sessionId = params.get('session_id');
    const bookingId = params.get('booking_id');
    const released = params.get('released');
    let cancelled = false;
    let timer: ReturnType<typeof setTimeout> | undefined;

    function cleanReturnUrl() {
      window.history.replaceState({}, '', window.location.pathname);
    }

    async function pollStatus(attempt = 0) {
      if (!sessionId || cancelled) return;
      setPaymentReturnState({ kind: 'checking', message: attempt === 0 ? 'Payment received. Confirming your booking…' : 'Payment received. Finalizing your calendar invitation…' });
      try {
        const data = await api<{ state: string; message?: string; booking?: Confirmation }>(`/api/payments/status?session_id=${encodeURIComponent(sessionId)}`);
        if (cancelled) return;
        if (data.state === 'confirmed' && data.booking) {
          setConfirmation(data.booking);
          setMeetingMode(data.booking.meetingMode);
          setPaymentReturnState(null);
          cleanReturnUrl();
          return;
        }
        if ((data.state === 'confirming' || data.state === 'processing') && attempt < 20) {
          setPaymentReturnState({ kind: 'checking', message: data.message || 'Finalizing your booking…' });
          timer = setTimeout(() => void pollStatus(attempt + 1), 1000);
          return;
        }
        setPaymentReturnState({ kind: 'error', message: data.message || 'Payment did not complete, so no booking was confirmed.' });
        cleanReturnUrl();
      } catch (err) {
        if (cancelled) return;
        if (attempt < 8) {
          timer = setTimeout(() => void pollStatus(attempt + 1), 1200);
          return;
        }
        setPaymentReturnState({ kind: 'error', message: err instanceof Error ? err.message : 'Could not verify the payment.' });
      }
    }

    async function releaseCancelledCheckout() {
      if (!bookingId || cancelled) return;
      setPaymentReturnState({ kind: 'checking', message: 'Cancelling payment and releasing the time…' });
      try {
        const data = await api<{ state: string; booking?: Confirmation }>('/api/payments/cancel', {
          method: 'POST',
          body: JSON.stringify({ bookingId }),
        });
        if (cancelled) return;
        if (data.state === 'confirmed' && data.booking) {
          setConfirmation(data.booking);
          setMeetingMode(data.booking.meetingMode);
          setPaymentReturnState(null);
        } else if (data.state === 'processing') {
          setPaymentReturnState({ kind: 'checking', message: 'Stripe is processing the payment. Please wait…' });
        } else {
          setPaymentReturnState({ kind: 'cancelled', message: 'Payment cancelled. No booking was confirmed and the time has been released.' });
        }
        cleanReturnUrl();
      } catch (err) {
        if (!cancelled) setPaymentReturnState({ kind: 'error', message: err instanceof Error ? err.message : 'Could not cancel the checkout cleanly.' });
      }
    }

    if (payment === 'success' && sessionId) void pollStatus();
    else if (payment === 'cancelled' && released === '1') {
      setPaymentReturnState({ kind: 'cancelled', message: 'Payment cancelled. No booking was confirmed and the time has been released.' });
      cleanReturnUrl();
    } else if (payment === 'cancelled' && bookingId) void releaseCancelledCheckout();

    return () => {
      cancelled = true;
      if (timer) clearTimeout(timer);
    };
  }, []);

  const cells = useMemo(() => monthCells(month), [month]);
  const today = config ? todayInZone(config.timezone) : dateKey(new Date());
  const maxDate = config ? dateKey(addDays(parseDateKey(today), config.bookingWindowDays)) : today;

  const isSelectable = (key: string, weekday: number, current: boolean) =>
    Boolean(config) && current && key >= today && key <= maxDate && config!.availableWeekdays.includes(weekday);

  async function chooseDate(key: string) {
    if (!config || key < today || key > maxDate) return;
    setSelectedDate(key);
    setSelectedSlot(null);
    setDuration(null);
    setConfirmation(null);
    setSlots([]);
    setError('');
    setAvailabilityNotice('');
    setLoadingSlots(true);
    try {
      const data = await api<{ slots: Slot[]; calendarError?: string }>(`/api/availability?date=${encodeURIComponent(key)}`);
      setSlots(data.slots);
      setAvailabilityNotice(data.calendarError ?? '');
    } catch (err) {
      const message = err instanceof Error ? err.message : 'Could not load availability';
      if (message.startsWith('Calendar setup needs attention')) setAvailabilityNotice(message);
      else setError(message);
    } finally {
      setLoadingSlots(false);
    }
  }

  async function submitBooking(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    if (!config || !selectedSlot || !duration) return;
    const form = new FormData(event.currentTarget);
    const name = String(form.get('name') ?? '').trim();
    const email = String(form.get('email') ?? '').trim();
    const nextErrors: { name?: string; email?: string } = {};
    if (!name) nextErrors.name = 'Please add your name.';
    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) nextErrors.email = 'Enter a valid email address.';
    setFieldErrors(nextErrors);
    if (Object.keys(nextErrors).length) return;

    setBooking(true);
    setError('');
    try {
      const data = await api<{ requiresPayment: boolean; checkoutUrl?: string; booking?: Confirmation }>('/api/book', {
        method: 'POST',
        body: JSON.stringify({
          start: selectedSlot.start,
          duration,
          meetingMode,
          name,
          email,
          phone: form.get('phone'),
          message: form.get('message'),
          website: form.get('website'),
        }),
      });
      if (data.requiresPayment) {
        if (!data.checkoutUrl) throw new Error('Stripe did not return a secure checkout link.');
        window.location.assign(data.checkoutUrl);
        return;
      }
      if (!data.booking) throw new Error('The booking response was incomplete.');
      setConfirmation(data.booking);
    } catch (err) {
      const message = err instanceof Error ? err.message : 'Booking failed';
      setError(message);
      if (message.toLowerCase().includes('slot') || message.toLowerCase().includes('time')) {
        await chooseDate(selectedDate);
      }
    } finally {
      setBooking(false);
    }
  }

  async function submitManageLookup(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const bookingId = manageBookingId.trim();
    if (!bookingId) {
      setManageState({ kind: 'error', message: 'Enter the booking ID from your confirmation or calendar invitation.' });
      return;
    }
    setManageState({ kind: 'loading', message: 'Loading booking…' });
    setManagedBooking(null);
    setManageAction('none');
    setManageSlots([]);
    setManageStart('');
    try {
      const result = await api<{ booking: ManagedBooking }>('/api/bookings/manage', {
        method: 'POST',
        body: JSON.stringify({ bookingId }),
      });
      setManagedBooking(result.booking);
      setManageBookingId(result.booking.bookingId);
      setManageDate(dateKeyForInstant(result.booking.start, result.booking.timezone));
      setManageState({ kind: 'idle', message: '' });
    } catch (err) {
      setManageState({ kind: 'error', message: err instanceof Error ? err.message : 'Could not find this booking.' });
    }
  }

  async function loadManageAvailability(date: string) {
    if (!managedBooking || !date) return;
    setManageDate(date);
    setManageStart('');
    setManageSlots([]);
    setManageSlotsLoading(true);
    setManageState({ kind: 'idle', message: '' });
    try {
      const result = await api<{ slots: Array<{ start: string; time: string }> }>('/api/bookings/manage/availability', {
        method: 'POST',
        body: JSON.stringify({ bookingId: managedBooking.bookingId, date }),
      });
      setManageSlots(result.slots);
    } catch (err) {
      setManageState({ kind: 'error', message: err instanceof Error ? err.message : 'Could not load available times.' });
    } finally {
      setManageSlotsLoading(false);
    }
  }

  async function cancelManagedBooking() {
    if (!managedBooking) return;
    setManageState({ kind: 'loading', message: 'Cancelling booking…' });
    try {
      const result = await api<{ ok: boolean; refunded?: boolean; refundStatus?: string; alreadyCancelled?: boolean; warning?: string }>('/api/bookings/cancel', {
        method: 'POST',
        body: JSON.stringify({ bookingId: managedBooking.bookingId }),
      });
      let message = result.alreadyCancelled
        ? 'This booking was already cancelled.'
        : result.refunded
          ? result.refundStatus === 'pending'
            ? 'Booking cancelled. Stripe accepted the refund and it is currently pending.'
            : 'Booking cancelled. The Stripe payment has been refunded.'
          : 'Booking cancelled. The calendar invitation has been cancelled.';
      if (result.warning) message += ` ${result.warning}`;
      setManagedBooking({ ...managedBooking, status: 'cancelled', canCancel: false, canReschedule: false, refundStatus: result.refundStatus ?? managedBooking.refundStatus });
      setManageAction('none');
      setManageState({ kind: 'success', message });
      if (confirmation?.bookingId && confirmation.bookingId.toUpperCase() === managedBooking.bookingId.toUpperCase()) setConfirmation(null);
      if (selectedDate) await chooseDate(selectedDate);
    } catch (err) {
      setManageState({ kind: 'error', message: err instanceof Error ? err.message : 'Could not cancel this booking.' });
    }
  }

  async function rescheduleManagedBooking() {
    if (!managedBooking || !manageStart) return;
    setManageState({ kind: 'loading', message: 'Rescheduling booking…' });
    try {
      const result = await api<{ ok: boolean; booking: ManagedBooking }>('/api/bookings/reschedule', {
        method: 'POST',
        body: JSON.stringify({ bookingId: managedBooking.bookingId, start: manageStart }),
      });
      setManagedBooking(result.booking);
      setManageDate(dateKeyForInstant(result.booking.start, result.booking.timezone));
      setManageStart('');
      setManageSlots([]);
      setManageAction('none');
      setManageState({ kind: 'success', message: 'Booking rescheduled. Google Calendar sent an updated invitation.' });
      if (selectedDate) await chooseDate(selectedDate);
    } catch (err) {
      setManageState({ kind: 'error', message: err instanceof Error ? err.message : 'Could not reschedule this booking.' });
    }
  }

  if (!config) {
    return <div className="center-screen"><Loader2 className="spin" /> Loading calendar…</div>;
  }

  const activeConfig = config;
  const bookingStep = confirmation
    ? 'confirmed'
    : !selectedSlot
      ? 'time'
      : !duration
        ? 'duration'
        : 'details';

  function changeTime() {
    setSelectedSlot(null);
    setDuration(null);
    setConfirmation(null);
  }

  function changeDuration() {
    setDuration(null);
    setConfirmation(null);
  }

  function bookAnotherTime() {
    setSelectedSlot(null);
    setDuration(null);
    setConfirmation(null);
    setFieldErrors({});
    setError('');
  }

  function renderTimesView() {
    const groups: Array<{ label: string; items: Slot[] }> = [
      { label: 'Morning', items: [] },
      { label: 'Afternoon', items: [] },
      { label: 'Evening', items: [] },
    ];
    for (const slot of slots) {
      const hour = Number(slot.time.slice(0, 2));
      const bucket = hour < 12 ? 0 : hour < 17 ? 1 : 2;
      groups[bucket].items.push(slot);
    }
    const nonEmpty = groups.filter((group) => group.items.length);
    const showLabels = slots.length > 6 && nonEmpty.length > 1;

    return (
      <div className="panel-view" key="times">
        <div className="selection-heading">
          <p className="step-label">02 · PICK A TIME</p>
          <h2>{prettyDate(selectedDate, activeConfig.timezone)}</h2>
        </div>

        {loadingSlots ? (
          <>
            <div className="loading-row"><Loader2 className="spin" /> Checking availability…</div>
            <div className="slot-skeleton" aria-hidden="true">
              {Array.from({ length: 9 }, (_, i) => <span key={i} />)}
            </div>
          </>
        ) : availabilityNotice ? (
          <div className="no-slots" role="status">{availabilityNotice}</div>
        ) : slots.length === 0 ? (
          <div className="no-slots" role="status">Nothing open on this day. Try another date.</div>
        ) : (
          <div className="time-scroll">
            {nonEmpty.map((group) => (
              <div className="time-group" key={group.label}>
                {showLabels && <span className="time-group-label">{group.label}</span>}
                <div className="time-grid">
                  {group.items.map((slot) => (
                    <button
                      key={slot.start}
                      type="button"
                      className={`time-button ${selectedSlot?.start === slot.start ? 'active' : ''}`}
                      aria-pressed={selectedSlot?.start === slot.start}
                      onClick={() => {
                        setSelectedSlot(slot);
                        setDuration(null);
                        setConfirmation(null);
                      }}
                    >
                      {slot.time}
                    </button>
                  ))}
                </div>
              </div>
            ))}
          </div>
        )}
      </div>
    );
  }

  function renderDurationView() {
    if (!selectedSlot) return null;
    const available = new Set(selectedSlot.durations);
    const disabledDurations = activeConfig.durations.filter((minutes) => !available.has(minutes));
    const hasDisabled = disabledDurations.length > 0;
    const disabledReasons = new Set(disabledDurations.map((minutes) => selectedSlot.blockedDurations?.[String(minutes)] ?? 'unavailable'));
    const disabledHint = disabledReasons.size === 1 && disabledReasons.has('outside_hours')
      ? "Greyed-out lengths extend past today's booking hours."
      : disabledReasons.size === 1 && disabledReasons.has('unavailable')
        ? 'Greyed-out lengths run into unavailable calendar time, an availability block, or a configured buffer.'
        : "Greyed-out lengths either extend past today's booking hours or run into unavailable time.";
    return (
      <div className="panel-view" key="duration">
        <button className="panel-back" type="button" onClick={changeTime}>
          <ArrowLeft size={16} /> Change time
        </button>
        <div className="selected-summary">
          <span>{prettyDate(selectedDate, activeConfig.timezone)}</span>
          <strong>{selectedSlot.time}</strong>
        </div>
        <div className="duration-picker-view">
          <p className="step-label">03 · HOW LONG?</p>
          <div className="duration-grid">
            {activeConfig.durations.map((minutes) => {
              const enabled = available.has(minutes);
              return (
                <button
                  key={minutes}
                  type="button"
                  className={`duration-button ${duration === minutes ? 'active' : ''}`}
                  disabled={!enabled}
                  aria-pressed={duration === minutes}
                  title={enabled ? undefined : (selectedSlot.blockedDurations?.[String(minutes)] === 'outside_hours' ? "Extends past today's booking hours" : 'Runs into unavailable calendar time or a configured block')}
                  onClick={() => setDuration(minutes)}
                >
                  <strong>{minutes}</strong>
                  <span>min</span>
                </button>
              );
            })}
          </div>
          {hasDisabled && (
            <p className="duration-hint">{disabledHint}</p>
          )}
        </div>
      </div>
    );
  }

  function renderDetailsView() {
    if (!selectedSlot || !duration) return null;
    const paymentRequiredForSelection = Boolean(activeConfig.payment && !activeConfig.payment.freeDurations.includes(duration));
    return (
      <div className="panel-view panel-view-scroll" key="details">
        <button className="panel-back" type="button" onClick={changeDuration}>
          <ArrowLeft size={16} /> Change duration
        </button>
        <div className="selected-summary">
          <span>{prettyDate(selectedDate, activeConfig.timezone)}</span>
          <strong>{selectedSlot.time} · {duration} min</strong>
        </div>
        <form className="details-form compact-details" onSubmit={submitBooking} noValidate>
          <p className="step-label">04 · YOUR DETAILS</p>
          <div className="form-row">
            <label>Name
              <input
                name="name"
                required
                autoComplete="name"
                placeholder="Your name"
                aria-invalid={fieldErrors.name ? 'true' : undefined}
                aria-describedby={fieldErrors.name ? 'name-error' : undefined}
                onInput={() => fieldErrors.name && setFieldErrors((e) => ({ ...e, name: undefined }))}
              />
              {fieldErrors.name && <span className="field-error" id="name-error">{fieldErrors.name}</span>}
            </label>
            <label>Email
              <input
                name="email"
                type="email"
                required
                autoComplete="email"
                placeholder="[email protected]"
                aria-invalid={fieldErrors.email ? 'true' : undefined}
                aria-describedby={fieldErrors.email ? 'email-error' : undefined}
                onInput={() => fieldErrors.email && setFieldErrors((e) => ({ ...e, email: undefined }))}
              />
              {fieldErrors.email && <span className="field-error" id="email-error">{fieldErrors.email}</span>}
            </label>
          </div>

          {activeConfig.allowMeetingModes.length > 1 && (
            <div className="mode-picker">
              <span className="field-label">Meeting</span>
              <div className="mode-options" role="radiogroup" aria-label="Meeting method">
                {activeConfig.allowMeetingModes.map((mode) => {
                  const Icon = modeMeta[mode].icon;
                  return (
                    <button
                      key={mode}
                      type="button"
                      role="radio"
                      aria-checked={meetingMode === mode}
                      className={`mode-option ${meetingMode === mode ? 'active' : ''}`}
                      onClick={() => setMeetingMode(mode)}
                    >
                      <Icon size={18} />
                      <span><strong>{modeMeta[mode].label}</strong><small>{modeMeta[mode].help}</small></span>
                    </button>
                  );
                })}
              </div>
            </div>
          )}

          {meetingMode === 'phone' && (
            <label>Phone number<input name="phone" required autoComplete="tel" placeholder="+46 ..." /></label>
          )}
          <label>Anything I should know? <span className="optional">Optional</span>
            <textarea name="message" rows={3} placeholder="Context, topic, links..." />
          </label>
          {activeConfig.payment && paymentRequiredForSelection && (
            <div className={`payment-note ${activeConfig.payment.ready ? '' : 'payment-unavailable'}`}>
              <CreditCard size={17} />
              <div>
                <strong>{activeConfig.payment.ready ? 'Payment required to confirm' : 'Payments are temporarily unavailable'}</strong>
                <span>
                  {activeConfig.payment.ready
                    ? <>{formatMoneyMajor(activeConfig.payment.ratePerMinute, activeConfig.payment.currency)} per minute · {duration} min = <strong>{formatMoneyMajor(activeConfig.payment.ratePerMinute * duration, activeConfig.payment.currency)}</strong>. Your calendar invitation is sent only after Stripe confirms payment.</>
                    : <>This session requires payment, but its Stripe connection is not ready. Please contact the owner.</>}
                </span>
              </div>
            </div>
          )}
          {activeConfig.payment && !paymentRequiredForSelection && (
            <div className="payment-note free-session-note">
              <Check size={17} />
              <div><strong>No payment required</strong><span>{duration}-minute sessions are configured as free.</span></div>
            </div>
          )}
          <label className="honeypot" aria-hidden="true">Website<input name="website" tabIndex={-1} autoComplete="off" /></label>

          {error && <div className="error-banner" role="alert">{error}</div>}
          <button className="primary-button" type="submit" disabled={booking || Boolean(paymentRequiredForSelection && activeConfig.payment && !activeConfig.payment.ready)}>
            {booking
              ? <><Loader2 className="spin" size={18} /> {paymentRequiredForSelection ? 'Opening payment…' : 'Booking…'}</>
              : <>{paymentRequiredForSelection ? (activeConfig.payment?.ready ? 'Continue to secure payment' : 'Payment unavailable') : 'Book this slot'} <ArrowRight size={18} /></>}
          </button>
        </form>
      </div>
    );
  }

  function renderConfirmationView() {
    if (!confirmation) return null;
    return <ConfirmationView title={activeConfig.title} confirmation={confirmation} onBookAnother={bookAnotherTime} />;
  }

  function renderRightPanel() {
    if (confirmation) return renderConfirmationView();
    if (paymentReturnState) {
      return (
        <div className="panel-view payment-return-panel" key="payment-return">
          {paymentReturnState.kind === 'checking' ? <Loader2 className="spin" size={28} /> : paymentReturnState.kind === 'cancelled' ? <CreditCard size={28} /> : <Inbox size={28} />}
          <p className="eyebrow">PAYMENT</p>
          <h2>{paymentReturnState.kind === 'checking' ? 'Almost done.' : paymentReturnState.kind === 'cancelled' ? 'Payment cancelled.' : 'Booking not confirmed.'}</h2>
          <p className="muted">{paymentReturnState.message}</p>
          {paymentReturnState.kind !== 'checking' && <button className="text-button" type="button" onClick={() => setPaymentReturnState(null)}>Choose another time</button>}
        </div>
      );
    }
    if (!activeConfig.connected) {
      return (
        <div className="empty-state" key="disconnected">
          <MonitorUp size={28} />
          <h3>Calendar is not connected yet</h3>
          <p>The owner needs to connect Google Calendar before times can be booked.</p>
        </div>
      );
    }
    if (!selectedDate) {
      return (
        <div className="empty-state subtle-empty" key="empty">
          <ArrowLeft size={28} />
          <h3>Start with the calendar</h3>
          <p>Pick a date and available times will appear here.</p>
        </div>
      );
    }
    if (bookingStep === 'duration') return renderDurationView();
    if (bookingStep === 'details') return renderDetailsView();
    return renderTimesView();
  }

  return (
    <main className="booking-page">
      <header className="topbar">
        <span className="timezone-pill"><Clock3 size={14} /> {activeConfig.timezone}</span>
      </header>

      <section className="booking-shell">
        <aside className="intro-panel">
          <div>
            <p className="eyebrow">SCHEDULE A MEETING</p>
            <h1>{activeConfig.title}</h1>
            <p className="intro-copy">{activeConfig.subtitle}</p>
          </div>
          <div className="intro-bottom">
            <div className="intro-note">
              <CalendarDays size={18} />
              <div>
                <strong>Your calendar, not a form maze.</strong>
                <span>Choose the day first. Everything else follows.</span>
              </div>
            </div>
            <div className={`public-manage ${manageOpen ? 'open' : ''}`}>
              {!manageOpen ? (
                <button
                  className="public-manage-link"
                  type="button"
                  onClick={() => {
                    setManageOpen(true);
                    setManageState({ kind: 'idle', message: '' });
                  }}
                >Manage a booking</button>
              ) : !managedBooking ? (
                <form className="public-manage-form" onSubmit={submitManageLookup}>
                  <div className="public-manage-heading">
                    <strong>Manage booking</strong>
                    <button className="text-button" type="button" onClick={() => setManageOpen(false)}>Close</button>
                  </div>
                  <label>
                    Booking ID
                    <input
                      value={manageBookingId}
                      onChange={(event) => {
                        setManageBookingId(event.target.value.toUpperCase());
                        if (manageState.kind !== 'loading') setManageState({ kind: 'idle', message: '' });
                      }}
                      autoComplete="off"
                      spellCheck={false}
                      placeholder="SLT-7K4Q-9M2P-W8RX-3FJN"
                      aria-label="Booking ID"
                    />
                  </label>
                  <button className="secondary-button public-manage-submit" type="submit" disabled={manageState.kind === 'loading'}>
                    {manageState.kind === 'loading' ? <><Loader2 className="spin" size={15} /> Loading…</> : 'Manage booking'}
                  </button>
                  <small>Your booking ID is shown on the confirmation screen and in the calendar invitation.</small>
                  {manageState.kind === 'error' && <p className="public-manage-message error" role="alert">{manageState.message}</p>}
                </form>
              ) : (
                <div className="public-manage-form">
                  <div className="public-manage-heading">
                    <strong>Manage booking</strong>
                    <button className="text-button" type="button" onClick={() => {
                      setManagedBooking(null);
                      setManageAction('none');
                      setManageState({ kind: 'idle', message: '' });
                    }}>Another ID</button>
                  </div>
                  <div className="public-manage-summary">
                    <code>{managedBooking.bookingId}</code>
                    <strong>{managedBooking.name}</strong>
                    <span>{formatDateShort(managedBooking.start, managedBooking.timezone)} · {formatClock(managedBooking.start, managedBooking.timezone)} · {managedBooking.duration} min</span>
                    {managedBooking.status !== 'confirmed' && <span className="public-manage-status">{managedBooking.status === 'cancelled' ? 'Cancelled' : managedBooking.status}</span>}
                  </div>

                  {manageAction === 'none' && managedBooking.status === 'confirmed' && (
                    <div className="public-manage-actions">
                      <button type="button" className="secondary-button" disabled={!managedBooking.canReschedule} onClick={() => {
                        setManageAction('reschedule');
                        const date = dateKeyForInstant(managedBooking.start, managedBooking.timezone);
                        void loadManageAvailability(date);
                      }}>Reschedule</button>
                      <button type="button" className="danger-button" disabled={!managedBooking.canCancel} onClick={() => setManageAction('cancel')}>Cancel</button>
                    </div>
                  )}

                  {manageAction === 'cancel' && (
                    <div className="public-manage-action-panel">
                      <strong>Cancel this booking?</strong>
                      <small>{managedBooking.paymentStatus === 'paid' ? 'The Stripe payment will be refunded to the original payment method before the meeting is cancelled.' : 'The calendar invitation will be cancelled and the time will become available again.'}</small>
                      <div className="public-manage-actions">
                        <button type="button" className="secondary-button" onClick={() => setManageAction('none')} disabled={manageState.kind === 'loading'}>Keep booking</button>
                        <button type="button" className="danger-button" onClick={cancelManagedBooking} disabled={manageState.kind === 'loading'}>{manageState.kind === 'loading' ? 'Cancelling…' : managedBooking.paymentStatus === 'paid' ? 'Cancel & refund' : 'Confirm cancellation'}</button>
                      </div>
                    </div>
                  )}

                  {manageAction === 'reschedule' && (
                    <div className="public-manage-action-panel">
                      <label>New date<input type="date" value={manageDate} onChange={(event) => void loadManageAvailability(event.target.value)} /></label>
                      {manageSlotsLoading ? <div className="public-manage-loading"><Loader2 className="spin" size={14} /> Checking times…</div> : manageSlots.length ? (
                        <div className="public-manage-times">
                          {manageSlots.map((slot) => <button type="button" key={slot.start} className={manageStart === slot.start ? 'active' : ''} onClick={() => setManageStart(slot.start)}>{slot.time}</button>)}
                        </div>
                      ) : <small>No matching {managedBooking.duration}-minute times are open on this date.</small>}
                      <div className="public-manage-actions">
                        <button type="button" className="secondary-button" onClick={() => { setManageAction('none'); setManageStart(''); }} disabled={manageState.kind === 'loading'}>Back</button>
                        <button type="button" className="primary-button" onClick={rescheduleManagedBooking} disabled={!manageStart || manageState.kind === 'loading'}>{manageState.kind === 'loading' ? 'Rescheduling…' : 'Reschedule booking'}</button>
                      </div>
                    </div>
                  )}

                  {manageState.kind === 'success' && <p className="public-manage-message success" role="status">{manageState.message}</p>}
                  {manageState.kind === 'error' && <p className="public-manage-message error" role="alert">{manageState.message}</p>}
                </div>
              )}
            </div>
          </div>
        </aside>

        <section className="calendar-panel" aria-label="Choose a date">
          <div className="calendar-toolbar">
            <div>
              <p className="step-label">01 · PICK A DAY</p>
              <h2 aria-live="polite">{month.toLocaleDateString('en-US', { month: 'long', year: 'numeric' })}</h2>
            </div>
            <div className="month-buttons">
              <button type="button" aria-label="Previous month" onClick={() => setMonth(new Date(month.getFullYear(), month.getMonth() - 1, 1))}><ChevronLeft /></button>
              <button type="button" aria-label="Next month" onClick={() => setMonth(new Date(month.getFullYear(), month.getMonth() + 1, 1))}><ChevronRight /></button>
            </div>
          </div>

          <div className="weekday-grid" aria-hidden="true">
            {shortDayNames.map((name) => <span key={name}>{name}</span>)}
          </div>
          <div className="calendar-grid">
            {cells.map(({ date, current }) => {
              const key = dateKey(date);
              const selectable = isSelectable(key, date.getDay(), current);
              const selected = key === selectedDate;
              const isToday = key === today;
              return (
                <button
                  key={key}
                  type="button"
                  className={`day ${current ? '' : 'outside'} ${selected ? 'selected' : ''} ${isToday ? 'today' : ''}`}
                  disabled={!selectable}
                  aria-pressed={selected}
                  aria-label={dayAriaLabel(date)}
                  onClick={() => chooseDate(key)}
                >
                  <span>{date.getDate()}</span>
                  {isToday && <small>Today</small>}
                </button>
              );
            })}
          </div>
        </section>

        <aside className="selection-panel" aria-live="polite">{renderRightPanel()}</aside>
      </section>

      {error && !(selectedSlot && duration) && <div className="floating-error" role="alert">{error}</div>}
      <footer>
        <a className="powered-by" href="https://www.nobgit.com/user/imalexnord/slot" target="_blank" rel="noreferrer">Powered by Slot</a>
      </footer>
    </main>
  );
}

function ConfirmationView({ title, confirmation, onBookAnother }: { title: string; confirmation: Confirmation; onBookAnother: () => void }) {
  const [copied, copy] = useCopy();
  const tz = confirmation.timezone;

  return (
    <div className="panel-view confirmation-panel panel-view-scroll" key="confirmed">
      <span className="success-icon"><Check /></span>
      <p className="eyebrow">YOU'RE BOOKED</p>
      <h2>See you then.</h2>

      <dl className="confirmation-details">
        <div className="confirm-row"><dt>Date</dt><dd>{formatDateShort(confirmation.start, tz)}</dd></div>
        <div className="confirm-row"><dt>Time</dt><dd>{formatClock(confirmation.start, tz)} – {formatClock(confirmation.end, tz)}</dd></div>
        <div className="confirm-row"><dt>Length</dt><dd>{confirmation.duration} min</dd></div>
        {confirmation.bookingId && <div className="confirm-row booking-id-row"><dt>Booking ID</dt><dd>{confirmation.bookingId}</dd></div>}
        <div className="confirm-row"><dt>Timezone</dt><dd>{tz}</dd></div>
        <div className="confirm-row"><dt>Meeting</dt><dd>{modeMeta[confirmation.meetingMode].label}</dd></div>
      </dl>

      <p className="muted">Your booking is confirmed. A calendar invitation has been sent to your email.</p>

      <div className="confirm-actions">
        <a className="primary-button" href={googleCalendarUrl(title, confirmation)} target="_blank" rel="noreferrer">
          <CalendarPlus size={16} /> Add to calendar
        </a>
        {confirmation.receiptUrl && (
          <a className="secondary-button" href={confirmation.receiptUrl} target="_blank" rel="noreferrer">
            View Stripe receipt <ExternalLink size={15} />
          </a>
        )}
        {confirmation.meetUrl && (
          <>
            <a className="secondary-button" href={confirmation.meetUrl} target="_blank" rel="noreferrer">
              Open Google Meet <ExternalLink size={15} />
            </a>
            <button
              type="button"
              className={`copy-button ${copied ? 'copied' : ''}`}
              onClick={() => copy(confirmation.meetUrl!)}
            >
              {copied ? <><Check size={15} /> Link copied</> : <><Copy size={15} /> Copy Meet link</>}
            </button>
          </>
        )}
        <button className="text-button" type="button" onClick={onBookAnother}>Book another time</button>
      </div>
    </div>
  );
}


function RescheduleSuggestionPage() {
  const token = decodeURIComponent(window.location.pathname.split('/').filter(Boolean)[1] ?? '');
  const [proposal, setProposal] = useState<RescheduleProposal | null>(null);
  const [accepted, setAccepted] = useState<Confirmation | null>(null);
  const [loading, setLoading] = useState(true);
  const [accepting, setAccepting] = useState(false);
  const [error, setError] = useState('');

  useEffect(() => {
    api<RescheduleProposal>(`/api/reschedule-suggestions/${encodeURIComponent(token)}`)
      .then((data) => {
        setProposal(data);
        document.title = `Reschedule · ${data.title}`;
      })
      .catch((err) => setError(err instanceof Error ? err.message : 'Could not load this suggestion.'))
      .finally(() => setLoading(false));
  }, [token]);

  async function acceptSuggestion() {
    setAccepting(true);
    setError('');
    try {
      const result = await api<{ booking: Confirmation }>(`/api/reschedule-suggestions/${encodeURIComponent(token)}/accept`, {
        method: 'POST',
      });
      setAccepted(result.booking);
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Could not accept the new time.');
    } finally {
      setAccepting(false);
    }
  }

  if (loading) return <div className="center-screen"><Loader2 className="spin" /> Loading suggestion…</div>;
  if (accepted && proposal) {
    return (
      <main className="admin-login-shell">
        <section className="admin-login-card reschedule-public-card">
          <ConfirmationView title={proposal.title} confirmation={accepted} onBookAnother={() => { window.location.href = '/'; }} />
        </section>
      </main>
    );
  }
  if (!proposal) {
    return (
      <main className="admin-login-shell">
        <section className="admin-login-card">
          <span className="admin-login-icon"><CalendarDays /></span>
          <p className="eyebrow">RESCHEDULE</p>
          <h1>Suggestion unavailable.</h1>
          <p>{error || 'This reschedule link has expired or was already used.'}</p>
        </section>
      </main>
    );
  }

  return (
    <main className="admin-login-shell">
      <section className="admin-login-card reschedule-public-card">
        <span className="admin-login-icon"><CalendarDays /></span>
        <p className="eyebrow">NEW TIME SUGGESTED</p>
        <h1>Does this time work?</h1>
        <p>Hi {proposal.name}. The organizer suggested moving your meeting. Your current time stays booked until you accept.</p>
        <div className="schedule-compare">
          <div>
            <span>Current</span>
            <strong>{formatDateShort(proposal.currentStart, proposal.timezone)}</strong>
            <small>{formatClock(proposal.currentStart, proposal.timezone)} – {formatClock(proposal.currentEnd, proposal.timezone)}</small>
          </div>
          <ArrowRight size={18} />
          <div className="proposed">
            <span>Suggested</span>
            <strong>{formatDateShort(proposal.proposedStart, proposal.timezone)}</strong>
            <small>{formatClock(proposal.proposedStart, proposal.timezone)} – {formatClock(proposal.proposedEnd, proposal.timezone)}</small>
          </div>
        </div>
        <p className="muted">{proposal.duration} min · {proposal.timezone} · {modeMeta[proposal.meetingMode].label}</p>
        {error && <div className="error-banner" role="alert">{error}</div>}
        <button className="primary-button inline-button" type="button" onClick={acceptSuggestion} disabled={accepting}>
          {accepting ? <><Loader2 className="spin" size={17} /> Rescheduling…</> : <>Accept new time <ArrowRight size={17} /></>}
        </button>
      </section>
    </main>
  );
}

function BookingRowItem({
  booking,
  timezone,
  onOpen,
}: {
  booking: BookingRow;
  timezone: string;
  onOpen: (booking: BookingRow) => void;
}) {
  const [copied, copy] = useCopy();
  const isCancelled = booking.status === 'cancelled';
  const isPaymentHold = booking.status === 'pending' && booking.payment_status !== 'paid';
  const isPast = Date.parse(booking.end_time) < Date.now() || isCancelled;
  const Icon = modeMeta[booking.meeting_mode].icon;
  return (
    <div className={`booking-list-row ${isPast ? 'past' : ''} ${isCancelled ? 'cancelled' : ''}`}>
      <div className="booking-avatar" aria-hidden="true">{booking.name.slice(0, 1).toUpperCase()}</div>
      <div className="booking-person">
        <strong>{booking.name}</strong>
        <span>{booking.email}</span>
      </div>
      <div className="booking-when">
        <strong>{formatDateShort(booking.start_time, timezone)}</strong>
        <span>{formatClock(booking.start_time, timezone)} – {formatClock(booking.end_time, timezone)}</span>
      </div>
      <div className="booking-meta">
        <span className="booking-method"><Icon size={13} /> {modeMeta[booking.meeting_mode].label}</span>
        <span>{booking.duration_minutes} min{booking.payment_status === 'paid' ? booking.refund_status === 'succeeded' ? ' · Refunded' : booking.refund_status === 'pending' ? ' · Refund pending' : ' · Paid' : isPaymentHold ? ` · Awaiting payment · ${checkoutSessionMode(booking.stripe_checkout_session_id).toUpperCase()}` : ''}</span>
      </div>
      <div className="booking-actions">
        <span className={`status-pill ${isCancelled ? 'cancelled' : isPast ? 'past' : 'upcoming'}`}>
          {isCancelled ? 'Cancelled' : isPaymentHold ? 'Payment hold' : isPast ? 'Past' : 'Upcoming'}
        </span>
        <button type="button" className="icon-button" aria-label="Open booking details" title="Open details" onClick={() => onOpen(booking)}>
          <Inbox size={15} />
        </button>
        {booking.status === 'confirmed' && booking.meet_url && (
          <>
            <button
              type="button"
              className={`icon-button ${copied ? 'copied' : ''}`}
              aria-label="Copy Meet link"
              title="Copy Meet link"
              onClick={() => copy(booking.meet_url!)}
            >
              {copied ? <Check size={15} /> : <Copy size={15} />}
            </button>
            <a className="icon-button" href={booking.meet_url} target="_blank" rel="noreferrer" aria-label="Open Google Meet" title="Open Google Meet">
              <Video size={15} />
            </a>
          </>
        )}
      </div>
    </div>
  );
}

function Admin() {
  const [session, setSession] = useState<AdminSession | null>(null);
  const [settings, setSettings] = useState<AppSettings | null>(null);
  const [bookings, setBookings] = useState<BookingRow[]>([]);
  const [recurringUnavailable, setRecurringUnavailable] = useState<RecurringUnavailableRule[]>([]);
  const [availabilityBlocks, setAvailabilityBlocks] = useState<AvailabilityBlock[]>([]);
  const [activity, setActivity] = useState<ActivityData | null>(null);
  const [activityLoading, setActivityLoading] = useState(false);
  const [attendeeData, setAttendeeData] = useState<AttendeeData | null>(null);
  const [attendeesLoading, setAttendeesLoading] = useState(false);
  const [attendeeSearch, setAttendeeSearch] = useState('');
  const [selectedAttendee, setSelectedAttendee] = useState<AttendeeRow | null>(null);
  const [attendeeBookings, setAttendeeBookings] = useState<BookingRow[]>([]);
  const [attendeeBookingsLoading, setAttendeeBookingsLoading] = useState(false);
  const [tab, setTab] = useState<'settings' | 'bookings' | 'activity' | 'attendees'>('bookings');
  const [bookingFilter, setBookingFilter] = useState<'upcoming' | 'past'>('upcoming');
  const [bookingSearch, setBookingSearch] = useState('');
  const [bookingSearchResults, setBookingSearchResults] = useState<BookingRow[] | null>(null);
  const [bookingSearchLoading, setBookingSearchLoading] = useState(false);
  const [selectedBooking, setSelectedBooking] = useState<BookingRow | null>(null);
  const [cancelTarget, setCancelTarget] = useState<BookingRow | null>(null);
  const [cancelling, setCancelling] = useState(false);
  const [scheduleTarget, setScheduleTarget] = useState<BookingRow | null>(null);
  const [scheduleAction, setScheduleAction] = useState<BookingScheduleAction>('reschedule');
  const [scheduleDate, setScheduleDate] = useState('');
  const [scheduleSlots, setScheduleSlots] = useState<Slot[]>([]);
  const [scheduleStart, setScheduleStart] = useState('');
  const [loadingScheduleSlots, setLoadingScheduleSlots] = useState(false);
  const [savingScheduleAction, setSavingScheduleAction] = useState(false);
  const [suggestionUrl, setSuggestionUrl] = useState('');
  const [scheduleLinkCopied, copyScheduleLink] = useCopy();
  const [recurringForm, setRecurringForm] = useState({ weekdays: [1, 2, 3, 4, 5], start: '12:00', end: '13:00', label: 'Lunch' });
  const [blockForm, setBlockForm] = useState({ date: '', start: '09:00', end: '12:00', allDay: false, label: '' });
  const [theme, setTheme] = useState<ThemeId>(() => {
    const stored = localStorage.getItem('meet-theme') as ThemeId | null;
    return stored && themes.some((item) => item.id === stored) ? stored : 'true-black';
  });
  const [saving, setSaving] = useState(false);
  const [notice, setNotice] = useState('');
  const [error, setError] = useState('');
  const [stripeConfigured, setStripeConfigured] = useState(false);
  const [stripeMode, setStripeMode] = useState<'test' | 'live' | 'unknown'>('unknown');

  useEffect(() => {
    document.title = 'Bookings';
  }, []);

  useEffect(() => {
    document.documentElement.dataset.theme = theme;
    localStorage.setItem('meet-theme', theme);
  }, [theme]);

  useEffect(() => {
    fetch('/api/admin/session')
      .then(async (response) => {
        if (!response.ok) throw new Error('unauthenticated');
        return response.json() as Promise<AdminSession>;
      })
      .then((data) => setSession(data))
      .catch(() => setSession({ authenticated: false, email: '', connected: false }));
  }, []);

  useEffect(() => {
    if (!session?.authenticated) return;
    Promise.all([
      api<{ settings: AppSettings; stripeConfigured: boolean; stripeMode: 'test' | 'live' | 'unknown' }>('/api/admin/settings'),
      api<{ bookings: BookingRow[] }>('/api/admin/bookings'),
      api<{ recurring: RecurringUnavailableRule[]; blocks: AvailabilityBlock[] }>('/api/admin/availability-rules'),
    ]).then(([settingsData, bookingData, availabilityData]) => {
      setSettings(settingsData.settings);
      setStripeConfigured(settingsData.stripeConfigured);
      setStripeMode(settingsData.stripeMode);
      setBookings(bookingData.bookings);
      setRecurringUnavailable(availabilityData.recurring);
      setAvailabilityBlocks(availabilityData.blocks);
    }).catch((err) => setError(err.message));
  }, [session]);


  useEffect(() => {
    if (!session?.authenticated || tab !== 'activity') return;
    void loadActivity();
  }, [session?.authenticated, tab]);

  useEffect(() => {
    if (!session?.authenticated || tab !== 'attendees') return;
    void loadAttendees();
  }, [session?.authenticated, tab]);

  useEffect(() => {
    if (!session?.authenticated || tab !== 'bookings') return;
    const query = bookingSearch.trim();
    if (!query) {
      setBookingSearchResults(null);
      setBookingSearchLoading(false);
      return;
    }
    let cancelled = false;
    setError('');
    setBookingSearchResults(null);
    const timer = window.setTimeout(() => {
      setBookingSearchLoading(true);
      api<{ bookings: BookingRow[] }>(`/api/admin/bookings?q=${encodeURIComponent(query)}`)
        .then((data) => { if (!cancelled) setBookingSearchResults(data.bookings); })
        .catch((err) => { if (!cancelled) setError(err instanceof Error ? err.message : 'Could not search bookings.'); })
        .finally(() => { if (!cancelled) setBookingSearchLoading(false); });
    }, 180);
    return () => { cancelled = true; window.clearTimeout(timer); };
  }, [session?.authenticated, tab, bookingSearch]);

  useEffect(() => {
    if (!scheduleTarget || !scheduleDate) {
      setScheduleSlots([]);
      return;
    }
    let cancelled = false;
    setLoadingScheduleSlots(true);
    setScheduleStart('');
    api<{ slots: Slot[] }>(`/api/admin/bookings/${scheduleTarget.id}/availability?date=${encodeURIComponent(scheduleDate)}`)
      .then((data) => {
        if (!cancelled) setScheduleSlots(data.slots);
      })
      .catch((err) => {
        if (!cancelled) {
          setScheduleSlots([]);
          setError(err instanceof Error ? err.message : 'Could not load available times.');
        }
      })
      .finally(() => {
        if (!cancelled) setLoadingScheduleSlots(false);
      });
    return () => { cancelled = true; };
  }, [scheduleTarget?.id, scheduleDate]);

  function openScheduleDialog(booking: BookingRow, action: BookingScheduleAction) {
    if (!settings) return;
    setScheduleTarget(booking);
    setScheduleAction(action);
    setScheduleDate(dateKeyForInstant(booking.start_time, settings.timezone));
    setScheduleStart('');
    setScheduleSlots([]);
    setSuggestionUrl('');
    setError('');
  }

  async function submitScheduleAction() {
    if (!scheduleTarget || !scheduleStart) return;
    setSavingScheduleAction(true);
    setError('');
    try {
      if (scheduleAction === 'reschedule') {
        await api(`/api/admin/bookings/${scheduleTarget.id}/reschedule`, {
          method: 'POST',
          body: JSON.stringify({ start: scheduleStart }),
        });
        await refreshAdminData();
        setScheduleTarget(null);
        setSelectedBooking(null);
        setNotice('Meeting rescheduled. Google Calendar sent the attendee an updated invitation.');
      } else {
        const result = await api<{ suggestionUrl: string }>(`/api/admin/bookings/${scheduleTarget.id}/suggest`, {
          method: 'POST',
          body: JSON.stringify({ start: scheduleStart }),
        });
        await refreshAdminData();
        setSuggestionUrl(result.suggestionUrl);
        setNotice('New time suggested. Google Calendar notified the attendee and the current meeting remains booked until they accept.');
      }
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Could not update the meeting.');
    } finally {
      setSavingScheduleAction(false);
    }
  }

  async function loadActivity() {
    setActivityLoading(true);
    setError('');
    try {
      setActivity(await api<ActivityData>('/api/admin/activity'));
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Could not load activity.');
    } finally {
      setActivityLoading(false);
    }
  }

  async function loadAttendees() {
    setAttendeesLoading(true);
    setError('');
    try {
      setAttendeeData(await api<AttendeeData>('/api/admin/attendees'));
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Could not load attendees.');
    } finally {
      setAttendeesLoading(false);
    }
  }

  async function openAttendee(attendee: AttendeeRow) {
    setSelectedAttendee(attendee);
    setAttendeeBookings([]);
    setAttendeeBookingsLoading(true);
    setError('');
    try {
      const data = await api<{ bookings: BookingRow[] }>(`/api/admin/attendee-bookings?email=${encodeURIComponent(attendee.email)}`);
      setAttendeeBookings(data.bookings);
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Could not load attendee history.');
    } finally {
      setAttendeeBookingsLoading(false);
    }
  }

  function openBookingFromAttendee(booking: BookingRow) {
    setTab('bookings');
    setBookingSearch(booking.public_booking_id || booking.id);
    setSelectedBooking(booking);
  }

  async function refreshAdminData() {
    const query = bookingSearch.trim();
    const [bookingData, availabilityData, searchedData] = await Promise.all([
      api<{ bookings: BookingRow[] }>('/api/admin/bookings'),
      api<{ recurring: RecurringUnavailableRule[]; blocks: AvailabilityBlock[] }>('/api/admin/availability-rules'),
      query ? api<{ bookings: BookingRow[] }>(`/api/admin/bookings?q=${encodeURIComponent(query)}`) : Promise.resolve(null),
    ]);
    setBookings(bookingData.bookings);
    if (searchedData) setBookingSearchResults(searchedData.bookings);
    setRecurringUnavailable(availabilityData.recurring);
    setAvailabilityBlocks(availabilityData.blocks);
    if (attendeeData) void loadAttendees();
  }

  async function save() {
    if (!settings) return;
    setSaving(true);
    setError('');
    setNotice('');
    try {
      const data = await api<{ settings: AppSettings }>('/api/admin/settings', {
        method: 'PUT',
        body: JSON.stringify(settings),
      });
      setSettings(data.settings);
      setNotice('Saved. The public calendar is using these settings now.');
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Could not save settings');
    } finally {
      setSaving(false);
    }
  }

  async function addRecurringUnavailable() {
    setError('');
    try {
      await api('/api/admin/availability-rules/recurring', {
        method: 'POST',
        body: JSON.stringify(recurringForm),
      });
      await refreshAdminData();
      setNotice('Recurring unavailable time added.');
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Could not add unavailable time');
    }
  }

  async function addAvailabilityBlock() {
    setError('');
    try {
      await api('/api/admin/availability-rules/blocks', {
        method: 'POST',
        body: JSON.stringify(blockForm),
      });
      await refreshAdminData();
      setBlockForm({ date: '', start: '09:00', end: '12:00', allDay: false, label: '' });
      setNotice('Blocked time added.');
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Could not add blocked time');
    }
  }

  async function deleteAvailabilityRule(kind: 'recurring' | 'block', id: string) {
    setError('');
    try {
      await api(`/api/admin/availability-rules/${kind}/${id}`, { method: 'DELETE' });
      await refreshAdminData();
      setNotice('Availability rule removed.');
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Could not remove availability rule');
    }
  }

  async function confirmCancelBooking() {
    if (!cancelTarget) return;
    const releasingPaymentHold = cancelTarget.status === 'pending';
    setCancelling(true);
    setError('');
    try {
      const result = await api<{ warning?: string; refunded?: boolean; refundStatus?: string }>(`/api/admin/bookings/${cancelTarget.id}/cancel`, {
        method: 'POST',
        body: JSON.stringify({ allowStaleStripeRelease: releasingPaymentHold }),
      });
      setCancelTarget(null);
      setSelectedBooking(null);
      await refreshAdminData();
      setNotice(releasingPaymentHold
        ? result.warning
          ? `Payment hold released. The time is available again. ${result.warning}`
          : 'Payment hold released. The time is available again.'
        : result.refunded
          ? `Meeting cancelled and Stripe refund ${result.refundStatus === 'pending' ? 'started' : 'issued'}.${result.warning ? ` ${result.warning}` : ''}`
          : `Meeting cancelled. Google Calendar will notify the attendee.${result.warning ? ` ${result.warning}` : ''}`);
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Could not cancel meeting');
    } finally {
      setCancelling(false);
    }
  }

  if (session === null) return <div className="center-screen"><Loader2 className="spin" /> Loading admin…</div>;

  if (!session.authenticated) {
    const params = new URLSearchParams(window.location.search);
    const oauthError = params.get('oauth_error');
    return (
      <main className="admin-login-shell">
        <section className="admin-login-card">
          <span className="admin-login-icon"><Settings2 /></span>
          <p className="eyebrow">PRIVATE</p>
          <h1>Control your availability.</h1>
          <p>Google login is only for the calendar owner. People booking you never need an account.</p>
          {oauthError && <div className="error-banner" role="alert">{oauthError}</div>}
          <a className="primary-button inline-button" href="/auth/google/start">Continue with Google <ArrowRight size={18} /></a>
          <a className="text-button" href="/">Back to booking page</a>
        </section>
      </main>
    );
  }

  if (!settings) return <div className="center-screen"><Loader2 className="spin" /> Loading settings…</div>;

  function setDay(day: number, patch: Partial<{ enabled: boolean; start: string; end: string }>) {
    setSettings((current) => {
      if (!current) return current;
      const existing = current.availability[String(day)]?.[0] ?? { start: '09:00', end: '17:00' };
      const enabled = patch.enabled ?? Boolean(current.availability[String(day)]?.length);
      return {
        ...current,
        availability: {
          ...current.availability,
          [String(day)]: enabled ? [{ start: patch.start ?? existing.start, end: patch.end ?? existing.end }] : [],
        },
      };
    });
  }

  const activeSettings = settings;
  const now = Date.now();
  const bookingSource = bookingSearch.trim() ? (bookingSearchResults ?? []) : bookings;
  const upcoming = bookingSource.filter((b) => b.status !== 'cancelled' && Date.parse(b.end_time) >= now);
  const past = bookingSource.filter((b) => b.status === 'cancelled' || Date.parse(b.end_time) < now);
  const shown = bookingFilter === 'upcoming' ? upcoming : past;
  const attendeeNeedle = attendeeSearch.trim().toLowerCase();
  const shownAttendees = (attendeeData?.attendees ?? []).filter((attendee) => !attendeeNeedle
    || attendee.name.toLowerCase().includes(attendeeNeedle)
    || attendee.email.toLowerCase().includes(attendeeNeedle));

  return (
    <main className="admin-page">
      <header className="admin-topbar">
        <div className="admin-user">
          <span>{session.email}</span>
          <button onClick={async () => { await api('/api/admin/logout', { method: 'POST' }); window.location.reload(); }}><LogOut size={16} /> Log out</button>
        </div>
      </header>

      <div className="admin-layout">
        <aside className="admin-nav">
          <button className={tab === 'bookings' ? 'active' : ''} onClick={() => setTab('bookings')}><CalendarDays size={17} /> Bookings</button>
          <button className={tab === 'settings' ? 'active' : ''} onClick={() => setTab('settings')}><Settings2 size={17} /> Settings</button>
          <button className={tab === 'activity' ? 'active' : ''} onClick={() => setTab('activity')}><Activity size={17} /> Activity</button>
          <button className={tab === 'attendees' ? 'active' : ''} onClick={() => setTab('attendees')}><Users size={17} /> Attendees</button>
          <a href="/" target="_blank"><ExternalLink size={17} /> Open booking page</a>
        </aside>

        <section className="admin-content">
          {tab === 'settings' ? (
            <>
              <div className="admin-heading">
                <div><p className="eyebrow">SETTINGS</p><h1>Keep it simple.</h1></div>
                <button className="primary-button compact" onClick={save} disabled={saving}>{saving ? <Loader2 className="spin" /> : <Check />} Save changes</button>
              </div>
              {notice && <div className="success-banner" role="status">{notice}</div>}
              {error && <div className="error-banner" role="alert">{error}</div>}

              <div className="settings-grid">
                <section className="settings-card wide-card appearance-card">
                  <div className="appearance-heading">
                    <div>
                      <p className="eyebrow">APPEARANCE</p>
                      <h2>Theme and contrast</h2>
                      <p>Choose how this app looks on this browser. Your selection is saved locally and applied before each page is shown.</p>
                    </div>
                    <span>{themes.find((item) => item.id === theme)?.name} selected</span>
                  </div>
                  <div className="theme-grid">
                    {themes.map((item) => (
                      <button
                        type="button"
                        key={item.id}
                        className={`theme-option ${theme === item.id ? 'active' : ''}`}
                        aria-pressed={theme === item.id}
                        onClick={() => setTheme(item.id)}
                      >
                        <span className={`theme-preview theme-preview-${item.id}`} aria-hidden="true">
                          <span />
                        </span>
                        <span className="theme-copy">
                          <strong>{item.name}</strong>
                          <small>{item.description}</small>
                          <em>{item.contrast}</em>
                        </span>
                      </button>
                    ))}
                  </div>
                  <p className="theme-footnote">Theme settings stay on this device and do not change your booking page for other visitors.</p>
                </section>

                <section className="settings-card wide-card">
                  <div className="card-heading"><h2>Availability</h2><p>Your Google Calendar blocks busy time inside these hours.</p></div>
                  <div className="availability-list">
                    {dayNames.map((name, day) => {
                      const interval = activeSettings.availability[String(day)]?.[0];
                      const enabled = Boolean(interval);
                      return (
                        <div className="availability-row" key={name}>
                          <label className="switch-row"><input type="checkbox" checked={enabled} onChange={(e) => setDay(day, { enabled: e.target.checked })} /><span>{name}</span></label>
                          {enabled ? (
                            <div className="time-range">
                              <input type="time" aria-label={`${name} start`} value={interval.start} onChange={(e) => setDay(day, { start: e.target.value })} />
                              <span>to</span>
                              <input type="time" aria-label={`${name} end`} value={interval.end} onChange={(e) => setDay(day, { end: e.target.value })} />
                            </div>
                          ) : <span className="unavailable">Unavailable</span>}
                        </div>
                      );
                    })}
                  </div>
                  <div className="availability-tools">
                    <section className="availability-tool">
                      <div className="card-heading"><h3>Recurring unavailable</h3><p>Private blocks such as lunch or focus time.</p></div>
                      <div className="weekday-picker">
                        {dayNames.map((name, day) => (
                          <button
                            type="button"
                            key={name}
                            className={recurringForm.weekdays.includes(day) ? 'active' : ''}
                            onClick={() => setRecurringForm((current) => ({
                              ...current,
                              weekdays: current.weekdays.includes(day)
                                ? current.weekdays.filter((value) => value !== day)
                                : [...current.weekdays, day].sort((a, b) => a - b),
                            }))}
                          >
                            {name.slice(0, 3)}
                          </button>
                        ))}
                      </div>
                      <div className="calendar-settings-row">
                        <label>From<input type="time" value={recurringForm.start} onChange={(e) => setRecurringForm({ ...recurringForm, start: e.target.value })} /></label>
                        <label>Until<input type="time" value={recurringForm.end} onChange={(e) => setRecurringForm({ ...recurringForm, end: e.target.value })} /></label>
                      </div>
                      <label>Private label<input value={recurringForm.label} onChange={(e) => setRecurringForm({ ...recurringForm, label: e.target.value })} placeholder="Lunch" /></label>
                      <button className="secondary-button" type="button" onClick={addRecurringUnavailable}>Add unavailable period</button>
                      <div className="rule-list">
                        {recurringUnavailable.length === 0 ? <span>No recurring unavailable periods.</span> : recurringUnavailable.map((rule) => (
                          <div className="rule-row" key={rule.id}>
                            <strong>{rule.weekdays.map((day) => dayNames[day].slice(0, 3)).join(', ')}</strong>
                            <span>{rule.start}-{rule.end}{rule.label ? ` · ${rule.label}` : ''}</span>
                            <button type="button" onClick={() => deleteAvailabilityRule('recurring', rule.id)}>Remove</button>
                          </div>
                        ))}
                      </div>
                    </section>

                    <section className="availability-tool">
                      <div className="card-heading"><h3>One-off blocked time</h3><p>Private exceptions for a single date.</p></div>
                      <label>Date<input type="date" value={blockForm.date} onChange={(e) => setBlockForm({ ...blockForm, date: e.target.value })} /></label>
                      <label className="switch-row"><input type="checkbox" checked={blockForm.allDay} onChange={(e) => setBlockForm({ ...blockForm, allDay: e.target.checked })} /><span>All day</span></label>
                      {!blockForm.allDay && (
                        <div className="calendar-settings-row">
                          <label>From<input type="time" value={blockForm.start} onChange={(e) => setBlockForm({ ...blockForm, start: e.target.value })} /></label>
                          <label>Until<input type="time" value={blockForm.end} onChange={(e) => setBlockForm({ ...blockForm, end: e.target.value })} /></label>
                        </div>
                      )}
                      <label>Private label<input value={blockForm.label} onChange={(e) => setBlockForm({ ...blockForm, label: e.target.value })} placeholder="Dentist" /></label>
                      <button className="secondary-button" type="button" onClick={addAvailabilityBlock}>Block time</button>
                      <div className="rule-list">
                        {availabilityBlocks.length === 0 ? <span>No one-off blocks.</span> : availabilityBlocks.map((block) => (
                          <div className="rule-row" key={block.id}>
                            <strong>{block.date}</strong>
                            <span>{block.allDay ? 'All day' : `${block.start}-${block.end}`}{block.label ? ` · ${block.label}` : ''}</span>
                            <button type="button" onClick={() => deleteAvailabilityRule('block', block.id)}>Remove</button>
                          </div>
                        ))}
                      </div>
                    </section>
                  </div>
                </section>

                <section className="settings-card">
                  <div className="card-heading"><h2>Page</h2><p>What visitors see first.</p></div>
                  <label>Title<input value={activeSettings.title} onChange={(e) => setSettings({ ...activeSettings, title: e.target.value })} /></label>
                  <label>Subtitle<textarea rows={3} value={activeSettings.subtitle} onChange={(e) => setSettings({ ...activeSettings, subtitle: e.target.value })} /></label>
                  <label>Timezone
                    <div className="timezone-control">
                      <select value={activeSettings.timezone} onChange={(e) => setSettings({ ...activeSettings, timezone: e.target.value })}>
                        {!timezoneOptions.includes(activeSettings.timezone) && <option value={activeSettings.timezone}>{activeSettings.timezone}</option>}
                        {timezoneOptions.map((zone) => <option key={zone} value={zone}>{zone}</option>)}
                      </select>
                      <button
                        type="button"
                        onClick={() => setSettings({ ...activeSettings, timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || activeSettings.timezone })}
                      >
                        Use this browser
                      </button>
                    </div>
                  </label>
                </section>

                <section className="settings-card">
                  <div className="card-heading"><h2>Booking rules</h2><p>Enough control, no dashboard archaeology.</p></div>
                  <div className="two-col-fields">
                    <label>Minimum notice<input type="number" min="0" value={activeSettings.minimumNoticeMinutes} onChange={(e) => setSettings({ ...activeSettings, minimumNoticeMinutes: Number(e.target.value) })} /><small>minutes</small></label>
                    <label>Book ahead<input type="number" min="1" max="365" value={activeSettings.bookingWindowDays} onChange={(e) => setSettings({ ...activeSettings, bookingWindowDays: Number(e.target.value) })} /><small>days</small></label>
                    <label>Buffer before<input type="number" min="0" value={activeSettings.bufferBeforeMinutes} onChange={(e) => setSettings({ ...activeSettings, bufferBeforeMinutes: Number(e.target.value) })} /><small>minutes</small></label>
                    <label>Buffer after<input type="number" min="0" value={activeSettings.bufferAfterMinutes} onChange={(e) => setSettings({ ...activeSettings, bufferAfterMinutes: Number(e.target.value) })} /><small>minutes</small></label>
                  </div>
                  <span className="field-label">Durations</span>
                  <div className="checkbox-chips">
                    {[15, 30, 45, 60, 90, 120].map((minutes) => (
                      <label key={minutes} className={activeSettings.durations.includes(minutes) ? 'active' : ''}>
                        <input
                          type="checkbox"
                          checked={activeSettings.durations.includes(minutes)}
                          onChange={(e) => setSettings({
                            ...activeSettings,
                            durations: e.target.checked
                              ? [...activeSettings.durations, minutes].sort((a, b) => a - b)
                              : activeSettings.durations.filter((value) => value !== minutes),
                            paymentFreeDurations: e.target.checked
                              ? activeSettings.paymentFreeDurations
                              : activeSettings.paymentFreeDurations.filter((value) => value !== minutes),
                          })}
                        />
                        {minutes} min
                      </label>
                    ))}
                  </div>
                </section>

                <section className="settings-card">
                  <div className="card-heading"><h2>Stripe payments</h2><p>Payment happens before the booking is confirmed or the invitation is sent.</p></div>
                  <div className="stripe-status-row">
                    <div className={`connection-badge ${stripeConfigured ? 'connected' : ''}`}>
                      <span className="status-dot" />
                      {stripeConfigured ? 'Stripe secrets configured' : 'Stripe secrets missing'}
                    </div>
                    {stripeConfigured && (
                      <span className={`stripe-mode-badge ${stripeMode}`}>
                        {stripeMode === 'test' ? 'TEST MODE' : stripeMode === 'live' ? 'LIVE MODE' : 'MODE UNKNOWN'}
                      </span>
                    )}
                  </div>
                  <div className="checkbox-chips">
                    <label className={activeSettings.paymentEnabled ? 'active' : ''}>
                      <input
                        type="checkbox"
                        checked={activeSettings.paymentEnabled}
                        onChange={(e) => setSettings({ ...activeSettings, paymentEnabled: e.target.checked })}
                      />
                      Enable Stripe payments
                    </label>
                  </div>
                  <div className="two-col-fields">
                    <label>Currency<input maxLength={3} value={activeSettings.paymentCurrency} onChange={(e) => setSettings({ ...activeSettings, paymentCurrency: e.target.value.toLowerCase() })} /><small>ISO code</small></label>
                    <label>Price per minute<input type="number" min="0" step="0.01" value={activeSettings.paymentRatePerMinute} onChange={(e) => setSettings({ ...activeSettings, paymentRatePerMinute: Number(e.target.value) })} /><small>{activeSettings.paymentCurrency.toUpperCase()} / minute</small></label>
                  </div>
                  <span className="field-label">Free durations <span className="optional">Optional</span></span>
                  <div className="checkbox-chips">
                    {activeSettings.durations.map((minutes) => (
                      <label key={minutes} className={activeSettings.paymentFreeDurations.includes(minutes) ? 'active' : ''}>
                        <input
                          type="checkbox"
                          checked={activeSettings.paymentFreeDurations.includes(minutes)}
                          onChange={(e) => setSettings({
                            ...activeSettings,
                            paymentFreeDurations: e.target.checked
                              ? [...activeSettings.paymentFreeDurations, minutes].sort((a, b) => a - b)
                              : activeSettings.paymentFreeDurations.filter((value) => value !== minutes),
                          })}
                        />
                        {minutes} min free
                      </label>
                    ))}
                  </div>
                  <p className="settings-note">Stripe can stay enabled while selected meeting lengths remain free. For example, make 15 minutes free and charge normally for 30, 45, and 60 minutes.</p>
                  <label>Checkout label<input value={activeSettings.paymentLabel} onChange={(e) => setSettings({ ...activeSettings, paymentLabel: e.target.value })} placeholder="Booking payment" /></label>
                  <p className="theme-footnote">
                    Enter normal currency, not Stripe minor units. Example: <strong>50</strong> with SEK means <strong>SEK 50 per minute</strong>, so a 15-minute booking costs <strong>{formatMoneyMajor(50 * 15, 'sek')}</strong>.
                  </p>
                  {activeSettings.paymentEnabled && !stripeConfigured && <div className="error-banner" role="alert">Stripe secrets are missing. Durations marked free can still be booked, but paid durations will be unavailable.</div>}
                  {stripeConfigured && stripeMode === 'unknown' && <div className="error-banner" role="alert">The Stripe secret key is configured, but its mode could not be identified. Slot expects a key beginning with sk_test_ or sk_live_.</div>}
                  {stripeConfigured && stripeMode === 'test' && <p className="settings-note">Stripe test mode only emails test receipts to verified Stripe account team-member addresses. Every successful payment still gets a hosted Stripe receipt link on Slot's confirmation screen.</p>}
                </section>

                <section className="settings-card wide-card">
                  <div className="card-heading"><h2>Google Calendar</h2><p>Busy time is read from Google. New bookings are created on your chosen calendar.</p></div>
                  <div className="google-status-row">
                    <div className={`connection-badge ${session.connected ? 'connected' : ''}`}>
                      <span className="status-dot" />
                      {session.connected ? 'Google connected' : 'Google disconnected'}
                    </div>
                    <div className="connection-actions">
                      <a href={session.connected ? '/auth/google/start?force=1' : '/auth/google/start'}>{session.connected ? 'Reconnect' : 'Connect Google'}</a>
                      {session.connected && (
                        <button type="button" onClick={async () => {
                          await api('/api/admin/disconnect-google', { method: 'POST' });
                          setSession({ ...session, connected: false });
                        }}>Disconnect</button>
                      )}
                    </div>
                  </div>
                  <div className="calendar-settings-row">
                    <label>Create events in<input value={activeSettings.calendarId} onChange={(e) => setSettings({ ...activeSettings, calendarId: e.target.value })} placeholder="primary" /></label>
                    <label>Calendars that block time<input value={activeSettings.busyCalendarIds.join(', ')} onChange={(e) => setSettings({ ...activeSettings, busyCalendarIds: e.target.value.split(',').map((v) => v.trim()).filter(Boolean) })} placeholder="primary" /></label>
                  </div>
                  <label>Default meeting type
                    <select value={activeSettings.defaultMeetingMode} onChange={(e) => setSettings({ ...activeSettings, defaultMeetingMode: e.target.value as MeetingMode })}>
                      {activeSettings.allowMeetingModes.map((mode) => <option value={mode} key={mode}>{modeMeta[mode].label}</option>)}
                    </select>
                  </label>
                  <span className="field-label">Allowed meeting types</span>
                  <div className="mode-admin-grid">
                    {(Object.keys(modeMeta) as MeetingMode[]).map((mode) => {
                      const Icon = modeMeta[mode].icon;
                      const active = activeSettings.allowMeetingModes.includes(mode);
                      return (
                        <label key={mode} className={active ? 'active' : ''}>
                          <input type="checkbox" checked={active} disabled={active && activeSettings.allowMeetingModes.length === 1} onChange={(e) => setSettings({
                            ...activeSettings,
                            allowMeetingModes: e.target.checked
                              ? [...activeSettings.allowMeetingModes, mode]
                              : activeSettings.allowMeetingModes.filter((value) => value !== mode),
                            defaultMeetingMode: !e.target.checked && activeSettings.defaultMeetingMode === mode
                              ? (activeSettings.allowMeetingModes.find((value) => value !== mode) ?? activeSettings.defaultMeetingMode)
                              : activeSettings.defaultMeetingMode,
                          })} />
                          <Icon size={18} /><span>{modeMeta[mode].label}</span>
                        </label>
                      );
                    })}
                  </div>
                  {activeSettings.allowMeetingModes.includes('in_person') && (
                    <label>In-person location<input value={activeSettings.inPersonLocation} onChange={(e) => setSettings({ ...activeSettings, inPersonLocation: e.target.value })} placeholder="Office, café, address…" /></label>
                  )}
                </section>
              </div>
            </>
          ) : tab === 'activity' ? (
            <>
              <div className="admin-heading">
                <div><p className="eyebrow">ACTIVITY</p><h1>Your Slot at a glance.</h1></div>
                <button className="secondary-button activity-refresh" type="button" onClick={loadActivity} disabled={activityLoading}>
                  {activityLoading ? <><Loader2 className="spin" size={16} /> Refreshing…</> : <><Activity size={16} /> Refresh</>}
                </button>
              </div>
              {error && <div className="error-banner" role="alert">{error}</div>}
              {activityLoading && !activity ? (
                <section className="activity-loading"><Loader2 className="spin" size={20} /> Loading activity…</section>
              ) : activity ? (
                <>
                  <section className="activity-metrics" aria-label="Booking activity summary">
                    <article className="activity-metric"><span>Bookings</span><strong>{activity.stats.total_bookings}</strong><small>All confirmed bookings, including later cancellations</small></article>
                    <article className="activity-metric"><span>Upcoming</span><strong>{activity.stats.upcoming}</strong><small>Confirmed meetings still ahead</small></article>
                    <article className="activity-metric"><span>Completed</span><strong>{activity.stats.completed}</strong><small>Confirmed meetings whose end time has passed</small></article>
                    <article className="activity-metric"><span>Cancelled</span><strong>{activity.stats.cancelled}</strong><small>Cancelled meetings, excluding abandoned payment holds</small></article>
                    <article className="activity-metric"><span>Reschedules</span><strong>{activity.stats.reschedules}</strong><small>Total times confirmed meetings were moved</small></article>
                    <article className="activity-metric"><span>Paid bookings</span><strong>{activity.stats.paid_bookings}</strong><small>Bookings with a completed Stripe payment</small></article>
                    <article className="activity-metric"><span>Average length</span><strong>{activity.stats.average_minutes} min</strong><small>Average duration of current confirmed meetings</small></article>
                    {activity.paymentsEnabled && (
                      <>
                        <article className="activity-metric money"><span>Revenue</span><strong>{activityMoneyText(activity.money, 'revenue', activeSettings.paymentCurrency)}</strong><small>Gross successful Stripe payments</small></article>
                        <article className="activity-metric money"><span>Refunds</span><strong>{activityMoneyText(activity.money, 'refunds', activeSettings.paymentCurrency)}</strong><small>Successful or currently pending refunds</small></article>
                        <article className="activity-metric money"><span>Net revenue</span><strong>{activityMoneyText(activity.money, 'net', activeSettings.paymentCurrency)}</strong><small>Gross revenue minus refunds</small></article>
                        <article className="activity-metric"><span>Open payment holds</span><strong>{activity.stats.open_payment_holds}</strong><small>Visitors currently inside the payment window</small></article>
                      </>
                    )}
                  </section>

                  <section className="activity-feed-card">
                    <div className="activity-feed-heading">
                      <div><p className="eyebrow">RECENT ACTIVITY</p><h2>What happened.</h2></div>
                      <span>Latest {Math.min(activity.events.length, 100)} events</span>
                    </div>
                    {activity.events.length === 0 ? (
                      <div className="empty-list activity-empty"><Activity size={25} /><strong>No activity yet</strong><span>Bookings, payments, reschedules, cancellations, and refunds will appear here.</span></div>
                    ) : (
                      <div className="activity-feed">
                        {activity.events.map((event) => {
                          const copy = activityEventCopy(event, activeSettings.timezone);
                          return (
                            <article className={`activity-feed-row type-${event.type}`} key={event.id}>
                              <span className="activity-feed-mark" aria-hidden="true" />
                              <div className="activity-feed-copy">
                                <strong>{copy.title}</strong>
                                <span>{copy.detail}</span>
                              </div>
                              <time dateTime={databaseInstant(event.occurred_at)}>{formatInstant(databaseInstant(event.occurred_at), activeSettings.timezone)}</time>
                            </article>
                          );
                        })}
                      </div>
                    )}
                  </section>
                </>
              ) : (
                <section className="activity-loading">Activity could not be loaded.</section>
              )}
            </>
          ) : tab === 'attendees' ? (
            <>
              <div className="admin-heading">
                <div><p className="eyebrow">ATTENDEES</p><h1>Who keeps booking.</h1></div>
                <button className="secondary-button activity-refresh" type="button" onClick={loadAttendees} disabled={attendeesLoading}>
                  {attendeesLoading ? <><Loader2 className="spin" size={16} /> Refreshing…</> : <><Users size={16} /> Refresh</>}
                </button>
              </div>
              {error && <div className="error-banner" role="alert">{error}</div>}
              <label className="admin-search attendee-search">
                <Search size={16} aria-hidden="true" />
                <input value={attendeeSearch} onChange={(event) => setAttendeeSearch(event.target.value)} placeholder="Search attendee name or email" aria-label="Search attendees" />
              </label>
              {attendeesLoading && !attendeeData ? (
                <section className="activity-loading"><Loader2 className="spin" size={20} /> Loading attendees…</section>
              ) : attendeeData ? (
                <>
                  <section className="attendee-list-card">
                    <div className="attendee-list-head">
                      <span>Attendee</span><span>Bookings</span><span>Completed</span><span>Cancelled</span><span>Reschedules</span><span>Last booked</span><span />
                    </div>
                    {shownAttendees.length === 0 ? (
                      <div className="empty-list activity-empty"><Users size={25} /><strong>No attendees found</strong><span>{attendeeSearch ? 'Try another name or email.' : 'Confirmed attendees will appear here after their first booking.'}</span></div>
                    ) : shownAttendees.map((attendee) => (
                      <article className="attendee-list-row" key={attendee.email.toLowerCase()}>
                        <div className="attendee-person"><span className="attendee-avatar" aria-hidden="true">{attendee.name.slice(0, 1).toUpperCase()}</span><div><strong>{attendee.name}</strong><span>{attendee.email}</span></div></div>
                        <strong>{attendee.total_bookings}</strong>
                        <span>{attendee.completed}</span>
                        <span>{attendee.cancelled}</span>
                        <span>{attendee.reschedules}</span>
                        <time>{attendee.last_booked_at ? formatInstant(databaseInstant(attendee.last_booked_at), activeSettings.timezone) : '—'}</time>
                        <button type="button" className="icon-button" aria-label={`Open ${attendee.name} attendee history`} onClick={() => void openAttendee(attendee)}><Inbox size={15} /></button>
                      </article>
                    ))}
                  </section>
                  {selectedAttendee && (
                    <section className="booking-detail-card attendee-detail-card">
                      <div className="card-heading">
                        <div><h2>{selectedAttendee.name}</h2><p>{selectedAttendee.email}</p></div>
                        <button type="button" className="text-button" onClick={() => setSelectedAttendee(null)}>Close</button>
                      </div>
                      <div className="attendee-metrics">
                        <article><span>Bookings</span><strong>{selectedAttendee.total_bookings}</strong></article>
                        <article><span>Upcoming</span><strong>{selectedAttendee.upcoming}</strong></article>
                        <article><span>Completed</span><strong>{selectedAttendee.completed}</strong></article>
                        <article><span>Cancelled</span><strong>{selectedAttendee.cancelled}</strong></article>
                        <article><span>Reschedules</span><strong>{selectedAttendee.reschedules}</strong></article>
                        <article><span>Booked time</span><strong>{selectedAttendee.total_minutes} min</strong></article>
                        <article><span>Average length</span><strong>{selectedAttendee.average_minutes} min</strong></article>
                        {attendeeData.paymentsEnabled && <article><span>Paid bookings</span><strong>{selectedAttendee.paid_bookings}</strong></article>}
                        {attendeeData.paymentsEnabled && <article><span>Refunded</span><strong>{selectedAttendee.refunded_bookings}</strong></article>}
                      </div>
                      <div className="attendee-history-heading"><div><p className="eyebrow">BOOKING HISTORY</p><h3>Recent meetings</h3></div><span>{attendeeBookings.length} shown</span></div>
                      {attendeeBookingsLoading ? (
                        <div className="activity-loading attendee-history-loading"><Loader2 className="spin" size={18} /> Loading booking history…</div>
                      ) : attendeeBookings.length ? (
                        <div className="attendee-booking-history">{attendeeBookings.map((booking) => <BookingRowItem key={booking.id} booking={booking} timezone={activeSettings.timezone} onOpen={openBookingFromAttendee} />)}</div>
                      ) : (
                        <div className="empty-list attendee-history-empty"><Inbox size={22} /><strong>No booking history</strong></div>
                      )}
                    </section>
                  )}
                </>
              ) : <section className="activity-loading">Attendees could not be loaded.</section>}
            </>
          ) : (
            <>
              <div className="admin-heading"><div><p className="eyebrow">BOOKINGS</p><h1>Your meetings.</h1></div></div>
              {notice && <div className="success-banner" role="status">{notice}</div>}
              {error && <div className="error-banner" role="alert">{error}</div>}
              <div className="booking-tabs" role="tablist" aria-label="Filter bookings">
                <button
                  role="tab"
                  aria-selected={bookingFilter === 'upcoming'}
                  className={`booking-tab ${bookingFilter === 'upcoming' ? 'active' : ''}`}
                  onClick={() => setBookingFilter('upcoming')}
                >
                  Upcoming <span className="count">{upcoming.length}</span>
                </button>
                <button
                  role="tab"
                  aria-selected={bookingFilter === 'past'}
                  className={`booking-tab ${bookingFilter === 'past' ? 'active' : ''}`}
                  onClick={() => setBookingFilter('past')}
                >
                  Past <span className="count">{past.length}</span>
                </button>
              </div>
              <label className="admin-search booking-search">
                <Search size={16} aria-hidden="true" />
                <input
                  value={bookingSearch}
                  onChange={(event) => setBookingSearch(event.target.value)}
                  placeholder="Search booking ID, name or email"
                  aria-label="Search bookings by booking ID, name or email"
                  autoComplete="off"
                />
                {bookingSearchLoading && <Loader2 className="spin admin-search-spinner" size={15} aria-label="Searching" />}
              </label>
              <section className="booking-list-card">
                {bookingSearchLoading && bookingSearch.trim() && bookingSearchResults === null ? (
                  <div className="empty-list"><Loader2 className="spin" size={23} /><strong>Searching bookings…</strong></div>
                ) : shown.length === 0 ? (
                  <div className="empty-list">
                    <Inbox size={26} />
                    <strong>{bookingSearch.trim() ? 'No matching bookings' : bookingFilter === 'upcoming' ? 'No upcoming bookings' : 'No past bookings'}</strong>
                    <span>{bookingSearch.trim() ? `No ${bookingFilter} booking matches “${bookingSearch.trim()}”.` : bookingFilter === 'upcoming' ? 'No Slot bookings are upcoming. Google Calendar busy time and availability rules can still make public times unavailable.' : 'Completed meetings will be listed here.'}</span>
                  </div>
                ) : (
                  shown.map((booking) => (
                    <BookingRowItem key={booking.id} booking={booking} timezone={activeSettings.timezone} onOpen={setSelectedBooking} />
                  ))
                )}
              </section>
              {selectedBooking && (
                <section className="booking-detail-card">
                  <div className="card-heading">
                    <div>
                      <h2>{selectedBooking.name}</h2>
                      <p>{selectedBooking.email}</p>
                    </div>
                    <button type="button" className="text-button" onClick={() => setSelectedBooking(null)}>Close</button>
                  </div>
                  <dl className="booking-detail-grid">
                    <div><dt>Status</dt><dd>{selectedBooking.status}</dd></div>
                    {selectedBooking.public_booking_id && <div><dt>Booking ID</dt><dd>{selectedBooking.public_booking_id}</dd></div>}
                    <div><dt>Date</dt><dd>{formatDateShort(selectedBooking.start_time, activeSettings.timezone)}</dd></div>
                    <div><dt>Start</dt><dd>{formatClock(selectedBooking.start_time, activeSettings.timezone)}</dd></div>
                    <div><dt>End</dt><dd>{formatClock(selectedBooking.end_time, activeSettings.timezone)}</dd></div>
                    <div><dt>Duration</dt><dd>{selectedBooking.duration_minutes} min</dd></div>
                    <div><dt>Timezone</dt><dd>{activeSettings.timezone}</dd></div>
                    <div><dt>Meeting</dt><dd>{modeMeta[selectedBooking.meeting_mode].label}</dd></div>
                    <div><dt>Calendar</dt><dd>{selectedBooking.calendar_provider ?? 'google'}</dd></div>
                    <div><dt>Sync</dt><dd>{selectedBooking.calendar_sync_status ?? 'synced'}</dd></div>
                    <div><dt>Payment</dt><dd>{selectedBooking.payment_status ?? 'not_requested'}</dd></div>
                    {selectedBooking.payment_status === 'paid' && <div><dt>Refund</dt><dd>{selectedBooking.refund_status ?? 'not_requested'}</dd></div>}
                    {selectedBooking.payment_amount != null && selectedBooking.payment_currency && (
                      <div><dt>Amount</dt><dd>{formatMoneyMinor(selectedBooking.payment_amount, selectedBooking.payment_currency)}</dd></div>
                    )}
                    {selectedBooking.paid_at && <div><dt>Paid</dt><dd>{formatInstant(`${selectedBooking.paid_at.replace(' ', 'T')}Z`, activeSettings.timezone)}</dd></div>}
                    {selectedBooking.created_at && <div><dt>Created</dt><dd>{formatInstant(`${selectedBooking.created_at.replace(' ', 'T')}Z`, activeSettings.timezone)}</dd></div>}
                    {selectedBooking.rescheduled_at && <div><dt>Rescheduled</dt><dd>{formatInstant(`${selectedBooking.rescheduled_at.replace(' ', 'T')}Z`, activeSettings.timezone)}{selectedBooking.reschedule_count ? ` · ${selectedBooking.reschedule_count}×` : ''}</dd></div>}
                    {selectedBooking.proposal_status === 'pending' && selectedBooking.proposed_start_time && <div><dt>Suggested time</dt><dd>{formatDateShort(selectedBooking.proposed_start_time, activeSettings.timezone)} {formatClock(selectedBooking.proposed_start_time, activeSettings.timezone)}</dd></div>}
                    {selectedBooking.refunded_at && <div><dt>Refunded</dt><dd>{formatInstant(`${selectedBooking.refunded_at.replace(' ', 'T')}Z`, activeSettings.timezone)}</dd></div>}
                    {selectedBooking.cancelled_at && <div><dt>Cancelled</dt><dd>{formatInstant(`${selectedBooking.cancelled_at.replace(' ', 'T')}Z`, activeSettings.timezone)}</dd></div>}
                  </dl>
                  {selectedBooking.calendar_sync_status === 'failed' && selectedBooking.calendar_sync_error && (
                    <div className="error-banner" role="alert">{selectedBooking.calendar_sync_error}</div>
                  )}
                  <div className="booking-detail-actions">
                    {selectedBooking.status === 'confirmed' && selectedBooking.meet_url && <a className="secondary-button" href={selectedBooking.meet_url} target="_blank" rel="noreferrer">Open meeting link</a>}
                    <button
                      className="secondary-button"
                      type="button"
                      disabled={selectedBooking.status !== 'confirmed' || Date.parse(selectedBooking.end_time) < Date.now()}
                      onClick={() => openScheduleDialog(selectedBooking, 'reschedule')}
                    >Reschedule</button>
                    <button
                      className="secondary-button"
                      type="button"
                      disabled={selectedBooking.status !== 'confirmed' || Date.parse(selectedBooking.end_time) < Date.now()}
                      onClick={() => openScheduleDialog(selectedBooking, 'suggest')}
                    >Suggest new time</button>
                    <button
                      className="danger-button"
                      type="button"
                      disabled={!(selectedBooking.status === 'pending' || selectedBooking.status === 'confirmed') || Date.parse(selectedBooking.end_time) < Date.now()}
                      onClick={() => setCancelTarget(selectedBooking)}
                    >
                      {selectedBooking.status === 'pending' ? 'Release payment hold' : 'Cancel meeting'}
                    </button>
                  </div>
                </section>
              )}
            </>
          )}
        </section>
      </div>
      {scheduleTarget && (
        <div className="modal-backdrop" role="presentation">
          <section className="confirm-dialog schedule-dialog" role="dialog" aria-modal="true" aria-labelledby="schedule-action-title">
            <p className="eyebrow">{scheduleAction === 'reschedule' ? 'RESCHEDULE' : 'SUGGEST NEW TIME'}</p>
            <h2 id="schedule-action-title">{scheduleAction === 'reschedule' ? 'Move this meeting?' : 'Propose a different time?'}</h2>
            <div className="cancel-summary">
              <strong>{scheduleTarget.name}</strong>
              <span>Current: {formatDateShort(scheduleTarget.start_time, activeSettings.timezone)}</span>
              <span>{formatClock(scheduleTarget.start_time, activeSettings.timezone)}-{formatClock(scheduleTarget.end_time, activeSettings.timezone)} · {scheduleTarget.duration_minutes} min</span>
            </div>
            <p className="muted">{scheduleAction === 'reschedule'
              ? 'Choose an available start time. The existing Google Calendar event is moved and the attendee receives an updated invitation.'
              : 'The existing meeting stays exactly where it is. Slot sends a Google Calendar update containing a secure acceptance link for the proposed time.'}</p>

            {!suggestionUrl ? (
              <>
                <label className="schedule-date-label">Date
                  <input
                    type="date"
                    min={todayInZone(activeSettings.timezone)}
                    value={scheduleDate}
                    onChange={(event) => setScheduleDate(event.target.value)}
                  />
                </label>
                <div className="schedule-slot-picker" aria-label="Available start times">
                  {loadingScheduleSlots ? (
                    <span className="schedule-loading"><Loader2 className="spin" size={17} /> Checking availability…</span>
                  ) : scheduleSlots.length ? (
                    scheduleSlots.map((slot) => (
                      <button
                        type="button"
                        key={slot.start}
                        className={scheduleStart === slot.start ? 'active' : ''}
                        onClick={() => setScheduleStart(slot.start)}
                      >
                        {slot.time}
                      </button>
                    ))
                  ) : (
                    <span className="schedule-loading">No {scheduleTarget.duration_minutes}-minute starts are available on this date.</span>
                  )}
                </div>
              </>
            ) : (
              <div className="suggestion-link-box">
                <strong>Suggestion sent</strong>
                <span>Google Calendar notified the attendee. You can also send this link directly:</span>
                <div className="suggestion-link-row">
                  <input readOnly value={suggestionUrl} aria-label="Reschedule suggestion link" />
                  <button type="button" className="secondary-button" onClick={() => copyScheduleLink(suggestionUrl)}>
                    {scheduleLinkCopied ? <><Check size={15} /> Copied</> : <><Copy size={15} /> Copy</>}
                  </button>
                </div>
              </div>
            )}

            <div className="dialog-actions">
              <button
                className="secondary-button"
                type="button"
                onClick={() => { setScheduleTarget(null); setSuggestionUrl(''); }}
                disabled={savingScheduleAction}
              >{suggestionUrl ? 'Done' : 'Close'}</button>
              {!suggestionUrl && (
                <button className="primary-button" type="button" onClick={submitScheduleAction} disabled={!scheduleStart || savingScheduleAction}>
                  {savingScheduleAction
                    ? <><Loader2 className="spin" size={16} /> {scheduleAction === 'reschedule' ? 'Rescheduling…' : 'Sending…'}</>
                    : scheduleAction === 'reschedule' ? 'Reschedule meeting' : 'Send suggestion'}
                </button>
              )}
            </div>
          </section>
        </div>
      )}
      {cancelTarget && (
        <div className="modal-backdrop" role="presentation">
          <section className="confirm-dialog" role="dialog" aria-modal="true" aria-labelledby="cancel-title">
            <p className="eyebrow">{cancelTarget.status === 'pending' ? 'PAYMENT HOLD' : 'CANCEL MEETING'}</p>
            <h2 id="cancel-title">{cancelTarget.status === 'pending' ? 'Release this payment hold?' : 'Cancel meeting?'}</h2>
            <div className="cancel-summary">
              <strong>{cancelTarget.name}</strong>
              <span>{formatDateShort(cancelTarget.start_time, activeSettings.timezone)}</span>
              <span>{formatClock(cancelTarget.start_time, activeSettings.timezone)}-{formatClock(cancelTarget.end_time, activeSettings.timezone)}</span>
              <span>{cancelTarget.duration_minutes} min</span>
            </div>
            <p className="muted">{cancelTarget.status === 'pending'
              ? 'This expires the open Stripe Checkout, removes the temporary calendar hold, and makes the time bookable again.'
              : 'This will also cancel the Google Calendar event and send normal Calendar cancellation notifications.'}</p>
            {cancelTarget.status === 'pending'
              && checkoutSessionMode(cancelTarget.stripe_checkout_session_id) !== 'unknown'
              && stripeMode !== 'unknown'
              && checkoutSessionMode(cancelTarget.stripe_checkout_session_id) !== stripeMode && (
                <div className="error-banner" role="alert">
                  This hold was created in Stripe {checkoutSessionMode(cancelTarget.stripe_checkout_session_id).toUpperCase()} mode, but Slot is currently using {stripeMode.toUpperCase()} mode. The current key cannot expire that old Checkout session. Releasing will still remove Slot's lock and calendar hold; the old Stripe Checkout may remain payable until its own expiry.
                </div>
            )}
            {cancelTarget.payment_status === 'paid' && (
              <div className="refund-banner" role="status">
                <strong>Full Stripe refund included</strong>
                <span>{cancelTarget.payment_amount != null && cancelTarget.payment_currency
                  ? `${formatMoneyMinor(cancelTarget.payment_amount, cancelTarget.payment_currency)} will be refunded to the original payment method before Slot finalizes the cancellation.`
                  : 'Slot will refund the Stripe payment to the original payment method before finalizing the cancellation.'}</span>
              </div>
            )}
            <div className="dialog-actions">
              <button className="secondary-button" type="button" onClick={() => setCancelTarget(null)} disabled={cancelling}>{cancelTarget.status === 'pending' ? 'Keep hold' : 'Keep meeting'}</button>
              <button className="danger-button" type="button" onClick={confirmCancelBooking} disabled={cancelling}>
                {cancelling
                  ? <><Loader2 className="spin" size={16} /> {cancelTarget.status === 'pending' ? 'Releasing...' : 'Cancelling...'}</>
                  : cancelTarget.status === 'pending' ? 'Release hold' : cancelTarget.payment_status === 'paid' ? 'Cancel & refund' : 'Cancel meeting'}
              </button>
            </div>
          </section>
        </div>
      )}
    </main>
  );
}

export default function App() {
  if (window.location.pathname.startsWith('/admin')) return <Admin />;
  if (window.location.pathname.startsWith('/reschedule/')) return <RescheduleSuggestionPage />;
  return <PublicBooking />;
}