Features

Bookings

Bookings

BETA

Booking system for slot-based appointments and inventory-based reservations.

Status: Beta -- API may change between minor releases.

Overview

The Bookings package (@fyit/crouton-bookings) provides a complete booking system supporting two modes:

  • Slot mode (default): Fixed time slots with optional per-slot capacity (courts, rooms, appointments)
  • Inventory mode: Quantity-based pool (equipment rentals, bike pools)

Features:

  • Cart system with batch checkout
  • Schedule rules (open days, per-slot day schedules, blocked date ranges)
  • Monthly booking limits per location per user
  • Optional email notifications (confirmations, reminders, cancellations, follow-ups)
  • Group-based bookings (e.g. age groups)
  • Map integration (optional, via @fyit/crouton-maps)
  • i18n support (EN, NL, FR)

Quick Start

Add the bookings layer to your nuxt.config.ts:

export default defineNuxtConfig({
  extends: [
    '@fyit/crouton',           // Core
    '@fyit/crouton-bookings',  // Bookings layer
    './layers/bookings'        // Generated collections
  ]
})

Dependencies:

  • Regular: @fyit/crouton-editor (installed automatically)
  • Peer: @fyit/crouton-core, @nuxtjs/i18n, @internationalized/date, @vueuse/core, zod
  • Optional peer: @fyit/crouton-maps (for map display in panel)

Booking Modes

Slot Mode (Default)

Each location defines named time slots with optional capacity:

// Location with slots (capacity defaults to 1)
const location = {
  title: 'Tennis Court A',
  slots: [
    { id: 'morning', label: '09:00 - 10:00' },
    { id: 'midday', label: '10:00 - 11:00' },
    { id: 'afternoon', label: '14:00 - 15:00', capacity: 4 }
  ]
}

When capacity > 1, the UI shows "X left" / "Full" badges. If no slots are defined, an implicit "All Day" slot is used.

Inventory Mode

Quantity-based pool where users book units per day:

const location = {
  title: 'Kayak Rental',
  inventoryMode: true,
  quantity: 20 // 20 kayaks available per day
}

Composables

useBookingCart()

The primary composable for the customer booking flow. Manages form state, cart persistence (localStorage), availability checking, and batch submission.

const {
  // State
  cart,                    // Ref<CartItem[]> - persisted in localStorage
  formState,               // reactive { locationId, date, slotIds, groupId, quantity, editingBookingId }
  isOpen,                  // Ref<boolean> - sidebar open state
  isCartOpen,              // Ref<boolean> - cart drawer open state
  isExpanded,              // Ref<boolean> - expanded mode (with map)
  activeTab,               // Ref<string> - 'book' | 'my-bookings'
  isSubmitting,            // Ref<boolean>
  availabilityLoading,     // Ref<boolean>
  cartPulse,               // Ref<number> - increments on add for animation

  // Mode detection
  isInventoryMode,         // ComputedRef<boolean>
  inventoryQuantity,       // ComputedRef<number>

  // Monthly limit
  monthlyBookingLimit,     // ComputedRef<number | null>
  monthlyBookingRemaining, // ComputedRef<number | null>

  // Groups
  enableGroups,            // ComputedRef<boolean>
  groupOptions,            // ComputedRef<Array>

  // Locations & slots
  locations,               // Ref<LocationData[]>
  locationsStatus,         // fetch status
  selectedLocation,        // ComputedRef<LocationData | null>
  allSlots,                // ComputedRef<SlotItem[]>
  availableSlots,          // ComputedRef<SlotItem[]> - filtered by rules + bookings
  rawSlots,                // ComputedRef<SlotItem[]> - from location config
  isSlotDisabled,          // (slotId: string) => boolean
  getSlotRemaining,        // (slotId: string) => number
  getSlotCapacity,         // (slotId: string) => number

  // Calendar helpers (API + cart combined)
  hasBookingsOnDate,       // (date: Date) => boolean
  isDateFullyBooked,       // (date: Date) => boolean
  getBookedSlotLabelsForDate, // (date: Date) => string[]
  getBookedSlotsForDate,   // (date: Date) => string[]
  getInventoryAvailability, // (date: Date) => { available, remaining, total, bookedCount }

  // Schedule rules
  isDateUnavailable,       // (date: Date | DateValue) => boolean
  getBlockedReason,        // (date: Date | DateValue) => string | null

  // My bookings
  myBookings,              // Ref<BookingData[]>
  myBookingsStatus,
  refreshMyBookings,

  // Computed
  canAddToCart,            // ComputedRef<boolean>
  cartCount,               // ComputedRef<number>
  upcomingBookingsCount,   // ComputedRef<number>

  // Actions
  addToCart,               // () => void
  toggleSlot,              // (slotId: string) => void
  removeFromCart,          // (id: string) => void
  clearCart,               // () => void
  submitAll,               // () => Promise<result | null>
  resetForm,               // () => void
  cancelBooking,           // (bookingId: string) => Promise<boolean>
  deleteBooking,           // (bookingId: string) => Promise<boolean>
  fetchAvailability,       // (startDate: Date, endDate: Date) => void

  // Signals
  lastBookingCreatedAt,    // Ref<number | null>
  lastCreatedBookingIds,   // Ref<string[]>
} = useBookingCart()

