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
settings.ts
import type { AppSettings, Env } from './types';

export const DEFAULT_SETTINGS: AppSettings = {
  title: 'Book a slot',
  subtitle: 'Pick a day, choose a time, and you are set.',
  timezone: 'Europe/Stockholm',
  bookingWindowDays: 365,
  minimumNoticeMinutes: 120,
  bufferBeforeMinutes: 0,
  bufferAfterMinutes: 15,
  slotIncrementMinutes: 15,
  durations: [15, 30, 45, 60],
  calendarId: 'primary',
  busyCalendarIds: ['primary'],
  defaultMeetingMode: 'google_meet',
  allowMeetingModes: ['google_meet'],
  inPersonLocation: '',
  paymentEnabled: false,
  paymentRatePerMinute: 0,
  paymentFreeDurations: [],
  paymentCurrency: 'sek',
  paymentLabel: 'Booking payment',
  availability: {
    '0': [],
    '1': [{ start: '09:00', end: '17:00' }],
    '2': [{ start: '09:00', end: '17:00' }],
    '3': [{ start: '09:00', end: '17:00' }],
    '4': [{ start: '09:00', end: '17:00' }],
    '5': [{ start: '09:00', end: '15:00' }],
    '6': [],
  },
};

export async function getSettings(env: Env): Promise<AppSettings> {
  const row = await env.DB.prepare('SELECT value FROM settings WHERE key = ?1')
    .bind('app_settings')
    .first<{ value: string }>();

  if (!row?.value) return DEFAULT_SETTINGS;

  try {
    const parsed = JSON.parse(row.value) as Partial<AppSettings> & { paymentAmountMinor?: number };
    const currency = typeof parsed.paymentCurrency === 'string' && /^[a-zA-Z]{3}$/.test(parsed.paymentCurrency)
      ? parsed.paymentCurrency.toLowerCase()
      : DEFAULT_SETTINGS.paymentCurrency;
    const hasPerMinuteRate = parsed.paymentRatePerMinute !== undefined
      && Number.isFinite(Number(parsed.paymentRatePerMinute));
    const paymentRatePerMinute = hasPerMinuteRate
      ? Math.max(0, Number(parsed.paymentRatePerMinute))
      : DEFAULT_SETTINGS.paymentRatePerMinute;
    return {
      ...DEFAULT_SETTINGS,
      ...parsed,
      // Old releases stored one fixed Stripe amount in `paymentAmountMinor`.
      // There is no safe way to reinterpret that as a per-minute rate, so legacy
      // payment settings are deliberately disabled until the owner saves a rate.
      paymentEnabled: hasPerMinuteRate ? Boolean(parsed.paymentEnabled) : false,
      paymentCurrency: currency,
      paymentRatePerMinute,
      paymentFreeDurations: Array.isArray(parsed.paymentFreeDurations)
        ? parsed.paymentFreeDurations.map(Number).filter((value) => Number.isFinite(value) && value > 0)
        : DEFAULT_SETTINGS.paymentFreeDurations,
      durations: Array.isArray(parsed.durations) ? parsed.durations : DEFAULT_SETTINGS.durations,
      busyCalendarIds: Array.isArray(parsed.busyCalendarIds) ? parsed.busyCalendarIds : DEFAULT_SETTINGS.busyCalendarIds,
      allowMeetingModes: Array.isArray(parsed.allowMeetingModes)
        ? parsed.allowMeetingModes
        : DEFAULT_SETTINGS.allowMeetingModes,
      availability: {
        ...DEFAULT_SETTINGS.availability,
        ...(parsed.availability ?? {}),
      },
    };
  } catch {
    return DEFAULT_SETTINGS;
  }
}

export async function saveSettings(env: Env, settings: AppSettings): Promise<void> {
  await env.DB.prepare(`
    INSERT INTO settings (key, value, updated_at)
    VALUES (?1, ?2, datetime('now'))
    ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at
  `).bind('app_settings', JSON.stringify(settings)).run();
}