Booking system for slot-based appointments and inventory-based reservations.
Status: Beta -- API may change between minor releases.
The Bookings package (@fyit/crouton-bookings) provides a complete booking system supporting two modes:
Features:
@fyit/crouton-maps)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:
@fyit/crouton-editor (installed automatically)@fyit/crouton-core, @nuxtjs/i18n, @internationalized/date, @vueuse/core, zod@fyit/crouton-maps (for map display in panel)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.
Quantity-based pool where users book units per day:
const location = {
title: 'Kayak Rental',
inventoryMode: true,
quantity: 20 // 20 kayaks available per day
}
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):
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()
All components use the prefix CroutonBookings (configured in nuxt.config.ts).
| Component | Purpose |
|---|---|
CroutonBookingsPanel | Main booking panel with tabs (book / my-bookings), filters, calendar, and list. Calendar functionality is integrated into this component. |
CroutonBookingsList | Scrollable bookings list with inline creation, date grouping, and highlighting |
CroutonBookingsBookingCard | Individual booking card showing date, slot, location, status, and email actions (includes inline activity timeline) |
CroutonBookingsBookingCreateCard | Inline booking creation form |
| Component | Purpose |
|---|---|
CroutonBookingsWeekStrip | Horizontal week date navigation strip |
CroutonBookingsDateBadge | Date display badge with formatting |
CroutonBookingsPanelFilters | Filter controls for status and location |
CroutonBookingsLocationCard | Location card with details |
CroutonBookingsLocationForm | Location editing form |
CroutonBookingsSlotIndicator | Visual slot capacity indicator |
CroutonBookingsOpenDaysPicker | Day-of-week picker for location scheduling |
CroutonBookingsScheduleGrid | Per-slot day-of-week schedule grid editor |
CroutonBookingsBlockedDateInput | Blocked date range input |
CroutonBookingsAvailabilityPreview | Preview of slot availability |
| Component | Purpose |
|---|---|
CroutonBookingsBlocksBookingBlockView | Booking block for content pages |
CroutonBookingsBlocksBookingBlockRender | Booking block renderer |
CroutonBookingsPanel| Prop | Type | Default | Description |
|---|---|---|---|
bookings | Booking[] | auto-fetched | Bookings data (external or auto-fetched via useBookingsList) |
locations | LocationData[] | auto-fetched | Locations data |
settings | SettingsData | null | auto-fetched | Settings data |
loading | boolean | false | Loading state (when providing external data) |
error | Error | null | - | Error state |
initialFilters | Partial<FilterState> | {} | Initial filter state |
title | string | - | Header title (empty string hides header) |
emptyMessage | string | - | Empty state message |
scope | 'personal' | 'team' | 'personal' | Booking scope |
CroutonBookingsBookingCard| Prop | Type | Default | Description |
|---|---|---|---|
booking | Booking | required | Booking data with relations |
highlighted | boolean | false | Visual highlight state |
justCreated | boolean | false | Temporary highlight for new bookings (fades) |
sendingEmailType | EmailTriggerType | null | null | Currently sending email type |
| Path | Method | Purpose |
|---|---|---|
/api/crouton-bookings/teams/[id]/availability | GET | Booked slots for a date range (query: locationId, startDate, endDate) |
/api/crouton-bookings/teams/[id]/customer-bookings | GET | Current user's bookings with email stats |
/api/crouton-bookings/teams/[id]/customer-bookings-batch | POST | Submit cart (batch checkout) |
/api/crouton-bookings/teams/[id]/customer-locations | GET | Locations accessible to current user |
/api/crouton-bookings/teams/[id]/admin-bookings | GET | All team bookings (team members) |
/api/crouton-bookings/teams/[id]/monthly-booking-count | GET | User's monthly booking count for a location |
/api/crouton-bookings/teams/[id]/bookings/[bookingId] | PATCH | Update a booking |
/api/crouton-bookings/teams/[id]/bookings/[bookingId]/resend-email | POST | Resend email for a booking |
| Path | Method | Purpose |
|---|---|---|
/api/crouton-bookings/teams/[id]/charts/bookings-by-date | GET | Booking counts by date |
/api/crouton-bookings/teams/[id]/charts/bookings-by-group | GET | Booking counts by group |
/api/crouton-bookings/teams/[id]/charts/bookings-by-location | GET | Booking counts by location |
/api/crouton-bookings/teams/[id]/charts/bookings-by-slot | GET | Booking counts by slot |
/api/crouton-bookings/teams/[id]/charts/bookings-by-status | GET | Booking counts by status |
The package uses tables prefixed with bookings:
| Schema File | Table Name | Key Fields |
|---|---|---|
booking.json | bookingsBookings | location (ref), date, slot (JSON array), quantity, group, status |
location.json | bookingsLocations | title, color, street/zip/city, slots (repeater), openDays, slotSchedule, blockedDates, inventoryMode, quantity, maxBookingsPerMonth |
settings.json | bookingsSettings | statuses (repeater), groups (repeater) |
email-template.json | bookingsEmailtemplates | name, subject, body (richtext), fromEmail, triggerType, recipientType, isActive, daysOffset, locationId |
email-log.json | bookingsEmaillogs | bookingId (ref), templateId (ref), recipientEmail, triggerType, status, sentAt, error |
// 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 }
}
}
}
})
When email is enabled (croutonBookings.email.enabled: true), manage templates at /dashboard/[team]/settings/email-templates.
| Trigger | When Sent |
|---|---|
booking_created | When a new booking is made |
reminder_before | Before the booking date (use daysOffset, e.g. -1 for day before) |
booking_cancelled | When a booking is cancelled |
follow_up_after | After the booking date (use daysOffset, e.g. 1 for day after) |
| Variable | Description | Example |
|---|---|---|
{{customer_name}} | Customer's name | Emma van der Berg |
{{customer_email}} | Customer's email | emma.vanderberg@gmail.com |
{{booking_date}} | Formatted booking date | Friday, January 24, 2025 |
{{booking_slot}} | Time slot(s) | 14:00 - 15:00 |
{{booking_reference}} | Booking reference number | BK-2025-0124 |
{{location_name}} | Location name | Court A |
{{location_title}} | Location title | Tennis Court A - Indoor |
{{location_street}} | Street address | Sportlaan 42 |
{{location_city}} | City | Amsterdam |
{{location_address}} | Full address | Sportlaan 42, 1081 KL Amsterdam |
{{location_content}} | Location description | Indoor court with professional lighting... |
{{team_name}} | Team/business name | Amsterdam Tennis Club |
{{team_email}} | Team contact email | info@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.
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.