useBookingAvailability(locationId, location)

Lower-level composable for checking slot/inventory availability. Used internally by useBookingCart and available standalone for admin-side checks.

const {
  loading,
  availabilityData,        // Ref<AvailabilityData>
  isInventoryMode,
  inventoryQuantity,
  allSlots,
  locationSlots,
  fetchAvailability,       // (startDate: Date, endDate: Date, excludeBookingId?: string) => void
  getBookedSlotsForDate,   // (date: Date | DateValue) => string[]
  getSlotBookedCountForDate, // (date: Date | DateValue, slotId: string) => number
  getSlotRemainingForDate, // (date: Date | DateValue, slotId: string) => number
  getAvailableSlotsForDate, // (date: Date | DateValue) => SlotItem[]
  getBookedCountForDate,   // (date: Date | DateValue) => number
  getInventoryAvailability, // (date: Date | DateValue, quantityOverride?: number) => InventoryAvailability
  hasBookingsOnDate,       // (date: Date | DateValue) => boolean
  isDateFullyBooked,       // (date: Date | DateValue) => boolean

  // Schedule rules (pass-through from useScheduleRules)
  isDateUnavailable,
  isSlotAvailableByRules,
  getBlockedReason,
  getRuleBlockedSlotIds,
} = useBookingAvailability(
  locationId,  // Ref<string | null>
  location     // Ref<LocationWithInventory | null | undefined>
)

useBookingsList(options?)

Fetches and manages the bookings list with settings and locations. Supports personal and team-wide scopes.

const {
  bookings,           // ComputedRef<Booking[]> - sorted by date
  calendarBookings,   // ComputedRef<Booking[]> - all team bookings for calendar indicators
  settings,           // ComputedRef<SettingsData | null>
  locations,          // ComputedRef<LocationData[]>
  loading,            // ComputedRef<boolean>
  error,              // ComputedRef<Error | null>
  refresh,            // () => Promise<void>
} = useBookingsList({ scope: 'personal' }) // or 'team'

useScheduleRules(location)

Evaluates location schedule rules client-side. Checks open days, per-slot day schedules, and blocked date ranges.

const {
  openDays,              // ComputedRef<number[] | null>
  slotSchedule,          // ComputedRef<SlotSchedule | null>
  blockedDates,          // ComputedRef<BlockedDateItem[]>
  isLocationOpenOnDate,  // (date: Date | DateValue) => boolean
  isDateBlocked,         // (date: Date | DateValue, slotId?: string) => boolean
  isDateUnavailable,     // (date: Date | DateValue) => boolean
  isSlotAvailableByRules, // (slotId: string, date: Date | DateValue) => boolean
  getBlockedReason,      // (date: Date | DateValue) => string | null
  getRuleBlockedSlotIds, // (date: Date | DateValue) => string[]
} = useScheduleRules(location) // Ref<ScheduleRuleLocation | null | undefined>

Rule precedence (all must pass for a slot to be available):

  1. Location open on this weekday (openDays)
  2. Slot scheduled for this weekday (slotSchedule, falls back to openDays)
  3. Date not in a blocked range (blockedDates)

useBookingEmail()

Check email status and resend emails for bookings.

const {
  isEmailEnabled,  // ComputedRef<boolean>
  resendEmail,     // (teamId, bookingId, triggerType) => Promise<{ success, error? }>
} = useBookingEmail()

useBookingSlots()

Utility functions for parsing and labeling slots.

const {
  parseSlotIds,       // (slot: string | string[] | null) => string[]
  parseLocationSlots, // (location: { slots? }) => SlotItem[]
  getSlotLabel,       // (slotId: string, slots: SlotItem[]) => string
} = useBookingSlots()

useBookingOptions()

Fetches booking settings and provides translated label lookups for statuses and groups.

const {
  statuses,          // ComputedRef<OptionItem[]>
  groups,            // ComputedRef<OptionItem[]>
  pending,
  error,
  refresh,
  getStatusLabel,    // (statusValue: string) => string
  getGroupLabel,     // (groupId: string) => string
  getTranslatedLabel, // (item: OptionItem) => string
} = useBookingOptions()

useBookingsSettings()

Returns the bookings settings collection configuration (schema, columns, defaults).

const {
  name,          // 'bookingsSettings'
  layer,         // 'bookings'
  apiPath,       // 'bookings-settings'
  schema,        // Zod schema
  defaultValues, // { statuses: [], enableGroups: false, groups: [] }
  columns,
} = useBookingsSettings()

useBookingMonthlyLimit(...)

Tracks and enforces monthly booking limits per user per location. Used internally by useBookingCart.

const {
  monthlyBookingCount,        // Ref<number>
  monthlyBookingCountLoading, // Ref<boolean>
  cartCountForLocationMonth,  // ComputedRef<number>
  monthlyBookingRemaining,    // ComputedRef<number | null>
  fetchMonthlyBookingCount,   // () => Promise<void>
} = useBookingMonthlyLimit(teamId, locationId, selectedDate, maxBookingsPerMonth, cart, editingBookingId)

useBookingEmailVariables()

Provides email template variables and demo data for preview rendering.

const {
  variables,        // EditorVariable[] - all available template variables
  demoData,         // object with demo customer, booking, location, team data
  getPreviewValues, // () => Record<string, string>
} = useBookingEmailVariables()

Components

All components use the prefix CroutonBookings (configured in nuxt.config.ts).

Main Components

ComponentPurpose
CroutonBookingsPanelMain booking panel with tabs (book / my-bookings), filters, calendar, and list. Calendar functionality is integrated into this component.
CroutonBookingsListScrollable bookings list with inline creation, date grouping, and highlighting
CroutonBookingsBookingCardIndividual booking card showing date, slot, location, status, and email actions (includes inline activity timeline)
CroutonBookingsBookingCreateCardInline booking creation form

Supporting Components

ComponentPurpose
CroutonBookingsWeekStripHorizontal week date navigation strip
CroutonBookingsDateBadgeDate display badge with formatting
CroutonBookingsPanelFiltersFilter controls for status and location
CroutonBookingsLocationCardLocation card with details
CroutonBookingsLocationFormLocation editing form
CroutonBookingsSlotIndicatorVisual slot capacity indicator
CroutonBookingsOpenDaysPickerDay-of-week picker for location scheduling
CroutonBookingsScheduleGridPer-slot day-of-week schedule grid editor
CroutonBookingsBlockedDateInputBlocked date range input
CroutonBookingsAvailabilityPreviewPreview of slot availability

Content Block Components

ComponentPurpose
CroutonBookingsBlocksBookingBlockViewBooking block for content pages
CroutonBookingsBlocksBookingBlockRenderBooking block renderer

Key Component Props

CroutonBookingsPanel

PropTypeDefaultDescription
bookingsBooking[]auto-fetchedBookings data (external or auto-fetched via useBookingsList)
locationsLocationData[]auto-fetchedLocations data
settingsSettingsData | nullauto-fetchedSettings data
loadingbooleanfalseLoading state (when providing external data)
errorError | null-Error state
initialFiltersPartial<FilterState>{}Initial filter state
titlestring-Header title (empty string hides header)
emptyMessagestring-Empty state message
scope'personal' | 'team''personal'Booking scope

CroutonBookingsBookingCard

PropTypeDefaultDescription
bookingBookingrequiredBooking data with relations
highlightedbooleanfalseVisual highlight state
justCreatedbooleanfalseTemporary highlight for new bookings (fades)
sendingEmailTypeEmailTriggerType | nullnullCurrently sending email type

API Endpoints

PathMethodPurpose
/api/crouton-bookings/teams/[id]/availabilityGETBooked slots for a date range (query: locationId, startDate, endDate)
/api/crouton-bookings/teams/[id]/customer-bookingsGETCurrent user's bookings with email stats
/api/crouton-bookings/teams/[id]/customer-bookings-batchPOSTSubmit cart (batch checkout)
/api/crouton-bookings/teams/[id]/customer-locationsGETLocations accessible to current user
/api/crouton-bookings/teams/[id]/admin-bookingsGETAll team bookings (team members)
/api/crouton-bookings/teams/[id]/monthly-booking-countGETUser's monthly booking count for a location
/api/crouton-bookings/teams/[id]/bookings/[bookingId]PATCHUpdate a booking
/api/crouton-bookings/teams/[id]/bookings/[bookingId]/resend-emailPOSTResend email for a booking

Chart Endpoints

PathMethodPurpose
/api/crouton-bookings/teams/[id]/charts/bookings-by-dateGETBooking counts by date
/api/crouton-bookings/teams/[id]/charts/bookings-by-groupGETBooking counts by group
/api/crouton-bookings/teams/[id]/charts/bookings-by-locationGETBooking counts by location
/api/crouton-bookings/teams/[id]/charts/bookings-by-slotGETBooking counts by slot
/api/crouton-bookings/teams/[id]/charts/bookings-by-statusGETBooking counts by status

Database Schemas

The package uses tables prefixed with bookings:

Schema FileTable NameKey Fields
booking.jsonbookingsBookingslocation (ref), date, slot (JSON array), quantity, group, status
location.jsonbookingsLocationstitle, color, street/zip/city, slots (repeater), openDays, slotSchedule, blockedDates, inventoryMode, quantity, maxBookingsPerMonth
settings.jsonbookingsSettingsstatuses (repeater), groups (repeater)
email-template.jsonbookingsEmailtemplatesname, subject, body (richtext), fromEmail, triggerType, recipientType, isActive, daysOffset, locationId
email-log.jsonbookingsEmaillogsbookingId (ref), templateId (ref), recipientEmail, triggerType, status, sentAt, error

Configuration

// nuxt.config.ts
export default defineNuxtConfig({
  extends: [
    '@fyit/crouton',
    '@fyit/crouton-bookings',
    './layers/bookings'        // Generated collections
  ],

  // Optional: Enable email notifications
  runtimeConfig: {
    croutonBookings: {
      email: { enabled: true }
    },
    public: {
      croutonBookings: {
        email: { enabled: true }
      }
    }
  }
})

Email Templates

When email is enabled (croutonBookings.email.enabled: true), manage templates at /dashboard/[team]/settings/email-templates.

Trigger Types

TriggerWhen Sent
booking_createdWhen a new booking is made
reminder_beforeBefore the booking date (use daysOffset, e.g. -1 for day before)
booking_cancelledWhen a booking is cancelled
follow_up_afterAfter the booking date (use daysOffset, e.g. 1 for day after)

Template Variables

VariableDescriptionExample
{{customer_name}}Customer's nameEmma van der Berg
{{customer_email}}Customer's emailemma.vanderberg@gmail.com
{{booking_date}}Formatted booking dateFriday, January 24, 2025
{{booking_slot}}Time slot(s)14:00 - 15:00
{{booking_reference}}Booking reference numberBK-2025-0124
{{location_name}}Location nameCourt A
{{location_title}}Location titleTennis Court A - Indoor
{{location_street}}Street addressSportlaan 42
{{location_city}}CityAmsterdam
{{location_address}}Full addressSportlaan 42, 1081 KL Amsterdam
{{location_content}}Location descriptionIndoor court with professional lighting...
{{team_name}}Team/business nameAmsterdam Tennis Club
{{team_email}}Team contact emailinfo@amsterdamtennis.nl
{{team_phone}}Team contact phone+31 20 987 6543

The template editor supports rich text editing via TipTap, variable autocomplete (type {{), live preview with demo data, translation tabs (EN, NL, FR), and per-location or global templates.

i18n

Translation keys are namespaced under bookings.*:

bookings.sidebar.*    - Sidebar UI
bookings.cart.*       - Cart UI
bookings.form.*       - Form labels
bookings.status.*     - Booking statuses
bookings.confirm.*    - Confirmation dialogs
bookings.buttons.*    - Action buttons
bookings.meta.*       - Metadata labels
bookings.common.*     - Common terms

Locale files are in i18n/locales/{en,nl,fr}.json and auto-merge when the layer is extended.

Use Cases

  • Sports facilities: Court reservations with per-slot capacity
  • Healthcare: Appointment scheduling with open day rules
  • Services: Salon bookings, consultations
  • Rentals: Equipment pools with inventory mode
  • Events: Workshops, classes with group bookings and monthly limits